Compare commits

..
63 changed files with 864 additions and 4274 deletions

View file

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

View file

@ -13,15 +13,9 @@ Versioning follows [Semantic Versioning](https://semver.org/); see
| Line | Latest | Spec revision | Status |
|--------|--------|---------------|--------|
| 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.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.0 — 2026-06-11
@ -48,8 +42,6 @@ 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)
@ -62,8 +54,6 @@ 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,12 +35,6 @@ 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:
@ -69,25 +63,15 @@ 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. 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
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
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** (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:
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:
- **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.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.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 |
## Maven Coordinates

View file

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

View file

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

View file

@ -164,10 +164,7 @@ Key features:
- Efficient bidirectional HTTP communication
- Session management for multiple client connections
- 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)
- Configurable keep-alive intervals
- Security validation support
- Graceful shutdown support
@ -444,102 +441,6 @@ 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.1.0-SNAPSHOT</version>
<version>2.0.1</version>
</parent>
<artifactId>mcp-bom</artifactId>

View file

@ -6,7 +6,7 @@
<parent>
<groupId>io.modelcontextprotocol.sdk</groupId>
<artifactId>mcp-parent</artifactId>
<version>2.1.0-SNAPSHOT</version>
<version>2.0.1</version>
</parent>
<artifactId>mcp-core</artifactId>
<packaging>jar</packaging>
@ -20,10 +20,6 @@
<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>
@ -41,7 +37,7 @@
Bundle-Name: Bundle ${project.groupId} : ${project.artifactId}
Bundle-SymbolicName: ${project.groupId}.${project.artifactId}
Bundle-Version: ${project.version}
Automatic-Module-Name: ${module.name}
Automatic-Module-Name: ${project.groupId}.${project.artifactId}
Import-Package: jakarta.*;resolution:=optional, \
*;
Service-Component: OSGI-INF/io.modelcontextprotocol.json.McpJsonDefaults.xml

View file

@ -494,10 +494,9 @@ public interface McpClient {
* calling any client operation. This allows to extract thread-locals and hand
* them over to the underlying transport.
* <p>
* 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.
* There is no direct equivalent in {@link AsyncSpec}. To achieve the same result,
* append {@code contextWrite(McpTransportContext.KEY, context)} to any
* {@link McpAsyncClient} call.
* @param contextProvider A supplier to create a context
* @return This builder for method chaining
*/
@ -581,21 +580,6 @@ 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_2025_03_26,
ProtocolVersions.MCP_2025_06_18, ProtocolVersions.MCP_2025_11_25);
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 McpHttpClientTransportAuthorizationErrorHandler authorizationErrorHandler = McpHttpClientTransportAuthorizationErrorHandler.NOOP;

View file

@ -258,13 +258,16 @@ public class StdioClientTransport implements McpClientTransport {
@Override
public Mono<Void> sendMessage(JSONRPCMessage message) {
try {
// busyLooping retries FAIL_NON_SERIALIZED under concurrent senders
this.outboundSink.emitNext(message, Sinks.EmitFailureHandler.busyLooping(Duration.ofMillis(100)));
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.
return Mono.empty();
}
catch (Sinks.EmissionException e) {
return Mono.error(new RuntimeException("Failed to enqueue message", e));
else {
return Mono.error(new RuntimeException("Failed to enqueue message"));
}
}

View file

@ -7,26 +7,18 @@ 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,28 +11,13 @@ 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. Write the
* context under this key to make it visible to the transport, for example
* {@code contextWrite(ctx -> ctx.put(McpTransportContext.KEY, context))}.
* Key for use in Reactor Context to transport the context to user land.
*/
String KEY = "MCP_TRANSPORT_CONTEXT";

View file

@ -57,11 +57,6 @@ 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

@ -1,81 +0,0 @@
/*
* 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,8 +118,6 @@ 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();
@ -148,7 +146,6 @@ 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);
@ -180,7 +177,6 @@ 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);
@ -541,30 +537,10 @@ public class McpAsyncServer {
private McpRequestHandler<McpSchema.ListToolsResult> toolsListRequestHandler() {
return (exchange, params) -> {
// 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());
};
}
List<Tool> tools = this.tools.stream().map(McpServerFeatures.AsyncToolSpecification::tool).toList();
/**
* 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());
return Mono.just(McpSchema.ListToolsResult.builder(tools).build());
};
}
private McpRequestHandler<CallToolResult> toolsCallRequestHandler() {

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