Soklet Logo

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();

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.

SettingDefaultContract
withPort(Integer) / port(Integer)RequiredBuilder entrypoint plus replaceable dedicated TCP port 0..65535; 0 requests an ephemeral port
host(String)127.0.0.1Nonblank bind host
requestHeaderTimeout(Duration)60 secondsPositive 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 secondsPositive finite bound for reading the body after headers; not an idle-progress timeout; null restores the default
maximumRequestSizeInBytes(Integer)10 MiBPositive 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)100Positive maximum number of HTTP header fields; null restores the default
maximumHeadersSizeInBytes(Integer)64 KiBPositive maximum HTTP header-section size; null restores the default
maximumRequestTargetLengthInBytes(Integer)8,192 bytesPositive maximum request-target length; null restores the default
requestReadBufferSizeInBytes(Integer)64 KiBPositive transport read-buffer size; null restores the default
concurrentConnectionLimit(Integer)8,192Nonnegative TCP connection cap; zero disables it only when another deployment layer supplies an effective bound; null restores the default
endpointRegistry(McpEndpointRegistry)Classpath introspectionNonempty immutable endpoint registry; null restores discovery from the context classloader at build time
admissionController(McpAdmissionController)Accept allThread-safe authentication/authorization/admission controller for JSON-RPC requests and notifications; null restores anonymous admission
maximumCursorSizeInBytes(Integer)4096Positive UTF-8 bound on application cursors
requestTimeout(Duration)60 secondsPositive absolute request deadline
protocolExecutorConcurrency(Integer)32Positive protocol-worker count, independent of application handlers; subscription projections require at least two
protocolExecutorQueueCapacity(Integer)128Positive queue for protocol work; exhaustion sheds before semantic start
requestHandlerConcurrency(Integer)32Positive server-wide active application-handler slots
requestHandlerQueueCapacity(Integer)128Positive admitted wait queue
requestHandlerExecutorServiceSupplier(Supplier)Built-inFresh running executor per listener generation; Soklet owns shutdown
streamQueueCapacity(Integer)128Positive pending-message bound per response/subscription stream
connectionQueueCapacity(Integer)128Alias of streamQueueCapacity; the last call to either setter wins
writeTimeout(Duration)30 secondsPositive maximum interval without a stream write
keepAliveInterval(Duration)15 secondsPositive idle SSE comment interval; must be shorter than write timeout
maximumSubscriptionsPerPartition(Integer)32Positive concurrent subscription limit per endpoint and admitted authorization/quota partition, per server instance; not a distributed quota
maximumSubscriptionDuration(Duration)24 hoursPositive lifetime of a subscription
subscriptionCatalogProjectionTimeout(Duration)5 secondsQueue-inclusive deadline for visible tool/prompt catalog projection during subscription maintenance
subscriptionAuthorizationTimeout(Duration)5 secondsQueue-inclusive deadline for one authorization callback
maximumSubscriptionAuthorizationDuration(Duration)1 minuteUpper bound on an authorization lease; shorter application expiry or subscription lifetime wins
catalogAccessPolicy(McpCatalogAccessPolicy)Allow allCaller-aware tool/prompt catalog and direct-access policy
skillAccessPolicy(McpSkillAccessPolicy)Allow all admitted callersIndependent Skills access and discovery; exact lookup and file reads recheck access
skillVariantSelector(McpSkillVariantSelector)Required for a multivariant groupSelects one accessible, discoverable registration per explicit locale group without rewriting bytes
subscriptionAuthorizer(McpSubscriptionAuthorizer)Deny all; required for modern subscriptions or selected legacy RESOURCE_UPDATEDWhole modern subscription authorization before acknowledgment and during maintenance; independent legacy URI grant authorization and renewal
handlerInterceptor(McpHandlerInterceptor)Pass-throughOne concurrent-safe application-handler hook
toolResultSanitizer(McpToolResultSanitizer)Unchanged resultOne concurrent-safe hook covering complete tool payloads and result metadata; null restores the default
sessionConfig(McpSessionConfig)DisabledRequired 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)DisabledSupplies 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 disabledOnce-per-admitted-RPC-request charge across every method, including Completion and catalogs
toolRateLimiter(McpRateLimiter)NoneOptional server fallback; endpoint/tool overrides replace it
rateLimiterRegistry(McpRateLimiterRegistry)EmptyResolves named endpoint and tool overrides at build time
corsAuthorizer(CorsAuthorizer)Reject present OriginsShared Origin authorizer; omission also emits a startup diagnostic
absentOriginPolicy(McpAbsentOriginPolicy)ALLOWAllow an absent Origin or require one
allowedHosts(Set<String>)Empty setReplacement 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)DisabledAttaches server-wide catalog localization; null disables localization
unknownMirroredHeaderPolicy(McpUnknownMirroredHeaderPolicy)IGNOREIgnore or reject unknown Mcp-Param-* request headers
unknownMirroredHeaderNameDiagnostics(Boolean)falseBounded, sanitized name-only diagnostics; never values
protectionConfig(McpProtectionConfig)UnconfiguredRequired if any operation uses framework-protected request state
traceCorrelationKey(McpTraceCorrelationKey)DisabledEnables pseudonymous trace correlation with one server-owned key copy
logRawValidatedTraceIds(Boolean)falseSeparately 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();

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:

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:

GetterMeaning
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 >= writeTimeout is rejected at build time.
  • A successful listener generation produces one terminal shutdown-component disposition.
  • Port 0 is discoverable only by obtaining getDiagnostics() and reading getBoundAddress() 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.

Previous
OAuth Integration