Model Context Protocol
MCP Server Configuration
McpServer is a sealed, built-in dedicated transport configuration. Build it with McpServer.withPort(Integer), attach it with SokletConfig.withMcpServer(McpServer), and let that one-shot Soklet lifecycle own its startup and shutdown.
Stability boundary
This guide covers Soklet 4.0.0. MCP Tasks and the 2025 session/HTTP transport package retain provisional protocol/API maturity labels. See the Public API Map for the Java types and Compatibility and Scope for supported protocol behavior.
Local development configuration
With the annotated endpoint compiled and its generated metadata on the classpath, this configuration creates the anonymous loopback server used by the quickstart and simulator examples:
McpServer.Builder mcpServerBuilder = McpServer.withPort(8082);
McpServer mcpServer = mcpServerBuilder.build();
McpServer.Builder mcpServerBuilder = McpServer.withPort(8082);
McpServer mcpServer = mcpServerBuilder.build();
The listener binds to 127.0.0.1:8082, discovers the generated endpoints, and accepts requests without credentials. Its automatic loopback Host aliases include 127.0.0.1:8082, matching the quickstart's curl commands and synthetic requests. No application rate quota is configured in this example.
Focused guides add their feature settings to a fresh mcpServerBuilder before build(). Use this local configuration when following their loopback examples, then attach the completed server to Soklet. For remote deployment, configure application admission, public hosts, and shared quotas as shown in the production example.
Complete builder reference
All scalar parameters use boxed Java types in the public API.
requestHeaderTimeout restarts after a response completes, so the next request shares its remaining idle/header budget. Its 60-second default is not above common nginx/ALB idle defaults or Caddy's two-minute upstream keep-alive default. Use a backend budget comfortably above the proxy's upstream idle window with latency/jitter margin, or reduce proxy reuse. This setting is separate from the semantic requestTimeout and SSE stream keep-alives. See production timeout coordination.
| Setting | Default | Contract |
|---|---|---|
withPort(Integer) / port(Integer) | Required | Builder entrypoint plus replaceable dedicated TCP port 0..65535; 0 requests an ephemeral port |
host(String) | 127.0.0.1 | Nonblank bind host |
requestHeaderTimeout(Duration) | 60 seconds | Positive finite request-line/header and keep-alive idle bound on reusable connections; header bytes do not reset it. Coordinate with the proxy's upstream idle timeout; null restores the default |
requestBodyTimeout(Duration) | 60 seconds | Positive finite bound for reading the body after headers; not an idle-progress timeout; null restores the default |
maximumRequestSizeInBytes(Integer) | 10 MiB | Positive MCP request-body byte limit, at most 16 MiB; independent of the 1,048,576-character limit for any JSON string or token; null restores the default |
maximumHeaderCount(Integer) | 100 | Positive maximum number of HTTP header fields; null restores the default |
maximumHeadersSizeInBytes(Integer) | 64 KiB | Positive maximum HTTP header-section size; null restores the default |
maximumRequestTargetLengthInBytes(Integer) | 8,192 bytes | Positive maximum request-target length; null restores the default |
requestReadBufferSizeInBytes(Integer) | 64 KiB | Positive transport read-buffer size; null restores the default |
concurrentConnectionLimit(Integer) | 8,192 | Nonnegative TCP connection cap; zero disables it only when another deployment layer supplies an effective bound; null restores the default |
endpointRegistry(McpEndpointRegistry) | Classpath introspection | Nonempty immutable endpoint registry; null restores discovery from the context classloader at build time |
admissionController(McpAdmissionController) | Accept all | Thread-safe authentication/authorization/admission controller for JSON-RPC requests and notifications; null restores anonymous admission |
maximumCursorSizeInBytes(Integer) | 4096 | Positive UTF-8 bound on application cursors |
requestTimeout(Duration) | 60 seconds | Positive absolute request deadline |
protocolExecutorConcurrency(Integer) | 32 | Positive protocol-worker count, independent of application handlers; subscription projections require at least two |
protocolExecutorQueueCapacity(Integer) | 128 | Positive queue for protocol work; exhaustion sheds before semantic start |
requestHandlerConcurrency(Integer) | 32 | Positive server-wide active application-handler slots |
requestHandlerQueueCapacity(Integer) | 128 | Positive admitted wait queue |
requestHandlerExecutorServiceSupplier(Supplier) | Built-in | Fresh running executor per listener generation; Soklet owns shutdown |
streamQueueCapacity(Integer) | 128 | Positive pending-message bound per response/subscription stream |
connectionQueueCapacity(Integer) | 128 | Alias of streamQueueCapacity; the last call to either setter wins |
writeTimeout(Duration) | 30 seconds | Positive maximum interval without a stream write |
keepAliveInterval(Duration) | 15 seconds | Positive idle SSE comment interval; must be shorter than write timeout |
maximumSubscriptionsPerPartition(Integer) | 32 | Positive concurrent subscription limit per endpoint and admitted authorization/quota partition, per server instance; not a distributed quota |
maximumSubscriptionDuration(Duration) | 24 hours | Positive lifetime of a subscription |
subscriptionCatalogProjectionTimeout(Duration) | 5 seconds | Queue-inclusive deadline for visible tool/prompt catalog projection during subscription maintenance |
subscriptionAuthorizationTimeout(Duration) | 5 seconds | Queue-inclusive deadline for one authorization callback |
maximumSubscriptionAuthorizationDuration(Duration) | 1 minute | Upper bound on an authorization lease; shorter application expiry or subscription lifetime wins |
catalogAccessPolicy(McpCatalogAccessPolicy) | Allow all | Caller-aware tool/prompt catalog and direct-access policy |
skillAccessPolicy(McpSkillAccessPolicy) | Allow all admitted callers | Independent Skills access and discovery; exact lookup and file reads recheck access |
skillVariantSelector(McpSkillVariantSelector) | Required for a multivariant group | Selects one accessible, discoverable registration per explicit locale group without rewriting bytes |
subscriptionAuthorizer(McpSubscriptionAuthorizer) | Deny all; required for modern subscriptions or selected legacy RESOURCE_UPDATED | Whole modern subscription authorization before acknowledgment and during maintenance; independent legacy URI grant authorization and renewal |
handlerInterceptor(McpHandlerInterceptor) | Pass-through | One concurrent-safe application-handler hook |
toolResultSanitizer(McpToolResultSanitizer) | Unchanged result | One concurrent-safe hook covering complete tool payloads and result metadata; null restores the default |
sessionConfig(McpSessionConfig) | Disabled | Required stable owner resolver and bounded configuration for endpoint sessionProtocolVersions explicitly enabling June/November 2025; null clears configuration. Missing or unused configuration fails construction. See 2025 session configuration. |
taskManager(McpTaskManager) | Disabled | Supplies one thread-safe application-owned manager. Tasks are enabled and advertised only on endpoints explicitly including V2026_07_28 in taskProtocolVersions; sibling endpoints do not inherit Tasks. An endpoint declaring Tasks requires a manager at construction. See Tasks capability negotiation. |
requestRateLimiter(McpRateLimiter) | Required if any completer exists; otherwise disabled | Once-per-admitted-RPC-request charge across every method, including Completion and catalogs |
toolRateLimiter(McpRateLimiter) | None | Optional server fallback; endpoint/tool overrides replace it |
rateLimiterRegistry(McpRateLimiterRegistry) | Empty | Resolves named endpoint and tool overrides at build time |
corsAuthorizer(CorsAuthorizer) | Reject present Origins | Shared Origin authorizer; omission also emits a startup diagnostic |
absentOriginPolicy(McpAbsentOriginPolicy) | ALLOW | Allow an absent Origin or require one |
allowedHosts(Set<String>) | Empty set | Replacement set of explicit hostname/IP literals; these accept a valid public port or no port. Automatic loopback aliases still require the listener port. |
localizer(McpLocalizer) | Disabled | Attaches server-wide catalog localization; null disables localization |
unknownMirroredHeaderPolicy(McpUnknownMirroredHeaderPolicy) | IGNORE | Ignore or reject unknown Mcp-Param-* request headers |
unknownMirroredHeaderNameDiagnostics(Boolean) | false | Bounded, sanitized name-only diagnostics; never values |
protectionConfig(McpProtectionConfig) | Unconfigured | Required if any operation uses framework-protected request state |
traceCorrelationKey(McpTraceCorrelationKey) | Disabled | Enables pseudonymous trace correlation with one server-owned key copy |
logRawValidatedTraceIds(Boolean) | false | Separately enables raw validated trace IDs in bounded MCP_TRACE_CORRELATION records |
build() verifies that all named limiters exist. Every endpoint enabling Tasks and every statically task-required tool requires a configured manager. Completion requires a request limiter; framework-protected request state requires protection configuration. Missing required collaborators fail with IllegalStateException during construction.
The combined body, header-section and request-target byte limits, plus 1,024 bytes of HTTP framing allowance, must fit within Integer.MAX_VALUE. An oversized combined limit, an invalid allowed host or a non-loopback bind without an explicit allowlist fails with IllegalArgumentException at construction. Individual setters can accept positive values whose combined limit is too large. Supplying a large executor cannot bypass the dispatcher slot or queue bounds.
The existing partition subscription limit, maximum subscription duration, authorization timeout/duration, stream queue, write timeout, and keepalive settings also bound explicitly enabled 2025 GET leases. One session shares a fixed partition registration across GETs and URI grants, with at most two logical GETs; replaced physical bodies retain evidence until cleanup. The session HTTP controller governs actual GET/DELETE admission without RPC request limiting. Bounded renewal scheduling retains capacity through non-cooperative callback exit, and temporary capacity fences delivery until current authorization succeeds within the original deadlines. Legacy grants are limited to 64 per session, 64 per owner, and 512 per server, with 65,536 retained URI bytes per session. Encoded queued GET notifications, including SSE framing, have independent 2 MiB owner and 16 MiB global caps. Pressure coalesces hints before shedding the largest pending stream in the affected budget; bounded dirty bits survive for a later authorized GET. GET/grant maintenance shares four physical jobs and 64 sliding-second dispatches, with an independent aggregate demand reservation for short leases. Due GET and URI renewals are ordered by the shortest remaining authorization lease, including after reconciliation. This priority does not interrupt a running callback or extend a deadline. A renewal backlog can exhaust a lease and retire the session. Grant total lifetime starts at original creation and is not reset by duplicate subscribe. Historical request evidence and physical callbacks remain charged until actual exit; arbitrary application object graphs remain application responsibility.
Custom IdGenerator configuration belongs to ordinary HTTP/SSE transport builders, and custom MultipartParser configuration belongs to ordinary HTTP. McpServer.Builder exposes neither setter. The underlying HTTP Request ID, the client's JSON-RPC request ID, and pseudonymous trace-correlation tokens are separate identities. traceCorrelationKey controls only the telemetry token; it is not a request-ID generator or a replacement for JSON-RPC IDs.
Raw trace logging is sensitive
logRawValidatedTraceIds(true) adds the validated lowercase trace ID to the bounded LogEventType.MCP_TRACE_CORRELATION record, or permits a trace-ID-only record when no correlation key is configured. It does not enable pseudonymous correlation by itself and never affects metric dimensions. Keep the default unless an explicit high-cardinality log retention and access policy is in place.
Listener binding
The MCP listener binds to 127.0.0.1 by default. This changed from 0.0.0.0 in Soklet 3.5.1; default HTTP and SSE listeners still bind to 0.0.0.0. An MCP listener using its default is reachable only through loopback. For a container or remote deployment, select the bind address explicitly with host(String) and supply a nonempty allowedHosts(Set) set for any non-loopback bind.
Allowed values are hostnames or IP literals, without a scheme, path or port. They describe the public authorities accepted by Host validation, independently of the listener's bind address. The example below permits a proxy to forward mcp.example.com:443 to the listener on 8082; automatic loopback aliases still require the effective listener port.
Example production-shaped configuration
McpServer.Builder mcpServerBuilder = McpServer.withPort(8082)
.endpointRegistry(registry)
.admissionController(admissionController)
.host("0.0.0.0")
.allowedHosts(Set.of("mcp.example.com"))
.requestRateLimiter(distributedRequestLimiter)
.toolRateLimiter(distributedToolLimiter)
.taskManager(distributedTaskManager)
.rateLimiterRegistry(namedLimiters)
.corsAuthorizer(CorsAuthorizer.fromWhitelistedOrigins(
Set.of("https://app.example.com")
))
.absentOriginPolicy(McpAbsentOriginPolicy.ALLOW)
.requestHandlerConcurrency(64)
.requestHandlerQueueCapacity(256)
.requestTimeout(Duration.ofSeconds(45))
.protectionConfig(protectionConfig);
McpServer server = mcpServerBuilder.build();
McpServer.Builder mcpServerBuilder = McpServer.withPort(8082)
.endpointRegistry(registry)
.admissionController(admissionController)
.host("0.0.0.0")
.allowedHosts(Set.of("mcp.example.com"))
.requestRateLimiter(distributedRequestLimiter)
.toolRateLimiter(distributedToolLimiter)
.taskManager(distributedTaskManager)
.rateLimiterRegistry(namedLimiters)
.corsAuthorizer(CorsAuthorizer.fromWhitelistedOrigins(
Set.of("https://app.example.com")
))
.absentOriginPolicy(McpAbsentOriginPolicy.ALLOW)
.requestHandlerConcurrency(64)
.requestHandlerQueueCapacity(256)
.requestTimeout(Duration.ofSeconds(45))
.protectionConfig(protectionConfig);
McpServer server = mcpServerBuilder.build();
Tool rate limiting is optional. A call with no tool, endpoint, or server limiter has no tool rate quota; the configured transport, queue, concurrency, and execution bounds still apply. Configure an explicit token bucket or an application-owned distributed limiter when your workload needs a quota. The production example uses application-owned collaborators and the public Host mcp.example.com; it does not accept the quickstart's 127.0.0.1:8082 requests.
Remote binding does not add TLS. Terminate TLS in a trusted deployment layer and ensure it preserves the Host/authority semantics expected by the listener.
For this example, Host: mcp.example.com, Host: mcp.example.com:443, and a valid public alternate port are accepted even though Soklet listens on 8082. Configure the edge to forward the public authority; Soklet does not infer an allowed hostname from Forwarded or X-Forwarded-Host. A well-formed but unauthorized authority receives HTTP 421; malformed or duplicate Host fields receive 400. Origin policy and application admission are separate checks. These statuses describe the real listener. The MCP simulator enforces the body limit and semantic Host authorization, but does not reproduce parser-owned header-count, header-byte, request-target-length, or Host syntax/cardinality validation. Test those cases against a loopback listener; the simulator can return 421 where the listener rejects malformed Host with 400.
Lifecycle
Soklet.run(config) returns the Soklet instance only after every configured lifecycle component reaches readiness. By default every run owns a JVM shutdown hook; application code owns any console or other input handling. requestShutdown() requests bounded shutdown; awaitShutdown() or awaitShutdownAsync() observes full finalization and the immutable ShutdownResult. McpServer itself exposes no independent public start(), stop(), or close() lifecycle.
A stopped Soklet cannot restart. A new lifecycle requires a new configuration with fresh transport instances; reusing an already claimed transport fails with TransportOwnershipException. Semantic metric ordering is scoped to one lifecycle, with no shared event queue across separately constructed lifecycles. MCP terminal metrics use the published component disposition, including NOT_STARTED. Metric delivery may follow result publication.
Admission, request/tool rate limiters, CORS, start observation and request-state opening share the protocol pool. Configure protocolExecutorConcurrency and protocolExecutorQueueCapacity separately from handler concurrency/executors; they default to 32 workers and 128 queued jobs. Null restores defaults and both values must be positive; catalog/task subscription projections require at least two workers, validated at build time. The queue allocates lazily. Use finite downstream deadlines for shared-pool callbacks. Subscription projections have no reserved worker; sustained protocol or policy saturation can delay hints.
Direct catalog and initial subscription work holds its protocol worker while it waits for an application-handler slot. This covers built-in paged catalog lists (tools/list, prompts/list, resources/list and resources/templates/list) on 2025-06-18 and 2025-11-25, and the catalogAccessPolicy step of tools/call, prompts/get, tool/prompt listing and prompt-argument completion on every revision. The same bound also covers initial 2025 session-GET admission, initial 2025 resources/subscribe authorization, and modern initial subscription authorization and catalog baselines. All of these stages share one allowance across the server's endpoints and protocol revisions.
At most a quarter of protocolExecutorConcurrency, rounded down, concurrent direct invocations receive permission to use the application-handler queue: eight at the default of 32, and none when the pool is smaller than four. Permission is acquired before handler admission and retained until the bounded invocation returns, including its time running. This limits queue-eligible invocations; it does not reserve queue entries, and active permission holders reduce how many others may wait. Queueing still requires capacity under requestHandlerQueueCapacity, preserves FIFO position and uses the original operation deadline. Without permission, such a request needs a free handler slot and an empty queue; otherwise Soklet returns the existing capacity response before that bounded callback enters. Initial legacy GET and resource authorization return bodyless HTTP 503; modern subscription and catalog operations use HTTP 503 with JSON-RPC -32603.
For protocol pools of at least four workers, scheduled subscription projections can hold up to another half of the pool. These two queueing categories alone cannot occupy every protocol worker. The remaining capacity is not reserved for ping, initialization, cancellation or discovery: running application callbacks, other admission hooks and start observers still consume protocol workers. Keep those callbacks prompt and bounded. Raise protocolExecutorConcurrency to increase the number of direct invocations eligible to queue. Ordinary operation handlers, including custom resource-list handlers, scheduled subscription maintenance and renewals retain their existing queue behavior. Session DELETE keeps its independent owner-policy dispatcher.
Finish observation and associated trace/failure logs use a separate bounded worker dispatcher with capacity reserved before semantic start. They do not block connection event loops, but a hung observer still retains physical work through shutdown. Bounded daemon platform workers run finish callbacks. Observation saturation omits a complete pair with a fixed rate-limited diagnostic; it does not reject protocol or cancellation traffic. The dispatcher has requestHandlerConcurrency workers and reserves up to handler concurrency + handler queue capacity + protocol concurrency transient observations (192 by default), subject to integer-capacity bounds. Modern subscription lifetimes use a separate reservation limit based on the connection cap, with 8,192 retained when that TCP cap is disabled. Accepted finishes remain queued across force. Each request's start precedes its finish; cross-client delivery order is unspecified.
A supplied handler-executor supplier is invoked once per listener generation and must return a fresh running executor. Soklet owns each returned executor and shuts it down.
The executor must execute accepted work or throw on rejection; silent discard policies are unsupported. Soklet prevents inline application execution on the protocol submitting thread. Initial executor rejection returns the fixed capacity response, HTTP 503 with JSON-RPC -32603, before an operation handler or bounded request-policy callback enters. An application callback that itself throws RejectedExecutionException keeps ordinary application-failure handling.
An exiting worker drains accepted queued tickets directly before returning to the executor, without resubmission. This also preserves admitted work during graceful drain when the executor has stopped accepting new submissions. A fresh handoff can still reject during a direct-handoff pool's worker-return gap, even when that pool has as many threads as Soklet's handler concurrency. If no worker remains, all queued tickets fail immediately with the fixed capacity 503. Use a queueing executor or size a direct-handoff pool above the handler concurrency; extra threads reduce that return-gap risk without increasing Soklet's handler bound. Forced cancelation and the original request deadlines still apply. This retains the configured handler and queue bounds and creates no retry jobs or replacement workers. Interrupt status is cleared between tickets, and each ticket's own requested interruption is applied at entry. Executor task boundaries may cover multiple invocations, so executor decorators must not assume a fresh task for every request. Application-owned thread-local cleanup remains the application's responsibility.
Queued requests keep the absolute deadline captured before admission; waiting never renews it. If the deadline owns while a request is still queued and the outer request control remains writable, Soklet atomically removes the request, never invokes its interceptor or handler, and returns the fixed queue-overload response: HTTP 503 with JSON-RPC -32603 and request outcome DEADLINE_EXCEEDED. If disconnect owns, Soklet writes nothing and reports one CLIENT_DISCONNECTED outcome. Promotion first ends the queued classification. A later pre-commit deadline owned by the application-handler path returns HTTP 200 with JSON-RPC -32603 on the modern revision; a deadline that expires while Soklet still owns framework protocol work returns a bodyless HTTP 504. Both record DEADLINE_EXCEEDED. Promotion, queued deadline, and disconnect share one queue-depth removal. This remains true when the application layer reserves a queued-deadline response and a disconnect makes the outer request control unwritable before handoff: the reserved response is abandoned with zero bytes and cannot create a second request outcome.
During shutdown, Soklet fences new work, closes indefinite subscriptions promptly, preserves already-admitted finite unary responses and request-progress streams while the queue and active work drain within the configured graceful-shutdown budget, and then force-cancels what remains within the forced-shutdown budget. Code that ignores cancelation or interruption may remain active after the listener closes. Durable tasks and application worker backlog are not request work: Soklet neither marks them failed nor cancels them when this listener stops. The MCP shutdown component's ShutdownComponentResult preserves the corresponding disposition, failures, and optional residual-activity evidence, while MCP diagnostics report RESIDUAL_ACTIVITY or TERMINATION_UNKNOWN as appropriate and the aggregate ShutdownResult is INCOMPLETE. See Failures, Retries, And Durable Work for the client-visible retry and abrupt-node-loss boundary.
Residual-activity evidence covers application request-pipeline callbacks—admission, rate limiting, protection—not only registered operation handlers. A later residual exit does not rewrite the immutable result from that lifecycle attempt.
Subscriptions still being admitted have a finite shutdown response. A validated modern subscriptions/listen returns HTTP 503 with a JSON-RPC error carrying its request ID and Connection: close. A session-enabled 2025 GET that has not opened returns a bodyless HTTP 503 with Connection: close, including when it was queued or paused in CORS before shutdown. Established modern subscriptions receive their terminal result; established 2025 GET streams finish with the closing HTTP chunk. Ordinary finite requests retain their response path during graceful drain.
The default shared graceful-drain budget is 15 seconds, independently of the 60-second MCP request deadline. Choose the drain budget for deployment policy: allow maximum remaining work and response transmission when finite requests must complete, or intentionally accept interruption sooner. Include the separate forced-shutdown budget and operational margin in platform shutdown settings. Indefinite subscriptions close promptly instead of extending that budget. See Graceful Shutdown.
Server getters and controls
The built server exposes its immutable endpoint registry through getEndpointRegistry(), admission controller through getAdmissionController(), and its remaining immutable configuration through getHandlerInterceptor(), getToolResultSanitizer(), getTaskManager(), getRequestRateLimiter(), getToolRateLimiter(), getRateLimiterRegistry(), getCorsAuthorizer(), and getMaximumCursorSizeInBytes(). Runtime status is part of getDiagnostics(). The server also exposes three control or snapshot surfaces:
getProtectionKeyringManager()for live production request-state key rotation;getTraceCorrelationKeyManager()for the active trace-correlation key control; andgetDiagnostics()for a point-in-time operational snapshot.
Controls never expose configured key material. See Input and Request State for protection rotation and Observability and Testing for trace behavior.
Diagnostics snapshot
McpServerDiagnostics is immutable. Retain it safely, but obtain a new snapshot to observe later state. It exposes exactly these values:
| Getter | Meaning |
|---|---|
getStatus() | NOT_STARTED, STARTING, RUNNING, SHUTTING_DOWN, TERMINATED, RESIDUAL_ACTIVITY, or TERMINATION_UNKNOWN |
getBoundAddress() | Effective address while the current listener generation is running, including an OS-assigned port |
getRequestHandlerConcurrency() | Configured active-handler capacity |
getRequestHandlerQueueCapacity() | Configured admitted queue capacity |
getActiveHandlerExecutions() | Current slot occupants, including residual executions |
getRequestHandlerQueueDepth() | Current admitted requests waiting for a slot |
getActiveRequestStreams() | Current request-scoped SSE streams |
getActiveSubscriptions() | Open resource, localization, or task subscriptions, never greater than request streams |
getProtectionMode() | Effective framework state-protection mode |
isApplicationRequestStateProtectorConfigured() | Whether the custom-protector mode is selected |
getProtectionKeyringFingerprint() | Optional secret-free live production-keyring fingerprint |
getTraceCorrelationFingerprint() | Optional secret-free active trace-configuration fingerprint |
Lifecycle/capacity/stream fields are captured at one runtime linearization point. Protection and trace fields are captured together at a separate security-control point. Do not infer an atomic invariant across those independently mutable domains.
Diagnostics are intentionally aggregate: no client identity, operation name, raw URI, request ID, trace ID, key material, or Throwable appears.
Operational invariants
Keep-alive comments and successful local writes do not prove that a remote client remains reachable. A silently lost subscription stream can retain its quota until transport failure is detected, its lease expires, or cleanup completes; the default partition limit is 32. Test this failure mode and reconnect overlap when choosing subscription duration and capacity. See dead peers and stream capacity.
- Endpoint catalogs are immutable after build.
- Handler slots and the admitted queue are server-wide across endpoint paths.
- Stream queues are per stream.
- The task manager, task publisher, durable state, and application workers remain application-owned; Soklet never closes or runs them.
- A progress or keep-alive write does not extend the request deadline.
keepAliveInterval>=writeTimeoutis rejected at build time.- A successful listener generation produces one terminal shutdown-component disposition.
- Port
0is discoverable only by obtaininggetDiagnostics()and readinggetBoundAddress()while running.
Localization
McpServer.Builder.localizer(...) attaches a localization seam, and McpServer.getLocalizationCatalogInvalidator() exposes the local reload control. Omitting the localizer preserves existing wire bytes exactly. Calling invalidateCatalogs() publishes coarse list-change invalidations to that one server's eligible streams after you atomically install a new catalog snapshot; it distributes nothing and is a no-op before start and after stop. See Localization.

