Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c7fef64f92 | ||
|
|
e3e0f5172a | ||
|
|
178fe219de | ||
|
|
f77aa1afe2 | ||
|
|
caafc85575 | ||
|
|
9948eecbb7 | ||
|
|
1178539050 | ||
|
|
65c0856518 | ||
|
|
b4d7ff6428 | ||
|
|
50991f9b6c | ||
|
|
04bc1e8710 | ||
|
|
ca633db18c | ||
|
|
7961dc58af | ||
|
|
183935bf80 | ||
|
|
4186ca13d1 | ||
|
|
5be2fa0688 | ||
|
|
2bb1481ec3 |
||
|
|
735a85190b | ||
|
|
39c225e46c | ||
|
|
59316252c4 | ||
|
|
24fbb119a9 | ||
|
|
dc952a2856 | ||
|
|
2293cf70b5 |
||
|
|
a7bfddc361 | ||
|
|
b31841eec0 | ||
|
|
bb0ca87da5 | ||
|
|
e7b389f846 | ||
|
|
fb029c8c76 | ||
|
|
c4dd4715ca | ||
|
|
305e4cbd03 | ||
|
|
cceba3f695 |
63 changed files with 4274 additions and 864 deletions
1
.github/workflows/ci.yml
vendored
1
.github/workflows/ci.yml
vendored
|
|
@ -2,6 +2,7 @@ name: CI
|
|||
|
||||
on:
|
||||
pull_request: {}
|
||||
workflow_dispatch: {}
|
||||
|
||||
jobs:
|
||||
build:
|
||||
|
|
|
|||
16
CHANGELOG.md
16
CHANGELOG.md
|
|
@ -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) –
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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 -->
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
101
docs/server.md
101
docs/server.md
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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
Loading…
Reference in a new issue