Compare commits

...
Author SHA1 Message Date
Daniel Garnier-Moiroux
c7fef64f92 Rewrite keep-alive and session-sweeping tests with VirtualTimeScheduler
Some checks failed
Conformance Tests / Server Conformance (push) Has been cancelled
Conformance Tests / Client Conformance (push) Has been cancelled
Conformance Tests / Client Conformance-1 (push) Has been cancelled
Conformance Tests / Client Conformance-2 (push) Has been cancelled
Conformance Tests / Client Conformance-3 (push) Has been cancelled
Conformance Tests / Auth Conformance (push) Has been cancelled
Conformance Tests / Auth Conformance-1 (push) Has been cancelled
Conformance Tests / Auth Conformance-2 (push) Has been cancelled
Conformance Tests / Auth Conformance-3 (push) Has been cancelled
Conformance Tests / Auth Conformance-4 (push) Has been cancelled
Conformance Tests / Auth Conformance-5 (push) Has been cancelled
Conformance Tests / Auth Conformance-6 (push) Has been cancelled
Conformance Tests / Auth Conformance-7 (push) Has been cancelled
Conformance Tests / Auth Conformance-8 (push) Has been cancelled
Conformance Tests / Auth Conformance-9 (push) Has been cancelled
Conformance Tests / Auth Conformance-10 (push) Has been cancelled
Conformance Tests / Auth Conformance-11 (push) Has been cancelled
Conformance Tests / Auth Conformance-12 (push) Has been cancelled
Conformance Tests / Auth Conformance-13 (push) Has been cancelled
Publish Snapshot / Build branch (push) Has been cancelled
Signed-off-by: Daniel Garnier-Moiroux <git@garnier.wf>
2026-09-18 11:37:00 +02:00
Daniel Garnier-Moiroux
e3e0f5172a Introduce timeout for session sweeping
Signed-off-by: Daniel Garnier-Moiroux <git@garnier.wf>
2026-09-18 11:37:00 +02:00
Daniel Garnier-Moiroux
178fe219de Update author tags and copyright info
Signed-off-by: Daniel Garnier-Moiroux <git@garnier.wf>
2026-09-18 11:37:00 +02:00
Daniel Garnier-Moiroux
f77aa1afe2 Mention session-eviction in docs
Signed-off-by: Daniel Garnier-Moiroux <git@garnier.wf>
2026-09-18 11:37:00 +02:00
Daniel Garnier-Moiroux
caafc85575 Make session-sweeping non-blocking
Signed-off-by: Daniel Garnier-Moiroux <git@garnier.wf>
2026-09-18 11:37:00 +02:00
Daniel Garnier-Moiroux
9948eecbb7 Streamable HTTP server: keep alive on, 30 minutes by default
Signed-off-by: Daniel Garnier-Moiroux <git@garnier.wf>
2026-09-18 11:37:00 +02:00
Daniel Garnier-Moiroux
1178539050 Skip session activity tracking when sessionSweepInterval is null
Signed-off-by: Daniel Garnier-Moiroux <git@garnier.wf>
2026-09-18 11:37:00 +02:00
Daniel Garnier-Moiroux
65c0856518 Fix hanging disconnected client during POST
Signed-off-by: Daniel Garnier-Moiroux <git@garnier.wf>
2026-09-18 11:37:00 +02:00
Daniel Garnier-Moiroux
b4d7ff6428 Error pending requests on session close
Signed-off-by: Daniel Garnier-Moiroux <git@garnier.wf>
2026-09-18 11:37:00 +02:00
Daniel Garnier-Moiroux
50991f9b6c Remove inactive sessions with a "sweeper" mechanism
- Sessions are marked active when a client makes a request
- A session with an open stream is also considered active
- Inactive sessions are removed at a regular interval

Signed-off-by: Daniel Garnier-Moiroux <git@garnier.wf>
2026-09-18 11:37:00 +02:00
Daniel Garnier-Moiroux
04bc1e8710 Close POST-related streams in McpStreamableServerSession
Signed-off-by: Daniel Garnier-Moiroux <git@garnier.wf>
2026-09-18 11:37:00 +02:00
Daniel Garnier-Moiroux
ca633db18c KeepAliveScheduler only pings sessions with an open stream
Signed-off-by: Daniel Garnier-Moiroux <git@garnier.wf>
2026-09-18 11:37:00 +02:00
Daniel Garnier-Moiroux
7961dc58af KeepAliveScheduler: align initial delay with interval
Signed-off-by: Daniel Garnier-Moiroux <git@garnier.wf>
2026-09-18 11:37:00 +02:00
Daniel Garnier-Moiroux
183935bf80 Fix Automatic-Module-Name without hypens
Signed-off-by: Daniel Garnier-Moiroux <git@garnier.wf>
2026-09-17 11:50:06 +02:00
Daniel Garnier-Moiroux
4186ca13d1 Fix stream leak when resuming streams with LAST_EVENT_ID
Signed-off-by: Daniel Garnier-Moiroux <git@garnier.wf>
2026-09-08 15:28:17 +02:00
Neeraj Bhatt
5be2fa0688 feat: introduce ServerHttpHeaderValidator replacing ServerTransportSecurityValidator
Same feature set as ServerTransportSecurityValidator but uses a lazy
"header accessor" function to read header values instead of extracting
all headers into a map. Matches more closely what modern frameworks and
JDK APIs do.

Fixes #870

Signed-off-by: Daniel Garnier-Moiroux <git@garnier.wf>
2026-09-07 14:35:15 +02:00
Nicolas Quinquenel
2bb1481ec3
Make STDIO transport sendMessage thread-safe (#304)
Concurrent calls to `sendMessage` on the STDIO transports raced on the
unicast sink and sporadically failed with `FAIL_NON_SERIALIZED`, dropping
the message with `RuntimeException("Failed to enqueue message")`.

Switch `tryEmitNext` to `emitNext` with a `busyLooping` EmitFailureHandler
so concurrent emissions are serialized as recommended by the Reactor
maintainers. Applied on both `StdioServerTransportProvider` and
`StdioClientTransport`.

Co-authored-by: Nicolas Quinquenel <nicolas.quinquenel@sonarsource.com>
Signed-off-by: Dariusz Jędrzejczyk <dariusz.jedrzejczyk@broadcom.com>
2026-09-04 19:51:54 +02:00
Daniel Garnier-Moiroux
735a85190b Remove 2024-11-05 protocol from Streamable HTTP transport
Signed-off-by: Daniel Garnier-Moiroux <git@garnier.wf>
2026-09-04 18:55:37 +02:00
Daniel Garnier-Moiroux
39c225e46c Contributing: clarify PR submission
Signed-off-by: Daniel Garnier-Moiroux <git@garnier.wf>
2026-09-01 22:10:43 +02:00
Daniel Garnier-Moiroux
59316252c4 Fix integration tests for roots/list
There is a race condition where the GET /mcp SSE stream in Streamable
HTTP is established _after_ the server tries to send notifications. This
surfaces errors from the MCP client sending notifications and breaks
some integration tests.

We add a utility showing to ensure the stream is established before
sending server notifications.

Fixes #1114

Signed-off-by: Daniel Garnier-Moiroux <git@garnier.wf>
2026-09-01 18:09:51 +02:00
Daniel Garnier-Moiroux
24fbb119a9 Use single Tomcat instance per test class
- Instead of running one Tomcat per test method, run one per test
- Improves the testing time of Servlet-based tests

Signed-off-by: Daniel Garnier-Moiroux <git@garnier.wf>
2026-08-31 15:05:23 +02:00
Daniel Garnier-Moiroux
dc952a2856 Run STDIO tests with node instead of npx
Signed-off-by: Daniel Garnier-Moiroux <git@garnier.wf>
2026-08-31 15:05:23 +02:00
Daniel Garnier-Moiroux
2293cf70b5
Add filtering for MCP tool list (#1108)
Signed-off-by: Daniel Garnier-Moiroux <git@garnier.wf>
2026-08-31 10:19:49 +02:00
Daniel Garnier-Moiroux
a7bfddc361 ci: add workflow_dispatch trigger to CI workflow
Lets contributors manually run CI against a branch on their own fork
before opening a PR, without needing to open a draft PR or push an
empty commit just to get a CI run.

Fixes #1116

Signed-off-by: Daniel Garnier-Moiroux <git@garnier.wf>
2026-08-28 17:36:24 +02:00
Sylwester Lachiewicz
b31841eec0 docs(core): document String structuredContent parsing in JsonSchemaValidator 2026-08-27 09:33:21 +02:00
Daniel Garnier-Moiroux
bb0ca87da5 Clarify McpTransportContext javadoc
See #1075

Signed-off-by: Daniel Garnier-Moiroux <git@garnier.wf>
2026-08-26 16:08:11 +02:00
Daniel Garnier-Moiroux
e7b389f846 Update CONTRIBUTING.md with an AI agent policy
Signed-off-by: Daniel Garnier-Moiroux <git@garnier.wf>
2026-08-26 15:44:50 +02:00
Daniel Garnier-Moiroux
fb029c8c76 Revert "Clean up AbstractMcpClientServerIntegrationTests"
This reverts commit 439dc90d25.
Spring AI still uses the old API from these tests because they have
muliple clients.

Signed-off-by: Daniel Garnier-Moiroux <git@garnier.wf>
2026-08-24 16:44:08 +02:00
Daniel Garnier-Moiroux
c4dd4715ca Implement AbstractStatelessIntegrationTests with Servlet
Signed-off-by: Daniel Garnier-Moiroux <git@garnier.wf>
2026-08-24 16:35:01 +02:00
Daniel Garnier-Moiroux
305e4cbd03 Update VERSIONING and CHANGELOG
Signed-off-by: Daniel Garnier-Moiroux <git@garnier.wf>
2026-08-19 16:00:42 +02:00
Daniel Garnier-Moiroux
cceba3f695 Next development version
Signed-off-by: Daniel Garnier-Moiroux <git@garnier.wf>
2026-08-19 15:27:57 +02:00
63 changed files with 4274 additions and 864 deletions

View file

@ -2,6 +2,7 @@ name: CI
on:
pull_request: {}
workflow_dispatch: {}
jobs:
build:

View file

@ -13,9 +13,15 @@ Versioning follows [Semantic Versioning](https://semver.org/); see
| Line | Latest | Spec revision | Status |
|--------|--------|---------------|--------|
| 2.0.x | [2.0.0](https://github.com/modelcontextprotocol/java-sdk/releases/tag/v2.0.0) (2026-06-11) | 2025-11-25 | Active development |
| 1.1.x | [1.1.3](https://github.com/modelcontextprotocol/java-sdk/releases/tag/v1.1.3) (2026-05-21) | 2025-06-18 | Security patches only |
| 0.18.x | [0.18.3](https://github.com/modelcontextprotocol/java-sdk/releases/tag/v0.18.3) (2026-06-09) | 2025-06-18 | Security patches only |
| 2.0.x | [2.0.1](https://github.com/modelcontextprotocol/java-sdk/releases/tag/v2.0.1) (2026-08-19) | 2025-11-25 | Active development |
| 1.1.x | [1.1.4](https://github.com/modelcontextprotocol/java-sdk/releases/tag/v1.1.4) (2026-08-19) | 2025-06-18 | Security patches only |
| 0.18.x | [0.18.4](https://github.com/modelcontextprotocol/java-sdk/releases/tag/v0.18.4) (2026-08-19) | 2025-06-18 | Security patches only |
## 2.0.1 — 2026-08-19
- Bound STDIO and HTTP client/server reads to a configurable maximum size
Full notes: [v2.0.1 release](https://github.com/modelcontextprotocol/java-sdk/releases/tag/v2.0.1)
## 2.0.0 — 2026-06-11
@ -42,6 +48,8 @@ Full notes: [v2.0.0 release](https://github.com/modelcontextprotocol/java-sdk/re
## 1.x
- [1.1.4](https://github.com/modelcontextprotocol/java-sdk/releases/tag/v1.1.4) — 2026-08-19:
bound HTTP client/server reads (backport)
- [1.1.3](https://github.com/modelcontextprotocol/java-sdk/releases/tag/v1.1.3) /
[1.0.2](https://github.com/modelcontextprotocol/java-sdk/releases/tag/v1.0.2) — 2026-05-21:
SSE client transport message-endpoint validation (backports)
@ -54,6 +62,8 @@ Full notes: [v2.0.0 release](https://github.com/modelcontextprotocol/java-sdk/re
## 0.x
- [0.18.4](https://github.com/modelcontextprotocol/java-sdk/releases/tag/v0.18.4) — 2026-08-19:
bound HTTP client/server reads (backport)
- [0.18.3](https://github.com/modelcontextprotocol/java-sdk/releases/tag/v0.18.3) — 2026-06-09:
security fix for GHSA-hv2w-8mjj-jw22
- [0.18.0](https://github.com/modelcontextprotocol/java-sdk/releases/tag/v0.18.0) –

View file

@ -35,6 +35,12 @@ propose an enhancement. Bug reports should have a reproducer in the form of a co
sample or a repository attached that the maintainers or contributors can work with to
address the problem.
## AI agents
**We accept contributions created with the help of AI coding agents, but they must be carefully reviewed by a human who remains accountable for the quality of the contribution.**
These can be issues or pull requests. For issues, please ensure you describe your particular use-case, and not general considerations found by an AI agent.
Contributions submitted by GitHub accounts controlled by autonomous AI bots are forbidden.
## Making Changes
1. Create a new branch:
@ -63,15 +69,25 @@ git checkout -b feature/your-feature-name
## Submitting Changes
Please don't submit pull requests:
- With GitHub accounts managed by autonomous AI bots
- For already assigned issues (as the assignee is or plans to work on it)
When submitting changes:
1. For non-trivial changes, please clarify with the maintainers in an issue whether
you can contribute the change and the desired scope of the change.
2. For trivial changes (for example a couple of lines or documentation changes) there
is no need to open an issue first.
3. Push your changes to your fork.
4. Submit a pull request to the main repository.
5. Follow the pull request template.
6. Wait for review.
7. For any follow-up work, please add new commits instead of force-pushing. This will
3. Maintainers will triage issues and act on them. There is no such guarantee on pull requests.
PRs opened without being discussed in an issue MAY be ignored, and maintainers reserve the right
to close these PRs without justification.
4. Push your changes to your fork.
5. Submit a pull request to the main repository.
6. Follow the pull request template.
7. Wait for review.
8. For any follow-up work, please add new commits instead of force-pushing. This will
allow the reviewer to focus on incremental changes instead of having to restart the
review process.

View file

@ -8,7 +8,7 @@ The SDK tracks implementation of MCP spec components via GitHub Projects, with a
### 2.x — Stable Line (2025-11-25 spec)
The current stable release line is **2.x** ([2.0.0](https://github.com/modelcontextprotocol/java-sdk/releases/tag/v2.0.0), June 2026), implementing the [2025-11-25 MCP specification revision](https://modelcontextprotocol.io/specification/2025-11-25), including:
The current stable release line is **2.x** (latest: [2.0.1](https://github.com/modelcontextprotocol/java-sdk/releases/tag/v2.0.1), August 2026), implementing the [2025-11-25 MCP specification revision](https://modelcontextprotocol.io/specification/2025-11-25), including:
- **Spec-accurate schema**: enforced required fields with lenient wire deserialization, and a JSON compatibility foundation for forward/backward wire compatibility
- **Enhanced schemas**: JSON Schema 2020-12 validation of tool inputs and embedded schema documents (SEP-1613)

View file

@ -45,9 +45,9 @@ The following are **not** considered breaking:
| Line | Latest | Spec revision | Status |
|--------|--------|---------------|--------|
| 2.0.x | [2.0.0](https://github.com/modelcontextprotocol/java-sdk/releases/tag/v2.0.0) (2026-06-11) | 2025-11-25 | Active development |
| 1.1.x | [1.1.3](https://github.com/modelcontextprotocol/java-sdk/releases/tag/v1.1.3) (2026-05-21) | 2025-06-18 | Security patches only |
| 0.18.x | [0.18.3](https://github.com/modelcontextprotocol/java-sdk/releases/tag/v0.18.3) (2026-06-09) | 2025-06-18 | Security patches only |
| 2.0.x | [2.0.1](https://github.com/modelcontextprotocol/java-sdk/releases/tag/v2.0.1) (2026-08-19) | 2025-11-25 | Active development |
| 1.1.x | [1.1.4](https://github.com/modelcontextprotocol/java-sdk/releases/tag/v1.1.4) (2026-08-19) | 2025-06-18 | Security patches only |
| 0.18.x | [0.18.4](https://github.com/modelcontextprotocol/java-sdk/releases/tag/v0.18.4) (2026-08-19) | 2025-06-18 | Security patches only |
## Maven Coordinates

View file

@ -6,7 +6,7 @@
<parent>
<groupId>io.modelcontextprotocol.sdk</groupId>
<artifactId>conformance-tests</artifactId>
<version>2.0.1</version>
<version>2.1.0-SNAPSHOT</version>
</parent>
<artifactId>client-jdk-http-client</artifactId>
<packaging>jar</packaging>
@ -21,6 +21,7 @@
</scm>
<properties>
<module.name>io.modelcontextprotocol.sdk.conformance.client.jdk</module.name>
<maven.deploy.skip>true</maven.deploy.skip>
</properties>
@ -28,7 +29,7 @@
<dependency>
<groupId>io.modelcontextprotocol.sdk</groupId>
<artifactId>mcp</artifactId>
<version>2.0.1</version>
<version>2.1.0-SNAPSHOT</version>
</dependency>
<!-- Logging -->

View file

@ -6,7 +6,7 @@
<parent>
<groupId>io.modelcontextprotocol.sdk</groupId>
<artifactId>conformance-tests</artifactId>
<version>2.0.1</version>
<version>2.1.0-SNAPSHOT</version>
</parent>
<artifactId>client-spring-http-client</artifactId>
<packaging>jar</packaging>
@ -21,6 +21,7 @@
</scm>
<properties>
<module.name>io.modelcontextprotocol.sdk.conformance.client.spring</module.name>
<java.version>17</java.version>
<spring-boot.version>4.1.0</spring-boot.version>
<spring-ai.version>2.0.0</spring-ai.version>

View file

@ -6,7 +6,7 @@
<parent>
<groupId>io.modelcontextprotocol.sdk</groupId>
<artifactId>mcp-parent</artifactId>
<version>2.0.1</version>
<version>2.1.0-SNAPSHOT</version>
</parent>
<artifactId>conformance-tests</artifactId>
<packaging>pom</packaging>

View file

@ -6,7 +6,7 @@
<parent>
<groupId>io.modelcontextprotocol.sdk</groupId>
<artifactId>conformance-tests</artifactId>
<version>2.0.1</version>
<version>2.1.0-SNAPSHOT</version>
</parent>
<artifactId>server-servlet</artifactId>
<packaging>jar</packaging>
@ -21,6 +21,7 @@
</scm>
<properties>
<module.name>io.modelcontextprotocol.sdk.conformance.server.servlet</module.name>
<maven.deploy.skip>true</maven.deploy.skip>
</properties>
@ -28,7 +29,7 @@
<dependency>
<groupId>io.modelcontextprotocol.sdk</groupId>
<artifactId>mcp</artifactId>
<version>2.0.1</version>
<version>2.1.0-SNAPSHOT</version>
</dependency>
<dependency>

View file

@ -164,7 +164,10 @@ Key features:
- Efficient bidirectional HTTP communication
- Session management for multiple client connections
- Configurable keep-alive intervals
- Keep-alive pings on sessions with an open stream, enabled by default every 30 minutes
(`keepAliveInterval`, `null` to disable)
- Eviction of idle sessions — no open stream and no request for a full interval — every
30 minutes by default (`sessionSweepInterval`, `null` to keep sessions until deleted)
- Security validation support
- Graceful shutdown support
@ -441,6 +444,102 @@ var syncToolSpecification = SyncToolSpecification.builder()
`ImageContent.builder(data, mimeType)` and `AudioContent.builder(data, mimeType)` both take base64-encoded binary data. `EmbeddedResource.builder(resourceContents)` wraps either a `TextResourceContents` (for text data) or a `BlobResourceContents` (for base64-encoded binary data) — see [Reading Binary Resources](#reading-binary-resources) for the `BlobResourceContents` shape.
### Filtering the Tool Listing per Request
By default every registered tool is advertised to every caller. Over an HTTP transport you can
vary the `tools/list` response per request — to hide tools the caller is not authorized to see,
or to trim a large catalog down to a relevant subset — by registering one or more tool filters.
The filter receives the `McpTransportContext` extracted from the current request, so it can key
on HTTP headers, a token, a resolved principal, or anything else your
`contextExtractor` puts there.
=== "Sync"
```java
McpServer.sync(transportProvider)
.tools(publicTool, adminTool)
.addToolFilter((transportContext, tool) ->
!tool.name().startsWith("admin-") || isAdmin(transportContext))
.build();
```
=== "Async"
```java
McpServer.async(transportProvider)
.tools(publicTool, adminTool)
.addToolFilter((transportContext, tool) -> {
if (!tool.name().startsWith("admin-")) {
return Mono.just(true);
}
return isAdmin(transportContext); // Mono<Boolean>
})
.build();
```
The same `addToolFilter(...)` method is available on the stateless builders.
!!! warning "Hiding a tool does not make it unreachable"
The filter controls **advertisement only**. A hidden tool called by name still executes:
you MUST enforce permissions in the tool's call handler. Use the filter to control what a
caller is told about, not what they are allowed to do.
**Evaluation semantics**
- The filter is consulted on **every** listing request and never cached, so the same session may
legitimately see different results for two successive requests carrying different credentials.
- Registration order is preserved; only omissions happen.
- Returning `Mono.empty()` from an async filter omits the tool. An error fails the whole listing
request rather than silently hiding tools: a client cannot tell a filtered-down listing from a
partial one, and MCP has no way to signal "this listing was incomplete, retry".
- A filter that errors is logged server-side and reported to the client as an opaque
`-32603 Internal error` with no `data`. If you want the client to see a specific error, throw an
`McpError`, those are passed through.
- Filters accumulate as a boolean **AND**: a tool is listed only when every registered filter accepts it, so a
later `addToolFilter(...)` can never widen access. Evaluation follows registration order and
short-circuits on the first filter that hides a tool.
- `toolFilters(Consumer<List<...>>)` hands you the list of filters registered so far, so you can
inspect, reorder or clear them before building — useful when filters come from several places:
```java
McpServer.sync(transportProvider)
.addToolFilter(tenantFilter)
.toolFilters(filters -> filters.add(0, cheapDenyAllForAnonymousFilter))
.build();
```
- Tools are tested one at a time, so a filter that performs I/O per tool costs one round trip per
tool. Sync filters also run on a shared scheduler thread — not the request thread — unless
`immediateExecution(true)` is set, so thread-bound request state (Spring Security's
`SecurityContextHolder`, MDC, custom `ThreadLocal` holders) is **not visible** inside the filter.
For both reasons, resolve per-request state **once** in the transport's `contextExtractor`,
which does run on the request thread, and read only the extracted context in the filter:
```java
// transport builder: one authorization lookup, on the request thread,
// shared by every tool tested in this request
var transportProvider = HttpServletStreamableServerTransportProvider.builder()
.contextExtractor(request -> McpTransportContext.create(
Map.of("perms", introspect(request.getHeader("Authorization")))))
// ...
.build();
// server builder: the filter reads only the extracted context
McpServer.sync(transportProvider)
.addToolFilter((context, tool) ->
((Set<String>) context.get("perms")).contains(tool.name()))
.build();
```
- `notifications/tools/list_changed` is **not** filtered. It is a server-initiated broadcast with
no request in flight, so there is no context to evaluate. A client may be told something changed
when its own visible set did not; it gets the correct view on its next `tools/list`. Consider disabling
this notification entirely when using tool filters.
- With STDIO there is no per-request metadata, so the filter receives `McpTransportContext.EMPTY` and has nothing to key
on.
### Resource Specification
Specification of a resource with its handler function.

View file

@ -7,7 +7,7 @@
<parent>
<groupId>io.modelcontextprotocol.sdk</groupId>
<artifactId>mcp-parent</artifactId>
<version>2.0.1</version>
<version>2.1.0-SNAPSHOT</version>
</parent>
<artifactId>mcp-bom</artifactId>

View file

@ -6,7 +6,7 @@
<parent>
<groupId>io.modelcontextprotocol.sdk</groupId>
<artifactId>mcp-parent</artifactId>
<version>2.0.1</version>
<version>2.1.0-SNAPSHOT</version>
</parent>
<artifactId>mcp-core</artifactId>
<packaging>jar</packaging>
@ -20,6 +20,10 @@
<developerConnection>scm:git:ssh://git@github.com/modelcontextprotocol/java-sdk.git</developerConnection>
</scm>
<properties>
<module.name>io.modelcontextprotocol.sdk.mcp.core</module.name>
</properties>
<build>
<plugins>
<plugin>
@ -37,7 +41,7 @@
Bundle-Name: Bundle ${project.groupId} : ${project.artifactId}
Bundle-SymbolicName: ${project.groupId}.${project.artifactId}
Bundle-Version: ${project.version}
Automatic-Module-Name: ${project.groupId}.${project.artifactId}
Automatic-Module-Name: ${module.name}
Import-Package: jakarta.*;resolution:=optional, \
*;
Service-Component: OSGI-INF/io.modelcontextprotocol.json.McpJsonDefaults.xml

View file

@ -494,9 +494,10 @@ public interface McpClient {
* calling any client operation. This allows to extract thread-locals and hand
* them over to the underlying transport.
* <p>
* There is no direct equivalent in {@link AsyncSpec}. To achieve the same result,
* append {@code contextWrite(McpTransportContext.KEY, context)} to any
* {@link McpAsyncClient} call.
* The supplier is invoked at subscription time, on the calling thread, and the
* resulting context is visible to the transport for every leg of the operation,
* including connections opened on other threads, such as the SSE stream started
* during initialization.
* @param contextProvider A supplier to create a context
* @return This builder for method chaining
*/
@ -580,6 +581,21 @@ public interface McpClient {
* <li>Change notification handlers for tools, resources, and prompts
* <li>Custom message sampling logic
* </ul>
*
* <p>
* Unlike {@link SyncSpec}, this specification has no
* {@code transportContextProvider}. This is deliberate: in a reactive pipeline the
* caller owns the Reactor context, and whatever the transport needs, such as an
* {@link McpTransportContext}, can be written into it directly. Write it once where
* the reactive chain starts, rather than at every call site, and every
* {@link McpAsyncClient} call downstream inherits it, including the connections
* opened during initialization: <pre>{@code
* chain.filter(exchange)
* .contextWrite(ctx -> ctx.put(McpTransportContext.KEY, context));
* }</pre> To bridge thread-locals into the chain, use Reactor's context propagation
* support.
*
* @see SyncSpec#transportContextProvider(Supplier)
*/
class AsyncSpec {

View file

@ -763,8 +763,8 @@ public class HttpClientStreamableHttpTransport implements McpClientTransport {
private Duration connectTimeout = Duration.ofSeconds(10);
private List<String> supportedProtocolVersions = List.of(ProtocolVersions.MCP_2024_11_05,
ProtocolVersions.MCP_2025_03_26, ProtocolVersions.MCP_2025_06_18, ProtocolVersions.MCP_2025_11_25);
private List<String> supportedProtocolVersions = List.of(ProtocolVersions.MCP_2025_03_26,
ProtocolVersions.MCP_2025_06_18, ProtocolVersions.MCP_2025_11_25);
private McpHttpClientTransportAuthorizationErrorHandler authorizationErrorHandler = McpHttpClientTransportAuthorizationErrorHandler.NOOP;

View file

@ -258,16 +258,13 @@ public class StdioClientTransport implements McpClientTransport {
@Override
public Mono<Void> sendMessage(JSONRPCMessage message) {
if (this.outboundSink.tryEmitNext(message).isSuccess()) {
// TODO: essentially we could reschedule ourselves in some time and make
// another attempt with the already read data but pause reading until
// success
// In this approach we delegate the retry and the backpressure onto the
// caller. This might be enough for most cases.
try {
// busyLooping retries FAIL_NON_SERIALIZED under concurrent senders
this.outboundSink.emitNext(message, Sinks.EmitFailureHandler.busyLooping(Duration.ofMillis(100)));
return Mono.empty();
}
else {
return Mono.error(new RuntimeException("Failed to enqueue message"));
catch (Sinks.EmissionException e) {
return Mono.error(new RuntimeException("Failed to enqueue message", e));
}
}

View file

@ -7,18 +7,26 @@ package io.modelcontextprotocol.client.transport.customizer;
import java.net.URI;
import java.net.http.HttpRequest;
import io.modelcontextprotocol.client.McpClient.SyncSpec;
import io.modelcontextprotocol.common.McpTransportContext;
import org.reactivestreams.Publisher;
import reactor.core.publisher.Mono;
import reactor.core.scheduler.Schedulers;
import reactor.util.annotation.Nullable;
import io.modelcontextprotocol.common.McpTransportContext;
/**
* Customize {@link HttpRequest.Builder} before executing the request, in either SSE or
* Streamable HTTP transport.
* <p>
* When used in a non-blocking context, implementations MUST be non-blocking.
* <p>
* The {@link McpTransportContext} handed to {@code customize} is read from the Reactor
* context, under {@link McpTransportContext#KEY}, and is
* {@link McpTransportContext#EMPTY} when the caller wrote nothing there. Write it once
* where the reactive chain starts, with
* {@code contextWrite(ctx -> ctx.put(McpTransportContext.KEY, context))}, rather than at
* every call site. With a synchronous client, configure
* {@link SyncSpec#transportContextProvider} instead.
*
* @author Daniel Garnier-Moiroux
*/

View file

@ -11,13 +11,28 @@ import java.util.Map;
* Context associated with the transport layer. It allows to add transport-level metadata
* for use further down the line. Specifically, it can be beneficial to extract HTTP
* request metadata for use in MCP feature implementations.
* <p>
* The context travels in the Reactor context, under {@link #KEY}. On the server side, the
* transports populate it from the incoming request. On the client side, writing it is the
* caller's responsibility:
* <ul>
* <li>with a synchronous client, configure
* {@code McpClient.SyncSpec#transportContextProvider(Supplier)}, which is invoked on the
* calling thread before every operation;
* <li>with an asynchronous client, write it into the Reactor context once, where the
* reactive chain starts, using
* {@code contextWrite(ctx -> ctx.put(McpTransportContext.KEY, context))}. Every client
* call downstream inherits it, so there is no need to repeat it at each call site.
* </ul>
*
* @author Dariusz Jędrzejczyk
*/
public interface McpTransportContext {
/**
* Key for use in Reactor Context to transport the context to user land.
* Key for use in Reactor Context to transport the context to user land. Write the
* context under this key to make it visible to the transport, for example
* {@code contextWrite(ctx -> ctx.put(McpTransportContext.KEY, context))}.
*/
String KEY = "MCP_TRANSPORT_CONTEXT";

View file

@ -57,6 +57,11 @@ public interface JsonSchemaValidator {
/**
* Validates the structured content against the provided JSON schema.
* <p>
* If {@code structuredContent} is a {@link String}, it is treated as a serialized
* JSON document and parsed before validation; quote embedded strings accordingly (for
* example {@code "\"red\""} for the JSON string value {@code red}). Any other type is
* converted to its JSON representation directly.
* @param schema The JSON schema to validate against.
* @param structuredContent The structured content to validate.
* @return A ValidationResponse indicating whether the validation was successful or

View file

@ -0,0 +1,81 @@
/*
* Copyright 2026-2026 the original author or authors.
*/
package io.modelcontextprotocol.server;
import java.util.List;
import io.modelcontextprotocol.common.McpTransportContext;
import io.modelcontextprotocol.spec.McpSchema.Tool;
import io.modelcontextprotocol.util.Assert;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.core.scheduler.Schedulers;
/**
* Decide per request whether a primitive is advertised in the corresponding listing, such
* as {@code tools/list}.
* <p>
* A primitive hidden by this filter is omitted from listings ONLY. It remains reachable
* through its own endpoint: a hidden tool called by name still executes. Permissions MUST
* be enforced in the primitive's handler.
*
* @author Daniel Garnier-Moiroux
* @see McpSyncListFilter
* @see McpTransportContextExtractor
*/
@FunctionalInterface
public interface McpAsyncListFilter<T> {
/**
* Whether the given primitive is visible to the caller of the current request.
* @param transportContext transport context containing, for example, HTTP headers or
* a resolved principal. Should never be {@code null}, but may
* {@link McpTransportContext#EMPTY} for transports that carry no per-request
* metadata, such as STDIO.
* @param primitive the primitive that is a candidate for inclusion in the listing,
* such as {@link Tool}.
* @return a publisher emitting {@code true} to include the primitive in the listing,
* {@code false} to omit it. Completing empty omits the primitive; erroring fails the
* listing request.
*/
Mono<Boolean> isVisible(McpTransportContext transportContext, T primitive);
/**
* Convert a potentially blocking, synchronous filter into an asynchronous one,
* offloading it to prevent accidental blocking of a non-blocking transport.
* @param filter the synchronous filter. MUST NOT be null.
* @param immediateExecution When true, do not offload work asynchronously. Do NOT set
* to true when the filter performs blocking I/O.
*/
static <T> McpAsyncListFilter<T> fromSync(McpSyncListFilter<T> filter, boolean immediateExecution) {
Assert.notNull(filter, "filter must not be null");
return (transportContext, primitive) -> {
var visible = Mono.fromCallable(() -> filter.isVisible(transportContext, primitive));
return immediateExecution ? visible : visible.subscribeOn(Schedulers.boundedElastic());
};
}
/**
* Combine multiple filters in a single AND-filter. An empty or {@code null} list
* makes everything visible, keeping listing on a single code path when nothing is
* configured.
* @param filters the filters to combine. May be {@code null} or empty, but MUST NOT
* contain {@code null} elements.
*/
static <T> McpAsyncListFilter<T> and(List<McpAsyncListFilter<T>> filters) {
if (filters == null || filters.isEmpty()) {
return (transportContext, primitive) -> Mono.just(Boolean.TRUE);
}
Assert.noNullElements(filters, "filters must not contain null elements");
if (filters.size() == 1) {
return filters.get(0);
}
List<McpAsyncListFilter<T>> snapshot = List.copyOf(filters);
return (transportContext, primitive) -> Flux.fromIterable(snapshot)
.concatMap(filter -> filter.isVisible(transportContext, primitive).defaultIfEmpty(Boolean.FALSE))
.all(Boolean.TRUE::equals);
}
}

View file

@ -118,6 +118,8 @@ public class McpAsyncServer {
private final ConcurrentHashMap<String, Set<String>> resourceSubscriptions = new ConcurrentHashMap<>();
private final McpAsyncListFilter<McpSchema.Tool> toolFilter;
private List<String> protocolVersions;
private McpUriTemplateManagerFactory uriTemplateManagerFactory = new DefaultMcpUriTemplateManagerFactory();
@ -146,6 +148,7 @@ public class McpAsyncServer {
this.uriTemplateManagerFactory = uriTemplateManagerFactory;
this.jsonSchemaValidator = jsonSchemaValidator;
this.validateToolInputs = validateToolInputs;
this.toolFilter = McpAsyncListFilter.and(features.toolFilters());
Map<String, McpRequestHandler<?>> requestHandlers = prepareRequestHandlers();
Map<String, McpNotificationHandler> notificationHandlers = prepareNotificationHandlers(features);
@ -177,6 +180,7 @@ public class McpAsyncServer {
this.uriTemplateManagerFactory = uriTemplateManagerFactory;
this.jsonSchemaValidator = jsonSchemaValidator;
this.validateToolInputs = validateToolInputs;
this.toolFilter = McpAsyncListFilter.and(features.toolFilters());
Map<String, McpRequestHandler<?>> requestHandlers = prepareRequestHandlers();
Map<String, McpNotificationHandler> notificationHandlers = prepareNotificationHandlers(features);
@ -537,12 +541,32 @@ public class McpAsyncServer {
private McpRequestHandler<McpSchema.ListToolsResult> toolsListRequestHandler() {
return (exchange, params) -> {
List<Tool> tools = this.tools.stream().map(McpServerFeatures.AsyncToolSpecification::tool).toList();
return Mono.just(McpSchema.ListToolsResult.builder(tools).build());
// TODO: Implement pagination. Cursors must be computed over the filtered
// view, otherwise page offsets leak the number of hidden tools.
return Flux.fromIterable(this.tools)
.map(McpServerFeatures.AsyncToolSpecification::tool)
.filterWhen(tool -> this.toolFilter.isVisible(exchange.transportContext(), tool)
.onErrorResume(error -> opaqueListFilterError(tool, error)))
.collectList()
.map(tools -> McpSchema.ListToolsResult.builder(tools).build());
};
}
/**
* Report a list filter failure to the client as an opaque {@code -32603} error, so
* that filter internals such as identity provider hostnames or the reason a principal
* was rejected never leave the server. The actual cause is logged instead. An
* {@link McpError} is deliberate on the filter's part and passes through untouched.
*/
private static Mono<Boolean> opaqueListFilterError(Tool tool, Throwable error) {
if (error instanceof McpError mcpError && mcpError.getJsonRpcError() != null) {
logger.debug("Tool list filter failed for tool '{}' with an explicit MCP error", tool.name(), error);
return Mono.error(mcpError);
}
logger.error("Tool list filter failed for tool '{}', failing the tools/list request", tool.name(), error);
return Mono.error(McpError.builder(ErrorCodes.INTERNAL_ERROR).message("Internal error").build());
}
private McpRequestHandler<CallToolResult> toolsCallRequestHandler() {
return (exchange, params) -> {
McpSchema.CallToolRequest callToolRequest = jsonMapper.convertValue(params,

Some files were not shown because too many files have changed in this diff Show more