diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index f055b61..0c79351 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -2,7 +2,6 @@ name: CI
on:
pull_request: {}
- workflow_dispatch: {}
jobs:
build:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 8153de2..9b72445 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -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) –
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index a7bf123..1adc091 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -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.
diff --git a/ROADMAP.md b/ROADMAP.md
index b71a1b5..e6eccde 100644
--- a/ROADMAP.md
+++ b/ROADMAP.md
@@ -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)
diff --git a/VERSIONING.md b/VERSIONING.md
index 83f15d7..d8efa60 100644
--- a/VERSIONING.md
+++ b/VERSIONING.md
@@ -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
diff --git a/conformance-tests/client-jdk-http-client/pom.xml b/conformance-tests/client-jdk-http-client/pom.xml
index 1d111dd..0a76c7a 100644
--- a/conformance-tests/client-jdk-http-client/pom.xml
+++ b/conformance-tests/client-jdk-http-client/pom.xml
@@ -6,7 +6,7 @@
io.modelcontextprotocol.sdkconformance-tests
- 2.1.0-SNAPSHOT
+ 2.0.1client-jdk-http-clientjar
@@ -21,7 +21,6 @@
- io.modelcontextprotocol.sdk.conformance.client.jdktrue
@@ -29,7 +28,7 @@
io.modelcontextprotocol.sdkmcp
- 2.1.0-SNAPSHOT
+ 2.0.1
diff --git a/conformance-tests/client-spring-http-client/pom.xml b/conformance-tests/client-spring-http-client/pom.xml
index ccc721c..32c81ba 100644
--- a/conformance-tests/client-spring-http-client/pom.xml
+++ b/conformance-tests/client-spring-http-client/pom.xml
@@ -6,7 +6,7 @@
io.modelcontextprotocol.sdkconformance-tests
- 2.1.0-SNAPSHOT
+ 2.0.1client-spring-http-clientjar
@@ -21,7 +21,6 @@
- io.modelcontextprotocol.sdk.conformance.client.spring174.1.02.0.0
diff --git a/conformance-tests/pom.xml b/conformance-tests/pom.xml
index 7e411a2..8856e7d 100644
--- a/conformance-tests/pom.xml
+++ b/conformance-tests/pom.xml
@@ -6,7 +6,7 @@
io.modelcontextprotocol.sdkmcp-parent
- 2.1.0-SNAPSHOT
+ 2.0.1conformance-testspom
diff --git a/conformance-tests/server-servlet/pom.xml b/conformance-tests/server-servlet/pom.xml
index ce8f558..df75a81 100644
--- a/conformance-tests/server-servlet/pom.xml
+++ b/conformance-tests/server-servlet/pom.xml
@@ -6,7 +6,7 @@
io.modelcontextprotocol.sdkconformance-tests
- 2.1.0-SNAPSHOT
+ 2.0.1server-servletjar
@@ -21,7 +21,6 @@
- io.modelcontextprotocol.sdk.conformance.server.servlettrue
@@ -29,7 +28,7 @@
io.modelcontextprotocol.sdkmcp
- 2.1.0-SNAPSHOT
+ 2.0.1
diff --git a/docs/server.md b/docs/server.md
index 5948f4a..8d74359 100644
--- a/docs/server.md
+++ b/docs/server.md
@@ -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
- })
- .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>)` 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) 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.
diff --git a/mcp-bom/pom.xml b/mcp-bom/pom.xml
index 4ee9f55..9f4f9ff 100644
--- a/mcp-bom/pom.xml
+++ b/mcp-bom/pom.xml
@@ -7,7 +7,7 @@
io.modelcontextprotocol.sdkmcp-parent
- 2.1.0-SNAPSHOT
+ 2.0.1mcp-bom
diff --git a/mcp-core/pom.xml b/mcp-core/pom.xml
index 79a417b..b63b61b 100644
--- a/mcp-core/pom.xml
+++ b/mcp-core/pom.xml
@@ -6,7 +6,7 @@
io.modelcontextprotocol.sdkmcp-parent
- 2.1.0-SNAPSHOT
+ 2.0.1mcp-corejar
@@ -20,10 +20,6 @@
scm:git:ssh://git@github.com/modelcontextprotocol/java-sdk.git
-
- io.modelcontextprotocol.sdk.mcp.core
-
-
@@ -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
diff --git a/mcp-core/src/main/java/io/modelcontextprotocol/client/McpClient.java b/mcp-core/src/main/java/io/modelcontextprotocol/client/McpClient.java
index 328566a..1af4eea 100644
--- a/mcp-core/src/main/java/io/modelcontextprotocol/client/McpClient.java
+++ b/mcp-core/src/main/java/io/modelcontextprotocol/client/McpClient.java
@@ -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.
*
- * 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 {
*
Change notification handlers for tools, resources, and prompts
*
Custom message sampling logic
*
- *
- *
- * 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:
To bridge thread-locals into the chain, use Reactor's context propagation
- * support.
- *
- * @see SyncSpec#transportContextProvider(Supplier)
*/
class AsyncSpec {
diff --git a/mcp-core/src/main/java/io/modelcontextprotocol/client/transport/HttpClientStreamableHttpTransport.java b/mcp-core/src/main/java/io/modelcontextprotocol/client/transport/HttpClientStreamableHttpTransport.java
index 5517823..07a8f5e 100644
--- a/mcp-core/src/main/java/io/modelcontextprotocol/client/transport/HttpClientStreamableHttpTransport.java
+++ b/mcp-core/src/main/java/io/modelcontextprotocol/client/transport/HttpClientStreamableHttpTransport.java
@@ -763,8 +763,8 @@ public class HttpClientStreamableHttpTransport implements McpClientTransport {
private Duration connectTimeout = Duration.ofSeconds(10);
- private List supportedProtocolVersions = List.of(ProtocolVersions.MCP_2025_03_26,
- ProtocolVersions.MCP_2025_06_18, ProtocolVersions.MCP_2025_11_25);
+ private List 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;
diff --git a/mcp-core/src/main/java/io/modelcontextprotocol/client/transport/StdioClientTransport.java b/mcp-core/src/main/java/io/modelcontextprotocol/client/transport/StdioClientTransport.java
index 60a8d7c..73522ae 100644
--- a/mcp-core/src/main/java/io/modelcontextprotocol/client/transport/StdioClientTransport.java
+++ b/mcp-core/src/main/java/io/modelcontextprotocol/client/transport/StdioClientTransport.java
@@ -258,13 +258,16 @@ public class StdioClientTransport implements McpClientTransport {
@Override
public Mono 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"));
}
}
diff --git a/mcp-core/src/main/java/io/modelcontextprotocol/client/transport/customizer/McpAsyncHttpClientRequestCustomizer.java b/mcp-core/src/main/java/io/modelcontextprotocol/client/transport/customizer/McpAsyncHttpClientRequestCustomizer.java
index f2951d8..756b39c 100644
--- a/mcp-core/src/main/java/io/modelcontextprotocol/client/transport/customizer/McpAsyncHttpClientRequestCustomizer.java
+++ b/mcp-core/src/main/java/io/modelcontextprotocol/client/transport/customizer/McpAsyncHttpClientRequestCustomizer.java
@@ -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.
*
* When used in a non-blocking context, implementations MUST be non-blocking.
- *
- * 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
*/
diff --git a/mcp-core/src/main/java/io/modelcontextprotocol/common/McpTransportContext.java b/mcp-core/src/main/java/io/modelcontextprotocol/common/McpTransportContext.java
index 4691c13..46a2ccf 100644
--- a/mcp-core/src/main/java/io/modelcontextprotocol/common/McpTransportContext.java
+++ b/mcp-core/src/main/java/io/modelcontextprotocol/common/McpTransportContext.java
@@ -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.
- *
- * 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:
- *
- *
with a synchronous client, configure
- * {@code McpClient.SyncSpec#transportContextProvider(Supplier)}, which is invoked on the
- * calling thread before every operation;
- *
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.
- *
*
* @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";
diff --git a/mcp-core/src/main/java/io/modelcontextprotocol/json/schema/JsonSchemaValidator.java b/mcp-core/src/main/java/io/modelcontextprotocol/json/schema/JsonSchemaValidator.java
index 6e89ab7..7eed339 100644
--- a/mcp-core/src/main/java/io/modelcontextprotocol/json/schema/JsonSchemaValidator.java
+++ b/mcp-core/src/main/java/io/modelcontextprotocol/json/schema/JsonSchemaValidator.java
@@ -57,11 +57,6 @@ public interface JsonSchemaValidator {
/**
* Validates the structured content against the provided JSON schema.
- *
- * 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
diff --git a/mcp-core/src/main/java/io/modelcontextprotocol/server/McpAsyncListFilter.java b/mcp-core/src/main/java/io/modelcontextprotocol/server/McpAsyncListFilter.java
deleted file mode 100644
index d6b1858..0000000
--- a/mcp-core/src/main/java/io/modelcontextprotocol/server/McpAsyncListFilter.java
+++ /dev/null
@@ -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}.
- *
- * 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 {
-
- /**
- * 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 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 McpAsyncListFilter fromSync(McpSyncListFilter 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 McpAsyncListFilter and(List> 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> snapshot = List.copyOf(filters);
- return (transportContext, primitive) -> Flux.fromIterable(snapshot)
- .concatMap(filter -> filter.isVisible(transportContext, primitive).defaultIfEmpty(Boolean.FALSE))
- .all(Boolean.TRUE::equals);
- }
-
-}
diff --git a/mcp-core/src/main/java/io/modelcontextprotocol/server/McpAsyncServer.java b/mcp-core/src/main/java/io/modelcontextprotocol/server/McpAsyncServer.java
index fc1f8a4..ac78c4f 100644
--- a/mcp-core/src/main/java/io/modelcontextprotocol/server/McpAsyncServer.java
+++ b/mcp-core/src/main/java/io/modelcontextprotocol/server/McpAsyncServer.java
@@ -118,8 +118,6 @@ public class McpAsyncServer {
private final ConcurrentHashMap> resourceSubscriptions = new ConcurrentHashMap<>();
- private final McpAsyncListFilter toolFilter;
-
private List 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> requestHandlers = prepareRequestHandlers();
Map 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> requestHandlers = prepareRequestHandlers();
Map notificationHandlers = prepareNotificationHandlers(features);
@@ -541,30 +537,10 @@ public class McpAsyncServer {
private McpRequestHandler 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 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 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 toolsCallRequestHandler() {
diff --git a/mcp-core/src/main/java/io/modelcontextprotocol/server/McpServer.java b/mcp-core/src/main/java/io/modelcontextprotocol/server/McpServer.java
index 2113fdb..a2333ae 100644
--- a/mcp-core/src/main/java/io/modelcontextprotocol/server/McpServer.java
+++ b/mcp-core/src/main/java/io/modelcontextprotocol/server/McpServer.java
@@ -12,7 +12,6 @@ import java.util.List;
import java.util.Map;
import java.util.function.BiConsumer;
import java.util.function.BiFunction;
-import java.util.function.Consumer;
import io.modelcontextprotocol.common.McpTransportContext;
import io.modelcontextprotocol.json.McpJsonDefaults;
@@ -239,7 +238,7 @@ public interface McpServer {
public McpAsyncServer build() {
var features = new McpServerFeatures.Async(this.serverInfo, this.serverCapabilities, this.tools,
this.resources, this.resourceTemplates, this.prompts, this.completions, this.rootsChangeHandlers,
- this.instructions, this.toolFilters);
+ this.instructions);
var jsonSchemaValidator = (this.jsonSchemaValidator != null) ? this.jsonSchemaValidator
: McpJsonDefaults.getSchemaValidator();
@@ -269,7 +268,7 @@ public interface McpServer {
public McpAsyncServer build() {
var features = new McpServerFeatures.Async(this.serverInfo, this.serverCapabilities, this.tools,
this.resources, this.resourceTemplates, this.prompts, this.completions, this.rootsChangeHandlers,
- this.instructions, this.toolFilters);
+ this.instructions);
var jsonSchemaValidator = this.jsonSchemaValidator != null ? this.jsonSchemaValidator
: McpJsonDefaults.getSchemaValidator();
@@ -302,8 +301,6 @@ public interface McpServer {
boolean validateToolInputs = true;
- final List> toolFilters = new ArrayList<>();
-
/**
* The Model Context Protocol (MCP) allows servers to expose tools that can be
* invoked by language models. Tools enable models to interact with external
@@ -443,43 +440,6 @@ public interface McpServer {
return this;
}
- /**
- * Adds a per-request filter deciding which tools are advertised in
- * {@code tools/list}, for example to hide tools the caller is not authorized to
- * see. Filters accumulate: a tool is listed only when every registered filter
- * accepts it, so a later registration can never widen access.
- *
- * A hidden tool is omitted from listings only. It remains callable by name, so
- * enforce permissions in the tool's call handler. Tools are NOT hidden from
- * {@code notifications/tools/list_changed}, as it is a per-client context rather
- * than per-request. Consider disabling list changed notifications entirely when
- * using filters.
- *
- * @param toolFilter the filter to add, must not be null
- * @return This builder instance for method chaining
- * @see McpAsyncListFilter
- */
- public AsyncSpecification addToolFilter(McpAsyncListFilter toolFilter) {
- Assert.notNull(toolFilter, "Tool filter must not be null");
- this.toolFilters.add(toolFilter);
- return this;
- }
-
- /**
- * Applies the given consumer to the list of registered tool filters, allowing
- * them to be inspected, reordered or cleared before the server is built.
- * @param toolFiltersConsumer consumer of the mutable list of registered filters,
- * must not be null
- * @return This builder instance for method chaining
- * @see #addToolFilter(McpAsyncListFilter)
- */
- public AsyncSpecification toolFilters(
- Consumer>> toolFiltersConsumer) {
- Assert.notNull(toolFiltersConsumer, "Tool filters consumer must not be null");
- toolFiltersConsumer.accept(this.toolFilters);
- return this;
- }
-
/**
* Sets the server capabilities that will be advertised to clients during
* connection initialization. Capabilities define what features the server
@@ -870,7 +830,7 @@ public interface McpServer {
public McpSyncServer build() {
McpServerFeatures.Sync syncFeatures = new McpServerFeatures.Sync(this.serverInfo, this.serverCapabilities,
this.tools, this.resources, this.resourceTemplates, this.prompts, this.completions,
- this.rootsChangeHandlers, this.instructions, this.toolFilters);
+ this.rootsChangeHandlers, this.instructions);
McpServerFeatures.Async asyncFeatures = McpServerFeatures.Async.fromSync(syncFeatures,
this.immediateExecution);
@@ -905,7 +865,7 @@ public interface McpServer {
public McpSyncServer build() {
McpServerFeatures.Sync syncFeatures = new McpServerFeatures.Sync(this.serverInfo, this.serverCapabilities,
this.tools, this.resources, this.resourceTemplates, this.prompts, this.completions,
- this.rootsChangeHandlers, this.instructions, this.toolFilters);
+ this.rootsChangeHandlers, this.instructions);
McpServerFeatures.Async asyncFeatures = McpServerFeatures.Async.fromSync(syncFeatures,
this.immediateExecution);
var jsonSchemaValidator = this.jsonSchemaValidator != null ? this.jsonSchemaValidator
@@ -940,8 +900,6 @@ public interface McpServer {
boolean validateToolInputs = true;
- final List> toolFilters = new ArrayList<>();
-
/**
* The Model Context Protocol (MCP) allows servers to expose tools that can be
* invoked by language models. Tools enable models to interact with external
@@ -1085,44 +1043,6 @@ public interface McpServer {
return this;
}
- /**
- * Adds a per-request filter deciding which tools are advertised in
- * {@code tools/list}, for example to hide tools the caller is not authorized to
- * see. Filters accumulate: a tool is listed only when every registered filter
- * accepts it, so a later registration can never widen access.
- *
- * A hidden tool is omitted from listings only. It remains callable by name, so
- * enforce permissions in the tool's call handler. Tools are NOT hidden from
- * {@code notifications/tools/list_changed}, as it is a per-client context rather
- * than per-request. Consider disabling list changed notifications entirely when
- * using filters.
- *
- * The filter is offloaded to a bounded elastic scheduler unless
- * {@link #immediateExecution(boolean)} is set.
- * @param toolFilter the filter to add, must not be null
- * @return This builder instance for method chaining
- * @see McpSyncListFilter
- */
- public SyncSpecification addToolFilter(McpSyncListFilter toolFilter) {
- Assert.notNull(toolFilter, "Tool filter must not be null");
- this.toolFilters.add(toolFilter);
- return this;
- }
-
- /**
- * Applies the given consumer to the list of registered tool filters, allowing
- * them to be inspected, reordered or cleared before the server is built.
- * @param toolFiltersConsumer consumer of the mutable list of registered filters,
- * must not be null
- * @return This builder instance for method chaining
- * @see #addToolFilter(McpSyncListFilter)
- */
- public SyncSpecification toolFilters(Consumer>> toolFiltersConsumer) {
- Assert.notNull(toolFiltersConsumer, "Tool filters consumer must not be null");
- toolFiltersConsumer.accept(this.toolFilters);
- return this;
- }
-
/**
* Sets the server capabilities that will be advertised to clients during
* connection initialization. Capabilities define what features the server
@@ -1522,8 +1442,6 @@ public interface McpServer {
boolean validateToolInputs = true;
- final List> toolFilters = new ArrayList<>();
-
/**
* The Model Context Protocol (MCP) allows servers to expose tools that can be
* invoked by language models. Tools enable models to interact with external
@@ -1664,40 +1582,6 @@ public interface McpServer {
return this;
}
- /**
- * Adds a per-request filter deciding which tools are advertised in
- * {@code tools/list}, for example to hide tools the caller is not authorized to
- * see. Filters accumulate: a tool is listed only when every registered filter
- * accepts it, so a later registration can never widen access.
- *
- * A hidden tool is omitted from listings only. It remains callable by name, so
- * enforce permissions in the tool's call handler.
- *
- * @param toolFilter the filter to add, must not be null
- * @return This builder instance for method chaining
- * @see McpAsyncListFilter
- */
- public StatelessAsyncSpecification addToolFilter(McpAsyncListFilter toolFilter) {
- Assert.notNull(toolFilter, "Tool filter must not be null");
- this.toolFilters.add(toolFilter);
- return this;
- }
-
- /**
- * Applies the given consumer to the list of registered tool filters, allowing
- * them to be inspected, reordered or cleared before the server is built.
- * @param toolFiltersConsumer consumer of the mutable list of registered filters,
- * must not be null
- * @return This builder instance for method chaining
- * @see #addToolFilter(McpAsyncListFilter)
- */
- public StatelessAsyncSpecification toolFilters(
- Consumer>> toolFiltersConsumer) {
- Assert.notNull(toolFiltersConsumer, "Tool filters consumer must not be null");
- toolFiltersConsumer.accept(this.toolFilters);
- return this;
- }
-
/**
* Sets the server capabilities that will be advertised to clients during
* connection initialization. Capabilities define what features the server
@@ -2024,8 +1908,7 @@ public interface McpServer {
public McpStatelessAsyncServer build() {
var features = new McpStatelessServerFeatures.Async(this.serverInfo, this.serverCapabilities, this.tools,
- this.resources, this.resourceTemplates, this.prompts, this.completions, this.instructions,
- this.toolFilters);
+ this.resources, this.resourceTemplates, this.prompts, this.completions, this.instructions);
var jsonSchemaValidator = this.jsonSchemaValidator != null ? this.jsonSchemaValidator
: McpJsonDefaults.getSchemaValidator();
@@ -2059,8 +1942,6 @@ public interface McpServer {
boolean validateToolInputs = true;
- final List> toolFilters = new ArrayList<>();
-
/**
* The Model Context Protocol (MCP) allows servers to expose tools that can be
* invoked by language models. Tools enable models to interact with external
@@ -2201,42 +2082,6 @@ public interface McpServer {
return this;
}
- /**
- * Adds a per-request filter deciding which tools are advertised in
- * {@code tools/list}, for example to hide tools the caller is not authorized to
- * see. Filters accumulate: a tool is listed only when every registered filter
- * accepts it, so a later registration can never widen access.
- *
- * A hidden tool is omitted from listings only. It remains callable by name, so
- * enforce permissions in the tool's call handler.
- *
- * The filter is offloaded to a bounded elastic scheduler unless
- * {@link #immediateExecution(boolean)} is set.
- * @param toolFilter the filter to add, must not be null
- * @return This builder instance for method chaining
- * @see McpSyncListFilter
- */
- public StatelessSyncSpecification addToolFilter(McpSyncListFilter toolFilter) {
- Assert.notNull(toolFilter, "Tool filter must not be null");
- this.toolFilters.add(toolFilter);
- return this;
- }
-
- /**
- * Applies the given consumer to the list of registered tool filters, allowing
- * them to be inspected, reordered or cleared before the server is built.
- * @param toolFiltersConsumer consumer of the mutable list of registered filters,
- * must not be null
- * @return This builder instance for method chaining
- * @see #addToolFilter(McpSyncListFilter)
- */
- public StatelessSyncSpecification toolFilters(
- Consumer>> toolFiltersConsumer) {
- Assert.notNull(toolFiltersConsumer, "Tool filters consumer must not be null");
- toolFiltersConsumer.accept(this.toolFilters);
- return this;
- }
-
/**
* Sets the server capabilities that will be advertised to clients during
* connection initialization. Capabilities define what features the server
@@ -2579,8 +2424,7 @@ public interface McpServer {
public McpStatelessSyncServer build() {
var syncFeatures = new McpStatelessServerFeatures.Sync(this.serverInfo, this.serverCapabilities, this.tools,
- this.resources, this.resourceTemplates, this.prompts, this.completions, this.instructions,
- this.toolFilters);
+ this.resources, this.resourceTemplates, this.prompts, this.completions, this.instructions);
var asyncFeatures = McpStatelessServerFeatures.Async.fromSync(syncFeatures, this.immediateExecution);
var jsonSchemaValidator = this.jsonSchemaValidator != null ? this.jsonSchemaValidator
: McpJsonDefaults.getSchemaValidator();
diff --git a/mcp-core/src/main/java/io/modelcontextprotocol/server/McpServerFeatures.java b/mcp-core/src/main/java/io/modelcontextprotocol/server/McpServerFeatures.java
index 77ecbc8..cfa28e6 100644
--- a/mcp-core/src/main/java/io/modelcontextprotocol/server/McpServerFeatures.java
+++ b/mcp-core/src/main/java/io/modelcontextprotocol/server/McpServerFeatures.java
@@ -38,7 +38,6 @@ public class McpServerFeatures {
* @param rootsChangeConsumers The list of consumers that will be notified when the
* roots list changes
* @param instructions The server instructions text
- * @param toolFilters The per-request filters deciding which tools are listed
*/
record Async(McpSchema.Implementation serverInfo, McpSchema.ServerCapabilities serverCapabilities,
List tools, Map resources,
@@ -46,7 +45,7 @@ public class McpServerFeatures {
Map prompts,
Map completions,
List, Mono>> rootsChangeConsumers,
- String instructions, List> toolFilters) {
+ String instructions) {
/**
* Create an instance and validate the arguments.
@@ -59,7 +58,6 @@ public class McpServerFeatures {
* @param rootsChangeConsumers The list of consumers that will be notified when
* the roots list changes
* @param instructions The server instructions text
- * @param toolFilters The per-request filters deciding which tools are listed
*/
Async(McpSchema.Implementation serverInfo, McpSchema.ServerCapabilities serverCapabilities,
List tools, Map resources,
@@ -67,7 +65,7 @@ public class McpServerFeatures {
Map prompts,
Map completions,
List, Mono>> rootsChangeConsumers,
- String instructions, List> toolFilters) {
+ String instructions) {
Assert.notNull(serverInfo, "Server info must not be null");
@@ -93,7 +91,6 @@ public class McpServerFeatures {
this.completions = (completions != null) ? completions : Map.of();
this.rootsChangeConsumers = (rootsChangeConsumers != null) ? rootsChangeConsumers : List.of();
this.instructions = instructions;
- this.toolFilters = (toolFilters != null) ? toolFilters : List.of();
}
/**
@@ -141,11 +138,7 @@ public class McpServerFeatures {
}
return new Async(syncSpec.serverInfo(), syncSpec.serverCapabilities(), tools, resources, resourceTemplates,
- prompts, completions, rootChangeConsumers, syncSpec.instructions(),
- syncSpec.toolFilters()
- .stream()
- .map(filter -> McpAsyncListFilter.fromSync(filter, immediateExecution))
- .toList());
+ prompts, completions, rootChangeConsumers, syncSpec.instructions());
}
}
@@ -168,8 +161,7 @@ public class McpServerFeatures {
Map resourceTemplates,
Map prompts,
Map completions,
- List>> rootsChangeConsumers, String instructions,
- List> toolFilters) {
+ List>> rootsChangeConsumers, String instructions) {
/**
* Create an instance and validate the arguments.
@@ -182,7 +174,6 @@ public class McpServerFeatures {
* @param rootsChangeConsumers The list of consumers that will be notified when
* the roots list changes
* @param instructions The server instructions text
- * @param toolFilters The per-request filters deciding which tools are listed
*/
Sync(McpSchema.Implementation serverInfo, McpSchema.ServerCapabilities serverCapabilities,
List tools,
@@ -190,8 +181,8 @@ public class McpServerFeatures {
Map resourceTemplates,
Map prompts,
Map completions,
- List>> rootsChangeConsumers, String instructions,
- List> toolFilters) {
+ List>> rootsChangeConsumers,
+ String instructions) {
Assert.notNull(serverInfo, "Server info must not be null");
@@ -217,7 +208,6 @@ public class McpServerFeatures {
this.completions = (completions != null) ? completions : new HashMap<>();
this.rootsChangeConsumers = (rootsChangeConsumers != null) ? rootsChangeConsumers : new ArrayList<>();
this.instructions = instructions;
- this.toolFilters = (toolFilters != null) ? toolFilters : List.of();
}
}
diff --git a/mcp-core/src/main/java/io/modelcontextprotocol/server/McpStatelessAsyncServer.java b/mcp-core/src/main/java/io/modelcontextprotocol/server/McpStatelessAsyncServer.java
index 95b694e..4211233 100644
--- a/mcp-core/src/main/java/io/modelcontextprotocol/server/McpStatelessAsyncServer.java
+++ b/mcp-core/src/main/java/io/modelcontextprotocol/server/McpStatelessAsyncServer.java
@@ -80,8 +80,6 @@ public class McpStatelessAsyncServer {
private final boolean validateToolInputs;
- private final McpAsyncListFilter toolFilter;
-
McpStatelessAsyncServer(McpStatelessServerTransport mcpTransport, McpJsonMapper jsonMapper,
McpStatelessServerFeatures.Async features, Duration requestTimeout,
McpUriTemplateManagerFactory uriTemplateManagerFactory, JsonSchemaValidator jsonSchemaValidator,
@@ -99,7 +97,6 @@ public class McpStatelessAsyncServer {
this.uriTemplateManagerFactory = uriTemplateManagerFactory;
this.jsonSchemaValidator = jsonSchemaValidator;
this.validateToolInputs = validateToolInputs;
- this.toolFilter = McpAsyncListFilter.and(features.toolFilters());
Map> requestHandlers = new HashMap<>();
@@ -420,32 +417,13 @@ public class McpStatelessAsyncServer {
private McpStatelessRequestHandler toolsListRequestHandler() {
return (ctx, 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)
+ List tools = this.tools.stream()
.map(McpStatelessServerFeatures.AsyncToolSpecification::tool)
- .filterWhen(tool -> this.toolFilter.isVisible(ctx, tool)
- .onErrorResume(error -> opaqueListFilterError(tool, error)))
- .collectList()
- .map(tools -> McpSchema.ListToolsResult.builder(tools).build());
+ .toList();
+ return Mono.just(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 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 McpStatelessRequestHandler toolsCallRequestHandler() {
return (ctx, params) -> {
McpSchema.CallToolRequest callToolRequest = jsonMapper.convertValue(params,
diff --git a/mcp-core/src/main/java/io/modelcontextprotocol/server/McpStatelessServerFeatures.java b/mcp-core/src/main/java/io/modelcontextprotocol/server/McpStatelessServerFeatures.java
index 8ffc2a6..0c1fbfb 100644
--- a/mcp-core/src/main/java/io/modelcontextprotocol/server/McpStatelessServerFeatures.java
+++ b/mcp-core/src/main/java/io/modelcontextprotocol/server/McpStatelessServerFeatures.java
@@ -37,7 +37,6 @@ public class McpStatelessServerFeatures {
* @param resourceTemplates The map of resource templates
* @param prompts The map of prompt specifications
* @param instructions The server instructions text
- * @param toolFilters The per-request filters deciding which tools are listed
*/
record Async(McpSchema.Implementation serverInfo, McpSchema.ServerCapabilities serverCapabilities,
List tools,
@@ -45,7 +44,7 @@ public class McpStatelessServerFeatures {
Map resourceTemplates,
Map prompts,
Map completions,
- String instructions, List> toolFilters) {
+ String instructions) {
/**
* Create an instance and validate the arguments.
@@ -56,7 +55,6 @@ public class McpStatelessServerFeatures {
* @param resourceTemplates The map of resource templates
* @param prompts The map of prompt specifications
* @param instructions The server instructions text
- * @param toolFilters The per-request filters deciding which tools are listed
*/
Async(McpSchema.Implementation serverInfo, McpSchema.ServerCapabilities serverCapabilities,
List tools,
@@ -64,7 +62,7 @@ public class McpStatelessServerFeatures {
Map resourceTemplates,
Map prompts,
Map completions,
- String instructions, List> toolFilters) {
+ String instructions) {
Assert.notNull(serverInfo, "Server info must not be null");
@@ -86,7 +84,6 @@ public class McpStatelessServerFeatures {
this.prompts = (prompts != null) ? prompts : Map.of();
this.completions = (completions != null) ? completions : Map.of();
this.instructions = instructions;
- this.toolFilters = (toolFilters != null) ? toolFilters : List.of();
}
/**
@@ -126,11 +123,7 @@ public class McpStatelessServerFeatures {
});
return new Async(syncSpec.serverInfo(), syncSpec.serverCapabilities(), tools, resources, resourceTemplates,
- prompts, completions, syncSpec.instructions(),
- syncSpec.toolFilters()
- .stream()
- .map(filter -> McpAsyncListFilter.fromSync(filter, immediateExecution))
- .toList());
+ prompts, completions, syncSpec.instructions());
}
}
@@ -144,7 +137,6 @@ public class McpStatelessServerFeatures {
* @param resourceTemplates The map of resource templates
* @param prompts The map of prompt specifications
* @param instructions The server instructions text
- * @param toolFilters The per-request filters deciding which tools are listed
*/
record Sync(McpSchema.Implementation serverInfo, McpSchema.ServerCapabilities serverCapabilities,
List tools,
@@ -152,7 +144,7 @@ public class McpStatelessServerFeatures {
Map resourceTemplates,
Map prompts,
Map completions,
- String instructions, List> toolFilters) {
+ String instructions) {
/**
* Create an instance and validate the arguments.
@@ -163,7 +155,6 @@ public class McpStatelessServerFeatures {
* @param resourceTemplates The map of resource templates
* @param prompts The map of prompt specifications
* @param instructions The server instructions text
- * @param toolFilters The per-request filters deciding which tools are listed
*/
Sync(McpSchema.Implementation serverInfo, McpSchema.ServerCapabilities serverCapabilities,
List tools,
@@ -171,7 +162,7 @@ public class McpStatelessServerFeatures {
Map resourceTemplates,
Map prompts,
Map completions,
- String instructions, List> toolFilters) {
+ String instructions) {
Assert.notNull(serverInfo, "Server info must not be null");
@@ -196,7 +187,6 @@ public class McpStatelessServerFeatures {
this.prompts = (prompts != null) ? prompts : new HashMap<>();
this.completions = (completions != null) ? completions : new HashMap<>();
this.instructions = instructions;
- this.toolFilters = (toolFilters != null) ? toolFilters : List.of();
}
}
diff --git a/mcp-core/src/main/java/io/modelcontextprotocol/server/McpSyncListFilter.java b/mcp-core/src/main/java/io/modelcontextprotocol/server/McpSyncListFilter.java
deleted file mode 100644
index 01d4d07..0000000
--- a/mcp-core/src/main/java/io/modelcontextprotocol/server/McpSyncListFilter.java
+++ /dev/null
@@ -1,38 +0,0 @@
-/*
- * Copyright 2026-2026 the original author or authors.
- */
-
-package io.modelcontextprotocol.server;
-
-import io.modelcontextprotocol.common.McpTransportContext;
-import io.modelcontextprotocol.spec.McpSchema;
-
-/**
- * Decide per request whether a primitive is advertised in the corresponding listing, such
- * as {@code tools/list}.
- *
- * 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 McpAsyncListFilter
- * @see McpTransportContextExtractor
- */
-@FunctionalInterface
-public interface McpSyncListFilter {
-
- /**
- * 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 McpSchema.Tool}.
- * @return {@code true} to include the primitive in the listing, {@code false} to omit
- * it. Throwing an exception fails the listing request.
- */
- boolean isVisible(McpTransportContext transportContext, T primitive);
-
-}
diff --git a/mcp-core/src/main/java/io/modelcontextprotocol/server/transport/DefaultServerTransportSecurityValidator.java b/mcp-core/src/main/java/io/modelcontextprotocol/server/transport/DefaultServerTransportSecurityValidator.java
index 725a67a..e96403e 100644
--- a/mcp-core/src/main/java/io/modelcontextprotocol/server/transport/DefaultServerTransportSecurityValidator.java
+++ b/mcp-core/src/main/java/io/modelcontextprotocol/server/transport/DefaultServerTransportSecurityValidator.java
@@ -7,7 +7,6 @@ package io.modelcontextprotocol.server.transport;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
-import java.util.Objects;
import io.modelcontextprotocol.util.Assert;
@@ -23,8 +22,7 @@ import io.modelcontextprotocol.util.Assert;
* @see ServerTransportSecurityValidator
* @see ServerTransportSecurityException
*/
-public final class DefaultServerTransportSecurityValidator
- implements ServerTransportSecurityValidator, ServerHttpHeaderValidator {
+public final class DefaultServerTransportSecurityValidator implements ServerTransportSecurityValidator {
private static final String ORIGIN_HEADER = "Origin";
@@ -49,24 +47,27 @@ public final class DefaultServerTransportSecurityValidator
}
@Override
- @Deprecated
public void validateHeaders(Map> headers) throws ServerTransportSecurityException {
- validate(new MapHeaderAccessor(headers));
- }
-
- @Override
- public void validate(HeaderAccessor headerAccessor) throws ServerTransportSecurityException {
- List originValues = headerAccessor.getHeader(ORIGIN_HEADER);
- if (originValues != null && !originValues.isEmpty()) {
- validateOrigin(originValues.get(0));
- }
-
- if (!allowedHosts.isEmpty()) {
- List hostValues = headerAccessor.getHeader(HOST_HEADER);
- if (hostValues == null || hostValues.isEmpty()) {
- throw new ServerTransportSecurityException(421, "Invalid Host header");
+ boolean missingHost = true;
+ for (Map.Entry> entry : headers.entrySet()) {
+ if (ORIGIN_HEADER.equalsIgnoreCase(entry.getKey())) {
+ List values = entry.getValue();
+ if (values == null || values.isEmpty()) {
+ throw new ServerTransportSecurityException(403, "Invalid Origin header");
+ }
+ validateOrigin(values.get(0));
}
- validateHost(hostValues.get(0));
+ else if (HOST_HEADER.equalsIgnoreCase(entry.getKey())) {
+ missingHost = false;
+ List values = entry.getValue();
+ if (values == null || values.isEmpty()) {
+ throw new ServerTransportSecurityException(421, "Invalid Host header");
+ }
+ validateHost(values.get(0));
+ }
+ }
+ if (!allowedHosts.isEmpty() && missingHost) {
+ throw new ServerTransportSecurityException(421, "Invalid Host header");
}
}
@@ -138,37 +139,6 @@ public final class DefaultServerTransportSecurityValidator
return new Builder();
}
- /**
- * {@link HeaderAccessor} view over a {@code Map>}, used to
- * bridge the deprecated {@link #validateHeaders(Map)} to
- * {@link #validate(HeaderAccessor)}.
- */
- private static final class MapHeaderAccessor implements HeaderAccessor {
-
- private final Map> headers;
-
- private MapHeaderAccessor(Map> headers) {
- this.headers = headers;
- }
-
- @Override
- public List getHeader(String name) {
- return headers.entrySet()
- .stream()
- .filter(entry -> entry.getKey().equalsIgnoreCase(name))
- .map(Map.Entry::getValue)
- .filter(Objects::nonNull)
- .findFirst()
- .orElse(List.of());
- }
-
- @Override
- public List getHeaderNames() {
- return List.copyOf(headers.keySet());
- }
-
- }
-
/**
* Builder for creating instances of {@link DefaultServerTransportSecurityValidator}.
*/
diff --git a/mcp-core/src/main/java/io/modelcontextprotocol/server/transport/HeaderAccessor.java b/mcp-core/src/main/java/io/modelcontextprotocol/server/transport/HeaderAccessor.java
deleted file mode 100644
index efed4a7..0000000
--- a/mcp-core/src/main/java/io/modelcontextprotocol/server/transport/HeaderAccessor.java
+++ /dev/null
@@ -1,34 +0,0 @@
-/*
- * Copyright 2026-2026 the original author or authors.
- */
-
-package io.modelcontextprotocol.server.transport;
-
-import java.util.List;
-
-/**
- * Abstraction for accessing HTTP headers from an incoming request. Implementations should
- * provide case-insensitive header name lookups (e.g., when backed by
- * {@code HttpServletRequest}).
- *
- * @author Neeraj Bhatt
- * @since 2.1.0
- * @see ServerHttpHeaderValidator
- */
-public interface HeaderAccessor {
-
- /**
- * Returns the values of the specified header, or an empty list if the header is not
- * present.
- * @param name the header name (case-insensitive)
- * @return the list of header values, never {@code null}
- */
- List getHeader(String name);
-
- /**
- * Returns all header names present in the request.
- * @return the list of header names, never {@code null}
- */
- List getHeaderNames();
-
-}
diff --git a/mcp-core/src/main/java/io/modelcontextprotocol/server/transport/HttpServletHeaderAccessor.java b/mcp-core/src/main/java/io/modelcontextprotocol/server/transport/HttpServletHeaderAccessor.java
deleted file mode 100644
index a06261c..0000000
--- a/mcp-core/src/main/java/io/modelcontextprotocol/server/transport/HttpServletHeaderAccessor.java
+++ /dev/null
@@ -1,41 +0,0 @@
-/*
- * Copyright 2026-2026 the original author or authors.
- */
-
-package io.modelcontextprotocol.server.transport;
-
-import java.util.Collections;
-import java.util.List;
-
-import jakarta.servlet.http.HttpServletRequest;
-
-/**
- * {@link HeaderAccessor} implementation backed by an {@link HttpServletRequest}. Header
- * name lookups are case-insensitive as per the Servlet specification.
- *
- *
- * For internal use only.
- *
- * @author Neeraj Bhatt
- * @since 2.1.0
- * @see HeaderAccessor
- */
-final class HttpServletHeaderAccessor implements HeaderAccessor {
-
- private final HttpServletRequest request;
-
- HttpServletHeaderAccessor(HttpServletRequest request) {
- this.request = request;
- }
-
- @Override
- public List getHeader(String name) {
- return Collections.list(this.request.getHeaders(name));
- }
-
- @Override
- public List getHeaderNames() {
- return Collections.list(this.request.getHeaderNames());
- }
-
-}
diff --git a/mcp-core/src/main/java/io/modelcontextprotocol/server/transport/HttpServletRequestUtils.java b/mcp-core/src/main/java/io/modelcontextprotocol/server/transport/HttpServletRequestUtils.java
index 5901c38..721bdf1 100644
--- a/mcp-core/src/main/java/io/modelcontextprotocol/server/transport/HttpServletRequestUtils.java
+++ b/mcp-core/src/main/java/io/modelcontextprotocol/server/transport/HttpServletRequestUtils.java
@@ -8,6 +8,11 @@ import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
+import java.util.Collections;
+import java.util.Enumeration;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
import jakarta.servlet.http.HttpServletRequest;
@@ -21,6 +26,21 @@ final class HttpServletRequestUtils {
private HttpServletRequestUtils() {
}
+ /**
+ * Extracts all headers from the HTTP request into a map.
+ * @param request The HTTP servlet request
+ * @return A map of header names to their values
+ */
+ static Map> extractHeaders(HttpServletRequest request) {
+ Map> headers = new HashMap<>();
+ Enumeration names = request.getHeaderNames();
+ while (names.hasMoreElements()) {
+ String name = names.nextElement();
+ headers.put(name, Collections.list(request.getHeaders(name)));
+ }
+ return headers;
+ }
+
/**
* Reads the request body, decoded using the request's character encoding (or UTF-8 if
* not specified), while bounding the number of bytes read.
diff --git a/mcp-core/src/main/java/io/modelcontextprotocol/server/transport/HttpServletSseServerTransportProvider.java b/mcp-core/src/main/java/io/modelcontextprotocol/server/transport/HttpServletSseServerTransportProvider.java
index a14271a..05dd862 100644
--- a/mcp-core/src/main/java/io/modelcontextprotocol/server/transport/HttpServletSseServerTransportProvider.java
+++ b/mcp-core/src/main/java/io/modelcontextprotocol/server/transport/HttpServletSseServerTransportProvider.java
@@ -161,7 +161,7 @@ public class HttpServletSseServerTransportProvider extends HttpServlet implement
/**
* Security validator for validating HTTP requests.
*/
- private final ServerHttpHeaderValidator httpHeaderValidator;
+ private final ServerTransportSecurityValidator securityValidator;
/**
* Creates a new HttpServletSseServerTransportProvider instance with a custom SSE
@@ -174,20 +174,20 @@ public class HttpServletSseServerTransportProvider extends HttpServlet implement
* @param keepAliveInterval The interval for keep-alive pings, or null to disable
* keep-alive functionality
* @param contextExtractor The extractor for transport context from the request.
- * @param httpHeaderValidator The HTTP header validator for validating HTTP requests.
+ * @param securityValidator The security validator for validating HTTP requests.
* @param requestMaxSize The maximum size, in bytes, of a single request body. Must be
* positive.
*/
private HttpServletSseServerTransportProvider(McpJsonMapper jsonMapper, String baseUrl, String messageEndpoint,
String sseEndpoint, Duration keepAliveInterval,
McpTransportContextExtractor contextExtractor,
- ServerHttpHeaderValidator httpHeaderValidator, int requestMaxSize) {
+ ServerTransportSecurityValidator securityValidator, int requestMaxSize) {
Assert.notNull(jsonMapper, "JsonMapper must not be null");
Assert.notNull(messageEndpoint, "messageEndpoint must not be null");
Assert.notNull(sseEndpoint, "sseEndpoint must not be null");
Assert.notNull(contextExtractor, "Context extractor must not be null");
- Assert.notNull(httpHeaderValidator, "HTTP header validator must not be null");
+ Assert.notNull(securityValidator, "Security validator must not be null");
Assert.isTrue(requestMaxSize > 0, "requestMaxSize must be positive");
this.jsonMapper = jsonMapper;
@@ -195,7 +195,7 @@ public class HttpServletSseServerTransportProvider extends HttpServlet implement
this.messageEndpoint = messageEndpoint;
this.sseEndpoint = sseEndpoint;
this.contextExtractor = contextExtractor;
- this.httpHeaderValidator = httpHeaderValidator;
+ this.securityValidator = securityValidator;
this.requestMaxSize = requestMaxSize;
if (keepAliveInterval != null) {
@@ -293,7 +293,8 @@ public class HttpServletSseServerTransportProvider extends HttpServlet implement
}
try {
- this.httpHeaderValidator.validate(new HttpServletHeaderAccessor(request));
+ Map> headers = HttpServletRequestUtils.extractHeaders(request);
+ this.securityValidator.validateHeaders(headers);
}
catch (ServerTransportSecurityException e) {
response.sendError(e.getStatusCode(), e.getMessage());
@@ -370,7 +371,8 @@ public class HttpServletSseServerTransportProvider extends HttpServlet implement
}
try {
- this.httpHeaderValidator.validate(new HttpServletHeaderAccessor(request));
+ Map> headers = HttpServletRequestUtils.extractHeaders(request);
+ this.securityValidator.validateHeaders(headers);
}
catch (ServerTransportSecurityException e) {
response.sendError(e.getStatusCode(), e.getMessage());
@@ -617,7 +619,7 @@ public class HttpServletSseServerTransportProvider extends HttpServlet implement
private Duration keepAliveInterval;
- private ServerHttpHeaderValidator httpHeaderValidator = ServerHttpHeaderValidator.NOOP;
+ private ServerTransportSecurityValidator securityValidator = ServerTransportSecurityValidator.NOOP;
private int requestMaxSize = DEFAULT_REQUEST_MAX_SIZE;
@@ -700,25 +702,10 @@ public class HttpServletSseServerTransportProvider extends HttpServlet implement
* @param securityValidator The security validator to use. Must not be null.
* @return This builder instance
* @throws IllegalArgumentException if securityValidator is null
- * @deprecated Use {@link #httpHeaderValidator(ServerHttpHeaderValidator)}
- * instead.
*/
- @Deprecated
public Builder securityValidator(ServerTransportSecurityValidator securityValidator) {
Assert.notNull(securityValidator, "Security validator must not be null");
- this.httpHeaderValidator = ServerTransportSecurityValidator.toHttpHeaderValidator(securityValidator);
- return this;
- }
-
- /**
- * Sets the HTTP header validator for validating HTTP requests.
- * @param httpHeaderValidator The HTTP header validator to use. Must not be null.
- * @return This builder instance
- * @throws IllegalArgumentException if httpHeaderValidator is null
- */
- public Builder httpHeaderValidator(ServerHttpHeaderValidator httpHeaderValidator) {
- Assert.notNull(httpHeaderValidator, "HTTP header validator must not be null");
- this.httpHeaderValidator = httpHeaderValidator;
+ this.securityValidator = securityValidator;
return this;
}
@@ -747,7 +734,7 @@ public class HttpServletSseServerTransportProvider extends HttpServlet implement
}
return new HttpServletSseServerTransportProvider(
jsonMapper == null ? McpJsonDefaults.getMapper() : jsonMapper, baseUrl, messageEndpoint,
- sseEndpoint, keepAliveInterval, contextExtractor, httpHeaderValidator, requestMaxSize);
+ sseEndpoint, keepAliveInterval, contextExtractor, securityValidator, requestMaxSize);
}
}
diff --git a/mcp-core/src/main/java/io/modelcontextprotocol/server/transport/HttpServletStatelessServerTransport.java b/mcp-core/src/main/java/io/modelcontextprotocol/server/transport/HttpServletStatelessServerTransport.java
index 9cd0d04..54f0ac0 100644
--- a/mcp-core/src/main/java/io/modelcontextprotocol/server/transport/HttpServletStatelessServerTransport.java
+++ b/mcp-core/src/main/java/io/modelcontextprotocol/server/transport/HttpServletStatelessServerTransport.java
@@ -7,6 +7,7 @@ package io.modelcontextprotocol.server.transport;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.List;
+import java.util.Map;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -67,7 +68,7 @@ public class HttpServletStatelessServerTransport extends HttpServlet implements
/**
* Security validator for validating HTTP requests.
*/
- private final ServerHttpHeaderValidator httpHeaderValidator;
+ private final ServerTransportSecurityValidator securityValidator;
/**
* Maximum size, in bytes, of a single request body accepted by this transport.
@@ -81,24 +82,24 @@ public class HttpServletStatelessServerTransport extends HttpServlet implements
* @param mcpEndpoint The endpoint URI where clients should send their JSON-RPC
* messages.
* @param contextExtractor The extractor for transport context from the request.
- * @param httpHeaderValidator The HTTP header validator for validating HTTP requests.
+ * @param securityValidator The security validator for validating HTTP requests.
* @param requestMaxSize The maximum size, in bytes, of a single request body. Must be
* positive.
* @throws IllegalArgumentException if any parameter is null
*/
private HttpServletStatelessServerTransport(McpJsonMapper jsonMapper, String mcpEndpoint,
McpTransportContextExtractor contextExtractor,
- ServerHttpHeaderValidator httpHeaderValidator, int requestMaxSize) {
+ ServerTransportSecurityValidator securityValidator, int requestMaxSize) {
Assert.notNull(jsonMapper, "jsonMapper must not be null");
Assert.notNull(mcpEndpoint, "mcpEndpoint must not be null");
Assert.notNull(contextExtractor, "contextExtractor must not be null");
- Assert.notNull(httpHeaderValidator, "HTTP header validator must not be null");
+ Assert.notNull(securityValidator, "Security validator must not be null");
Assert.isTrue(requestMaxSize > 0, "requestMaxSize must be positive");
this.jsonMapper = jsonMapper;
this.mcpEndpoint = mcpEndpoint;
this.contextExtractor = contextExtractor;
- this.httpHeaderValidator = httpHeaderValidator;
+ this.securityValidator = securityValidator;
this.requestMaxSize = requestMaxSize;
}
@@ -161,7 +162,8 @@ public class HttpServletStatelessServerTransport extends HttpServlet implements
}
try {
- this.httpHeaderValidator.validate(new HttpServletHeaderAccessor(request));
+ Map> headers = HttpServletRequestUtils.extractHeaders(request);
+ this.securityValidator.validateHeaders(headers);
}
catch (ServerTransportSecurityException e) {
response.sendError(e.getStatusCode(), e.getMessage());
@@ -298,7 +300,7 @@ public class HttpServletStatelessServerTransport extends HttpServlet implements
private McpTransportContextExtractor contextExtractor = (
serverRequest) -> McpTransportContext.EMPTY;
- private ServerHttpHeaderValidator httpHeaderValidator = ServerHttpHeaderValidator.NOOP;
+ private ServerTransportSecurityValidator securityValidator = ServerTransportSecurityValidator.NOOP;
private int requestMaxSize = DEFAULT_REQUEST_MAX_SIZE;
@@ -352,25 +354,10 @@ public class HttpServletStatelessServerTransport extends HttpServlet implements
* @param securityValidator The security validator to use. Must not be null.
* @return this builder instance
* @throws IllegalArgumentException if securityValidator is null
- * @deprecated Use {@link #httpHeaderValidator(ServerHttpHeaderValidator)}
- * instead.
*/
- @Deprecated
public Builder securityValidator(ServerTransportSecurityValidator securityValidator) {
Assert.notNull(securityValidator, "Security validator must not be null");
- this.httpHeaderValidator = ServerTransportSecurityValidator.toHttpHeaderValidator(securityValidator);
- return this;
- }
-
- /**
- * Sets the HTTP header validator for validating HTTP requests.
- * @param httpHeaderValidator The HTTP header validator to use. Must not be null.
- * @return this builder instance
- * @throws IllegalArgumentException if httpHeaderValidator is null
- */
- public Builder httpHeaderValidator(ServerHttpHeaderValidator httpHeaderValidator) {
- Assert.notNull(httpHeaderValidator, "HTTP header validator must not be null");
- this.httpHeaderValidator = httpHeaderValidator;
+ this.securityValidator = securityValidator;
return this;
}
@@ -397,7 +384,7 @@ public class HttpServletStatelessServerTransport extends HttpServlet implements
Assert.notNull(mcpEndpoint, "Message endpoint must be set");
return new HttpServletStatelessServerTransport(
jsonMapper == null ? McpJsonDefaults.getMapper() : jsonMapper, mcpEndpoint, contextExtractor,
- httpHeaderValidator, requestMaxSize);
+ securityValidator, requestMaxSize);
}
}
diff --git a/mcp-core/src/main/java/io/modelcontextprotocol/server/transport/HttpServletStreamableServerTransportProvider.java b/mcp-core/src/main/java/io/modelcontextprotocol/server/transport/HttpServletStreamableServerTransportProvider.java
index 5f883cb..324a2ec 100644
--- a/mcp-core/src/main/java/io/modelcontextprotocol/server/transport/HttpServletStreamableServerTransportProvider.java
+++ b/mcp-core/src/main/java/io/modelcontextprotocol/server/transport/HttpServletStreamableServerTransportProvider.java
@@ -9,9 +9,8 @@ import java.io.PrintWriter;
import java.time.Duration;
import java.util.ArrayList;
import java.util.List;
-import java.util.Set;
+import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
-import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.locks.ReentrantLock;
import io.modelcontextprotocol.common.McpTransportContext;
@@ -22,17 +21,12 @@ import io.modelcontextprotocol.server.McpTransportContextExtractor;
import io.modelcontextprotocol.spec.HttpHeaders;
import io.modelcontextprotocol.spec.McpError;
import io.modelcontextprotocol.spec.McpSchema;
-import io.modelcontextprotocol.spec.McpSession;
import io.modelcontextprotocol.spec.McpStreamableServerSession;
import io.modelcontextprotocol.spec.McpStreamableServerTransport;
import io.modelcontextprotocol.spec.McpStreamableServerTransportProvider;
-import io.modelcontextprotocol.spec.McpTransportException;
-import io.modelcontextprotocol.spec.ProtocolVersions;
import io.modelcontextprotocol.util.Assert;
import io.modelcontextprotocol.util.KeepAliveScheduler;
import jakarta.servlet.AsyncContext;
-import jakarta.servlet.AsyncEvent;
-import jakarta.servlet.AsyncListener;
import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
@@ -40,10 +34,8 @@ import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
-import reactor.core.Disposable;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
-import reactor.core.scheduler.Schedulers;
/**
* Server-side implementation of the Model Context Protocol (MCP) streamable transport
@@ -60,7 +52,6 @@ import reactor.core.scheduler.Schedulers;
* @author Zachary German
* @author Christian Tzolov
* @author Dariusz Jędrzejczyk
- * @author Daniel Garnier-Moiroux
* @see McpStreamableServerTransportProvider
* @see HttpServlet
*/
@@ -123,14 +114,6 @@ public class HttpServletStreamableServerTransportProvider extends HttpServlet
*/
private final ConcurrentHashMap sessions = new ConcurrentHashMap<>();
- /**
- * IDs of the sessions which received a request since the last sweep. The set is
- * swapped for an empty one on every sweep, so it only ever holds the activity of the
- * current interval. Only populated when a sweeper runs, as nothing would ever swap it
- * otherwise.
- */
- private final AtomicReference> activeSessions = new AtomicReference<>(ConcurrentHashMap.newKeySet());
-
private McpTransportContextExtractor contextExtractor;
/**
@@ -144,18 +127,10 @@ public class HttpServletStreamableServerTransportProvider extends HttpServlet
*/
private KeepAliveScheduler keepAliveScheduler;
- /**
- * Periodic eviction of the sessions no client came back to. {@code null} if no
- * sessionSweepInterval is set, in which case sessions are never reclaimed.
- */
- private final Disposable sessionSweeper;
-
- private static Duration SESSION_SWEEP_TIMEOUT = Duration.ofSeconds(30);
-
/**
* Security validator for validating HTTP requests.
*/
- private final ServerHttpHeaderValidator httpHeaderValidator;
+ private final ServerTransportSecurityValidator securityValidator;
/**
* Constructs a new HttpServletStreamableServerTransportProvider instance.
@@ -167,122 +142,38 @@ public class HttpServletStreamableServerTransportProvider extends HttpServlet
* @param contextExtractor The extractor for transport context from the request.
* @param keepAliveInterval The interval for keep-alive pings. If null, no keep-alive
* will be scheduled.
- * @param httpHeaderValidator The HTTP header validator for validating HTTP requests.
+ * @param securityValidator The security validator for validating HTTP requests.
* @param requestMaxSize The maximum size, in bytes, of a single request body. Must be
* positive.
- * @param sessionSweepInterval The interval at which idle sessions are evicted. If
- * null, no sweeping will be scheduled.
* @throws IllegalArgumentException if any parameter is null
*/
private HttpServletStreamableServerTransportProvider(McpJsonMapper jsonMapper, String mcpEndpoint,
boolean disallowDelete, McpTransportContextExtractor contextExtractor,
- Duration keepAliveInterval, ServerHttpHeaderValidator httpHeaderValidator, int requestMaxSize,
- Duration sessionSweepInterval) {
+ Duration keepAliveInterval, ServerTransportSecurityValidator securityValidator, int requestMaxSize) {
Assert.notNull(jsonMapper, "JsonMapper must not be null");
Assert.notNull(mcpEndpoint, "MCP endpoint must not be null");
Assert.notNull(contextExtractor, "Context extractor must not be null");
- Assert.notNull(httpHeaderValidator, "HTTP header validator must not be null");
+ Assert.notNull(securityValidator, "Security validator must not be null");
Assert.isTrue(requestMaxSize > 0, "requestMaxSize must be positive");
this.jsonMapper = jsonMapper;
this.mcpEndpoint = mcpEndpoint;
this.disallowDelete = disallowDelete;
this.contextExtractor = contextExtractor;
- this.httpHeaderValidator = httpHeaderValidator;
+ this.securityValidator = securityValidator;
this.requestMaxSize = requestMaxSize;
if (keepAliveInterval != null) {
- this.keepAliveScheduler = KeepAliveScheduler.builder(this::sessionsToPing)
+ this.keepAliveScheduler = KeepAliveScheduler
+ .builder(() -> (isClosing) ? Flux.empty() : Flux.fromIterable(sessions.values()))
.initialDelay(keepAliveInterval)
.interval(keepAliveInterval)
- .onPingFailure(session -> {
- // The stream the ping was written to is dead. The session survives:
- // the client may reconnect to it, and the idle timeout reclaims it if
- // it never does.
- if (session instanceof McpStreamableServerSession streamableSession) {
- streamableSession.releaseListeningStream();
- }
- })
.build();
this.keepAliveScheduler.start();
}
- this.sessionSweeper = sessionSweepInterval == null ? null : startSessionSweeper(sessionSweepInterval);
- }
-
- /**
- * Schedules the periodic eviction of idle sessions.
- * @param sessionSweepInterval the interval between two sweeps
- * @return the handle on the scheduled sweeps, to be disposed when the transport shuts
- * down
- */
- private Disposable startSessionSweeper(Duration sessionSweepInterval) {
- return Flux
- .interval(sessionSweepInterval, sessionSweepInterval,
- Schedulers.newSingle("streamable-http-server-transport-session-sweeper"))
- .concatMap(
- tick -> sweepSessions().doOnError(e -> logger.error("Session sweep failed", e)).onErrorComplete())
- .subscribe(next -> {
- }, error -> logger.error("Session sweeper error", error));
- }
-
- /**
- * Evicts the sessions no client is using anymore. A session is kept if it holds an
- * open stream, which a client can legitimately sit on without ever writing to it, or
- * if it received a request during the interval which just elapsed. Anything else is a
- * session whose client went away without deleting it: the protocol lets a client
- * reconnect to a session, so nothing else ever reclaims it.
- * @return a Mono completing once every evicted session has been closed
- */
- private Mono sweepSessions() {
- if (this.isClosing) {
- return Mono.empty();
- }
- Set active = this.activeSessions.getAndSet(ConcurrentHashMap.newKeySet());
- return Flux.fromIterable(this.sessions.values()).filter(session -> {
- if (session.hasOpenStream() || active.contains(session.getId())) {
- return false;
- }
- return this.sessions.remove(session.getId(), session);
- }).flatMap(session -> {
- logger.debug("Evicting idle session {}", session.getId());
- return session.closeGracefully()
- .timeout(SESSION_SWEEP_TIMEOUT)
- .doOnError(e -> logger.warn("Failed to close idle session {}: {}", session.getId(), e.getMessage()))
- .onErrorComplete();
- }).then();
- }
-
- /**
- * Records that the given session is being used, so that the next sweep does not
- * mistake it for a session whose client is gone.
- * @param sessionId the session the current request belongs to
- */
- private void markSessionActive(String sessionId) {
- if (this.sessionSweeper == null) {
- // No sweep ever swaps the set of active sessions, so recording activity would
- // only accumulate the ID of every session which ever issued a request
- return;
- }
- this.activeSessions.get().add(sessionId);
- }
-
- /**
- * Returns the sessions a keep-alive ping can be sent to, that is the sessions having
- * a listening stream. A session without one, e.g. a client which only ever issues
- * POST requests, has nothing to write a ping to: pinging it would fail on every
- * interval without ever telling us anything about the client being alive.
- * @return the sessions to ping
- */
- private Flux sessionsToPing() {
- if (this.isClosing) {
- return Flux.empty();
- }
- return Flux.fromIterable(this.sessions.values())
- .filter(McpStreamableServerSession::hasListeningStream)
- .cast(McpSession.class);
}
@Override
@@ -357,9 +248,6 @@ public class HttpServletStreamableServerTransportProvider extends HttpServlet
if (this.keepAliveScheduler != null) {
this.keepAliveScheduler.shutdown();
}
- if (this.sessionSweeper != null) {
- this.sessionSweeper.dispose();
- }
});
}
@@ -386,7 +274,8 @@ public class HttpServletStreamableServerTransportProvider extends HttpServlet
}
try {
- this.httpHeaderValidator.validate(new HttpServletHeaderAccessor(request));
+ Map> headers = HttpServletRequestUtils.extractHeaders(request);
+ this.securityValidator.validateHeaders(headers);
}
catch (ServerTransportSecurityException e) {
response.sendError(e.getStatusCode(), e.getMessage());
@@ -419,7 +308,6 @@ public class HttpServletStreamableServerTransportProvider extends HttpServlet
response.sendError(HttpServletResponse.SC_NOT_FOUND);
return;
}
- this.markSessionActive(sessionId);
logger.debug("Handling GET request for session: {}", sessionId);
@@ -437,21 +325,61 @@ public class HttpServletStreamableServerTransportProvider extends HttpServlet
HttpServletStreamableMcpSessionTransport sessionTransport = new HttpServletStreamableMcpSessionTransport(
sessionId, asyncContext, response.getWriter());
- // Replay the messages the client missed while its stream was broken
- String lastEventId = request.getHeader(HttpHeaders.LAST_EVENT_ID);
- if (lastEventId != null
- && !this.tryReplayMissedMessages(session, lastEventId, sessionTransport, transportContext)) {
- // The replay failed and already closed the transport
- return;
+ // Check if this is a replay request
+ if (request.getHeader(HttpHeaders.LAST_EVENT_ID) != null) {
+ String lastId = request.getHeader(HttpHeaders.LAST_EVENT_ID);
+
+ try {
+ session.replay(lastId)
+ .contextWrite(ctx -> ctx.put(McpTransportContext.KEY, transportContext))
+ .toIterable()
+ .forEach(message -> {
+ try {
+ sessionTransport.sendMessage(message)
+ .contextWrite(ctx -> ctx.put(McpTransportContext.KEY, transportContext))
+ .block();
+ }
+ catch (Exception e) {
+ logger.error("Failed to replay message: {}", e.getMessage());
+ asyncContext.complete();
+ }
+ });
+ }
+ catch (Exception e) {
+ logger.error("Failed to replay messages: {}", e.getMessage());
+ asyncContext.complete();
+ }
}
+ else {
+ // Establish new listening stream
+ McpStreamableServerSession.McpStreamableServerSessionStream listeningStream = session
+ .listeningStream(sessionTransport);
- // Establish the listening stream. Resumed streams are registered too, so
- // that the session keeps delivering messages to the reconnected client and
- // the async context is completed once the client goes away.
- McpStreamableServerSession.McpStreamableServerSessionStream listeningStream = session
- .listeningStream(sessionTransport);
+ asyncContext.addListener(new jakarta.servlet.AsyncListener() {
+ @Override
+ public void onComplete(jakarta.servlet.AsyncEvent event) throws IOException {
+ logger.debug("SSE connection completed for session: {}", sessionId);
+ listeningStream.close();
+ }
- registerAsyncLifecycle(asyncContext, sessionId, listeningStream::releaseTransport);
+ @Override
+ public void onTimeout(jakarta.servlet.AsyncEvent event) throws IOException {
+ logger.debug("SSE connection timed out for session: {}", sessionId);
+ listeningStream.close();
+ }
+
+ @Override
+ public void onError(jakarta.servlet.AsyncEvent event) throws IOException {
+ logger.debug("SSE connection error for session: {}", sessionId);
+ listeningStream.close();
+ }
+
+ @Override
+ public void onStartAsync(jakarta.servlet.AsyncEvent event) throws IOException {
+ // No action needed
+ }
+ });
+ }
}
catch (Exception e) {
logger.error("Failed to handle GET request for session {}: {}", sessionId, e.getMessage());
@@ -486,7 +414,8 @@ public class HttpServletStreamableServerTransportProvider extends HttpServlet
}
try {
- this.httpHeaderValidator.validate(new HttpServletHeaderAccessor(request));
+ Map> headers = HttpServletRequestUtils.extractHeaders(request);
+ this.securityValidator.validateHeaders(headers);
}
catch (ServerTransportSecurityException e) {
response.sendError(e.getStatusCode(), e.getMessage());
@@ -525,7 +454,6 @@ public class HttpServletStreamableServerTransportProvider extends HttpServlet
});
McpStreamableServerSession.McpStreamableServerSessionInit init = this.sessionFactory
.startSession(initializeRequest);
- this.markSessionActive(init.session().getId());
this.sessions.put(init.session().getId(), init.session());
try {
@@ -577,8 +505,6 @@ public class HttpServletStreamableServerTransportProvider extends HttpServlet
return;
}
- this.markSessionActive(sessionId);
-
if (message instanceof McpSchema.JSONRPCResponse jsonrpcResponse) {
session.accept(jsonrpcResponse)
.contextWrite(ctx -> ctx.put(McpTransportContext.KEY, transportContext))
@@ -604,16 +530,8 @@ public class HttpServletStreamableServerTransportProvider extends HttpServlet
HttpServletStreamableMcpSessionTransport sessionTransport = new HttpServletStreamableMcpSessionTransport(
sessionId, asyncContext, response.getWriter());
- // The listener is given the stream rather than its transport, so that the
- // end of the connection detaches the stream from the session instead of
- // only dropping the socket: a stream outliving its connection keeps the
- // session looking busy and spares it from the sweeper
- McpStreamableServerSession.McpStreamableServerSessionStream responseStream = session
- .responseStream(sessionTransport);
- registerAsyncLifecycle(asyncContext, sessionId, responseStream::releaseTransport);
-
try {
- responseStream.handle(jsonrpcRequest)
+ session.responseStream(jsonrpcRequest, sessionTransport)
.contextWrite(ctx -> ctx.put(McpTransportContext.KEY, transportContext))
.block();
}
@@ -675,7 +593,8 @@ public class HttpServletStreamableServerTransportProvider extends HttpServlet
}
try {
- this.httpHeaderValidator.validate(new HttpServletHeaderAccessor(request));
+ Map> headers = HttpServletRequestUtils.extractHeaders(request);
+ this.securityValidator.validateHeaders(headers);
}
catch (ServerTransportSecurityException e) {
response.sendError(e.getStatusCode(), e.getMessage());
@@ -767,70 +686,6 @@ public class HttpServletStreamableServerTransportProvider extends HttpServlet
super.destroy();
}
- /**
- * Replays the messages the client missed while its SSE stream was broken.
- * @param session the session the client is resuming
- * @param lastEventId the ID of the last event received by the client
- * @param sessionTransport the transport of the resumed SSE stream
- * @param transportContext the context extracted from the request
- * @return {@code true} if the replay completed, {@code false} if it failed, in which
- * case the transport has been closed
- */
- private static boolean tryReplayMissedMessages(McpStreamableServerSession session, String lastEventId,
- McpStreamableServerTransport sessionTransport, McpTransportContext transportContext) {
- try {
- for (McpSchema.JSONRPCMessage message : session.replay(lastEventId)
- .contextWrite(ctx -> ctx.put(McpTransportContext.KEY, transportContext))
- .toIterable()) {
- sessionTransport.sendMessage(message)
- .contextWrite(ctx -> ctx.put(McpTransportContext.KEY, transportContext))
- .block();
- }
- return true;
- }
- catch (Exception e) {
- logger.error("Failed to replay messages for session {}: {}", session.getId(), e.getMessage());
- sessionTransport.close();
- return false;
- }
- }
-
- /**
- * Registers a listener releasing the SSE stream carried by the given asynchronous
- * request once the container is done with it, whether the client went away, the
- * request timed out or it errored. Without this, the connection is left open, holding
- * on to a socket and a container thread, until the process restarts.
- * @param asyncContext the asynchronous context of the SSE request
- * @param sessionId the session the stream belongs to
- * @param onConnectionEnd the action releasing the stream
- */
- private static void registerAsyncLifecycle(AsyncContext asyncContext, String sessionId, Runnable onConnectionEnd) {
- asyncContext.addListener(new AsyncListener() {
- @Override
- public void onComplete(AsyncEvent event) throws IOException {
- logger.debug("SSE connection completed for session: {}", sessionId);
- onConnectionEnd.run();
- }
-
- @Override
- public void onTimeout(AsyncEvent event) throws IOException {
- logger.debug("SSE connection timed out for session: {}", sessionId);
- onConnectionEnd.run();
- }
-
- @Override
- public void onError(AsyncEvent event) throws IOException {
- logger.debug("SSE connection error for session: {}", sessionId);
- onConnectionEnd.run();
- }
-
- @Override
- public void onStartAsync(AsyncEvent event) throws IOException {
- // No action needed
- }
- });
- }
-
/**
* Implementation of McpStreamableServerTransport for HttpServlet SSE sessions. This
* class handles the transport-level communication for a specific client session.
@@ -840,6 +695,7 @@ public class HttpServletStreamableServerTransportProvider extends HttpServlet
* underlying PrintWriter to prevent race conditions when multiple threads attempt to
* send messages concurrently.
*/
+
private class HttpServletStreamableMcpSessionTransport implements McpStreamableServerTransport {
private final String sessionId;
@@ -903,17 +759,9 @@ public class HttpServletStreamableServerTransportProvider extends HttpServlet
logger.debug("Message sent to session {} with ID {}", this.sessionId, messageId);
}
catch (Exception e) {
- // The connection is gone, the session is not: the client may come
- // back for it, and the sweeper reclaims it if it never does
logger.error("Failed to send message to session {}: {}", this.sessionId, e.getMessage());
- this.close();
- // Surfaced to the caller rather than swallowed: whoever is writing to
- // this stream has to learn that it no longer leads anywhere, or it
- // keeps producing messages for a client which is gone. A request
- // being streamed a response would never finish, holding on to the
- // container thread which has to be given back before the end of the
- // connection can be acted upon.
- throw new McpTransportException("Failed to send message to session " + this.sessionId, e);
+ HttpServletStreamableServerTransportProvider.this.sessions.remove(this.sessionId);
+ this.asyncContext.complete();
}
finally {
lock.unlock();
@@ -957,6 +805,8 @@ public class HttpServletStreamableServerTransportProvider extends HttpServlet
}
this.closed = true;
+
+ // HttpServletStreamableServerTransportProvider.this.sessions.remove(this.sessionId);
this.asyncContext.complete();
logger.debug("Successfully completed async context for session {}", sessionId);
}
@@ -970,12 +820,6 @@ public class HttpServletStreamableServerTransportProvider extends HttpServlet
}
- @Override
- public List protocolVersions() {
- return List.of(ProtocolVersions.MCP_2025_03_26, ProtocolVersions.MCP_2025_06_18,
- ProtocolVersions.MCP_2025_11_25);
- }
-
public static Builder builder() {
return new Builder();
}
@@ -995,14 +839,12 @@ public class HttpServletStreamableServerTransportProvider extends HttpServlet
private McpTransportContextExtractor contextExtractor = (
serverRequest) -> McpTransportContext.EMPTY;
- private Duration keepAliveInterval = Duration.ofMinutes(30);
+ private Duration keepAliveInterval;
- private ServerHttpHeaderValidator httpHeaderValidator = ServerHttpHeaderValidator.NOOP;
+ private ServerTransportSecurityValidator securityValidator = ServerTransportSecurityValidator.NOOP;
private int requestMaxSize = DEFAULT_REQUEST_MAX_SIZE;
- private Duration sessionSweepInterval = Duration.ofMinutes(30);
-
/**
* Sets the JsonMapper to use for JSON serialization/deserialization of MCP
* messages.
@@ -1054,7 +896,7 @@ public class HttpServletStreamableServerTransportProvider extends HttpServlet
* Sets the keep-alive interval for the transport. If set, a keep-alive scheduler
* will be activated to periodically ping active sessions.
* @param keepAliveInterval The interval for keep-alive pings. If null, no
- * keep-alive will be scheduled. Defaults to 30 minutes.
+ * keep-alive will be scheduled.
* @return this builder instance
*/
public Builder keepAliveInterval(Duration keepAliveInterval) {
@@ -1067,25 +909,10 @@ public class HttpServletStreamableServerTransportProvider extends HttpServlet
* @param securityValidator The security validator to use. Must not be null.
* @return this builder instance
* @throws IllegalArgumentException if securityValidator is null
- * @deprecated Use {@link #httpHeaderValidator(ServerHttpHeaderValidator)}
- * instead.
*/
- @Deprecated
public Builder securityValidator(ServerTransportSecurityValidator securityValidator) {
Assert.notNull(securityValidator, "Security validator must not be null");
- this.httpHeaderValidator = ServerTransportSecurityValidator.toHttpHeaderValidator(securityValidator);
- return this;
- }
-
- /**
- * Sets the HTTP header validator for validating HTTP requests.
- * @param httpHeaderValidator The HTTP header validator to use. Must not be null.
- * @return this builder instance
- * @throws IllegalArgumentException if httpHeaderValidator is null
- */
- public Builder httpHeaderValidator(ServerHttpHeaderValidator httpHeaderValidator) {
- Assert.notNull(httpHeaderValidator, "HTTP header validator must not be null");
- this.httpHeaderValidator = httpHeaderValidator;
+ this.securityValidator = securityValidator;
return this;
}
@@ -1102,22 +929,6 @@ public class HttpServletStreamableServerTransportProvider extends HttpServlet
return this;
}
- /**
- * Sets the interval at which idle sessions are evicted. A session is idle once it
- * holds no open stream and has received no request for a full interval, which is
- * how a session whose client went away without deleting it looks. Nothing else
- * reclaims those sessions, as the protocol lets a client reconnect to a session
- * it has been disconnected from.
- * @param sessionSweepInterval The interval between two sweeps. If null, no
- * sweeping will be scheduled and sessions are kept until they are deleted or the
- * server shuts down. Defaults to 30 minutes.
- * @return this builder instance
- */
- public Builder sessionSweepInterval(Duration sessionSweepInterval) {
- this.sessionSweepInterval = sessionSweepInterval;
- return this;
- }
-
/**
* Builds a new instance of {@link HttpServletStreamableServerTransportProvider}
* with the configured settings.
@@ -1128,7 +939,7 @@ public class HttpServletStreamableServerTransportProvider extends HttpServlet
Assert.notNull(this.mcpEndpoint, "MCP endpoint must be set");
return new HttpServletStreamableServerTransportProvider(
jsonMapper == null ? McpJsonDefaults.getMapper() : jsonMapper, mcpEndpoint, disallowDelete,
- contextExtractor, keepAliveInterval, httpHeaderValidator, requestMaxSize, sessionSweepInterval);
+ contextExtractor, keepAliveInterval, securityValidator, requestMaxSize);
}
}
diff --git a/mcp-core/src/main/java/io/modelcontextprotocol/server/transport/ServerHttpHeaderValidator.java b/mcp-core/src/main/java/io/modelcontextprotocol/server/transport/ServerHttpHeaderValidator.java
deleted file mode 100644
index 6f2a3dc..0000000
--- a/mcp-core/src/main/java/io/modelcontextprotocol/server/transport/ServerHttpHeaderValidator.java
+++ /dev/null
@@ -1,30 +0,0 @@
-/*
- * Copyright 2026-2026 the original author or authors.
- */
-
-package io.modelcontextprotocol.server.transport;
-
-/**
- * Validates HTTP request headers in server transports.
- *
- * @author Neeraj Bhatt
- * @see HeaderAccessor
- * @see ServerTransportSecurityException
- */
-@FunctionalInterface
-public interface ServerHttpHeaderValidator {
-
- /**
- * A no-op validator that accepts all requests without validation.
- */
- ServerHttpHeaderValidator NOOP = headerAccessor -> {
- };
-
- /**
- * Validates the HTTP headers from an incoming request.
- * @param headerAccessor provides access to request headers
- * @throws ServerTransportSecurityException if validation fails
- */
- void validate(HeaderAccessor headerAccessor) throws ServerTransportSecurityException;
-
-}
diff --git a/mcp-core/src/main/java/io/modelcontextprotocol/server/transport/ServerTransportSecurityValidator.java b/mcp-core/src/main/java/io/modelcontextprotocol/server/transport/ServerTransportSecurityValidator.java
index 023f7f3..ce80593 100644
--- a/mcp-core/src/main/java/io/modelcontextprotocol/server/transport/ServerTransportSecurityValidator.java
+++ b/mcp-core/src/main/java/io/modelcontextprotocol/server/transport/ServerTransportSecurityValidator.java
@@ -6,8 +6,6 @@ package io.modelcontextprotocol.server.transport;
import java.util.List;
import java.util.Map;
-import java.util.function.Function;
-import java.util.stream.Collectors;
/**
* Interface for validating HTTP requests in server transports. Implementations can
@@ -15,12 +13,9 @@ import java.util.stream.Collectors;
* to the MCP specification.
*
* @author Daniel Garnier-Moiroux
- * @see ServerHttpHeaderValidator
+ * @see DefaultServerTransportSecurityValidator
* @see ServerTransportSecurityException
- * @deprecated Use {@link ServerHttpHeaderValidator} instead. This interface will be
- * removed in a future major version.
*/
-@Deprecated
@FunctionalInterface
public interface ServerTransportSecurityValidator {
@@ -30,20 +25,6 @@ public interface ServerTransportSecurityValidator {
ServerTransportSecurityValidator NOOP = headers -> {
};
- /**
- * Adapts a legacy map-based validator to a header accessor validator.
- * @param validator the legacy validator to adapt
- * @return a header accessor validator delegating to {@code validator}
- */
- static ServerHttpHeaderValidator toHttpHeaderValidator(ServerTransportSecurityValidator validator) {
- return accessor -> {
- var collectedHeaders = accessor.getHeaderNames()
- .stream()
- .collect(Collectors.toUnmodifiableMap(Function.identity(), accessor::getHeader));
- validator.validateHeaders(collectedHeaders);
- };
- }
-
/**
* Validates the HTTP headers from an incoming request.
* @param headers A map of header names to their values (multi-valued headers
diff --git a/mcp-core/src/main/java/io/modelcontextprotocol/server/transport/StdioServerTransportProvider.java b/mcp-core/src/main/java/io/modelcontextprotocol/server/transport/StdioServerTransportProvider.java
index 8370e41..cc51233 100644
--- a/mcp-core/src/main/java/io/modelcontextprotocol/server/transport/StdioServerTransportProvider.java
+++ b/mcp-core/src/main/java/io/modelcontextprotocol/server/transport/StdioServerTransportProvider.java
@@ -10,13 +10,13 @@ import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.nio.charset.StandardCharsets;
-import java.time.Duration;
import java.util.List;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Function;
import io.modelcontextprotocol.json.TypeRef;
+import io.modelcontextprotocol.spec.McpError;
import io.modelcontextprotocol.spec.McpSchema;
import io.modelcontextprotocol.spec.McpSchema.JSONRPCMessage;
import io.modelcontextprotocol.spec.McpServerSession;
@@ -175,12 +175,11 @@ public class StdioServerTransportProvider implements McpServerTransportProvider
public Mono sendMessage(McpSchema.JSONRPCMessage message) {
return Mono.zip(inboundReady.asMono(), outboundReady.asMono()).then(Mono.defer(() -> {
- try {
- outboundSink.emitNext(message, Sinks.EmitFailureHandler.busyLooping(Duration.ofMillis(100)));
+ if (outboundSink.tryEmitNext(message).isSuccess()) {
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"));
}
}));
}
diff --git a/mcp-core/src/main/java/io/modelcontextprotocol/spec/McpStreamableServerSession.java b/mcp-core/src/main/java/io/modelcontextprotocol/spec/McpStreamableServerSession.java
index 5ac0863..e7fac7b 100644
--- a/mcp-core/src/main/java/io/modelcontextprotocol/spec/McpStreamableServerSession.java
+++ b/mcp-core/src/main/java/io/modelcontextprotocol/spec/McpStreamableServerSession.java
@@ -1,31 +1,29 @@
/*
- * Copyright 2024-2026 the original author or authors.
+ * Copyright 2024-2025 the original author or authors.
*/
package io.modelcontextprotocol.spec;
import java.time.Duration;
-import java.util.ArrayList;
-import java.util.List;
import java.util.Map;
-import java.util.Set;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.AtomicReference;
-import java.util.function.Predicate;
import java.util.function.Supplier;
-import io.modelcontextprotocol.common.McpTransportContext;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
import io.modelcontextprotocol.json.TypeRef;
+
+import io.modelcontextprotocol.common.McpTransportContext;
import io.modelcontextprotocol.json.schema.JsonSchemaValidator;
import io.modelcontextprotocol.server.McpAsyncServerExchange;
import io.modelcontextprotocol.server.McpNotificationHandler;
import io.modelcontextprotocol.server.McpRequestHandler;
import io.modelcontextprotocol.spec.McpSchema.ErrorCodes;
import io.modelcontextprotocol.util.Assert;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.core.publisher.MonoSink;
@@ -38,26 +36,12 @@ import reactor.core.publisher.MonoSink;
*
* @author Dariusz Jędrzejczyk
* @author Yanming Zhou
- * @author Daniel Garnier-Moiroux
*/
public class McpStreamableServerSession implements McpLoggableSession {
private static final Logger logger = LoggerFactory.getLogger(McpStreamableServerSession.class);
- /**
- * Every server-initiated request still awaiting its response, keyed by request ID.
- * Tracked per session rather than per stream, because the client answers over a
- * separate HTTP POST which outlives the stream the request was sent on: a stream
- * going away must not lose the requests it carried, but the end of the session must
- * resolve all of them.
- */
- private final ConcurrentHashMap pendingRequests = new ConcurrentHashMap<>();
-
- /**
- * Every stream with a connection currently attached, whether the listening stream or
- * a POST response stream, so that they can all be released when the session ends.
- */
- private final Set openStreams = ConcurrentHashMap.newKeySet();
+ private final ConcurrentHashMap