Soklet Logo

Model Context Protocol

MCP Progress and Subscriptions

Progress belongs to one application invocation and is available on explicitly selected 2025-06-18, 2025-11-25, and 2026-07-28 operations. The 2026-07-28 transport also provides subscriptions/listen, a framework-owned long-lived stream of coarse resource events, durable task snapshots, and localization catalog invalidations. Explicitly enabled 2025 sessions add remembered public client metadata and active-request cancelation. Leased GET, verified DELETE, session-owned URI grants, and resource/catalog invalidations are available with explicit HTTP admission and subscription configuration. Delivery and recovery behavior in particular hosts remains unverified.

Invocation features

Every selected application tool, prompt, resource-read, and custom resource-list handler receives McpInvocationFeatures. Retrieve features by exact type:

CancelationToken cancelation = invocationFeatures.getCancelationToken();

invocationFeatures.getProgressReporter().ifPresent(reporter ->
  reporter.report(McpProgressUpdate.withProgress(50.0)
    .total(100.0)
    .message("Halfway")
    .build())
);

cancelation.throwIfCanceled();

The direct accessors expose Soklet's built-in features. The generic find(Class) and require(Class) methods remain available for custom feature types: find returns an Optional, while require throws if the feature is absent. Annotated handlers can inject CancelationToken, an Optional containing McpProgressReporter, and McpInvocationFeatures directly. Direct injection and lookup return the same request-scoped instances. A bare non-optional reporter parameter is invalid because progress is legitimately unavailable on some invocations.

Cooperative cancelation

The CancelationToken is always present after an application handler is selected. Soklet signals it for absolute deadlines, forced shutdown after the graceful-drain budget, or stream timeout, internal failure, and backpressure. Disconnects cancel modern requests and finite/uncommitted or queued legacy requests. A committed legacy POST SSE disconnect or lost-writer write failure instead detaches delivery as described below. Beginning graceful drain does not cancel an already-admitted finite request; its unary or request-scoped progress response may still finish within that budget.

Cancelation is cooperative. Check isCanceled() or call throwIfCanceled() between expensive or blocking steps. onCancel(Runnable) registers a callback and returns an unchecked-close CallbackRegistration. Closing that registration before the callback is claimed removes it; duplicate registrations are independent. MCP application and bounded-policy callbacks run on a bounded callback executor after the handler is signaled and the terminal response is offered or transport ownership is detached. They do not block the network event loop or deadline timer. Their physical handler capacity remains charged until both handler work and callback delivery exit, including during shutdown. Registration after delivery has begun can invoke inline on the registering application thread. Keep callbacks prompt. Normal completion discards cancelation callbacks rather than invoking them.

A signaled token does not forcibly stop Java code. A non-cooperative handler retains its concurrency slot until it actually exits and can become residual shutdown work. Normal completion does not mark its token canceled.

The built-in MCP listener treats input EOF as response-channel loss. This includes a normal TCP FIN and a deliberate client shutdownOutput(): clients must keep their sending side open while awaiting the response. Before commitment, EOF cancels the request. This also applies to fire-and-forget 2025 notifications and session DELETE: keep the sending side open until the 202 or 204 acknowledgment, or Soklet can discard the pending operation and record a rejection. A committed modern response cancels work; a committed 2025 POST SSE response detaches its writer under the existing legacy rules. TCP cannot distinguish full close from input half-close, so this is an MCP transport policy. Ordinary HTTP preserves half-close and pipelining; see Streaming Responses for idle-feed limits.

Cancelation is not transaction rollback and does not prove that application side effects did not occur. A retry after disconnect, deadline, or forced shutdown is a new invocation and must follow the application's idempotency policy. See Failures, Retries, And Durable Work.

An identifiable HTTP notifications/cancelled notification is validated, admitted, and request-limited, then returns an empty 202 on the modern and session-disabled 2025 paths. Those paths ignore its payload. On an explicitly session-enabled 2025 view, verified session binding permits the active-request effect described below; transport deadlines and termination signals also remain authoritative.

Notification envelope boundary

That empty 202 is an HTTP transport acknowledgment with an empty body, not a JSON-RPC response. Other well-formed notifications can receive a different HTTP status after protocol, admission, or rate-limit processing, but their classified notification path likewise emits no JSON-RPC response body. A nested cancelation requestId remains notification data; it is not a top-level JSON-RPC ID.

Server-sent progress, modern subscription-acknowledgment, resource-change, and task-state notifications contain jsonrpc and method but no top-level id; notifications with data also contain params. Legacy catalog invalidations carry no resource selection or context data, while resource updates contain only the URI. Nested correlation values are valid and preserved: progress carries progressToken, while subscription frames carry io.modelcontextprotocol/subscriptionId. The terminal result is a response rather than a notification, so it retains the initiating request's correlated top-level id.

For example, a client cancelation notification carries the target request ID inside params:

{"jsonrpc":"2.0","method":"notifications/cancelled","params":{"requestId":"job-1"}}

A progress notification instead carries the progress token and current value:

{"jsonrpc":"2.0","method":"notifications/progress","params":{"progressToken":"job-1","progress":1}}

Neither notification has a top-level id. Soklet also implements the Tasks extension's notifications/tasks projection but exposes no arbitrary extension-notification handler API.

Progress availability and ordering

An McpProgressReporter exists only when:

  • the initiating request supplied a valid string or integer at params._meta.progressToken; and
  • Soklet can safely commit request-scoped SSE.

Soklet preserves the opaque token's string or integer identity. Application code never copies or manages it.

McpProgressUpdate requires a finite Double progress value and accepts optional finite total and message values. Progress is monotonic:

  • a value greater than the last accepted value is emitted;
  • an equal value is coalesced; and
  • a lower value throws IllegalArgumentException while the invocation is active.

After cancelation, terminal completion, or writer detachment, reports have no effect. Reporting is synchronous through the bounded response queue, so it may briefly block under backpressure. Interrupt status is preserved if that wait is interrupted. Progress and keep-alive writes do not extend the absolute request deadline.

On 2026-07-28, when an operation has an unmet CONDITIONAL input capability, Soklet must keep the response uncommitted until the handler chooses a complete or input_required result. The reporter is therefore absent even if the request has a token; suppressed progress is not replayed later.

A deterministic test drives that hold through a real two-leg loopback socket proxy. The proxy forwards the request, observes zero backend and client-visible response bytes through its exact manually advanced idle boundary, then resets both legs. Soklet records one client-disconnect outcome, signals cooperative cancelation once, suppresses the retained handler's late result, and releases framework-owned request and stream state. A capability-present control through the same proxy forwards the SSE head, progress notification, and terminal result byte-for-byte. This proves behavior at one configured intermediary boundary; it does not promise a universal proxy timeout, turn the proxy's manual clock into production wall time, or guarantee prompt exit from non-cooperative application code.

Session-bound client cancelation

On an explicitly session-enabled 2025 endpoint, the notification traverses fresh admission, applicable request/control limiting, and owner/path/revision/generation verification. Small, valid cancellation uses the reserved control quota after owner/session verification; malformed or oversized cancellation keeps ordinary limiting. A usable requestId can target logically active client work only if cancelation wins its atomic terminal reservation. Unknown, completed, initialization, or otherwise uncancellable targets are ignored. Optional malformed reason text does not invalidate a usable target; free-form reasons never enter built-in logs or metric labels. A signal that overtakes target registration may be ignored.

Winning cancelation signals StreamTerminationReason.CLIENT_CANCELED, fences late progress/results, and completes the target as zero-event 200 SSE if it was finite, or cleanly ends its committed SSE without a result. A losing cancelation preserves the reserved terminal response. This is cooperative: Java code and side effects are not rolled back, and physical worker/evidence reservations remain until that work exits.

Active duplicate request IDs return JSON-RPC -32600; integer 1 and string "1" are distinct. Clients must obey the no-reuse rule for the whole session; Soklet retains no lifetime ID history. Hard session expiry can signal active work with token reason RESPONSE_TIMEOUT and MCP stream reason SESSION_EXPIRED, attempting a neutral correlated -32603 on a usable POST unless a terminal response already won. Session IDs and owner keys are not metric labels or credentials. Verified DELETE uses token CLIENT_CANCELED and MCP SESSION_CLOSED, preserving an earlier terminal winner and physical work ownership. Automatic host recovery after expiry, reconnect, and GET/DELETE failure remains unverified.

Legacy GET leases and DELETE

Explicit HTTP transport admission authorizes actual GET/DELETE requests with a fresh identity, explicit expiry, and selected offered families. These paths have no JSON-RPC method/context and no RPC request-limiter or request metrics. GET requires selected legacy subscriptions and effective sources; it opens persistent leased SSE with keepalives. DELETE requires no families and retires a verified session with empty 204. GET carries current authorized resource/catalog invalidations; its admission alone grants no URI. Neither POST progress nor a lost POST result is moved to GET.

Renewal uses the original GET credentials and current owner/identity/partition authorization, clipped by application expiry, configured authorization duration, fixed total GET lifetime, and session deadlines. Reconciliation fences prior authorization before bounded reevaluation. At most two logical GETs share a fixed session partition registration; replacement retains old request evidence until physical cleanup, and capacity failure preserves existing streams. GET lifetimes use subscription metrics; host-specific behavior and recovery remain unverified.

GET streams emit no empty priming event, id:/retry: fields, event history, or Last-Event-ID recovery. The bounded input header is ignored. Soklet omits November's recommended priming event, as it does for persistent POST streams. The effect on particular clients remains unverified; see the persistent POST path below.

The existing simulator supports GET/DELETE through the same session and lease path. DELETE ends a simulated GET with SESSION_CLOSED and no JSON-RPC message; disconnect uses CLIENT_DISCONNECTED and leaves ordinary POST usable. Real-socket tests supplement simulation for physical writes and cleanup.

Legacy URI grants and invalidations

On explicitly subscription-enabled 2025 session views, resources/subscribe and resources/unsubscribe are real POST requests through fresh admission and session verification. Subscribe uses ordinary request limiting; small, valid unsubscribe uses the reserved control quota. Subscribe exposes one validated URI on McpAdmissionContext and checks a readable route for that revision; unsubscribe exposes its target through getOperationName() with an empty selection. Both successful requests return their matching JSON-RPC ID and result: {}. Missing and denied subscribe targets share a neutral error. A route or GET permission does not grant URI notification permission.

The existing McpSubscriptionAuthorizer authorizes and renews each URI grant using the real subscribe request. Its historical credentials and latest successful application context remain retained evidence; check current authority on every renewal. Duplicate subscribe obtains fresh authorization and replaces evidence atomically under a new generation without resetting the original total grant lifetime. Unsubscribe and reconciliation fence establishing and active grants, so a late callback cannot restore old permission. Deferred cancellation targets older-generation checks and preserves a fresh renewal already started under the fenced generation.

To refresh credentials, open a newly admitted GET for stream permission and send a fresh or duplicate subscribe for each URI permission. A new GET does not replace the credentials retained by URI grants. Check revocation against current authority in both renewal callbacks; request reconciliation when permission changes need prompt reevaluation.

A transient URI renewal exception or timeout fences delivery and permits at most three consecutive failed attempts under the unchanged lease, with 50 ms then 100 ms delays. Successful authorization resets the count. Capacity retries remain bounded by maintenance/control quotas and lease expiry. A callback that ignores cancellation keeps its physical slot and historical evidence; another authorization check for that URI cannot overlap it.

Explicit renewal denial, exhausted failure retries, or expiry of an established URI grant's lease/total lifetime retires the whole session. Its GETs close, current calls are cancelled, and subsequent verified session lookup returns neutral HTTP 404. Reinitialize, refresh credentials as needed, and resubscribe. Legacy protocols have no subscription-ended notification, so session retirement makes lost permission recoverable instead of leaving a healthy-looking stream with a forgotten subscription. Initial subscribe rejection and explicit unsubscribe do not themselves retire a session; a stale callback cannot retire a newer grant.

Grants belong to the session and survive GET loss within their leases, fixed total lifetime, and session deadline. A freshly admitted replacement GET receives only its selected families and currently authorized URI updates. Soklet prefers the newest eligible GET and sends each invalidation to only one connected stream per session, including when GETs overlap. Resource updates contain only a URI; resource/tool/prompt catalog events are coarse invalidation hints. Configured publisher events and applicable localization/caller-dependent catalogs provide the effective change sources; immutable caller-independent catalogs do not advertise a false change capability. Modern subscriptions/listen remains separate.

One pending dirty bit per catalog family or URI grant survives GET gaps when no stream has completed its notification write. A new authorized GET can receive that pending invalidation. A message fully written to one stream is not resent on another GET or reconnect. A failed offer or dropped unwritten frame leaves the invalidation pending; the next writer waits until the original frame's write/drop outcome is known. A URI change during a queued frame remains pending for a subsequent hint. Catalog hints coalesce until a freshly admitted corresponding list operation rearms them. This follows the 2025 single-stream rule. This is best effort with no receipt guarantee, historical payload, event ID, replay, POST result recovery, legacy subscription acknowledgement/ended message, or Tasks notification.

Guarded queued frames check current GET/grant generations and expiry before every socket write. Revoked unwritten frames are purged; a partially written revoked frame closes before further bytes. Encoded-byte reservations release exactly once after write, drop, or close. The server bounds limit grants, retained URI/evidence bytes, queued output, and aggregate maintenance demand. Keep opaque application principals/contexts small: framework byte accounting does not measure their arbitrary object graphs. Host-specific refresh, credential replacement, and reconnect behavior remain unverified.

Legacy POST progress and delivery loss

On either selected 2025 revision, the existing reporter sends progress on the originating POST response. Its first accepted update commits SSE; a handler that emits no update returns JSON. Progress notifications preserve the supplied token, and one whole sanitized terminal result or error uses the selected revision's wire projection. Tool results remain complete results; SSE does not make tool text or structured content into incremental result fragments. Errors before commitment retain ordinary JSON/HTTP handling. Stateless tokens are POST-local; clients must still choose unique tokens across active requests. Within an enabled 2025 session, an active token collision suppresses the reporter while accepting the call. Writer detachment releases that token registration without providing result recovery.

A client disconnect or lost-writer write failure after legacy POST SSE commitment detaches that writer and wakes blocked reporters. Later reports are inert and the eventual result is discarded, without canceling the handler solely because delivery was lost. The absolute deadline and forced-shutdown cancellation still apply, and a non-cooperative handler retains its physical execution slot until it exits. Finite/uncommitted or queued legacy requests and modern requests retain disconnect cancellation. Simulation follows the same distinction. A lost POST result is never moved to GET or replayed; retry according to application idempotency policy.

The November transport specification recommends an initial empty SSE event with an event ID to prime reconnect/polling. Soklet intentionally omits that SHOULD behavior for persistent, nonresumable legacy POST streams. They carry no empty priming event, id: or retry: field, polling, event history, or Last-Event-ID recovery. Progress display and the effect of omitted priming events remain unverified in particular clients and hosts.

Enable resource subscriptions

Subscriptions are opt-in per endpoint. This example assumes application-owned List<McpResourceRegistration> resources, McpImplementation serverInformation, and a thread-safe subscriptionAuthorizer that checks current permission for the whole requested subscription:

For these resource notification families, the list must include an exact or templated resource enabled for V2026_07_28. An endpoint configured with a custom resource-list handler can also supply the resource catalog.

McpSubscriptionEventPublisher publisher =
  McpSubscriptionEventPublisher.fromInMemoryDefaults();

McpSubscriptionConfig subscriptions = McpSubscriptionConfig
  .withEventPublisherAndNotificationTypes(publisher, Set.of(
    McpSubscriptionNotificationType.RESOURCES_LIST_CHANGED,
    McpSubscriptionNotificationType.RESOURCE_UPDATED))
  .build();

McpEndpoint endpoint = McpEndpoint.withPath(
    "/catalog/mcp", serverInformation,
    Set.of(McpProtocolVersion.V2026_07_28))
  .resourceRegistrations(resources)
  .subscriptionConfig(subscriptions)
  .subscriptionProtocolVersions(Set.of(McpProtocolVersion.V2026_07_28))
  .build();

McpServer server = McpServer.withPort(8082)
  .endpointRegistry(McpEndpointRegistry.fromEndpoints(Set.of(endpoint)))
  .subscriptionAuthorizer(subscriptionAuthorizer)
  .build();

The server requires an explicitly selected McpSubscriptionAuthorizer for modern subscription support; configuring only the endpoint does not satisfy that requirement. Explicitly selecting denyAllInstance() is valid and rejects every grant. authorize(...) checks the whole candidate subscription before acknowledgment, then the acknowledged selection during renewal or reconciliation. Its initial request context is historical evidence; each check must consult current application authority. An allowed result needs an explicit expiry; null, denial, or an exception fails closed.

The effective lease is capped by the application's expiry, the server's maximumSubscriptionAuthorizationDuration (one minute by default), and the fixed subscription lifetime. subscriptionAuthorizationTimeout bounds each callback including queue time (five seconds by default). Renewal rechecks authority before expiry; keep-alives do not extend permission. After policy invalidation, make current policy available on every applicable node and call its getSubscriptionReconciler().reconcileSubscriptions(). This fences local delivery and schedules checks; it does not wait for their completion, recall bytes already written, or migrate streams. See Server Configuration for the authorization settings and Listen-stream behavior for established denial/expiry handling.

For an unchanged renewal, return the same application-context object instance, or continue omitting it. Soklet compares identity, not equals. Use an immutable authority snapshot, replace it when authority changes, and request reconciliation for prompt revocation. A fresh context instance or changed accepted-task set fences old delivery even if its values compare equal. Unwritten task notifications are rechecked after every accepted renewal. A partly written frame may finish across an unchanged renewal while its authorization generation and lease remain valid; actual revocation still stops it and can close the stream mid-frame.

At least one notification type is required. The application publisher provides resource-list changed, requested-resource updated, tool-list changed, and prompt-list changed hints. publishToolsListChanged() and publishPromptsListChanged() invalidate caller-dependent views; underlying registrations remain immutable. When localization is enabled and subscriptions/listen is available, however, Soklet advertises and acknowledges tool, prompt, and resource list-change filters for the catalog families that actually contain localizable text. After the application atomically installs a new translation snapshot, server.getLocalizationCatalogInvalidator().invalidateCatalogs() emits those coarse framework-owned invalidations; it does not make the underlying registrations mutable.

For generated annotated endpoints, attach subscription configuration with registry.withSubscriptionConfig(AnnotatedEndpoint.class, subscriptions). The exact class identity must belong to a descriptor already loaded by that registry.

Publish change events

Application code emits only coarse changes:

publisher.publishResourcesListChanged();
publisher.publishResourceUpdated(URI.create("catalog://items/ABC-123"));

McpSubscriptionEvent is the sealed family ResourcesListChanged, ResourceUpdated, ToolsListChanged, and PromptsListChanged. The sealed root owns direct construction through resourcesListChanged(), resourceUpdated(uri), toolsListChanged(), and promptsListChanged(); the publisher conveniences above delegate to those factories. The nested variants are public final types for typed pattern matching but have private constructors. A matched ResourceUpdated exposes its validated URI through getResourceUri(). Updated URIs must be absolute, normalized ASCII wire values, and their toString() rendering is redacted.

Events do not identify an endpoint, principal, or connected client. Soklet owns endpoint configuration, requested-filter matching, authorization isolation, per-stream coalescing, bounded queues, backpressure, and wire serialization.

Publisher SPI and ownership

McpSubscriptionEventPublisher is an application-owned, thread-safe broadcast SPI:

McpSubscriptionEventRegistration subscribe(
  McpSubscriptionEventListener listener
);

void publish(McpSubscriptionEvent event);

Every event must reach every current listener. A competing-consumer work queue is not valid. Soklet closes only the listener registration returned by subscribe; it never closes the publisher.

Shutdown fences the listener before closing its registration outside lifecycle locks. A completed close attempt that threw is retried in the force phase; an attempt still running is retained and interrupted without starting a concurrent close on the same registration. Cleanup uses the shared graceful and forced budgets in LifecyclePolicy. Failed or still-running cleanup at the deadline can leave residual activity or missing termination proof in the immutable ShutdownResult; a later exit does not rewrite that result. A new lifecycle requires fresh transport instances.

McpSubscriptionEventPublisher.fromInMemoryDefaults() supplies the built-in process-local implementation. It broadcasts synchronously on the publishing thread and attempts every current listener before rethrowing the first listener failure with later failures suppressed. It does not coordinate across instances. A custom implementation may bridge a distributed bus but must retain broadcast semantics.

A distributed publisher supplies fleet-wide delivery to currently live listeners; it does not add reconnect replay, retained subscription positions, or a durable event log.

Subscribe to task changes

Task notification support comes from the server-level McpTaskManager, independently of endpoint resource-subscription configuration. If McpTaskManager.getTaskEventPublisher() returns a publisher, a Tasks-capable client can request task IDs through subscriptions/listen:

{
  "jsonrpc": "2.0",
  "id": "task-watch-1",
  "method": "subscriptions/listen",
  "params": {
    "_meta": {
      "io.modelcontextprotocol/protocolVersion": "2026-07-28",
      "io.modelcontextprotocol/clientCapabilities": {
        "extensions": {
          "io.modelcontextprotocol/tasks": {}
        }
      }
    },
    "notifications": {
      "taskIds": ["01K4REPORTTASK"]
    }
  }
}

Soklet deduplicates the requested IDs, bounds the request to 256 distinct task IDs, and performs a fresh authorized manager lookup for each one before opening the stream. The acknowledgment echoes only accepted, authorized task IDs; unknown, unauthorized, and capability-incompatible tasks disclose nothing. A server with Tasks but no task event publisher still supports authoritative tasks/get polling and does not claim task-notification delivery.

McpTaskEventPublisher broadcasts coarse task-ID hints after a state change is durable. On every hint, Soklet performs another authorized lookup and projects the current McpTask snapshot as notifications/tasks. Duplicate, delayed, and lost hints are valid; the event does not contain authoritative task state. Within one stream, Soklet suppresses stale regressions and contradictory states after a terminal snapshot.

Each subscription contributes at most one queued or running task-projection job to the shared scheduler. Distinct accepted task IDs retain first-event order, while repeated hints for one ID coalesce to the newest generation without losing a hint that arrives during lookup. This lets the full 256-ID filter survive a simultaneous burst without filling the shared queue by itself. Lookups are serialized within one subscription, so a slow task-manager lookup delays that subscription's other IDs while unrelated subscriptions continue through bounded parallel workers.

The task publisher must use broadcast rather than competing-consumer semantics. A fleet-wide implementation must deliver each hint to every Soklet node that might host a matching live subscription. Soklet bounds projection work separately from ordinary request execution and closes a subscription that cannot keep up; it does not sacrifice tasks/get capacity to preserve an advisory notification stream.

The built-in McpInMemoryTaskManager includes a process-local publisher for development and simulation. A production manager should expose its application-owned distributed publisher through the stable getTaskEventPublisher() property. Soklet subscribes separately for each built server, closes only its own registration, and never closes the manager or publisher. See MCP Tasks for the durability and reconnect contract.

Listen-stream behavior

subscriptions/listen is a framework-owned long-lived POST SSE request:

  • It traverses admission and the optional request limiter.
  • It does not invoke an application handler or handler interceptor, consume a handler slot, or use a tool limiter.
  • The acknowledgment is always the first stream message.
  • Each message carries the listen request's exact ID as io.modelcontextprotocol/subscriptionId.
  • Equal IDs on independent requests still identify independent streams.
  • On graceful shutdown, a writable stream receives only the tagged empty terminal complete result, never the stdio-only notifications/cancelled message.

A server-initiated end also covers authorization denial or expiry, a required check failure, and bounded queue pressure. Soklet fences notification delivery and, when the transport remains usable, sends the same tagged empty complete result before closing. It removes unwritten notifications while retaining the acknowledgment before the terminal. Its separate bounded terminal reservation remains available when the regular frame queue is full. A stalled writer still has a write-idle timeout; a disconnect, failed writer, or partially written revoked frame can end abruptly. The completion contains no policy or exception data. Stream and subscription metrics retain the actual close reason, and request outcomes retain the cancellation or failure. The simulator exposes that reason together with its terminal message.

Tool and prompt catalog projections compare the caller's current view with the latest digest covered by a queued hint. An accepted hint or a change folded into a wholly unwritten hint advances that baseline. Once a hint starts writing, a later change gets a bounded successor because the client may already have consumed the earlier event. This preserves back-and-forth catalog changes without emitting duplicates for an unchanged view. Clients re-list under current authorization; the baseline is not a delivery receipt or retained catalog snapshot.

Soklet computes accepted tool and prompt baselines before the acknowledgment. If their catalogs change during opening, it keeps the current grant and queues a bounded comparison after the acknowledgment. Catalog churn does not repeat subscription authorization; an actual authorization reconciliation still requires a fresh grant. An unchanged caller-visible catalog emits no hint.

The server defaults to 32 active subscriptions per endpoint and admitted authorization/quota partition per server instance, a 24-hour maximum duration, a 128-item pending stream queue, 15-second keep-alives, and a 30-second write timeout. keepAliveInterval must be strictly shorter than writeTimeout. A slow or disconnected subscriber is removed without blocking unrelated streams.

After a listen connection is lost, establish and acknowledge its replacement subscription first and begin consuming new events. Then recover authoritative state: re-read or re-list resources, and call tasks/get for each task. Reconcile that snapshot with buffered and subsequent events by an application-defined version or cursor. This order can produce harmless duplicate processing, but it avoids the unobservable gap created by reading first and subscribing afterward. Soklet ignores Last-Event-ID and provides no built-in replay position; applications that require gap-free resource delivery need a durable event log and application-level reconciliation protocol, while task clients always retain tasks/get as their authoritative recovery path. See Failures, Retries, And Durable Work.

Shutdown fences the listener generation and closes application listener registrations outside lifecycle locks with bounded cleanup. A failing or still-running registration close contributes residual activity or missing termination proof to that one-shot lifecycle's immutable shutdown result.

Configure these limits in Server Configuration and observe them through MCP metrics and diagnostics.

Previous
Tasks