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:
|
on:
|
||||||
pull_request: {}
|
pull_request: {}
|
||||||
|
workflow_dispatch: {}
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
|
|
|
||||||
16
CHANGELOG.md
16
CHANGELOG.md
|
|
@ -13,9 +13,15 @@ Versioning follows [Semantic Versioning](https://semver.org/); see
|
||||||
|
|
||||||
| Line | Latest | Spec revision | Status |
|
| 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 |
|
| 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.3](https://github.com/modelcontextprotocol/java-sdk/releases/tag/v1.1.3) (2026-05-21) | 2025-06-18 | Security patches only |
|
| 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.3](https://github.com/modelcontextprotocol/java-sdk/releases/tag/v0.18.3) (2026-06-09) | 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
|
## 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.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.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:
|
[1.0.2](https://github.com/modelcontextprotocol/java-sdk/releases/tag/v1.0.2) — 2026-05-21:
|
||||||
SSE client transport message-endpoint validation (backports)
|
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.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:
|
- [0.18.3](https://github.com/modelcontextprotocol/java-sdk/releases/tag/v0.18.3) — 2026-06-09:
|
||||||
security fix for GHSA-hv2w-8mjj-jw22
|
security fix for GHSA-hv2w-8mjj-jw22
|
||||||
- [0.18.0](https://github.com/modelcontextprotocol/java-sdk/releases/tag/v0.18.0) –
|
- [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
|
sample or a repository attached that the maintainers or contributors can work with to
|
||||||
address the problem.
|
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
|
## Making Changes
|
||||||
|
|
||||||
1. Create a new branch:
|
1. Create a new branch:
|
||||||
|
|
@ -63,15 +69,25 @@ git checkout -b feature/your-feature-name
|
||||||
|
|
||||||
## Submitting Changes
|
## 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
|
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.
|
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
|
2. For trivial changes (for example a couple of lines or documentation changes) there
|
||||||
is no need to open an issue first.
|
is no need to open an issue first.
|
||||||
3. Push your changes to your fork.
|
3. Maintainers will triage issues and act on them. There is no such guarantee on pull requests.
|
||||||
4. Submit a pull request to the main repository.
|
PRs opened without being discussed in an issue MAY be ignored, and maintainers reserve the right
|
||||||
5. Follow the pull request template.
|
to close these PRs without justification.
|
||||||
6. Wait for review.
|
4. Push your changes to your fork.
|
||||||
7. For any follow-up work, please add new commits instead of force-pushing. This will
|
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
|
allow the reviewer to focus on incremental changes instead of having to restart the
|
||||||
review process.
|
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)
|
### 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
|
- **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)
|
- **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 |
|
| 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 |
|
| 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.3](https://github.com/modelcontextprotocol/java-sdk/releases/tag/v1.1.3) (2026-05-21) | 2025-06-18 | Security patches only |
|
| 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.3](https://github.com/modelcontextprotocol/java-sdk/releases/tag/v0.18.3) (2026-06-09) | 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
|
## Maven Coordinates
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@
|
||||||
<parent>
|
<parent>
|
||||||
<groupId>io.modelcontextprotocol.sdk</groupId>
|
<groupId>io.modelcontextprotocol.sdk</groupId>
|
||||||
<artifactId>conformance-tests</artifactId>
|
<artifactId>conformance-tests</artifactId>
|
||||||
<version>2.0.1</version>
|
<version>2.1.0-SNAPSHOT</version>
|
||||||
</parent>
|
</parent>
|
||||||
<artifactId>client-jdk-http-client</artifactId>
|
<artifactId>client-jdk-http-client</artifactId>
|
||||||
<packaging>jar</packaging>
|
<packaging>jar</packaging>
|
||||||
|
|
@ -21,6 +21,7 @@
|
||||||
</scm>
|
</scm>
|
||||||
|
|
||||||
<properties>
|
<properties>
|
||||||
|
<module.name>io.modelcontextprotocol.sdk.conformance.client.jdk</module.name>
|
||||||
<maven.deploy.skip>true</maven.deploy.skip>
|
<maven.deploy.skip>true</maven.deploy.skip>
|
||||||
</properties>
|
</properties>
|
||||||
|
|
||||||
|
|
@ -28,7 +29,7 @@
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>io.modelcontextprotocol.sdk</groupId>
|
<groupId>io.modelcontextprotocol.sdk</groupId>
|
||||||
<artifactId>mcp</artifactId>
|
<artifactId>mcp</artifactId>
|
||||||
<version>2.0.1</version>
|
<version>2.1.0-SNAPSHOT</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<!-- Logging -->
|
<!-- Logging -->
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@
|
||||||
<parent>
|
<parent>
|
||||||
<groupId>io.modelcontextprotocol.sdk</groupId>
|
<groupId>io.modelcontextprotocol.sdk</groupId>
|
||||||
<artifactId>conformance-tests</artifactId>
|
<artifactId>conformance-tests</artifactId>
|
||||||
<version>2.0.1</version>
|
<version>2.1.0-SNAPSHOT</version>
|
||||||
</parent>
|
</parent>
|
||||||
<artifactId>client-spring-http-client</artifactId>
|
<artifactId>client-spring-http-client</artifactId>
|
||||||
<packaging>jar</packaging>
|
<packaging>jar</packaging>
|
||||||
|
|
@ -21,6 +21,7 @@
|
||||||
</scm>
|
</scm>
|
||||||
|
|
||||||
<properties>
|
<properties>
|
||||||
|
<module.name>io.modelcontextprotocol.sdk.conformance.client.spring</module.name>
|
||||||
<java.version>17</java.version>
|
<java.version>17</java.version>
|
||||||
<spring-boot.version>4.1.0</spring-boot.version>
|
<spring-boot.version>4.1.0</spring-boot.version>
|
||||||
<spring-ai.version>2.0.0</spring-ai.version>
|
<spring-ai.version>2.0.0</spring-ai.version>
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@
|
||||||
<parent>
|
<parent>
|
||||||
<groupId>io.modelcontextprotocol.sdk</groupId>
|
<groupId>io.modelcontextprotocol.sdk</groupId>
|
||||||
<artifactId>mcp-parent</artifactId>
|
<artifactId>mcp-parent</artifactId>
|
||||||
<version>2.0.1</version>
|
<version>2.1.0-SNAPSHOT</version>
|
||||||
</parent>
|
</parent>
|
||||||
<artifactId>conformance-tests</artifactId>
|
<artifactId>conformance-tests</artifactId>
|
||||||
<packaging>pom</packaging>
|
<packaging>pom</packaging>
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@
|
||||||
<parent>
|
<parent>
|
||||||
<groupId>io.modelcontextprotocol.sdk</groupId>
|
<groupId>io.modelcontextprotocol.sdk</groupId>
|
||||||
<artifactId>conformance-tests</artifactId>
|
<artifactId>conformance-tests</artifactId>
|
||||||
<version>2.0.1</version>
|
<version>2.1.0-SNAPSHOT</version>
|
||||||
</parent>
|
</parent>
|
||||||
<artifactId>server-servlet</artifactId>
|
<artifactId>server-servlet</artifactId>
|
||||||
<packaging>jar</packaging>
|
<packaging>jar</packaging>
|
||||||
|
|
@ -21,6 +21,7 @@
|
||||||
</scm>
|
</scm>
|
||||||
|
|
||||||
<properties>
|
<properties>
|
||||||
|
<module.name>io.modelcontextprotocol.sdk.conformance.server.servlet</module.name>
|
||||||
<maven.deploy.skip>true</maven.deploy.skip>
|
<maven.deploy.skip>true</maven.deploy.skip>
|
||||||
</properties>
|
</properties>
|
||||||
|
|
||||||
|
|
@ -28,7 +29,7 @@
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>io.modelcontextprotocol.sdk</groupId>
|
<groupId>io.modelcontextprotocol.sdk</groupId>
|
||||||
<artifactId>mcp</artifactId>
|
<artifactId>mcp</artifactId>
|
||||||
<version>2.0.1</version>
|
<version>2.1.0-SNAPSHOT</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
|
|
|
||||||
101
docs/server.md
101
docs/server.md
|
|
@ -164,7 +164,10 @@ Key features:
|
||||||
|
|
||||||
- Efficient bidirectional HTTP communication
|
- Efficient bidirectional HTTP communication
|
||||||
- Session management for multiple client connections
|
- 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
|
- Security validation support
|
||||||
- Graceful shutdown 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.
|
`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
|
### Resource Specification
|
||||||
|
|
||||||
Specification of a resource with its handler function.
|
Specification of a resource with its handler function.
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@
|
||||||
<parent>
|
<parent>
|
||||||
<groupId>io.modelcontextprotocol.sdk</groupId>
|
<groupId>io.modelcontextprotocol.sdk</groupId>
|
||||||
<artifactId>mcp-parent</artifactId>
|
<artifactId>mcp-parent</artifactId>
|
||||||
<version>2.0.1</version>
|
<version>2.1.0-SNAPSHOT</version>
|
||||||
</parent>
|
</parent>
|
||||||
|
|
||||||
<artifactId>mcp-bom</artifactId>
|
<artifactId>mcp-bom</artifactId>
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@
|
||||||
<parent>
|
<parent>
|
||||||
<groupId>io.modelcontextprotocol.sdk</groupId>
|
<groupId>io.modelcontextprotocol.sdk</groupId>
|
||||||
<artifactId>mcp-parent</artifactId>
|
<artifactId>mcp-parent</artifactId>
|
||||||
<version>2.0.1</version>
|
<version>2.1.0-SNAPSHOT</version>
|
||||||
</parent>
|
</parent>
|
||||||
<artifactId>mcp-core</artifactId>
|
<artifactId>mcp-core</artifactId>
|
||||||
<packaging>jar</packaging>
|
<packaging>jar</packaging>
|
||||||
|
|
@ -20,6 +20,10 @@
|
||||||
<developerConnection>scm:git:ssh://git@github.com/modelcontextprotocol/java-sdk.git</developerConnection>
|
<developerConnection>scm:git:ssh://git@github.com/modelcontextprotocol/java-sdk.git</developerConnection>
|
||||||
</scm>
|
</scm>
|
||||||
|
|
||||||
|
<properties>
|
||||||
|
<module.name>io.modelcontextprotocol.sdk.mcp.core</module.name>
|
||||||
|
</properties>
|
||||||
|
|
||||||
<build>
|
<build>
|
||||||
<plugins>
|
<plugins>
|
||||||
<plugin>
|
<plugin>
|
||||||
|
|
@ -37,7 +41,7 @@
|
||||||
Bundle-Name: Bundle ${project.groupId} : ${project.artifactId}
|
Bundle-Name: Bundle ${project.groupId} : ${project.artifactId}
|
||||||
Bundle-SymbolicName: ${project.groupId}.${project.artifactId}
|
Bundle-SymbolicName: ${project.groupId}.${project.artifactId}
|
||||||
Bundle-Version: ${project.version}
|
Bundle-Version: ${project.version}
|
||||||
Automatic-Module-Name: ${project.groupId}.${project.artifactId}
|
Automatic-Module-Name: ${module.name}
|
||||||
Import-Package: jakarta.*;resolution:=optional, \
|
Import-Package: jakarta.*;resolution:=optional, \
|
||||||
*;
|
*;
|
||||||
Service-Component: OSGI-INF/io.modelcontextprotocol.json.McpJsonDefaults.xml
|
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
|
* calling any client operation. This allows to extract thread-locals and hand
|
||||||
* them over to the underlying transport.
|
* them over to the underlying transport.
|
||||||
* <p>
|
* <p>
|
||||||
* There is no direct equivalent in {@link AsyncSpec}. To achieve the same result,
|
* The supplier is invoked at subscription time, on the calling thread, and the
|
||||||
* append {@code contextWrite(McpTransportContext.KEY, context)} to any
|
* resulting context is visible to the transport for every leg of the operation,
|
||||||
* {@link McpAsyncClient} call.
|
* including connections opened on other threads, such as the SSE stream started
|
||||||
|
* during initialization.
|
||||||
* @param contextProvider A supplier to create a context
|
* @param contextProvider A supplier to create a context
|
||||||
* @return This builder for method chaining
|
* @return This builder for method chaining
|
||||||
*/
|
*/
|
||||||
|
|
@ -580,6 +581,21 @@ public interface McpClient {
|
||||||
* <li>Change notification handlers for tools, resources, and prompts
|
* <li>Change notification handlers for tools, resources, and prompts
|
||||||
* <li>Custom message sampling logic
|
* <li>Custom message sampling logic
|
||||||
* </ul>
|
* </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 {
|
class AsyncSpec {
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -763,8 +763,8 @@ public class HttpClientStreamableHttpTransport implements McpClientTransport {
|
||||||
|
|
||||||
private Duration connectTimeout = Duration.ofSeconds(10);
|
private Duration connectTimeout = Duration.ofSeconds(10);
|
||||||
|
|
||||||
private List<String> supportedProtocolVersions = List.of(ProtocolVersions.MCP_2024_11_05,
|
private List<String> supportedProtocolVersions = List.of(ProtocolVersions.MCP_2025_03_26,
|
||||||
ProtocolVersions.MCP_2025_03_26, ProtocolVersions.MCP_2025_06_18, ProtocolVersions.MCP_2025_11_25);
|
ProtocolVersions.MCP_2025_06_18, ProtocolVersions.MCP_2025_11_25);
|
||||||
|
|
||||||
private McpHttpClientTransportAuthorizationErrorHandler authorizationErrorHandler = McpHttpClientTransportAuthorizationErrorHandler.NOOP;
|
private McpHttpClientTransportAuthorizationErrorHandler authorizationErrorHandler = McpHttpClientTransportAuthorizationErrorHandler.NOOP;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -258,16 +258,13 @@ public class StdioClientTransport implements McpClientTransport {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Mono<Void> sendMessage(JSONRPCMessage message) {
|
public Mono<Void> sendMessage(JSONRPCMessage message) {
|
||||||
if (this.outboundSink.tryEmitNext(message).isSuccess()) {
|
try {
|
||||||
// TODO: essentially we could reschedule ourselves in some time and make
|
// busyLooping retries FAIL_NON_SERIALIZED under concurrent senders
|
||||||
// another attempt with the already read data but pause reading until
|
this.outboundSink.emitNext(message, Sinks.EmitFailureHandler.busyLooping(Duration.ofMillis(100)));
|
||||||
// success
|
|
||||||
// In this approach we delegate the retry and the backpressure onto the
|
|
||||||
// caller. This might be enough for most cases.
|
|
||||||
return Mono.empty();
|
return Mono.empty();
|
||||||
}
|
}
|
||||||
else {
|
catch (Sinks.EmissionException e) {
|
||||||
return Mono.error(new RuntimeException("Failed to enqueue message"));
|
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.URI;
|
||||||
import java.net.http.HttpRequest;
|
import java.net.http.HttpRequest;
|
||||||
|
|
||||||
|
import io.modelcontextprotocol.client.McpClient.SyncSpec;
|
||||||
|
import io.modelcontextprotocol.common.McpTransportContext;
|
||||||
import org.reactivestreams.Publisher;
|
import org.reactivestreams.Publisher;
|
||||||
import reactor.core.publisher.Mono;
|
import reactor.core.publisher.Mono;
|
||||||
import reactor.core.scheduler.Schedulers;
|
import reactor.core.scheduler.Schedulers;
|
||||||
import reactor.util.annotation.Nullable;
|
import reactor.util.annotation.Nullable;
|
||||||
|
|
||||||
import io.modelcontextprotocol.common.McpTransportContext;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Customize {@link HttpRequest.Builder} before executing the request, in either SSE or
|
* Customize {@link HttpRequest.Builder} before executing the request, in either SSE or
|
||||||
* Streamable HTTP transport.
|
* Streamable HTTP transport.
|
||||||
* <p>
|
* <p>
|
||||||
* When used in a non-blocking context, implementations MUST be non-blocking.
|
* 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
|
* @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
|
* 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
|
* for use further down the line. Specifically, it can be beneficial to extract HTTP
|
||||||
* request metadata for use in MCP feature implementations.
|
* 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
|
* @author Dariusz Jędrzejczyk
|
||||||
*/
|
*/
|
||||||
public interface McpTransportContext {
|
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";
|
String KEY = "MCP_TRANSPORT_CONTEXT";
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -57,6 +57,11 @@ public interface JsonSchemaValidator {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Validates the structured content against the provided JSON schema.
|
* 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 schema The JSON schema to validate against.
|
||||||
* @param structuredContent The structured content to validate.
|
* @param structuredContent The structured content to validate.
|
||||||
* @return A ValidationResponse indicating whether the validation was successful or
|
* @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 ConcurrentHashMap<String, Set<String>> resourceSubscriptions = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
|
private final McpAsyncListFilter<McpSchema.Tool> toolFilter;
|
||||||
|
|
||||||
private List<String> protocolVersions;
|
private List<String> protocolVersions;
|
||||||
|
|
||||||
private McpUriTemplateManagerFactory uriTemplateManagerFactory = new DefaultMcpUriTemplateManagerFactory();
|
private McpUriTemplateManagerFactory uriTemplateManagerFactory = new DefaultMcpUriTemplateManagerFactory();
|
||||||
|
|
@ -146,6 +148,7 @@ public class McpAsyncServer {
|
||||||
this.uriTemplateManagerFactory = uriTemplateManagerFactory;
|
this.uriTemplateManagerFactory = uriTemplateManagerFactory;
|
||||||
this.jsonSchemaValidator = jsonSchemaValidator;
|
this.jsonSchemaValidator = jsonSchemaValidator;
|
||||||
this.validateToolInputs = validateToolInputs;
|
this.validateToolInputs = validateToolInputs;
|
||||||
|
this.toolFilter = McpAsyncListFilter.and(features.toolFilters());
|
||||||
|
|
||||||
Map<String, McpRequestHandler<?>> requestHandlers = prepareRequestHandlers();
|
Map<String, McpRequestHandler<?>> requestHandlers = prepareRequestHandlers();
|
||||||
Map<String, McpNotificationHandler> notificationHandlers = prepareNotificationHandlers(features);
|
Map<String, McpNotificationHandler> notificationHandlers = prepareNotificationHandlers(features);
|
||||||
|
|
@ -177,6 +180,7 @@ public class McpAsyncServer {
|
||||||
this.uriTemplateManagerFactory = uriTemplateManagerFactory;
|
this.uriTemplateManagerFactory = uriTemplateManagerFactory;
|
||||||
this.jsonSchemaValidator = jsonSchemaValidator;
|
this.jsonSchemaValidator = jsonSchemaValidator;
|
||||||
this.validateToolInputs = validateToolInputs;
|
this.validateToolInputs = validateToolInputs;
|
||||||
|
this.toolFilter = McpAsyncListFilter.and(features.toolFilters());
|
||||||
|
|
||||||
Map<String, McpRequestHandler<?>> requestHandlers = prepareRequestHandlers();
|
Map<String, McpRequestHandler<?>> requestHandlers = prepareRequestHandlers();
|
||||||
Map<String, McpNotificationHandler> notificationHandlers = prepareNotificationHandlers(features);
|
Map<String, McpNotificationHandler> notificationHandlers = prepareNotificationHandlers(features);
|
||||||
|
|
@ -537,12 +541,32 @@ public class McpAsyncServer {
|
||||||
|
|
||||||
private McpRequestHandler<McpSchema.ListToolsResult> toolsListRequestHandler() {
|
private McpRequestHandler<McpSchema.ListToolsResult> toolsListRequestHandler() {
|
||||||
return (exchange, params) -> {
|
return (exchange, params) -> {
|
||||||
List<Tool> tools = this.tools.stream().map(McpServerFeatures.AsyncToolSpecification::tool).toList();
|
// TODO: Implement pagination. Cursors must be computed over the filtered
|
||||||
|
// view, otherwise page offsets leak the number of hidden tools.
|
||||||
return Mono.just(McpSchema.ListToolsResult.builder(tools).build());
|
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() {
|
private McpRequestHandler<CallToolResult> toolsCallRequestHandler() {
|
||||||
return (exchange, params) -> {
|
return (exchange, params) -> {
|
||||||
McpSchema.CallToolRequest callToolRequest = jsonMapper.convertValue(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