Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 18 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ on:

jobs:
build:
name: Build branch
name: Build and Test
runs-on: ubuntu-latest
steps:
- name: Checkout source code
Expand All @@ -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
5 changes: 4 additions & 1 deletion .github/workflows/maven-central-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
3 changes: 3 additions & 0 deletions .github/workflows/publish-snapshot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }}
Expand Down
23 changes: 17 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,11 +83,11 @@ The following sections explain what we chose, why it made sense, and how the cho

### 1. JSON Serialization

* **SDK Choice**: Jackson for JSON serialization and deserialization, behind an SDK abstraction (`mcp-json`)
* **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 zero-dependency abstraction (`mcp-json`). Jackson is shipped as the default implementation (`mcp-jackson2`), but alternatives can be plugged in.
* **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.

Expand Down Expand Up @@ -168,15 +168,26 @@ MCP supports both clients (applications consuming MCP servers) and servers (appl

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)
* `mcp-json` – JSON abstraction
* `mcp-jackson2` – Jackson implementation of JSON binding
* `mcp` – Convenience bundle (core + Jackson)
* `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
* `mcp-spring` – Spring integrations (WebClient, WebFlux, WebMVC)

For example, a minimal adopter may depend only on `mcp` (core + Jackson), while a Spring-based application can use `mcp-spring` 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:
Expand Down
7 changes: 0 additions & 7 deletions mcp-bom/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -40,13 +40,6 @@
<version>${project.version}</version>
</dependency>

<!-- MCP JSON -->
<dependency>
<groupId>io.modelcontextprotocol.sdk</groupId>
<artifactId>mcp-json</artifactId>
<version>${project.version}</version>
</dependency>

<!-- MCP JSON Jackson -->
<dependency>
<groupId>io.modelcontextprotocol.sdk</groupId>
Expand Down
59 changes: 1 addition & 58 deletions mcp-core/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
Automatic-Module-Name: ${project.groupId}.${project.artifactId}
Import-Package: jakarta.*;resolution:=optional, \
*;
Service-Component: OSGI-INF/io.modelcontextprotocol.json.McpJsonDefaults.xml
Export-Package: io.modelcontextprotocol.*;version="${version}";-noimport:=true
-noimportjava: true;
-nouses: true;
Expand All @@ -65,11 +66,6 @@
</build>

<dependencies>
<dependency>
<groupId>io.modelcontextprotocol.sdk</groupId>
<artifactId>mcp-json</artifactId>
<version>0.18.0-SNAPSHOT</version>
</dependency>

<dependency>
<groupId>org.slf4j</groupId>
Expand Down Expand Up @@ -97,45 +93,6 @@
<scope>provided</scope>
</dependency>

<!-- Test dependencies -->
<dependency>
<groupId>io.modelcontextprotocol.sdk</groupId>
<artifactId>mcp-json-jackson3</artifactId>
<version>0.18.0-SNAPSHOT</version>
<scope>test</scope>
</dependency>

<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>${springframework.version}</version>
<scope>test</scope>
</dependency>


<dependency>
<groupId>io.projectreactor.netty</groupId>
<artifactId>reactor-netty-http</artifactId>
<scope>test</scope>
</dependency>

<!-- The Spring Context is required due to the reactor-netty connector being dependent on
the Spring Lifecycle, as discussed here:
https://github.com/spring-projects/spring-framework/issues/31180 -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>${springframework.version}</version>
<scope>test</scope>
</dependency>

<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>${springframework.version}</version>
<scope>test</scope>
</dependency>

<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
Expand Down Expand Up @@ -201,20 +158,6 @@
<scope>test</scope>
</dependency>

<!-- Tomcat dependencies for testing -->
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-core</artifactId>
<version>${tomcat.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-websocket</artifactId>
<version>${tomcat.version}</version>
<scope>test</scope>
</dependency>

<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>toxiproxy</artifactId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
package io.modelcontextprotocol.client;

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;
Expand Down Expand Up @@ -491,9 +492,12 @@ public McpSyncClient build() {

McpClientFeatures.Async asyncFeatures = McpClientFeatures.Async.fromSync(syncFeatures);

return new McpSyncClient(new McpAsyncClient(transport, this.requestTimeout, this.initializationTimeout,
jsonSchemaValidator != null ? jsonSchemaValidator : JsonSchemaValidator.getDefault(),
asyncFeatures), this.contextProvider);
return new McpSyncClient(
new McpAsyncClient(transport, this.requestTimeout, this.initializationTimeout,
jsonSchemaValidator != null ? jsonSchemaValidator
: McpJsonDefaults.getDefaultJsonSchemaValidator(),
asyncFeatures),
this.contextProvider);
}

}
Expand Down Expand Up @@ -826,7 +830,7 @@ public AsyncSpec enableCallToolSchemaCaching(boolean enableCallToolSchemaCaching
*/
public McpAsyncClient build() {
var jsonSchemaValidator = (this.jsonSchemaValidator != null) ? this.jsonSchemaValidator
: JsonSchemaValidator.getDefault();
: McpJsonDefaults.getDefaultJsonSchemaValidator();
return new McpAsyncClient(this.transport, this.requestTimeout, this.initializationTimeout,
jsonSchemaValidator,
new McpClientFeatures.Async(this.clientInfo, this.capabilities, this.roots,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
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;
Expand Down Expand Up @@ -327,7 +328,7 @@ public Builder connectTimeout(Duration connectTimeout) {
public HttpClientSseClientTransport build() {
HttpClient httpClient = this.clientBuilder.connectTimeout(this.connectTimeout).build();
return new HttpClientSseClientTransport(httpClient, requestBuilder, baseUri, sseEndpoint,
jsonMapper == null ? McpJsonMapper.getDefault() : jsonMapper, httpRequestCustomizer);
jsonMapper == null ? McpJsonDefaults.getDefaultMcpJsonMapper() : jsonMapper, httpRequestCustomizer);
}

}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
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;
Expand Down Expand Up @@ -842,9 +843,10 @@ public Builder supportedProtocolVersions(List<String> supportedProtocolVersions)
*/
public HttpClientStreamableHttpTransport build() {
HttpClient httpClient = this.clientBuilder.connectTimeout(this.connectTimeout).build();
return new HttpClientStreamableHttpTransport(jsonMapper == null ? McpJsonMapper.getDefault() : jsonMapper,
httpClient, requestBuilder, baseUri, endpoint, resumableStreams, openConnectionOnStartup,
httpRequestCustomizer, supportedProtocolVersions);
return new HttpClientStreamableHttpTransport(
jsonMapper == null ? McpJsonDefaults.getDefaultMcpJsonMapper() : jsonMapper, httpClient,
requestBuilder, baseUri, endpoint, resumableStreams, openConnectionOnStartup, httpRequestCustomizer,
supportedProtocolVersions);
}

}
Expand Down
Original file line number Diff line number Diff line change
@@ -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 McpJsonMapper and to the
* default JsonSchemaValidator instances via the static methods: getDefaultMcpJsonMapper
* and getDefaultJsonSchemaValidator.
* <p>
* The initialization of (singleton) instances of this class is different in non-OSGi
* environments and OSGi environments. Specifically, in non-OSGi environments The
* 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 McpServer instance. At that first time, the
* mcpMapperServiceLoader and mcpValidatorServiceLoader will be null, and the
* McpJsonDefaults constructor will be called, creating/initializing the
* mcpMapperServiceLoader and the mcpValidatorServiceLoader...which will then be used to
* call the ServiceLoader.load method.
* <p>
* In OSGi environments, upon bundle activation SCR will create a new (singleton) instance
* of McpJsonDefaults (via the constructor), and then inject suppliers via the
* setMcpJsonMapperSupplier and 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 OSGi-INF directory of mcp-core (this project/jar/bundle), and the jsonmapper
* and jsonschemvalidator provider jars/bundles (e.g. mcp-json-jackson2, 3, or others).
*/
public class McpJsonDefaults {

protected static McpServiceLoader<McpJsonMapperSupplier, McpJsonMapper> mcpMapperServiceLoader;

protected static McpServiceLoader<JsonSchemaValidatorSupplier, JsonSchemaValidator> mcpValidatorServiceLoader;

public McpJsonDefaults() {
mcpMapperServiceLoader = new McpServiceLoader<McpJsonMapperSupplier, McpJsonMapper>(
McpJsonMapperSupplier.class);
mcpValidatorServiceLoader = new McpServiceLoader<JsonSchemaValidatorSupplier, JsonSchemaValidator>(
JsonSchemaValidatorSupplier.class);
}

void setMcpJsonMapperSupplier(McpJsonMapperSupplier supplier) {
mcpMapperServiceLoader.setSupplier(supplier);
}

void unsetMcpJsonMapperSupplier(McpJsonMapperSupplier supplier) {
mcpMapperServiceLoader.unsetSupplier(supplier);
}

public synchronized static McpJsonMapper getDefaultMcpJsonMapper() {
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 getDefaultJsonSchemaValidator() {
if (mcpValidatorServiceLoader == null) {
new McpJsonDefaults();
}
return mcpValidatorServiceLoader.getDefault();
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -87,24 +87,4 @@ public interface McpJsonMapper {
*/
byte[] writeValueAsBytes(Object value) throws IOException;

/**
* Returns the default {@link McpJsonMapper}.
* @return The default {@link McpJsonMapper}
* @throws IllegalStateException If no {@link McpJsonMapper} implementation exists on
* the classpath.
*/
static McpJsonMapper getDefault() {
return McpJsonInternal.getDefaultMapper();
}

/**
* Creates a new default {@link McpJsonMapper}.
* @return The default {@link McpJsonMapper}
* @throws IllegalStateException If no {@link McpJsonMapper} implementation exists on
* the classpath.
*/
static McpJsonMapper createDefault() {
return McpJsonInternal.createDefaultMapper();
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@

/**
* Captures generic type information at runtime for parameterized JSON (de)serialization.
* Usage: TypeRef&lt;List&lt;Foo&gt;&gt; ref = new TypeRef&lt;&gt;(){};
* Usage: TypeRef<List<Foo>> ref = new TypeRef<>(){};
*/
public abstract class TypeRef<T> {

Expand Down
Loading