diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md
index 8658ac785..ac967ff9f 100644
--- a/.github/ISSUE_TEMPLATE/bug_report.md
+++ b/.github/ISSUE_TEMPLATE/bug_report.md
@@ -2,7 +2,7 @@
name: Bug report
about: Create a bug report to help us improve the project
title: ''
-labels: 'type: bug, status: waiting-for-triage'
+labels: status/waiting for triage
assignees: ''
---
diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml
index a07b6a840..c903204cd 100644
--- a/.github/ISSUE_TEMPLATE/config.yml
+++ b/.github/ISSUE_TEMPLATE/config.yml
@@ -1,5 +1,5 @@
blank_issues_enabled: false
contact_links:
- name: Questions and Community Support
- url: https://stackoverflow.com/questions/tagged/spring-ai-mcp
- about: Please ask and answer questions on StackOverflow with the spring-ai tag
+ url: https://stackoverflow.com/questions/tagged/mcp-java-sdk
+ about: Please ask and answer questions on StackOverflow with the mcp-java-sdk tag
diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md
index aba7d39de..16ba64eef 100644
--- a/.github/ISSUE_TEMPLATE/feature_request.md
+++ b/.github/ISSUE_TEMPLATE/feature_request.md
@@ -2,7 +2,7 @@
name: Feature request
about: Suggest an idea for this project
title: ''
-labels: 'status: waiting-for-triage, type: feature'
+labels: status/waiting for triage
assignees: ''
---
diff --git a/.github/ISSUE_TEMPLATE/miscellaneous.md b/.github/ISSUE_TEMPLATE/miscellaneous.md
index d77c625c3..1db42e3b9 100644
--- a/.github/ISSUE_TEMPLATE/miscellaneous.md
+++ b/.github/ISSUE_TEMPLATE/miscellaneous.md
@@ -2,7 +2,7 @@
name: Miscellaneous
about: Suggest an improvement for this project
title: ''
-labels: 'status: waiting-for-triage'
+labels: status/waiting for triage
assignees: ''
---
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
new file mode 100644
index 000000000..c25de745b
--- /dev/null
+++ b/.github/dependabot.yml
@@ -0,0 +1,22 @@
+version: 2
+updates:
+ - package-ecosystem: 'github-actions'
+ directory: '/'
+ schedule:
+ interval: monthly
+ - package-ecosystem: 'maven'
+ directory: '/'
+ schedule:
+ interval: monthly
+ open-pull-requests-limit: 10
+ ignore:
+ # Freeze production dependencies of mcp-core
+ - dependency-name: 'org.slf4j:slf4j-api'
+ - dependency-name: 'com.fasterxml.jackson.core:jackson-annotations'
+ - dependency-name: 'tools.jackson.core:jackson-databind'
+ - dependency-name: 'io.projectreactor:reactor-bom'
+ - dependency-name: 'io.projectreactor:reactor-core'
+ - dependency-name: 'jakarta.servlet:jakarta.servlet-api'
+ # mcp-json-jackson2 and mcp-json-jackson3 dependencies
+ - dependency-name: 'com.fasterxml.jackson.core:jackson-databind'
+ - dependency-name: 'com.networknt:json-schema-validator'
\ No newline at end of file
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 7c73d9f38..0c79351a6 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -5,7 +5,7 @@ on:
jobs:
build:
- name: Build branch
+ name: Build and Test
runs-on: ubuntu-latest
steps:
- name: Checkout source code
@@ -20,3 +20,20 @@ jobs:
- name: Build
run: mvn verify
+
+ jackson2-tests:
+ name: Jackson 2 Integration Tests
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout source code
+ uses: actions/checkout@v4
+
+ - name: Set up JDK 17
+ uses: actions/setup-java@v4
+ with:
+ java-version: '17'
+ distribution: 'temurin'
+ cache: 'maven'
+
+ - name: Jackson 2 Integration Tests
+ run: mvn -pl mcp-test -am -Pjackson2 test
diff --git a/.github/workflows/conformance.yml b/.github/workflows/conformance.yml
new file mode 100644
index 000000000..efd06938f
--- /dev/null
+++ b/.github/workflows/conformance.yml
@@ -0,0 +1,104 @@
+name: Conformance Tests
+
+on:
+ pull_request: {}
+ push:
+ branches: [main]
+ workflow_dispatch:
+
+jobs:
+ server:
+ name: Server Conformance
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Set up JDK 17
+ uses: actions/setup-java@v4
+ with:
+ java-version: '17'
+ distribution: 'temurin'
+ cache: 'maven'
+
+ - name: Build and start server
+ run: |
+ mvn clean install -DskipTests
+ mvn exec:java -pl conformance-tests/server-servlet -Dexec.mainClass="io.modelcontextprotocol.conformance.server.ConformanceServlet" &
+ timeout 30 bash -c 'until curl -s http://localhost:8080/mcp > /dev/null 2>&1; do sleep 0.5; done'
+
+ - name: Run conformance tests
+ uses: modelcontextprotocol/conformance@v0.1.11
+ with:
+ mode: server
+ url: http://localhost:8080/mcp
+ suite: active
+ expected-failures: ./conformance-tests/conformance-baseline.yml
+
+ client:
+ name: Client Conformance
+ runs-on: ubuntu-latest
+ strategy:
+ matrix:
+ scenario: [initialize, tools_call, elicitation-sep1034-client-defaults, sse-retry]
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Set up JDK 17
+ uses: actions/setup-java@v4
+ with:
+ java-version: '17'
+ distribution: 'temurin'
+ cache: 'maven'
+
+ - name: Build client
+ run: mvn clean install -DskipTests
+
+ - name: Run conformance test
+ uses: modelcontextprotocol/conformance@v0.1.11
+ with:
+ mode: client
+ command: 'java -jar conformance-tests/client-jdk-http-client/target/client-jdk-http-client-*-SNAPSHOT.jar'
+ scenario: ${{ matrix.scenario }}
+ expected-failures: ./conformance-tests/conformance-baseline.yml
+
+ auth:
+ name: Auth Conformance
+ runs-on: ubuntu-latest
+ strategy:
+ matrix:
+ scenario:
+ - auth/metadata-default
+ - auth/metadata-var1
+ - auth/metadata-var2
+ - auth/metadata-var3
+ - auth/basic-cimd
+ - auth/scope-from-www-authenticate
+ - auth/scope-from-scopes-supported
+ - auth/scope-omitted-when-undefined
+ - auth/scope-step-up
+ - auth/scope-retry-limit
+ - auth/token-endpoint-auth-basic
+ - auth/token-endpoint-auth-post
+ - auth/token-endpoint-auth-none
+ - auth/pre-registration
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Set up JDK 17
+ uses: actions/setup-java@v4
+ with:
+ java-version: '17'
+ distribution: 'temurin'
+ cache: 'maven'
+
+ - name: Build client
+ run: mvn clean install -DskipTests
+
+ - name: Run conformance test
+ uses: modelcontextprotocol/conformance@v0.1.15
+ with:
+ node-version: '22' # see https://github.com/modelcontextprotocol/conformance/pull/162
+ mode: client
+ command: 'java -jar conformance-tests/client-spring-http-client/target/client-spring-http-client-*-SNAPSHOT.jar'
+ scenario: ${{ matrix.scenario }}
+ expected-failures: ./conformance-tests/conformance-baseline.yml
\ No newline at end of file
diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml
new file mode 100644
index 000000000..56b5a1207
--- /dev/null
+++ b/.github/workflows/docs.yml
@@ -0,0 +1,54 @@
+name: Deploy Documentation
+
+on:
+ push:
+ branches:
+ - main
+ paths:
+ - 'docs/**'
+ - 'mkdocs.yml'
+ release:
+ types:
+ - published
+ workflow_dispatch:
+
+permissions:
+ contents: write
+
+jobs:
+ deploy:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ with:
+ fetch-depth: 0
+
+ - uses: actions/setup-python@v5
+ with:
+ python-version: 3.x
+
+ - run: pip install mkdocs-material mike
+
+ - name: Configure git user
+ run: |
+ git config user.name "github-actions[bot]"
+ git config user.email "github-actions[bot]@users.noreply.github.com"
+
+ - name: Deploy docs (push to main)
+ if: github.event_name == 'push' || github.event_name == 'workflow_dispatch'
+ run: |
+ PROJECT_VERSION=$(mvn help:evaluate -Dexpression=project.version --quiet -DforceStdout)
+ if [[ "${PROJECT_VERSION}" == *-SNAPSHOT ]]; then
+ ALIAS="latest-snapshot"
+ else
+ ALIAS="latest"
+ fi
+ mike deploy --push --update-aliases "${PROJECT_VERSION}" "${ALIAS}"
+ mike set-default latest --push
+
+ - name: Deploy versioned docs (release)
+ if: github.event_name == 'release'
+ run: |
+ VERSION=${GITHUB_REF_NAME}
+ mike deploy --push --update-aliases "${VERSION}" latest
+ mike set-default latest --push
diff --git a/.github/workflows/maven-central-release.yml b/.github/workflows/maven-central-release.yml
index c6c9d3ab6..8df337ec8 100644
--- a/.github/workflows/maven-central-release.yml
+++ b/.github/workflows/maven-central-release.yml
@@ -25,7 +25,10 @@ jobs:
uses: actions/setup-node@v4
with:
node-version: '20'
-
+
+ - name: Jackson 2 Integration Tests
+ run: mvn -pl mcp-test -am -Pjackson2 test
+
- name: Build and Test
run: mvn clean verify
diff --git a/.github/workflows/publish-snapshot.yml b/.github/workflows/publish-snapshot.yml
index 5d9b4aa39..1a61d336c 100644
--- a/.github/workflows/publish-snapshot.yml
+++ b/.github/workflows/publish-snapshot.yml
@@ -32,6 +32,9 @@ jobs:
- name: Generate Java docs
run: mvn -Pjavadoc -B javadoc:aggregate
+ - name: Jackson 2 Integration Tests
+ run: mvn -pl mcp-test -am -Pjackson2 test
+
- name: Build with Maven and deploy to Sonatype snapshot repository
env:
MAVEN_USERNAME: ${{ secrets.OSSRH_USERNAME }}
diff --git a/.gitignore b/.gitignore
index b80dac20d..1fc975c0a 100644
--- a/.gitignore
+++ b/.gitignore
@@ -10,6 +10,7 @@ build/
out
/.gradletasknamecache
**/*.flattened-pom.xml
+**/dependency-reduced-pom.xml
### IDE - Eclipse/STS ###
.apt_generated
@@ -56,6 +57,9 @@ node_modules/
package-lock.json
package.json
+### MkDocs ###
+site/
+
### Other ###
.antlr/
.profiler/
diff --git a/DEPENDENCY_POLICY.md b/DEPENDENCY_POLICY.md
new file mode 100644
index 000000000..5714a6b57
--- /dev/null
+++ b/DEPENDENCY_POLICY.md
@@ -0,0 +1,26 @@
+# Dependency Policy
+
+As a library consumed by downstream projects, the MCP Java SDK takes a conservative approach to dependency updates. Dependencies are kept stable unless there is a specific reason to update, such as a security vulnerability, a bug fix, or a need for new functionality.
+
+## Update Triggers
+
+Dependencies are updated when:
+
+- A **security vulnerability** is disclosed (via GitHub security alerts).
+- A bug in a dependency directly affects the SDK.
+- A new dependency feature is needed for SDK development.
+- A dependency drops support for a Java version the SDK still targets.
+
+Routine version bumps without a clear motivation are avoided to minimize churn for downstream consumers.
+
+## What We Don't Do
+
+The SDK does not run scheduled version bumps for production Maven dependencies. Updating a dependency can force downstream consumers to adopt that update transitively, which can be disruptive for projects with strict dependency policies.
+
+Dependencies are only updated when there is a concrete reason, not simply because a newer version is available.
+
+## Automated Tooling
+
+- **GitHub security updates** are enabled at the repository level and automatically open pull requests for Maven packages with known vulnerabilities. This is a GitHub repo setting, separate from the `dependabot.yml` configuration.
+- **GitHub Actions versions** are kept up to date via Dependabot on a monthly schedule (see `.github/dependabot.yml`).
+- **Maven dependencies** are monitored via Dependabot on a monthly schedule for non-production updates only (see `.github/dependabot.yml`).
diff --git a/MIGRATION-1.0.md b/MIGRATION-1.0.md
new file mode 100644
index 000000000..d1ef0fae8
--- /dev/null
+++ b/MIGRATION-1.0.md
@@ -0,0 +1,300 @@
+# MCP Java SDK Migration Guide: 0.18.1 → 1.0.0
+
+This document covers the breaking changes between **0.18.1** and **1.0.0** of the MCP Java SDK. All items listed here were already deprecated (with `@Deprecated` or `@Deprecated(forRemoval = true)`) in 0.18.1 and are now removed.
+
+> **If you are on a version earlier than 0.18.1**, upgrade progressively to **0.18.1** first. That release already provides the replacement APIs described below alongside the deprecated ones, so you can resolve all deprecation warnings before moving to 1.0.0. Many types and APIs that existed in older 0.x versions (e.g., `ClientMcpTransport`, `ServerMcpTransport`, `DefaultMcpSession`, `StdioServerTransport`, `HttpServletSseServerTransport`, `FlowSseClient`) were already removed well before 0.18.1 and are not covered here.
+
+---
+
+## 1. The `mcp` aggregator module now defaults to Jackson 3
+
+The module structure (`mcp-core`, `mcp-json-jackson2`, `mcp-json-jackson3`, `mcp`) is unchanged. What changes is the default JSON binding in the `mcp` convenience artifact:
+
+| Version | `mcp` artifact includes |
+|---|---|
+| 0.18.1 | `mcp-core` + `mcp-json-jackson2` |
+| 1.0.0 | `mcp-core` + `mcp-json-jackson3` |
+
+If your project uses **Jackson 2** (the `com.fasterxml.jackson` 2.x line), stop depending on the `mcp` aggregator and depend on the individual modules instead:
+
+```xml
+
+ io.modelcontextprotocol.sdk
+ mcp-core
+ 1.0.0-RC3
+
+
+ io.modelcontextprotocol.sdk
+ mcp-json-jackson2
+ 1.0.0-RC3
+
+```
+
+If you are ready to adopt **Jackson 3**, you can simply continue using the `mcp` aggregator:
+
+```xml
+
+ io.modelcontextprotocol.sdk
+ mcp
+ 1.0.0-RC3
+
+```
+
+### Deprecated `io.modelcontextprotocol.json.jackson` package removed
+
+In `mcp-json-jackson2`, the classes under the old `io.modelcontextprotocol.json.jackson` package (deprecated in 0.18.1) have been removed. Use the equivalent classes under `io.modelcontextprotocol.json.jackson2`:
+
+| Removed (old package) | Replacement (already available in 0.18.1) |
+|---|---|
+| `io.modelcontextprotocol.json.jackson.JacksonMcpJsonMapper` | `io.modelcontextprotocol.json.jackson2.JacksonMcpJsonMapper` |
+| `io.modelcontextprotocol.json.jackson.JacksonMcpJsonMapperSupplier` | `io.modelcontextprotocol.json.jackson2.JacksonMcpJsonMapperSupplier` |
+| `io.modelcontextprotocol.json.schema.jackson.DefaultJsonSchemaValidator` | `io.modelcontextprotocol.json.schema.jackson2.DefaultJsonSchemaValidator` |
+| `io.modelcontextprotocol.json.schema.jackson.JacksonJsonSchemaValidatorSupplier` | `io.modelcontextprotocol.json.schema.jackson2.JacksonJsonSchemaValidatorSupplier` |
+
+---
+
+## 2. Spring transport modules (`mcp-spring-webflux`, `mcp-spring-webmvc`)
+
+These modules have been moved to the **Spring AI** project starting with Spring AI 2.0. The artifact names remain the same but the **Maven group has changed**:
+
+| 0.18.1 (MCP Java SDK) | 1.0.0+ (Spring AI 2.0) |
+|---|---|
+| `io.modelcontextprotocol.sdk:mcp-spring-webflux` | `org.springframework.ai:mcp-spring-webflux` |
+| `io.modelcontextprotocol.sdk:mcp-spring-webmvc` | `org.springframework.ai:mcp-spring-webmvc` |
+
+Update your dependency coordinates:
+
+```xml
+
+
+ io.modelcontextprotocol.sdk
+ mcp-spring-webflux
+ 0.18.1
+
+
+
+
+ org.springframework.ai
+ mcp-spring-webflux
+ ${spring-ai.version}
+
+```
+
+The Java package names and class names within these artifacts are unchanged — no source code modifications are needed beyond updating the dependency coordinates.
+
+---
+
+## 3. Tool handler signature — `tool()` removed, use `toolCall()`
+
+The `tool()` method on the `McpServer` builder (both sync and async variants) has been removed. It was deprecated in 0.18.1 in favor of `toolCall()`, which accepts a handler that receives the full `CallToolRequest` instead of a raw `Map`.
+
+#### Before (deprecated, removed in 1.0.0):
+
+```java
+McpServer.sync(transportProvider)
+ .tool(
+ myTool,
+ (exchange, args) -> new CallToolResult(List.of(new TextContent("Result: " + calculate(args))), false)
+ )
+ .build();
+```
+
+#### After (already available in 0.18.1):
+
+```java
+McpServer.sync(transportProvider)
+ .toolCall(
+ myTool,
+ (exchange, request) -> CallToolResult.builder()
+ .content(List.of(new TextContent("Result: " + calculate(request.arguments()))))
+ .isError(false)
+ .build()
+ )
+ .build();
+```
+
+---
+
+## 4. `AsyncToolSpecification` / `SyncToolSpecification` — `call` field removed
+
+The deprecated `call` record component (which accepted `Map`) has been removed from both `AsyncToolSpecification` and `SyncToolSpecification`. Only `callHandler` (which accepts `CallToolRequest`) remains.
+
+The deprecated constructors that accepted a `call` function have also been removed. Use the builder:
+
+```java
+McpServerFeatures.AsyncToolSpecification.builder()
+ .tool(tool)
+ .callHandler((exchange, request) -> Mono.just(
+ CallToolResult.builder()
+ .content(List.of(new TextContent("Done")))
+ .build()))
+ .build();
+```
+
+---
+
+## 5. Content types — deprecated `audience`/`priority` constructors and accessors removed
+
+`TextContent`, `ImageContent`, and `EmbeddedResource` previously had constructors and accessors that took inline `List audience` and `Double priority` parameters. These were deprecated in favor of the `Annotations` record. The deprecated forms are now removed.
+
+#### Before (deprecated, removed in 1.0.0):
+
+```java
+new TextContent(List.of(Role.USER), 0.8, "Hello world")
+textContent.audience() // deprecated accessor
+textContent.priority() // deprecated accessor
+```
+
+#### After (already available in 0.18.1):
+
+```java
+new TextContent(new Annotations(List.of(Role.USER), 0.8), "Hello world")
+textContent.annotations().audience()
+textContent.annotations().priority()
+```
+
+The simple `new TextContent("text")` constructor continues to work.
+
+---
+
+## 6. `CallToolResult` and `Resource` — deprecated constructors removed
+
+The constructors on `CallToolResult` and `Resource` that were deprecated in 0.18.1 have been removed. Use the builders instead.
+
+#### `CallToolResult`
+
+```java
+// Removed:
+new CallToolResult(List.of(new TextContent("result")), false);
+new CallToolResult("result text", false);
+new CallToolResult(content, isError, structuredContent);
+
+// Use instead:
+CallToolResult.builder()
+ .content(List.of(new TextContent("result")))
+ .isError(false)
+ .build();
+```
+
+#### `Resource`
+
+```java
+// Removed:
+new Resource(uri, name, description, mimeType, annotations);
+new Resource(uri, name, title, description, mimeType, size, annotations);
+
+// Use instead:
+Resource.builder()
+ .uri(uri)
+ .name(name)
+ .title(title)
+ .description(description)
+ .mimeType(mimeType)
+ .size(size)
+ .annotations(annotations)
+ .build();
+```
+
+---
+
+## 7. `McpError(Object)` constructor removed
+
+The deprecated `McpError(Object error)` constructor, which was commonly used as `new McpError("message string")`, has been removed. Construct `McpError` instances using the builder with a JSON-RPC error code:
+
+```java
+// Removed:
+throw new McpError("Something went wrong");
+
+// Use instead:
+throw McpError.builder(McpSchema.ErrorCodes.INTERNAL_ERROR)
+ .message("Something went wrong")
+ .build();
+```
+
+Additionally, several places in the SDK that previously threw `McpError` for validation or state-checking purposes now throw standard Java exceptions (`IllegalStateException`, `IllegalArgumentException`). If you were catching `McpError` in those scenarios, update your catch blocks accordingly.
+
+---
+
+## 8. `McpSchema.LATEST_PROTOCOL_VERSION` constant removed
+
+The deprecated `McpSchema.LATEST_PROTOCOL_VERSION` constant has been removed. Use the `ProtocolVersions` interface directly:
+
+```java
+// Removed:
+McpSchema.LATEST_PROTOCOL_VERSION
+
+// Use instead:
+ProtocolVersions.MCP_2025_11_25
+```
+
+---
+
+## 9. Deprecated session constructors and inner interfaces removed
+
+The following deprecated constructors and inner interfaces, all of which already had replacements available in 0.18.1, have been removed:
+
+### `McpServerSession`
+
+| Removed | Replacement (available since 0.18.1) |
+|---|---|
+| Constructor with `InitNotificationHandler` parameter | Constructor without `InitNotificationHandler` — use `McpInitRequestHandler` in the map |
+| `McpServerSession.InitRequestHandler` (inner interface) | `McpInitRequestHandler` (top-level interface) |
+| `McpServerSession.RequestHandler` (inner interface) | `McpRequestHandler` (top-level interface) |
+| `McpServerSession.NotificationHandler` (inner interface) | `McpNotificationHandler` (top-level interface) |
+
+### `McpClientSession`
+
+| Removed | Replacement (available since 0.18.1) |
+|---|---|
+| Constructor without `connectHook` parameter | Constructor that accepts a `Function super Mono, ? extends Publisher> connectHook` |
+
+### `McpAsyncServerExchange`
+
+| Removed | Replacement (available since 0.18.1) |
+|---|---|
+| Constructor `McpAsyncServerExchange(McpSession, ClientCapabilities, Implementation)` | Constructor `McpAsyncServerExchange(String, McpLoggableSession, ClientCapabilities, Implementation, McpTransportContext)` |
+
+---
+
+## 10. `McpAsyncServer.loggingNotification()` / `McpSyncServer.loggingNotification()` removed
+
+The `loggingNotification(LoggingMessageNotification)` methods on `McpAsyncServer` and `McpSyncServer` were deprecated because they incorrectly broadcast to all connected clients. They have been removed. Use the per-session exchange method instead:
+
+```java
+// Removed:
+server.loggingNotification(notification);
+
+// Use instead (inside a handler with access to the exchange):
+exchange.loggingNotification(notification);
+```
+
+---
+
+## 11. `HttpClientSseClientTransport.Builder` — deprecated constructor removed
+
+The deprecated `new HttpClientSseClientTransport.Builder(String baseUri)` constructor has been removed. Use the static factory method:
+
+```java
+// Removed:
+new HttpClientSseClientTransport.Builder("http://localhost:8080")
+
+// Use instead:
+HttpClientSseClientTransport.builder("http://localhost:8080")
+```
+
+---
+
+## Summary checklist
+
+Before upgrading to 1.0.0, verify that your 0.18.1 build has **zero deprecation warnings** related to the MCP SDK. Every removal in 1.0.0 was preceded by a deprecation in 0.18.1 with a pointer to the replacement. Once you are clean on 0.18.1:
+
+1. Update your dependency versions — either bump the `mcp-bom` version, or bump the specific module dependencies you use (e.g., `mcp-core`, `mcp-json-jackson2`). If you were relying on the `mcp` aggregator, note it now pulls in Jackson 3 — switch to `mcp-core` + `mcp-json-jackson2` if you need to stay on Jackson 2.
+2. Replace `io.modelcontextprotocol.sdk:mcp-spring-webflux` / `mcp-spring-webmvc` with `org.springframework.ai:mcp-spring-webflux` / `mcp-spring-webmvc`.
+3. If you use the `mcp-json-jackson2` module, update imports from `io.modelcontextprotocol.json.jackson` to `io.modelcontextprotocol.json.jackson2` (and similarly for the schema validator package).
+4. Compile and verify — no further source changes should be needed.
+
+---
+
+## Need help?
+
+If you run into issues during migration or have questions, please open an issue or start a discussion in the [MCP Java SDK GitHub repository](https://github.com/modelcontextprotocol/java-sdk).
diff --git a/README.md b/README.md
index 436104c63..34133a796 100644
--- a/README.md
+++ b/README.md
@@ -1,7 +1,11 @@
# MCP Java SDK
+[](https://opensource.org/license/MIT)
[](https://github.com/modelcontextprotocol/java-sdk/actions/workflows/publish-snapshot.yml)
+[](https://central.sonatype.com/artifact/io.modelcontextprotocol.sdk/mcp)
+[](https://www.oracle.com/java/technologies/javase/jdk17-archive-downloads.html)
-A set of projects that provide Java SDK integration for the [Model Context Protocol](https://modelcontextprotocol.org/docs/concepts/architecture).
+
+A set of projects that provide Java SDK integration for the [Model Context Protocol](https://modelcontextprotocol.io/docs/concepts/architecture).
This SDK enables Java applications to interact with AI models and tools through a standardized interface, supporting both synchronous and asynchronous communication patterns.
## 📚 Reference Documentation
@@ -9,14 +13,17 @@ This SDK enables Java applications to interact with AI models and tools through
#### MCP Java SDK documentation
For comprehensive guides and SDK API documentation
-- [Features](https://modelcontextprotocol.io/sdk/java/mcp-overview#features) - Overview the features provided by the Java MCP SDK
-- [Architecture](https://modelcontextprotocol.io/sdk/java/mcp-overview#architecture) - Java MCP SDK architecture overview.
-- [Java Dependencies / BOM](https://modelcontextprotocol.io/sdk/java/mcp-overview#dependencies) - Java dependencies and BOM.
-- [Java MCP Client](https://modelcontextprotocol.io/sdk/java/mcp-client) - Learn how to use the MCP client to interact with MCP servers.
-- [Java MCP Server](https://modelcontextprotocol.io/sdk/java/mcp-server) - Learn how to implement and configure a MCP servers.
+- [Features](https://modelcontextprotocol.github.io/java-sdk/#features) - Overview the features provided by the Java MCP SDK
+- [Architecture](https://modelcontextprotocol.github.io/java-sdk/#architecture) - Java MCP SDK architecture overview.
+- [Java Dependencies / BOM](https://modelcontextprotocol.github.io/java-sdk/quickstart/#dependencies) - Java dependencies and BOM.
+- [Java MCP Client](https://modelcontextprotocol.github.io/java-sdk/client/) - Learn how to use the MCP client to interact with MCP servers.
+- [Java MCP Server](https://modelcontextprotocol.github.io/java-sdk/server/) - Learn how to implement and configure a MCP servers.
#### Spring AI MCP documentation
-[Spring AI MCP](https://docs.spring.io/spring-ai/reference/api/mcp/mcp-overview.html) extends the MCP Java SDK with Spring Boot integration, providing both [client](https://docs.spring.io/spring-ai/reference/api/mcp/mcp-client-boot-starter-docs.html) and [server](https://docs.spring.io/spring-ai/reference/api/mcp/mcp-server-boot-starter-docs.html) starters. Bootstrap your AI applications with MCP support using [Spring Initializer](https://start.spring.io).
+[Spring AI MCP](https://docs.spring.io/spring-ai/reference/2.0-SNAPSHOT/api/mcp/mcp-overview.html) extends the MCP Java SDK with Spring Boot integration, providing both [client](https://docs.spring.io/spring-ai/reference/2.0-SNAPSHOT/api/mcp/mcp-client-boot-starter-docs.html) and [server](https://docs.spring.io/spring-ai/reference/2.0-SNAPSHOT/api/mcp/mcp-server-boot-starter-docs.html) starters.
+The [MCP Annotations](https://docs.spring.io/spring-ai/reference/2.0-SNAPSHOT/api/mcp/mcp-annotations-overview.html) - provides annotation-based method handling for MCP servers and clients in Java.
+The [MCP Security](https://docs.spring.io/spring-ai/reference/2.0-SNAPSHOT/api/mcp/mcp-security.html) - provides comprehensive OAuth 2.0 and API key-based security support for Model Context Protocol implementations in Spring AI.
+Bootstrap your AI applications with MCP support using [Spring Initializer](https://start.spring.io).
## Development
@@ -43,6 +50,7 @@ Please follow the [Contributing Guidelines](CONTRIBUTING.md).
- Christian Tzolov
- Dariusz Jędrzejczyk
+- Daniel Garnier-Moiroux
## Links
@@ -50,6 +58,145 @@ Please follow the [Contributing Guidelines](CONTRIBUTING.md).
- [Issue Tracker](https://github.com/modelcontextprotocol/java-sdk/issues)
- [CI/CD](https://github.com/modelcontextprotocol/java-sdk/actions)
+## Architecture and Design Decisions
+
+### Introduction
+
+Building a general-purpose MCP Java SDK requires making technology decisions in areas where the JDK provides limited or no support. The Java ecosystem is powerful but fragmented: multiple valid approaches exist, each with strong communities.
+Our goal is not to prescribe "the one true way," but to provide a reference implementation of the MCP specification that is:
+
+* **Pragmatic** – makes developers productive quickly
+* **Interoperable** – aligns with widely used libraries and practices
+* **Pluggable** – allows alternatives where projects prefer different stacks
+* **Grounded in team familiarity** – we chose technologies the team can be productive with today, while remaining open to community contributions that broaden the SDK
+
+### Key Choices and Considerations
+
+The SDK had to make decisions in the following areas:
+
+1. **JSON serialization** – mapping between JSON and Java types
+
+2. **Programming model** – supporting asynchronous processing, cancellation, and streaming while staying simple for blocking use cases
+
+3. **Observability** – logging and enabling integration with metrics/tracing
+
+4. **Remote clients and servers** – supporting both consuming MCP servers (client transport) and exposing MCP endpoints (server transport with authorization)
+
+The following sections explain what we chose, why it made sense, and how the choices align with the SDK's goals.
+
+### 1. JSON Serialization
+
+* **SDK Choice**: Jackson for JSON serialization and deserialization, behind an SDK abstraction (package `io.modelcontextprotocol.json` in `mcp-core`)
+
+* **Why**: Jackson is widely adopted across the Java ecosystem, provides strong performance and a mature annotation model, and is familiar to the SDK team and many potential contributors.
+
+* **How we expose it**: Public APIs use a bundled abstraction. Jackson is shipped as the default implementation (`mcp-json-jackson3`), but alternatives can be plugged in.
+
+* **How it fits the SDK**: This offers a pragmatic default while keeping flexibility for projects that prefer different JSON libraries.
+
+### 2. Programming Model
+
+* **SDK Choice**: Reactive Streams for public APIs, with Project Reactor as the internal implementation and a synchronous facade for blocking use cases
+
+* **Why**: MCP builds on JSON-RPC's asynchronous nature and defines a bidirectional protocol on top of it, enabling asynchronous and streaming interactions. MCP explicitly supports:
+
+ * Multiple in-flight requests and responses
+ * Notifications that do not expect a reply
+ * STDIO transports for inter-process communication using pipes
+ * Streaming transports such as Server-Sent Events and Streamable HTTP
+
+ These requirements call for a programming model more powerful than single-result futures like `CompletableFuture`.
+
+ * **Reactive Streams: the Community Standard**
+
+ Reactive Streams is a small Java specification that standardizes asynchronous stream processing with backpressure. It defines four minimal interfaces (Publisher, Subscriber, Subscription, and Processor). These interfaces are widely recognized as the standard contract for async, non-blocking pipelines in Java.
+
+ * **Reactive Streams Implementation**
+
+ The SDK uses Project Reactor as its implementation of the Reactive Streams specification. Reactor is mature, widely adopted, provides rich operators, and integrates well with observability through context propagation. Team familiarity also allowed us to deliver a solid foundation quickly.
+ We plan to convert the public API to only expose Reactive Streams interfaces. By defining the public API in terms of Reactive Streams interfaces and using Reactor internally, the SDK stays standards-based while benefiting from a practical, production-ready implementation.
+
+ * **Synchronous Facade in the SDK**
+
+ Not all MCP use cases require streaming pipelines. Many scenarios are as simple as "send a request and block until I get the result."
+ To support this, the SDK provides a synchronous facade layered on top of the reactive core. Developers can stay in a blocking model when it's enough, while still having access to asynchronous streaming when needed.
+
+* **How it fits the SDK**: This design balances scalability, approachability, and future evolution such as Virtual Threads and Structured Concurrency in upcoming JDKs.
+
+### 3. Observability
+
+* **SDK Choice**: SLF4J for logging; Reactor Context for observability propagation
+
+* **Why**: SLF4J is the de facto logging facade in Java, with broad compatibility. Reactor Context enables propagation of observability data such as correlation IDs and tracing state across async boundaries. This ensures interoperability with modern observability frameworks.
+
+* **How we expose it**: Public APIs log through SLF4J only, with no backend included. Observability metadata flows through Reactor pipelines. The SDK itself does not ship metrics or tracing implementations.
+
+* **How it fits the SDK**: This provides reliable logging by default and seamless integration with Micrometer, OpenTelemetry, or similar systems for metrics and tracing.
+
+### 4. Remote MCP Clients and Servers
+
+MCP supports both clients (applications consuming MCP servers) and servers (applications exposing MCP endpoints). The SDK provides support for both sides.
+
+#### Client Transport in the SDK
+
+* **SDK Choice**: JDK HttpClient (Java 11+) as the default client
+
+* **Why**: The JDK HttpClient is built-in, portable, and supports streaming responses. This keeps the default lightweight with no extra dependencies.
+
+* **How we expose it**: MCP Client APIs are transport-agnostic. The core module ships with JDK HttpClient transport. Spring WebClient-based transport is available in [Spring AI](https://docs.spring.io/spring-ai/reference/2.0-SNAPSHOT/api/mcp/mcp-overview.html) 2.0+.
+
+* **How it fits the SDK**: This ensures all applications can talk to MCP servers out of the box, while allowing richer integration in Spring and other environments.
+
+#### Server Transport in the SDK
+
+* **SDK Choice**: Jakarta Servlet implementation in core
+
+* **Why**: Servlet is the most widely deployed Java server API, providing broad reach across blocking and non-blocking models without additional dependencies.
+
+* **How we expose it**: Server APIs are transport-agnostic. Core includes Servlet support. Spring WebFlux and WebMVC server transports are available in [Spring AI](https://docs.spring.io/spring-ai/reference/2.0-SNAPSHOT/api/mcp/mcp-overview.html) 2.0+.
+
+* **How it fits the SDK**: This allows developers to expose MCP servers in the most common Java environments today, while enabling other transport implementations such as Netty, Vert.x, or Helidon.
+
+#### Authorization in the SDK
+
+* **SDK Choice**: Pluggable authorization hooks for MCP servers; no built-in implementation
+
+* **Why**: MCP servers must restrict access to authenticated and authorized clients. Authorization needs differ across environments such as Spring Security, MicroProfile JWT, or custom solutions. Providing hooks avoids lock-in and leverages proven libraries.
+
+* **How we expose it**: Authorization is integrated into the server transport layer. The SDK does not include its own authorization system.
+
+* **How it fits the SDK**: This keeps server-side security ecosystem-neutral, while ensuring applications can plug in their preferred authorization strategy.
+
+### Project Structure of the SDK
+
+The SDK is organized into modules to separate concerns and allow adopters to bring in only what they need:
+* `mcp-bom` – Dependency versions
+* `mcp-core` – Reference implementation (STDIO, JDK HttpClient, Servlet), JSON binding interface definitions
+* `mcp-json-jackson2` – Jackson 2 implementation of JSON binding
+* `mcp-json-jackson3` – Jackson 3 implementation of JSON binding
+* `mcp` – Convenience bundle (core + Jackson 3)
+* `mcp-test` – Shared testing utilities
+
+Spring integrations (WebClient, WebFlux, WebMVC) are now part of [Spring AI](https://docs.spring.io/spring-ai/reference/2.0-SNAPSHOT/api/mcp/mcp-overview.html) 2.0+ (group `org.springframework.ai`).
+
+For example, a minimal adopter may depend only on `mcp` (core + Jackson), while a Spring-based application can use the Spring AI `mcp-spring-webflux` or `mcp-spring-webmvc` artifacts for deeper framework integration.
+
+Additionally, `mcp-test` contains integration tests for `mcp-core`.
+`mcp-core` needs a JSON implementation to run full integration tests.
+Implementations such as `mcp-json-jackson3`, depend on `mcp-core`, and therefore cannot be imported in `mcp-core` for tests.
+Instead, all integration tests that need a JSON implementation are now in `mcp-test`, and use `jackson3` by default.
+A `jackson2` maven profile allows to run integration tests with Jackson 2, like so:
+
+
+```bash
+./mvnw -pl mcp-test -am -Pjackson2 test
+```
+
+### Future Directions
+
+The SDK is designed to evolve with the Java ecosystem. Areas we are actively watching include:
+Concurrency in the JDK – Virtual Threads and Structured Concurrency may simplify the synchronous API story
+
## License
This project is licensed under the [MIT License](LICENSE).
diff --git a/ROADMAP.md b/ROADMAP.md
new file mode 100644
index 000000000..b5b7dc4d7
--- /dev/null
+++ b/ROADMAP.md
@@ -0,0 +1,45 @@
+# Roadmap
+
+## Spec Implementation Tracking
+
+The SDK tracks implementation of MCP spec components via GitHub Projects, with a dedicated project board for each spec revision. For example, see the [2025-11-25 spec revision board](https://github.com/orgs/modelcontextprotocol/projects/26/views/1).
+
+## Current Focus Areas
+
+### 2025-11-25 Spec Implementation
+
+The Java SDK is actively implementing the [2025-11-25 MCP specification revision](https://github.com/orgs/modelcontextprotocol/projects/26/views/1).
+
+Key features in this revision include:
+
+- **Tasks**: Experimental support for tracking durable requests with polling and deferred result retrieval
+- **Tool calling in sampling**: Support for `tools` and `toolChoice` parameters
+- **URL mode elicitation**: Client-side URL elicitation requests
+- **Icons metadata**: Servers can expose icons for tools, resources, resource templates, and prompts
+- **Enhanced schemas**: JSON Schema 2020-12 as default, improved enum support, default values for elicitation
+- **Security improvements**: Updated security best practices, enhanced authorization flows, enabling OAuth integrations
+
+See the full [changelog](https://modelcontextprotocol.io/specification/2025-11-25/changelog) for details.
+
+### Tier 1 SDK Support
+
+Once we catch up on the most recent MCP specification revision we aim to fully support all the upcoming specification features on the day of its release.
+
+### v1.x Development
+
+The Java SDK is currently in active development as v1.x, following a recent stable 1.0.0 release. The SDK provides:
+
+- MCP protocol implementation
+- Synchronous and asynchronous programming models
+- Multiple transport options (STDIO, HTTP/SSE, Servlet)
+- Pluggable JSON serialization (Jackson 2 and Jackson 3)
+
+Development is tracked via [GitHub Issues](https://github.com/modelcontextprotocol/java-sdk/issues) and [GitHub Projects](https://github.com/orgs/modelcontextprotocol/projects).
+
+### Future Versions
+
+Major version updates will align with MCP specification changes and breaking API changes as needed. The SDK is designed to evolve with the Java ecosystem, including:
+
+- Virtual Threads and Structured Concurrency support
+- Additional transport implementations
+- Performance optimizations
diff --git a/SECURITY.md b/SECURITY.md
index 74e9880fd..502924200 100644
--- a/SECURITY.md
+++ b/SECURITY.md
@@ -1,21 +1,21 @@
# Security Policy
-Thank you for helping us keep the SDKs and systems they interact with secure.
+Thank you for helping keep the Model Context Protocol and its ecosystem secure.
## Reporting Security Issues
-This SDK is maintained by [Anthropic](https://www.anthropic.com/) as part of the Model
-Context Protocol project.
+If you discover a security vulnerability in this repository, please report it through
+the [GitHub Security Advisory process](https://docs.github.com/en/code-security/security-advisories/guidance-on-reporting-and-writing-information-about-vulnerabilities/privately-reporting-a-security-vulnerability)
+for this repository.
-The security of our systems and user data is Anthropic’s top priority. We appreciate the
-work of security researchers acting in good faith in identifying and reporting potential
-vulnerabilities.
+Please **do not** report security vulnerabilities through public GitHub issues, discussions,
+or pull requests.
-Our security program is managed on HackerOne and we ask that any validated vulnerability
-in this functionality be reported through their
-[submission form](https://hackerone.com/anthropic-vdp/reports/new?type=team&report_type=vulnerability).
+## What to Include
-## Vulnerability Disclosure Program
+To help us triage and respond quickly, please include:
-Our Vulnerability Program Guidelines are defined on our
-[HackerOne program page](https://hackerone.com/anthropic-vdp).
\ No newline at end of file
+- A description of the vulnerability
+- Steps to reproduce the issue
+- The potential impact
+- Any suggested fixes (optional)
diff --git a/VERSIONING.md b/VERSIONING.md
new file mode 100644
index 000000000..331c6d05e
--- /dev/null
+++ b/VERSIONING.md
@@ -0,0 +1,46 @@
+# Versioning Policy
+
+The MCP Java SDK (`io.modelcontextprotocol.sdk`) follows [Semantic Versioning 2.0.0](https://semver.org/).
+
+## Version Format
+
+`MAJOR.MINOR.PATCH`
+
+- **MAJOR**: Incremented for breaking changes (see below).
+- **MINOR**: Incremented for new features that are backward-compatible.
+- **PATCH**: Incremented for backward-compatible bug fixes.
+
+## What Constitutes a Breaking Change
+
+The following changes are considered breaking and require a major version bump:
+
+- Removing or renaming a public API (class, interface, method, or constant).
+- Changing the signature of a public method in a way that breaks existing callers (removing parameters, changing required/optional status, changing types).
+- Removing or renaming a public interface method or field.
+- Changing the behavior of an existing API in a way that breaks documented contracts.
+- Dropping support for a Java LTS version.
+- Removing support for a transport type.
+- Changes to the MCP protocol version that require client/server code changes.
+- Removing a module from the SDK.
+
+The following are **not** considered breaking:
+
+- Adding new methods with default implementations to interfaces.
+- Adding new public APIs, classes, interfaces, or methods.
+- Adding new optional parameters to existing methods (through method overloading).
+- Bug fixes that correct behavior to match documented intent.
+- Internal refactoring that does not affect the public API.
+- Adding support for new MCP spec features.
+- Changes to test dependencies or build tooling.
+- Adding new modules to the SDK.
+
+## How Breaking Changes Are Communicated
+
+1. **Changelog**: All breaking changes are documented in the GitHub release notes with migration instructions.
+2. **Deprecation**: When feasible, APIs are deprecated for at least one minor release before removal using `@Deprecated` annotations, which surface warnings through Java tooling and IDEs.
+3. **Migration guide**: Major version releases include a migration guide describing what changed and how to update.
+4. **PR labels**: Pull requests containing breaking changes are labeled with `breaking change`.
+
+## Maven Coordinates
+
+All SDK modules share the same version number and are released together. The BOM (`mcp-bom`) provides dependency management for all SDK modules to ensure version consistency.
diff --git a/conformance-tests/VALIDATION_RESULTS.md b/conformance-tests/VALIDATION_RESULTS.md
new file mode 100644
index 000000000..19e74330c
--- /dev/null
+++ b/conformance-tests/VALIDATION_RESULTS.md
@@ -0,0 +1,124 @@
+# MCP Java SDK Conformance Test Validation Results
+
+## Summary
+
+**Server Tests:** 37/40 passed (92.5%)
+**Client Tests:** 3/4 scenarios passed (9/10 checks passed)
+**Auth Tests:** 12/14 scenarios fully passing (178 passed, 1 failed, 1 warning, 85.7% scenarios, 98.9% checks)
+
+## Server Test Results
+
+### Passing (37/40)
+
+- **Lifecycle & Utilities (4/4):** initialize, ping, logging-set-level, completion-complete
+- **Tools (11/11):** All scenarios including progress notifications ✨
+- **Elicitation (10/10):** SEP-1034 defaults (5 checks), SEP-1330 enums (5 checks)
+- **Resources (4/6):** list, read-text, read-binary, templates-read
+- **Prompts (4/4):** list, simple, with-args, embedded-resource, with-image
+- **SSE Transport (2/2):** Multiple streams
+- **Security (2/2):** Localhost validation passes, DNS rebinding protection
+
+### Failing (3/40)
+
+1. **resources-subscribe** - Not implemented in SDK
+2. **resources-unsubscribe** - Not implemented in SDK
+
+## Client Test Results
+
+### Passing (3/4 scenarios, 9/10 checks)
+
+- **initialize (1/1):** Protocol negotiation, clientInfo, capabilities
+- **tools_call (1/1):** Tool discovery and invocation
+- **elicitation-sep1034-client-defaults (5/5):** Default values for string, integer, number, enum, boolean
+
+### Partially Passing (1/4 scenarios, 1/2 checks)
+
+- **sse-retry (1/2 + 1 warning):**
+ - ✅ Reconnects after stream closure
+ - ❌ Does not respect retry timing
+ - ⚠️ Does not send Last-Event-ID header (SHOULD requirement)
+
+**Issue:** Client treats `retry:` SSE field as invalid instead of parsing it for reconnection timing.
+
+## Auth Test Results (Spring HTTP Client)
+
+**Status: 178 passed, 1 failed, 1 warning across 14 scenarios**
+
+Uses the `client-spring-http-client` module with Spring Security OAuth2 and the [mcp-client-security](https://github.com/springaicommunity/mcp-client-security) library.
+
+### Fully Passing (12/14 scenarios)
+
+- **auth/metadata-default (12/12):** Default metadata discovery
+- **auth/metadata-var1 (12/12):** Metadata discovery variant 1
+- **auth/metadata-var2 (12/12):** Metadata discovery variant 2
+- **auth/metadata-var3 (12/12):** Metadata discovery variant 3
+- **auth/scope-from-www-authenticate (13/13):** Scope extraction from WWW-Authenticate header
+- **auth/scope-from-scopes-supported (13/13):** Scope extraction from scopes_supported
+- **auth/scope-omitted-when-undefined (13/13):** Scope omitted when not defined
+- **auth/scope-retry-limit (11/11):** Scope retry limit handling
+- **auth/token-endpoint-auth-basic (17/17):** Token endpoint with HTTP Basic auth
+- **auth/token-endpoint-auth-post (17/17):** Token endpoint with POST body auth
+- **auth/token-endpoint-auth-none (17/17):** Token endpoint with no client auth
+- **auth/pre-registration (6/6):** Pre-registered client credentials flow
+
+### Partially Passing (2/14 scenarios)
+
+- **auth/basic-cimd (12/12 + 1 warning):** Basic Client-Initiated Metadata Discovery — all checks pass, minor warning
+- **auth/scope-step-up (11/12):** Scope step-up challenge — 1 failure, client does not fully handle scope escalation after initial authorization
+
+## Known Limitations
+
+1. **Resource Subscriptions:** SDK doesn't implement `resources/subscribe` and `resources/unsubscribe` handlers
+2. **Client SSE Retry:** Client doesn't parse or respect the `retry:` field, reconnects immediately, and doesn't send Last-Event-ID header
+3. **Auth Scope Step-Up:** Client does not fully handle scope step-up challenges where the server requests additional scopes after initial authorization
+4. **Auth Basic CIMD:** Minor conformance warning in the basic Client-Initiated Metadata Discovery flow
+
+## Running Tests
+
+### Server
+```bash
+# Start server
+cd conformance-tests/server-servlet
+../../mvnw compile exec:java -Dexec.mainClass="io.modelcontextprotocol.conformance.server.ConformanceServlet"
+
+# Run tests (in another terminal)
+npx @modelcontextprotocol/conformance server --url http://localhost:8080/mcp --suite active
+```
+
+### Client
+```bash
+# Build
+cd conformance-tests/client-jdk-http-client
+../../mvnw clean package -DskipTests
+
+# Run all scenarios
+for scenario in initialize tools_call elicitation-sep1034-client-defaults sse-retry; do
+ npx @modelcontextprotocol/conformance client \
+ --command "java -jar target/client-jdk-http-client-1.0.0-SNAPSHOT.jar" \
+ --scenario $scenario
+done
+```
+
+### Auth (Spring HTTP Client)
+
+Ensure you run with the conformance testing suite `0.1.15` or higher.
+
+```bash
+# Build
+cd conformance-tests/client-spring-http-client
+../../mvnw clean package -DskipTests
+
+# Run auth suite
+npx @modelcontextprotocol/conformance@0.1.15 client \
+ --spec-version 2025-11-25 \
+ --command "java -jar target/client-spring-http-client-0.18.0-SNAPSHOT.jar" \
+ --suite auth
+```
+
+## Recommendations
+
+### High Priority
+1. Fix client SSE retry field handling in `HttpClientStreamableHttpTransport`
+2. Implement resource subscription handlers in `McpStatelessAsyncServer`
+3. Implement CIMD
+4. Implement scope step up
diff --git a/conformance-tests/client-jdk-http-client/README.md b/conformance-tests/client-jdk-http-client/README.md
new file mode 100644
index 000000000..44eccedf0
--- /dev/null
+++ b/conformance-tests/client-jdk-http-client/README.md
@@ -0,0 +1,135 @@
+# MCP Conformance Tests - JDK HTTP Client
+
+This module provides a conformance test client implementation for the Java MCP SDK using the JDK HTTP Client with Streamable HTTP transport.
+
+## Overview
+
+The conformance test client is designed to work with the [MCP Conformance Test Framework](https://github.com/modelcontextprotocol/conformance). It validates that the Java MCP SDK client properly implements the MCP specification.
+
+## Architecture
+
+The client reads test scenarios from environment variables and accepts the server URL as a command-line argument, following the conformance framework's conventions:
+
+- **MCP_CONFORMANCE_SCENARIO**: Environment variable specifying which test scenario to run
+- **Server URL**: Passed as the last command-line argument
+
+## Supported Scenarios
+
+Currently implemented scenarios:
+
+- **initialize**: Tests the MCP client initialization handshake only
+ - ✅ Validates protocol version negotiation
+ - ✅ Validates clientInfo (name and version)
+ - ✅ Validates proper handling of server capabilities
+ - Does NOT call any tools or perform additional operations
+
+- **tools_call**: Tests tool discovery and invocation
+ - ✅ Initializes the client
+ - ✅ Lists available tools from the server
+ - ✅ Calls the `add_numbers` tool with test arguments (a=5, b=3)
+ - ✅ Validates the tool result
+
+- **elicitation-sep1034-client-defaults**: Tests client applies default values for omitted elicitation fields (SEP-1034)
+ - ✅ Initializes the client
+ - ✅ Lists available tools from the server
+ - ✅ Calls the `test_client_elicitation_defaults` tool
+ - ✅ Validates that the client properly applies default values from JSON schema to elicitation responses (5/5 checks pass)
+
+- **sse-retry**: Tests client respects SSE retry field timing and reconnects properly (SEP-1699)
+ - ⚠️ Initializes the client
+ - ⚠️ Lists available tools from the server
+ - ⚠️ Calls the `test_reconnection` tool which triggers SSE stream closure
+ - ✅ Client reconnects after stream closure (PASSING)
+ - ❌ Client does not respect retry timing (FAILING)
+ - ⚠️ Client does not send Last-Event-ID header (WARNING - SHOULD requirement)
+
+## Building
+
+Build the executable JAR:
+
+```bash
+cd conformance-tests/client-jdk-http-client
+../../mvnw clean package -DskipTests
+```
+
+This creates an executable JAR at:
+```
+target/client-jdk-http-client-1.0.0-SNAPSHOT.jar
+```
+
+## Running Tests
+
+### Using the Conformance Framework
+
+Run a single scenario:
+
+```bash
+npx @modelcontextprotocol/conformance client \
+ --command "java -jar conformance-tests/client-jdk-http-client/target/client-jdk-http-client-1.0.0-SNAPSHOT.jar" \
+ --scenario initialize
+
+npx @modelcontextprotocol/conformance client \
+ --command "java -jar conformance-tests/client-jdk-http-client/target/client-jdk-http-client-1.0.0-SNAPSHOT.jar" \
+ --scenario tools_call
+
+npx @modelcontextprotocol/conformance client \
+ --command "java -jar conformance-tests/client-jdk-http-client/target/client-jdk-http-client-1.0.0-SNAPSHOT.jar" \
+ --scenario elicitation-sep1034-client-defaults
+
+npx @modelcontextprotocol/conformance client \
+ --command "java -jar conformance-tests/client-jdk-http-client/target/client-jdk-http-client-1.0.0-SNAPSHOT.jar" \
+ --scenario sse-retry
+```
+
+Run with verbose output:
+
+```bash
+npx @modelcontextprotocol/conformance client \
+ --command "java -jar conformance-tests/client-jdk-http-client/target/client-jdk-http-client-1.0.0-SNAPSHOT.jar" \
+ --scenario initialize \
+ --verbose
+```
+
+### Manual Testing
+
+You can also run the client manually if you have a test server:
+
+```bash
+export MCP_CONFORMANCE_SCENARIO=initialize
+java -jar conformance-tests/client-jdk-http-client/target/client-jdk-http-client-1.0.0-SNAPSHOT.jar http://localhost:3000/mcp
+```
+
+## Test Results
+
+The conformance framework generates test results showing:
+
+**Current Status (3/4 scenarios passing):**
+- ✅ initialize: 1/1 checks passed
+- ✅ tools_call: 1/1 checks passed
+- ✅ elicitation-sep1034-client-defaults: 5/5 checks passed
+- ⚠️ sse-retry: 1/2 checks passed, 1 warning
+
+Test result files are generated in `results/-/`:
+- `checks.json`: Array of conformance check results with pass/fail status
+- `stdout.txt`: Client stdout output
+- `stderr.txt`: Client stderr output
+
+### Known Issue: SSE Retry Handling
+
+The `sse-retry` scenario currently fails because:
+1. The client treats the SSE `retry:` field as invalid instead of parsing it
+2. The client does not implement retry timing (reconnects immediately)
+3. The client does not send the Last-Event-ID header on reconnection
+
+This is a known limitation in the `HttpClientStreamableHttpTransport` implementation.
+
+## Next Steps
+
+Future enhancements:
+
+- Fix SSE retry field handling (SEP-1699) to properly parse and respect retry timing
+- Implement Last-Event-ID header on reconnection for resumability
+- Add auth scenarios (currently excluded as per requirements)
+- Implement a comprehensive "everything-client" pattern
+- Add to CI/CD pipeline
+- Create expected-failures baseline for known issues
diff --git a/conformance-tests/client-jdk-http-client/pom.xml b/conformance-tests/client-jdk-http-client/pom.xml
new file mode 100644
index 000000000..f30361438
--- /dev/null
+++ b/conformance-tests/client-jdk-http-client/pom.xml
@@ -0,0 +1,82 @@
+
+
+ 4.0.0
+
+ io.modelcontextprotocol.sdk
+ conformance-tests
+ 1.1.0-SNAPSHOT
+
+ client-jdk-http-client
+ jar
+ MCP Conformance Tests - JDK HTTP Client
+ JDK HTTP Client conformance tests for the Java MCP SDK
+ https://github.com/modelcontextprotocol/java-sdk
+
+
+ https://github.com/modelcontextprotocol/java-sdk
+ git://github.com/modelcontextprotocol/java-sdk.git
+ git@github.com/modelcontextprotocol/java-sdk.git
+
+
+
+ true
+
+
+
+
+ io.modelcontextprotocol.sdk
+ mcp
+ 1.1.0-SNAPSHOT
+
+
+
+
+ ch.qos.logback
+ logback-classic
+ ${logback.version}
+ runtime
+
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-shade-plugin
+ 3.5.1
+
+
+ package
+
+ shade
+
+
+
+
+ io.modelcontextprotocol.conformance.client.ConformanceJdkClientMcpClient
+
+
+
+
+
+ *:*
+
+ META-INF/*.SF
+ META-INF/*.DSA
+ META-INF/*.RSA
+
+
+
+
+
+
+
+
+
+
+
diff --git a/conformance-tests/client-jdk-http-client/src/main/java/io/modelcontextprotocol/conformance/client/ConformanceJdkClientMcpClient.java b/conformance-tests/client-jdk-http-client/src/main/java/io/modelcontextprotocol/conformance/client/ConformanceJdkClientMcpClient.java
new file mode 100644
index 000000000..570c4614e
--- /dev/null
+++ b/conformance-tests/client-jdk-http-client/src/main/java/io/modelcontextprotocol/conformance/client/ConformanceJdkClientMcpClient.java
@@ -0,0 +1,286 @@
+package io.modelcontextprotocol.conformance.client;
+
+import java.time.Duration;
+
+import io.modelcontextprotocol.client.McpClient;
+import io.modelcontextprotocol.client.McpSyncClient;
+import io.modelcontextprotocol.client.transport.HttpClientStreamableHttpTransport;
+import io.modelcontextprotocol.spec.McpSchema;
+
+/**
+ * MCP Conformance Test Client - JDK HTTP Client Implementation
+ *
+ *
+ * This client is designed to work with the MCP conformance test framework. It reads the
+ * test scenario from the MCP_CONFORMANCE_SCENARIO environment variable and the server URL
+ * from command-line arguments.
+ *
+ *
+ * Usage: ConformanceJdkClientMcpClient <server-url>
+ *
+ * @see MCP Conformance
+ * Test Framework
+ */
+public class ConformanceJdkClientMcpClient {
+
+ public static void main(String[] args) {
+ if (args.length == 0) {
+ System.err.println("Usage: ConformanceJdkClientMcpClient ");
+ System.err.println("The server URL must be provided as the last command-line argument.");
+ System.err.println("The MCP_CONFORMANCE_SCENARIO environment variable must be set.");
+ System.exit(1);
+ }
+
+ String scenario = System.getenv("MCP_CONFORMANCE_SCENARIO");
+ if (scenario == null || scenario.isEmpty()) {
+ System.err.println("Error: MCP_CONFORMANCE_SCENARIO environment variable is not set");
+ System.exit(1);
+ }
+
+ String serverUrl = args[args.length - 1];
+
+ try {
+ switch (scenario) {
+ case "initialize":
+ runInitializeScenario(serverUrl);
+ break;
+ case "tools_call":
+ runToolsCallScenario(serverUrl);
+ break;
+ case "elicitation-sep1034-client-defaults":
+ runElicitationDefaultsScenario(serverUrl);
+ break;
+ case "sse-retry":
+ runSSERetryScenario(serverUrl);
+ break;
+ default:
+ System.err.println("Unknown scenario: " + scenario);
+ System.err.println("Available scenarios:");
+ System.err.println(" - initialize");
+ System.err.println(" - tools_call");
+ System.err.println(" - elicitation-sep1034-client-defaults");
+ System.err.println(" - sse-retry");
+ System.exit(1);
+ }
+ System.exit(0);
+ }
+ catch (Exception e) {
+ System.err.println("Error: " + e.getMessage());
+ e.printStackTrace();
+ System.exit(1);
+ }
+ }
+
+ /**
+ * Helper method to create and configure an MCP client with transport.
+ * @param serverUrl the URL of the MCP server
+ * @return configured McpSyncClient instance
+ */
+ private static McpSyncClient createClient(String serverUrl) {
+ HttpClientStreamableHttpTransport transport = HttpClientStreamableHttpTransport.builder(serverUrl).build();
+
+ return McpClient.sync(transport)
+ .clientInfo(new McpSchema.Implementation("test-client", "1.0.0"))
+ .requestTimeout(Duration.ofSeconds(30))
+ .build();
+ }
+
+ /**
+ * Helper method to create and configure an MCP client with elicitation support.
+ * @param serverUrl the URL of the MCP server
+ * @return configured McpSyncClient instance with elicitation handler
+ */
+ private static McpSyncClient createClientWithElicitation(String serverUrl) {
+ HttpClientStreamableHttpTransport transport = HttpClientStreamableHttpTransport.builder(serverUrl).build();
+
+ // Build client capabilities with elicitation support
+ var capabilities = McpSchema.ClientCapabilities.builder().elicitation().build();
+
+ return McpClient.sync(transport)
+ .clientInfo(new McpSchema.Implementation("test-client", "1.0.0"))
+ .requestTimeout(Duration.ofSeconds(30))
+ .capabilities(capabilities)
+ .elicitation(request -> {
+ // Apply default values from the schema to create the content
+ var content = new java.util.HashMap();
+ var schema = request.requestedSchema();
+
+ if (schema != null && schema.containsKey("properties")) {
+ @SuppressWarnings("unchecked")
+ var properties = (java.util.Map) schema.get("properties");
+
+ // Apply defaults for each property
+ properties.forEach((key, propDef) -> {
+ @SuppressWarnings("unchecked")
+ var propMap = (java.util.Map) propDef;
+ if (propMap.containsKey("default")) {
+ content.put(key, propMap.get("default"));
+ }
+ });
+ }
+
+ // Return accept action with the defaults applied
+ return new McpSchema.ElicitResult(McpSchema.ElicitResult.Action.ACCEPT, content, null);
+ })
+ .build();
+ }
+
+ /**
+ * Initialize scenario: Tests MCP client initialization handshake.
+ * @param serverUrl the URL of the MCP server
+ * @throws Exception if any error occurs during execution
+ */
+ private static void runInitializeScenario(String serverUrl) throws Exception {
+ McpSyncClient client = createClient(serverUrl);
+
+ try {
+ // Initialize client
+ client.initialize();
+
+ System.out.println("Successfully connected to MCP server");
+ }
+ finally {
+ // Close the client (which will close the transport)
+ client.close();
+ System.out.println("Connection closed successfully");
+ }
+ }
+
+ /**
+ * Tools call scenario: Tests tool listing and invocation functionality.
+ * @param serverUrl the URL of the MCP server
+ * @throws Exception if any error occurs during execution
+ */
+ private static void runToolsCallScenario(String serverUrl) throws Exception {
+ McpSyncClient client = createClient(serverUrl);
+
+ try {
+ // Initialize client
+ client.initialize();
+
+ System.out.println("Successfully connected to MCP server");
+
+ // List available tools
+ McpSchema.ListToolsResult toolsResult = client.listTools();
+ System.out.println("Successfully listed tools");
+
+ // Call the add_numbers tool if it exists
+ if (toolsResult != null && toolsResult.tools() != null) {
+ for (McpSchema.Tool tool : toolsResult.tools()) {
+ if ("add_numbers".equals(tool.name())) {
+ // Call the add_numbers tool with test arguments
+ var arguments = new java.util.HashMap();
+ arguments.put("a", 5);
+ arguments.put("b", 3);
+
+ McpSchema.CallToolResult result = client
+ .callTool(new McpSchema.CallToolRequest("add_numbers", arguments));
+
+ System.out.println("Successfully called add_numbers tool");
+ if (result != null && result.content() != null) {
+ System.out.println("Tool result: " + result.content());
+ }
+ break;
+ }
+ }
+ }
+ }
+ finally {
+ // Close the client (which will close the transport)
+ client.close();
+ System.out.println("Connection closed successfully");
+ }
+ }
+
+ /**
+ * Elicitation defaults scenario: Tests client applies default values for omitted
+ * elicitation fields (SEP-1034).
+ * @param serverUrl the URL of the MCP server
+ * @throws Exception if any error occurs during execution
+ */
+ private static void runElicitationDefaultsScenario(String serverUrl) throws Exception {
+ McpSyncClient client = createClientWithElicitation(serverUrl);
+
+ try {
+ // Initialize client
+ client.initialize();
+
+ System.out.println("Successfully connected to MCP server");
+
+ // List available tools
+ McpSchema.ListToolsResult toolsResult = client.listTools();
+ System.out.println("Successfully listed tools");
+
+ // Call the test_client_elicitation_defaults tool if it exists
+ if (toolsResult != null && toolsResult.tools() != null) {
+ for (McpSchema.Tool tool : toolsResult.tools()) {
+ if ("test_client_elicitation_defaults".equals(tool.name())) {
+ // Call the tool which will trigger an elicitation request
+ var arguments = new java.util.HashMap();
+
+ McpSchema.CallToolResult result = client
+ .callTool(new McpSchema.CallToolRequest("test_client_elicitation_defaults", arguments));
+
+ System.out.println("Successfully called test_client_elicitation_defaults tool");
+ if (result != null && result.content() != null) {
+ System.out.println("Tool result: " + result.content());
+ }
+ break;
+ }
+ }
+ }
+ }
+ finally {
+ // Close the client (which will close the transport)
+ client.close();
+ System.out.println("Connection closed successfully");
+ }
+ }
+
+ /**
+ * SSE retry scenario: Tests client respects SSE retry field timing and reconnects
+ * properly (SEP-1699).
+ * @param serverUrl the URL of the MCP server
+ * @throws Exception if any error occurs during execution
+ */
+ private static void runSSERetryScenario(String serverUrl) throws Exception {
+ McpSyncClient client = createClient(serverUrl);
+
+ try {
+ // Initialize client
+ client.initialize();
+
+ System.out.println("Successfully connected to MCP server");
+
+ // List available tools
+ McpSchema.ListToolsResult toolsResult = client.listTools();
+ System.out.println("Successfully listed tools");
+
+ // Call the test_reconnection tool if it exists
+ if (toolsResult != null && toolsResult.tools() != null) {
+ for (McpSchema.Tool tool : toolsResult.tools()) {
+ if ("test_reconnection".equals(tool.name())) {
+ // Call the tool which will trigger SSE stream closure and
+ // reconnection
+ var arguments = new java.util.HashMap();
+
+ McpSchema.CallToolResult result = client
+ .callTool(new McpSchema.CallToolRequest("test_reconnection", arguments));
+
+ System.out.println("Successfully called test_reconnection tool");
+ if (result != null && result.content() != null) {
+ System.out.println("Tool result: " + result.content());
+ }
+ break;
+ }
+ }
+ }
+ }
+ finally {
+ // Close the client (which will close the transport)
+ client.close();
+ System.out.println("Connection closed successfully");
+ }
+ }
+
+}
diff --git a/conformance-tests/client-jdk-http-client/src/main/resources/logback.xml b/conformance-tests/client-jdk-http-client/src/main/resources/logback.xml
new file mode 100644
index 000000000..bb8e3795d
--- /dev/null
+++ b/conformance-tests/client-jdk-http-client/src/main/resources/logback.xml
@@ -0,0 +1,16 @@
+
+
+
+
+ %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n
+
+
+
+
+
+
+
+
+
+
+
diff --git a/conformance-tests/client-spring-http-client/README.md b/conformance-tests/client-spring-http-client/README.md
new file mode 100644
index 000000000..876a86e1d
--- /dev/null
+++ b/conformance-tests/client-spring-http-client/README.md
@@ -0,0 +1,124 @@
+# MCP Conformance Tests - Spring HTTP Client (Auth Suite)
+
+This module provides a conformance test client implementation for the Java MCP SDK's **auth** suite.
+
+OAuth2 support is not implemented in the SDK itself, but we provide hooks to implement the Authorization section of the specification. One such implementation is done in Spring, with Sprign AI and the [mcp-client-security](https://github.com/springaicommunity/mcp-client-security) library.
+
+This is a Spring web application, we interact with it through a normal HTTP-client that follows redirects and performs OAuth2 authorization flows.
+
+## Overview
+
+The conformance test client is designed to work with the [MCP Conformance Test Framework](https://github.com/modelcontextprotocol/conformance). It validates that the Java MCP SDK client, combined with Spring Security's OAuth2 support, properly implements the MCP authorization specification.
+
+Test with @modelcontextprotocol/conformance@0.1.15.
+
+## Conformance Test Results
+
+**Status: 178 passed, 1 failed, 1 warning across 14 scenarios**
+
+| Scenario | Result | Details |
+|---|---|---|
+| auth/metadata-default | ✅ Pass | 12/12 |
+| auth/metadata-var1 | ✅ Pass | 12/12 |
+| auth/metadata-var2 | ✅ Pass | 12/12 |
+| auth/metadata-var3 | ✅ Pass | 12/12 |
+| auth/basic-cimd | ⚠️ Warning | 12/12 passed, 1 warning |
+| auth/scope-from-www-authenticate | ✅ Pass | 13/13 |
+| auth/scope-from-scopes-supported | ✅ Pass | 13/13 |
+| auth/scope-omitted-when-undefined | ✅ Pass | 13/13 |
+| auth/scope-step-up | ❌ Fail | 11/12 (1 failed) |
+| auth/scope-retry-limit | ✅ Pass | 11/11 |
+| auth/token-endpoint-auth-basic | ✅ Pass | 17/17 |
+| auth/token-endpoint-auth-post | ✅ Pass | 17/17 |
+| auth/token-endpoint-auth-none | ✅ Pass | 17/17 |
+| auth/pre-registration | ✅ Pass | 6/6 |
+
+See [VALIDATION_RESULTS.md](../VALIDATION_RESULTS.md) for the full project validation results.
+
+## Architecture
+
+The client is a Spring Boot application that reads test scenarios from environment variables and accepts the server URL as a command-line argument, following the conformance framework's conventions:
+
+- **MCP_CONFORMANCE_SCENARIO**: Environment variable specifying which test scenario to run
+- **MCP_CONFORMANCE_CONTEXT**: Environment variable with JSON context (used by `auth/pre-registration`)
+- **Server URL**: Passed as the last command-line argument
+
+### Scenario Routing
+
+The application uses Spring's conditional configuration to select the appropriate scenario at startup:
+
+- **`DefaultConfiguration`** — Activated for all scenarios except `auth/pre-registration`. Uses the OAuth2 Authorization Code flow with dynamic client registration via `McpClientOAuth2Configurer`.
+- **`PreRegistrationConfiguration`** — Activated only for `auth/pre-registration`. Uses the Client Credentials flow with pre-registered client credentials read from `MCP_CONFORMANCE_CONTEXT`.
+
+### Key Dependencies
+
+- **Spring Boot 4.0** with Spring Security OAuth2 Client
+- **Spring AI MCP Client** (`spring-ai-starter-mcp-client`)
+- **mcp-client-security** — Community library providing MCP-specific OAuth2 integration (metadata discovery, dynamic client registration, transport context)
+
+## Building
+
+Build the executable JAR:
+
+```bash
+cd conformance-tests/client-spring-http-client
+../../mvnw clean package -DskipTests
+```
+
+This creates an executable JAR at:
+```
+target/client-spring-http-client-0.18.0-SNAPSHOT.jar
+```
+
+## Running Tests
+
+### Using the Conformance Framework
+
+Run the full auth suite:
+
+```bash
+npx @modelcontextprotocol/conformance@0.1.15 client \
+ --spec-version 2025-11-25 \
+ --command "java -jar conformance-tests/client-spring-http-client/target/client-spring-http-client-0.18.0-SNAPSHOT.jar" \
+ --suite auth
+```
+
+Run a single scenario:
+
+```bash
+npx @modelcontextprotocol/conformance@0.1.15 client \
+ --spec-version 2025-11-25 \
+ --command "java -jar conformance-tests/client-spring-http-client/target/client-spring-http-client-0.18.0-SNAPSHOT.jar" \
+ --scenario auth/metadata-default
+```
+
+Run with verbose output:
+
+```bash
+npx @modelcontextprotocol/conformance@0.1.15 client \
+ --spec-version 2025-11-25 \
+ --command "java -jar conformance-tests/client-spring-http-client/target/client-spring-http-client-0.18.0-SNAPSHOT.jar" \
+ --scenario auth/metadata-default \
+ --verbose
+```
+
+### Manual Testing
+
+You can also run the client manually if you have a test server:
+
+```bash
+export MCP_CONFORMANCE_SCENARIO=auth/metadata-default
+java -jar conformance-tests/client-spring-http-client/target/client-spring-http-client-0.18.0-SNAPSHOT.jar http://localhost:3000/mcp
+```
+
+## Known Issues
+
+1. **auth/scope-step-up** (1 failure) — The client does not fully handle scope step-up challenges where the server requests additional scopes after initial authorization.
+2. **auth/basic-cimd** (1 warning) — Minor conformance warning in the basic Client-Initiated Metadata Discovery flow.
+
+## References
+
+- [MCP Specification — Authorization](https://modelcontextprotocol.io/specification/2025-11-25/basic/authorization)
+- [MCP Conformance Tests](https://github.com/modelcontextprotocol/conformance)
+- [mcp-client-security Library](https://github.com/springaicommunity/mcp-client-security)
+- [SDK Integration Guide](https://github.com/modelcontextprotocol/conformance/blob/main/SDK_INTEGRATION.md)
diff --git a/conformance-tests/client-spring-http-client/pom.xml b/conformance-tests/client-spring-http-client/pom.xml
new file mode 100644
index 000000000..94923fb5c
--- /dev/null
+++ b/conformance-tests/client-spring-http-client/pom.xml
@@ -0,0 +1,91 @@
+
+
+ 4.0.0
+
+ org.springframework.boot
+ spring-boot-starter-parent
+ 4.0.2
+
+
+ io.modelcontextprotocol.sdk
+ client-spring-http-client
+ 1.0.0-SNAPSHOT
+ jar
+ MCP Conformance Tests - Spring HTTP Client
+ Spring HTTP Client conformance tests for the Java MCP SDK
+ https://github.com/modelcontextprotocol/java-sdk
+
+
+ https://github.com/modelcontextprotocol/java-sdk
+ git://github.com/modelcontextprotocol/java-sdk.git
+ git@github.com/modelcontextprotocol/java-sdk.git
+
+
+
+ 17
+ 2.0.0-M2
+ true
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-webmvc
+
+
+
+ org.springframework.boot
+ spring-boot-starter-restclient
+
+
+
+ org.springframework.ai
+ spring-ai-starter-mcp-client
+ ${spring-ai.version}
+
+
+
+ org.springframework.boot
+ spring-boot-starter-oauth2-client
+
+
+
+ org.springaicommunity
+ mcp-client-security
+ 0.1.2
+
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+
+
+
+
+
+
+ maven-central
+ https://repo.maven.apache.org/maven2/
+
+ false
+
+
+ true
+
+
+
+ spring-milestones
+ Spring Milestones
+ https://repo.spring.io/milestone
+
+ false
+
+
+
+
+
diff --git a/conformance-tests/client-spring-http-client/src/main/java/io/modelcontextprotocol/conformance/client/ConformanceSpringClientApplication.java b/conformance-tests/client-spring-http-client/src/main/java/io/modelcontextprotocol/conformance/client/ConformanceSpringClientApplication.java
new file mode 100644
index 000000000..00582c9f2
--- /dev/null
+++ b/conformance-tests/client-spring-http-client/src/main/java/io/modelcontextprotocol/conformance/client/ConformanceSpringClientApplication.java
@@ -0,0 +1,99 @@
+/*
+ * Copyright 2026-2026 the original author or authors.
+ */
+
+package io.modelcontextprotocol.conformance.client;
+
+import java.util.Optional;
+
+import io.modelcontextprotocol.conformance.client.scenario.Scenario;
+import org.springaicommunity.mcp.security.client.sync.oauth2.metadata.McpMetadataDiscoveryService;
+import org.springaicommunity.mcp.security.client.sync.oauth2.registration.DynamicClientRegistrationService;
+import org.springaicommunity.mcp.security.client.sync.oauth2.registration.InMemoryMcpClientRegistrationRepository;
+
+import org.springframework.boot.ApplicationArguments;
+import org.springframework.boot.ApplicationRunner;
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.context.annotation.Bean;
+
+/**
+ * MCP Conformance Test Client - Spring HTTP Client Implementation.
+ *
+ *
+ * This client is designed to work with the MCP conformance test framework. It reads the
+ * test scenario from the MCP_CONFORMANCE_SCENARIO environment variable and the server URL
+ * from command-line arguments.
+ *
+ *
+ * It specifically tests the {@code auth} conformance suite. It requires Spring to work.
+ *
+ *
+ * Usage: java -jar client-spring-http-client.jar <server-url>
+ *
+ * @see MCP Conformance
+ * Test Framework
+ */
+@SpringBootApplication
+public class ConformanceSpringClientApplication {
+
+ public static final String REGISTRATION_ID = "default_registration";
+
+ public static void main(String[] args) {
+ SpringApplication.run(ConformanceSpringClientApplication.class, args);
+ }
+
+ @Bean
+ McpMetadataDiscoveryService discovery() {
+ return new McpMetadataDiscoveryService();
+ }
+
+ @Bean
+ InMemoryMcpClientRegistrationRepository clientRegistrationRepository(McpMetadataDiscoveryService discovery) {
+ return new InMemoryMcpClientRegistrationRepository(new DynamicClientRegistrationService(), discovery);
+ }
+
+ @Bean
+ ApplicationRunner conformanceRunner(Optional scenario, ServerUrl serverUrl) {
+ return args -> {
+ String scenarioName = System.getenv("MCP_CONFORMANCE_SCENARIO");
+ if (scenarioName == null || scenarioName.isEmpty()) {
+ System.err.println("Error: MCP_CONFORMANCE_SCENARIO environment variable is not set");
+ System.exit(1);
+ }
+
+ if (scenario.isEmpty()) {
+ System.err.println("Unsupported scenario type");
+ System.exit(1);
+ }
+
+ try {
+ System.out.println("Executing " + scenarioName);
+ scenario.get().execute(serverUrl.value());
+ System.exit(0);
+ }
+ catch (Exception e) {
+ System.err.println("Error: " + e.getMessage());
+ e.printStackTrace();
+ System.exit(1);
+ }
+ };
+ }
+
+ public record ServerUrl(String value) {
+ }
+
+ @Bean
+ ServerUrl serverUrl(ApplicationArguments args) {
+ var nonOptionArgs = args.getNonOptionArgs();
+ if (nonOptionArgs.isEmpty()) {
+ System.err.println("Usage: ConformanceSpringClientApplication ");
+ System.err.println("The server URL must be provided as a command-line argument.");
+ System.err.println("The MCP_CONFORMANCE_SCENARIO environment variable must be set.");
+ System.exit(1);
+ }
+
+ return new ServerUrl(nonOptionArgs.get(nonOptionArgs.size() - 1));
+ }
+
+}
diff --git a/conformance-tests/client-spring-http-client/src/main/java/io/modelcontextprotocol/conformance/client/McpClientController.java b/conformance-tests/client-spring-http-client/src/main/java/io/modelcontextprotocol/conformance/client/McpClientController.java
new file mode 100644
index 000000000..e02cfd416
--- /dev/null
+++ b/conformance-tests/client-spring-http-client/src/main/java/io/modelcontextprotocol/conformance/client/McpClientController.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright 2026-2026 the original author or authors.
+ */
+
+package io.modelcontextprotocol.conformance.client;
+
+import io.modelcontextprotocol.conformance.client.scenario.Scenario;
+
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+/**
+ * Expose MCP client in a web environment.
+ */
+@RestController
+class McpClientController {
+
+ private final Scenario scenario;
+
+ McpClientController(Scenario scenario) {
+ this.scenario = scenario;
+ }
+
+ @GetMapping("/initialize-mcp-client")
+ public String execute() {
+ this.scenario.getMcpClient().initialize();
+ return "OK";
+ }
+
+}
diff --git a/conformance-tests/client-spring-http-client/src/main/java/io/modelcontextprotocol/conformance/client/configuration/DefaultConfiguration.java b/conformance-tests/client-spring-http-client/src/main/java/io/modelcontextprotocol/conformance/client/configuration/DefaultConfiguration.java
new file mode 100644
index 000000000..acf26d94e
--- /dev/null
+++ b/conformance-tests/client-spring-http-client/src/main/java/io/modelcontextprotocol/conformance/client/configuration/DefaultConfiguration.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2026-2026 the original author or authors.
+ */
+
+package io.modelcontextprotocol.conformance.client.configuration;
+
+import io.modelcontextprotocol.conformance.client.ConformanceSpringClientApplication;
+import io.modelcontextprotocol.conformance.client.scenario.DefaultScenario;
+import org.springaicommunity.mcp.security.client.sync.config.McpClientOAuth2Configurer;
+import org.springaicommunity.mcp.security.client.sync.oauth2.registration.McpClientRegistrationRepository;
+
+import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression;
+import org.springframework.boot.web.server.servlet.context.ServletWebServerApplicationContext;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.security.config.annotation.web.builders.HttpSecurity;
+import org.springframework.security.oauth2.client.web.OAuth2AuthorizedClientRepository;
+import org.springframework.security.web.SecurityFilterChain;
+import static io.modelcontextprotocol.conformance.client.ConformanceSpringClientApplication.REGISTRATION_ID;
+
+@Configuration
+@ConditionalOnExpression("#{environment['MCP_CONFORMANCE_SCENARIO'] != 'auth/pre-registration'}")
+public class DefaultConfiguration {
+
+ @Bean
+ DefaultScenario defaultScenario(McpClientRegistrationRepository clientRegistrationRepository,
+ ServletWebServerApplicationContext serverCtx,
+ OAuth2AuthorizedClientRepository oAuth2AuthorizedClientRepository) {
+ return new DefaultScenario(clientRegistrationRepository, serverCtx, oAuth2AuthorizedClientRepository);
+ }
+
+ @Bean
+ SecurityFilterChain securityFilterChain(HttpSecurity http, ConformanceSpringClientApplication.ServerUrl serverUrl) {
+ return http.authorizeHttpRequests(authz -> authz.anyRequest().permitAll())
+ .with(new McpClientOAuth2Configurer(),
+ mcp -> mcp.registerMcpOAuth2Client(REGISTRATION_ID, serverUrl.value()))
+ .build();
+ }
+
+}
diff --git a/conformance-tests/client-spring-http-client/src/main/java/io/modelcontextprotocol/conformance/client/configuration/PreRegistrationConfiguration.java b/conformance-tests/client-spring-http-client/src/main/java/io/modelcontextprotocol/conformance/client/configuration/PreRegistrationConfiguration.java
new file mode 100644
index 000000000..afe03f85a
--- /dev/null
+++ b/conformance-tests/client-spring-http-client/src/main/java/io/modelcontextprotocol/conformance/client/configuration/PreRegistrationConfiguration.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2026-2026 the original author or authors.
+ */
+
+package io.modelcontextprotocol.conformance.client.configuration;
+
+import io.modelcontextprotocol.conformance.client.scenario.PreRegistrationScenario;
+import org.springaicommunity.mcp.security.client.sync.config.McpClientOAuth2Configurer;
+import org.springaicommunity.mcp.security.client.sync.oauth2.metadata.McpMetadataDiscoveryService;
+import org.springaicommunity.mcp.security.client.sync.oauth2.registration.McpClientRegistrationRepository;
+
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.security.config.Customizer;
+import org.springframework.security.config.annotation.web.builders.HttpSecurity;
+import org.springframework.security.oauth2.client.OAuth2AuthorizedClientService;
+import org.springframework.security.web.SecurityFilterChain;
+
+@Configuration
+@ConditionalOnProperty(name = "mcp.conformance.scenario", havingValue = "auth/pre-registration")
+public class PreRegistrationConfiguration {
+
+ @Bean
+ PreRegistrationScenario defaultScenario(McpClientRegistrationRepository clientRegistrationRepository,
+ McpMetadataDiscoveryService mcpMetadataDiscovery,
+ OAuth2AuthorizedClientService oAuth2AuthorizedClientService) {
+ return new PreRegistrationScenario(clientRegistrationRepository, mcpMetadataDiscovery,
+ oAuth2AuthorizedClientService);
+ }
+
+ @Bean
+ SecurityFilterChain securityFilterChain(HttpSecurity http) {
+ return http.authorizeHttpRequests(authz -> authz.anyRequest().permitAll())
+ .with(new McpClientOAuth2Configurer(), Customizer.withDefaults())
+ .build();
+ }
+
+}
diff --git a/conformance-tests/client-spring-http-client/src/main/java/io/modelcontextprotocol/conformance/client/scenario/DefaultScenario.java b/conformance-tests/client-spring-http-client/src/main/java/io/modelcontextprotocol/conformance/client/scenario/DefaultScenario.java
new file mode 100644
index 000000000..d82637de9
--- /dev/null
+++ b/conformance-tests/client-spring-http-client/src/main/java/io/modelcontextprotocol/conformance/client/scenario/DefaultScenario.java
@@ -0,0 +1,100 @@
+/*
+ * Copyright 2026-2026 the original author or authors.
+ */
+
+package io.modelcontextprotocol.conformance.client.scenario;
+
+import java.net.CookieManager;
+import java.net.CookiePolicy;
+import java.net.http.HttpClient;
+import java.time.Duration;
+
+import io.modelcontextprotocol.client.McpClient;
+import io.modelcontextprotocol.client.McpSyncClient;
+import io.modelcontextprotocol.client.transport.HttpClientStreamableHttpTransport;
+import io.modelcontextprotocol.spec.McpSchema;
+import org.jspecify.annotations.NonNull;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springaicommunity.mcp.security.client.sync.AuthenticationMcpTransportContextProvider;
+import org.springaicommunity.mcp.security.client.sync.oauth2.http.client.OAuth2AuthorizationCodeSyncHttpRequestCustomizer;
+import org.springaicommunity.mcp.security.client.sync.oauth2.registration.McpClientRegistrationRepository;
+
+import org.springframework.boot.web.server.servlet.context.ServletWebServerApplicationContext;
+import org.springframework.http.client.JdkClientHttpRequestFactory;
+import org.springframework.security.oauth2.client.web.DefaultOAuth2AuthorizedClientManager;
+import org.springframework.security.oauth2.client.web.OAuth2AuthorizedClientRepository;
+import org.springframework.web.client.RestClient;
+import static io.modelcontextprotocol.conformance.client.ConformanceSpringClientApplication.REGISTRATION_ID;
+
+public class DefaultScenario implements Scenario {
+
+ private static final Logger log = LoggerFactory
+ .getLogger(DefaultScenario.class);
+
+ private final ServletWebServerApplicationContext serverCtx;
+
+ private final DefaultOAuth2AuthorizedClientManager authorizedClientManager;
+
+ private McpSyncClient client;
+
+ public DefaultScenario(McpClientRegistrationRepository clientRegistrationRepository,
+ ServletWebServerApplicationContext serverCtx,
+ OAuth2AuthorizedClientRepository oAuth2AuthorizedClientRepository) {
+ this.serverCtx = serverCtx;
+ this.authorizedClientManager = new DefaultOAuth2AuthorizedClientManager(clientRegistrationRepository,
+ oAuth2AuthorizedClientRepository);
+ }
+
+ @Override
+ public void execute(String serverUrl) {
+ log.info("Executing DefaultScenario");
+ var testServerUrl = "http://localhost:" + serverCtx.getWebServer().getPort();
+ var testClient = buildTestClient(testServerUrl);
+
+ var customizer = new OAuth2AuthorizationCodeSyncHttpRequestCustomizer(authorizedClientManager, REGISTRATION_ID);
+ HttpClientStreamableHttpTransport transport = HttpClientStreamableHttpTransport.builder(serverUrl)
+ .httpRequestCustomizer(customizer)
+ .build();
+
+ this.client = McpClient.sync(transport)
+ .transportContextProvider(new AuthenticationMcpTransportContextProvider())
+ .clientInfo(new McpSchema.Implementation("test-client", "1.0.0"))
+ .requestTimeout(Duration.ofSeconds(30))
+ .build();
+
+ try {
+ testClient.get().uri("/initialize-mcp-client").retrieve().toBodilessEntity();
+ }
+ finally {
+ // Close the client (which will close the transport)
+ this.client.close();
+
+ System.out.println("Connection closed successfully");
+ }
+ }
+
+ private static @NonNull RestClient buildTestClient(String testServerUrl) {
+ var cookieManager = new CookieManager();
+ cookieManager.setCookiePolicy(CookiePolicy.ACCEPT_ALL);
+ var httpClient = HttpClient.newBuilder()
+ .cookieHandler(cookieManager)
+ .followRedirects(HttpClient.Redirect.ALWAYS)
+ .build();
+ var testClient = RestClient.builder()
+ .baseUrl(testServerUrl)
+ .requestFactory(new JdkClientHttpRequestFactory(httpClient))
+ .build();
+ return testClient;
+ }
+
+ @Override
+ public McpSyncClient getMcpClient() {
+ if (this.client == null) {
+ return Scenario.super.getMcpClient();
+ }
+
+ return this.client;
+ }
+
+}
diff --git a/conformance-tests/client-spring-http-client/src/main/java/io/modelcontextprotocol/conformance/client/scenario/PreRegistrationScenario.java b/conformance-tests/client-spring-http-client/src/main/java/io/modelcontextprotocol/conformance/client/scenario/PreRegistrationScenario.java
new file mode 100644
index 000000000..8e6bbe228
--- /dev/null
+++ b/conformance-tests/client-spring-http-client/src/main/java/io/modelcontextprotocol/conformance/client/scenario/PreRegistrationScenario.java
@@ -0,0 +1,110 @@
+/*
+ * Copyright 2026-2026 the original author or authors.
+ */
+
+package io.modelcontextprotocol.conformance.client.scenario;
+
+import java.time.Duration;
+
+import io.modelcontextprotocol.client.McpClient;
+import io.modelcontextprotocol.client.transport.HttpClientStreamableHttpTransport;
+import io.modelcontextprotocol.spec.McpSchema;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springaicommunity.mcp.security.client.sync.AuthenticationMcpTransportContextProvider;
+import org.springaicommunity.mcp.security.client.sync.oauth2.http.client.OAuth2ClientCredentialsSyncHttpRequestCustomizer;
+import org.springaicommunity.mcp.security.client.sync.oauth2.metadata.McpMetadataDiscoveryService;
+import org.springaicommunity.mcp.security.client.sync.oauth2.registration.McpClientRegistrationRepository;
+import tools.jackson.databind.PropertyNamingStrategies;
+import tools.jackson.databind.annotation.JsonNaming;
+import tools.jackson.databind.json.JsonMapper;
+
+import org.springframework.security.oauth2.client.AuthorizedClientServiceOAuth2AuthorizedClientManager;
+import org.springframework.security.oauth2.client.OAuth2AuthorizedClientService;
+import org.springframework.security.oauth2.client.registration.ClientRegistrations;
+import org.springframework.security.oauth2.core.AuthorizationGrantType;
+import static io.modelcontextprotocol.conformance.client.ConformanceSpringClientApplication.REGISTRATION_ID;
+
+public class PreRegistrationScenario implements Scenario {
+
+ private static final Logger log = LoggerFactory.getLogger(PreRegistrationScenario.class);
+
+ private final JsonMapper mapper;
+
+ private final McpClientRegistrationRepository clientRegistrationRepository;
+
+ private final AuthorizedClientServiceOAuth2AuthorizedClientManager authorizedClientManager;
+
+ private final McpMetadataDiscoveryService mcpMetadataDiscovery;
+
+ public PreRegistrationScenario(McpClientRegistrationRepository clientRegistrationRepository,
+ McpMetadataDiscoveryService mcpMetadataDiscovery, OAuth2AuthorizedClientService authorizedClientService) {
+ this.mapper = JsonMapper.shared();
+ this.clientRegistrationRepository = clientRegistrationRepository;
+ this.authorizedClientManager = new AuthorizedClientServiceOAuth2AuthorizedClientManager(
+ clientRegistrationRepository, authorizedClientService);
+ this.mcpMetadataDiscovery = mcpMetadataDiscovery;
+ }
+
+ @Override
+ public void execute(String serverUrl) {
+ log.info("Executing PreRegistrationScenario");
+
+ var oauthCredentials = extractCredentialsFromContext();
+ setClientRegistration(serverUrl, oauthCredentials);
+
+ var customizer = new OAuth2ClientCredentialsSyncHttpRequestCustomizer(authorizedClientManager, REGISTRATION_ID);
+ HttpClientStreamableHttpTransport transport = HttpClientStreamableHttpTransport.builder(serverUrl)
+ .httpRequestCustomizer(customizer)
+ .build();
+
+ var client = McpClient.sync(transport)
+ .transportContextProvider(new AuthenticationMcpTransportContextProvider())
+ .clientInfo(new McpSchema.Implementation("test-client", "1.0.0"))
+ .requestTimeout(Duration.ofSeconds(30))
+ .build();
+
+ try {
+ // Initialize client
+ client.initialize();
+
+ System.out.println("Successfully connected to MCP server");
+ }
+ finally {
+ // Close the client (which will close the transport)
+ client.close();
+
+ System.out.println("Connection closed successfully");
+ }
+ }
+
+ private void setClientRegistration(String mcpServerUrl, PreRegistrationContext oauthCredentials) {
+ var metadata = this.mcpMetadataDiscovery.getMcpMetadata(mcpServerUrl);
+ var registration = ClientRegistrations
+ .fromIssuerLocation(metadata.protectedResourceMetadata().authorizationServers().get(0))
+ .registrationId(REGISTRATION_ID)
+ .clientId(oauthCredentials.clientId())
+ .clientSecret(oauthCredentials.clientSecret())
+ .authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)
+ .build();
+ clientRegistrationRepository.addPreRegisteredClient(registration,
+ metadata.protectedResourceMetadata().resource());
+ }
+
+ private PreRegistrationContext extractCredentialsFromContext() {
+ String contextEnv = System.getenv("MCP_CONFORMANCE_CONTEXT");
+ if (contextEnv == null || contextEnv.isEmpty()) {
+ var errorMessage = "Error: MCP_CONFORMANCE_CONTEXT environment variable is not set";
+ System.err.println(errorMessage);
+ throw new RuntimeException(errorMessage);
+ }
+
+ return mapper.readValue(contextEnv, PreRegistrationContext.class);
+ }
+
+ @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
+ private record PreRegistrationContext(String clientId, String clientSecret) {
+
+ }
+
+}
diff --git a/conformance-tests/client-spring-http-client/src/main/java/io/modelcontextprotocol/conformance/client/scenario/Scenario.java b/conformance-tests/client-spring-http-client/src/main/java/io/modelcontextprotocol/conformance/client/scenario/Scenario.java
new file mode 100644
index 000000000..9054db83b
--- /dev/null
+++ b/conformance-tests/client-spring-http-client/src/main/java/io/modelcontextprotocol/conformance/client/scenario/Scenario.java
@@ -0,0 +1,17 @@
+/*
+ * Copyright 2026-2026 the original author or authors.
+ */
+
+package io.modelcontextprotocol.conformance.client.scenario;
+
+import io.modelcontextprotocol.client.McpSyncClient;
+
+public interface Scenario {
+
+ default McpSyncClient getMcpClient() {
+ throw new IllegalStateException("Client not set");
+ }
+
+ void execute(String serverUrl);
+
+}
diff --git a/conformance-tests/client-spring-http-client/src/main/resources/application.properties b/conformance-tests/client-spring-http-client/src/main/resources/application.properties
new file mode 100644
index 000000000..0c4a77438
--- /dev/null
+++ b/conformance-tests/client-spring-http-client/src/main/resources/application.properties
@@ -0,0 +1,4 @@
+# Server runs on random port
+server.port=0
+# Disable Spring AI MCP client auto-configuration (we configure the client manually)
+spring.ai.mcp.client.enabled=false
diff --git a/conformance-tests/conformance-baseline.yml b/conformance-tests/conformance-baseline.yml
new file mode 100644
index 000000000..4ab144063
--- /dev/null
+++ b/conformance-tests/conformance-baseline.yml
@@ -0,0 +1,18 @@
+# MCP Java SDK Conformance Test Baseline
+# This file lists known failing scenarios that are expected to fail until fixed.
+# See: https://github.com/modelcontextprotocol/conformance/blob/main/SDK_INTEGRATION.md
+
+server:
+ # Resource subscription not implemented in SDK
+ - resources-subscribe
+ - resources-unsubscribe
+
+client:
+ # SSE retry field handling not implemented
+ # - Client does not parse or respect retry: field timing
+ # - Client does not send Last-Event-ID header
+ - sse-retry
+ # CIMD not implemented yet
+ - auth/basic-cimd
+ # Scope step up beyond initial authorization request not implemented
+ - auth/scope-step-up
diff --git a/conformance-tests/pom.xml b/conformance-tests/pom.xml
new file mode 100644
index 000000000..d1bef2a24
--- /dev/null
+++ b/conformance-tests/pom.xml
@@ -0,0 +1,33 @@
+
+
+ 4.0.0
+
+ io.modelcontextprotocol.sdk
+ mcp-parent
+ 1.1.0-SNAPSHOT
+
+ conformance-tests
+ pom
+ MCP Conformance Tests
+ Conformance tests for the Java MCP SDK
+ https://github.com/modelcontextprotocol/java-sdk
+
+
+ https://github.com/modelcontextprotocol/java-sdk
+ git://github.com/modelcontextprotocol/java-sdk.git
+ git@github.com/modelcontextprotocol/java-sdk.git
+
+
+
+ true
+
+
+
+ client-jdk-http-client
+ client-spring-http-client
+ server-servlet
+
+
+
diff --git a/conformance-tests/server-servlet/README.md b/conformance-tests/server-servlet/README.md
new file mode 100644
index 000000000..bd86636b6
--- /dev/null
+++ b/conformance-tests/server-servlet/README.md
@@ -0,0 +1,205 @@
+# MCP Conformance Tests - Servlet Server
+
+This module contains a comprehensive MCP (Model Context Protocol) server implementation for conformance testing using the servlet stack with an embedded Tomcat server and streamable HTTP transport.
+
+## Conformance Test Results
+
+**Status: 37 out of 40 tests passing (92.5%)**
+
+The server has been validated against the official [MCP conformance test suite](https://github.com/modelcontextprotocol/conformance). See [VALIDATION_RESULTS.md](../VALIDATION_RESULTS.md) for detailed results.
+
+### What's Implemented
+
+✅ **Lifecycle & Utilities** (4/4)
+- Server initialization, ping, logging, completion
+
+✅ **Tools** (11/11)
+- Text, image, audio, embedded resources, mixed content
+- Logging, error handling, sampling, elicitation
+- Progress notifications
+
+✅ **Elicitation** (10/10)
+- SEP-1034: Default values for all primitive types
+- SEP-1330: All enum schema variants
+
+✅ **Resources** (4/6)
+- List, read text/binary, templates
+- ⚠️ Subscribe/unsubscribe (SDK limitation)
+
+✅ **Prompts** (4/4)
+- Simple, parameterized, embedded resources, images
+
+✅ **SSE Transport** (2/2)
+- Multiple streams support
+
+✅ **Security** (2/2)
+- ✅ DNS rebinding protection
+
+## Features
+
+- Embedded Tomcat servlet container
+- MCP server using HttpServletStreamableServerTransportProvider
+- Comprehensive test coverage with 15+ tools
+- Streamable HTTP transport with SSE on `/mcp` endpoint
+- Support for all MCP content types (text, image, audio, resources)
+- Advanced features: sampling, elicitation, progress (partial), completion
+
+## Running the Server
+
+To run the conformance server:
+
+```bash
+cd conformance-tests/server-servlet
+../../mvnw compile exec:java -Dexec.mainClass="io.modelcontextprotocol.conformance.server.ConformanceServlet"
+```
+
+Or from the root directory:
+
+```bash
+./mvnw compile exec:java -pl conformance-tests/server-servlet -Dexec.mainClass="io.modelcontextprotocol.conformance.server.ConformanceServlet"
+```
+
+The server will start on port 8080 with the MCP endpoint at `/mcp`.
+
+## Running Conformance Tests
+
+Once the server is running, you can validate it against the official MCP conformance test suite using `npx`:
+
+### Run Full Active Test Suite
+
+```bash
+npx @modelcontextprotocol/conformance server --url http://localhost:8080/mcp --suite active
+```
+
+### Run Specific Scenarios
+
+```bash
+# Test tools
+npx @modelcontextprotocol/conformance server --url http://localhost:8080/mcp --scenario tools-list --verbose
+
+# Test prompts
+npx @modelcontextprotocol/conformance server --url http://localhost:8080/mcp --scenario prompts-list --verbose
+
+# Test resources
+npx @modelcontextprotocol/conformance server --url http://localhost:8080/mcp --scenario resources-read-text --verbose
+
+# Test elicitation with defaults
+npx @modelcontextprotocol/conformance server --url http://localhost:8080/mcp --scenario elicitation-sep1034-defaults --verbose
+```
+
+### Available Test Suites
+
+- `active` (default) - All active/stable tests (30 scenarios)
+- `all` - All tests including pending/experimental
+- `pending` - Only pending/experimental tests
+
+### Common Scenarios
+
+**Lifecycle & Utilities:**
+- `server-initialize` - Server initialization
+- `ping` - Ping utility
+- `logging-set-level` - Logging configuration
+- `completion-complete` - Argument completion
+
+**Tools:**
+- `tools-list` - List available tools
+- `tools-call-simple-text` - Simple text response
+- `tools-call-image` - Image content
+- `tools-call-audio` - Audio content
+- `tools-call-with-logging` - Logging during execution
+- `tools-call-with-progress` - Progress notifications
+- `tools-call-sampling` - LLM sampling
+- `tools-call-elicitation` - User input requests
+
+**Resources:**
+- `resources-list` - List resources
+- `resources-read-text` - Read text resource
+- `resources-read-binary` - Read binary resource
+- `resources-templates-read` - Resource templates
+- `resources-subscribe` - Subscribe to resource updates
+- `resources-unsubscribe` - Unsubscribe from updates
+
+**Prompts:**
+- `prompts-list` - List prompts
+- `prompts-get-simple` - Simple prompt
+- `prompts-get-with-args` - Parameterized prompt
+- `prompts-get-embedded-resource` - Prompt with resource
+- `prompts-get-with-image` - Prompt with image
+
+**Elicitation:**
+- `elicitation-sep1034-defaults` - Default values (SEP-1034)
+- `elicitation-sep1330-enums` - Enum schemas (SEP-1330)
+
+## Testing with curl
+
+You can also test the endpoint manually:
+
+```bash
+# Check endpoint (will show SSE requirement)
+curl -X GET http://localhost:8080/mcp
+
+# Initialize session with proper headers
+curl -X POST http://localhost:8080/mcp \
+ -H "Content-Type: application/json" \
+ -H "Accept: text/event-stream" \
+ -H "mcp-session-id: test-session-123" \
+ -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"test-client","version":"1.0.0"}}}'
+```
+
+## Architecture
+
+- **Transport**: HttpServletStreamableServerTransportProvider (streamable HTTP with SSE)
+- **Container**: Embedded Apache Tomcat
+- **Protocol**: Streamable HTTP with Server-Sent Events
+- **Port**: 8080 (default)
+- **Endpoint**: `/mcp`
+- **Request Timeout**: 30 seconds
+
+## Implemented Tools
+
+### Content Type Tools
+- `test_simple_text` - Returns simple text content
+- `test_image_content` - Returns a minimal PNG image (1x1 red pixel)
+- `test_audio_content` - Returns a minimal WAV audio file
+- `test_embedded_resource` - Returns embedded resource content
+- `test_multiple_content_types` - Returns mixed text, image, and resource content
+
+### Behavior Tools
+- `test_tool_with_logging` - Sends log notifications during execution
+- `test_error_handling` - Intentionally returns an error for testing
+- `test_tool_with_progress` - Reports progress notifications (⚠️ SDK issue)
+
+### Interactive Tools
+- `test_sampling` - Requests LLM sampling from client
+- `test_elicitation` - Requests user input from client
+- `test_elicitation_sep1034_defaults` - Elicitation with default values (SEP-1034)
+- `test_elicitation_sep1330_enums` - Elicitation with enum schemas (SEP-1330)
+
+## Implemented Prompts
+
+- `test_simple_prompt` - Simple prompt without arguments
+- `test_prompt_with_arguments` - Prompt with required arguments (arg1, arg2)
+- `test_prompt_with_embedded_resource` - Prompt with embedded resource content
+- `test_prompt_with_image` - Prompt with image content
+
+## Implemented Resources
+
+- `test://static-text` - Static text resource
+- `test://static-binary` - Static binary resource (PNG image)
+- `test://watched-resource` - Resource that can be subscribed to
+- `test://template/{id}/data` - Resource template with parameter substitution
+
+## Known Limitations
+
+See [VALIDATION_RESULTS.md](../VALIDATION_RESULTS.md) for details on:
+
+1. **Resource Subscriptions** - Not implemented in Java SDK
+2. **DNS Rebinding Protection** - Missing Host/Origin validation
+
+These are SDK-level limitations that require fixes in the core framework.
+
+## References
+
+- [MCP Specification](https://modelcontextprotocol.io/specification/)
+- [MCP Conformance Tests](https://github.com/modelcontextprotocol/conformance)
+- [SDK Integration Guide](https://github.com/modelcontextprotocol/conformance/blob/main/SDK_INTEGRATION.md)
diff --git a/conformance-tests/server-servlet/pom.xml b/conformance-tests/server-servlet/pom.xml
new file mode 100644
index 000000000..68da42158
--- /dev/null
+++ b/conformance-tests/server-servlet/pom.xml
@@ -0,0 +1,73 @@
+
+
+ 4.0.0
+
+ io.modelcontextprotocol.sdk
+ conformance-tests
+ 1.1.0-SNAPSHOT
+
+ server-servlet
+ jar
+ MCP Conformance Tests - Servlet Server
+ Servlet Server conformance tests for the Java MCP SDK
+ https://github.com/modelcontextprotocol/java-sdk
+
+
+ https://github.com/modelcontextprotocol/java-sdk
+ git://github.com/modelcontextprotocol/java-sdk.git
+ git@github.com/modelcontextprotocol/java-sdk.git
+
+
+
+ true
+
+
+
+
+ io.modelcontextprotocol.sdk
+ mcp
+ 1.1.0-SNAPSHOT
+
+
+
+ org.slf4j
+ slf4j-api
+ ${slf4j-api.version}
+
+
+
+ ch.qos.logback
+ logback-classic
+ ${logback.version}
+
+
+
+ jakarta.servlet
+ jakarta.servlet-api
+ ${jakarta.servlet.version}
+ provided
+
+
+
+ org.apache.tomcat.embed
+ tomcat-embed-core
+ ${tomcat.version}
+
+
+
+
+
+
+ org.codehaus.mojo
+ exec-maven-plugin
+ 3.1.0
+
+ io.modelcontextprotocol.conformance.server.ConformanceServlet
+
+
+
+
+
+
\ No newline at end of file
diff --git a/conformance-tests/server-servlet/src/main/java/io/modelcontextprotocol/conformance/server/ConformanceServlet.java b/conformance-tests/server-servlet/src/main/java/io/modelcontextprotocol/conformance/server/ConformanceServlet.java
new file mode 100644
index 000000000..3d162a5de
--- /dev/null
+++ b/conformance-tests/server-servlet/src/main/java/io/modelcontextprotocol/conformance/server/ConformanceServlet.java
@@ -0,0 +1,596 @@
+package io.modelcontextprotocol.conformance.server;
+
+import java.time.Duration;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+
+import io.modelcontextprotocol.server.McpServer;
+import io.modelcontextprotocol.server.McpServerFeatures;
+import io.modelcontextprotocol.server.transport.DefaultServerTransportSecurityValidator;
+import io.modelcontextprotocol.server.transport.HttpServletStreamableServerTransportProvider;
+import io.modelcontextprotocol.spec.McpSchema.AudioContent;
+import io.modelcontextprotocol.spec.McpSchema.BlobResourceContents;
+import io.modelcontextprotocol.spec.McpSchema.CallToolResult;
+import io.modelcontextprotocol.spec.McpSchema.CompleteResult;
+import io.modelcontextprotocol.spec.McpSchema.CreateMessageRequest;
+import io.modelcontextprotocol.spec.McpSchema.CreateMessageResult;
+import io.modelcontextprotocol.spec.McpSchema.ElicitRequest;
+import io.modelcontextprotocol.spec.McpSchema.ElicitResult;
+import io.modelcontextprotocol.spec.McpSchema.EmbeddedResource;
+import io.modelcontextprotocol.spec.McpSchema.GetPromptResult;
+import io.modelcontextprotocol.spec.McpSchema.ImageContent;
+import io.modelcontextprotocol.spec.McpSchema.JsonSchema;
+import io.modelcontextprotocol.spec.McpSchema.LoggingLevel;
+import io.modelcontextprotocol.spec.McpSchema.LoggingMessageNotification;
+import io.modelcontextprotocol.spec.McpSchema.ProgressNotification;
+import io.modelcontextprotocol.spec.McpSchema.Prompt;
+import io.modelcontextprotocol.spec.McpSchema.PromptArgument;
+import io.modelcontextprotocol.spec.McpSchema.PromptMessage;
+import io.modelcontextprotocol.spec.McpSchema.PromptReference;
+import io.modelcontextprotocol.spec.McpSchema.ReadResourceResult;
+import io.modelcontextprotocol.spec.McpSchema.Resource;
+import io.modelcontextprotocol.spec.McpSchema.ResourceTemplate;
+import io.modelcontextprotocol.spec.McpSchema.Role;
+import io.modelcontextprotocol.spec.McpSchema.SamplingMessage;
+import io.modelcontextprotocol.spec.McpSchema.ServerCapabilities;
+import io.modelcontextprotocol.spec.McpSchema.TextContent;
+import io.modelcontextprotocol.spec.McpSchema.TextResourceContents;
+import io.modelcontextprotocol.spec.McpSchema.Tool;
+import org.apache.catalina.Context;
+import org.apache.catalina.LifecycleException;
+import org.apache.catalina.startup.Tomcat;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class ConformanceServlet {
+
+ private static final Logger logger = LoggerFactory.getLogger(ConformanceServlet.class);
+
+ private static final int PORT = 8080;
+
+ private static final String MCP_ENDPOINT = "/mcp";
+
+ private static final JsonSchema EMPTY_JSON_SCHEMA = new JsonSchema("object", Collections.emptyMap(), null, null,
+ null, null);
+
+ // Minimal 1x1 red pixel PNG (base64 encoded)
+ private static final String RED_PIXEL_PNG = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8DwHwAFBQIAX8jx0gAAAABJRU5ErkJggg==";
+
+ // Minimal WAV file (base64 encoded) - 1 sample at 8kHz
+ private static final String MINIMAL_WAV = "UklGRiQAAABXQVZFZm10IBAAAAABAAEAQB8AAAB9AAACABAAZGF0YQAAAAA=";
+
+ public static void main(String[] args) throws Exception {
+ logger.info("Starting MCP Conformance Tests - Servlet Server");
+
+ HttpServletStreamableServerTransportProvider transportProvider = HttpServletStreamableServerTransportProvider
+ .builder()
+ .mcpEndpoint(MCP_ENDPOINT)
+ .keepAliveInterval(Duration.ofSeconds(30))
+ .securityValidator(DefaultServerTransportSecurityValidator.builder()
+ .allowedOrigin("http://localhost:*")
+ .allowedHost("localhost:*")
+ .build())
+ .build();
+
+ // Build server with all conformance test features
+ var mcpServer = McpServer.sync(transportProvider)
+ .serverInfo("mcp-conformance-server", "1.0.0")
+ .capabilities(ServerCapabilities.builder()
+ .completions()
+ .resources(true, false)
+ .tools(false)
+ .prompts(false)
+ .build())
+ .tools(createToolSpecs())
+ .prompts(createPromptSpecs())
+ .resources(createResourceSpecs())
+ .resourceTemplates(createResourceTemplateSpecs())
+ .completions(createCompletionSpecs())
+ .requestTimeout(Duration.ofSeconds(30))
+ .build();
+
+ // Set up embedded Tomcat
+ Tomcat tomcat = createEmbeddedTomcat(transportProvider);
+
+ try {
+ tomcat.start();
+ logger.info("Conformance MCP Servlet Server started on port {} with endpoint {}", PORT, MCP_ENDPOINT);
+ logger.info("Server URL: http://localhost:{}{}", PORT, MCP_ENDPOINT);
+
+ // Keep the server running
+ tomcat.getServer().await();
+ }
+ catch (LifecycleException e) {
+ logger.error("Failed to start Tomcat server", e);
+ throw e;
+ }
+ finally {
+ logger.info("Shutting down MCP server...");
+ mcpServer.closeGracefully();
+ try {
+ tomcat.stop();
+ tomcat.destroy();
+ }
+ catch (LifecycleException e) {
+ logger.error("Error during Tomcat shutdown", e);
+ }
+ }
+ }
+
+ private static Tomcat createEmbeddedTomcat(HttpServletStreamableServerTransportProvider transportProvider) {
+ Tomcat tomcat = new Tomcat();
+ tomcat.setPort(PORT);
+
+ String baseDir = System.getProperty("java.io.tmpdir");
+ tomcat.setBaseDir(baseDir);
+
+ Context context = tomcat.addContext("", baseDir);
+
+ // Add the MCP servlet to Tomcat
+ org.apache.catalina.Wrapper wrapper = context.createWrapper();
+ wrapper.setName("mcpServlet");
+ wrapper.setServlet(transportProvider);
+ wrapper.setLoadOnStartup(1);
+ wrapper.setAsyncSupported(true);
+ context.addChild(wrapper);
+ context.addServletMappingDecoded("/*", "mcpServlet");
+
+ var connector = tomcat.getConnector();
+ connector.setAsyncTimeout(30000);
+ return tomcat;
+ }
+
+ private static List createToolSpecs() {
+ return List.of(
+ // test_simple_text - Returns simple text content
+ McpServerFeatures.SyncToolSpecification.builder()
+ .tool(Tool.builder()
+ .name("test_simple_text")
+ .description("Returns simple text content for testing")
+ .inputSchema(EMPTY_JSON_SCHEMA)
+ .build())
+ .callHandler((exchange, request) -> {
+ logger.info("Tool 'test_simple_text' called");
+ return CallToolResult.builder()
+ .content(List.of(new TextContent("This is a simple text response for testing.")))
+ .isError(false)
+ .build();
+ })
+ .build(),
+
+ // test_image_content - Returns image content
+ McpServerFeatures.SyncToolSpecification.builder()
+ .tool(Tool.builder()
+ .name("test_image_content")
+ .description("Returns image content for testing")
+ .inputSchema(EMPTY_JSON_SCHEMA)
+ .build())
+ .callHandler((exchange, request) -> {
+ logger.info("Tool 'test_image_content' called");
+ return CallToolResult.builder()
+ .content(List.of(new ImageContent(null, RED_PIXEL_PNG, "image/png")))
+ .isError(false)
+ .build();
+ })
+ .build(),
+
+ // test_audio_content - Returns audio content
+ McpServerFeatures.SyncToolSpecification.builder()
+ .tool(Tool.builder()
+ .name("test_audio_content")
+ .description("Returns audio content for testing")
+ .inputSchema(EMPTY_JSON_SCHEMA)
+ .build())
+ .callHandler((exchange, request) -> {
+ logger.info("Tool 'test_audio_content' called");
+ return CallToolResult.builder()
+ .content(List.of(new AudioContent(null, MINIMAL_WAV, "audio/wav")))
+ .isError(false)
+ .build();
+ })
+ .build(),
+
+ // test_embedded_resource - Returns embedded resource content
+ McpServerFeatures.SyncToolSpecification.builder()
+ .tool(Tool.builder()
+ .name("test_embedded_resource")
+ .description("Returns embedded resource content for testing")
+ .inputSchema(EMPTY_JSON_SCHEMA)
+ .build())
+ .callHandler((exchange, request) -> {
+ logger.info("Tool 'test_embedded_resource' called");
+ TextResourceContents resourceContents = new TextResourceContents("test://embedded-resource",
+ "text/plain", "This is an embedded resource content.");
+ EmbeddedResource embeddedResource = new EmbeddedResource(null, resourceContents);
+ return CallToolResult.builder().content(List.of(embeddedResource)).isError(false).build();
+ })
+ .build(),
+
+ // test_multiple_content_types - Returns multiple content types
+ McpServerFeatures.SyncToolSpecification.builder()
+ .tool(Tool.builder()
+ .name("test_multiple_content_types")
+ .description("Returns multiple content types for testing")
+ .inputSchema(EMPTY_JSON_SCHEMA)
+ .build())
+ .callHandler((exchange, request) -> {
+ logger.info("Tool 'test_multiple_content_types' called");
+ TextResourceContents resourceContents = new TextResourceContents(
+ "test://mixed-content-resource", "application/json",
+ "{\"test\":\"data\",\"value\":123}");
+ EmbeddedResource embeddedResource = new EmbeddedResource(null, resourceContents);
+ return CallToolResult.builder()
+ .content(List.of(new TextContent("Multiple content types test:"),
+ new ImageContent(null, RED_PIXEL_PNG, "image/png"), embeddedResource))
+ .isError(false)
+ .build();
+ })
+ .build(),
+
+ // test_tool_with_logging - Tool that sends log messages during execution
+ McpServerFeatures.SyncToolSpecification.builder()
+ .tool(Tool.builder()
+ .name("test_tool_with_logging")
+ .description("Tool that sends log messages during execution")
+ .inputSchema(EMPTY_JSON_SCHEMA)
+ .build())
+ .callHandler((exchange, request) -> {
+ logger.info("Tool 'test_tool_with_logging' called");
+ // Send log notifications
+ exchange.loggingNotification(LoggingMessageNotification.builder()
+ .level(LoggingLevel.INFO)
+ .data("Tool execution started")
+ .build());
+ exchange.loggingNotification(LoggingMessageNotification.builder()
+ .level(LoggingLevel.INFO)
+ .data("Tool processing data")
+ .build());
+ exchange.loggingNotification(LoggingMessageNotification.builder()
+ .level(LoggingLevel.INFO)
+ .data("Tool execution completed")
+ .build());
+ return CallToolResult.builder()
+ .content(List.of(new TextContent("Tool execution completed with logging")))
+ .isError(false)
+ .build();
+ })
+ .build(),
+
+ // test_error_handling - Tool that always returns an error
+ McpServerFeatures.SyncToolSpecification.builder()
+ .tool(Tool.builder()
+ .name("test_error_handling")
+ .description("Tool that returns an error for testing error handling")
+ .inputSchema(EMPTY_JSON_SCHEMA)
+ .build())
+ .callHandler((exchange, request) -> {
+ logger.info("Tool 'test_error_handling' called");
+ return CallToolResult.builder()
+ .content(List.of(new TextContent("This tool intentionally returns an error for testing")))
+ .isError(true)
+ .build();
+ })
+ .build(),
+
+ // test_tool_with_progress - Tool that reports progress
+ McpServerFeatures.SyncToolSpecification.builder()
+ .tool(Tool.builder()
+ .name("test_tool_with_progress")
+ .description("Tool that reports progress notifications")
+ .inputSchema(EMPTY_JSON_SCHEMA)
+ .build())
+ .callHandler((exchange, request) -> {
+ logger.info("Tool 'test_tool_with_progress' called");
+ Object progressToken = request.meta().get("progressToken");
+ if (progressToken != null) {
+ // Send progress notifications sequentially
+ exchange.progressNotification(new ProgressNotification(progressToken, 0.0, 100.0, null));
+ // try {
+ // Thread.sleep(50);
+ // }
+ // catch (InterruptedException e) {
+ // Thread.currentThread().interrupt();
+ // }
+ exchange.progressNotification(new ProgressNotification(progressToken, 50.0, 100.0, null));
+ // try {
+ // Thread.sleep(50);
+ // }
+ // catch (InterruptedException e) {
+ // Thread.currentThread().interrupt();
+ // }
+ exchange.progressNotification(new ProgressNotification(progressToken, 100.0, 100.0, null));
+ return CallToolResult.builder()
+ .content(List.of(new TextContent("Tool execution completed with progress")))
+ .isError(false)
+ .build();
+ }
+ else {
+ // No progress token, just execute with delays
+ // try {
+ // Thread.sleep(100);
+ // }
+ // catch (InterruptedException e) {
+ // Thread.currentThread().interrupt();
+ // }
+ return CallToolResult.builder()
+ .content(List.of(new TextContent("Tool execution completed without progress")))
+ .isError(false)
+ .build();
+ }
+ })
+ .build(),
+
+ // test_sampling - Tool that requests LLM sampling from client
+ McpServerFeatures.SyncToolSpecification.builder()
+ .tool(Tool.builder()
+ .name("test_sampling")
+ .description("Tool that requests LLM sampling from client")
+ .inputSchema(new JsonSchema("object",
+ Map.of("prompt",
+ Map.of("type", "string", "description", "The prompt to send to the LLM")),
+ List.of("prompt"), null, null, null))
+ .build())
+ .callHandler((exchange, request) -> {
+ logger.info("Tool 'test_sampling' called");
+ String prompt = (String) request.arguments().get("prompt");
+
+ // Request sampling from client
+ CreateMessageRequest samplingRequest = CreateMessageRequest.builder()
+ .messages(List.of(new SamplingMessage(Role.USER, new TextContent(prompt))))
+ .maxTokens(100)
+ .build();
+
+ CreateMessageResult response = exchange.createMessage(samplingRequest);
+ String responseText = "LLM response: " + ((TextContent) response.content()).text();
+ return CallToolResult.builder()
+ .content(List.of(new TextContent(responseText)))
+ .isError(false)
+ .build();
+ })
+ .build(),
+
+ // test_elicitation - Tool that requests user input from client
+ McpServerFeatures.SyncToolSpecification.builder()
+ .tool(Tool.builder()
+ .name("test_elicitation")
+ .description("Tool that requests user input from client")
+ .inputSchema(new JsonSchema("object",
+ Map.of("message",
+ Map.of("type", "string", "description", "The message to show the user")),
+ List.of("message"), null, null, null))
+ .build())
+ .callHandler((exchange, request) -> {
+ logger.info("Tool 'test_elicitation' called");
+ String message = (String) request.arguments().get("message");
+
+ // Request elicitation from client
+ Map requestedSchema = Map.of("type", "object", "properties",
+ Map.of("username", Map.of("type", "string", "description", "User's response"), "email",
+ Map.of("type", "string", "description", "User's email address")),
+ "required", List.of("username", "email"));
+
+ ElicitRequest elicitRequest = new ElicitRequest(message, requestedSchema);
+
+ ElicitResult response = exchange.createElicitation(elicitRequest);
+ String responseText = "User response: action=" + response.action() + ", content="
+ + response.content();
+ return CallToolResult.builder()
+ .content(List.of(new TextContent(responseText)))
+ .isError(false)
+ .build();
+ })
+ .build(),
+
+ // test_elicitation_sep1034_defaults - Tool with default values for all
+ // primitive types
+ McpServerFeatures.SyncToolSpecification.builder()
+ .tool(Tool.builder()
+ .name("test_elicitation_sep1034_defaults")
+ .description("Tool that requests elicitation with default values for all primitive types")
+ .inputSchema(EMPTY_JSON_SCHEMA)
+ .build())
+ .callHandler((exchange, request) -> {
+ logger.info("Tool 'test_elicitation_sep1034_defaults' called");
+
+ // Create schema with default values for all primitive types
+ Map requestedSchema = Map.of("type", "object", "properties",
+ Map.of("name", Map.of("type", "string", "default", "John Doe"), "age",
+ Map.of("type", "integer", "default", 30), "score",
+ Map.of("type", "number", "default", 95.5), "status",
+ Map.of("type", "string", "enum", List.of("active", "inactive", "pending"),
+ "default", "active"),
+ "verified", Map.of("type", "boolean", "default", true)),
+ "required", List.of("name", "age", "score", "status", "verified"));
+
+ ElicitRequest elicitRequest = new ElicitRequest("Please provide your information with defaults",
+ requestedSchema);
+
+ ElicitResult response = exchange.createElicitation(elicitRequest);
+ String responseText = "Elicitation completed: action=" + response.action() + ", content="
+ + response.content();
+ return CallToolResult.builder()
+ .content(List.of(new TextContent(responseText)))
+ .isError(false)
+ .build();
+ })
+ .build(),
+
+ // test_elicitation_sep1330_enums - Tool with enum schema improvements
+ McpServerFeatures.SyncToolSpecification.builder()
+ .tool(Tool.builder()
+ .name("test_elicitation_sep1330_enums")
+ .description("Tool that requests elicitation with enum schema improvements")
+ .inputSchema(EMPTY_JSON_SCHEMA)
+ .build())
+ .callHandler((exchange, request) -> {
+ logger.info("Tool 'test_elicitation_sep1330_enums' called");
+
+ // Create schema with all 5 enum variants
+ Map requestedSchema = Map.of("type", "object", "properties", Map.of(
+ // 1. Untitled single-select
+ "untitledSingle",
+ Map.of("type", "string", "enum", List.of("option1", "option2", "option3")),
+ // 2. Titled single-select using oneOf with const/title
+ "titledSingle",
+ Map.of("type", "string", "oneOf",
+ List.of(Map.of("const", "value1", "title", "First Option"),
+ Map.of("const", "value2", "title", "Second Option"),
+ Map.of("const", "value3", "title", "Third Option"))),
+ // 3. Legacy titled using enumNames (deprecated)
+ "legacyEnum",
+ Map.of("type", "string", "enum", List.of("opt1", "opt2", "opt3"), "enumNames",
+ List.of("Option One", "Option Two", "Option Three")),
+ // 4. Untitled multi-select
+ "untitledMulti",
+ Map.of("type", "array", "items",
+ Map.of("type", "string", "enum", List.of("option1", "option2", "option3"))),
+ // 5. Titled multi-select using items.anyOf with
+ // const/title
+ "titledMulti",
+ Map.of("type", "array", "items",
+ Map.of("anyOf",
+ List.of(Map.of("const", "value1", "title", "First Choice"),
+ Map.of("const", "value2", "title", "Second Choice"),
+ Map.of("const", "value3", "title", "Third Choice"))))),
+ "required", List.of("untitledSingle", "titledSingle", "legacyEnum", "untitledMulti",
+ "titledMulti"));
+
+ ElicitRequest elicitRequest = new ElicitRequest("Select your preferences", requestedSchema);
+
+ ElicitResult response = exchange.createElicitation(elicitRequest);
+ String responseText = "Elicitation completed: action=" + response.action() + ", content="
+ + response.content();
+ return CallToolResult.builder()
+ .content(List.of(new TextContent(responseText)))
+ .isError(false)
+ .build();
+ })
+ .build());
+ }
+
+ private static List createPromptSpecs() {
+ return List.of(
+ // test_simple_prompt - Simple prompt without arguments
+ new McpServerFeatures.SyncPromptSpecification(
+ new Prompt("test_simple_prompt", null, "A simple prompt for testing", List.of()),
+ (exchange, request) -> {
+ logger.info("Prompt 'test_simple_prompt' requested");
+ return new GetPromptResult(null, List.of(new PromptMessage(Role.USER,
+ new TextContent("This is a simple prompt for testing."))));
+ }),
+
+ // test_prompt_with_arguments - Prompt with arguments
+ new McpServerFeatures.SyncPromptSpecification(
+ new Prompt("test_prompt_with_arguments", null, "A prompt with arguments for testing",
+ List.of(new PromptArgument("arg1", "First test argument", true),
+ new PromptArgument("arg2", "Second test argument", true))),
+ (exchange, request) -> {
+ logger.info("Prompt 'test_prompt_with_arguments' requested");
+ String arg1 = (String) request.arguments().get("arg1");
+ String arg2 = (String) request.arguments().get("arg2");
+ String text = String.format("Prompt with arguments: arg1='%s', arg2='%s'", arg1, arg2);
+ return new GetPromptResult(null,
+ List.of(new PromptMessage(Role.USER, new TextContent(text))));
+ }),
+
+ // test_prompt_with_embedded_resource - Prompt with embedded resource
+ new McpServerFeatures.SyncPromptSpecification(
+ new Prompt("test_prompt_with_embedded_resource", null,
+ "A prompt with embedded resource for testing",
+ List.of(new PromptArgument("resourceUri", "URI of the resource to embed", true))),
+ (exchange, request) -> {
+ logger.info("Prompt 'test_prompt_with_embedded_resource' requested");
+ String resourceUri = (String) request.arguments().get("resourceUri");
+ TextResourceContents resourceContents = new TextResourceContents(resourceUri, "text/plain",
+ "Embedded resource content for testing.");
+ EmbeddedResource embeddedResource = new EmbeddedResource(null, resourceContents);
+ return new GetPromptResult(null,
+ List.of(new PromptMessage(Role.USER, embeddedResource), new PromptMessage(Role.USER,
+ new TextContent("Please process the embedded resource above."))));
+ }),
+
+ // test_prompt_with_image - Prompt with image content
+ new McpServerFeatures.SyncPromptSpecification(new Prompt("test_prompt_with_image", null,
+ "A prompt with image content for testing", List.of()), (exchange, request) -> {
+ logger.info("Prompt 'test_prompt_with_image' requested");
+ return new GetPromptResult(null, List.of(
+ new PromptMessage(Role.USER, new ImageContent(null, RED_PIXEL_PNG, "image/png")),
+ new PromptMessage(Role.USER, new TextContent("Please analyze the image above."))));
+ }));
+ }
+
+ private static List createResourceSpecs() {
+ return List.of(
+ // test://static-text - Static text resource
+ new McpServerFeatures.SyncResourceSpecification(Resource.builder()
+ .uri("test://static-text")
+ .name("Static Text Resource")
+ .description("A static text resource for testing")
+ .mimeType("text/plain")
+ .build(), (exchange, request) -> {
+ logger.info("Resource 'test://static-text' requested");
+ return new ReadResourceResult(List.of(new TextResourceContents("test://static-text",
+ "text/plain", "This is the content of the static text resource.")));
+ }),
+
+ // test://static-binary - Static binary resource (image)
+ new McpServerFeatures.SyncResourceSpecification(Resource.builder()
+ .uri("test://static-binary")
+ .name("Static Binary Resource")
+ .description("A static binary resource for testing")
+ .mimeType("image/png")
+ .build(), (exchange, request) -> {
+ logger.info("Resource 'test://static-binary' requested");
+ return new ReadResourceResult(
+ List.of(new BlobResourceContents("test://static-binary", "image/png", RED_PIXEL_PNG)));
+ }),
+
+ // test://watched-resource - Resource that can be subscribed to
+ new McpServerFeatures.SyncResourceSpecification(Resource.builder()
+ .uri("test://watched-resource")
+ .name("Watched Resource")
+ .description("A resource that can be subscribed to for updates")
+ .mimeType("text/plain")
+ .build(), (exchange, request) -> {
+ logger.info("Resource 'test://watched-resource' requested");
+ return new ReadResourceResult(List.of(new TextResourceContents("test://watched-resource",
+ "text/plain", "This is a watched resource content.")));
+ }));
+ }
+
+ private static List createResourceTemplateSpecs() {
+ return List.of(
+ // test://template/{id}/data - Resource template with parameter
+ // substitution
+ new McpServerFeatures.SyncResourceTemplateSpecification(ResourceTemplate.builder()
+ .uriTemplate("test://template/{id}/data")
+ .name("Template Resource")
+ .description("A resource template for testing parameter substitution")
+ .mimeType("application/json")
+ .build(), (exchange, request) -> {
+ logger.info("Resource template 'test://template/{{id}}/data' requested for URI: {}",
+ request.uri());
+ // Extract id from URI
+ String uri = request.uri();
+ String id = uri.replaceAll("test://template/(.+)/data", "$1");
+ String jsonContent = String
+ .format("{\"id\":\"%s\",\"templateTest\":true,\"data\":\"Data for ID: %s\"}", id, id);
+ return new ReadResourceResult(
+ List.of(new TextResourceContents(uri, "application/json", jsonContent)));
+ }));
+ }
+
+ private static List createCompletionSpecs() {
+ return List.of(
+ // Completion for test_prompt_with_arguments
+ new McpServerFeatures.SyncCompletionSpecification(new PromptReference("test_prompt_with_arguments"),
+ (exchange, request) -> {
+ logger.info("Completion requested for prompt 'test_prompt_with_arguments', argument: {}",
+ request.argument().name());
+ // Return minimal completion with required fields
+ return new CompleteResult(new CompleteResult.CompleteCompletion(List.of(), 0, false));
+ }));
+ }
+
+}
diff --git a/conformance-tests/server-servlet/src/main/resources/logback.xml b/conformance-tests/server-servlet/src/main/resources/logback.xml
new file mode 100644
index 000000000..af69ac902
--- /dev/null
+++ b/conformance-tests/server-servlet/src/main/resources/logback.xml
@@ -0,0 +1,14 @@
+
+
+
+
+ %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n
+
+
+
+
+
+
+
+
+
diff --git a/docs/blog/.authors.yml b/docs/blog/.authors.yml
new file mode 100644
index 000000000..7b255c403
--- /dev/null
+++ b/docs/blog/.authors.yml
@@ -0,0 +1,5 @@
+authors:
+ mcp-team:
+ name: MCP Java SDK Team
+ description: Maintainers of the MCP Java SDK
+ avatar: https://github.com/modelcontextprotocol.png
diff --git a/docs/blog/index.md b/docs/blog/index.md
new file mode 100644
index 000000000..e61459078
--- /dev/null
+++ b/docs/blog/index.md
@@ -0,0 +1 @@
+# News
diff --git a/docs/blog/posts/mcp-server-performance-benchmark.md b/docs/blog/posts/mcp-server-performance-benchmark.md
new file mode 100644
index 000000000..a08b807b6
--- /dev/null
+++ b/docs/blog/posts/mcp-server-performance-benchmark.md
@@ -0,0 +1,72 @@
+---
+date: 2026-02-15
+authors:
+ - mcp-team
+categories:
+ - Performance
+ - Benchmarks
+---
+
+# Java Leads MCP Server Performance Benchmarks with Sub-Millisecond Latency
+
+A comprehensive independent benchmark of MCP server implementations across four major languages puts Java at the top of the performance charts — delivering sub-millisecond latency, the highest throughput, and the best CPU efficiency of all tested platforms.
+
+
+
+## The Benchmark
+
+[TM Dev Lab](https://www.tmdevlab.com/mcp-server-performance-benchmark.html) published a rigorous performance comparison of MCP server implementations spanning **3.9 million total requests** across three independent test rounds. The benchmark evaluated four implementations under identical conditions:
+
+- **Java** — Spring Boot 4.0.0 + Spring AI 2.0.0-M2 on Java 21
+- **Go** — Official MCP SDK v1.2.0
+- **Node.js** — @modelcontextprotocol/sdk v1.26.0
+- **Python** — FastMCP 2.12.0+ with FastAPI 0.109.0+
+
+Each server was tested with 50 concurrent virtual users over 5-minute sustained runs in Docker containers (1-core CPU, 1GB memory) on Ubuntu 24.04.3 LTS. Four standardized benchmark tools measured CPU-intensive, I/O-intensive, data transformation, and latency-handling scenarios — all with a **0% error rate** across every implementation.
+
+## Java's Performance Highlights
+
+The results speak for themselves:
+
+| Server | Avg Latency | Throughput (RPS) | CPU Efficiency (RPS/CPU%) |
+|------------|-------------|------------------|---------------------------|
+| **Java** | **0.835 ms** | **1,624** | **57.2** |
+| Go | 0.855 ms | 1,624 | 50.4 |
+| Node.js | 10.66 ms | 559 | 5.7 |
+| Python | 26.45 ms | 292 | 3.2 |
+
+```mermaid
+---
+config:
+ xyChart:
+ width: 700
+ height: 400
+ themeVariables:
+ xyChart:
+ backgroundColor: transparent
+---
+xychart-beta
+ title "Average Latency Comparison (milliseconds)"
+ x-axis [Java, Go, "Node.js", Python]
+ y-axis "Latency (ms)" 0 --> 30
+ bar [0.84, 0.86, 10.66, 26.45]
+```
+
+Java achieved the **lowest average latency** at 0.835 ms — edging out Go's 0.855 ms — while matching its throughput at 1,624 requests per second. Where Java truly stands out is **CPU efficiency**: at 57.2 RPS per CPU%, it extracts more performance per compute cycle than any other implementation, including Go (50.4).
+
+In CPU-bound workloads like Fibonacci calculation, Java excelled with a **0.369 ms** response time, showcasing the JVM's highly optimized just-in-time compilation.
+
+## A Clear Performance Tier
+
+The benchmark reveals two distinct performance tiers:
+
+- **High-performance tier**: Java and Go deliver sub-millisecond latencies and 1,600+ RPS
+- **Standard tier**: Node.js (12x slower) and Python (31x slower) trail significantly
+
+Java's throughput is **2.9x higher than Node.js** and **5.6x higher than Python**. For latency-sensitive MCP deployments, the difference is even more pronounced — Java responds **12.8x faster than Node.js** and **31.7x faster than Python**.
+
+## What This Means for MCP Developers
+
+For teams building production MCP servers that need to handle high concurrency and low-latency tool interactions, Java with Spring Boot and Spring AI provides a battle-tested, high-performance foundation. The JVM's mature ecosystem, strong typing, and proven scalability make it an excellent choice for enterprise MCP deployments where performance and reliability are paramount.
+
+The full benchmark details, methodology, and raw data are available at [TM Dev Lab](https://www.tmdevlab.com/mcp-server-performance-benchmark.html).
diff --git a/docs/client.md b/docs/client.md
new file mode 100644
index 000000000..6a99928c5
--- /dev/null
+++ b/docs/client.md
@@ -0,0 +1,439 @@
+---
+title: MCP Client
+description: Learn how to use the Model Context Protocol (MCP) client to interact with MCP servers
+---
+
+# MCP Client
+
+The MCP Client is a key component in the Model Context Protocol (MCP) architecture, responsible for establishing and managing connections with MCP servers. It implements the client-side of the protocol, handling:
+
+- Protocol version negotiation to ensure compatibility with servers
+- Capability negotiation to determine available features
+- Message transport and JSON-RPC communication
+- Tool discovery and execution with optional schema validation
+- Resource access and management
+- Prompt system interactions
+- Optional features like roots management, sampling, and elicitation support
+- Progress tracking for long-running operations
+
+!!! tip
+ The core `io.modelcontextprotocol.sdk:mcp` module provides STDIO, SSE, and Streamable HTTP client transport implementations without requiring external web frameworks.
+
+ The Spring-specific WebFlux transport (`mcp-spring-webflux`) is now part of [Spring AI](https://docs.spring.io/spring-ai/reference/2.0-SNAPSHOT/api/mcp/mcp-overview.html) 2.0+ (group `org.springframework.ai`) and is no longer shipped by this SDK.
+ See the [MCP Client Boot Starter](https://docs.spring.io/spring-ai/reference/2.0-SNAPSHOT/api/mcp/mcp-client-boot-starter-docs.html) documentation for Spring-based client setup.
+
+The client provides both synchronous and asynchronous APIs for flexibility in different application contexts.
+
+=== "Sync API"
+
+ ```java
+ // Create a sync client with custom configuration
+ McpSyncClient client = McpClient.sync(transport)
+ .requestTimeout(Duration.ofSeconds(10))
+ .capabilities(ClientCapabilities.builder()
+ .roots(true) // Enable roots capability
+ .sampling() // Enable sampling capability
+ .elicitation() // Enable elicitation capability
+ .build())
+ .sampling(request -> new CreateMessageResult(response))
+ .elicitation(request -> new ElicitResult(ElicitResult.Action.ACCEPT, content))
+ .build();
+
+ // Initialize connection
+ client.initialize();
+
+ // List available tools
+ ListToolsResult tools = client.listTools();
+
+ // Call a tool
+ CallToolResult result = client.callTool(
+ new CallToolRequest("calculator",
+ Map.of("operation", "add", "a", 2, "b", 3))
+ );
+
+ // List and read resources
+ ListResourcesResult resources = client.listResources();
+ ReadResourceResult resource = client.readResource(
+ new ReadResourceRequest("resource://uri")
+ );
+
+ // List and use prompts
+ ListPromptsResult prompts = client.listPrompts();
+ GetPromptResult prompt = client.getPrompt(
+ new GetPromptRequest("greeting", Map.of("name", "Spring"))
+ );
+
+ // Add/remove roots
+ client.addRoot(new Root("file:///path", "description"));
+ client.removeRoot("file:///path");
+
+ // Close client
+ client.closeGracefully();
+ ```
+
+=== "Async API"
+
+ ```java
+ // Create an async client with custom configuration
+ McpAsyncClient client = McpClient.async(transport)
+ .requestTimeout(Duration.ofSeconds(10))
+ .capabilities(ClientCapabilities.builder()
+ .roots(true) // Enable roots capability
+ .sampling() // Enable sampling capability
+ .elicitation() // Enable elicitation capability
+ .build())
+ .sampling(request -> Mono.just(new CreateMessageResult(response)))
+ .elicitation(request -> Mono.just(new ElicitResult(ElicitResult.Action.ACCEPT, content)))
+ .toolsChangeConsumer(tools -> Mono.fromRunnable(() -> {
+ logger.info("Tools updated: {}", tools);
+ }))
+ .resourcesChangeConsumer(resources -> Mono.fromRunnable(() -> {
+ logger.info("Resources updated: {}", resources);
+ }))
+ .promptsChangeConsumer(prompts -> Mono.fromRunnable(() -> {
+ logger.info("Prompts updated: {}", prompts);
+ }))
+ .progressConsumer(progress -> Mono.fromRunnable(() -> {
+ logger.info("Progress: {}", progress);
+ }))
+ .build();
+
+ // Initialize connection and use features
+ client.initialize()
+ .flatMap(initResult -> client.listTools())
+ .flatMap(tools -> {
+ return client.callTool(new CallToolRequest(
+ "calculator",
+ Map.of("operation", "add", "a", 2, "b", 3)
+ ));
+ })
+ .flatMap(result -> {
+ return client.listResources()
+ .flatMap(resources ->
+ client.readResource(new ReadResourceRequest("resource://uri"))
+ );
+ })
+ .flatMap(resource -> {
+ return client.listPrompts()
+ .flatMap(prompts ->
+ client.getPrompt(new GetPromptRequest(
+ "greeting",
+ Map.of("name", "Spring")
+ ))
+ );
+ })
+ .flatMap(prompt -> {
+ return client.addRoot(new Root("file:///path", "description"))
+ .then(client.removeRoot("file:///path"));
+ })
+ .doFinally(signalType -> {
+ client.closeGracefully().subscribe();
+ })
+ .subscribe();
+ ```
+
+## Client Transport
+
+The transport layer handles the communication between MCP clients and servers, providing different implementations for various use cases. The client transport manages message serialization, connection establishment, and protocol-specific communication patterns.
+
+### STDIO
+
+Creates transport for process-based communication using stdin/stdout:
+
+```java
+ServerParameters params = ServerParameters.builder("npx")
+ .args("-y", "@modelcontextprotocol/server-everything", "dir")
+ .build();
+McpTransport transport = new StdioClientTransport(params);
+```
+
+### Streamable HTTP
+
+=== "Streamable HttpClient"
+
+ Creates a Streamable HTTP client transport for efficient bidirectional communication. Included in the core `mcp` module:
+
+ ```java
+ McpTransport transport = HttpClientStreamableHttpTransport
+ .builder("http://your-mcp-server")
+ .endpoint("/mcp")
+ .build();
+ ```
+
+ The Streamable HTTP transport supports:
+
+ - Resumable streams for connection recovery
+ - Configurable connect timeout
+ - Custom HTTP request customization
+ - Multiple protocol version negotiation
+
+=== "Streamable WebClient (external)"
+
+ Creates Streamable HTTP WebClient-based client transport. Requires the `mcp-spring-webflux` dependency from [Spring AI](https://docs.spring.io/spring-ai/reference/2.0-SNAPSHOT/api/mcp/mcp-overview.html) 2.0+ (group `org.springframework.ai`):
+
+ ```java
+ McpTransport transport = WebFluxSseClientTransport
+ .builder(WebClient.builder().baseUrl("http://your-mcp-server"))
+ .build();
+ ```
+
+### SSE HTTP (Legacy)
+
+=== "SSE HttpClient"
+
+ Creates a framework-agnostic (pure Java API) SSE client transport. Included in the core `mcp` module:
+
+ ```java
+ McpTransport transport = new HttpClientSseClientTransport("http://your-mcp-server");
+ ```
+=== "SSE WebClient (external)"
+
+ Creates WebFlux-based SSE client transport. Requires the `mcp-spring-webflux` dependency from [Spring AI](https://docs.spring.io/spring-ai/reference/2.0-SNAPSHOT/api/mcp/mcp-overview.html) 2.0+ (group `org.springframework.ai`):
+
+ ```java
+ WebClient.Builder webClientBuilder = WebClient.builder()
+ .baseUrl("http://your-mcp-server");
+ McpTransport transport = new WebFluxSseClientTransport(webClientBuilder);
+ ```
+
+
+## Client Capabilities
+
+The client can be configured with various capabilities:
+
+```java
+var capabilities = ClientCapabilities.builder()
+ .roots(true) // Enable filesystem roots support with list changes notifications
+ .sampling() // Enable LLM sampling support
+ .elicitation() // Enable elicitation support (form and URL modes)
+ .build();
+```
+
+You can also configure elicitation with specific mode support:
+
+```java
+var capabilities = ClientCapabilities.builder()
+ .elicitation(true, false) // Enable form-based elicitation, disable URL-based
+ .build();
+```
+
+### Roots Support
+
+Roots define the boundaries of where servers can operate within the filesystem:
+
+```java
+// Add a root dynamically
+client.addRoot(new Root("file:///path", "description"));
+
+// Remove a root
+client.removeRoot("file:///path");
+
+// Notify server of roots changes
+client.rootsListChangedNotification();
+```
+
+The roots capability allows servers to:
+
+- Request the list of accessible filesystem roots
+- Receive notifications when the roots list changes
+- Understand which directories and files they have access to
+
+### Sampling Support
+
+Sampling enables servers to request LLM interactions ("completions" or "generations") through the client:
+
+```java
+// Configure sampling handler
+Function samplingHandler = request -> {
+ // Sampling implementation that interfaces with LLM
+ return new CreateMessageResult(response);
+};
+
+// Create client with sampling support
+var client = McpClient.sync(transport)
+ .capabilities(ClientCapabilities.builder()
+ .sampling()
+ .build())
+ .sampling(samplingHandler)
+ .build();
+```
+
+This capability allows:
+
+- Servers to leverage AI capabilities without requiring API keys
+- Clients to maintain control over model access and permissions
+- Support for both text and image-based interactions
+- Optional inclusion of MCP server context in prompts
+
+### Elicitation Support
+
+Elicitation enables servers to request additional information or user input through the client. This is useful when a server needs clarification or confirmation during an operation:
+
+```java
+// Configure elicitation handler
+Function elicitationHandler = request -> {
+ // Present the request to the user and collect their response
+ // The request contains a message and a schema describing the expected input
+ Map userResponse = collectUserInput(request.message(), request.requestedSchema());
+ return new ElicitResult(ElicitResult.Action.ACCEPT, userResponse);
+};
+
+// Create client with elicitation support
+var client = McpClient.sync(transport)
+ .capabilities(ClientCapabilities.builder()
+ .elicitation()
+ .build())
+ .elicitation(elicitationHandler)
+ .build();
+```
+
+The `ElicitResult` supports three actions:
+
+- `ACCEPT` - The user accepted and provided the requested information
+- `DECLINE` - The user declined to provide the information
+- `CANCEL` - The operation was cancelled
+
+### Logging Support
+
+The client can register a logging consumer to receive log messages from the server and set the minimum logging level to filter messages:
+
+```java
+var mcpClient = McpClient.sync(transport)
+ .loggingConsumer(notification -> {
+ System.out.println("Received log message: " + notification.data());
+ })
+ .build();
+
+mcpClient.initialize();
+
+mcpClient.setLoggingLevel(McpSchema.LoggingLevel.INFO);
+
+// Call the tool that sends logging notifications
+CallToolResult result = mcpClient.callTool(new CallToolRequest("logging-test", Map.of()));
+```
+
+Clients can control the minimum logging level they receive through the `mcpClient.setLoggingLevel(level)` request. Messages below the set level will be filtered out.
+Supported logging levels (in order of increasing severity): DEBUG (0), INFO (1), NOTICE (2), WARNING (3), ERROR (4), CRITICAL (5), ALERT (6), EMERGENCY (7)
+
+### Progress Notifications
+
+The client can register a progress consumer to track the progress of long-running operations:
+
+```java
+var mcpClient = McpClient.sync(transport)
+ .progressConsumer(progress -> {
+ System.out.println("Progress: " + progress.progress() + "/" + progress.total());
+ })
+ .build();
+```
+
+## Using MCP Clients
+
+### Tool Execution
+
+Tools are server-side functions that clients can discover and execute. The MCP client provides methods to list available tools and execute them with specific parameters. Each tool has a unique name and accepts a map of parameters.
+
+=== "Sync API"
+
+ ```java
+ // List available tools
+ ListToolsResult tools = client.listTools();
+
+ // Call a tool with a CallToolRequest
+ CallToolResult result = client.callTool(
+ new CallToolRequest("calculator", Map.of(
+ "operation", "add",
+ "a", 1,
+ "b", 2
+ ))
+ );
+ ```
+
+=== "Async API"
+
+ ```java
+ // List available tools asynchronously
+ client.listTools()
+ .doOnNext(tools -> tools.tools().forEach(tool ->
+ System.out.println(tool.name())))
+ .subscribe();
+
+ // Call a tool asynchronously
+ client.callTool(new CallToolRequest("calculator", Map.of(
+ "operation", "add",
+ "a", 1,
+ "b", 2
+ )))
+ .subscribe();
+ ```
+
+### Tool Schema Validation and Caching
+
+The client supports optional JSON schema validation for tool call results and automatic schema caching:
+
+```java
+var client = McpClient.sync(transport)
+ .jsonSchemaValidator(myValidator) // Enable schema validation
+ .enableCallToolSchemaCaching(true) // Cache tool schemas
+ .build();
+```
+
+### Resource Access
+
+Resources represent server-side data sources that clients can access using URI templates. The MCP client provides methods to discover available resources and retrieve their contents through a standardized interface.
+
+=== "Sync API"
+
+ ```java
+ // List available resources
+ ListResourcesResult resources = client.listResources();
+
+ // Read a resource
+ ReadResourceResult resource = client.readResource(
+ new ReadResourceRequest("resource://uri")
+ );
+ ```
+
+=== "Async API"
+
+ ```java
+ // List available resources asynchronously
+ client.listResources()
+ .doOnNext(resources -> resources.resources().forEach(resource ->
+ System.out.println(resource.name())))
+ .subscribe();
+
+ // Read a resource asynchronously
+ client.readResource(new ReadResourceRequest("resource://uri"))
+ .subscribe();
+ ```
+
+### Prompt System
+
+The prompt system enables interaction with server-side prompt templates. These templates can be discovered and executed with custom parameters, allowing for dynamic text generation based on predefined patterns.
+
+=== "Sync API"
+
+ ```java
+ // List available prompt templates
+ ListPromptsResult prompts = client.listPrompts();
+
+ // Get a prompt with parameters
+ GetPromptResult prompt = client.getPrompt(
+ new GetPromptRequest("greeting", Map.of("name", "World"))
+ );
+ ```
+
+=== "Async API"
+
+ ```java
+ // List available prompt templates asynchronously
+ client.listPrompts()
+ .doOnNext(prompts -> prompts.prompts().forEach(prompt ->
+ System.out.println(prompt.name())))
+ .subscribe();
+
+ // Get a prompt asynchronously
+ client.getPrompt(new GetPromptRequest("greeting", Map.of("name", "World")))
+ .subscribe();
+ ```
diff --git a/docs/contribute.md b/docs/contribute.md
new file mode 100644
index 000000000..3199dd51f
--- /dev/null
+++ b/docs/contribute.md
@@ -0,0 +1,106 @@
+---
+title: Contributing
+description: How to contribute to the MCP Java SDK
+---
+
+# Contributing
+
+Thank you for your interest in contributing to the Model Context Protocol Java SDK!
+This guide outlines how to contribute to this project.
+
+## Prerequisites
+
+!!! info "Required Software"
+ - **Java 17** or above
+ - **Docker**
+ - **npx**
+
+## Getting Started
+
+1. Fork the repository
+2. Clone your fork:
+
+ ```bash
+ git clone https://github.com/YOUR-USERNAME/java-sdk.git
+ cd java-sdk
+ ```
+
+3. Build from source:
+
+ ```bash
+ ./mvnw clean install -DskipTests # skip the tests
+ ./mvnw test # run tests
+ ```
+
+## Reporting Issues
+
+Please create an issue in the repository if you discover a bug or would like to
+propose an enhancement. Bug reports should have a reproducer in the form of a code
+sample or a repository attached that the maintainers or contributors can work with to
+address the problem.
+
+## Making Changes
+
+1. Create a new branch:
+
+ ```bash
+ git checkout -b feature/your-feature-name
+ ```
+
+2. Make your changes.
+
+3. Validate your changes:
+
+ ```bash
+ ./mvnw clean test
+ ```
+
+### Change Proposal Guidelines
+
+#### Principles of MCP
+
+1. **Simple + Minimal**: It is much easier to add things to the codebase than it is to
+ remove them. To maintain simplicity, we keep a high bar for adding new concepts and
+ primitives as each addition requires maintenance and compatibility consideration.
+2. **Concrete**: Code changes need to be based on specific usage and implementation
+ challenges and not on speculative ideas. Most importantly, the SDK is meant to
+ implement the MCP specification.
+
+## Submitting Changes
+
+1. For non-trivial changes, please clarify with the maintainers in an issue whether
+ you can contribute the change and the desired scope of the change.
+2. For trivial changes (for example a couple of lines or documentation changes) there
+ is no need to open an issue first.
+3. Push your changes to your fork.
+4. Submit a pull request to the main repository.
+5. Follow the pull request template.
+6. Wait for review.
+7. For any follow-up work, please add new commits instead of force-pushing. This will
+ allow the reviewer to focus on incremental changes instead of having to restart the
+ review process.
+
+## Code of Conduct
+
+This project follows a Code of Conduct. Please review it in
+[CODE_OF_CONDUCT.md](https://github.com/modelcontextprotocol/java-sdk/blob/main/CODE_OF_CONDUCT.md).
+
+## Questions
+
+If you have questions, please create a discussion in the repository.
+
+## License
+
+By contributing, you agree that your contributions will be licensed under the MIT
+License.
+
+## Security
+
+This SDK is maintained by [Anthropic](https://www.anthropic.com/) as part of the Model Context Protocol project.
+
+The security of our systems and user data is Anthropic's top priority. We appreciate the work of security researchers acting in good faith in identifying and reporting potential vulnerabilities.
+
+!!! warning "Reporting Security Vulnerabilities"
+ Do **not** report security vulnerabilities through public GitHub issues. Instead, report them through our HackerOne [submission form](https://hackerone.com/anthropic-vdp/reports/new?type=team&report_type=vulnerability).
+
+Our Vulnerability Disclosure Program guidelines are defined on our [HackerOne program page](https://hackerone.com/anthropic-vdp).
diff --git a/docs/development.md b/docs/development.md
new file mode 100644
index 000000000..e00c7268b
--- /dev/null
+++ b/docs/development.md
@@ -0,0 +1,75 @@
+---
+title: Documentation
+description: How to contribute to the MCP Java SDK documentation
+---
+
+# Documentation Development
+
+This guide covers how to set up and preview the MCP Java SDK documentation locally.
+
+!!! info "Prerequisites"
+ - Python 3.x
+ - pip (Python package manager)
+
+## Setup
+
+Install mkdocs-material:
+
+```bash
+pip install mkdocs-material
+```
+
+## Preview Locally
+
+From the project root directory, run:
+
+```bash
+mkdocs serve
+```
+
+A local preview of the documentation will be available at `http://localhost:8000`.
+
+### Custom Ports
+
+By default, mkdocs uses port 8000. You can customize the port with the `-a` flag:
+
+```bash
+mkdocs serve -a localhost:3333
+```
+
+## Building
+
+To build the static site for deployment:
+
+```bash
+mkdocs build
+```
+
+The built site will be output to the `site/` directory.
+
+## Project Structure
+
+```
+docs/
+├── index.md # Overview page
+├── quickstart.md # Quickstart guide
+├── client.md # MCP Client documentation
+├── server.md # MCP Server documentation
+├── contributing.md # Contributing guide
+├── development.md # This page
+├── images/ # Images and diagrams
+└── stylesheets/ # Custom CSS
+mkdocs.yml # MkDocs configuration
+```
+
+## Writing Guidelines
+
+- Documentation pages use standard Markdown with [mkdocs-material extensions](https://squidfunk.github.io/mkdocs-material/reference/)
+- Use content tabs (`=== "Tab Label"`) for Maven/Gradle or Sync/Async code examples
+- Use admonitions (`!!! tip`, `!!! info`, `!!! warning`) for callouts
+- All code blocks should specify a language for syntax highlighting
+- Images go in the `docs/images/` directory
+
+## IDE Support
+
+We suggest using extensions on your IDE to recognize and format Markdown. If you're a VSCode user, consider the [Markdown All in One](https://marketplace.visualstudio.com/items?itemName=yzhang.markdown-all-in-one) extension for enhanced Markdown support, and [Prettier](https://marketplace.visualstudio.com/items?itemName=esbenp.prettier-vscode) for code formatting.
diff --git a/docs/images/favicon.svg b/docs/images/favicon.svg
new file mode 100644
index 000000000..fe5edb725
--- /dev/null
+++ b/docs/images/favicon.svg
@@ -0,0 +1,69 @@
+
+
diff --git a/docs/images/java-mcp-client-architecture.jpg b/docs/images/java-mcp-client-architecture.jpg
new file mode 100644
index 000000000..688a2b4ad
Binary files /dev/null and b/docs/images/java-mcp-client-architecture.jpg differ
diff --git a/docs/images/java-mcp-server-architecture.jpg b/docs/images/java-mcp-server-architecture.jpg
new file mode 100644
index 000000000..4b05ca139
Binary files /dev/null and b/docs/images/java-mcp-server-architecture.jpg differ
diff --git a/docs/images/java-mcp-uml-classdiagram.svg b/docs/images/java-mcp-uml-classdiagram.svg
new file mode 100644
index 000000000..f83a586e7
--- /dev/null
+++ b/docs/images/java-mcp-uml-classdiagram.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/docs/images/logo-dark.svg b/docs/images/logo-dark.svg
new file mode 100644
index 000000000..03d9f85d3
--- /dev/null
+++ b/docs/images/logo-dark.svg
@@ -0,0 +1,12 @@
+
diff --git a/docs/images/logo-light.svg b/docs/images/logo-light.svg
new file mode 100644
index 000000000..fe5edb725
--- /dev/null
+++ b/docs/images/logo-light.svg
@@ -0,0 +1,69 @@
+
+
diff --git a/docs/images/mcp-stack.svg b/docs/images/mcp-stack.svg
new file mode 100644
index 000000000..3847eaa8d
--- /dev/null
+++ b/docs/images/mcp-stack.svg
@@ -0,0 +1,197 @@
+
+
diff --git a/docs/index.md b/docs/index.md
new file mode 100644
index 000000000..e6062b5ff
--- /dev/null
+++ b/docs/index.md
@@ -0,0 +1,84 @@
+---
+title: Index
+description: Introduction to the Model Context Protocol (MCP) Java SDK
+---
+
+# MCP Java SDK
+
+Java SDK for the [Model Context Protocol](https://modelcontextprotocol.io/docs/concepts/architecture)
+enables standardized integration between AI models and tools.
+
+## Features
+
+- MCP Client and MCP Server implementations supporting:
+ - Protocol [version compatibility negotiation](https://modelcontextprotocol.io/specification/2025-11-25/basic/lifecycle#initialization) with multiple protocol versions
+ - [Tools](https://modelcontextprotocol.io/specification/2025-11-25/server/tools) discovery, execution, list change notifications, and structured output with schema validation
+ - [Resources](https://modelcontextprotocol.io/specification/2025-11-25/server/resources) management with URI templates
+ - [Roots](https://modelcontextprotocol.io/specification/2025-11-25/client/roots) list management and notifications
+ - [Prompts](https://modelcontextprotocol.io/specification/2025-11-25/server/prompts) handling and management
+ - [Sampling](https://modelcontextprotocol.io/specification/2025-11-25/client/sampling) support for AI model interactions
+ - [Elicitation](https://modelcontextprotocol.io/specification/2025-11-25/client/elicitation) support for requesting user input from servers
+ - [Completions](https://modelcontextprotocol.io/specification/2025-11-25/server/utilities/completion) for argument autocompletion suggestions
+ - [Progress](https://modelcontextprotocol.io/specification/2025-11-25/basic/utilities/progress) - progress notifications for tracking long-running operations
+ - [Logging](https://modelcontextprotocol.io/specification/2025-11-25/server/utilities/logging) - structured logging with configurable severity levels
+- Multiple transport implementations:
+ - Default transports (included in core `mcp` module, no external web frameworks required):
+ - [STDIO](https://modelcontextprotocol.io/specification/2025-11-25/basic/transports#stdio)-based transport for process-based communication
+ - Java HttpClient-based SSE client transport for HTTP SSE Client-side streaming
+ - Servlet-based SSE server transport for HTTP SSE Server streaming
+ - [Streamable HTTP](https://modelcontextprotocol.io/specification/2025-11-25/basic/transports#streamable-http) transport for efficient bidirectional communication (client and server)
+ - Optional Spring-based transports (available in [Spring AI](https://docs.spring.io/spring-ai/reference/2.0-SNAPSHOT/api/mcp/mcp-overview.html) 2.0+, no longer part of this SDK):
+ - WebFlux SSE client and server transports for reactive HTTP streaming
+ - WebFlux Streamable HTTP server transport
+ - WebMVC SSE server transport for servlet-based HTTP streaming
+ - WebMVC Streamable HTTP server transport
+ - WebMVC Stateless server transport
+- Supports Synchronous and Asynchronous programming paradigms
+- Pluggable JSON serialization (Jackson 2.x and Jackson 3.x)
+- Pluggable authorization hooks for server security
+- DNS rebinding protection with Host/Origin header validation
+
+!!! tip
+ The core `io.modelcontextprotocol.sdk:mcp` module provides default STDIO, SSE, and Streamable HTTP client and server transport implementations without requiring external web frameworks.
+
+ Spring-specific transports (WebFlux, WebMVC) are now part of [Spring AI](https://docs.spring.io/spring-ai/reference/2.0-SNAPSHOT/api/mcp/mcp-overview.html) 2.0+ and are no longer shipped by this SDK.
+ Use the [MCP Client Boot Starter](https://docs.spring.io/spring-ai/reference/2.0-SNAPSHOT/api/mcp/mcp-client-boot-starter-docs.html) and [MCP Server Boot Starter](https://docs.spring.io/spring-ai/reference/2.0-SNAPSHOT/api/mcp/mcp-server-boot-starter-docs.html) from Spring AI.
+ Also consider the [MCP Annotations](https://docs.spring.io/spring-ai/reference/2.0-SNAPSHOT/api/mcp/mcp-annotations-overview.html) and [MCP Security](https://docs.spring.io/spring-ai/reference/2.0-SNAPSHOT/api/mcp/mcp-security.html).
+
+## Next Steps
+
+
+
+- :rocket:{ .lg .middle } **Quickstart**
+
+ ---
+
+ Get started with dependencies and BOM configuration.
+
+ [:octicons-arrow-right-24: Quickstart](quickstart.md)
+
+- :material-monitor:{ .lg .middle } **MCP Client**
+
+ ---
+
+ Learn how to create and configure MCP clients.
+
+ [:octicons-arrow-right-24: Client](client.md)
+
+- :material-server:{ .lg .middle } **MCP Server**
+
+ ---
+
+ Learn how to implement and configure MCP servers.
+
+ [:octicons-arrow-right-24: Server](server.md)
+
+- :fontawesome-brands-github:{ .lg .middle } **GitHub**
+
+ ---
+
+ View the source code and contribute.
+
+ [:octicons-arrow-right-24: Repository](https://github.com/modelcontextprotocol/java-sdk)
+
+
diff --git a/docs/overview.md b/docs/overview.md
new file mode 100644
index 000000000..9084b6a6a
--- /dev/null
+++ b/docs/overview.md
@@ -0,0 +1,93 @@
+---
+title: Overview
+description: Introduction to the Model Context Protocol (MCP) Java SDK
+---
+
+# Overview
+
+## Architecture
+
+The SDK follows a layered architecture with clear separation of concerns:
+
+
+
+- **Client/Server Layer (McpClient/McpServer)**: Both use McpSession for sync/async operations,
+ with McpClient handling client-side protocol operations and McpServer managing server-side protocol operations.
+- **Session Layer (McpSession)**: Manages communication patterns and state.
+- **Transport Layer (McpTransport)**: Handles JSON-RPC message serialization/deserialization via:
+ - StdioTransport (stdin/stdout) in the core module
+ - HTTP SSE transports in dedicated transport modules (Java HttpClient, Servlet)
+ - Streamable HTTP transports for efficient bidirectional communication
+ - Spring WebFlux and Spring WebMVC transports (available in [Spring AI](https://docs.spring.io/spring-ai/reference/2.0-SNAPSHOT/api/mcp/mcp-overview.html) 2.0+)
+
+The MCP Client is a key component in the Model Context Protocol (MCP) architecture, responsible for establishing and managing connections with MCP servers.
+It implements the client-side of the protocol.
+
+
+
+The MCP Server is a foundational component in the Model Context Protocol (MCP) architecture that provides tools, resources, and capabilities to clients.
+It implements the server-side of the protocol.
+
+
+
+Key Interactions:
+
+- **Client/Server Initialization**: Transport setup, protocol compatibility check, capability negotiation, and implementation details exchange.
+- **Message Flow**: JSON-RPC message handling with validation, type-safe response processing, and error handling.
+- **Resource Management**: Resource discovery, URI template-based access, subscription system, and content retrieval.
+
+## Module Structure
+
+The SDK is organized into modules to separate concerns and allow adopters to bring in only what they need:
+
+| Module | Artifact ID | Group | Purpose |
+|--------|------------|-------|---------|
+| `mcp-bom` | `mcp-bom` | `io.modelcontextprotocol.sdk` | Bill of Materials for dependency management |
+| `mcp-core` | `mcp-core` | `io.modelcontextprotocol.sdk` | Core reference implementation (STDIO, JDK HttpClient, Servlet, Streamable HTTP) |
+| `mcp-json-jackson2` | `mcp-json-jackson2` | `io.modelcontextprotocol.sdk` | Jackson 2.x JSON serialization implementation |
+| `mcp-json-jackson3` | `mcp-json-jackson3` | `io.modelcontextprotocol.sdk` | Jackson 3.x JSON serialization implementation |
+| `mcp` | `mcp` | `io.modelcontextprotocol.sdk` | Convenience bundle (`mcp-core` + `mcp-json-jackson3`) |
+| `mcp-test` | `mcp-test` | `io.modelcontextprotocol.sdk` | Shared testing utilities and integration tests |
+| `mcp-spring-webflux` _(external)_ | `mcp-spring-webflux` | `org.springframework.ai` | Spring WebFlux integration — part of [Spring AI](https://docs.spring.io/spring-ai/reference/2.0-SNAPSHOT/api/mcp/mcp-overview.html) 2.0+ |
+| `mcp-spring-webmvc` _(external)_ | `mcp-spring-webmvc` | `org.springframework.ai` | Spring WebMVC integration — part of [Spring AI](https://docs.spring.io/spring-ai/reference/2.0-SNAPSHOT/api/mcp/mcp-overview.html) 2.0+ |
+
+!!! tip
+ A minimal adopter may depend only on `mcp` (core + Jackson 3). Spring-based applications should use the `mcp-spring-webflux` or `mcp-spring-webmvc` artifacts from [Spring AI](https://docs.spring.io/spring-ai/reference/2.0-SNAPSHOT/api/mcp/mcp-overview.html) 2.0+ (group `org.springframework.ai`), no longer part of this SDK.
+
+## Next Steps
+
+
+
+- :rocket:{ .lg .middle } **Quickstart**
+
+ ---
+
+ Get started with dependencies and BOM configuration.
+
+ [:octicons-arrow-right-24: Quickstart](quickstart.md)
+
+- :material-monitor:{ .lg .middle } **MCP Client**
+
+ ---
+
+ Learn how to create and configure MCP clients.
+
+ [:octicons-arrow-right-24: Client](client.md)
+
+- :material-server:{ .lg .middle } **MCP Server**
+
+ ---
+
+ Learn how to implement and configure MCP servers.
+
+ [:octicons-arrow-right-24: Server](server.md)
+
+- :fontawesome-brands-github:{ .lg .middle } **GitHub**
+
+ ---
+
+ View the source code and contribute.
+
+ [:octicons-arrow-right-24: Repository](https://github.com/modelcontextprotocol/java-sdk)
+
+
diff --git a/docs/quickstart.md b/docs/quickstart.md
new file mode 100644
index 000000000..e7e76bc88
--- /dev/null
+++ b/docs/quickstart.md
@@ -0,0 +1,163 @@
+---
+title: Quickstart
+description: Get started with the MCP Java SDK dependencies and configuration
+---
+
+# Quickstart
+
+## Dependencies
+
+Add the following dependency to your project:
+
+=== "Maven"
+
+ The convenience `mcp` module bundles `mcp-core` with Jackson 3.x JSON serialization:
+
+ ```xml
+
+ io.modelcontextprotocol.sdk
+ mcp
+
+ ```
+
+ This includes default STDIO, SSE, and Streamable HTTP transport implementations without requiring external web frameworks.
+
+ If you need only the core module without a JSON implementation (e.g., to bring your own):
+
+ ```xml
+
+ io.modelcontextprotocol.sdk
+ mcp-core
+
+ ```
+
+ For Jackson 2.x instead of Jackson 3.x:
+
+ ```xml
+
+ io.modelcontextprotocol.sdk
+ mcp-core
+
+
+ io.modelcontextprotocol.sdk
+ mcp-json-jackson2
+
+ ```
+
+ If you're using Spring Framework, the Spring-specific transport implementations are now part of [Spring AI](https://docs.spring.io/spring-ai/reference/2.0-SNAPSHOT/api/mcp/mcp-overview.html) 2.0+ (group `org.springframework.ai`):
+
+ ```xml
+
+
+ org.springframework.ai
+ mcp-spring-webflux
+
+
+
+
+ org.springframework.ai
+ mcp-spring-webmvc
+
+ ```
+
+ !!! note
+ When using the `spring-ai-bom` or Spring AI starter dependencies (`spring-ai-starter-mcp-server-webflux`, `spring-ai-starter-mcp-server-webmvc`, `spring-ai-starter-mcp-client-webflux`) no explicit version is needed — the BOM manages it automatically.
+
+=== "Gradle"
+
+ The convenience `mcp` module bundles `mcp-core` with Jackson 3.x JSON serialization:
+
+ ```groovy
+ dependencies {
+ implementation "io.modelcontextprotocol.sdk:mcp"
+ }
+ ```
+
+ This includes default STDIO, SSE, and Streamable HTTP transport implementations without requiring external web frameworks.
+
+ If you need only the core module without a JSON implementation (e.g., to bring your own):
+
+ ```groovy
+ dependencies {
+ implementation "io.modelcontextprotocol.sdk:mcp-core"
+ }
+ ```
+
+ For Jackson 2.x instead of Jackson 3.x:
+
+ ```groovy
+ dependencies {
+ implementation "io.modelcontextprotocol.sdk:mcp-core"
+ implementation "io.modelcontextprotocol.sdk:mcp-json-jackson2"
+ }
+ ```
+
+ If you're using Spring Framework, the Spring-specific transport implementations are now part of [Spring AI](https://docs.spring.io/spring-ai/reference/2.0-SNAPSHOT/api/mcp/mcp-overview.html) 2.0+ (group `org.springframework.ai`):
+
+ ```groovy
+ // Optional: Spring WebFlux-based SSE and Streamable HTTP client and server transport (Spring AI 2.0+)
+ dependencies {
+ implementation "org.springframework.ai:mcp-spring-webflux"
+ }
+
+ // Optional: Spring WebMVC-based SSE and Streamable HTTP server transport (Spring AI 2.0+)
+ dependencies {
+ implementation "org.springframework.ai:mcp-spring-webmvc"
+ }
+ ```
+
+## Bill of Materials (BOM)
+
+The Bill of Materials (BOM) declares the recommended versions of all the dependencies used by a given release.
+Using the BOM from your application's build script avoids the need for you to specify and maintain the dependency versions yourself.
+Instead, the version of the BOM you're using determines the utilized dependency versions.
+It also ensures that you're using supported and tested versions of the dependencies by default, unless you choose to override them.
+
+Add the BOM to your project:
+
+=== "Maven"
+
+ ```xml
+
+
+
+ io.modelcontextprotocol.sdk
+ mcp-bom
+ 1.0.0
+ pom
+ import
+
+
+
+ ```
+
+=== "Gradle"
+
+ ```groovy
+ dependencies {
+ implementation platform("io.modelcontextprotocol.sdk:mcp-bom:1.0.0")
+ //...
+ }
+ ```
+
+ Gradle users can also leverage Gradle (5.0+) native support for declaring dependency constraints using a Maven BOM.
+ This is implemented by adding a 'platform' dependency handler method to the dependencies section of your Gradle build script.
+ As shown in the snippet above this can then be followed by version-less declarations of the dependencies.
+
+Replace the version number with the latest version from [Maven Central](https://central.sonatype.com/artifact/io.modelcontextprotocol.sdk/mcp).
+
+## Available Dependencies
+
+The following dependencies are available and managed by the BOM:
+
+- **Core Dependencies**
+ - `io.modelcontextprotocol.sdk:mcp-core` - Core MCP library providing the base functionality, APIs, and default transport implementations (STDIO, SSE, Streamable HTTP). JSON binding is abstracted for pluggability.
+ - `io.modelcontextprotocol.sdk:mcp` - Convenience bundle that combines `mcp-core` with `mcp-json-jackson3` for out-of-the-box usage.
+- **JSON Serialization**
+ - `io.modelcontextprotocol.sdk:mcp-json-jackson3` - Jackson 3.x JSON serialization implementation (included in `mcp` bundle).
+ - `io.modelcontextprotocol.sdk:mcp-json-jackson2` - Jackson 2.x JSON serialization implementation for projects that require Jackson 2.x compatibility.
+- **Optional Spring Transport Dependencies** (part of [Spring AI](https://docs.spring.io/spring-ai/reference/2.0-SNAPSHOT/api/mcp/mcp-overview.html) 2.0+, group `org.springframework.ai`)
+ - `org.springframework.ai:mcp-spring-webflux` - WebFlux-based SSE and Streamable HTTP transport implementation for reactive applications.
+ - `org.springframework.ai:mcp-spring-webmvc` - WebMVC-based SSE and Streamable HTTP transport implementation for servlet-based applications.
+- **Testing Dependencies**
+ - `io.modelcontextprotocol.sdk:mcp-test` - Testing utilities and support for MCP-based applications.
diff --git a/docs/server.md b/docs/server.md
new file mode 100644
index 000000000..0753726e2
--- /dev/null
+++ b/docs/server.md
@@ -0,0 +1,761 @@
+---
+title: MCP Server
+description: Learn how to implement and configure a Model Context Protocol (MCP) server
+---
+
+# MCP Server
+
+## Overview
+
+The MCP Server is a foundational component in the Model Context Protocol (MCP) architecture that provides tools, resources, and capabilities to clients. It implements the server-side of the protocol, responsible for:
+
+- Exposing tools that clients can discover and execute
+- Managing resources with URI-based access patterns and resource templates
+- Providing prompt templates and handling prompt requests
+- Supporting capability negotiation with clients
+- Providing argument autocompletion suggestions (completions)
+- Implementing server-side protocol operations
+- Managing concurrent client connections
+- Providing structured logging and notifications
+
+!!! tip
+ The core `io.modelcontextprotocol.sdk:mcp` module provides STDIO, SSE, and Streamable HTTP server transport implementations without requiring external web frameworks.
+
+ Spring-specific transport implementations (`mcp-spring-webflux`, `mcp-spring-webmvc`) are now part of [Spring AI](https://docs.spring.io/spring-ai/reference/2.0-SNAPSHOT/api/mcp/mcp-overview.html) 2.0+ (group `org.springframework.ai`) and are no longer shipped by this SDK.
+ See the [MCP Server Boot Starter](https://docs.spring.io/spring-ai/reference/2.0-SNAPSHOT/api/mcp/mcp-server-boot-starter-docs.html) documentation for Spring-based server setup.
+
+The server supports both synchronous and asynchronous APIs, allowing for flexible integration in different application contexts.
+
+=== "Sync API"
+
+ ```java
+ // Create a server with custom configuration
+ McpSyncServer syncServer = McpServer.sync(transportProvider)
+ .serverInfo("my-server", "1.0.0")
+ .capabilities(ServerCapabilities.builder()
+ .resources(false, true) // Enable resource support with list changes
+ .tools(true) // Enable tool support with list changes
+ .prompts(true) // Enable prompt support with list changes
+ .completions() // Enable completions support
+ .logging() // Enable logging support
+ .build())
+ .build();
+
+ // Register tools, resources, and prompts
+ syncServer.addTool(syncToolSpecification);
+ syncServer.addResource(syncResourceSpecification);
+ syncServer.addPrompt(syncPromptSpecification);
+
+ // Close the server when done
+ syncServer.close();
+ ```
+
+=== "Async API"
+
+ ```java
+ // Create an async server with custom configuration
+ McpAsyncServer asyncServer = McpServer.async(transportProvider)
+ .serverInfo("my-server", "1.0.0")
+ .capabilities(ServerCapabilities.builder()
+ .resources(false, true) // Enable resource support with list changes
+ .tools(true) // Enable tool support with list changes
+ .prompts(true) // Enable prompt support with list changes
+ .completions() // Enable completions support
+ .logging() // Enable logging support
+ .build())
+ .build();
+
+ // Register tools, resources, and prompts
+ asyncServer.addTool(asyncToolSpecification)
+ .doOnSuccess(v -> logger.info("Tool registered"))
+ .subscribe();
+
+ asyncServer.addResource(asyncResourceSpecification)
+ .doOnSuccess(v -> logger.info("Resource registered"))
+ .subscribe();
+
+ asyncServer.addPrompt(asyncPromptSpecification)
+ .doOnSuccess(v -> logger.info("Prompt registered"))
+ .subscribe();
+
+ // Close the server when done
+ asyncServer.close()
+ .doOnSuccess(v -> logger.info("Server closed"))
+ .subscribe();
+ ```
+
+### Server Types
+
+The SDK supports multiple server creation patterns depending on your transport requirements:
+
+```java
+// Single-session server with SSE transport provider
+McpSyncServer server = McpServer.sync(sseTransportProvider).build();
+
+// Streamable HTTP server
+McpSyncServer server = McpServer.sync(streamableTransportProvider).build();
+
+// Stateless server (no session management)
+McpSyncServer server = McpServer.sync(statelessTransport).build();
+```
+
+## Server Transport Providers
+
+The transport layer in the MCP SDK is responsible for handling the communication between clients and servers.
+It provides different implementations to support various communication protocols and patterns.
+The SDK includes several built-in transport provider implementations:
+
+### STDIO
+
+Create process-based transport using stdin/stdout:
+
+```java
+StdioServerTransportProvider transportProvider =
+ new StdioServerTransportProvider(new ObjectMapper());
+```
+
+Provides bidirectional JSON-RPC message handling over standard input/output streams with non-blocking message processing, serialization/deserialization, and graceful shutdown support.
+
+Key features:
+
+- Bidirectional communication through stdin/stdout
+- Process-based integration support
+- Simple setup and configuration
+- Lightweight implementation
+
+### Streamable HTTP
+
+=== "Streamable HTTP Servlet"
+
+ Creates a Servlet-based Streamable HTTP server transport. Included in the core `mcp` module:
+
+ ```java
+ HttpServletStreamableServerTransportProvider transportProvider =
+ HttpServletStreamableServerTransportProvider.builder()
+ .jsonMapper(jsonMapper)
+ .mcpEndpoint("/mcp")
+ .build();
+ ```
+
+ To use with a Spring Web application, register it as a Servlet bean:
+
+ ```java
+ @Configuration
+ @EnableWebMvc
+ public class McpServerConfig implements WebMvcConfigurer {
+
+ @Bean
+ public HttpServletStreamableServerTransportProvider transportProvider(McpJsonMapper jsonMapper) {
+ return HttpServletStreamableServerTransportProvider.builder()
+ .jsonMapper(jsonMapper)
+ .mcpEndpoint("/mcp")
+ .build();
+ }
+
+ @Bean
+ public ServletRegistrationBean> mcpServlet(
+ HttpServletStreamableServerTransportProvider transportProvider) {
+ return new ServletRegistrationBean<>(transportProvider);
+ }
+ }
+ ```
+
+ Key features:
+
+ - Efficient bidirectional HTTP communication
+ - Session management for multiple client connections
+ - Configurable keep-alive intervals
+ - Security validation support
+ - Graceful shutdown support
+
+=== "Streamable HTTP WebFlux (external)"
+
+ Creates WebFlux-based Streamable HTTP server transport. Requires the `mcp-spring-webflux` dependency from [Spring AI](https://docs.spring.io/spring-ai/reference/2.0-SNAPSHOT/api/mcp/mcp-overview.html) 2.0+ (group `org.springframework.ai`):
+
+ ```java
+ @Configuration
+ class McpConfig {
+ @Bean
+ WebFluxStreamableServerTransportProvider transportProvider(McpJsonMapper jsonMapper) {
+ return WebFluxStreamableServerTransportProvider.builder()
+ .jsonMapper(jsonMapper)
+ .messageEndpoint("/mcp")
+ .build();
+ }
+
+ @Bean
+ RouterFunction> mcpRouterFunction(
+ WebFluxStreamableServerTransportProvider transportProvider) {
+ return transportProvider.getRouterFunction();
+ }
+ }
+ ```
+
+ Key features:
+
+ - Reactive HTTP streaming with WebFlux
+ - Concurrent client connections
+ - Configurable keep-alive intervals
+ - Security validation support
+
+=== "Streamable HTTP WebMvc (external)"
+
+ Creates WebMvc-based Streamable HTTP server transport. Requires the `mcp-spring-webmvc` dependency from [Spring AI](https://docs.spring.io/spring-ai/reference/2.0-SNAPSHOT/api/mcp/mcp-overview.html) 2.0+ (group `org.springframework.ai`):
+
+ ```java
+ @Configuration
+ @EnableWebMvc
+ class McpConfig {
+ @Bean
+ WebMvcStreamableServerTransportProvider transportProvider(McpJsonMapper jsonMapper) {
+ return WebMvcStreamableServerTransportProvider.builder()
+ .jsonMapper(jsonMapper)
+ .mcpEndpoint("/mcp")
+ .build();
+ }
+
+ @Bean
+ RouterFunction mcpRouterFunction(
+ WebMvcStreamableServerTransportProvider transportProvider) {
+ return transportProvider.getRouterFunction();
+ }
+ }
+ ```
+
+### SSE HTTP (Legacy)
+
+=== "SSE Servlet"
+
+ Creates a Servlet-based SSE server transport. Included in the core `mcp` module.
+ The `HttpServletSseServerTransportProvider` can be used with any Servlet container.
+ To use it with a Spring Web application, you can register it as a Servlet bean:
+
+ ```java
+ @Configuration
+ @EnableWebMvc
+ public class McpServerConfig implements WebMvcConfigurer {
+
+ @Bean
+ public HttpServletSseServerTransportProvider servletSseServerTransportProvider() {
+ return new HttpServletSseServerTransportProvider(new ObjectMapper(), "/mcp/message");
+ }
+
+ @Bean
+ public ServletRegistrationBean> customServletBean(
+ HttpServletSseServerTransportProvider transportProvider) {
+ return new ServletRegistrationBean<>(transportProvider);
+ }
+ }
+ ```
+
+ Implements the MCP HTTP with SSE transport specification using the traditional Servlet API, providing:
+
+ - Asynchronous message handling using Servlet 6.0 async support
+ - Session management for multiple client connections
+ - Two types of endpoints:
+ - SSE endpoint (`/sse`) for server-to-client events
+ - Message endpoint (configurable) for client-to-server requests
+ - Error handling and response formatting
+ - Graceful shutdown support
+
+=== "SSE WebFlux (external)"
+
+ Creates WebFlux-based SSE server transport. Requires the `mcp-spring-webflux` dependency from [Spring AI](https://docs.spring.io/spring-ai/reference/2.0-SNAPSHOT/api/mcp/mcp-overview.html) 2.0+ (group `org.springframework.ai`):
+
+ ```java
+ @Configuration
+ class McpConfig {
+ @Bean
+ WebFluxSseServerTransportProvider webFluxSseServerTransportProvider(ObjectMapper mapper) {
+ return new WebFluxSseServerTransportProvider(mapper, "/mcp/message");
+ }
+
+ @Bean
+ RouterFunction> mcpRouterFunction(WebFluxSseServerTransportProvider transportProvider) {
+ return transportProvider.getRouterFunction();
+ }
+ }
+ ```
+
+ Implements the MCP HTTP with SSE transport specification, providing:
+
+ - Reactive HTTP streaming with WebFlux
+ - Concurrent client connections through SSE endpoints
+ - Message routing and session management
+ - Graceful shutdown capabilities
+
+=== "SSE WebMvc (external)"
+
+ Creates WebMvc-based SSE server transport. Requires the `mcp-spring-webmvc` dependency from [Spring AI](https://docs.spring.io/spring-ai/reference/2.0-SNAPSHOT/api/mcp/mcp-overview.html) 2.0+ (group `org.springframework.ai`):
+
+ ```java
+ @Configuration
+ @EnableWebMvc
+ class McpConfig {
+ @Bean
+ WebMvcSseServerTransportProvider webMvcSseServerTransportProvider(ObjectMapper mapper) {
+ return new WebMvcSseServerTransportProvider(mapper, "/mcp/message");
+ }
+
+ @Bean
+ RouterFunction mcpRouterFunction(
+ WebMvcSseServerTransportProvider transportProvider) {
+ return transportProvider.getRouterFunction();
+ }
+ }
+ ```
+
+ Implements the MCP HTTP with SSE transport specification, providing:
+
+ - Server-side event streaming
+ - Integration with Spring WebMVC
+ - Support for traditional web applications
+ - Synchronous operation handling
+
+
+## Server Capabilities
+
+The server can be configured with various capabilities:
+
+```java
+var capabilities = ServerCapabilities.builder()
+ .resources(false, true) // Resource support (subscribe, listChanged)
+ .tools(true) // Tool support with list changes notifications
+ .prompts(true) // Prompt support with list changes notifications
+ .completions() // Enable completions support
+ .logging() // Enable logging support
+ .build();
+```
+
+### Tool Specification
+
+The Model Context Protocol allows servers to [expose tools](https://spec.modelcontextprotocol.io/specification/2024-11-05/server/tools/) that can be invoked by language models.
+The Java SDK allows implementing Tool Specifications with their handler functions.
+Tools enable AI models to perform calculations, access external APIs, query databases, and manipulate files.
+
+The recommended approach is to use the builder pattern and `CallToolRequest` as the handler parameter:
+
+=== "Sync"
+
+ ```java
+ // Sync tool specification using builder
+ var syncToolSpecification = SyncToolSpecification.builder()
+ .tool(Tool.builder()
+ .name("calculator")
+ .description("Basic calculator")
+ .inputSchema(schema)
+ .build())
+ .callHandler((exchange, request) -> {
+ // Access arguments via request.arguments()
+ String operation = (String) request.arguments().get("operation");
+ int a = (int) request.arguments().get("a");
+ int b = (int) request.arguments().get("b");
+ // Tool implementation
+ return CallToolResult.builder()
+ .content(List.of(new McpSchema.TextContent("Result: " + result)))
+ .build();
+ })
+ .build();
+ ```
+
+=== "Async"
+
+ ```java
+ // Async tool specification using builder
+ var asyncToolSpecification = AsyncToolSpecification.builder()
+ .tool(Tool.builder()
+ .name("calculator")
+ .description("Basic calculator")
+ .inputSchema(schema)
+ .build())
+ .callHandler((exchange, request) -> {
+ // Access arguments via request.arguments()
+ String operation = (String) request.arguments().get("operation");
+ int a = (int) request.arguments().get("a");
+ int b = (int) request.arguments().get("b");
+ // Tool implementation
+ return Mono.just(CallToolResult.builder()
+ .content(List.of(new McpSchema.TextContent("Result: " + result)))
+ .build());
+ })
+ .build();
+ ```
+
+The Tool specification includes a Tool definition with `name`, `description`, and `inputSchema` followed by a call handler that implements the tool's logic.
+The handler receives `McpSyncServerExchange`/`McpAsyncServerExchange` for client interaction and a `CallToolRequest` containing the tool arguments.
+
+You can also register tools directly on the server builder using the `toolCall` convenience method:
+
+```java
+var server = McpServer.sync(transportProvider)
+ .toolCall(
+ Tool.builder().name("echo").description("Echoes input").inputSchema(schema).build(),
+ (exchange, request) -> CallToolResult.builder()
+ .content(List.of(new McpSchema.TextContent(request.arguments().get("text").toString())))
+ .build()
+ )
+ .build();
+```
+
+### Resource Specification
+
+Specification of a resource with its handler function.
+Resources provide context to AI models by exposing data such as: File contents, Database records, API responses, System information, Application state.
+
+=== "Sync"
+
+ ```java
+ // Sync resource specification
+ var syncResourceSpecification = new McpServerFeatures.SyncResourceSpecification(
+ Resource.builder()
+ .uri("custom://resource")
+ .name("name")
+ .description("description")
+ .mimeType("text/plain")
+ .build(),
+ (exchange, request) -> {
+ // Resource read implementation
+ return new ReadResourceResult(contents);
+ }
+ );
+ ```
+
+=== "Async"
+
+ ```java
+ // Async resource specification
+ var asyncResourceSpecification = new McpServerFeatures.AsyncResourceSpecification(
+ Resource.builder()
+ .uri("custom://resource")
+ .name("name")
+ .description("description")
+ .mimeType("text/plain")
+ .build(),
+ (exchange, request) -> {
+ // Resource read implementation
+ return Mono.just(new ReadResourceResult(contents));
+ }
+ );
+ ```
+
+### Resource Template Specification
+
+Resource templates allow servers to expose parameterized resources using URI templates:
+
+```java
+// Resource template specification
+var resourceTemplateSpec = new McpServerFeatures.SyncResourceTemplateSpecification(
+ ResourceTemplate.builder()
+ .uriTemplate("file://{path}")
+ .name("File Resource")
+ .description("Access files by path")
+ .mimeType("application/octet-stream")
+ .build(),
+ (exchange, request) -> {
+ // Read the file at the requested URI
+ return new ReadResourceResult(contents);
+ }
+);
+```
+
+### Prompt Specification
+
+As part of the [Prompting capabilities](https://spec.modelcontextprotocol.io/specification/2024-11-05/server/prompts/), MCP provides a standardized way for servers to expose prompt templates to clients.
+The Prompt Specification is a structured template for AI model interactions that enables consistent message formatting, parameter substitution, context injection, response formatting, and instruction templating.
+
+=== "Sync"
+
+ ```java
+ // Sync prompt specification
+ var syncPromptSpecification = new McpServerFeatures.SyncPromptSpecification(
+ new Prompt("greeting", "description", List.of(
+ new PromptArgument("name", "description", true)
+ )),
+ (exchange, request) -> {
+ // Prompt implementation
+ return new GetPromptResult(description, messages);
+ }
+ );
+ ```
+
+=== "Async"
+
+ ```java
+ // Async prompt specification
+ var asyncPromptSpecification = new McpServerFeatures.AsyncPromptSpecification(
+ new Prompt("greeting", "description", List.of(
+ new PromptArgument("name", "description", true)
+ )),
+ (exchange, request) -> {
+ // Prompt implementation
+ return Mono.just(new GetPromptResult(description, messages));
+ }
+ );
+ ```
+
+The prompt definition includes name (identifier for the prompt), description (purpose of the prompt), and list of arguments (parameters for templating).
+The handler function processes requests and returns formatted templates.
+The first argument is `McpSyncServerExchange`/`McpAsyncServerExchange` for client interaction, and the second argument is a `GetPromptRequest` instance.
+
+### Completion Specification
+
+Completions allow servers to provide argument autocompletion suggestions for prompts and resources:
+
+=== "Sync"
+
+ ```java
+ // Sync completion specification
+ var syncCompletionSpec = new McpServerFeatures.SyncCompletionSpecification(
+ new McpSchema.PromptReference("greeting"), // Reference to a prompt
+ (exchange, request) -> {
+ String argName = request.argument().name();
+ String partial = request.argument().value();
+ // Return matching suggestions
+ List suggestions = findMatches(partial);
+ return new McpSchema.CompleteResult(
+ new McpSchema.CompleteResult.CompleteCompletion(suggestions, suggestions.size(), false)
+ );
+ }
+ );
+ ```
+
+=== "Async"
+
+ ```java
+ // Async completion specification
+ var asyncCompletionSpec = new McpServerFeatures.AsyncCompletionSpecification(
+ new McpSchema.PromptReference("greeting"),
+ (exchange, request) -> {
+ String argName = request.argument().name();
+ String partial = request.argument().value();
+ List suggestions = findMatches(partial);
+ return Mono.just(new McpSchema.CompleteResult(
+ new McpSchema.CompleteResult.CompleteCompletion(suggestions, suggestions.size(), false)
+ ));
+ }
+ );
+ ```
+
+Completions can be registered for both `PromptReference` and `ResourceReference` types.
+
+### Using Sampling from a Server
+
+To use [Sampling capabilities](https://spec.modelcontextprotocol.io/specification/2024-11-05/client/sampling/), connect to a client that supports sampling.
+No special server configuration is needed, but verify client sampling support before making requests.
+Learn about [client sampling support](client.md#sampling-support).
+
+Once connected to a compatible client, the server can request language model generations:
+
+=== "Sync API"
+
+ ```java
+ // Create a server
+ McpSyncServer server = McpServer.sync(transportProvider)
+ .serverInfo("my-server", "1.0.0")
+ .build();
+
+ // Define a tool that uses sampling
+ var calculatorTool = SyncToolSpecification.builder()
+ .tool(Tool.builder()
+ .name("ai-calculator")
+ .description("Performs calculations using AI")
+ .inputSchema(schema)
+ .build())
+ .callHandler((exchange, request) -> {
+ // Check if client supports sampling
+ if (exchange.getClientCapabilities().sampling() == null) {
+ return CallToolResult.builder()
+ .content(List.of(new McpSchema.TextContent("Client does not support AI capabilities")))
+ .build();
+ }
+
+ // Create a sampling request
+ CreateMessageRequest samplingRequest = CreateMessageRequest.builder()
+ .messages(List.of(new McpSchema.SamplingMessage(McpSchema.Role.USER,
+ new McpSchema.TextContent("Calculate: " + request.arguments().get("expression")))))
+ .modelPreferences(McpSchema.ModelPreferences.builder()
+ .hints(List.of(
+ McpSchema.ModelHint.of("claude-3-sonnet"),
+ McpSchema.ModelHint.of("claude")
+ ))
+ .intelligencePriority(0.8)
+ .speedPriority(0.5)
+ .build())
+ .systemPrompt("You are a helpful calculator assistant. Provide only the numerical answer.")
+ .maxTokens(100)
+ .build();
+
+ // Request sampling from the client
+ CreateMessageResult result = exchange.createMessage(samplingRequest);
+
+ // Process the result
+ String answer = ((McpSchema.TextContent) result.content()).text();
+ return CallToolResult.builder()
+ .content(List.of(new McpSchema.TextContent(answer)))
+ .build();
+ })
+ .build();
+
+ // Add the tool to the server
+ server.addTool(calculatorTool);
+ ```
+
+=== "Async API"
+
+ ```java
+ // Create a server
+ McpAsyncServer server = McpServer.async(transportProvider)
+ .serverInfo("my-server", "1.0.0")
+ .build();
+
+ // Define a tool that uses sampling
+ var calculatorTool = AsyncToolSpecification.builder()
+ .tool(Tool.builder()
+ .name("ai-calculator")
+ .description("Performs calculations using AI")
+ .inputSchema(schema)
+ .build())
+ .callHandler((exchange, request) -> {
+ // Check if client supports sampling
+ if (exchange.getClientCapabilities().sampling() == null) {
+ return Mono.just(CallToolResult.builder()
+ .content(List.of(new McpSchema.TextContent("Client does not support AI capabilities")))
+ .build());
+ }
+
+ // Create a sampling request
+ CreateMessageRequest samplingRequest = CreateMessageRequest.builder()
+ .messages(List.of(new McpSchema.SamplingMessage(McpSchema.Role.USER,
+ new McpSchema.TextContent("Calculate: " + request.arguments().get("expression")))))
+ .modelPreferences(McpSchema.ModelPreferences.builder()
+ .hints(List.of(
+ McpSchema.ModelHint.of("claude-3-sonnet"),
+ McpSchema.ModelHint.of("claude")
+ ))
+ .intelligencePriority(0.8)
+ .speedPriority(0.5)
+ .build())
+ .systemPrompt("You are a helpful calculator assistant. Provide only the numerical answer.")
+ .maxTokens(100)
+ .build();
+
+ // Request sampling from the client
+ return exchange.createMessage(samplingRequest)
+ .map(result -> {
+ String answer = ((McpSchema.TextContent) result.content()).text();
+ return CallToolResult.builder()
+ .content(List.of(new McpSchema.TextContent(answer)))
+ .build();
+ });
+ })
+ .build();
+
+ // Add the tool to the server
+ server.addTool(calculatorTool)
+ .subscribe();
+ ```
+
+The `CreateMessageRequest` object allows you to specify: `Content` - the input text or image for the model,
+`Model Preferences` - hints and priorities for model selection, `System Prompt` - instructions for the model's behavior and
+`Max Tokens` - maximum length of the generated response.
+
+### Using Elicitation from a Server
+
+Servers can request user input from connected clients that support elicitation:
+
+```java
+var tool = SyncToolSpecification.builder()
+ .tool(Tool.builder()
+ .name("confirm-action")
+ .description("Confirms an action with the user")
+ .inputSchema(schema)
+ .build())
+ .callHandler((exchange, request) -> {
+ // Check if client supports elicitation
+ if (exchange.getClientCapabilities().elicitation() == null) {
+ return CallToolResult.builder()
+ .content(List.of(new McpSchema.TextContent("Client does not support elicitation")))
+ .build();
+ }
+
+ // Request user confirmation
+ ElicitRequest elicitRequest = ElicitRequest.builder()
+ .message("Do you want to proceed with this action?")
+ .requestedSchema(Map.of(
+ "type", "object",
+ "properties", Map.of("confirmed", Map.of("type", "boolean"))
+ ))
+ .build();
+
+ ElicitResult result = exchange.elicit(elicitRequest);
+
+ if (result.action() == ElicitResult.Action.ACCEPT) {
+ // User accepted
+ return CallToolResult.builder()
+ .content(List.of(new McpSchema.TextContent("Action confirmed")))
+ .build();
+ } else {
+ return CallToolResult.builder()
+ .content(List.of(new McpSchema.TextContent("Action declined")))
+ .build();
+ }
+ })
+ .build();
+```
+
+### Logging Support
+
+The server provides structured logging capabilities that allow sending log messages to clients with different severity levels.
+Log notifications can only be sent from within an existing client session, such as tools, resources, and prompts calls.
+
+The server can send log messages using the `McpAsyncServerExchange`/`McpSyncServerExchange` object in the tool/resource/prompt handler function:
+
+```java
+var tool = new McpServerFeatures.AsyncToolSpecification(
+ Tool.builder().name("logging-test").description("Test logging notifications").inputSchema(emptyJsonSchema).build(),
+ null,
+ (exchange, request) -> {
+
+ exchange.loggingNotification( // Use the exchange to send log messages
+ McpSchema.LoggingMessageNotification.builder()
+ .level(McpSchema.LoggingLevel.DEBUG)
+ .logger("test-logger")
+ .data("Debug message")
+ .build())
+ .block();
+
+ return Mono.just(CallToolResult.builder()
+ .content(List.of(new McpSchema.TextContent("Logging test completed")))
+ .build());
+ });
+
+var mcpServer = McpServer.async(mcpServerTransportProvider)
+ .serverInfo("test-server", "1.0.0")
+ .capabilities(
+ ServerCapabilities.builder()
+ .logging() // Enable logging support
+ .tools(true)
+ .build())
+ .tools(tool)
+ .build();
+```
+
+On the client side, you can register a logging consumer to receive log messages from the server:
+
+```java
+var mcpClient = McpClient.sync(transport)
+ .loggingConsumer(notification -> {
+ System.out.println("Received log message: " + notification.data());
+ })
+ .build();
+
+mcpClient.initialize();
+mcpClient.setLoggingLevel(McpSchema.LoggingLevel.INFO);
+```
+
+Clients can control the minimum logging level they receive through the `mcpClient.setLoggingLevel(level)` request. Messages below the set level will be filtered out.
+Supported logging levels (in order of increasing severity): DEBUG (0), INFO (1), NOTICE (2), WARNING (3), ERROR (4), CRITICAL (5), ALERT (6), EMERGENCY (7)
+
+## Error Handling
+
+The SDK provides comprehensive error handling through the McpError class, covering protocol compatibility, transport communication, JSON-RPC messaging, tool execution, resource management, prompt handling, timeouts, and connection issues. This unified error handling approach ensures consistent and reliable error management across both synchronous and asynchronous operations.
diff --git a/mcp-bom/pom.xml b/mcp-bom/pom.xml
index 83d8bc510..fb6f3a32a 100644
--- a/mcp-bom/pom.xml
+++ b/mcp-bom/pom.xml
@@ -7,7 +7,7 @@
io.modelcontextprotocol.sdkmcp-parent
- 0.12.0-SNAPSHOT
+ 1.1.0-SNAPSHOTmcp-bom
@@ -29,28 +29,28 @@
io.modelcontextprotocol.sdk
- mcp
+ mcp-core${project.version}
-
+
io.modelcontextprotocol.sdk
- mcp-test
+ mcp${project.version}
-
+
io.modelcontextprotocol.sdk
- mcp-spring-webflux
+ mcp-json-jackson2${project.version}
-
+
io.modelcontextprotocol.sdk
- mcp-spring-webmvc
+ mcp-test${project.version}
diff --git a/mcp-spring/mcp-spring-webflux/pom.xml b/mcp-core/pom.xml
similarity index 50%
rename from mcp-spring/mcp-spring-webflux/pom.xml
rename to mcp-core/pom.xml
index 300d518e7..4de0fba2b 100644
--- a/mcp-spring/mcp-spring-webflux/pom.xml
+++ b/mcp-core/pom.xml
@@ -6,13 +6,12 @@
io.modelcontextprotocol.sdkmcp-parent
- 0.12.0-SNAPSHOT
- ../../pom.xml
+ 1.1.0-SNAPSHOT
- mcp-spring-webflux
+ mcp-corejar
- WebFlux transports
- WebFlux implementation for the SSE and Streamable Http Client and Server transports
+ Java MCP SDK Core
+ Core classes of the Java SDK implementation of the Model Context Protocol, enabling seamless integration with language models and AI toolshttps://github.com/modelcontextprotocol/java-sdk
@@ -21,47 +20,76 @@
git@github.com/modelcontextprotocol/java-sdk.git
+
+
+
+ biz.aQute.bnd
+ bnd-maven-plugin
+ ${bnd-maven-plugin.version}
+
+
+ bnd-process
+
+ bnd-process
+
+
+
+
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-jar-plugin
+
+
+ ${project.build.outputDirectory}/META-INF/MANIFEST.MF
+
+
+
+
+
+
-
- io.modelcontextprotocol.sdk
- mcp
- 0.12.0-SNAPSHOT
-
- io.modelcontextprotocol.sdk
- mcp-test
- 0.12.0-SNAPSHOT
- test
+ org.slf4j
+ slf4j-api
+ ${slf4j-api.version}
- org.springframework
- spring-webflux
- ${springframework.version}
+ com.fasterxml.jackson.core
+ jackson-annotations
+ ${jackson-annotations.version}
- io.projectreactor.netty
- reactor-netty-http
- test
+ io.projectreactor
+ reactor-core
-
-
- org.springframework
- spring-context
- ${springframework.version}
- test
-
+
- org.springframework
- spring-test
- ${springframework.version}
- test
+ jakarta.servlet
+ jakarta.servlet-api
+ ${jakarta.servlet.version}
+ provided
@@ -76,12 +104,20 @@
${junit.version}test
+
+ org.junit.jupiter
+ junit-jupiter-params
+ ${junit.version}
+ test
+ org.mockitomockito-core${mockito.version}test
+
+
net.bytebuddybyte-buddy
@@ -99,12 +135,6 @@
${testcontainers.version}test
-
- org.testcontainers
- toxiproxy
- ${toxiproxy.version}
- test
- org.awaitility
@@ -121,19 +151,26 @@
- org.junit.jupiter
- junit-jupiter-params
- ${junit-jupiter.version}
+ net.javacrumbs.json-unit
+ json-unit-assertj
+ ${json-unit-assertj.version}test
- net.javacrumbs.json-unit
- json-unit-assertj
- ${json-unit-assertj.version}
+ org.testcontainers
+ toxiproxy
+ ${toxiproxy.version}test
+
+
+ com.google.code.gson
+ gson
+ 2.10.1
+ test
+
diff --git a/mcp/src/main/java/io/modelcontextprotocol/client/LifecycleInitializer.java b/mcp-core/src/main/java/io/modelcontextprotocol/client/LifecycleInitializer.java
similarity index 89%
rename from mcp/src/main/java/io/modelcontextprotocol/client/LifecycleInitializer.java
rename to mcp-core/src/main/java/io/modelcontextprotocol/client/LifecycleInitializer.java
index 2cc1c5dba..07d86f40e 100644
--- a/mcp/src/main/java/io/modelcontextprotocol/client/LifecycleInitializer.java
+++ b/mcp-core/src/main/java/io/modelcontextprotocol/client/LifecycleInitializer.java
@@ -11,14 +11,13 @@
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Function;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
import io.modelcontextprotocol.spec.McpClientSession;
import io.modelcontextprotocol.spec.McpError;
import io.modelcontextprotocol.spec.McpSchema;
import io.modelcontextprotocol.spec.McpTransportSessionNotFoundException;
import io.modelcontextprotocol.util.Assert;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
import reactor.core.publisher.Mono;
import reactor.core.publisher.Sinks;
import reactor.util.context.ContextView;
@@ -99,21 +98,30 @@ class LifecycleInitializer {
*/
private final Duration initializationTimeout;
+ /**
+ * Post-initialization hook to perform additional operations after every successful
+ * initialization.
+ */
+ private final Function> postInitializationHook;
+
public LifecycleInitializer(McpSchema.ClientCapabilities clientCapabilities, McpSchema.Implementation clientInfo,
List protocolVersions, Duration initializationTimeout,
- Function sessionSupplier) {
+ Function sessionSupplier,
+ Function> postInitializationHook) {
Assert.notNull(sessionSupplier, "Session supplier must not be null");
Assert.notNull(clientCapabilities, "Client capabilities must not be null");
Assert.notNull(clientInfo, "Client info must not be null");
Assert.notEmpty(protocolVersions, "Protocol versions must not be empty");
Assert.notNull(initializationTimeout, "Initialization timeout must not be null");
+ Assert.notNull(postInitializationHook, "Post-initialization hook must not be null");
this.sessionSupplier = sessionSupplier;
this.clientCapabilities = clientCapabilities;
this.clientInfo = clientInfo;
this.protocolVersions = Collections.unmodifiableList(new ArrayList<>(protocolVersions));
this.initializationTimeout = initializationTimeout;
+ this.postInitializationHook = postInitializationHook;
}
/**
@@ -148,10 +156,6 @@ interface Initialization {
}
- /**
- * Default implementation of the {@link Initialization} interface that manages the MCP
- * client initialization process.
- */
private static class DefaultInitialization implements Initialization {
/**
@@ -199,29 +203,20 @@ private void setMcpClientSession(McpClientSession mcpClientSession) {
this.mcpClientSession.set(mcpClientSession);
}
- /**
- * Returns a Mono that completes when the MCP client initialization is complete.
- * This allows subscribers to wait for the initialization to finish before
- * proceeding with further operations.
- * @return A Mono that emits the result of the MCP initialization process
- */
private Mono await() {
return this.initSink.asMono();
}
- /**
- * Completes the initialization process with the given result. It caches the
- * result and emits it to all subscribers waiting for the initialization to
- * complete.
- * @param initializeResult The result of the MCP initialization process
- */
private void complete(McpSchema.InitializeResult initializeResult) {
- // first ensure the result is cached
- this.result.set(initializeResult);
// inform all the subscribers waiting for the initialization
this.initSink.emitValue(initializeResult, Sinks.EmitFailureHandler.FAIL_FAST);
}
+ private void cacheResult(McpSchema.InitializeResult initializeResult) {
+ // first ensure the result is cached
+ this.result.set(initializeResult);
+ }
+
private void error(Throwable t) {
this.initSink.emitError(t, Sinks.EmitFailureHandler.FAIL_FAST);
}
@@ -263,7 +258,7 @@ public void handleException(Throwable t) {
}
// Providing an empty operation since we are only interested in triggering
// the implicit initialization step.
- withIntitialization("re-initializing", result -> Mono.empty()).subscribe();
+ this.withInitialization("re-initializing", result -> Mono.empty()).subscribe();
}
}
@@ -275,7 +270,7 @@ public void handleException(Throwable t) {
* @param operation The operation to execute when the client is initialized
* @return A Mono that completes with the result of the operation
*/
- public Mono withIntitialization(String actionName, Function> operation) {
+ public Mono withInitialization(String actionName, Function> operation) {
return Mono.deferContextual(ctx -> {
DefaultInitialization newInit = new DefaultInitialization();
DefaultInitialization previous = this.initializationRef.compareAndExchange(null, newInit);
@@ -283,19 +278,24 @@ public Mono withIntitialization(String actionName, Function initializationJob = needsToInitialize ? doInitialize(newInit, ctx)
- : previous.await();
+ Mono initializationJob = needsToInitialize
+ ? this.doInitialize(newInit, this.postInitializationHook, ctx) : previous.await();
return initializationJob.map(initializeResult -> this.initializationRef.get())
.timeout(this.initializationTimeout)
.onErrorResume(ex -> {
+ this.initializationRef.compareAndSet(newInit, null);
return Mono.error(new RuntimeException("Client failed to initialize " + actionName, ex));
})
- .flatMap(operation);
+ .flatMap(res -> operation.apply(res)
+ .contextWrite(c -> c.put(McpAsyncClient.NEGOTIATED_PROTOCOL_VERSION,
+ res.initializeResult().protocolVersion())));
});
}
- private Mono doInitialize(DefaultInitialization initialization, ContextView ctx) {
+ private Mono doInitialize(DefaultInitialization initialization,
+ Function> postInitOperation, ContextView ctx) {
+
initialization.setMcpClientSession(this.sessionSupplier.apply(ctx));
McpClientSession mcpClientSession = initialization.mcpSession();
@@ -321,7 +321,12 @@ private Mono doInitialize(DefaultInitialization init
}
return mcpClientSession.sendNotification(McpSchema.METHOD_NOTIFICATION_INITIALIZED, null)
+ .contextWrite(
+ c -> c.put(McpAsyncClient.NEGOTIATED_PROTOCOL_VERSION, initializeResult.protocolVersion()))
.thenReturn(initializeResult);
+ }).flatMap(initializeResult -> {
+ initialization.cacheResult(initializeResult);
+ return postInitOperation.apply(initialization).thenReturn(initializeResult);
}).doOnNext(initialization::complete).onErrorResume(ex -> {
initialization.error(ex);
return Mono.error(ex);
diff --git a/mcp/src/main/java/io/modelcontextprotocol/client/McpAsyncClient.java b/mcp-core/src/main/java/io/modelcontextprotocol/client/McpAsyncClient.java
similarity index 81%
rename from mcp/src/main/java/io/modelcontextprotocol/client/McpAsyncClient.java
rename to mcp-core/src/main/java/io/modelcontextprotocol/client/McpAsyncClient.java
index eb6d42f68..93fcc332a 100644
--- a/mcp/src/main/java/io/modelcontextprotocol/client/McpAsyncClient.java
+++ b/mcp-core/src/main/java/io/modelcontextprotocol/client/McpAsyncClient.java
@@ -15,14 +15,13 @@
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Function;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import com.fasterxml.jackson.core.type.TypeReference;
-
+import io.modelcontextprotocol.client.LifecycleInitializer.Initialization;
+import io.modelcontextprotocol.json.TypeRef;
+import io.modelcontextprotocol.json.schema.JsonSchemaValidator;
import io.modelcontextprotocol.spec.McpClientSession;
+import io.modelcontextprotocol.spec.McpClientSession.NotificationHandler;
+import io.modelcontextprotocol.spec.McpClientSession.RequestHandler;
import io.modelcontextprotocol.spec.McpClientTransport;
-import io.modelcontextprotocol.spec.McpError;
import io.modelcontextprotocol.spec.McpSchema;
import io.modelcontextprotocol.spec.McpSchema.ClientCapabilities;
import io.modelcontextprotocol.spec.McpSchema.CreateMessageRequest;
@@ -31,15 +30,16 @@
import io.modelcontextprotocol.spec.McpSchema.ElicitResult;
import io.modelcontextprotocol.spec.McpSchema.GetPromptRequest;
import io.modelcontextprotocol.spec.McpSchema.GetPromptResult;
+import io.modelcontextprotocol.util.ToolNameValidator;
import io.modelcontextprotocol.spec.McpSchema.ListPromptsResult;
import io.modelcontextprotocol.spec.McpSchema.LoggingLevel;
import io.modelcontextprotocol.spec.McpSchema.LoggingMessageNotification;
import io.modelcontextprotocol.spec.McpSchema.PaginatedRequest;
import io.modelcontextprotocol.spec.McpSchema.Root;
-import io.modelcontextprotocol.spec.McpClientSession.NotificationHandler;
-import io.modelcontextprotocol.spec.McpClientSession.RequestHandler;
import io.modelcontextprotocol.util.Assert;
import io.modelcontextprotocol.util.Utils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
@@ -76,6 +76,7 @@
* @author Dariusz Jędrzejczyk
* @author Christian Tzolov
* @author Jihoon Kim
+ * @author Anurag Pant
* @see McpClient
* @see McpSchema
* @see McpClientSession
@@ -85,27 +86,29 @@ public class McpAsyncClient {
private static final Logger logger = LoggerFactory.getLogger(McpAsyncClient.class);
- private static final TypeReference VOID_TYPE_REFERENCE = new TypeReference<>() {
+ private static final TypeRef VOID_TYPE_REFERENCE = new TypeRef<>() {
};
- public static final TypeReference
*/
public Mono initialize() {
- return this.initializer.withIntitialization("by explicit API call", init -> Mono.just(init.initializeResult()));
+ return this.initializer.withInitialization("by explicit API call", init -> Mono.just(init.initializeResult()));
}
// --------------------------
@@ -400,13 +445,14 @@ public Mono initialize() {
* @return A Mono that completes with the server's ping response
*/
public Mono ping() {
- return this.initializer.withIntitialization("pinging the server",
+ return this.initializer.withInitialization("pinging the server",
init -> init.mcpSession().sendRequest(McpSchema.METHOD_PING, null, OBJECT_TYPE_REF));
}
// --------------------------
// Roots
// --------------------------
+
/**
* Adds a new root to the client's root list.
* @param root The root to add.
@@ -481,7 +527,7 @@ public Mono removeRoot(String rootUri) {
* @return A Mono that completes when the notification is sent.
*/
public Mono rootsListChangedNotification() {
- return this.initializer.withIntitialization("sending roots list changed notification",
+ return this.initializer.withInitialization("sending roots list changed notification",
init -> init.mcpSession().sendNotification(McpSchema.METHOD_NOTIFICATION_ROOTS_LIST_CHANGED));
}
@@ -512,7 +558,7 @@ private RequestHandler samplingCreateMessageHandler() {
// --------------------------
private RequestHandler elicitationCreateHandler() {
return params -> {
- ElicitRequest request = transport.unmarshalFrom(params, new TypeReference<>() {
+ ElicitRequest request = transport.unmarshalFrom(params, new TypeRef<>() {
});
return this.elicitationHandler.apply(request);
@@ -522,10 +568,10 @@ private RequestHandler elicitationCreateHandler() {
// --------------------------
// Tools
// --------------------------
- private static final TypeReference CALL_TOOL_RESULT_TYPE_REF = new TypeReference<>() {
+ private static final TypeRef CALL_TOOL_RESULT_TYPE_REF = new TypeRef<>() {
};
- private static final TypeReference LIST_TOOLS_RESULT_TYPE_REF = new TypeReference<>() {
+ private static final TypeRef LIST_TOOLS_RESULT_TYPE_REF = new TypeRef<>() {
};
/**
@@ -540,27 +586,57 @@ private RequestHandler elicitationCreateHandler() {
* @see #listTools()
*/
public Mono callTool(McpSchema.CallToolRequest callToolRequest) {
- return this.initializer.withIntitialization("calling tools", init -> {
+ return this.initializer.withInitialization("calling tool", init -> {
if (init.initializeResult().capabilities().tools() == null) {
return Mono.error(new IllegalStateException("Server does not provide tools capability"));
}
+
return init.mcpSession()
- .sendRequest(McpSchema.METHOD_TOOLS_CALL, callToolRequest, CALL_TOOL_RESULT_TYPE_REF);
+ .sendRequest(McpSchema.METHOD_TOOLS_CALL, callToolRequest, CALL_TOOL_RESULT_TYPE_REF)
+ .flatMap(result -> Mono.just(validateToolResult(callToolRequest.name(), result)));
});
}
+ private McpSchema.CallToolResult validateToolResult(String toolName, McpSchema.CallToolResult result) {
+
+ if (!this.enableCallToolSchemaCaching || result == null || result.isError() == Boolean.TRUE) {
+ // if tool schema caching is disabled or tool call resulted in an error - skip
+ // validation and return the result as it is
+ return result;
+ }
+
+ Map optOutputSchema = this.toolsOutputSchemaCache.get(toolName);
+
+ if (optOutputSchema == null) {
+ logger.warn(
+ "Calling a tool with no outputSchema is not expected to return result with structured content, but got: {}",
+ result.structuredContent());
+ return result;
+ }
+
+ // Validate the tool output against the cached output schema
+ var validation = this.jsonSchemaValidator.validate(optOutputSchema, result.structuredContent());
+
+ if (!validation.valid()) {
+ logger.warn("Tool call result validation failed: {}", validation.errorMessage());
+ throw new IllegalArgumentException("Tool call result validation failed: " + validation.errorMessage());
+ }
+
+ return result;
+ }
+
/**
* Retrieves the list of all tools provided by the server.
* @return A Mono that emits the list of all tools result
*/
public Mono listTools() {
- return this.listTools(McpSchema.FIRST_PAGE)
- .expand(result -> (result.nextCursor() != null) ? this.listTools(result.nextCursor()) : Mono.empty())
- .reduce(new McpSchema.ListToolsResult(new ArrayList<>(), null), (allToolsResult, result) -> {
- allToolsResult.tools().addAll(result.tools());
- return allToolsResult;
- })
- .map(result -> new McpSchema.ListToolsResult(Collections.unmodifiableList(result.tools()), null));
+ return this.listTools(McpSchema.FIRST_PAGE).expand(result -> {
+ String next = result.nextCursor();
+ return (next != null && !next.isEmpty()) ? this.listTools(next) : Mono.empty();
+ }).reduce(new McpSchema.ListToolsResult(new ArrayList<>(), null), (allToolsResult, result) -> {
+ allToolsResult.tools().addAll(result.tools());
+ return allToolsResult;
+ }).map(result -> new McpSchema.ListToolsResult(Collections.unmodifiableList(result.tools()), null));
}
/**
@@ -569,14 +645,30 @@ public Mono listTools() {
* @return A Mono that emits the list of tools result
*/
public Mono listTools(String cursor) {
- return this.initializer.withIntitialization("listing tools", init -> {
- if (init.initializeResult().capabilities().tools() == null) {
- return Mono.error(new IllegalStateException("Server does not provide tools capability"));
- }
- return init.mcpSession()
- .sendRequest(McpSchema.METHOD_TOOLS_LIST, new McpSchema.PaginatedRequest(cursor),
- LIST_TOOLS_RESULT_TYPE_REF);
- });
+ return this.initializer.withInitialization("listing tools", init -> this.listToolsInternal(init, cursor));
+ }
+
+ private Mono listToolsInternal(Initialization init, String cursor) {
+
+ if (init.initializeResult().capabilities().tools() == null) {
+ return Mono.error(new IllegalStateException("Server does not provide tools capability"));
+ }
+ return init.mcpSession()
+ .sendRequest(McpSchema.METHOD_TOOLS_LIST, new McpSchema.PaginatedRequest(cursor),
+ LIST_TOOLS_RESULT_TYPE_REF)
+ .doOnNext(result -> {
+ // Validate tool names (warn only)
+ if (result.tools() != null) {
+ result.tools().forEach(tool -> ToolNameValidator.validate(tool.name(), false));
+ }
+ if (this.enableCallToolSchemaCaching && result.tools() != null) {
+ // Cache tools output schema
+ result.tools()
+ .stream()
+ .filter(tool -> tool.outputSchema() != null)
+ .forEach(tool -> this.toolsOutputSchemaCache.put(tool.name(), tool.outputSchema()));
+ }
+ });
}
private NotificationHandler asyncToolsChangeNotificationHandler(
@@ -596,13 +688,13 @@ private NotificationHandler asyncToolsChangeNotificationHandler(
// Resources
// --------------------------
- private static final TypeReference LIST_RESOURCES_RESULT_TYPE_REF = new TypeReference<>() {
+ private static final TypeRef LIST_RESOURCES_RESULT_TYPE_REF = new TypeRef<>() {
};
- private static final TypeReference READ_RESOURCE_RESULT_TYPE_REF = new TypeReference<>() {
+ private static final TypeRef READ_RESOURCE_RESULT_TYPE_REF = new TypeRef<>() {
};
- private static final TypeReference LIST_RESOURCE_TEMPLATES_RESULT_TYPE_REF = new TypeReference<>() {
+ private static final TypeRef LIST_RESOURCE_TEMPLATES_RESULT_TYPE_REF = new TypeRef<>() {
};
/**
@@ -633,7 +725,7 @@ public Mono listResources() {
* @see #readResource(McpSchema.Resource)
*/
public Mono listResources(String cursor) {
- return this.initializer.withIntitialization("listing resources", init -> {
+ return this.initializer.withInitialization("listing resources", init -> {
if (init.initializeResult().capabilities().resources() == null) {
return Mono.error(new IllegalStateException("Server does not provide the resources capability"));
}
@@ -665,7 +757,7 @@ public Mono readResource(McpSchema.Resource resour
* @see McpSchema.ReadResourceResult
*/
public Mono readResource(McpSchema.ReadResourceRequest readResourceRequest) {
- return this.initializer.withIntitialization("reading resources", init -> {
+ return this.initializer.withInitialization("reading resources", init -> {
if (init.initializeResult().capabilities().resources() == null) {
return Mono.error(new IllegalStateException("Server does not provide the resources capability"));
}
@@ -703,7 +795,7 @@ public Mono listResourceTemplates() {
* @see McpSchema.ListResourceTemplatesResult
*/
public Mono listResourceTemplates(String cursor) {
- return this.initializer.withIntitialization("listing resource templates", init -> {
+ return this.initializer.withInitialization("listing resource templates", init -> {
if (init.initializeResult().capabilities().resources() == null) {
return Mono.error(new IllegalStateException("Server does not provide the resources capability"));
}
@@ -723,7 +815,7 @@ public Mono listResourceTemplates(String
* @see #unsubscribeResource(McpSchema.UnsubscribeRequest)
*/
public Mono subscribeResource(McpSchema.SubscribeRequest subscribeRequest) {
- return this.initializer.withIntitialization("subscribing to resources", init -> init.mcpSession()
+ return this.initializer.withInitialization("subscribing to resources", init -> init.mcpSession()
.sendRequest(McpSchema.METHOD_RESOURCES_SUBSCRIBE, subscribeRequest, VOID_TYPE_REFERENCE));
}
@@ -737,7 +829,7 @@ public Mono subscribeResource(McpSchema.SubscribeRequest subscribeRequest)
* @see #subscribeResource(McpSchema.SubscribeRequest)
*/
public Mono unsubscribeResource(McpSchema.UnsubscribeRequest unsubscribeRequest) {
- return this.initializer.withIntitialization("unsubscribing from resources", init -> init.mcpSession()
+ return this.initializer.withInitialization("unsubscribing from resources", init -> init.mcpSession()
.sendRequest(McpSchema.METHOD_RESOURCES_UNSUBSCRIBE, unsubscribeRequest, VOID_TYPE_REFERENCE));
}
@@ -756,7 +848,7 @@ private NotificationHandler asyncResourcesUpdatedNotificationHandler(
List, Mono>> resourcesUpdateConsumers) {
return params -> {
McpSchema.ResourcesUpdatedNotification resourcesUpdatedNotification = transport.unmarshalFrom(params,
- new TypeReference<>() {
+ new TypeRef<>() {
});
return readResource(new McpSchema.ReadResourceRequest(resourcesUpdatedNotification.uri()))
@@ -773,10 +865,10 @@ private NotificationHandler asyncResourcesUpdatedNotificationHandler(
// --------------------------
// Prompts
// --------------------------
- private static final TypeReference LIST_PROMPTS_RESULT_TYPE_REF = new TypeReference<>() {
+ private static final TypeRef LIST_PROMPTS_RESULT_TYPE_REF = new TypeRef<>() {
};
- private static final TypeReference GET_PROMPT_RESULT_TYPE_REF = new TypeReference<>() {
+ private static final TypeRef GET_PROMPT_RESULT_TYPE_REF = new TypeRef<>() {
};
/**
@@ -803,7 +895,7 @@ public Mono listPrompts() {
* @see #getPrompt(GetPromptRequest)
*/
public Mono listPrompts(String cursor) {
- return this.initializer.withIntitialization("listing prompts", init -> init.mcpSession()
+ return this.initializer.withInitialization("listing prompts", init -> init.mcpSession()
.sendRequest(McpSchema.METHOD_PROMPT_LIST, new PaginatedRequest(cursor), LIST_PROMPTS_RESULT_TYPE_REF));
}
@@ -817,7 +909,7 @@ public Mono listPrompts(String cursor) {
* @see #listPrompts()
*/
public Mono getPrompt(GetPromptRequest getPromptRequest) {
- return this.initializer.withIntitialization("getting prompts", init -> init.mcpSession()
+ return this.initializer.withInitialization("getting prompts", init -> init.mcpSession()
.sendRequest(McpSchema.METHOD_PROMPT_GET, getPromptRequest, GET_PROMPT_RESULT_TYPE_REF));
}
@@ -835,14 +927,6 @@ private NotificationHandler asyncPromptsChangeNotificationHandler(
// --------------------------
// Logging
// --------------------------
- /**
- * Create a notification handler for logging notifications from the server. This
- * handler automatically distributes logging messages to all registered consumers.
- * @param loggingConsumers List of consumers that will be notified when a logging
- * message is received. Each consumer receives the logging message notification.
- * @return A NotificationHandler that processes log notifications by distributing the
- * message to all registered consumers
- */
private NotificationHandler asyncLoggingNotificationHandler(
List>> loggingConsumers) {
@@ -868,7 +952,7 @@ public Mono setLoggingLevel(LoggingLevel loggingLevel) {
return Mono.error(new IllegalArgumentException("Logging level must not be null"));
}
- return this.initializer.withIntitialization("setting logging level", init -> {
+ return this.initializer.withInitialization("setting logging level", init -> {
if (init.initializeResult().capabilities().logging() == null) {
return Mono.error(new IllegalStateException("Server's Logging capabilities are not enabled!"));
}
@@ -877,15 +961,6 @@ public Mono setLoggingLevel(LoggingLevel loggingLevel) {
});
}
- /**
- * Create a notification handler for progress notifications from the server. This
- * handler automatically distributes progress notifications to all registered
- * consumers.
- * @param progressConsumers List of consumers that will be notified when a progress
- * message is received. Each consumer receives the progress notification.
- * @return A NotificationHandler that processes progress notifications by distributing
- * the message to all registered consumers
- */
private NotificationHandler asyncProgressNotificationHandler(
List>> progressConsumers) {
@@ -911,7 +986,7 @@ void setProtocolVersions(List protocolVersions) {
// --------------------------
// Completions
// --------------------------
- private static final TypeReference COMPLETION_COMPLETE_RESULT_TYPE_REF = new TypeReference<>() {
+ private static final TypeRef COMPLETION_COMPLETE_RESULT_TYPE_REF = new TypeRef<>() {
};
/**
@@ -925,7 +1000,7 @@ void setProtocolVersions(List protocolVersions) {
* @see McpSchema.CompleteResult
*/
public Mono completeCompletion(McpSchema.CompleteRequest completeRequest) {
- return this.initializer.withIntitialization("complete completions", init -> init.mcpSession()
+ return this.initializer.withInitialization("complete completions", init -> init.mcpSession()
.sendRequest(McpSchema.METHOD_COMPLETION_COMPLETE, completeRequest, COMPLETION_COMPLETE_RESULT_TYPE_REF));
}
diff --git a/mcp/src/main/java/io/modelcontextprotocol/client/McpClient.java b/mcp-core/src/main/java/io/modelcontextprotocol/client/McpClient.java
similarity index 85%
rename from mcp/src/main/java/io/modelcontextprotocol/client/McpClient.java
rename to mcp-core/src/main/java/io/modelcontextprotocol/client/McpClient.java
index c8af28ac1..12f34e60a 100644
--- a/mcp/src/main/java/io/modelcontextprotocol/client/McpClient.java
+++ b/mcp-core/src/main/java/io/modelcontextprotocol/client/McpClient.java
@@ -4,17 +4,11 @@
package io.modelcontextprotocol.client;
-import java.time.Duration;
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.function.Consumer;
-import java.util.function.Function;
-
+import io.modelcontextprotocol.common.McpTransportContext;
+import io.modelcontextprotocol.json.McpJsonDefaults;
+import io.modelcontextprotocol.json.schema.JsonSchemaValidator;
import io.modelcontextprotocol.spec.McpClientTransport;
import io.modelcontextprotocol.spec.McpSchema;
-import io.modelcontextprotocol.spec.McpTransport;
import io.modelcontextprotocol.spec.McpSchema.ClientCapabilities;
import io.modelcontextprotocol.spec.McpSchema.CreateMessageRequest;
import io.modelcontextprotocol.spec.McpSchema.CreateMessageResult;
@@ -22,9 +16,19 @@
import io.modelcontextprotocol.spec.McpSchema.ElicitResult;
import io.modelcontextprotocol.spec.McpSchema.Implementation;
import io.modelcontextprotocol.spec.McpSchema.Root;
+import io.modelcontextprotocol.spec.McpTransport;
import io.modelcontextprotocol.util.Assert;
import reactor.core.publisher.Mono;
+import java.time.Duration;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.function.Consumer;
+import java.util.function.Function;
+import java.util.function.Supplier;
+
/**
* Factory class for creating Model Context Protocol (MCP) clients. MCP is a protocol that
* enables AI models to interact with external tools and resources through a standardized
@@ -72,6 +76,7 @@
* .resourcesChangeConsumer(resources -> Mono.fromRunnable(() -> System.out.println("Resources updated: " + resources)))
* .promptsChangeConsumer(prompts -> Mono.fromRunnable(() -> System.out.println("Prompts updated: " + prompts)))
* .loggingConsumer(message -> Mono.fromRunnable(() -> System.out.println("Log message: " + message)))
+ * .resourcesUpdateConsumer(resourceContents -> Mono.fromRunnable(() -> System.out.println("Resources contents updated: " + resourceContents)))
* .build();
* }
*
@@ -97,6 +102,7 @@
*
* @author Christian Tzolov
* @author Dariusz Jędrzejczyk
+ * @author Anurag Pant
* @see McpAsyncClient
* @see McpSyncClient
* @see McpTransport
@@ -163,7 +169,7 @@ class SyncSpec {
private ClientCapabilities capabilities;
- private Implementation clientInfo = new Implementation("Java SDK MCP Client", "1.0.0");
+ private Implementation clientInfo = new Implementation("Java SDK MCP Client", "0.15.0");
private final Map roots = new HashMap<>();
@@ -183,6 +189,12 @@ class SyncSpec {
private Function elicitationHandler;
+ private Supplier contextProvider = () -> McpTransportContext.EMPTY;
+
+ private JsonSchemaValidator jsonSchemaValidator;
+
+ private boolean enableCallToolSchemaCaching = false; // Default to false
+
private SyncSpec(McpClientTransport transport) {
Assert.notNull(transport, "Transport must not be null");
this.transport = transport;
@@ -336,6 +348,22 @@ public SyncSpec resourcesChangeConsumer(Consumer> resou
return this;
}
+ /**
+ * Adds a consumer to be notified when a specific resource is updated. This allows
+ * the client to react to changes in individual resources, such as updates to
+ * their content or metadata.
+ * @param resourcesUpdateConsumer A consumer function that processes the updated
+ * resource and returns a Mono indicating the completion of the processing. Must
+ * not be null.
+ * @return This builder instance for method chaining.
+ * @throws IllegalArgumentException If the resourcesUpdateConsumer is null.
+ */
+ public SyncSpec resourcesUpdateConsumer(Consumer> resourcesUpdateConsumer) {
+ Assert.notNull(resourcesUpdateConsumer, "Resources update consumer must not be null");
+ this.resourcesUpdateConsumers.add(resourcesUpdateConsumer);
+ return this;
+ }
+
/**
* Adds a consumer to be notified when the available prompts change. This allows
* the client to react to changes in the server's prompt templates, such as new
@@ -409,6 +437,48 @@ public SyncSpec progressConsumers(List>
return this;
}
+ /**
+ * Add a provider of {@link McpTransportContext}, providing a context before
+ * calling any client operation. This allows to extract thread-locals and hand
+ * them over to the underlying transport.
+ *
+ * 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
+ */
+ public SyncSpec transportContextProvider(Supplier contextProvider) {
+ this.contextProvider = contextProvider;
+ return this;
+ }
+
+ /**
+ * Add a {@link JsonSchemaValidator} to validate the JSON structure of the
+ * structured output.
+ * @param jsonSchemaValidator A validator to validate the JSON structure of the
+ * structured output. Must not be null.
+ * @return This builder for method chaining
+ * @throws IllegalArgumentException if jsonSchemaValidator is null
+ */
+ public SyncSpec jsonSchemaValidator(JsonSchemaValidator jsonSchemaValidator) {
+ Assert.notNull(jsonSchemaValidator, "JsonSchemaValidator must not be null");
+ this.jsonSchemaValidator = jsonSchemaValidator;
+ return this;
+ }
+
+ /**
+ * Enables automatic schema caching during callTool operations. When a tool's
+ * output schema is not found in the cache, callTool will automatically fetch and
+ * cache all tool schemas via listTools.
+ * @param enableCallToolSchemaCaching true to enable, false to disable
+ * @return This builder instance for method chaining
+ */
+ public SyncSpec enableCallToolSchemaCaching(boolean enableCallToolSchemaCaching) {
+ this.enableCallToolSchemaCaching = enableCallToolSchemaCaching;
+ return this;
+ }
+
/**
* Create an instance of {@link McpSyncClient} with the provided configurations or
* sensible defaults.
@@ -418,12 +488,13 @@ public McpSyncClient build() {
McpClientFeatures.Sync syncFeatures = new McpClientFeatures.Sync(this.clientInfo, this.capabilities,
this.roots, this.toolsChangeConsumers, this.resourcesChangeConsumers, this.resourcesUpdateConsumers,
this.promptsChangeConsumers, this.loggingConsumers, this.progressConsumers, this.samplingHandler,
- this.elicitationHandler);
+ this.elicitationHandler, this.enableCallToolSchemaCaching);
McpClientFeatures.Async asyncFeatures = McpClientFeatures.Async.fromSync(syncFeatures);
- return new McpSyncClient(
- new McpAsyncClient(transport, this.requestTimeout, this.initializationTimeout, asyncFeatures));
+ return new McpSyncClient(new McpAsyncClient(transport, this.requestTimeout, this.initializationTimeout,
+ jsonSchemaValidator != null ? jsonSchemaValidator : McpJsonDefaults.getSchemaValidator(),
+ asyncFeatures), this.contextProvider);
}
}
@@ -454,7 +525,7 @@ class AsyncSpec {
private ClientCapabilities capabilities;
- private Implementation clientInfo = new Implementation("Spring AI MCP Client", "0.3.1");
+ private Implementation clientInfo = new Implementation("Java SDK MCP Client", "0.15.0");
private final Map roots = new HashMap<>();
@@ -474,6 +545,10 @@ class AsyncSpec {
private Function> elicitationHandler;
+ private JsonSchemaValidator jsonSchemaValidator;
+
+ private boolean enableCallToolSchemaCaching = false; // Default to false
+
private AsyncSpec(McpClientTransport transport) {
Assert.notNull(transport, "Transport must not be null");
this.transport = transport;
@@ -720,17 +795,45 @@ public AsyncSpec progressConsumers(
return this;
}
+ /**
+ * Sets the JSON schema validator to use for validating tool responses against
+ * output schemas.
+ * @param jsonSchemaValidator The validator to use. Must not be null.
+ * @return This builder instance for method chaining
+ * @throws IllegalArgumentException if jsonSchemaValidator is null
+ */
+ public AsyncSpec jsonSchemaValidator(JsonSchemaValidator jsonSchemaValidator) {
+ Assert.notNull(jsonSchemaValidator, "JsonSchemaValidator must not be null");
+ this.jsonSchemaValidator = jsonSchemaValidator;
+ return this;
+ }
+
+ /**
+ * Enables automatic schema caching during callTool operations. When a tool's
+ * output schema is not found in the cache, callTool will automatically fetch and
+ * cache all tool schemas via listTools.
+ * @param enableCallToolSchemaCaching true to enable, false to disable
+ * @return This builder instance for method chaining
+ */
+ public AsyncSpec enableCallToolSchemaCaching(boolean enableCallToolSchemaCaching) {
+ this.enableCallToolSchemaCaching = enableCallToolSchemaCaching;
+ return this;
+ }
+
/**
* Create an instance of {@link McpAsyncClient} with the provided configurations
* or sensible defaults.
* @return a new instance of {@link McpAsyncClient}.
*/
public McpAsyncClient build() {
+ var jsonSchemaValidator = (this.jsonSchemaValidator != null) ? this.jsonSchemaValidator
+ : McpJsonDefaults.getSchemaValidator();
return new McpAsyncClient(this.transport, this.requestTimeout, this.initializationTimeout,
+ jsonSchemaValidator,
new McpClientFeatures.Async(this.clientInfo, this.capabilities, this.roots,
this.toolsChangeConsumers, this.resourcesChangeConsumers, this.resourcesUpdateConsumers,
this.promptsChangeConsumers, this.loggingConsumers, this.progressConsumers,
- this.samplingHandler, this.elicitationHandler));
+ this.samplingHandler, this.elicitationHandler, this.enableCallToolSchemaCaching));
}
}
diff --git a/mcp/src/main/java/io/modelcontextprotocol/client/McpClientFeatures.java b/mcp-core/src/main/java/io/modelcontextprotocol/client/McpClientFeatures.java
similarity index 94%
rename from mcp/src/main/java/io/modelcontextprotocol/client/McpClientFeatures.java
rename to mcp-core/src/main/java/io/modelcontextprotocol/client/McpClientFeatures.java
index 3b6550765..127d53337 100644
--- a/mcp/src/main/java/io/modelcontextprotocol/client/McpClientFeatures.java
+++ b/mcp-core/src/main/java/io/modelcontextprotocol/client/McpClientFeatures.java
@@ -62,6 +62,7 @@ class McpClientFeatures {
* @param progressConsumers the progress consumers.
* @param samplingHandler the sampling handler.
* @param elicitationHandler the elicitation handler.
+ * @param enableCallToolSchemaCaching whether to enable call tool schema caching.
*/
record Async(McpSchema.Implementation clientInfo, McpSchema.ClientCapabilities clientCapabilities,
Map roots, List, Mono>> toolsChangeConsumers,
@@ -71,7 +72,8 @@ record Async(McpSchema.Implementation clientInfo, McpSchema.ClientCapabilities c
List>> loggingConsumers,
List>> progressConsumers,
Function> samplingHandler,
- Function> elicitationHandler) {
+ Function> elicitationHandler,
+ boolean enableCallToolSchemaCaching) {
/**
* Create an instance and validate the arguments.
@@ -84,6 +86,7 @@ record Async(McpSchema.Implementation clientInfo, McpSchema.ClientCapabilities c
* @param progressConsumers the progress consumers.
* @param samplingHandler the sampling handler.
* @param elicitationHandler the elicitation handler.
+ * @param enableCallToolSchemaCaching whether to enable call tool schema caching.
*/
public Async(McpSchema.Implementation clientInfo, McpSchema.ClientCapabilities clientCapabilities,
Map roots,
@@ -94,7 +97,8 @@ public Async(McpSchema.Implementation clientInfo, McpSchema.ClientCapabilities c
List>> loggingConsumers,
List>> progressConsumers,
Function> samplingHandler,
- Function> elicitationHandler) {
+ Function> elicitationHandler,
+ boolean enableCallToolSchemaCaching) {
Assert.notNull(clientInfo, "Client info must not be null");
this.clientInfo = clientInfo;
@@ -113,6 +117,7 @@ public Async(McpSchema.Implementation clientInfo, McpSchema.ClientCapabilities c
this.progressConsumers = progressConsumers != null ? progressConsumers : List.of();
this.samplingHandler = samplingHandler;
this.elicitationHandler = elicitationHandler;
+ this.enableCallToolSchemaCaching = enableCallToolSchemaCaching;
}
/**
@@ -129,7 +134,7 @@ public Async(McpSchema.Implementation clientInfo, McpSchema.ClientCapabilities c
Function> elicitationHandler) {
this(clientInfo, clientCapabilities, roots, toolsChangeConsumers, resourcesChangeConsumers,
resourcesUpdateConsumers, promptsChangeConsumers, loggingConsumers, List.of(), samplingHandler,
- elicitationHandler);
+ elicitationHandler, false);
}
/**
@@ -187,7 +192,8 @@ public static Async fromSync(Sync syncSpec) {
return new Async(syncSpec.clientInfo(), syncSpec.clientCapabilities(), syncSpec.roots(),
toolsChangeConsumers, resourcesChangeConsumers, resourcesUpdateConsumers, promptsChangeConsumers,
- loggingConsumers, progressConsumers, samplingHandler, elicitationHandler);
+ loggingConsumers, progressConsumers, samplingHandler, elicitationHandler,
+ syncSpec.enableCallToolSchemaCaching);
}
}
@@ -205,6 +211,7 @@ public static Async fromSync(Sync syncSpec) {
* @param progressConsumers the progress consumers.
* @param samplingHandler the sampling handler.
* @param elicitationHandler the elicitation handler.
+ * @param enableCallToolSchemaCaching whether to enable call tool schema caching.
*/
public record Sync(McpSchema.Implementation clientInfo, McpSchema.ClientCapabilities clientCapabilities,
Map roots, List>> toolsChangeConsumers,
@@ -214,7 +221,8 @@ public record Sync(McpSchema.Implementation clientInfo, McpSchema.ClientCapabili
List> loggingConsumers,
List> progressConsumers,
Function samplingHandler,
- Function elicitationHandler) {
+ Function elicitationHandler,
+ boolean enableCallToolSchemaCaching) {
/**
* Create an instance and validate the arguments.
@@ -229,6 +237,7 @@ public record Sync(McpSchema.Implementation clientInfo, McpSchema.ClientCapabili
* @param progressConsumers the progress consumers.
* @param samplingHandler the sampling handler.
* @param elicitationHandler the elicitation handler.
+ * @param enableCallToolSchemaCaching whether to enable call tool schema caching.
*/
public Sync(McpSchema.Implementation clientInfo, McpSchema.ClientCapabilities clientCapabilities,
Map roots, List>> toolsChangeConsumers,
@@ -238,7 +247,8 @@ public Sync(McpSchema.Implementation clientInfo, McpSchema.ClientCapabilities cl
List> loggingConsumers,
List> progressConsumers,
Function samplingHandler,
- Function elicitationHandler) {
+ Function elicitationHandler,
+ boolean enableCallToolSchemaCaching) {
Assert.notNull(clientInfo, "Client info must not be null");
this.clientInfo = clientInfo;
@@ -257,6 +267,7 @@ public Sync(McpSchema.Implementation clientInfo, McpSchema.ClientCapabilities cl
this.progressConsumers = progressConsumers != null ? progressConsumers : List.of();
this.samplingHandler = samplingHandler;
this.elicitationHandler = elicitationHandler;
+ this.enableCallToolSchemaCaching = enableCallToolSchemaCaching;
}
/**
@@ -272,7 +283,7 @@ public Sync(McpSchema.Implementation clientInfo, McpSchema.ClientCapabilities cl
Function elicitationHandler) {
this(clientInfo, clientCapabilities, roots, toolsChangeConsumers, resourcesChangeConsumers,
resourcesUpdateConsumers, promptsChangeConsumers, loggingConsumers, List.of(), samplingHandler,
- elicitationHandler);
+ elicitationHandler, false);
}
}
diff --git a/mcp/src/main/java/io/modelcontextprotocol/client/McpSyncClient.java b/mcp-core/src/main/java/io/modelcontextprotocol/client/McpSyncClient.java
similarity index 82%
rename from mcp/src/main/java/io/modelcontextprotocol/client/McpSyncClient.java
rename to mcp-core/src/main/java/io/modelcontextprotocol/client/McpSyncClient.java
index 33784adcd..7fdaa8941 100644
--- a/mcp/src/main/java/io/modelcontextprotocol/client/McpSyncClient.java
+++ b/mcp-core/src/main/java/io/modelcontextprotocol/client/McpSyncClient.java
@@ -5,16 +5,19 @@
package io.modelcontextprotocol.client;
import java.time.Duration;
+import java.util.function.Supplier;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
+import io.modelcontextprotocol.common.McpTransportContext;
import io.modelcontextprotocol.spec.McpSchema;
import io.modelcontextprotocol.spec.McpSchema.ClientCapabilities;
import io.modelcontextprotocol.spec.McpSchema.GetPromptRequest;
import io.modelcontextprotocol.spec.McpSchema.GetPromptResult;
import io.modelcontextprotocol.spec.McpSchema.ListPromptsResult;
import io.modelcontextprotocol.util.Assert;
+import reactor.core.publisher.Mono;
/**
* A synchronous client implementation for the Model Context Protocol (MCP) that wraps an
@@ -63,14 +66,20 @@ public class McpSyncClient implements AutoCloseable {
private final McpAsyncClient delegate;
+ private final Supplier contextProvider;
+
/**
* Create a new McpSyncClient with the given delegate.
* @param delegate the asynchronous kernel on top of which this synchronous client
* provides a blocking API.
+ * @param contextProvider the supplier of context before calling any non-blocking
+ * operation on underlying delegate
*/
- McpSyncClient(McpAsyncClient delegate) {
+ McpSyncClient(McpAsyncClient delegate, Supplier contextProvider) {
Assert.notNull(delegate, "The delegate can not be null");
+ Assert.notNull(contextProvider, "The contextProvider can not be null");
this.delegate = delegate;
+ this.contextProvider = contextProvider;
}
/**
@@ -177,14 +186,14 @@ public boolean closeGracefully() {
public McpSchema.InitializeResult initialize() {
// TODO: block takes no argument here as we assume the async client is
// configured with a requestTimeout at all times
- return this.delegate.initialize().block();
+ return withProvidedContext(this.delegate.initialize()).block();
}
/**
* Send a roots/list_changed notification.
*/
public void rootsListChangedNotification() {
- this.delegate.rootsListChangedNotification().block();
+ withProvidedContext(this.delegate.rootsListChangedNotification()).block();
}
/**
@@ -206,7 +215,7 @@ public void removeRoot(String rootUri) {
* @return
*/
public Object ping() {
- return this.delegate.ping().block();
+ return withProvidedContext(this.delegate.ping()).block();
}
// --------------------------
@@ -224,7 +233,8 @@ public Object ping() {
* Boolean indicating if the execution failed (true) or succeeded (false/absent)
*/
public McpSchema.CallToolResult callTool(McpSchema.CallToolRequest callToolRequest) {
- return this.delegate.callTool(callToolRequest).block();
+ return withProvidedContext(this.delegate.callTool(callToolRequest)).block();
+
}
/**
@@ -234,7 +244,7 @@ public McpSchema.CallToolResult callTool(McpSchema.CallToolRequest callToolReque
* pagination if more tools are available
*/
public McpSchema.ListToolsResult listTools() {
- return this.delegate.listTools().block();
+ return withProvidedContext(this.delegate.listTools()).block();
}
/**
@@ -245,7 +255,8 @@ public McpSchema.ListToolsResult listTools() {
* pagination if more tools are available
*/
public McpSchema.ListToolsResult listTools(String cursor) {
- return this.delegate.listTools(cursor).block();
+ return withProvidedContext(this.delegate.listTools(cursor)).block();
+
}
// --------------------------
@@ -257,7 +268,8 @@ public McpSchema.ListToolsResult listTools(String cursor) {
* @return The list of all resources result
*/
public McpSchema.ListResourcesResult listResources() {
- return this.delegate.listResources().block();
+ return withProvidedContext(this.delegate.listResources()).block();
+
}
/**
@@ -266,7 +278,8 @@ public McpSchema.ListResourcesResult listResources() {
* @return The list of resources result
*/
public McpSchema.ListResourcesResult listResources(String cursor) {
- return this.delegate.listResources(cursor).block();
+ return withProvidedContext(this.delegate.listResources(cursor)).block();
+
}
/**
@@ -275,7 +288,8 @@ public McpSchema.ListResourcesResult listResources(String cursor) {
* @return the resource content.
*/
public McpSchema.ReadResourceResult readResource(McpSchema.Resource resource) {
- return this.delegate.readResource(resource).block();
+ return withProvidedContext(this.delegate.readResource(resource)).block();
+
}
/**
@@ -284,7 +298,8 @@ public McpSchema.ReadResourceResult readResource(McpSchema.Resource resource) {
* @return the resource content.
*/
public McpSchema.ReadResourceResult readResource(McpSchema.ReadResourceRequest readResourceRequest) {
- return this.delegate.readResource(readResourceRequest).block();
+ return withProvidedContext(this.delegate.readResource(readResourceRequest)).block();
+
}
/**
@@ -292,7 +307,8 @@ public McpSchema.ReadResourceResult readResource(McpSchema.ReadResourceRequest r
* @return The list of all resource templates result.
*/
public McpSchema.ListResourceTemplatesResult listResourceTemplates() {
- return this.delegate.listResourceTemplates().block();
+ return withProvidedContext(this.delegate.listResourceTemplates()).block();
+
}
/**
@@ -304,7 +320,8 @@ public McpSchema.ListResourceTemplatesResult listResourceTemplates() {
* @return The list of resource templates result.
*/
public McpSchema.ListResourceTemplatesResult listResourceTemplates(String cursor) {
- return this.delegate.listResourceTemplates(cursor).block();
+ return withProvidedContext(this.delegate.listResourceTemplates(cursor)).block();
+
}
/**
@@ -317,7 +334,8 @@ public McpSchema.ListResourceTemplatesResult listResourceTemplates(String cursor
* subscribe to.
*/
public void subscribeResource(McpSchema.SubscribeRequest subscribeRequest) {
- this.delegate.subscribeResource(subscribeRequest).block();
+ withProvidedContext(this.delegate.subscribeResource(subscribeRequest)).block();
+
}
/**
@@ -326,7 +344,8 @@ public void subscribeResource(McpSchema.SubscribeRequest subscribeRequest) {
* to unsubscribe from.
*/
public void unsubscribeResource(McpSchema.UnsubscribeRequest unsubscribeRequest) {
- this.delegate.unsubscribeResource(unsubscribeRequest).block();
+ withProvidedContext(this.delegate.unsubscribeResource(unsubscribeRequest)).block();
+
}
// --------------------------
@@ -338,7 +357,7 @@ public void unsubscribeResource(McpSchema.UnsubscribeRequest unsubscribeRequest)
* @return The list of all prompts result.
*/
public ListPromptsResult listPrompts() {
- return this.delegate.listPrompts().block();
+ return withProvidedContext(this.delegate.listPrompts()).block();
}
/**
@@ -347,11 +366,12 @@ public ListPromptsResult listPrompts() {
* @return The list of prompts result.
*/
public ListPromptsResult listPrompts(String cursor) {
- return this.delegate.listPrompts(cursor).block();
+ return withProvidedContext(this.delegate.listPrompts(cursor)).block();
+
}
public GetPromptResult getPrompt(GetPromptRequest getPromptRequest) {
- return this.delegate.getPrompt(getPromptRequest).block();
+ return withProvidedContext(this.delegate.getPrompt(getPromptRequest)).block();
}
/**
@@ -359,7 +379,8 @@ public GetPromptResult getPrompt(GetPromptRequest getPromptRequest) {
* @param loggingLevel the min logging level
*/
public void setLoggingLevel(McpSchema.LoggingLevel loggingLevel) {
- this.delegate.setLoggingLevel(loggingLevel).block();
+ withProvidedContext(this.delegate.setLoggingLevel(loggingLevel)).block();
+
}
/**
@@ -369,7 +390,18 @@ public void setLoggingLevel(McpSchema.LoggingLevel loggingLevel) {
* @return the completion result containing suggested values.
*/
public McpSchema.CompleteResult completeCompletion(McpSchema.CompleteRequest completeRequest) {
- return this.delegate.completeCompletion(completeRequest).block();
+ return withProvidedContext(this.delegate.completeCompletion(completeRequest)).block();
+
+ }
+
+ /**
+ * For a given action, on assembly, capture the "context" via the
+ * {@link #contextProvider} and store it in the Reactor context.
+ * @param action the action to perform
+ * @return the result of the action
+ */
+ private Mono withProvidedContext(Mono action) {
+ return action.contextWrite(ctx -> ctx.put(McpTransportContext.KEY, this.contextProvider.get()));
}
}
diff --git a/mcp/src/main/java/io/modelcontextprotocol/client/transport/HttpClientSseClientTransport.java b/mcp-core/src/main/java/io/modelcontextprotocol/client/transport/HttpClientSseClientTransport.java
similarity index 69%
rename from mcp/src/main/java/io/modelcontextprotocol/client/transport/HttpClientSseClientTransport.java
rename to mcp-core/src/main/java/io/modelcontextprotocol/client/transport/HttpClientSseClientTransport.java
index 0f3511afb..be4e4cf97 100644
--- a/mcp/src/main/java/io/modelcontextprotocol/client/transport/HttpClientSseClientTransport.java
+++ b/mcp-core/src/main/java/io/modelcontextprotocol/client/transport/HttpClientSseClientTransport.java
@@ -18,16 +18,19 @@
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
-
-import com.fasterxml.jackson.core.type.TypeReference;
-import com.fasterxml.jackson.databind.ObjectMapper;
-
import io.modelcontextprotocol.client.transport.ResponseSubscribers.ResponseEvent;
+import io.modelcontextprotocol.client.transport.customizer.McpAsyncHttpClientRequestCustomizer;
+import io.modelcontextprotocol.client.transport.customizer.McpSyncHttpClientRequestCustomizer;
+import io.modelcontextprotocol.common.McpTransportContext;
+import io.modelcontextprotocol.json.McpJsonDefaults;
+import io.modelcontextprotocol.json.McpJsonMapper;
+import io.modelcontextprotocol.json.TypeRef;
+import io.modelcontextprotocol.spec.HttpHeaders;
import io.modelcontextprotocol.spec.McpClientTransport;
import io.modelcontextprotocol.spec.McpSchema;
-import io.modelcontextprotocol.spec.ProtocolVersions;
import io.modelcontextprotocol.spec.McpSchema.JSONRPCMessage;
import io.modelcontextprotocol.spec.McpTransportException;
+import io.modelcontextprotocol.spec.ProtocolVersions;
import io.modelcontextprotocol.util.Assert;
import io.modelcontextprotocol.util.Utils;
import reactor.core.Disposable;
@@ -94,8 +97,8 @@ public class HttpClientSseClientTransport implements McpClientTransport {
/** HTTP request builder for building requests to send messages to the server */
private final HttpRequest.Builder requestBuilder;
- /** JSON object mapper for message serialization/deserialization */
- protected ObjectMapper objectMapper;
+ /** JSON mapper for message serialization/deserialization */
+ protected McpJsonMapper jsonMapper;
/** Flag indicating if the transport is in closing state */
private volatile boolean isClosing = false;
@@ -112,83 +115,7 @@ public class HttpClientSseClientTransport implements McpClientTransport {
/**
* Customizer to modify requests before they are executed.
*/
- private final AsyncHttpRequestCustomizer httpRequestCustomizer;
-
- /**
- * Creates a new transport instance with default HTTP client and object mapper.
- * @param baseUri the base URI of the MCP server
- * @deprecated Use {@link HttpClientSseClientTransport#builder(String)} instead. This
- * constructor will be removed in future versions.
- */
- @Deprecated(forRemoval = true)
- public HttpClientSseClientTransport(String baseUri) {
- this(HttpClient.newBuilder(), baseUri, new ObjectMapper());
- }
-
- /**
- * Creates a new transport instance with custom HTTP client builder and object mapper.
- * @param clientBuilder the HTTP client builder to use
- * @param baseUri the base URI of the MCP server
- * @param objectMapper the object mapper for JSON serialization/deserialization
- * @throws IllegalArgumentException if objectMapper or clientBuilder is null
- * @deprecated Use {@link HttpClientSseClientTransport#builder(String)} instead. This
- * constructor will be removed in future versions.
- */
- @Deprecated(forRemoval = true)
- public HttpClientSseClientTransport(HttpClient.Builder clientBuilder, String baseUri, ObjectMapper objectMapper) {
- this(clientBuilder, baseUri, DEFAULT_SSE_ENDPOINT, objectMapper);
- }
-
- /**
- * Creates a new transport instance with custom HTTP client builder and object mapper.
- * @param clientBuilder the HTTP client builder to use
- * @param baseUri the base URI of the MCP server
- * @param sseEndpoint the SSE endpoint path
- * @param objectMapper the object mapper for JSON serialization/deserialization
- * @throws IllegalArgumentException if objectMapper or clientBuilder is null
- * @deprecated Use {@link HttpClientSseClientTransport#builder(String)} instead. This
- * constructor will be removed in future versions.
- */
- @Deprecated(forRemoval = true)
- public HttpClientSseClientTransport(HttpClient.Builder clientBuilder, String baseUri, String sseEndpoint,
- ObjectMapper objectMapper) {
- this(clientBuilder, HttpRequest.newBuilder(), baseUri, sseEndpoint, objectMapper);
- }
-
- /**
- * Creates a new transport instance with custom HTTP client builder, object mapper,
- * and headers.
- * @param clientBuilder the HTTP client builder to use
- * @param requestBuilder the HTTP request builder to use
- * @param baseUri the base URI of the MCP server
- * @param sseEndpoint the SSE endpoint path
- * @param objectMapper the object mapper for JSON serialization/deserialization
- * @throws IllegalArgumentException if objectMapper, clientBuilder, or headers is null
- * @deprecated Use {@link HttpClientSseClientTransport#builder(String)} instead. This
- * constructor will be removed in future versions.
- */
- @Deprecated(forRemoval = true)
- public HttpClientSseClientTransport(HttpClient.Builder clientBuilder, HttpRequest.Builder requestBuilder,
- String baseUri, String sseEndpoint, ObjectMapper objectMapper) {
- this(clientBuilder.connectTimeout(Duration.ofSeconds(10)).build(), requestBuilder, baseUri, sseEndpoint,
- objectMapper);
- }
-
- /**
- * Creates a new transport instance with custom HTTP client builder, object mapper,
- * and headers.
- * @param httpClient the HTTP client to use
- * @param requestBuilder the HTTP request builder to use
- * @param baseUri the base URI of the MCP server
- * @param sseEndpoint the SSE endpoint path
- * @param objectMapper the object mapper for JSON serialization/deserialization
- * @throws IllegalArgumentException if objectMapper, clientBuilder, or headers is null
- */
- @Deprecated(forRemoval = true)
- HttpClientSseClientTransport(HttpClient httpClient, HttpRequest.Builder requestBuilder, String baseUri,
- String sseEndpoint, ObjectMapper objectMapper) {
- this(httpClient, requestBuilder, baseUri, sseEndpoint, objectMapper, AsyncHttpRequestCustomizer.NOOP);
- }
+ private final McpAsyncHttpClientRequestCustomizer httpRequestCustomizer;
/**
* Creates a new transport instance with custom HTTP client builder, object mapper,
@@ -197,14 +124,14 @@ public HttpClientSseClientTransport(HttpClient.Builder clientBuilder, HttpReques
* @param requestBuilder the HTTP request builder to use
* @param baseUri the base URI of the MCP server
* @param sseEndpoint the SSE endpoint path
- * @param objectMapper the object mapper for JSON serialization/deserialization
+ * @param jsonMapper the object mapper for JSON serialization/deserialization
* @param httpRequestCustomizer customizer for the requestBuilder before executing
* requests
* @throws IllegalArgumentException if objectMapper, clientBuilder, or headers is null
*/
HttpClientSseClientTransport(HttpClient httpClient, HttpRequest.Builder requestBuilder, String baseUri,
- String sseEndpoint, ObjectMapper objectMapper, AsyncHttpRequestCustomizer httpRequestCustomizer) {
- Assert.notNull(objectMapper, "ObjectMapper must not be null");
+ String sseEndpoint, McpJsonMapper jsonMapper, McpAsyncHttpClientRequestCustomizer httpRequestCustomizer) {
+ Assert.notNull(jsonMapper, "jsonMapper must not be null");
Assert.hasText(baseUri, "baseUri must not be empty");
Assert.hasText(sseEndpoint, "sseEndpoint must not be empty");
Assert.notNull(httpClient, "httpClient must not be null");
@@ -212,7 +139,7 @@ public HttpClientSseClientTransport(HttpClient.Builder clientBuilder, HttpReques
Assert.notNull(httpRequestCustomizer, "httpRequestCustomizer must not be null");
this.baseUri = URI.create(baseUri);
this.sseEndpoint = sseEndpoint;
- this.objectMapper = objectMapper;
+ this.jsonMapper = jsonMapper;
this.httpClient = httpClient;
this.requestBuilder = requestBuilder;
this.httpRequestCustomizer = httpRequestCustomizer;
@@ -241,16 +168,15 @@ public static class Builder {
private String sseEndpoint = DEFAULT_SSE_ENDPOINT;
- private HttpClient.Builder clientBuilder = HttpClient.newBuilder()
- .version(HttpClient.Version.HTTP_1_1)
- .connectTimeout(Duration.ofSeconds(10));
+ private HttpClient.Builder clientBuilder = HttpClient.newBuilder().version(HttpClient.Version.HTTP_1_1);
+
+ private McpJsonMapper jsonMapper;
- private ObjectMapper objectMapper = new ObjectMapper();
+ private HttpRequest.Builder requestBuilder = HttpRequest.newBuilder();
- private HttpRequest.Builder requestBuilder = HttpRequest.newBuilder()
- .header("Content-Type", "application/json");
+ private McpAsyncHttpClientRequestCustomizer httpRequestCustomizer = McpAsyncHttpClientRequestCustomizer.NOOP;
- private AsyncHttpRequestCustomizer httpRequestCustomizer = AsyncHttpRequestCustomizer.NOOP;
+ private Duration connectTimeout = Duration.ofSeconds(10);
/**
* Creates a new builder instance.
@@ -259,19 +185,6 @@ public static class Builder {
// Default constructor
}
- /**
- * Creates a new builder with the specified base URI.
- * @param baseUri the base URI of the MCP server
- * @deprecated Use {@link HttpClientSseClientTransport#builder(String)} instead.
- * This constructor is deprecated and will be removed or made {@code protected} or
- * {@code private} in a future release.
- */
- @Deprecated(forRemoval = true)
- public Builder(String baseUri) {
- Assert.hasText(baseUri, "baseUri must not be empty");
- this.baseUri = baseUri;
- }
-
/**
* Sets the base URI.
* @param baseUri the base URI
@@ -339,13 +252,13 @@ public Builder customizeRequest(final Consumer requestCusto
}
/**
- * Sets the object mapper for JSON serialization/deserialization.
- * @param objectMapper the object mapper
+ * Sets the JSON mapper implementation to use for serialization/deserialization.
+ * @param jsonMapper the JSON mapper
* @return this builder
*/
- public Builder objectMapper(ObjectMapper objectMapper) {
- Assert.notNull(objectMapper, "objectMapper must not be null");
- this.objectMapper = objectMapper;
+ public Builder jsonMapper(McpJsonMapper jsonMapper) {
+ Assert.notNull(jsonMapper, "jsonMapper must not be null");
+ this.jsonMapper = jsonMapper;
return this;
}
@@ -354,16 +267,17 @@ public Builder objectMapper(ObjectMapper objectMapper) {
* executing them.
*
* This overrides the customizer from
- * {@link #asyncHttpRequestCustomizer(AsyncHttpRequestCustomizer)}.
+ * {@link #asyncHttpRequestCustomizer(McpAsyncHttpClientRequestCustomizer)}.
*
- * Do NOT use a blocking {@link SyncHttpRequestCustomizer} in a non-blocking
- * context. Use {@link #asyncHttpRequestCustomizer(AsyncHttpRequestCustomizer)}
+ * Do NOT use a blocking {@link McpSyncHttpClientRequestCustomizer} in a
+ * non-blocking context. Use
+ * {@link #asyncHttpRequestCustomizer(McpAsyncHttpClientRequestCustomizer)}
* instead.
* @param syncHttpRequestCustomizer the request customizer
* @return this builder
*/
- public Builder httpRequestCustomizer(SyncHttpRequestCustomizer syncHttpRequestCustomizer) {
- this.httpRequestCustomizer = AsyncHttpRequestCustomizer.fromSync(syncHttpRequestCustomizer);
+ public Builder httpRequestCustomizer(McpSyncHttpClientRequestCustomizer syncHttpRequestCustomizer) {
+ this.httpRequestCustomizer = McpAsyncHttpClientRequestCustomizer.fromSync(syncHttpRequestCustomizer);
return this;
}
@@ -372,24 +286,36 @@ public Builder httpRequestCustomizer(SyncHttpRequestCustomizer syncHttpRequestCu
* executing them.
*
* This overrides the customizer from
- * {@link #httpRequestCustomizer(SyncHttpRequestCustomizer)}.
+ * {@link #httpRequestCustomizer(McpSyncHttpClientRequestCustomizer)}.
*
* Do NOT use a blocking implementation in a non-blocking context.
* @param asyncHttpRequestCustomizer the request customizer
* @return this builder
*/
- public Builder asyncHttpRequestCustomizer(AsyncHttpRequestCustomizer asyncHttpRequestCustomizer) {
+ public Builder asyncHttpRequestCustomizer(McpAsyncHttpClientRequestCustomizer asyncHttpRequestCustomizer) {
this.httpRequestCustomizer = asyncHttpRequestCustomizer;
return this;
}
+ /**
+ * Sets the connection timeout for the HTTP client.
+ * @param connectTimeout the connection timeout duration
+ * @return this builder
+ */
+ public Builder connectTimeout(Duration connectTimeout) {
+ Assert.notNull(connectTimeout, "connectTimeout must not be null");
+ this.connectTimeout = connectTimeout;
+ return this;
+ }
+
/**
* Builds a new {@link HttpClientSseClientTransport} instance.
* @return a new transport instance
*/
public HttpClientSseClientTransport build() {
- return new HttpClientSseClientTransport(clientBuilder.build(), requestBuilder, baseUri, sseEndpoint,
- objectMapper, httpRequestCustomizer);
+ HttpClient httpClient = this.clientBuilder.connectTimeout(this.connectTimeout).build();
+ return new HttpClientSseClientTransport(httpClient, requestBuilder, baseUri, sseEndpoint,
+ jsonMapper == null ? McpJsonDefaults.getMapper() : jsonMapper, httpRequestCustomizer);
}
}
@@ -398,14 +324,15 @@ public HttpClientSseClientTransport build() {
public Mono connect(Function, Mono> handler) {
var uri = Utils.resolveUri(this.baseUri, this.sseEndpoint);
- return Mono.defer(() -> {
+ return Mono.deferContextual(ctx -> {
var builder = requestBuilder.copy()
.uri(uri)
.header("Accept", "text/event-stream")
.header("Cache-Control", "no-cache")
.header(MCP_PROTOCOL_VERSION_HEADER_NAME, MCP_PROTOCOL_VERSION)
.GET();
- return Mono.from(this.httpRequestCustomizer.customize(builder, "GET", uri, null));
+ var transportContext = ctx.getOrDefault(McpTransportContext.KEY, McpTransportContext.EMPTY);
+ return Mono.from(this.httpRequestCustomizer.customize(builder, "GET", uri, null, transportContext));
}).flatMap(requestBuilder -> Mono.create(sink -> {
Disposable connection = Flux.create(sseSink -> this.httpClient
.sendAsync(requestBuilder.build(),
@@ -435,7 +362,7 @@ public Mono connect(Function, Mono> h
}
}
else if (MESSAGE_EVENT_TYPE.equals(responseEvent.sseEvent().event())) {
- JSONRPCMessage message = McpSchema.deserializeJsonRpcMessage(objectMapper,
+ JSONRPCMessage message = McpSchema.deserializeJsonRpcMessage(jsonMapper,
responseEvent.sseEvent().data());
sink.success();
return Flux.just(message);
@@ -516,7 +443,7 @@ public Mono sendMessage(JSONRPCMessage message) {
private Mono serializeMessage(final JSONRPCMessage message) {
return Mono.defer(() -> {
try {
- return Mono.just(objectMapper.writeValueAsString(message));
+ return Mono.just(jsonMapper.writeValueAsString(message));
}
catch (IOException e) {
return Mono.error(new McpTransportException("Failed to serialize message", e));
@@ -526,12 +453,14 @@ private Mono serializeMessage(final JSONRPCMessage message) {
private Mono> sendHttpPost(final String endpoint, final String body) {
final URI requestUri = Utils.resolveUri(baseUri, endpoint);
- return Mono.defer(() -> {
+ return Mono.deferContextual(ctx -> {
var builder = this.requestBuilder.copy()
.uri(requestUri)
+ .header(HttpHeaders.CONTENT_TYPE, "application/json")
.header(MCP_PROTOCOL_VERSION_HEADER_NAME, MCP_PROTOCOL_VERSION)
.POST(HttpRequest.BodyPublishers.ofString(body));
- return Mono.from(this.httpRequestCustomizer.customize(builder, "POST", requestUri, body));
+ var transportContext = ctx.getOrDefault(McpTransportContext.KEY, McpTransportContext.EMPTY);
+ return Mono.from(this.httpRequestCustomizer.customize(builder, "POST", requestUri, body, transportContext));
}).flatMap(customizedBuilder -> {
var request = customizedBuilder.build();
return Mono.fromFuture(httpClient.sendAsync(request, HttpResponse.BodyHandlers.ofString()));
@@ -565,8 +494,8 @@ public Mono closeGracefully() {
* @return the unmarshalled object
*/
@Override
- public T unmarshalFrom(Object data, TypeReference typeRef) {
- return this.objectMapper.convertValue(data, typeRef);
+ public T unmarshalFrom(Object data, TypeRef typeRef) {
+ return this.jsonMapper.convertValue(data, typeRef);
}
}
diff --git a/mcp/src/main/java/io/modelcontextprotocol/client/transport/HttpClientStreamableHttpTransport.java b/mcp-core/src/main/java/io/modelcontextprotocol/client/transport/HttpClientStreamableHttpTransport.java
similarity index 74%
rename from mcp/src/main/java/io/modelcontextprotocol/client/transport/HttpClientStreamableHttpTransport.java
rename to mcp-core/src/main/java/io/modelcontextprotocol/client/transport/HttpClientStreamableHttpTransport.java
index 93c28422a..d6b01e17f 100644
--- a/mcp/src/main/java/io/modelcontextprotocol/client/transport/HttpClientStreamableHttpTransport.java
+++ b/mcp-core/src/main/java/io/modelcontextprotocol/client/transport/HttpClientStreamableHttpTransport.java
@@ -11,6 +11,8 @@
import java.net.http.HttpResponse;
import java.net.http.HttpResponse.BodyHandler;
import java.time.Duration;
+import java.util.Collections;
+import java.util.Comparator;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.CompletionException;
@@ -18,14 +20,15 @@
import java.util.function.Consumer;
import java.util.function.Function;
-import org.reactivestreams.Publisher;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import com.fasterxml.jackson.core.type.TypeReference;
-import com.fasterxml.jackson.databind.ObjectMapper;
-
+import io.modelcontextprotocol.client.McpAsyncClient;
import io.modelcontextprotocol.client.transport.ResponseSubscribers.ResponseEvent;
+import io.modelcontextprotocol.client.transport.customizer.McpAsyncHttpClientRequestCustomizer;
+import io.modelcontextprotocol.client.transport.customizer.McpSyncHttpClientRequestCustomizer;
+import io.modelcontextprotocol.common.McpTransportContext;
+import io.modelcontextprotocol.json.McpJsonDefaults;
+import io.modelcontextprotocol.json.McpJsonMapper;
+import io.modelcontextprotocol.json.TypeRef;
+import io.modelcontextprotocol.spec.ClosedMcpTransportSession;
import io.modelcontextprotocol.spec.DefaultMcpTransportSession;
import io.modelcontextprotocol.spec.DefaultMcpTransportStream;
import io.modelcontextprotocol.spec.HttpHeaders;
@@ -38,6 +41,9 @@
import io.modelcontextprotocol.spec.ProtocolVersions;
import io.modelcontextprotocol.util.Assert;
import io.modelcontextprotocol.util.Utils;
+import org.reactivestreams.Publisher;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
import reactor.core.Disposable;
import reactor.core.publisher.Flux;
import reactor.core.publisher.FluxSink;
@@ -74,8 +80,6 @@ public class HttpClientStreamableHttpTransport implements McpClientTransport {
private static final Logger logger = LoggerFactory.getLogger(HttpClientStreamableHttpTransport.class);
- private static final String MCP_PROTOCOL_VERSION = ProtocolVersions.MCP_2025_03_26;
-
private static final String DEFAULT_ENDPOINT = "/mcp";
/**
@@ -103,7 +107,7 @@ public class HttpClientStreamableHttpTransport implements McpClientTransport {
public static int BAD_REQUEST = 400;
- private final ObjectMapper objectMapper;
+ private final McpJsonMapper jsonMapper;
private final URI baseUri;
@@ -113,18 +117,23 @@ public class HttpClientStreamableHttpTransport implements McpClientTransport {
private final boolean resumableStreams;
- private final AsyncHttpRequestCustomizer httpRequestCustomizer;
+ private final McpAsyncHttpClientRequestCustomizer httpRequestCustomizer;
- private final AtomicReference activeSession = new AtomicReference<>();
+ private final AtomicReference> activeSession = new AtomicReference<>();
private final AtomicReference, Mono>> handler = new AtomicReference<>();
private final AtomicReference> exceptionHandler = new AtomicReference<>();
- private HttpClientStreamableHttpTransport(ObjectMapper objectMapper, HttpClient httpClient,
+ private final List supportedProtocolVersions;
+
+ private final String latestSupportedProtocolVersion;
+
+ private HttpClientStreamableHttpTransport(McpJsonMapper jsonMapper, HttpClient httpClient,
HttpRequest.Builder requestBuilder, String baseUri, String endpoint, boolean resumableStreams,
- boolean openConnectionOnStartup, AsyncHttpRequestCustomizer httpRequestCustomizer) {
- this.objectMapper = objectMapper;
+ boolean openConnectionOnStartup, McpAsyncHttpClientRequestCustomizer httpRequestCustomizer,
+ List supportedProtocolVersions) {
+ this.jsonMapper = jsonMapper;
this.httpClient = httpClient;
this.requestBuilder = requestBuilder;
this.baseUri = URI.create(baseUri);
@@ -133,11 +142,16 @@ private HttpClientStreamableHttpTransport(ObjectMapper objectMapper, HttpClient
this.openConnectionOnStartup = openConnectionOnStartup;
this.activeSession.set(createTransportSession());
this.httpRequestCustomizer = httpRequestCustomizer;
+ this.supportedProtocolVersions = Collections.unmodifiableList(supportedProtocolVersions);
+ this.latestSupportedProtocolVersion = this.supportedProtocolVersions.stream()
+ .sorted(Comparator.reverseOrder())
+ .findFirst()
+ .get();
}
@Override
public List protocolVersions() {
- return List.of(ProtocolVersions.MCP_2024_11_05, ProtocolVersions.MCP_2025_03_26);
+ return supportedProtocolVersions;
}
public static Builder builder(String baseUri) {
@@ -159,23 +173,34 @@ public Mono connect(Function, Mono createTransportSession() {
Function> onClose = sessionId -> sessionId == null ? Mono.empty()
: createDelete(sessionId);
return new DefaultMcpTransportSession(onClose);
}
+ private McpTransportSession createClosedSession(McpTransportSession existingSession) {
+ var existingSessionId = Optional.ofNullable(existingSession)
+ .filter(session -> !(session instanceof ClosedMcpTransportSession))
+ .flatMap(McpTransportSession::sessionId)
+ .orElse(null);
+ return new ClosedMcpTransportSession<>(existingSessionId);
+ }
+
private Publisher createDelete(String sessionId) {
var uri = Utils.resolveUri(this.baseUri, this.endpoint);
- return Mono.defer(() -> {
+ return Mono.deferContextual(ctx -> {
var builder = this.requestBuilder.copy()
.uri(uri)
.header("Cache-Control", "no-cache")
.header(HttpHeaders.MCP_SESSION_ID, sessionId)
- .header(HttpHeaders.PROTOCOL_VERSION, MCP_PROTOCOL_VERSION)
+ .header(HttpHeaders.PROTOCOL_VERSION,
+ ctx.getOrDefault(McpAsyncClient.NEGOTIATED_PROTOCOL_VERSION,
+ this.latestSupportedProtocolVersion))
.DELETE();
- return Mono.from(this.httpRequestCustomizer.customize(builder, "DELETE", uri, null));
+ var transportContext = ctx.getOrDefault(McpTransportContext.KEY, McpTransportContext.EMPTY);
+ return Mono.from(this.httpRequestCustomizer.customize(builder, "DELETE", uri, null, transportContext));
}).flatMap(requestBuilder -> {
var request = requestBuilder.build();
return Mono.fromFuture(() -> this.httpClient.sendAsync(request, HttpResponse.BodyHandlers.ofString()));
@@ -205,9 +230,9 @@ private void handleException(Throwable t) {
public Mono closeGracefully() {
return Mono.defer(() -> {
logger.debug("Graceful close triggered");
- DefaultMcpTransportSession currentSession = this.activeSession.getAndSet(createTransportSession());
+ McpTransportSession currentSession = this.activeSession.getAndUpdate(this::createClosedSession);
if (currentSession != null) {
- return currentSession.closeGracefully();
+ return Mono.from(currentSession.closeGracefully());
}
return Mono.empty();
});
@@ -228,7 +253,7 @@ private Mono reconnect(McpTransportStream stream) {
final McpTransportSession transportSession = this.activeSession.get();
var uri = Utils.resolveUri(this.baseUri, this.endpoint);
- Disposable connection = Mono.defer(() -> {
+ Disposable connection = Mono.deferContextual(connectionCtx -> {
HttpRequest.Builder requestBuilder = this.requestBuilder.copy();
if (transportSession != null && transportSession.sessionId().isPresent()) {
@@ -241,11 +266,14 @@ private Mono reconnect(McpTransportStream stream) {
}
var builder = requestBuilder.uri(uri)
- .header("Accept", TEXT_EVENT_STREAM)
+ .header(HttpHeaders.ACCEPT, TEXT_EVENT_STREAM)
.header("Cache-Control", "no-cache")
- .header(HttpHeaders.PROTOCOL_VERSION, MCP_PROTOCOL_VERSION)
+ .header(HttpHeaders.PROTOCOL_VERSION,
+ connectionCtx.getOrDefault(McpAsyncClient.NEGOTIATED_PROTOCOL_VERSION,
+ this.latestSupportedProtocolVersion))
.GET();
- return Mono.from(this.httpRequestCustomizer.customize(builder, "GET", uri, null));
+ var transportContext = connectionCtx.getOrDefault(McpTransportContext.KEY, McpTransportContext.EMPTY);
+ return Mono.from(this.httpRequestCustomizer.customize(builder, "GET", uri, null, transportContext));
})
.flatMapMany(
requestBuilder -> Flux.create(
@@ -268,12 +296,23 @@ private Mono reconnect(McpTransportStream stream) {
if (statusCode >= 200 && statusCode < 300) {
if (MESSAGE_EVENT_TYPE.equals(responseEvent.sseEvent().event())) {
+ String data = responseEvent.sseEvent().data();
+ // Per 2025-11-25 spec (SEP-1699), servers may
+ // send SSE events
+ // with empty data to prime the client for
+ // reconnection.
+ // Skip these events as they contain no JSON-RPC
+ // message.
+ if (data == null || data.isBlank()) {
+ logger.debug("Skipping SSE event with empty data (stream primer)");
+ return Flux.empty();
+ }
try {
// We don't support batching ATM and probably
// won't since the next version considers
// removing it.
- McpSchema.JSONRPCMessage message = McpSchema.deserializeJsonRpcMessage(
- this.objectMapper, responseEvent.sseEvent().data());
+ McpSchema.JSONRPCMessage message = McpSchema
+ .deserializeJsonRpcMessage(this.jsonMapper, data);
Tuple2, Iterable> idWithMessages = Tuples
.of(Optional.ofNullable(responseEvent.sseEvent().id()),
@@ -365,7 +404,7 @@ private BodyHandler toSendMessageBodySubscriber(FluxSink si
BodyHandler responseBodyHandler = responseInfo -> {
- String contentType = responseInfo.headers().firstValue("Content-Type").orElse("").toLowerCase();
+ String contentType = responseInfo.headers().firstValue(HttpHeaders.CONTENT_TYPE).orElse("").toLowerCase();
if (contentType.contains(TEXT_EVENT_STREAM)) {
// For SSE streams, use line subscriber that returns Void
@@ -388,7 +427,7 @@ else if (contentType.contains(APPLICATION_JSON)) {
public String toString(McpSchema.JSONRPCMessage message) {
try {
- return this.objectMapper.writeValueAsString(message);
+ return this.jsonMapper.writeValueAsString(message);
}
catch (IOException e) {
throw new RuntimeException("Failed to serialize JSON-RPC message", e);
@@ -405,7 +444,7 @@ public Mono sendMessage(McpSchema.JSONRPCMessage sentMessage) {
var uri = Utils.resolveUri(this.baseUri, this.endpoint);
String jsonBody = this.toString(sentMessage);
- Disposable connection = Mono.defer(() -> {
+ Disposable connection = Mono.deferContextual(ctx -> {
HttpRequest.Builder requestBuilder = this.requestBuilder.copy();
if (transportSession != null && transportSession.sessionId().isPresent()) {
@@ -414,12 +453,16 @@ public Mono sendMessage(McpSchema.JSONRPCMessage sentMessage) {
}
var builder = requestBuilder.uri(uri)
- .header("Accept", APPLICATION_JSON + ", " + TEXT_EVENT_STREAM)
- .header("Content-Type", APPLICATION_JSON)
- .header("Cache-Control", "no-cache")
- .header(HttpHeaders.PROTOCOL_VERSION, MCP_PROTOCOL_VERSION)
+ .header(HttpHeaders.ACCEPT, APPLICATION_JSON + ", " + TEXT_EVENT_STREAM)
+ .header(HttpHeaders.CONTENT_TYPE, APPLICATION_JSON)
+ .header(HttpHeaders.CACHE_CONTROL, "no-cache")
+ .header(HttpHeaders.PROTOCOL_VERSION,
+ ctx.getOrDefault(McpAsyncClient.NEGOTIATED_PROTOCOL_VERSION,
+ this.latestSupportedProtocolVersion))
.POST(HttpRequest.BodyPublishers.ofString(jsonBody));
- return Mono.from(this.httpRequestCustomizer.customize(builder, "POST", uri, jsonBody));
+ var transportContext = ctx.getOrDefault(McpTransportContext.KEY, McpTransportContext.EMPTY);
+ return Mono
+ .from(this.httpRequestCustomizer.customize(builder, "POST", uri, jsonBody, transportContext));
}).flatMapMany(requestBuilder -> Flux.create(responseEventSink -> {
// Create the async request with proper body subscriber selection
@@ -451,28 +494,43 @@ public Mono sendMessage(McpSchema.JSONRPCMessage sentMessage) {
String contentType = responseEvent.responseInfo()
.headers()
- .firstValue("Content-Type")
+ .firstValue(HttpHeaders.CONTENT_TYPE)
.orElse("")
.toLowerCase();
- if (contentType.isBlank()) {
- logger.debug("No content type returned for POST in session {}", sessionRepresentation);
+ String contentLength = responseEvent.responseInfo()
+ .headers()
+ .firstValue(HttpHeaders.CONTENT_LENGTH)
+ .orElse(null);
+
+ // For empty content or HTTP code 202 (ACCEPTED), assume success
+ if (contentType.isBlank() || "0".equals(contentLength) || statusCode == 202) {
+ // if (contentType.isBlank() || "0".equals(contentLength)) {
+ logger.debug("No body returned for POST in session {}", sessionRepresentation);
// No content type means no response body, so we can just
- // return
- // an empty stream
+ // return an empty stream
deliveredSink.success();
return Flux.empty();
}
else if (contentType.contains(TEXT_EVENT_STREAM)) {
return Flux.just(((ResponseSubscribers.SseResponseEvent) responseEvent).sseEvent())
.flatMap(sseEvent -> {
+ String data = sseEvent.data();
+ // Per 2025-11-25 spec (SEP-1699), servers may send SSE
+ // events
+ // with empty data to prime the client for reconnection.
+ // Skip these events as they contain no JSON-RPC message.
+ if (data == null || data.isBlank()) {
+ logger.debug("Skipping SSE event with empty data (stream primer)");
+ return Flux.empty();
+ }
try {
// We don't support batching ATM and probably
// won't
// since the
// next version considers removing it.
McpSchema.JSONRPCMessage message = McpSchema
- .deserializeJsonRpcMessage(this.objectMapper, sseEvent.data());
+ .deserializeJsonRpcMessage(this.jsonMapper, data);
Tuple2, Iterable> idWithMessages = Tuples
.of(Optional.ofNullable(sseEvent.id()), List.of(message));
@@ -495,13 +553,14 @@ else if (contentType.contains(TEXT_EVENT_STREAM)) {
else if (contentType.contains(APPLICATION_JSON)) {
deliveredSink.success();
String data = ((ResponseSubscribers.AggregateResponseEvent) responseEvent).data();
- if (sentMessage instanceof McpSchema.JSONRPCNotification && Utils.hasText(data)) {
- logger.warn("Notification: {} received non-compliant response: {}", sentMessage, data);
+ if (sentMessage instanceof McpSchema.JSONRPCNotification) {
+ logger.warn("Notification: {} received non-compliant response: {}", sentMessage,
+ Utils.hasText(data) ? data : "[empty]");
return Mono.empty();
}
try {
- return Mono.just(McpSchema.deserializeJsonRpcMessage(objectMapper, data));
+ return Mono.just(McpSchema.deserializeJsonRpcMessage(jsonMapper, data));
}
catch (IOException e) {
return Mono.error(new McpTransportException(
@@ -575,8 +634,8 @@ private static String sessionIdOrPlaceholder(McpTransportSession> transportSes
}
@Override
- public T unmarshalFrom(Object data, TypeReference typeRef) {
- return this.objectMapper.convertValue(data, typeRef);
+ public T unmarshalFrom(Object data, TypeRef typeRef) {
+ return this.jsonMapper.convertValue(data, typeRef);
}
/**
@@ -586,11 +645,9 @@ public static class Builder {
private final String baseUri;
- private ObjectMapper objectMapper;
+ private McpJsonMapper jsonMapper;
- private HttpClient.Builder clientBuilder = HttpClient.newBuilder()
- .version(HttpClient.Version.HTTP_1_1)
- .connectTimeout(Duration.ofSeconds(10));
+ private HttpClient.Builder clientBuilder = HttpClient.newBuilder().version(HttpClient.Version.HTTP_1_1);
private String endpoint = DEFAULT_ENDPOINT;
@@ -600,7 +657,12 @@ public static class Builder {
private HttpRequest.Builder requestBuilder = HttpRequest.newBuilder();
- private AsyncHttpRequestCustomizer httpRequestCustomizer = AsyncHttpRequestCustomizer.NOOP;
+ private McpAsyncHttpClientRequestCustomizer httpRequestCustomizer = McpAsyncHttpClientRequestCustomizer.NOOP;
+
+ private Duration connectTimeout = Duration.ofSeconds(10);
+
+ 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);
/**
* Creates a new builder with the specified base URI.
@@ -656,13 +718,13 @@ public Builder customizeRequest(final Consumer requestCusto
}
/**
- * Configure the {@link ObjectMapper} to use.
- * @param objectMapper instance to use
+ * Configure a custom {@link McpJsonMapper} implementation to use.
+ * @param jsonMapper instance to use
* @return the builder instance
*/
- public Builder objectMapper(ObjectMapper objectMapper) {
- Assert.notNull(objectMapper, "ObjectMapper must not be null");
- this.objectMapper = objectMapper;
+ public Builder jsonMapper(McpJsonMapper jsonMapper) {
+ Assert.notNull(jsonMapper, "jsonMapper must not be null");
+ this.jsonMapper = jsonMapper;
return this;
}
@@ -709,16 +771,17 @@ public Builder openConnectionOnStartup(boolean openConnectionOnStartup) {
* executing them.
*
* This overrides the customizer from
- * {@link #asyncHttpRequestCustomizer(AsyncHttpRequestCustomizer)}.
+ * {@link #asyncHttpRequestCustomizer(McpAsyncHttpClientRequestCustomizer)}.
*
- * Do NOT use a blocking {@link SyncHttpRequestCustomizer} in a non-blocking
- * context. Use {@link #asyncHttpRequestCustomizer(AsyncHttpRequestCustomizer)}
+ * Do NOT use a blocking {@link McpSyncHttpClientRequestCustomizer} in a
+ * non-blocking context. Use
+ * {@link #asyncHttpRequestCustomizer(McpAsyncHttpClientRequestCustomizer)}
* instead.
* @param syncHttpRequestCustomizer the request customizer
* @return this builder
*/
- public Builder httpRequestCustomizer(SyncHttpRequestCustomizer syncHttpRequestCustomizer) {
- this.httpRequestCustomizer = AsyncHttpRequestCustomizer.fromSync(syncHttpRequestCustomizer);
+ public Builder httpRequestCustomizer(McpSyncHttpClientRequestCustomizer syncHttpRequestCustomizer) {
+ this.httpRequestCustomizer = McpAsyncHttpClientRequestCustomizer.fromSync(syncHttpRequestCustomizer);
return this;
}
@@ -727,27 +790,62 @@ public Builder httpRequestCustomizer(SyncHttpRequestCustomizer syncHttpRequestCu
* executing them.
*
* This overrides the customizer from
- * {@link #httpRequestCustomizer(SyncHttpRequestCustomizer)}.
+ * {@link #httpRequestCustomizer(McpSyncHttpClientRequestCustomizer)}.
*
* Do NOT use a blocking implementation in a non-blocking context.
* @param asyncHttpRequestCustomizer the request customizer
* @return this builder
*/
- public Builder asyncHttpRequestCustomizer(AsyncHttpRequestCustomizer asyncHttpRequestCustomizer) {
+ public Builder asyncHttpRequestCustomizer(McpAsyncHttpClientRequestCustomizer asyncHttpRequestCustomizer) {
this.httpRequestCustomizer = asyncHttpRequestCustomizer;
return this;
}
+ /**
+ * Sets the connection timeout for the HTTP client.
+ * @param connectTimeout the connection timeout duration
+ * @return this builder
+ */
+ public Builder connectTimeout(Duration connectTimeout) {
+ Assert.notNull(connectTimeout, "connectTimeout must not be null");
+ this.connectTimeout = connectTimeout;
+ return this;
+ }
+
+ /**
+ * Sets the list of supported protocol versions used in version negotiation. By
+ * default, the client will send the latest of those versions in the
+ * {@code MCP-Protocol-Version} header.
+ *
+ * Setting this value only updates the values used in version negotiation, and
+ * does NOT impact the actual capabilities of the transport. It should only be
+ * used for compatibility with servers having strict requirements around the
+ * {@code MCP-Protocol-Version} header.
+ * @param supportedProtocolVersions protocol versions supported by this transport
+ * @return this builder
+ * @see version
+ * negotiation specification
+ * @see Protocol
+ * Version Header
+ */
+ public Builder supportedProtocolVersions(List supportedProtocolVersions) {
+ Assert.notEmpty(supportedProtocolVersions, "supportedProtocolVersions must not be empty");
+ this.supportedProtocolVersions = Collections.unmodifiableList(supportedProtocolVersions);
+ return this;
+ }
+
/**
* Construct a fresh instance of {@link HttpClientStreamableHttpTransport} using
* the current builder configuration.
* @return a new instance of {@link HttpClientStreamableHttpTransport}
*/
public HttpClientStreamableHttpTransport build() {
- ObjectMapper objectMapper = this.objectMapper != null ? this.objectMapper : new ObjectMapper();
-
- return new HttpClientStreamableHttpTransport(objectMapper, clientBuilder.build(), requestBuilder, baseUri,
- endpoint, resumableStreams, openConnectionOnStartup, httpRequestCustomizer);
+ HttpClient httpClient = this.clientBuilder.connectTimeout(this.connectTimeout).build();
+ return new HttpClientStreamableHttpTransport(jsonMapper == null ? McpJsonDefaults.getMapper() : jsonMapper,
+ httpClient, requestBuilder, baseUri, endpoint, resumableStreams, openConnectionOnStartup,
+ httpRequestCustomizer, supportedProtocolVersions);
}
}
diff --git a/mcp/src/main/java/io/modelcontextprotocol/client/transport/ResponseSubscribers.java b/mcp-core/src/main/java/io/modelcontextprotocol/client/transport/ResponseSubscribers.java
similarity index 89%
rename from mcp/src/main/java/io/modelcontextprotocol/client/transport/ResponseSubscribers.java
rename to mcp-core/src/main/java/io/modelcontextprotocol/client/transport/ResponseSubscribers.java
index 296d1a17d..29dc23c35 100644
--- a/mcp/src/main/java/io/modelcontextprotocol/client/transport/ResponseSubscribers.java
+++ b/mcp-core/src/main/java/io/modelcontextprotocol/client/transport/ResponseSubscribers.java
@@ -141,7 +141,6 @@ protected void hookOnSubscribe(Subscription subscription) {
@Override
protected void hookOnNext(String line) {
-
if (line.isEmpty()) {
// Empty line means end of event
if (this.eventBuilder.length() > 0) {
@@ -158,23 +157,27 @@ protected void hookOnNext(String line) {
if (matcher.find()) {
this.eventBuilder.append(matcher.group(1).trim()).append("\n");
}
+ upstream().request(1);
}
else if (line.startsWith("id:")) {
var matcher = EVENT_ID_PATTERN.matcher(line);
if (matcher.find()) {
this.currentEventId.set(matcher.group(1).trim());
}
+ upstream().request(1);
}
else if (line.startsWith("event:")) {
var matcher = EVENT_TYPE_PATTERN.matcher(line);
if (matcher.find()) {
this.currentEventType.set(matcher.group(1).trim());
}
+ upstream().request(1);
}
else if (line.startsWith(":")) {
// Ignore comment lines starting with ":"
// This is a no-op, just to skip comments
logger.debug("Ignoring comment line: {}", line);
+ upstream().request(1);
}
else {
// If the response is not successful, emit an error
@@ -220,6 +223,8 @@ static class AggregateSubscriber extends BaseSubscriber {
*/
private ResponseInfo responseInfo;
+ volatile boolean hasRequestedDemand = false;
+
/**
* Creates a new JsonLineSubscriber that will emit parsed JSON-RPC messages.
* @param sink the {@link FluxSink} to emit parsed {@link ResponseEvent} objects
@@ -233,7 +238,13 @@ public AggregateSubscriber(ResponseInfo responseInfo, FluxSink si
@Override
protected void hookOnSubscribe(Subscription subscription) {
- sink.onRequest(subscription::request);
+
+ sink.onRequest(n -> {
+ if (!hasRequestedDemand) {
+ subscription.request(Long.MAX_VALUE);
+ }
+ hasRequestedDemand = true;
+ });
// Register disposal callback to cancel subscription when Flux is disposed
sink.onDispose(subscription::cancel);
@@ -246,8 +257,11 @@ protected void hookOnNext(String line) {
@Override
protected void hookOnComplete() {
- String data = this.eventBuilder.toString();
- this.sink.next(new AggregateResponseEvent(responseInfo, data));
+
+ if (hasRequestedDemand) {
+ String data = this.eventBuilder.toString();
+ this.sink.next(new AggregateResponseEvent(responseInfo, data));
+ }
this.sink.complete();
}
@@ -268,6 +282,8 @@ static class BodilessResponseLineSubscriber extends BaseSubscriber {
private final ResponseInfo responseInfo;
+ volatile boolean hasRequestedDemand = false;
+
public BodilessResponseLineSubscriber(ResponseInfo responseInfo, FluxSink sink) {
this.sink = sink;
this.responseInfo = responseInfo;
@@ -277,7 +293,10 @@ public BodilessResponseLineSubscriber(ResponseInfo responseInfo, FluxSink {
- subscription.request(n);
+ if (!hasRequestedDemand) {
+ subscription.request(Long.MAX_VALUE);
+ }
+ hasRequestedDemand = true;
});
// Register disposal callback to cancel subscription when Flux is disposed
@@ -288,11 +307,13 @@ protected void hookOnSubscribe(Subscription subscription) {
@Override
protected void hookOnComplete() {
- // emit dummy event to be able to inspect the response info
- // this is a shortcut allowing for a more streamlined processing using
- // operator composition instead of having to deal with the CompletableFuture
- // along the Subscriber for inspecting the result
- this.sink.next(new DummyEvent(responseInfo));
+ if (hasRequestedDemand) {
+ // emit dummy event to be able to inspect the response info
+ // this is a shortcut allowing for a more streamlined processing using
+ // operator composition instead of having to deal with the
+ // CompletableFuture along the Subscriber for inspecting the result
+ this.sink.next(new DummyEvent(responseInfo));
+ }
this.sink.complete();
}
diff --git a/mcp/src/main/java/io/modelcontextprotocol/client/transport/ServerParameters.java b/mcp-core/src/main/java/io/modelcontextprotocol/client/transport/ServerParameters.java
similarity index 100%
rename from mcp/src/main/java/io/modelcontextprotocol/client/transport/ServerParameters.java
rename to mcp-core/src/main/java/io/modelcontextprotocol/client/transport/ServerParameters.java
diff --git a/mcp/src/main/java/io/modelcontextprotocol/client/transport/StdioClientTransport.java b/mcp-core/src/main/java/io/modelcontextprotocol/client/transport/StdioClientTransport.java
similarity index 92%
rename from mcp/src/main/java/io/modelcontextprotocol/client/transport/StdioClientTransport.java
rename to mcp-core/src/main/java/io/modelcontextprotocol/client/transport/StdioClientTransport.java
index 009d415e0..1b4eaca97 100644
--- a/mcp/src/main/java/io/modelcontextprotocol/client/transport/StdioClientTransport.java
+++ b/mcp-core/src/main/java/io/modelcontextprotocol/client/transport/StdioClientTransport.java
@@ -15,8 +15,8 @@
import java.util.function.Consumer;
import java.util.function.Function;
-import com.fasterxml.jackson.core.type.TypeReference;
-import com.fasterxml.jackson.databind.ObjectMapper;
+import io.modelcontextprotocol.json.TypeRef;
+import io.modelcontextprotocol.json.McpJsonMapper;
import io.modelcontextprotocol.spec.McpClientTransport;
import io.modelcontextprotocol.spec.McpSchema;
import io.modelcontextprotocol.spec.McpSchema.JSONRPCMessage;
@@ -48,7 +48,7 @@ public class StdioClientTransport implements McpClientTransport {
/** The server process being communicated with */
private Process process;
- private ObjectMapper objectMapper;
+ private McpJsonMapper jsonMapper;
/** Scheduler for handling inbound messages from the server process */
private Scheduler inboundScheduler;
@@ -70,29 +70,20 @@ public class StdioClientTransport implements McpClientTransport {
private Consumer stdErrorHandler = error -> logger.info("STDERR Message received: {}", error);
/**
- * Creates a new StdioClientTransport with the specified parameters and default
- * ObjectMapper.
+ * Creates a new StdioClientTransport with the specified parameters and JsonMapper.
* @param params The parameters for configuring the server process
+ * @param jsonMapper The JsonMapper to use for JSON serialization/deserialization
*/
- public StdioClientTransport(ServerParameters params) {
- this(params, new ObjectMapper());
- }
-
- /**
- * Creates a new StdioClientTransport with the specified parameters and ObjectMapper.
- * @param params The parameters for configuring the server process
- * @param objectMapper The ObjectMapper to use for JSON serialization/deserialization
- */
- public StdioClientTransport(ServerParameters params, ObjectMapper objectMapper) {
+ public StdioClientTransport(ServerParameters params, McpJsonMapper jsonMapper) {
Assert.notNull(params, "The params can not be null");
- Assert.notNull(objectMapper, "The ObjectMapper can not be null");
+ Assert.notNull(jsonMapper, "The JsonMapper can not be null");
this.inboundSink = Sinks.many().unicast().onBackpressureBuffer();
this.outboundSink = Sinks.many().unicast().onBackpressureBuffer();
this.params = params;
- this.objectMapper = objectMapper;
+ this.jsonMapper = jsonMapper;
this.errorSink = Sinks.many().unicast().onBackpressureBuffer();
@@ -259,7 +250,7 @@ private void startInboundProcessing() {
String line;
while (!isClosing && (line = processReader.readLine()) != null) {
try {
- JSONRPCMessage message = McpSchema.deserializeJsonRpcMessage(this.objectMapper, line);
+ JSONRPCMessage message = McpSchema.deserializeJsonRpcMessage(this.jsonMapper, line);
if (!this.inboundSink.tryEmitNext(message).isSuccess()) {
if (!isClosing) {
logger.error("Failed to enqueue inbound message: {}", message);
@@ -300,7 +291,7 @@ private void startOutboundProcessing() {
.handle((message, s) -> {
if (message != null && !isClosing) {
try {
- String jsonMessage = objectMapper.writeValueAsString(message);
+ String jsonMessage = jsonMapper.writeValueAsString(message);
// Escape any embedded newlines in the JSON message as per spec:
// https://spec.modelcontextprotocol.io/specification/basic/transports/#stdio
// - Messages are delimited by newlines, and MUST NOT contain
@@ -392,8 +383,8 @@ public Sinks.Many getErrorSink() {
}
@Override
- public T unmarshalFrom(Object data, TypeReference typeRef) {
- return this.objectMapper.convertValue(data, typeRef);
+ public T unmarshalFrom(Object data, TypeRef typeRef) {
+ return this.jsonMapper.convertValue(data, typeRef);
}
}
diff --git a/mcp-core/src/main/java/io/modelcontextprotocol/client/transport/customizer/DelegatingMcpAsyncHttpClientRequestCustomizer.java b/mcp-core/src/main/java/io/modelcontextprotocol/client/transport/customizer/DelegatingMcpAsyncHttpClientRequestCustomizer.java
new file mode 100644
index 000000000..2492efe18
--- /dev/null
+++ b/mcp-core/src/main/java/io/modelcontextprotocol/client/transport/customizer/DelegatingMcpAsyncHttpClientRequestCustomizer.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2024-2025 the original author or authors.
+ */
+package io.modelcontextprotocol.client.transport.customizer;
+
+import java.net.URI;
+import java.net.http.HttpRequest;
+import java.util.List;
+
+import org.reactivestreams.Publisher;
+
+import io.modelcontextprotocol.common.McpTransportContext;
+import io.modelcontextprotocol.util.Assert;
+
+import reactor.core.publisher.Mono;
+
+/**
+ * Composable {@link McpAsyncHttpClientRequestCustomizer} that applies multiple
+ * customizers, in order.
+ *
+ * @author Daniel Garnier-Moiroux
+ */
+public class DelegatingMcpAsyncHttpClientRequestCustomizer implements McpAsyncHttpClientRequestCustomizer {
+
+ private final List customizers;
+
+ public DelegatingMcpAsyncHttpClientRequestCustomizer(List customizers) {
+ Assert.notNull(customizers, "Customizers must not be null");
+ this.customizers = customizers;
+ }
+
+ @Override
+ public Publisher customize(HttpRequest.Builder builder, String method, URI endpoint,
+ String body, McpTransportContext context) {
+ var result = Mono.just(builder);
+ for (var customizer : this.customizers) {
+ result = result.flatMap(b -> Mono.from(customizer.customize(b, method, endpoint, body, context)));
+ }
+ return result;
+ }
+
+}
diff --git a/mcp-core/src/main/java/io/modelcontextprotocol/client/transport/customizer/DelegatingMcpSyncHttpClientRequestCustomizer.java b/mcp-core/src/main/java/io/modelcontextprotocol/client/transport/customizer/DelegatingMcpSyncHttpClientRequestCustomizer.java
new file mode 100644
index 000000000..e627e7e69
--- /dev/null
+++ b/mcp-core/src/main/java/io/modelcontextprotocol/client/transport/customizer/DelegatingMcpSyncHttpClientRequestCustomizer.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright 2024-2025 the original author or authors.
+ */
+
+package io.modelcontextprotocol.client.transport.customizer;
+
+import java.net.URI;
+import java.net.http.HttpRequest;
+import java.util.List;
+
+import io.modelcontextprotocol.common.McpTransportContext;
+import io.modelcontextprotocol.util.Assert;
+
+/**
+ * Composable {@link McpSyncHttpClientRequestCustomizer} that applies multiple
+ * customizers, in order.
+ *
+ * @author Daniel Garnier-Moiroux
+ */
+public class DelegatingMcpSyncHttpClientRequestCustomizer implements McpSyncHttpClientRequestCustomizer {
+
+ private final List delegates;
+
+ public DelegatingMcpSyncHttpClientRequestCustomizer(List customizers) {
+ Assert.notNull(customizers, "Customizers must not be null");
+ this.delegates = customizers;
+ }
+
+ @Override
+ public void customize(HttpRequest.Builder builder, String method, URI endpoint, String body,
+ McpTransportContext context) {
+ this.delegates.forEach(delegate -> delegate.customize(builder, method, endpoint, body, context));
+ }
+
+}
diff --git a/mcp/src/main/java/io/modelcontextprotocol/client/transport/AsyncHttpRequestCustomizer.java b/mcp-core/src/main/java/io/modelcontextprotocol/client/transport/customizer/McpAsyncHttpClientRequestCustomizer.java
similarity index 62%
rename from mcp/src/main/java/io/modelcontextprotocol/client/transport/AsyncHttpRequestCustomizer.java
rename to mcp-core/src/main/java/io/modelcontextprotocol/client/transport/customizer/McpAsyncHttpClientRequestCustomizer.java
index dee026d96..756b39c35 100644
--- a/mcp/src/main/java/io/modelcontextprotocol/client/transport/AsyncHttpRequestCustomizer.java
+++ b/mcp-core/src/main/java/io/modelcontextprotocol/client/transport/customizer/McpAsyncHttpClientRequestCustomizer.java
@@ -2,15 +2,18 @@
* Copyright 2024-2025 the original author or authors.
*/
-package io.modelcontextprotocol.client.transport;
+package io.modelcontextprotocol.client.transport.customizer;
import java.net.URI;
import java.net.http.HttpRequest;
+
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.
@@ -19,12 +22,12 @@
*
* @author Daniel Garnier-Moiroux
*/
-public interface AsyncHttpRequestCustomizer {
+public interface McpAsyncHttpClientRequestCustomizer {
Publisher customize(HttpRequest.Builder builder, String method, URI endpoint,
- @Nullable String body);
+ @Nullable String body, McpTransportContext context);
- AsyncHttpRequestCustomizer NOOP = new Noop();
+ McpAsyncHttpClientRequestCustomizer NOOP = new Noop();
/**
* Wrap a sync implementation in an async wrapper.
@@ -32,18 +35,18 @@ Publisher customize(HttpRequest.Builder builder, String met
* Do NOT wrap a blocking implementation for use in a non-blocking context. For a
* blocking implementation, consider using {@link Schedulers#boundedElastic()}.
*/
- static AsyncHttpRequestCustomizer fromSync(SyncHttpRequestCustomizer customizer) {
- return (builder, method, uri, body) -> Mono.fromSupplier(() -> {
- customizer.customize(builder, method, uri, body);
+ static McpAsyncHttpClientRequestCustomizer fromSync(McpSyncHttpClientRequestCustomizer customizer) {
+ return (builder, method, uri, body, context) -> Mono.fromSupplier(() -> {
+ customizer.customize(builder, method, uri, body, context);
return builder;
});
}
- class Noop implements AsyncHttpRequestCustomizer {
+ class Noop implements McpAsyncHttpClientRequestCustomizer {
@Override
public Publisher customize(HttpRequest.Builder builder, String method, URI endpoint,
- String body) {
+ String body, McpTransportContext context) {
return Mono.just(builder);
}
diff --git a/mcp-core/src/main/java/io/modelcontextprotocol/client/transport/customizer/McpSyncHttpClientRequestCustomizer.java b/mcp-core/src/main/java/io/modelcontextprotocol/client/transport/customizer/McpSyncHttpClientRequestCustomizer.java
new file mode 100644
index 000000000..e22e3aa62
--- /dev/null
+++ b/mcp-core/src/main/java/io/modelcontextprotocol/client/transport/customizer/McpSyncHttpClientRequestCustomizer.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2024-2025 the original author or authors.
+ */
+
+package io.modelcontextprotocol.client.transport.customizer;
+
+import java.net.URI;
+import java.net.http.HttpRequest;
+
+import reactor.util.annotation.Nullable;
+
+import io.modelcontextprotocol.client.McpClient.SyncSpec;
+import io.modelcontextprotocol.common.McpTransportContext;
+
+/**
+ * Customize {@link HttpRequest.Builder} before executing the request, either in SSE or
+ * Streamable HTTP transport. Do not rely on thread-locals in this implementation, instead
+ * use {@link SyncSpec#transportContextProvider} to extract context, and then consume it
+ * through {@link McpTransportContext}.
+ *
+ * @author Daniel Garnier-Moiroux
+ */
+public interface McpSyncHttpClientRequestCustomizer {
+
+ void customize(HttpRequest.Builder builder, String method, URI endpoint, @Nullable String body,
+ McpTransportContext context);
+
+}
diff --git a/mcp-core/src/main/java/io/modelcontextprotocol/common/DefaultMcpTransportContext.java b/mcp-core/src/main/java/io/modelcontextprotocol/common/DefaultMcpTransportContext.java
new file mode 100644
index 000000000..cde637b15
--- /dev/null
+++ b/mcp-core/src/main/java/io/modelcontextprotocol/common/DefaultMcpTransportContext.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright 2024-2025 the original author or authors.
+ */
+
+package io.modelcontextprotocol.common;
+
+import java.util.Map;
+
+import io.modelcontextprotocol.util.Assert;
+
+/**
+ * Default implementation for {@link McpTransportContext} which uses a map as storage.
+ *
+ * @author Dariusz Jędrzejczyk
+ * @author Daniel Garnier-Moiroux
+ */
+class DefaultMcpTransportContext implements McpTransportContext {
+
+ private final Map metadata;
+
+ DefaultMcpTransportContext(Map metadata) {
+ Assert.notNull(metadata, "The metadata cannot be null");
+ this.metadata = metadata;
+ }
+
+ @Override
+ public Object get(String key) {
+ return this.metadata.get(key);
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (o == null || getClass() != o.getClass())
+ return false;
+
+ DefaultMcpTransportContext that = (DefaultMcpTransportContext) o;
+ return this.metadata.equals(that.metadata);
+ }
+
+ @Override
+ public int hashCode() {
+ return this.metadata.hashCode();
+ }
+
+}
diff --git a/mcp/src/main/java/io/modelcontextprotocol/server/McpTransportContext.java b/mcp-core/src/main/java/io/modelcontextprotocol/common/McpTransportContext.java
similarity index 68%
rename from mcp/src/main/java/io/modelcontextprotocol/server/McpTransportContext.java
rename to mcp-core/src/main/java/io/modelcontextprotocol/common/McpTransportContext.java
index 1cd540f72..46a2ccf84 100644
--- a/mcp/src/main/java/io/modelcontextprotocol/server/McpTransportContext.java
+++ b/mcp-core/src/main/java/io/modelcontextprotocol/common/McpTransportContext.java
@@ -2,9 +2,10 @@
* Copyright 2024-2025 the original author or authors.
*/
-package io.modelcontextprotocol.server;
+package io.modelcontextprotocol.common;
import java.util.Collections;
+import java.util.Map;
/**
* Context associated with the transport layer. It allows to add transport-level metadata
@@ -26,6 +27,15 @@ public interface McpTransportContext {
@SuppressWarnings("unchecked")
McpTransportContext EMPTY = new DefaultMcpTransportContext(Collections.EMPTY_MAP);
+ /**
+ * Create an unmodifiable context containing the given metadata.
+ * @param metadata the transport metadata
+ * @return the context containing the metadata
+ */
+ static McpTransportContext create(Map metadata) {
+ return new DefaultMcpTransportContext(metadata);
+ }
+
/**
* Extract a value from the context.
* @param key the key under the data is expected
@@ -33,18 +43,4 @@ public interface McpTransportContext {
*/
Object get(String key);
- /**
- * Inserts a value for a given key.
- * @param key a String representing the key
- * @param value the value to store
- */
- void put(String key, Object value);
-
- /**
- * Copies the contents of the context to allow further modifications without affecting
- * the initial object.
- * @return a new instance with the underlying storage copied.
- */
- McpTransportContext copy();
-
}
diff --git a/mcp-core/src/main/java/io/modelcontextprotocol/json/McpJsonDefaults.java b/mcp-core/src/main/java/io/modelcontextprotocol/json/McpJsonDefaults.java
new file mode 100644
index 000000000..11b370ed8
--- /dev/null
+++ b/mcp-core/src/main/java/io/modelcontextprotocol/json/McpJsonDefaults.java
@@ -0,0 +1,76 @@
+/**
+ * Copyright 2026 - 2026 the original author or authors.
+ */
+package io.modelcontextprotocol.json;
+
+import io.modelcontextprotocol.json.schema.JsonSchemaValidator;
+import io.modelcontextprotocol.json.schema.JsonSchemaValidatorSupplier;
+import io.modelcontextprotocol.util.McpServiceLoader;
+
+/**
+ * This class is to be used to provide access to the default {@link McpJsonMapper} and to
+ * the default {@link JsonSchemaValidator} instances via the static methods:
+ * {@link #getMapper()} and {@link #getSchemaValidator()}.
+ *
+ * The initialization of (singleton) instances of this class is different in non-OSGi
+ * environments and OSGi environments. Specifically, in non-OSGi environments the
+ * {@code McpJsonDefaults} class will be loaded by whatever classloader is used to call
+ * one of the existing static get methods for the first time. For servers, this will
+ * usually be in response to the creation of the first {@code McpServer} instance. At that
+ * first time, the {@code mcpMapperServiceLoader} and {@code mcpValidatorServiceLoader}
+ * will be null, and the {@code McpJsonDefaults} constructor will be called,
+ * creating/initializing the {@code mcpMapperServiceLoader} and the
+ * {@code mcpValidatorServiceLoader}...which will then be used to call the
+ * {@code ServiceLoader.load} method.
+ *
+ * In OSGi environments, upon bundle activation SCR will create a new (singleton) instance
+ * of {@code McpJsonDefaults} (via the constructor), and then inject suppliers via the
+ * {@code setMcpJsonMapperSupplier} and {@code setJsonSchemaValidatorSupplier} methods
+ * with the SCR-discovered instances of those services. This does depend upon the
+ * jars/bundles providing those suppliers to be started/activated. This SCR behavior is
+ * dictated by xml files in {@code OSGi-INF} directory of {@code mcp-core} (this
+ * project/jar/bundle), and the jsonmapper and jsonschemavalidator provider jars/bundles
+ * (e.g. {@code mcp-json-jackson2}, {@code mcp-json-jackson3}, or others).
+ */
+public class McpJsonDefaults {
+
+ protected static McpServiceLoader mcpMapperServiceLoader;
+
+ protected static McpServiceLoader mcpValidatorServiceLoader;
+
+ public McpJsonDefaults() {
+ mcpMapperServiceLoader = new McpServiceLoader<>(McpJsonMapperSupplier.class);
+ mcpValidatorServiceLoader = new McpServiceLoader<>(JsonSchemaValidatorSupplier.class);
+ }
+
+ void setMcpJsonMapperSupplier(McpJsonMapperSupplier supplier) {
+ mcpMapperServiceLoader.setSupplier(supplier);
+ }
+
+ void unsetMcpJsonMapperSupplier(McpJsonMapperSupplier supplier) {
+ mcpMapperServiceLoader.unsetSupplier(supplier);
+ }
+
+ public synchronized static McpJsonMapper getMapper() {
+ if (mcpMapperServiceLoader == null) {
+ new McpJsonDefaults();
+ }
+ return mcpMapperServiceLoader.getDefault();
+ }
+
+ void setJsonSchemaValidatorSupplier(JsonSchemaValidatorSupplier supplier) {
+ mcpValidatorServiceLoader.setSupplier(supplier);
+ }
+
+ void unsetJsonSchemaValidatorSupplier(JsonSchemaValidatorSupplier supplier) {
+ mcpValidatorServiceLoader.unsetSupplier(supplier);
+ }
+
+ public synchronized static JsonSchemaValidator getSchemaValidator() {
+ if (mcpValidatorServiceLoader == null) {
+ new McpJsonDefaults();
+ }
+ return mcpValidatorServiceLoader.getDefault();
+ }
+
+}
diff --git a/mcp-core/src/main/java/io/modelcontextprotocol/json/McpJsonMapper.java b/mcp-core/src/main/java/io/modelcontextprotocol/json/McpJsonMapper.java
new file mode 100644
index 000000000..8481d1703
--- /dev/null
+++ b/mcp-core/src/main/java/io/modelcontextprotocol/json/McpJsonMapper.java
@@ -0,0 +1,90 @@
+/*
+ * Copyright 2025 - 2025 the original author or authors.
+ */
+
+package io.modelcontextprotocol.json;
+
+import java.io.IOException;
+
+/**
+ * Abstraction for JSON serialization/deserialization to decouple the SDK from any
+ * specific JSON library. A default implementation backed by Jackson is provided in
+ * io.modelcontextprotocol.spec.json.jackson.JacksonJsonMapper.
+ */
+public interface McpJsonMapper {
+
+ /**
+ * Deserialize JSON string into a target type.
+ * @param content JSON as String
+ * @param type target class
+ * @return deserialized instance
+ * @param generic type
+ * @throws IOException on parse errors
+ */
+ T readValue(String content, Class type) throws IOException;
+
+ /**
+ * Deserialize JSON bytes into a target type.
+ * @param content JSON as bytes
+ * @param type target class
+ * @return deserialized instance
+ * @param generic type
+ * @throws IOException on parse errors
+ */
+ T readValue(byte[] content, Class type) throws IOException;
+
+ /**
+ * Deserialize JSON string into a parameterized target type.
+ * @param content JSON as String
+ * @param type parameterized type reference
+ * @return deserialized instance
+ * @param generic type
+ * @throws IOException on parse errors
+ */
+ T readValue(String content, TypeRef type) throws IOException;
+
+ /**
+ * Deserialize JSON bytes into a parameterized target type.
+ * @param content JSON as bytes
+ * @param type parameterized type reference
+ * @return deserialized instance
+ * @param generic type
+ * @throws IOException on parse errors
+ */
+ T readValue(byte[] content, TypeRef type) throws IOException;
+
+ /**
+ * Convert a value to a given type, useful for mapping nested JSON structures.
+ * @param fromValue source value
+ * @param type target class
+ * @return converted value
+ * @param generic type
+ */
+ T convertValue(Object fromValue, Class type);
+
+ /**
+ * Convert a value to a given parameterized type.
+ * @param fromValue source value
+ * @param type target type reference
+ * @return converted value
+ * @param generic type
+ */
+ T convertValue(Object fromValue, TypeRef type);
+
+ /**
+ * Serialize an object to JSON string.
+ * @param value object to serialize
+ * @return JSON as String
+ * @throws IOException on serialization errors
+ */
+ String writeValueAsString(Object value) throws IOException;
+
+ /**
+ * Serialize an object to JSON bytes.
+ * @param value object to serialize
+ * @return JSON as bytes
+ * @throws IOException on serialization errors
+ */
+ byte[] writeValueAsBytes(Object value) throws IOException;
+
+}
diff --git a/mcp-core/src/main/java/io/modelcontextprotocol/json/McpJsonMapperSupplier.java b/mcp-core/src/main/java/io/modelcontextprotocol/json/McpJsonMapperSupplier.java
new file mode 100644
index 000000000..619f96040
--- /dev/null
+++ b/mcp-core/src/main/java/io/modelcontextprotocol/json/McpJsonMapperSupplier.java
@@ -0,0 +1,14 @@
+/*
+ * Copyright 2025 - 2025 the original author or authors.
+ */
+
+package io.modelcontextprotocol.json;
+
+import java.util.function.Supplier;
+
+/**
+ * Strategy interface for resolving a {@link McpJsonMapper}.
+ */
+public interface McpJsonMapperSupplier extends Supplier {
+
+}
diff --git a/mcp-core/src/main/java/io/modelcontextprotocol/json/TypeRef.java b/mcp-core/src/main/java/io/modelcontextprotocol/json/TypeRef.java
new file mode 100644
index 000000000..725513c66
--- /dev/null
+++ b/mcp-core/src/main/java/io/modelcontextprotocol/json/TypeRef.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright 2025 - 2025 the original author or authors.
+ */
+
+package io.modelcontextprotocol.json;
+
+import java.lang.reflect.ParameterizedType;
+import java.lang.reflect.Type;
+
+/**
+ * Captures generic type information at runtime for parameterized JSON (de)serialization.
+ * Usage: TypeRef> ref = new TypeRef<>(){};
+ */
+public abstract class TypeRef {
+
+ private final Type type;
+
+ /**
+ * Constructs a new TypeRef instance, capturing the generic type information of the
+ * subclass. This constructor should be called from an anonymous subclass to capture
+ * the actual type arguments. For example:
+ * TypeRef<List<Foo>> ref = new TypeRef<>(){};
+ *
+ * @throws IllegalStateException if TypeRef is not subclassed with actual type
+ * information
+ */
+ protected TypeRef() {
+ Type superClass = getClass().getGenericSuperclass();
+ if (superClass instanceof Class) {
+ throw new IllegalStateException("TypeRef constructed without actual type information");
+ }
+ this.type = ((ParameterizedType) superClass).getActualTypeArguments()[0];
+ }
+
+ /**
+ * Returns the captured type information.
+ * @return the Type representing the actual type argument captured by this TypeRef
+ * instance
+ */
+ public Type getType() {
+ return type;
+ }
+
+}
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
new file mode 100644
index 000000000..09fe604f4
--- /dev/null
+++ b/mcp-core/src/main/java/io/modelcontextprotocol/json/schema/JsonSchemaValidator.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright 2024-2024 the original author or authors.
+ */
+package io.modelcontextprotocol.json.schema;
+
+import java.util.Map;
+
+/**
+ * Interface for validating structured content against a JSON schema. This interface
+ * defines a method to validate structured content based on the provided output schema.
+ *
+ * @author Christian Tzolov
+ */
+public interface JsonSchemaValidator {
+
+ /**
+ * Represents the result of a validation operation.
+ *
+ * @param valid Indicates whether the validation was successful.
+ * @param errorMessage An error message if the validation failed, otherwise null.
+ * @param jsonStructuredOutput The text structured content in JSON format if the
+ * validation was successful, otherwise null.
+ */
+ record ValidationResponse(boolean valid, String errorMessage, String jsonStructuredOutput) {
+
+ public static ValidationResponse asValid(String jsonStructuredOutput) {
+ return new ValidationResponse(true, null, jsonStructuredOutput);
+ }
+
+ public static ValidationResponse asInvalid(String message) {
+ return new ValidationResponse(false, message, null);
+ }
+ }
+
+ /**
+ * Validates the structured content against the provided JSON schema.
+ * @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
+ * not.
+ */
+ ValidationResponse validate(Map schema, Object structuredContent);
+
+}
diff --git a/mcp-core/src/main/java/io/modelcontextprotocol/json/schema/JsonSchemaValidatorSupplier.java b/mcp-core/src/main/java/io/modelcontextprotocol/json/schema/JsonSchemaValidatorSupplier.java
new file mode 100644
index 000000000..6f69169a0
--- /dev/null
+++ b/mcp-core/src/main/java/io/modelcontextprotocol/json/schema/JsonSchemaValidatorSupplier.java
@@ -0,0 +1,19 @@
+/*
+ * Copyright 2025 - 2025 the original author or authors.
+ */
+
+package io.modelcontextprotocol.json.schema;
+
+import java.util.function.Supplier;
+
+/**
+ * A supplier interface that provides a {@link JsonSchemaValidator} instance.
+ * Implementations of this interface are expected to return a new or cached instance of
+ * {@link JsonSchemaValidator} when {@link #get()} is invoked.
+ *
+ * @see JsonSchemaValidator
+ * @see Supplier
+ */
+public interface JsonSchemaValidatorSupplier extends Supplier {
+
+}
diff --git a/mcp/src/main/java/io/modelcontextprotocol/server/DefaultMcpStatelessServerHandler.java b/mcp-core/src/main/java/io/modelcontextprotocol/server/DefaultMcpStatelessServerHandler.java
similarity index 91%
rename from mcp/src/main/java/io/modelcontextprotocol/server/DefaultMcpStatelessServerHandler.java
rename to mcp-core/src/main/java/io/modelcontextprotocol/server/DefaultMcpStatelessServerHandler.java
index 2df3514b6..660a15e6a 100644
--- a/mcp/src/main/java/io/modelcontextprotocol/server/DefaultMcpStatelessServerHandler.java
+++ b/mcp-core/src/main/java/io/modelcontextprotocol/server/DefaultMcpStatelessServerHandler.java
@@ -4,6 +4,7 @@
package io.modelcontextprotocol.server;
+import io.modelcontextprotocol.common.McpTransportContext;
import io.modelcontextprotocol.spec.McpError;
import io.modelcontextprotocol.spec.McpSchema;
import org.slf4j.Logger;
@@ -31,7 +32,9 @@ public Mono handleRequest(McpTransportContext transpo
McpSchema.JSONRPCRequest request) {
McpStatelessRequestHandler> requestHandler = this.requestHandlers.get(request.method());
if (requestHandler == null) {
- return Mono.error(new McpError("Missing handler for request type: " + request.method()));
+ return Mono.error(McpError.builder(McpSchema.ErrorCodes.METHOD_NOT_FOUND)
+ .message("Missing handler for request type: " + request.method())
+ .build());
}
return requestHandler.handle(transportContext, request.params())
.map(result -> new McpSchema.JSONRPCResponse(McpSchema.JSONRPC_VERSION, request.id(), result, null))
diff --git a/mcp/src/main/java/io/modelcontextprotocol/server/McpAsyncServer.java b/mcp-core/src/main/java/io/modelcontextprotocol/server/McpAsyncServer.java
similarity index 67%
rename from mcp/src/main/java/io/modelcontextprotocol/server/McpAsyncServer.java
rename to mcp-core/src/main/java/io/modelcontextprotocol/server/McpAsyncServer.java
index a51c2e36c..32256987a 100644
--- a/mcp/src/main/java/io/modelcontextprotocol/server/McpAsyncServer.java
+++ b/mcp-core/src/main/java/io/modelcontextprotocol/server/McpAsyncServer.java
@@ -5,7 +5,6 @@
package io.modelcontextprotocol.server;
import java.time.Duration;
-import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@@ -15,34 +14,37 @@
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.function.BiFunction;
+import io.modelcontextprotocol.json.McpJsonMapper;
+import io.modelcontextprotocol.json.TypeRef;
+import io.modelcontextprotocol.json.schema.JsonSchemaValidator;
import io.modelcontextprotocol.spec.DefaultMcpStreamableServerSessionFactory;
-import io.modelcontextprotocol.spec.McpServerTransportProviderBase;
-import io.modelcontextprotocol.spec.McpStreamableServerTransportProvider;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import com.fasterxml.jackson.core.type.TypeReference;
-import com.fasterxml.jackson.databind.ObjectMapper;
-
-import io.modelcontextprotocol.spec.JsonSchemaValidator;
import io.modelcontextprotocol.spec.McpClientSession;
import io.modelcontextprotocol.spec.McpError;
import io.modelcontextprotocol.spec.McpSchema;
import io.modelcontextprotocol.spec.McpSchema.CallToolResult;
+import io.modelcontextprotocol.spec.McpSchema.CompleteResult.CompleteCompletion;
+import io.modelcontextprotocol.spec.McpSchema.ErrorCodes;
import io.modelcontextprotocol.spec.McpSchema.LoggingLevel;
import io.modelcontextprotocol.spec.McpSchema.LoggingMessageNotification;
-import io.modelcontextprotocol.spec.McpSchema.ResourceTemplate;
+import io.modelcontextprotocol.spec.McpSchema.PromptReference;
+import io.modelcontextprotocol.spec.McpSchema.ResourceReference;
import io.modelcontextprotocol.spec.McpSchema.SetLevelRequest;
import io.modelcontextprotocol.spec.McpSchema.Tool;
import io.modelcontextprotocol.spec.McpServerSession;
import io.modelcontextprotocol.spec.McpServerTransportProvider;
+import io.modelcontextprotocol.spec.McpServerTransportProviderBase;
+import io.modelcontextprotocol.spec.McpStreamableServerTransportProvider;
import io.modelcontextprotocol.util.Assert;
-import io.modelcontextprotocol.util.DeafaultMcpUriTemplateManagerFactory;
+import io.modelcontextprotocol.util.DefaultMcpUriTemplateManagerFactory;
import io.modelcontextprotocol.util.McpUriTemplateManagerFactory;
import io.modelcontextprotocol.util.Utils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
+import static io.modelcontextprotocol.spec.McpError.RESOURCE_NOT_FOUND;
+
/**
* The Model Context Protocol (MCP) server implementation that provides asynchronous
* communication using Project Reactor's Mono and Flux types.
@@ -91,7 +93,7 @@ public class McpAsyncServer {
private final McpServerTransportProviderBase mcpTransportProvider;
- private final ObjectMapper objectMapper;
+ private final McpJsonMapper jsonMapper;
private final JsonSchemaValidator jsonSchemaValidator;
@@ -103,10 +105,10 @@ public class McpAsyncServer {
private final CopyOnWriteArrayList tools = new CopyOnWriteArrayList<>();
- private final CopyOnWriteArrayList resourceTemplates = new CopyOnWriteArrayList<>();
-
private final ConcurrentHashMap resources = new ConcurrentHashMap<>();
+ private final ConcurrentHashMap resourceTemplates = new ConcurrentHashMap<>();
+
private final ConcurrentHashMap prompts = new ConcurrentHashMap<>();
// FIXME: this field is deprecated and should be remvoed together with the
@@ -117,26 +119,26 @@ public class McpAsyncServer {
private List protocolVersions;
- private McpUriTemplateManagerFactory uriTemplateManagerFactory = new DeafaultMcpUriTemplateManagerFactory();
+ private McpUriTemplateManagerFactory uriTemplateManagerFactory = new DefaultMcpUriTemplateManagerFactory();
/**
* Create a new McpAsyncServer with the given transport provider and capabilities.
* @param mcpTransportProvider The transport layer implementation for MCP
* communication.
* @param features The MCP server supported features.
- * @param objectMapper The ObjectMapper to use for JSON serialization/deserialization
+ * @param jsonMapper The JsonMapper to use for JSON serialization/deserialization
*/
- McpAsyncServer(McpServerTransportProvider mcpTransportProvider, ObjectMapper objectMapper,
+ McpAsyncServer(McpServerTransportProvider mcpTransportProvider, McpJsonMapper jsonMapper,
McpServerFeatures.Async features, Duration requestTimeout,
McpUriTemplateManagerFactory uriTemplateManagerFactory, JsonSchemaValidator jsonSchemaValidator) {
this.mcpTransportProvider = mcpTransportProvider;
- this.objectMapper = objectMapper;
+ this.jsonMapper = jsonMapper;
this.serverInfo = features.serverInfo();
this.serverCapabilities = features.serverCapabilities().mutate().logging().build();
this.instructions = features.instructions();
this.tools.addAll(withStructuredOutputHandling(jsonSchemaValidator, features.tools()));
this.resources.putAll(features.resources());
- this.resourceTemplates.addAll(features.resourceTemplates());
+ this.resourceTemplates.putAll(features.resourceTemplates());
this.prompts.putAll(features.prompts());
this.completions.putAll(features.completions());
this.uriTemplateManagerFactory = uriTemplateManagerFactory;
@@ -151,17 +153,17 @@ public class McpAsyncServer {
requestTimeout, transport, this::asyncInitializeRequestHandler, requestHandlers, notificationHandlers));
}
- McpAsyncServer(McpStreamableServerTransportProvider mcpTransportProvider, ObjectMapper objectMapper,
+ McpAsyncServer(McpStreamableServerTransportProvider mcpTransportProvider, McpJsonMapper jsonMapper,
McpServerFeatures.Async features, Duration requestTimeout,
McpUriTemplateManagerFactory uriTemplateManagerFactory, JsonSchemaValidator jsonSchemaValidator) {
this.mcpTransportProvider = mcpTransportProvider;
- this.objectMapper = objectMapper;
+ this.jsonMapper = jsonMapper;
this.serverInfo = features.serverInfo();
this.serverCapabilities = features.serverCapabilities().mutate().logging().build();
this.instructions = features.instructions();
this.tools.addAll(withStructuredOutputHandling(jsonSchemaValidator, features.tools()));
this.resources.putAll(features.resources());
- this.resourceTemplates.addAll(features.resourceTemplates());
+ this.resourceTemplates.putAll(features.resourceTemplates());
this.prompts.putAll(features.prompts());
this.completions.putAll(features.completions());
this.uriTemplateManagerFactory = uriTemplateManagerFactory;
@@ -319,25 +321,24 @@ private McpNotificationHandler asyncRootsListChangedNotificationHandler(
*/
public Mono addTool(McpServerFeatures.AsyncToolSpecification toolSpecification) {
if (toolSpecification == null) {
- return Mono.error(new McpError("Tool specification must not be null"));
+ return Mono.error(new IllegalArgumentException("Tool specification must not be null"));
}
if (toolSpecification.tool() == null) {
- return Mono.error(new McpError("Tool must not be null"));
+ return Mono.error(new IllegalArgumentException("Tool must not be null"));
}
- if (toolSpecification.call() == null && toolSpecification.callHandler() == null) {
- return Mono.error(new McpError("Tool call handler must not be null"));
+ if (toolSpecification.callHandler() == null) {
+ return Mono.error(new IllegalArgumentException("Tool call handler must not be null"));
}
if (this.serverCapabilities.tools() == null) {
- return Mono.error(new McpError("Server must be configured with tool capabilities"));
+ return Mono.error(new IllegalStateException("Server must be configured with tool capabilities"));
}
var wrappedToolSpecification = withStructuredOutputHandling(this.jsonSchemaValidator, toolSpecification);
return Mono.defer(() -> {
- // Check for duplicate tool names
- if (this.tools.stream().anyMatch(th -> th.tool().name().equals(wrappedToolSpecification.tool().name()))) {
- return Mono.error(
- new McpError("Tool with name '" + wrappedToolSpecification.tool().name() + "' already exists"));
+ // Remove tools with duplicate tool names first
+ if (this.tools.removeIf(th -> th.tool().name().equals(wrappedToolSpecification.tool().name()))) {
+ logger.warn("Replace existing Tool with name '{}'", wrappedToolSpecification.tool().name());
}
this.tools.add(wrappedToolSpecification);
@@ -376,6 +377,11 @@ public Mono apply(McpAsyncServerExchange exchange, McpSchema.Cal
return this.delegateCallToolResult.apply(exchange, request).map(result -> {
+ if (Boolean.TRUE.equals(result.isError())) {
+ // If the tool call resulted in an error, skip further validation
+ return result;
+ }
+
if (outputSchema == null) {
if (result.structuredContent() != null) {
logger.warn(
@@ -391,11 +397,12 @@ public Mono apply(McpAsyncServerExchange exchange, McpSchema.Cal
// results that conform to this schema.
// https://modelcontextprotocol.io/specification/2025-06-18/server/tools#output-schema
if (result.structuredContent() == null) {
- logger.warn(
- "Response missing structured content which is expected when calling tool with non-empty outputSchema");
- return new CallToolResult(
- "Response missing structured content which is expected when calling tool with non-empty outputSchema",
- true);
+ String content = "Response missing structured content which is expected when calling tool with non-empty outputSchema";
+ logger.warn(content);
+ return CallToolResult.builder()
+ .content(List.of(new McpSchema.TextContent(content)))
+ .isError(true)
+ .build();
}
// Validate the result against the output schema
@@ -403,7 +410,10 @@ public Mono apply(McpAsyncServerExchange exchange, McpSchema.Cal
if (!validation.valid()) {
logger.warn("Tool call result validation failed: {}", validation.errorMessage());
- return new CallToolResult(validation.errorMessage(), true);
+ return CallToolResult.builder()
+ .content(List.of(new McpSchema.TextContent(validation.errorMessage())))
+ .isError(true)
+ .build();
}
if (Utils.isEmpty(result.content())) {
@@ -413,8 +423,11 @@ public Mono apply(McpAsyncServerExchange exchange, McpSchema.Cal
// TextContent block.)
// https://modelcontextprotocol.io/specification/2025-06-18/server/tools#structured-content
- return new CallToolResult(List.of(new McpSchema.TextContent(validation.jsonStructuredOutput())),
- result.isError(), result.structuredContent());
+ return CallToolResult.builder()
+ .content(List.of(new McpSchema.TextContent(validation.jsonStructuredOutput())))
+ .isError(result.isError())
+ .structuredContent(result.structuredContent())
+ .build();
}
return result;
@@ -453,6 +466,14 @@ private static McpServerFeatures.AsyncToolSpecification withStructuredOutputHand
.build();
}
+ /**
+ * List all registered tools.
+ * @return A Flux stream of all registered tools
+ */
+ public Flux listTools() {
+ return Flux.fromIterable(this.tools).map(McpServerFeatures.AsyncToolSpecification::tool);
+ }
+
/**
* Remove a tool handler at runtime.
* @param toolName The name of the tool handler to remove
@@ -460,23 +481,25 @@ private static McpServerFeatures.AsyncToolSpecification withStructuredOutputHand
*/
public Mono removeTool(String toolName) {
if (toolName == null) {
- return Mono.error(new McpError("Tool name must not be null"));
+ return Mono.error(new IllegalArgumentException("Tool name must not be null"));
}
if (this.serverCapabilities.tools() == null) {
- return Mono.error(new McpError("Server must be configured with tool capabilities"));
+ return Mono.error(new IllegalStateException("Server must be configured with tool capabilities"));
}
return Mono.defer(() -> {
- boolean removed = this.tools
- .removeIf(toolSpecification -> toolSpecification.tool().name().equals(toolName));
- if (removed) {
+ if (this.tools.removeIf(toolSpecification -> toolSpecification.tool().name().equals(toolName))) {
+
logger.debug("Removed tool handler: {}", toolName);
if (this.serverCapabilities.tools().listChanged()) {
return notifyToolsListChanged();
}
- return Mono.empty();
}
- return Mono.error(new McpError("Tool with name '" + toolName + "' not found"));
+ else {
+ logger.warn("Ignore as a Tool with name '{}' not found", toolName);
+ }
+
+ return Mono.empty();
});
}
@@ -498,8 +521,8 @@ private McpRequestHandler toolsListRequestHandler() {
private McpRequestHandler toolsCallRequestHandler() {
return (exchange, params) -> {
- McpSchema.CallToolRequest callToolRequest = objectMapper.convertValue(params,
- new TypeReference() {
+ McpSchema.CallToolRequest callToolRequest = jsonMapper.convertValue(params,
+ new TypeRef() {
});
Optional toolSpecification = this.tools.stream()
@@ -507,11 +530,13 @@ private McpRequestHandler toolsCallRequestHandler() {
.findAny();
if (toolSpecification.isEmpty()) {
- return Mono.error(new McpError("Tool not found: " + callToolRequest.name()));
+ return Mono.error(McpError.builder(McpSchema.ErrorCodes.INVALID_PARAMS)
+ .message("Unknown tool: invalid_tool_name")
+ .data("Tool not found: " + callToolRequest.name())
+ .build());
}
- return toolSpecification.map(tool -> Mono.defer(() -> tool.callHandler().apply(exchange, callToolRequest)))
- .orElse(Mono.error(new McpError("Tool not found: " + callToolRequest.name())));
+ return toolSpecification.get().callHandler().apply(exchange, callToolRequest);
};
}
@@ -526,19 +551,22 @@ private McpRequestHandler toolsCallRequestHandler() {
*/
public Mono addResource(McpServerFeatures.AsyncResourceSpecification resourceSpecification) {
if (resourceSpecification == null || resourceSpecification.resource() == null) {
- return Mono.error(new McpError("Resource must not be null"));
+ return Mono.error(new IllegalArgumentException("Resource must not be null"));
}
if (this.serverCapabilities.resources() == null) {
- return Mono.error(new McpError("Server must be configured with resource capabilities"));
+ return Mono.error(new IllegalStateException(
+ "Server must be configured with resource capabilities to allow adding resources"));
}
return Mono.defer(() -> {
- if (this.resources.putIfAbsent(resourceSpecification.resource().uri(), resourceSpecification) != null) {
- return Mono.error(new McpError(
- "Resource with URI '" + resourceSpecification.resource().uri() + "' already exists"));
+ var previous = this.resources.put(resourceSpecification.resource().uri(), resourceSpecification);
+ if (previous != null) {
+ logger.warn("Replace existing Resource with URI '{}'", resourceSpecification.resource().uri());
+ }
+ else {
+ logger.debug("Added resource handler: {}", resourceSpecification.resource().uri());
}
- logger.debug("Added resource handler: {}", resourceSpecification.resource().uri());
if (this.serverCapabilities.resources().listChanged()) {
return notifyResourcesListChanged();
}
@@ -546,6 +574,14 @@ public Mono addResource(McpServerFeatures.AsyncResourceSpecification resou
});
}
+ /**
+ * List all registered resources.
+ * @return A Flux stream of all registered resources
+ */
+ public Flux listResources() {
+ return Flux.fromIterable(this.resources.values()).map(McpServerFeatures.AsyncResourceSpecification::resource);
+ }
+
/**
* Remove a resource handler at runtime.
* @param resourceUri The URI of the resource handler to remove
@@ -553,10 +589,11 @@ public Mono addResource(McpServerFeatures.AsyncResourceSpecification resou
*/
public Mono removeResource(String resourceUri) {
if (resourceUri == null) {
- return Mono.error(new McpError("Resource URI must not be null"));
+ return Mono.error(new IllegalArgumentException("Resource URI must not be null"));
}
if (this.serverCapabilities.resources() == null) {
- return Mono.error(new McpError("Server must be configured with resource capabilities"));
+ return Mono.error(new IllegalStateException(
+ "Server must be configured with resource capabilities to allow removing resources"));
}
return Mono.defer(() -> {
@@ -568,7 +605,74 @@ public Mono removeResource(String resourceUri) {
}
return Mono.empty();
}
- return Mono.error(new McpError("Resource with URI '" + resourceUri + "' not found"));
+ else {
+ logger.warn("Ignore as a Resource with URI '{}' not found", resourceUri);
+ }
+ return Mono.empty();
+ });
+ }
+
+ /**
+ * Add a new resource template at runtime.
+ * @param resourceTemplateSpecification The resource template to add
+ * @return Mono that completes when clients have been notified of the change
+ */
+ public Mono