Skip to content

Add server support for the MCP 2026-07-28 ("modern") protocol revision - #7284

Merged
tzolov merged 15 commits into
spring-projects:mainfrom
tzolov:modern-mcp-server-2
Nov 2, 2026
Merged

tzolov merged 15 commits into
spring-projects:mainfrom
tzolov:modern-mcp-server-2

Conversation

@tzolov

@tzolov tzolov commented Oct 30, 2026 •

Copy link
Copy Markdown
Contributor

Adds a server for the 2026-07-28 ("modern") MCP revision next to the existing STDIO, SSE, STREAMABLE and STATELESS servers, activated with spring.ai.mcp.server.protocol=MODERN. The revision drops initialize and sessions: server/discover is a cacheable request, every request carries the protocol version and the client capabilities in _meta, server-initiated input (elicitation, sampling, roots) is a multiround-trip request, and change notifications go over subscriptions/listen.

The integration builds on the modern API of MCP Java SDK 2.2.0 (io.modelcontextprotocol.modern), in particular the McpSyncTool/McpSyncPrompt/McpSyncResource features with repositories (modelcontextprotocol/java-sdk#1282) and required client capabilities (modelcontextprotocol/java-sdk#1283).

What is in the change

  • Transports - WebMvcModernServerTransport and WebFluxMcpTransport serve the revision over a single POST endpoint: header/body consistency checks, JSON-RPC error to HTTP status mapping, bounded body reading, server-sent event streaming with keep-alives and an early header commit, and subscriptions/listen. The WebFlux transport runs sync handlers on boundedElastic and cancels in-flight work on client
    disconnect.
  • Repositories (spring-ai-mcp) - In-memory repositories for tools, prompts, resources and completions with runtime add/remove, change broadcasts and key-cursor paging. SyncResourceEntry and SyncResourceTemplateEntry pair a resource definition with its handler, which the SDK's McpSyncResource does not carry. A legacy/modern McpSchemaConverter, ToolCallback adaptation and a header-based transport context convention complete the module.
  • Annotations (spring-ai-mcp-annotations) - Modern method callbacks for @McpTool, @McpResource, @McpPrompt and @McpComplete implementing the SDK handler interfaces, and providers that build the SDK types from annotated beans. Methods may declare McpModernSyncRequestContext for the request data and the MRTR fields (inputResponses, requestState), and the SDK McpSyncNotifier for progress; a method streams if and only if it declares the notifier.
  • Required client capabilities - requiredCapabilities on @McpTool, @McpPrompt and @McpResource lists the McpClientCapability values a method cannot work without; the server answers -32021 before the method runs. A runtime check on InputRequiredResult remains as the safety net.
  • Definition attributes - @McpIcon with icons on the three annotations, @McpArg(title), @McpResource(size, ttlMs, cacheScope) and @McpToolParam(header = true), which emits the x-mcp-header schema keyword. The attributes are wired on the legacy, stateless and modern providers alike; the modern-only ones are ignored with a warning elsewhere.
  • Auto-configuration - Publishes the scanned primitives as list beans (List<McpSyncTool> etc.), builds an in-memory repository per primitive unless a repository bean of the SDK interface is defined, wires McpServer with the features, the change feed and the request state codec, and registers the transports. New properties under spring.ai.mcp.server.modern.* (endpoint, list and discover TTLs, page size, request state secret and TTL).
  • Documentation - A reference page for the modern server (mcp-modern-server-boot-starter-docs.adoc) and the new attributes on the annotations page.

Example

@McpTool(name = "delete_task", description = "Deletes a task after asking the user to confirm",
        requiredCapabilities = McpClientCapability.ELICITATION_FORM)
public CallToolOutcome deleteTask(McpSyncNotifier notifier, McpModernSyncRequestContext ctx,
        @McpToolParam(description = "Task id") String id) {
    Optional<ElicitResponse> answer = ctx.inputResponse("confirm", ElicitResponse.class);
    if (answer.isEmpty()) {
        notifier.progress(10, 100.0, "asking for confirmation");
        return InputRequiredResult.builder()
            .elicit("confirm", ElicitFormRequest.builder("Delete task " + id + "?", CONFIRM_SCHEMA).build())
            .requestState(id)
            .build();
    }
    ...
}

Annotated tools, resources and prompts of a STATELESS server migrate with the property change alone; the session-bound calls of a STREAMABLE server (ctx.elicit, ctx.sample, ctx.progress, client logging) are rewritten as above. Working samples for both migrations are in spring-projects/spring-ai-examples#151 (model-context-protocol/modern-migration).

Scope and follow-ups

  • Only the synchronous API type is supported; type=ASYNC with protocol=MODERN fails at startup.
  • Per-request client logging is not exposed, as the revision deprecates it (SEP-2577).
  • Per-caller visibility of primitives is done by wrapping a repository bean; no filter interface yet.

Dependencies

mcp.sdk.version moves from 2.1.0 to 2.2.0-SNAPSHOT. The PR can be merged once MCP Java SDK 2.2.0 (or its first M/RC) is released and the version is updated.

@tzolov tzolov added enhancement New feature or request mcp labels Oct 30, 2026
@tzolov tzolov added this to the 2.1.0-RC2 milestone Oct 30, 2026
Comment on lines +39 to +41
public McpTransportContext extract(ServerRequest request) {
return McpTransportContexts.fromHeaders(request.headers().asHttpHeaders()::forEach);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As mentioned in the meeting this will only generate headaches for us. Let users implement it, as we cannot ever have a full white or black list.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed, and done in 8d514d2: the extractor is gone, and so is the context-headers property that briefly replaced it. The transport context is empty unless the application defines a McpTransportContextExtractor bean. McpTransportContexts.fromHeaders(names, lookup) stays as a helper for such a bean, and the reference page shows one in the per-caller filtering example.

tzolov and others added 13 commits November 2, 2026 19:22
The 2026-07-28 ("modern") server API lives in the
`io.modelcontextprotocol.modern` package, which is only available in
the 2.2.0 line of the SDK. The snapshot must be installed locally from
the java-sdk checkout until the SDK is released.

Signed-off-by: Christian Tzolov <christian.tzolov@broadcom.com>
`WebMvcModernServerTransport` serves the 2026-07-28 revision over a
single POST endpoint: header consistency checks against the body,
bounded body reading, JSON-RPC error to HTTP status mapping, server-sent
event streaming with keep-alives and an early header commit, and
`subscriptions/listen` over the servlet async context. The protocol
helpers mirror the SDK's servlet transport in `ModernTransportSupport`.

Signed-off-by: Christian Tzolov <christian.tzolov@broadcom.com>
Add WebFluxMcpTransport, a stateless POST-only transport serving the
2026-07-28 MCP revision through the SDK's McpRequestManager.

Requests are resolved with the non-blocking handle(), so sync handlers
run on boundedElastic and never on the event loop. Streaming responses
and subscriptions/listen are sent as server-sent events with
keep-alives, and a leading comment commits the response headers right
away. Client disconnects cancel in-flight work. Headers are validated
against the body, and JSON-RPC errors are mapped to HTTP statuses.

Taken from chemicL/spring-ai branch modern-webflux-transport, commit
70afa59, without its module-local SDK
version pin.

Signed-off-by: Dariusz Jędrzejczyk <dariusz.jedrzejczyk@broadcom.com>
Signed-off-by: Christian Tzolov <christian.tzolov@broadcom.com>
Reject non-JSON request bodies with 415 as the SDK servlet transport
does, add `no-transform` to the stream cache header, and tag the public
API `@since 2.2.0` in line with the rest of the modern integration.

Signed-off-by: Christian Tzolov <christian.tzolov@broadcom.com>
The SDK repositories return a McpSyncTool from find and no longer have
a call method, so the three test fixtures now return McpSyncTool.of(tool,
this::call). The new types carry @SInCE 2.1.0.

Signed-off-by: Christian Tzolov <christian.tzolov@broadcom.com>
In-memory repositories for the four primitives on the SDK's McpSyncTool,
McpSyncPrompt and McpSyncResource, with runtime add/remove, change
broadcasts and key-cursor paging; SyncResourceEntry and
SyncResourceTemplateEntry pair a resource or template definition with
its handler, which McpSyncResource does not carry; a legacy/modern
schema converter, ToolCallback adaptation, the transport context header
convention and the modern server customizer.

Signed-off-by: Christian Tzolov <christian.tzolov@broadcom.com>
Modern method callbacks for @mcptool, @McpResource, @McpPrompt and
@McpComplete that implement the SDK handler interfaces, the providers
that build McpSyncTool, McpSyncPrompt, SyncResourceEntry and
SyncResourceTemplateEntry from annotated beans, and the
McpModernSyncRequestContext parameter type with progress and MRTR
support. Input responses are converted with the server's JSON mapper.

Signed-off-by: Christian Tzolov <christian.tzolov@broadcom.com>
Activated by spring.ai.mcp.server.protocol=MODERN. Publishes the scanned
primitives as list beans, builds an in-memory repository per primitive
unless a repository bean is defined, wires the McpServer with the
features, the change feed and the request state codec, and adds the
WebMvc and WebFlux transports with a default header-based transport
context extractor.

Signed-off-by: Christian Tzolov <christian.tzolov@broadcom.com>
Design document, SDK review for the second iteration, migration guide
and the reference page for the modern server starter.

Signed-off-by: Christian Tzolov <christian.tzolov@broadcom.com>
A method that declared McpModernSyncRequestContext was always served as
a streaming response, so a parameter type decided the response shape.
The SDK leaves that choice to the handler, which returns either
McpSyncResponse.result or McpSyncResponse.streaming; multi round-trip
methods that never report progress, and calls without a progress token,
were streamed for nothing.

The context now carries the request data and the MRTR fields only:
progressEnabled(), progress(...) and notify(...) are removed, and
DefaultMcpModernSyncRequestContext no longer holds a notifier. A tool,
resource or prompt method streams if and only if it declares the SDK
McpSyncNotifier, and reports progress through it. The reference page,
the design documents and the migration guide describe the rule, and the
tests cover a token-less streaming call and a context-only plain result.

Signed-off-by: Christian Tzolov <christian.tzolov@broadcom.com>
The MCP schema has long carried fields the annotations could not set,
and the 2026-07-28 revision adds one more. `@McpIcon` and an `icons`
attribute on `@McpTool`, `@McpResource` and `@McpPrompt` publish icons
in the definitions; `@McpArg` gains `title`; `@McpResource` gains
`size`. `@McpToolParam(header = true)` emits the `x-mcp-header` schema
keyword so clients mirror a parameter into the `Mcp-Param-<name>`
header. The keyword is allowed on parameters whose generated schema
type is string, integer or boolean, so enums qualify, and other types
are rejected at startup.

The attributes are wired on the legacy, stateless and modern providers
alike; the modern definitions receive them through the JSON round-trip
of `McpSchemaConverter.toModern`, which also derives the modern icons
from the legacy ones. The reference page documents the attributes.

The design folder is reorganised: `MODERN.md` is now a one-page
summary, the full design with its diagrams moves to
`MODERN-detailed.md`, and the folder README is removed.

Signed-off-by: Christian Tzolov <christian.tzolov@broadcom.com>
Annotated modern methods were built with ClientCapabilities.NONE, so a
client lacking elicitation, sampling or roots was refused only after
the method had run, one missing capability at a time, and after a
streaming method had opened its stream.

@mcptool, @McpPrompt and @McpResource now take requiredCapabilities, a
list of McpClientCapability values, which the modern providers pass to
McpSync*.of(definition, requiredCapabilities, handler). The SDK refuses
such requests with -32021 before the method runs. SyncResourceEntry and
SyncResourceTemplateEntry gain the matching of(...) overloads.

The runtime check on InputRequiredResult stays as a safety net and
warns once per callback. The modern provider warns when the return type
cannot carry an InputRequiredResult; the legacy providers warn that the
attribute is ignored. logback-classic joins the test classpath so the
warnings can be asserted.

Signed-off-by: Christian Tzolov <christian.tzolov@broadcom.com>
Rename SyncResourceEntry, SyncResourceTemplateEntry and
SyncCompletionSpecification to SyncResourceRegistration,
SyncResourceTemplateRegistration and SyncCompletionRegistration, and
the completion provider methods to modernCompletions.

McpModernSyncRequestContext.inputResponse and
McpModernToolUtils.getMcpRequestContext return @nullable values
instead of Optional.

Pin the MCP Java SDK to 2.2.0-M2.

Signed-off-by: Christian Tzolov <christian.tzolov@broadcom.com>
@tzolov
tzolov force-pushed the modern-mcp-server-2 branch from 8a8c6b9 to dca1b1b Compare November 2, 2026 19:01
The WebMvc and WebFlux auto-configurations stored every request header
in the transport context, including Authorization and Cookie, in front
of every handler, repository and ToolCallback. The SDK and the legacy
Spring transports default to an empty context.

spring.ai.mcp.server.modern.context-headers names the headers to keep;
nothing is stored by default. McpTransportContexts.fromHeaders(names,
lookup) builds the context from the named headers only and returns the
empty context when none is present.

Signed-off-by: Christian Tzolov <christian.tzolov@broadcom.com>
- Mirror the Mcp-Param-* headers into the transport context on every
  tools/call, where the SDK tools feature validates them, and put the
  bare parameter name in x-mcp-header as the SDK expects
- Decode Mcp-Name with the SDK McpHeaderDecoder; the local copy failed
  on "=?base64?="
- Answer a malformed Content-Type with 415 and a failed notification
  with 500
- Require a request-state-secret of at least 32 bytes
- Drop the built-in header extractor: the transport context is empty
  unless the application defines a McpTransportContextExtractor bean

Signed-off-by: Christian Tzolov <christian.tzolov@broadcom.com>
@tzolov
tzolov merged commit 109bb1c into spring-projects:main Nov 2, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request mcp

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants