You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Third R6b delivery for #1640, after the server side (#1664, #1665, #1666). This adds the MCP 2026-07-28 client, over HTTP. The SDK (mcp-core 2.2.0-M2) has no modern client, only the server, so this client is built on the SDK's modern schema and JSON-RPC types.
API.
McpClientAdapter.setProtocolVersion(McpProtocol.MODERN_VERSION) switches the adapter to 2026-07-28. start() then runs server/discover, and fails if the server doesn't list the revision.
getModernClient() returns the new McpModernClient. In this mode getSdkClient() throws, since the SDK's McpAsyncClient speaks 2025-11-25 only. A stdio target fails at start() with UnsupportedOperationException; the stdio client comes next.
McpModernClient is reactive, on the SDK's modern types:
listen(SubscriptionFilter), which returns a Flux of change notifications.
New adapter settings:
setModernCapabilities(): the client capabilities sent with each request, none by default;
setInputHandler();
setMaxInputRounds(), default 8.
McpInputHandler is a new functional interface.
Behaviour.
Every request carries the protocol version and the client capabilities in its _meta, as the revision requires. Ikanos's client omits the capabilities. Each request also sends MCP-Protocol-Version, Mcp-Method and Mcp-Name, base64-encoded when needed.
Mcp-Param headers: for tools this client has listed, the arguments their x-mcp-header annotations name are copied into Mcp-Param-* headers, numbers in plain notation. A call to an annotated tool the client hasn't listed fails with a header mismatch, as documented. These headers are worked out when the call is sent, so listTools().then(callTool(...)) works.
Progress: a per-call consumer receives the call's progress notifications as they stream in, before the result. A malformed notification, or a consumer that throws, is dropped rather than failing the call.
input_required: the input handler answers and the request is retried with its responses and the sealed requestState, up to the round limit. Without a handler, past the limit, or when the handler returns an empty Mono, the result is returned to the caller.
subscriptions/listen: no time limit, and its own pool, so a listen stream doesn't use the call limit's slots. The stream completes when the server ends it, for example on stop.
Cancellation: cancelling the returned Mono or Flux aborts the HTTP exchange, and the server cancels the handler.
Errors: a JSON-RPC error fails with McpException. A non-JSON or empty error reply, or an event stream that ends without a response, fails with an IOException. Responses are matched to their request's id.
Threads: the client's handlers and the code it hands results to run off the HTTP worker, which frees its slot as soon as the exchange ends. A handler may therefore make blocking calls, even with a call limit of 1.
Tests.McpModernClientTestCase has 12 tests against McpServerAdapter over real HTTP connectors:
discovery, tools, resources and prompts over HTTP/1.1 and HTTP/2, including a non-ASCII argument;
streamed progress;
input_required returned without a handler, retried by hand, and retried by a handler;
a listen stream receiving a published change, then completing without error when the server stops;
a cancelled call cancelling the tool on the server, with the next call still served;
Mcp-Param mirroring: refused before listing, served after it, worked out after a chained listing, and with a number like 1.0E7;
an error response;
the modern mode's limits;
an input handler making a blocking nested call with a call limit of 1. This test fails without the fix.
An independent review found, and this PR fixes:
client code ran on the interrupted worker, which still held its call slot. That broke retries at the call limit and blocking handlers, and dropped the connection after each streamed response;
Mcp-Param headers were worked out when the call was built, not when it was sent;
numbers were formatted in a way the server rejects;
responses weren't matched to their request's id;
a malformed progress notification failed the call.
mvn -pl org.restlet.ext.mcp verify: 85 tests, coverage and SpotBugs pass.
McpClientAdapter speaks 2026-07-28 when its protocol version is set to
it: start() runs server/discover, and getModernClient() returns an
McpModernClient on the SDK's modern types for listings, tool calls,
resource reads and prompts, with streamed progress, subscriptions/listen
and cancellation by disposing the request. An input_required result is
answered by an McpInputHandler and retried within a round limit, or
returned without one. Each request carries the protocol version and the
client capabilities in its _meta, and listed tools' x-mcp-header
arguments are mirrored into Mcp-Param headers.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Name the repeated "_meta" and "request" literals, drop the transport's
always-default listen-stream limit parameter, and keep a single throwing
call inside the tests' assertThrows lambdas.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Address the Copilot review of the modern HTTP transport:
- Read each message only once it is demanded, so a slow listen subscriber
or progress consumer holds back the server instead of buffering without
bound; cancelling still interrupts a worker waiting for demand.
- Compare response ids exactly, sharing the stdio side's id key, so 1.5 no
longer matches 1 and large ids no longer wrap.
- Drop other requests' responses, and require a JSON reply to be the
request's own response, so a listen stream ends only on its response.
- Decode streamed replies as strict UTF-8.
- Limit each streamed message in UTF-8 bytes, with SSE framing bounded on
its own.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The reason will be displayed to describe this comment to others. Learn more.
🔵 Needs a closer look
The tools cache refresh is not atomic, allowing concurrent calls to miss tool definitions and omit required Mcp-Param-* headers, causing spurious header-mismatch errors.
No open issues 🎉
1 previously missed issue
In code that hasn't changed since last review
Prevent tool calls from observing a partially refreshed cache
Refreshing the first tools page clears the shared cache before repopulating it. A concurrent callTool() subscription can run paramHeaders() in that interval, find no definition for an already-listed annotated tool, and send the call without its required Mcp-Param-* headers, causing a spurious header-mismatch error. Please publish the refreshed cache atomically (for example, replace an immutable snapshot), or synchronize both the update and lookup so calls cannot observe a partially refreshed page.
The reason will be displayed to describe this comment to others. Learn more.
🟢 Approval recommended
No active review issues are reported for the experimental MCP HTTP client addition.
No open issues 🎉
Review effort: Balanced
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Third R6b delivery for #1640, after the server side (#1664, #1665, #1666). This adds the MCP 2026-07-28 client, over HTTP. The SDK (
mcp-core2.2.0-M2) has no modern client, only the server, so this client is built on the SDK's modern schema and JSON-RPC types.API.
McpClientAdapter.setProtocolVersion(McpProtocol.MODERN_VERSION)switches the adapter to 2026-07-28.start()then runsserver/discover, and fails if the server doesn't list the revision.getModernClient()returns the newMcpModernClient. In this modegetSdkClient()throws, since the SDK'sMcpAsyncClientspeaks 2025-11-25 only. A stdio target fails atstart()withUnsupportedOperationException; the stdio client comes next.McpModernClientis reactive, on the SDK's modern types:discover,listTools,callTool,listResources,listResourceTemplates,readResource,listPrompts,getPrompt;listen(SubscriptionFilter), which returns aFluxof change notifications.setModernCapabilities(): the client capabilities sent with each request, none by default;setInputHandler();setMaxInputRounds(), default 8.McpInputHandleris a new functional interface.Behaviour.
_meta, as the revision requires. Ikanos's client omits the capabilities. Each request also sendsMCP-Protocol-Version,Mcp-MethodandMcp-Name, base64-encoded when needed.Mcp-Paramheaders: for tools this client has listed, the arguments theirx-mcp-headerannotations name are copied intoMcp-Param-*headers, numbers in plain notation. A call to an annotated tool the client hasn't listed fails with a header mismatch, as documented. These headers are worked out when the call is sent, solistTools().then(callTool(...))works.input_required: the input handler answers and the request is retried with its responses and the sealedrequestState, up to the round limit. Without a handler, past the limit, or when the handler returns an emptyMono, the result is returned to the caller.subscriptions/listen: no time limit, and its own pool, so a listen stream doesn't use the call limit's slots. The stream completes when the server ends it, for example on stop.MonoorFluxaborts the HTTP exchange, and the server cancels the handler.McpException. A non-JSON or empty error reply, or an event stream that ends without a response, fails with anIOException. Responses are matched to their request's id.Tests.
McpModernClientTestCasehas 12 tests againstMcpServerAdapterover real HTTP connectors:input_requiredreturned without a handler, retried by hand, and retried by a handler;Mcp-Parammirroring: refused before listing, served after it, worked out after a chained listing, and with a number like1.0E7;An independent review found, and this PR fixes:
Mcp-Paramheaders were worked out when the call was built, not when it was sent;mvn -pl org.restlet.ext.mcp verify: 85 tests, coverage and SpotBugs pass.🤖 Generated with Claude Code