Soklet Logo

Core Concepts

Model Context Protocol

Model Context Protocol (or MCP) is a JSON-RPC based protocol for exposing tools, prompts, and resources to MCP clients. Soklet provides a first-class MCP server implementation that handles the transport, session lifecycle, SSE stream management, simulator testing, and most of the protocol plumbing so your application code can stay focused on endpoint behavior.

At a high level, Soklet's MCP support consists of 3 pieces:

  1. One or more endpoint classes annotated with @McpServerEndpoint
  2. One or more annotated or programmatic handlers for tools, prompts, resources, or resource listing
  3. An McpServer configured on your SokletConfig

Endpoint Definition

MCP endpoints are not normal Soklet Resource Methods. Instead, you define a class that implements McpEndpoint and decorate it with @McpServerEndpoint.

You may define more than one MCP endpoint in the same application. A single McpServer can host multiple endpoint classes as long as they use distinct paths, and Soklet routes MCP transport requests to the matching endpoint by path.

Within that class, you can expose:

Here's a representative endpoint:

@McpServerEndpoint(
  path = "/tenants/{tenantId}/mcp",
  name = "catalog",
  version = "1.0.0",
  title = "Catalog MCP",
  instructions = "Use read-only mode."
)
public class CatalogEndpoint implements McpEndpoint {
  @Override
  public McpSessionContext initialize(
    McpInitializationContext context,
    McpSessionContext session
  ) {
    return session.with(
      "tenantId",
      context.getEndpointPathParameter("tenantId").orElseThrow()
    );
  }

  @McpTool(name = "sum", description = "Adds numbers.")
  public McpToolResult sum(
    @McpArgument("count") Integer count,
    McpSessionContext sessionContext
  ) {
    String tenantId = sessionContext.get("tenantId", String.class).orElseThrow();

    return McpToolResult.builder()
      .content(McpTextContent.fromText(
        "%s for %s".formatted(count, tenantId)
      ))
      .build();
  }

  @McpPrompt(
    name = "greet",
    description = "Creates a greeting.",
    title = "Greeting"
  )
  public McpPromptResult greet(@McpArgument("name") String name) {
    return McpPromptResult.fromMessages(
      McpPromptMessage.fromAssistantText("Hello %s".formatted(name))
    );
  }

  @McpResource(
    uri = "catalog://tenants/{tenantId}/recipes/{recipeId}",
    name = "recipe",
    mimeType = "text/plain"
  )
  public McpResourceContents recipe(
    @McpEndpointPathParameter("tenantId") String tenantId,
    @McpUriParameter("recipeId") String recipeId
  ) {
    return McpResourceContents.fromText(
      "catalog://tenants/%s/recipes/%s".formatted(tenantId, recipeId),
      "tenant=%s recipe=%s".formatted(tenantId, recipeId),
      "text/plain"
    );
  }
}

Soklet uses the same ValueConverterRegistry for MCP endpoint and resource URI parameters that it uses for ordinary HTTP request binding. That means both annotation-based parameters like @McpEndpointPathParameter UUID tenantId / @McpUriParameter Integer recipeId and programmatic helpers like context.getEndpointPathParameter("tenantId", UUID.class) or resourceContext.getUriParameter("recipeId", Integer.class) share the same conversion rules described in Value Conversions.

Soklet also supports programmatic handler registration via McpHandlerResolver, which is useful when annotations are not enough or when you want to layer handlers onto an endpoint dynamically.

References:

Programmatic Handlers

McpHandlerResolver can discover endpoint classes and then layer programmatic handlers onto them. This is useful when handlers are assembled from modules, generated from configuration, or simply easier to express in code than with annotations.

McpHandlerResolver handlerResolver = McpHandlerResolver
  .fromClasses(Set.of(CatalogEndpoint.class))
  .withTool(new CatalogSearchToolHandler(), CatalogEndpoint.class)
  .withPrompt(new CatalogSummaryPromptHandler(), CatalogEndpoint.class)
  .withResource(new CatalogNoteResourceHandler(), CatalogEndpoint.class)
  .withResourceList(new CatalogResourceListHandler(), CatalogEndpoint.class);

SokletConfig config = SokletConfig.withMcpServer(
  McpServer.withPort(8082)
    .handlerResolver(handlerResolver)
    .build()
).build();

Programmatic tool handlers provide metadata plus a handle(...) method:

public final class CatalogSearchToolHandler implements McpToolHandler {
  @Override
  public String getName() {
    return "search_catalog";
  }

  @Override
  public String getDescription() {
    return "Searches the catalog.";
  }

  @Override
  public McpSchema getInputSchema() {
    return McpSchema.object()
      .required("query", McpType.STRING)
      .optionalEnum("mode", "keyword", "semantic")
      .build();
  }

  @Override
  public McpToolResult handle(McpToolHandlerContext context) {
    UUID tenantId = context
      .getEndpointPathParameter("tenantId", UUID.class)
      .orElseThrow();
    String query = ((McpString) context.getArguments()
      .get("query")
      .orElseThrow()).value();

    return McpToolResult.builder()
      .content(McpTextContent.fromText(
        "tenant=%s query=%s".formatted(tenantId, query)
      ))
      .build();
  }
}

The other programmatic contracts follow the same pattern:

McpSchema is Soklet's JSON-schema DSL for programmatic MCP handler metadata. McpSchema::object supports required and optional scalar properties plus enum-backed string properties through McpSchema.ObjectBuilder::required, McpSchema.ObjectBuilder::optional, McpSchema.ObjectBuilder::requiredEnum, and McpSchema.ObjectBuilder::optionalEnum.

Configuration

MCP runs on its own McpServer. Like Soklet's SSE support, this is a separate server and cannot share the same port as any regular HttpServer you might also configure.

SokletConfig config = SokletConfig.withMcpServer(
  McpServer.withPort(8082)
    .handlerResolver(McpHandlerResolver.fromClasspathIntrospection())
    .build()
).build();

If the same application also serves ordinary HTTP Resource Methods, add .httpServer(HttpServer.fromPort(8080)) to the builder.

For tests or small examples, McpHandlerResolver::fromClasses is often more convenient:

McpServer.withPort(0)
  .handlerResolver(McpHandlerResolver.fromClasses(
    Set.of(CatalogEndpoint.class)
  ))
  .build();

If your application exposes multiple MCP endpoints, register all of them with the same resolver:

McpServer.withPort(8082)
  .handlerResolver(McpHandlerResolver.fromClasses(
    Set.of(CatalogEndpoint.class, AdminEndpoint.class)
  ))
  .build();

Each endpoint keeps its own path, session namespace, and serverInfo metadata. For example, /tenants/{tenantId}/mcp and /admin/mcp can coexist on the same MCP server.

The builder exposes the expected transport knobs: request timeout, handler timeout, handler concurrency, queue capacity, request size limits, heartbeat interval, write timeout, concurrent connection limits, custom session stores, request admission, request interception, and custom MCP CORS authorization. The default session store has its own builder for idle expiry, concurrent session limits, and MCP session ID generation. The handler timeout covers MCP JSON-RPC handlers, including framework-managed McpEndpoint::initialize.

MCP transport requests expose parsed W3C trace data through the same Request::getTraceContext accessor used by ordinary HTTP resources. Initialization, admission, request handlers, lifecycle callbacks, and metrics callbacks can reach the transport request through their existing context objects. Session-bound MCP SSE streams expose the establishing request through McpSseStream::getRequest. Core Soklet does not create MCP spans, but OpenTelemetryLifecycleObserver in soklet-otel can use the parsed context as the parent context for MCP JSON-RPC and MCP SSE spans.

You can omit sessionStore(...) to use Soklet's default in-memory store. Configure it explicitly when you want to tune idle expiry or the active-session cap:

McpServer.withPort(8082)
  .handlerResolver(McpHandlerResolver.fromClasses(
    Set.of(CatalogEndpoint.class)
  ))
  .sessionStore(McpSessionStore.builder()
    .idleTimeout(Duration.ofHours(24))
    .concurrentSessionLimit(8_192)
    .build())
  .build();

By default, Soklet's in-memory McpSessionStore generates cryptographically strong, header-safe MCP-Session-Id values. To add deployment-specific routing hints, configure the store with an IdGenerator that still leaves the session ID unguessable:

IdGenerator<String> sessionIds = IdGenerator.defaultSessionInstance();

McpServer.withPort(8082)
  .handlerResolver(McpHandlerResolver.fromClasses(
    Set.of(CatalogEndpoint.class)
  ))
  .sessionStore(McpSessionStore.builder()
    .sessionIdGenerator(request ->
      "iad-3." + sessionIds.generateId(request)
    )
    .build())
  .build();

For clustered deployments, a custom McpSessionStore is usually appropriate. Redis-backed and SQL-backed implementations are both reasonable choices, as long as they generate session IDs, apply any concurrent-session cap, persist new sessions atomically, and preserve the compare-and-set semantics of the store contract.

Browser Clients

The default McpCorsAuthorizer is McpCorsAuthorizer::nonBrowserClientsOnlyInstance, which rejects browser-originated MCP transport requests that carry an Origin header and keeps browser CORS disabled by default. Non-browser clients that do not send Origin are unaffected.

To allow browser-based MCP clients, configure an McpCorsAuthorizer explicitly:

McpServer.withPort(8082)
  .handlerResolver(McpHandlerResolver.fromClasses(
    Set.of(CatalogEndpoint.class)
  ))
  .corsAuthorizer(McpCorsAuthorizer.fromWhitelistedOrigins(
    Set.of("https://chat.openai.com"),
    origin -> true
  ))
  .build();

That enables OPTIONS preflight handling plus Access-Control-* response headers on MCP POST, GET, and DELETE responses for the configured origins. It also authorizes MCP transport requests that carry an Origin header; if Origin is present and the configured McpCorsAuthorizer does not authorize it, Soklet rejects the request with HTTP 403 before running MCP endpoint code or mutating session/stream state.

Error Mapping

McpEndpoint gives you two override points for customizing framework-generated MCP errors.

Use McpEndpoint::handleToolError for exceptions thrown during tools/call:

@Override
public McpToolResult handleToolError(
  Throwable throwable,
  McpToolCallContext context
) {
  return McpToolResult.fromErrorMessage(
    "catalog tool failed: %s".formatted(throwable.getMessage())
  );
}

Use McpEndpoint::handleError for non-tool MCP failures such as initialize, prompts/get, resources/list, or resources/read:

@Override
public McpJsonRpcError handleError(
  Throwable throwable,
  McpRequestContext context
) {
  return McpJsonRpcError.fromCodeAndMessage(
    -32001,
    "Catalog request failed"
  );
}

If you do not override them, Soklet's defaults are intentionally conservative: tool failures become an error-valued McpToolResult, while non-tool failures become a JSON-RPC -32603 Internal error.

Structured Tool Responses

McpToolResult can include both human-readable text content and optional structuredContent. This is the MCP field intended for machine-readable tool results.

By default, Soklet expects structuredContent to already be expressed as Soklet MCP values such as McpObject, McpArray, McpString, and McpNumber. If you want to return arbitrary POJOs or records instead, configure a custom McpResponseMarshaler on your McpServer.

Do not pass gson.toJson(...) or other raw JSON strings into structuredContent. MCP structured content should be an object or array tree, not a pre-serialized JSON string.

Native McpValue Trees

If you are comfortable building the MCP value tree directly, no extra marshaling setup is required:

@McpTool(name = "lookup_recipe", description = "Looks up a recipe.")
public McpToolResult lookupRecipe(@McpArgument("recipeId") String recipeId) {
  return McpToolResult.builder()
    .content(McpTextContent.fromText("Recipe found: tomato soup"))
    .structuredContent(new McpObject(Map.of(
      "recipeId", new McpString(recipeId),
      "title", new McpString("Tomato Soup"),
      "servings", new McpNumber(BigDecimal.valueOf(4)),
      "tags", new McpArray(List.of(
        new McpString("soup"),
        new McpString("vegetarian")
      ))
    )))
    .build();
}

That produces an MCP tool result whose structuredContent is an actual JSON object:

{
  "jsonrpc": "2.0",
  "id": "req-2",
  "result": {
    "content": [
      {
        "type": "text",
        "text": "Recipe found: tomato soup"
      }
    ],
    "isError": false,
    "structuredContent": {
      "recipeId": "recipe-123",
      "title": "Tomato Soup",
      "servings": 4,
      "tags": ["soup", "vegetarian"]
    }
  }
}

JSON Conversion

If your application already models tool results as normal Java objects, configure a custom McpResponseMarshaler that converts those objects into Soklet's McpValue tree.

For example, with Gson:

public final class GsonMcpResponseMarshaler implements McpResponseMarshaler {
  private final Gson gson;

  public GsonMcpResponseMarshaler(Gson gson) {
    this.gson = Objects.requireNonNull(gson);
  }

  @Override
  public McpValue marshalStructuredContent(
    Object value,
    McpStructuredContentContext context
  ) {
    if (value == null)
      return McpNull.INSTANCE;

    if (value instanceof McpValue mcpValue)
      return mcpValue;

    return toMcpValue(this.gson.toJsonTree(value));
  }

  private McpValue toMcpValue(JsonElement jsonElement) {
    if (jsonElement == null || jsonElement.isJsonNull())
      return McpNull.INSTANCE;

    if (jsonElement.isJsonObject()) {
      Map<String, McpValue> values = new LinkedHashMap<>();

      for (Map.Entry<String, JsonElement> entry : jsonElement
        .getAsJsonObject()
        .entrySet()) {
        values.put(entry.getKey(), toMcpValue(entry.getValue()));
      }

      return new McpObject(values);
    }

    if (jsonElement.isJsonArray()) {
      List<McpValue> values = new ArrayList<>();

      for (JsonElement child : jsonElement.getAsJsonArray()) {
        values.add(toMcpValue(child));
      }

      return new McpArray(values);
    }

    JsonPrimitive primitive = jsonElement.getAsJsonPrimitive();

    if (primitive.isBoolean())
      return new McpBoolean(primitive.getAsBoolean());

    if (primitive.isNumber())
      return new McpNumber(primitive.getAsBigDecimal());

    return new McpString(primitive.getAsString());
  }
}

Then wire it into your MCP server:

Gson gson = new Gson();

SokletConfig config = SokletConfig.withMcpServer(
  McpServer.withPort(8082)
    .handlerResolver(McpHandlerResolver.fromClasses(
      Set.of(CatalogEndpoint.class)
    ))
    .responseMarshaler(new GsonMcpResponseMarshaler(gson))
    .build()
).build();

Now your tool can return a normal app-owned type as structuredContent:

public record RecipeLookupResponse(
  String recipeId,
  String title,
  Integer servings,
  List<String> tags
) {}

@McpTool(name = "lookup_recipe", description = "Looks up a recipe.")
public McpToolResult lookupRecipe(@McpArgument("recipeId") String recipeId) {
  RecipeLookupResponse response = new RecipeLookupResponse(
    recipeId,
    "Tomato Soup",
    4,
    List.of("soup", "vegetarian")
  );

  return McpToolResult.builder()
    .content(McpTextContent.fromText("Recipe found: tomato soup"))
    .structuredContent(response)
    .build();
}

This approach is often the best fit when your application already uses a JSON library like Gson elsewhere and you want MCP structured content to follow the same serialization rules.

Transport And Session Model

Soklet owns the MCP transport for each path declared by @McpServerEndpoint. Your endpoint code does not implement the transport methods directly.

For example, if one endpoint path is /tenants/{tenantId}/mcp, Soklet handles POST, GET, and DELETE on that path. If another endpoint path is /admin/mcp, Soklet separately handles the same transport methods there too.

Protocol version

Soklet currently implements the MCP 2025-11-25 protocol version. During initialize, Soklet negotiates to that supported version and returns it in the initialize result. After initialization, every session-bound POST, GET, and DELETE must include MCP-Protocol-Version, and the value must match the negotiated session version.

Clients should treat the version returned by initialize as authoritative for the rest of the session. Soklet does not currently expose a multi-version protocol registry; future protocol versions will be added deliberately as the MCP specification stabilizes.

POST to the MCP endpoint

POST requests to the MCP endpoint handle JSON-RPC messages:

  • initialize
  • ping
  • tools/list
  • tools/call
  • prompts/list
  • prompts/get
  • resources/list
  • resources/templates/list
  • resources/read
  • notifications/initialized
  • notifications/cancelled

Important rules:

  • Content-Type must be application/json
  • Accept must allow both application/json and text/event-stream
  • initialize creates the session and returns MCP-Session-Id
  • after initialize, all MCP requests must send both MCP-Session-Id and MCP-Protocol-Version
  • before notifications/initialized, only initialize, notifications/initialized, and ping are allowed
  • unknown id-less methods are treated as JSON-RPC notifications after normal session and protocol validation; admission and interceptor contexts see McpOperationType.UNKNOWN; unknown methods with an id, including "id": null, receive a JSON-RPC -32601 Method not found error

Soklet's MCP JSON codec is intentionally strict. Incoming JSON rejects trailing content, leading BOMs, duplicate object keys, invalid surrogate-half escapes such as a lone \uDC00, and numbers outside Soklet's nesting, token-length, or exponent limits. Outgoing JSON preserves valid surrogate pairs, rejects unpaired surrogate code units, escapes U+2028 and U+2029, and serializes numbers in BigDecimal's canonical form so parsed MCP values can be serialized and parsed again without changing their structure.

Soklet recognizes notifications/cancelled as a framework-managed notification: it validates the session, runs MCP admission/interception/lifecycle/metrics hooks with McpOperationType.NOTIFICATIONS_CANCELED, touches the session, and returns 202 Accepted without a JSON-RPC body. If the notification names a JSON-RPC request ID that is still in flight for the same MCP session, Soklet marks that request's McpCancelationToken as canceled.

Cancelation is cooperative. Soklet does not interrupt handler threads or stop application work automatically. Long-running handlers should poll the token and return at an application-defined safe point:

@McpTool(name = "generate_report", description = "Builds a report.")
public McpToolResult generateReport(McpCancelationToken cancelationToken) {
  while (!cancelationToken.isCancelationRequested()) {
    // Do the next bounded unit of work, then check again.
  }

  return McpToolResult.fromErrorMessage(
    cancelationToken.getReason().orElse("Request canceled")
  );
}

Annotated MCP tool, prompt, resource, and resources/list handlers can accept McpCancelationToken directly. Programmatic handlers can reach the same token through McpRequestContext::getCancelationToken. Cancelation for an unknown, already-completed, or different-node request is a no-op; sticky routing by MCP-Session-Id is still the right production shape for MCP deployments.

If a tool call uses McpProgressReporter via an incoming _meta.progressToken, Soklet first tries to emit notifications/progress immediately to that session's active MCP GET stream on the same node. If no live GET stream is available, Soklet falls back to upgrading that POST response to text/event-stream, replaying buffered progress notifications, and then emitting the terminal JSON-RPC response on the same stream.

GET from the MCP endpoint

GET requests to the MCP endpoint establish a session-bound SSE stream for outbound server messages:

  • Accept must allow text/event-stream
  • MCP-Session-Id and MCP-Protocol-Version are required
  • the session must already be initialized and have received notifications/initialized
  • Last-Event-ID is rejected because resumability and replay are not implemented yet

Multiple live GET streams per session are allowed, but a given server-originated message is routed to one stream, not broadcast to every open stream for that session.

DELETE the MCP session

DELETE requests to the MCP endpoint terminate an MCP session:

  • MCP-Session-Id and MCP-Protocol-Version are required
  • a successful delete returns 204 No Content
  • live GET streams for that session are closed

Clustered Deployments

The default McpSessionStore is in-memory with a 24-hour idle timeout, opportunistic expiry during lookup and later session-creation activity, cryptographically strong MCP-Session-Id generation, and a default 8_192 active-session cap. Configure those defaults with McpSessionStore::builder. When the default store's cap is reached, new initialize requests receive 503 Service Unavailable before session state is created. That is fine for a single-node deployment, and it can also work in front of a load balancer if all MCP requests for a given session stay on the same Soklet node.

For a real distributed deployment, you will usually want:

  • A shared McpSessionStore, commonly backed by Redis or SQL, with session ID generation, concurrency caps, and idle expiry appropriate for your clients
  • Session-affine routing keyed by MCP-Session-Id

That second requirement matters because Soklet stores the live MCP GET stream on the node that accepted it. A shared session store makes the session metadata available cluster-wide, but it does not make the open stream socket cluster-wide. Live tool progress notifications follow the same rule: they can be written immediately to an active GET stream only when the tool-call POST reaches the node that owns that stream.

The recommended pattern is:

  1. initialize may land on any node and creates the session in the shared store.
  2. The edge proxy or gateway records which node owns that MCP-Session-Id, or consistently routes that session to the same node by hashing on MCP-Session-Id.
  3. Later POST, GET, and DELETE requests for that session are routed back to the same node.

This keeps session metadata and live stream ownership aligned, which is the best fit for Soklet's MCP architecture.

In practice, the most common way to do this is to place Soklet behind an ingress or proxy that supports consistent hashing on request headers, using MCP-Session-Id as the hash key. Proxies such as Envoy, NGINX, and HAProxy are a better fit for that model than an AWS Application Load Balancer, which can match on headers for coarse routing but does not provide per-header-value affinity to a specific target.

If you need true non-sticky cluster transparency, you would need more than a custom session store. You would also need your own distributed stream-ownership and message-delivery control plane so one node can tell another node to write to or close the live stream. Soklet does not provide that layer out of the box.

Custom session store contract

A custom distributed McpSessionStore should own all session-admission decisions for its backing system:

  • create(request, endpointClass) should generate a valid, unguessable MCP-Session-Id, enforce any concurrent-session cap atomically, and persist a new uninitialized session
  • return Optional::empty from create(...) when the store declines admission; Soklet maps that to 503 Service Unavailable before endpoint initialization runs
  • findBySessionId(...) should hide expired or deleted sessions
  • replace(expected, updated) must preserve compare-and-set semantics, because Soklet uses it to persist initialization state, session touches, initialized notifications, and termination
  • deleteBySessionId(...) should make later session-bound requests fail as unknown sessions

For Redis, that usually means a Lua script or transaction for create(...) and a version-checked update for replace(...). For SQL, that usually means a uniqueness constraint on session ID plus version-checked UPDATE ... WHERE session_id = ? AND version = ? writes.

One final practical note: McpSessionContext can hold arbitrary Java objects. If you are writing a distributed store, keep the stored session context small and serializable, or store lightweight IDs there and load heavier state from your own backing systems.

Lifecycle And Metrics

MCP traffic participates in Soklet's generic LifecycleObserver and MetricsCollector transport hooks. Connection accept/reject, request accept/reject, request read/fail, and response write/fail callbacks receive ServerType.MCP, so shared observers and collectors can track MCP alongside HTTP and SSE transport traffic.

MCP also emits MCP-specific lifecycle and metrics callbacks for server start/stop, session creation and termination, JSON-RPC request handling, and SSE stream establishment and termination. Use the generic hooks for transport-level visibility and the MCP-specific hooks when you need endpoint class, session ID, JSON-RPC method, request outcome, or StreamTerminationReason details for active MCP SSE streams.

Admission control and interception for MCP use McpRequestAdmissionPolicy and McpRequestInterceptor, since those hooks receive MCP-specific request and session context.

Automated Testing

Soklet offers first-class MCP simulator support. See the Testing documentation for full examples.

The short version:

Implementation Status

Soklet implements a substantial subset of the MCP 2025-11-25 Specification, including the core transport, session lifecycle, tools, prompts, resources, resource templates, and progress reporting. Some optional and more advanced features are not yet implemented; see below for details.

Implemented Now

  • MCP server transport over dedicated POST, GET, and DELETE handling on each configured MCP endpoint path
  • Session creation, protocol negotiation, idle expiry, explicit deletion, and server-stop termination
  • Default in-memory active-session cap for initialize churn protection, with custom stores responsible for their own caps
  • Framework-managed initialize, notifications/initialized, notifications/cancelled, and ping
  • Annotated and programmatic tool handlers
  • Annotated and programmatic prompt handlers
  • Annotated and programmatic resource-read handlers
  • Annotated and programmatic resources/list handlers with optional pagination cursor support
  • resources/templates/list
  • Request-scoped progress reporting for tool calls, including live delivery to active same-node GET streams and automatic POST upgrade to SSE when no live stream is available
  • Cooperative cancelation for in-flight application handlers through McpCancelationToken
  • Simulator support for ordinary MCP requests, GET streams, and progress-upgraded POST streams
  • Generic LifecycleObserver and MetricsCollector transport hooks for MCP via ServerType.MCP, plus MCP-specific hooks for sessions, JSON-RPC requests, and streams
  • Browser-oriented MCP CORS via McpCorsAuthorizer, including OPTIONS preflight handling and Access-Control-* response headers on MCP transport responses
  • Admission control and interception via McpRequestAdmissionPolicy and McpRequestInterceptor

Not Implemented Yet

These are the main items that are not currently supported. Some might be supported in the future, others might not - MCP is rapidly evolving and committing "too early" to features with moving parts might result in an awkward Soklet API, so the design goal is to implement the "core" and evaluate enhancements case-by-case in the future.

  • tools.listChanged, prompts.listChanged, resources.listChanged, and resources.subscribe
  • Public session-scoped outbound notifications beyond request-scoped progress reporting
  • Formal support for resumability and replay for MCP GET streams, including Last-Event-ID
  • Richer content blocks such as image, audio, resource-link, embedded-resource, and annotation support
  • Optional JSON-RPC error.data
  • Built-in authorization and principal modeling
  • Broader progress-reporting surfaces beyond active tool calls
  • JSON-RPC batch handling
  • logging, completions, tasks, and experimental capabilities
  • Additional retention policies for the default in-memory McpSessionStore

References:

Previous
Server-Sent Events (SSE)