feat: Introduce McpJsonMapper interface to decouple from Jackson (#543)
This pull request creates two modules, `mcp-json` and `mcp-json-jackson`. It removes the `com.fasterxml.jackson.core:jackson-databind` and `com.networknt:json-schema-validator` dependencies from the `mcp` module. The `mcp` module now only depends on `com.fasterxml.jackson.core:jackson-annotations`. To use Jackson, you have to add `mcp-jackson` to your dependencies in addition to `mcp`. I added the dependency `mcp-jackson` to both `mcp-spring-mvc` and `mcp-spring-webflux` to avoid a breaking change in those modules. It provides two [SPI](https://docs.oracle.com/javase/tutorial/sound/SPI-intro.html) `JsonSchemaValidatorSupplier` and `JacksonJsonSchemaValidatorSupplier` to allow easy replacement for consumers who don't want to use Jackson. This pull request also ensures no `McpJsonMapper` is instantiated if one is provided via a builder method. Only if the builders don't receive a `McpJsonMapper` mapper, one is instantiated in the `build` method of the builder. The logic behind this is to allow frameworks to provide a `McpJsonMapper` mapper singleton implementation and feed it to the builders without paying the price of instantiating `McpJsonMappers`, which will not be used. The goal is to be able to use the `ObjectMapper` singleton of an application also for the MCP code. Signed-off-by: Christian Tzolov <christian.tzolov@broadcom.com>
This commit is contained in:
parent
d8959ef854
commit
80d0ad82a6
118 changed files with 2043 additions and 1421 deletions
|
|
@ -33,6 +33,20 @@
|
||||||
<version>${project.version}</version>
|
<version>${project.version}</version>
|
||||||
</dependency>
|
</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>
|
||||||
|
<artifactId>mcp-json-jackson2</artifactId>
|
||||||
|
<version>${project.version}</version>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
<!-- MCP Test -->
|
<!-- MCP Test -->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>io.modelcontextprotocol.sdk</groupId>
|
<groupId>io.modelcontextprotocol.sdk</groupId>
|
||||||
|
|
|
||||||
53
mcp-json-jackson2/pom.xml
Normal file
53
mcp-json-jackson2/pom.xml
Normal file
|
|
@ -0,0 +1,53 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||||
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
<parent>
|
||||||
|
<groupId>io.modelcontextprotocol.sdk</groupId>
|
||||||
|
<artifactId>mcp-parent</artifactId>
|
||||||
|
<version>0.13.0-SNAPSHOT</version>
|
||||||
|
</parent>
|
||||||
|
<artifactId>mcp-json-jackson2</artifactId>
|
||||||
|
<packaging>jar</packaging>
|
||||||
|
<name>Java MCP SDK JSON Jackson</name>
|
||||||
|
<description>Java MCP SDK JSON implementation based on Jackson</description>
|
||||||
|
<url>https://github.com/modelcontextprotocol/java-sdk</url>
|
||||||
|
<scm>
|
||||||
|
<url>https://github.com/modelcontextprotocol/java-sdk</url>
|
||||||
|
<connection>git://github.com/modelcontextprotocol/java-sdk.git</connection>
|
||||||
|
<developerConnection>git@github.com/modelcontextprotocol/java-sdk.git</developerConnection>
|
||||||
|
</scm>
|
||||||
|
<build>
|
||||||
|
<plugins>
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.apache.maven.plugins</groupId>
|
||||||
|
<artifactId>maven-jar-plugin</artifactId>
|
||||||
|
<configuration>
|
||||||
|
<archive>
|
||||||
|
<manifest>
|
||||||
|
<addDefaultImplementationEntries>true</addDefaultImplementationEntries>
|
||||||
|
</manifest>
|
||||||
|
</archive>
|
||||||
|
</configuration>
|
||||||
|
</plugin>
|
||||||
|
</plugins>
|
||||||
|
</build>
|
||||||
|
<dependencies>
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.modelcontextprotocol.sdk</groupId>
|
||||||
|
<artifactId>mcp-json</artifactId>
|
||||||
|
<version>0.13.0-SNAPSHOT</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.fasterxml.jackson.core</groupId>
|
||||||
|
<artifactId>jackson-databind</artifactId>
|
||||||
|
<version>${jackson.version}</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.networknt</groupId>
|
||||||
|
<artifactId>json-schema-validator</artifactId>
|
||||||
|
<version>${json-schema-validator.version}</version>
|
||||||
|
</dependency>
|
||||||
|
</dependencies>
|
||||||
|
</project>
|
||||||
|
|
@ -0,0 +1,87 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2025 - 2025 the original author or authors.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.modelcontextprotocol.json.jackson;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.JavaType;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import io.modelcontextprotocol.json.McpJsonMapper;
|
||||||
|
import io.modelcontextprotocol.json.TypeRef;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Jackson-based implementation of JsonMapper. Wraps a Jackson ObjectMapper but keeps the
|
||||||
|
* SDK decoupled from Jackson at the API level.
|
||||||
|
*/
|
||||||
|
public final class JacksonMcpJsonMapper implements McpJsonMapper {
|
||||||
|
|
||||||
|
private final ObjectMapper objectMapper;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs a new JacksonMcpJsonMapper instance with the given ObjectMapper.
|
||||||
|
* @param objectMapper the ObjectMapper to be used for JSON serialization and
|
||||||
|
* deserialization. Must not be null.
|
||||||
|
* @throws IllegalArgumentException if the provided ObjectMapper is null.
|
||||||
|
*/
|
||||||
|
public JacksonMcpJsonMapper(ObjectMapper objectMapper) {
|
||||||
|
if (objectMapper == null) {
|
||||||
|
throw new IllegalArgumentException("ObjectMapper must not be null");
|
||||||
|
}
|
||||||
|
this.objectMapper = objectMapper;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the underlying Jackson {@link ObjectMapper} used for JSON serialization and
|
||||||
|
* deserialization.
|
||||||
|
* @return the ObjectMapper instance
|
||||||
|
*/
|
||||||
|
public ObjectMapper getObjectMapper() {
|
||||||
|
return objectMapper;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public <T> T readValue(String content, Class<T> type) throws IOException {
|
||||||
|
return objectMapper.readValue(content, type);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public <T> T readValue(byte[] content, Class<T> type) throws IOException {
|
||||||
|
return objectMapper.readValue(content, type);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public <T> T readValue(String content, TypeRef<T> type) throws IOException {
|
||||||
|
JavaType javaType = objectMapper.getTypeFactory().constructType(type.getType());
|
||||||
|
return objectMapper.readValue(content, javaType);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public <T> T readValue(byte[] content, TypeRef<T> type) throws IOException {
|
||||||
|
JavaType javaType = objectMapper.getTypeFactory().constructType(type.getType());
|
||||||
|
return objectMapper.readValue(content, javaType);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public <T> T convertValue(Object fromValue, Class<T> type) {
|
||||||
|
return objectMapper.convertValue(fromValue, type);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public <T> T convertValue(Object fromValue, TypeRef<T> type) {
|
||||||
|
JavaType javaType = objectMapper.getTypeFactory().constructType(type.getType());
|
||||||
|
return objectMapper.convertValue(fromValue, javaType);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String writeValueAsString(Object value) throws IOException {
|
||||||
|
return objectMapper.writeValueAsString(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public byte[] writeValueAsBytes(Object value) throws IOException {
|
||||||
|
return objectMapper.writeValueAsBytes(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,32 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2025 - 2025 the original author or authors.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.modelcontextprotocol.json.jackson;
|
||||||
|
|
||||||
|
import io.modelcontextprotocol.json.McpJsonMapper;
|
||||||
|
import io.modelcontextprotocol.json.McpJsonMapperSupplier;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A supplier of {@link McpJsonMapper} instances that uses the Jackson library for JSON
|
||||||
|
* serialization and deserialization.
|
||||||
|
* <p>
|
||||||
|
* This implementation provides a {@link McpJsonMapper} backed by a Jackson
|
||||||
|
* {@link com.fasterxml.jackson.databind.ObjectMapper}.
|
||||||
|
*/
|
||||||
|
public class JacksonMcpJsonMapperSupplier implements McpJsonMapperSupplier {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a new instance of {@link McpJsonMapper} that uses the Jackson library for
|
||||||
|
* JSON serialization and deserialization.
|
||||||
|
* <p>
|
||||||
|
* The returned {@link McpJsonMapper} is backed by a new instance of
|
||||||
|
* {@link com.fasterxml.jackson.databind.ObjectMapper}.
|
||||||
|
* @return a new {@link McpJsonMapper} instance
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public McpJsonMapper get() {
|
||||||
|
return new JacksonMcpJsonMapper(new com.fasterxml.jackson.databind.ObjectMapper());
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,172 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2024-2024 the original author or authors.
|
||||||
|
*/
|
||||||
|
package io.modelcontextprotocol.json.schema.jackson;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
|
||||||
|
import io.modelcontextprotocol.json.schema.JsonSchemaValidator;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||||
|
import com.fasterxml.jackson.databind.JsonNode;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||||
|
import com.networknt.schema.JsonSchema;
|
||||||
|
import com.networknt.schema.JsonSchemaFactory;
|
||||||
|
import com.networknt.schema.SpecVersion;
|
||||||
|
import com.networknt.schema.ValidationMessage;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Default implementation of the {@link JsonSchemaValidator} interface. This class
|
||||||
|
* provides methods to validate structured content against a JSON schema. It uses the
|
||||||
|
* NetworkNT JSON Schema Validator library for validation.
|
||||||
|
*
|
||||||
|
* @author Christian Tzolov
|
||||||
|
*/
|
||||||
|
public class DefaultJsonSchemaValidator implements JsonSchemaValidator {
|
||||||
|
|
||||||
|
private static final Logger logger = LoggerFactory.getLogger(DefaultJsonSchemaValidator.class);
|
||||||
|
|
||||||
|
private final ObjectMapper objectMapper;
|
||||||
|
|
||||||
|
private final JsonSchemaFactory schemaFactory;
|
||||||
|
|
||||||
|
// TODO: Implement a strategy to purge the cache (TTL, size limit, etc.)
|
||||||
|
private final ConcurrentHashMap<String, JsonSchema> schemaCache;
|
||||||
|
|
||||||
|
public DefaultJsonSchemaValidator() {
|
||||||
|
this(new ObjectMapper());
|
||||||
|
}
|
||||||
|
|
||||||
|
public DefaultJsonSchemaValidator(ObjectMapper objectMapper) {
|
||||||
|
this.objectMapper = objectMapper;
|
||||||
|
this.schemaFactory = JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V202012);
|
||||||
|
this.schemaCache = new ConcurrentHashMap<>();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ValidationResponse validate(Map<String, Object> schema, Object structuredContent) {
|
||||||
|
|
||||||
|
if (schema == null) {
|
||||||
|
throw new IllegalArgumentException("Schema must not be null");
|
||||||
|
}
|
||||||
|
if (structuredContent == null) {
|
||||||
|
throw new IllegalArgumentException("Structured content must not be null");
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
|
||||||
|
JsonNode jsonStructuredOutput = this.objectMapper.valueToTree(structuredContent);
|
||||||
|
|
||||||
|
Set<ValidationMessage> validationResult = this.getOrCreateJsonSchema(schema).validate(jsonStructuredOutput);
|
||||||
|
|
||||||
|
// Check if validation passed
|
||||||
|
if (!validationResult.isEmpty()) {
|
||||||
|
return ValidationResponse
|
||||||
|
.asInvalid("Validation failed: structuredContent does not match tool outputSchema. "
|
||||||
|
+ "Validation errors: " + validationResult);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ValidationResponse.asValid(jsonStructuredOutput.toString());
|
||||||
|
|
||||||
|
}
|
||||||
|
catch (JsonProcessingException e) {
|
||||||
|
logger.error("Failed to validate CallToolResult: Error parsing schema: {}", e);
|
||||||
|
return ValidationResponse.asInvalid("Error parsing tool JSON Schema: " + e.getMessage());
|
||||||
|
}
|
||||||
|
catch (Exception e) {
|
||||||
|
logger.error("Failed to validate CallToolResult: Unexpected error: {}", e);
|
||||||
|
return ValidationResponse.asInvalid("Unexpected validation error: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets a cached JsonSchema or creates and caches a new one.
|
||||||
|
* @param schema the schema map to convert
|
||||||
|
* @return the compiled JsonSchema
|
||||||
|
* @throws JsonProcessingException if schema processing fails
|
||||||
|
*/
|
||||||
|
private JsonSchema getOrCreateJsonSchema(Map<String, Object> schema) throws JsonProcessingException {
|
||||||
|
// Generate cache key based on schema content
|
||||||
|
String cacheKey = this.generateCacheKey(schema);
|
||||||
|
|
||||||
|
// Try to get from cache first
|
||||||
|
JsonSchema cachedSchema = this.schemaCache.get(cacheKey);
|
||||||
|
if (cachedSchema != null) {
|
||||||
|
return cachedSchema;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create new schema if not in cache
|
||||||
|
JsonSchema newSchema = this.createJsonSchema(schema);
|
||||||
|
|
||||||
|
// Cache the schema
|
||||||
|
JsonSchema existingSchema = this.schemaCache.putIfAbsent(cacheKey, newSchema);
|
||||||
|
return existingSchema != null ? existingSchema : newSchema;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new JsonSchema from the given schema map.
|
||||||
|
* @param schema the schema map
|
||||||
|
* @return the compiled JsonSchema
|
||||||
|
* @throws JsonProcessingException if schema processing fails
|
||||||
|
*/
|
||||||
|
private JsonSchema createJsonSchema(Map<String, Object> schema) throws JsonProcessingException {
|
||||||
|
// Convert schema map directly to JsonNode (more efficient than string
|
||||||
|
// serialization)
|
||||||
|
JsonNode schemaNode = this.objectMapper.valueToTree(schema);
|
||||||
|
|
||||||
|
// Handle case where ObjectMapper might return null (e.g., in mocked scenarios)
|
||||||
|
if (schemaNode == null) {
|
||||||
|
throw new JsonProcessingException("Failed to convert schema to JsonNode") {
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle additionalProperties setting
|
||||||
|
if (schemaNode.isObject()) {
|
||||||
|
ObjectNode objectSchemaNode = (ObjectNode) schemaNode;
|
||||||
|
if (!objectSchemaNode.has("additionalProperties")) {
|
||||||
|
// Clone the node before modification to avoid mutating the original
|
||||||
|
objectSchemaNode = objectSchemaNode.deepCopy();
|
||||||
|
objectSchemaNode.put("additionalProperties", false);
|
||||||
|
schemaNode = objectSchemaNode;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.schemaFactory.getSchema(schemaNode);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates a cache key for the given schema map.
|
||||||
|
* @param schema the schema map
|
||||||
|
* @return a cache key string
|
||||||
|
*/
|
||||||
|
protected String generateCacheKey(Map<String, Object> schema) {
|
||||||
|
if (schema.containsKey("$id")) {
|
||||||
|
// Use the (optional) "$id" field as the cache key if present
|
||||||
|
return "" + schema.get("$id");
|
||||||
|
}
|
||||||
|
// Fall back to schema's hash code as a simple cache key
|
||||||
|
// For more sophisticated caching, could use content-based hashing
|
||||||
|
return String.valueOf(schema.hashCode());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clears the schema cache. Useful for testing or memory management.
|
||||||
|
*/
|
||||||
|
public void clearCache() {
|
||||||
|
this.schemaCache.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the current size of the schema cache.
|
||||||
|
* @return the number of cached schemas
|
||||||
|
*/
|
||||||
|
public int getCacheSize() {
|
||||||
|
return this.schemaCache.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,29 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2025 - 2025 the original author or authors.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.modelcontextprotocol.json.schema.jackson;
|
||||||
|
|
||||||
|
import io.modelcontextprotocol.json.schema.JsonSchemaValidator;
|
||||||
|
import io.modelcontextprotocol.json.schema.JsonSchemaValidatorSupplier;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A concrete implementation of {@link JsonSchemaValidatorSupplier} that provides a
|
||||||
|
* {@link JsonSchemaValidator} instance based on the Jackson library.
|
||||||
|
*
|
||||||
|
* @see JsonSchemaValidatorSupplier
|
||||||
|
* @see JsonSchemaValidator
|
||||||
|
*/
|
||||||
|
public class JacksonJsonSchemaValidatorSupplier implements JsonSchemaValidatorSupplier {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a new instance of {@link JsonSchemaValidator} that uses the Jackson library
|
||||||
|
* for JSON schema validation.
|
||||||
|
* @return A {@link JsonSchemaValidator} instance.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public JsonSchemaValidator get() {
|
||||||
|
return new DefaultJsonSchemaValidator();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
io.modelcontextprotocol.json.jackson.JacksonMcpJsonMapperSupplier
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
io.modelcontextprotocol.json.schema.jackson.JacksonJsonSchemaValidatorSupplier
|
||||||
39
mcp-json/pom.xml
Normal file
39
mcp-json/pom.xml
Normal file
|
|
@ -0,0 +1,39 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||||
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
<parent>
|
||||||
|
<groupId>io.modelcontextprotocol.sdk</groupId>
|
||||||
|
<artifactId>mcp-parent</artifactId>
|
||||||
|
<version>0.13.0-SNAPSHOT</version>
|
||||||
|
</parent>
|
||||||
|
<artifactId>mcp-json</artifactId>
|
||||||
|
<packaging>jar</packaging>
|
||||||
|
<name>Java MCP SDK JSON Support</name>
|
||||||
|
<description>Java MCP SDK JSON Support API</description>
|
||||||
|
<url>https://github.com/modelcontextprotocol/java-sdk</url>
|
||||||
|
<scm>
|
||||||
|
<url>https://github.com/modelcontextprotocol/java-sdk</url>
|
||||||
|
<connection>git://github.com/modelcontextprotocol/java-sdk.git</connection>
|
||||||
|
<developerConnection>git@github.com/modelcontextprotocol/java-sdk.git</developerConnection>
|
||||||
|
</scm>
|
||||||
|
<build>
|
||||||
|
<plugins>
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.apache.maven.plugins</groupId>
|
||||||
|
<artifactId>maven-jar-plugin</artifactId>
|
||||||
|
<configuration>
|
||||||
|
<archive>
|
||||||
|
<manifest>
|
||||||
|
<addDefaultImplementationEntries>true</addDefaultImplementationEntries>
|
||||||
|
</manifest>
|
||||||
|
</archive>
|
||||||
|
</configuration>
|
||||||
|
</plugin>
|
||||||
|
</plugins>
|
||||||
|
</build>
|
||||||
|
<dependencies>
|
||||||
|
|
||||||
|
</dependencies>
|
||||||
|
</project>
|
||||||
|
|
@ -0,0 +1,84 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2025 - 2025 the original author or authors.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.modelcontextprotocol.json;
|
||||||
|
|
||||||
|
import java.util.ServiceLoader;
|
||||||
|
import java.util.concurrent.atomic.AtomicReference;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utility class for creating a default {@link McpJsonMapper} instance. This class
|
||||||
|
* provides a single method to create a default mapper using the {@link ServiceLoader}
|
||||||
|
* mechanism.
|
||||||
|
*/
|
||||||
|
final class McpJsonInternal {
|
||||||
|
|
||||||
|
private static McpJsonMapper defaultJsonMapper = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the cached default {@link McpJsonMapper} instance. If the default mapper
|
||||||
|
* has not been created yet, it will be initialized using the
|
||||||
|
* {@link #createDefaultMapper()} method.
|
||||||
|
* @return the default {@link McpJsonMapper} instance
|
||||||
|
* @throws IllegalStateException if no default {@link McpJsonMapper} implementation is
|
||||||
|
* found
|
||||||
|
*/
|
||||||
|
static McpJsonMapper getDefaultMapper() {
|
||||||
|
if (defaultJsonMapper == null) {
|
||||||
|
defaultJsonMapper = McpJsonInternal.createDefaultMapper();
|
||||||
|
}
|
||||||
|
return defaultJsonMapper;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a default {@link McpJsonMapper} instance using the {@link ServiceLoader}
|
||||||
|
* mechanism. The default mapper is resolved by loading the first available
|
||||||
|
* {@link McpJsonMapperSupplier} implementation on the classpath.
|
||||||
|
* @return the default {@link McpJsonMapper} instance
|
||||||
|
* @throws IllegalStateException if no default {@link McpJsonMapper} implementation is
|
||||||
|
* found
|
||||||
|
*/
|
||||||
|
static McpJsonMapper createDefaultMapper() {
|
||||||
|
AtomicReference<IllegalStateException> ex = new AtomicReference<>();
|
||||||
|
return ServiceLoader.load(McpJsonMapperSupplier.class).stream().flatMap(p -> {
|
||||||
|
try {
|
||||||
|
McpJsonMapperSupplier supplier = p.get();
|
||||||
|
return Stream.ofNullable(supplier);
|
||||||
|
}
|
||||||
|
catch (Exception e) {
|
||||||
|
addException(ex, e);
|
||||||
|
return Stream.empty();
|
||||||
|
}
|
||||||
|
}).flatMap(jsonMapperSupplier -> {
|
||||||
|
try {
|
||||||
|
return Stream.ofNullable(jsonMapperSupplier.get());
|
||||||
|
}
|
||||||
|
catch (Exception e) {
|
||||||
|
addException(ex, e);
|
||||||
|
return Stream.empty();
|
||||||
|
}
|
||||||
|
}).findFirst().orElseThrow(() -> {
|
||||||
|
if (ex.get() != null) {
|
||||||
|
return ex.get();
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return new IllegalStateException("No default McpJsonMapper implementation found");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void addException(AtomicReference<IllegalStateException> ref, Exception toAdd) {
|
||||||
|
ref.updateAndGet(existing -> {
|
||||||
|
if (existing == null) {
|
||||||
|
return new IllegalStateException("Failed to initialize default McpJsonMapper", toAdd);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
existing.addSuppressed(toAdd);
|
||||||
|
return existing;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,110 @@
|
||||||
|
/*
|
||||||
|
* 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 <T> generic type
|
||||||
|
* @throws IOException on parse errors
|
||||||
|
*/
|
||||||
|
<T> T readValue(String content, Class<T> type) throws IOException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deserialize JSON bytes into a target type.
|
||||||
|
* @param content JSON as bytes
|
||||||
|
* @param type target class
|
||||||
|
* @return deserialized instance
|
||||||
|
* @param <T> generic type
|
||||||
|
* @throws IOException on parse errors
|
||||||
|
*/
|
||||||
|
<T> T readValue(byte[] content, Class<T> 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 <T> generic type
|
||||||
|
* @throws IOException on parse errors
|
||||||
|
*/
|
||||||
|
<T> T readValue(String content, TypeRef<T> 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 <T> generic type
|
||||||
|
* @throws IOException on parse errors
|
||||||
|
*/
|
||||||
|
<T> T readValue(byte[] content, TypeRef<T> 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 <T> generic type
|
||||||
|
*/
|
||||||
|
<T> T convertValue(Object fromValue, Class<T> type);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert a value to a given parameterized type.
|
||||||
|
* @param fromValue source value
|
||||||
|
* @param type target type reference
|
||||||
|
* @return converted value
|
||||||
|
* @param <T> generic type
|
||||||
|
*/
|
||||||
|
<T> T convertValue(Object fromValue, TypeRef<T> 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;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -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<McpJsonMapper> {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -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<List<Foo>> ref = new TypeRef<>(){};
|
||||||
|
*/
|
||||||
|
public abstract class TypeRef<T> {
|
||||||
|
|
||||||
|
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: <pre>
|
||||||
|
* TypeRef<List<Foo>> ref = new TypeRef<>(){};
|
||||||
|
* </pre>
|
||||||
|
* @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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,83 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2025 - 2025 the original author or authors.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.modelcontextprotocol.json.schema;
|
||||||
|
|
||||||
|
import java.util.ServiceLoader;
|
||||||
|
import java.util.concurrent.atomic.AtomicReference;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Internal utility class for creating a default {@link JsonSchemaValidator} instance.
|
||||||
|
* This class uses the {@link ServiceLoader} to discover and instantiate a
|
||||||
|
* {@link JsonSchemaValidatorSupplier} implementation.
|
||||||
|
*/
|
||||||
|
final class JsonSchemaInternal {
|
||||||
|
|
||||||
|
private static JsonSchemaValidator defaultValidator = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the default {@link JsonSchemaValidator} instance. If the default validator
|
||||||
|
* has not been initialized, it will be created using the {@link ServiceLoader} to
|
||||||
|
* discover and instantiate a {@link JsonSchemaValidatorSupplier} implementation.
|
||||||
|
* @return The default {@link JsonSchemaValidator} instance.
|
||||||
|
* @throws IllegalStateException If no {@link JsonSchemaValidatorSupplier}
|
||||||
|
* implementation exists on the classpath or if an error occurs during instantiation.
|
||||||
|
*/
|
||||||
|
static JsonSchemaValidator getDefaultValidator() {
|
||||||
|
if (defaultValidator == null) {
|
||||||
|
defaultValidator = JsonSchemaInternal.createDefaultValidator();
|
||||||
|
}
|
||||||
|
return defaultValidator;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a default {@link JsonSchemaValidator} instance by loading a
|
||||||
|
* {@link JsonSchemaValidatorSupplier} implementation using the {@link ServiceLoader}.
|
||||||
|
* @return A default {@link JsonSchemaValidator} instance.
|
||||||
|
* @throws IllegalStateException If no {@link JsonSchemaValidatorSupplier}
|
||||||
|
* implementation is found or if an error occurs during instantiation.
|
||||||
|
*/
|
||||||
|
static JsonSchemaValidator createDefaultValidator() {
|
||||||
|
AtomicReference<IllegalStateException> ex = new AtomicReference<>();
|
||||||
|
return ServiceLoader.load(JsonSchemaValidatorSupplier.class).stream().flatMap(p -> {
|
||||||
|
try {
|
||||||
|
JsonSchemaValidatorSupplier supplier = p.get();
|
||||||
|
return Stream.ofNullable(supplier);
|
||||||
|
}
|
||||||
|
catch (Exception e) {
|
||||||
|
addException(ex, e);
|
||||||
|
return Stream.empty();
|
||||||
|
}
|
||||||
|
}).flatMap(jsonMapperSupplier -> {
|
||||||
|
try {
|
||||||
|
return Stream.of(jsonMapperSupplier.get());
|
||||||
|
}
|
||||||
|
catch (Exception e) {
|
||||||
|
addException(ex, e);
|
||||||
|
return Stream.empty();
|
||||||
|
}
|
||||||
|
}).findFirst().orElseThrow(() -> {
|
||||||
|
if (ex.get() != null) {
|
||||||
|
return ex.get();
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return new IllegalStateException("No default JsonSchemaValidatorSupplier implementation found");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void addException(AtomicReference<IllegalStateException> ref, Exception toAdd) {
|
||||||
|
ref.updateAndGet(existing -> {
|
||||||
|
if (existing == null) {
|
||||||
|
return new IllegalStateException("Failed to initialize default JsonSchemaValidatorSupplier", toAdd);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
existing.addSuppressed(toAdd);
|
||||||
|
return existing;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,64 @@
|
||||||
|
/*
|
||||||
|
* 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<String, Object> schema, Object structuredContent);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates the default {@link JsonSchemaValidator}.
|
||||||
|
* @return The default {@link JsonSchemaValidator}
|
||||||
|
* @throws IllegalStateException If no {@link JsonSchemaValidator} implementation
|
||||||
|
* exists on the classpath.
|
||||||
|
*/
|
||||||
|
static JsonSchemaValidator createDefault() {
|
||||||
|
return JsonSchemaInternal.createDefaultValidator();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the default {@link JsonSchemaValidator}.
|
||||||
|
* @return The default {@link JsonSchemaValidator}
|
||||||
|
* @throws IllegalStateException If no {@link JsonSchemaValidator} implementation
|
||||||
|
* exists on the classpath.
|
||||||
|
*/
|
||||||
|
static JsonSchemaValidator getDefault() {
|
||||||
|
return JsonSchemaInternal.getDefaultValidator();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -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<JsonSchemaValidator> {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -22,7 +22,13 @@
|
||||||
</scm>
|
</scm>
|
||||||
|
|
||||||
<dependencies>
|
<dependencies>
|
||||||
<dependency>
|
<dependency>
|
||||||
|
<groupId>io.modelcontextprotocol.sdk</groupId>
|
||||||
|
<artifactId>mcp-json-jackson2</artifactId>
|
||||||
|
<version>0.13.0-SNAPSHOT</version>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
<groupId>io.modelcontextprotocol.sdk</groupId>
|
<groupId>io.modelcontextprotocol.sdk</groupId>
|
||||||
<artifactId>mcp</artifactId>
|
<artifactId>mcp</artifactId>
|
||||||
<version>0.13.0-SNAPSHOT</version>
|
<version>0.13.0-SNAPSHOT</version>
|
||||||
|
|
|
||||||
|
|
@ -22,8 +22,8 @@ import org.springframework.web.reactive.function.client.ClientResponse;
|
||||||
import org.springframework.web.reactive.function.client.WebClient;
|
import org.springframework.web.reactive.function.client.WebClient;
|
||||||
import org.springframework.web.reactive.function.client.WebClientResponseException;
|
import org.springframework.web.reactive.function.client.WebClientResponseException;
|
||||||
|
|
||||||
import com.fasterxml.jackson.core.type.TypeReference;
|
import io.modelcontextprotocol.json.TypeRef;
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import io.modelcontextprotocol.json.McpJsonMapper;
|
||||||
|
|
||||||
import io.modelcontextprotocol.spec.DefaultMcpTransportSession;
|
import io.modelcontextprotocol.spec.DefaultMcpTransportSession;
|
||||||
import io.modelcontextprotocol.spec.DefaultMcpTransportStream;
|
import io.modelcontextprotocol.spec.DefaultMcpTransportStream;
|
||||||
|
|
@ -88,7 +88,7 @@ public class WebClientStreamableHttpTransport implements McpClientTransport {
|
||||||
private static final ParameterizedTypeReference<ServerSentEvent<String>> PARAMETERIZED_TYPE_REF = new ParameterizedTypeReference<>() {
|
private static final ParameterizedTypeReference<ServerSentEvent<String>> PARAMETERIZED_TYPE_REF = new ParameterizedTypeReference<>() {
|
||||||
};
|
};
|
||||||
|
|
||||||
private final ObjectMapper objectMapper;
|
private final McpJsonMapper jsonMapper;
|
||||||
|
|
||||||
private final WebClient webClient;
|
private final WebClient webClient;
|
||||||
|
|
||||||
|
|
@ -104,9 +104,9 @@ public class WebClientStreamableHttpTransport implements McpClientTransport {
|
||||||
|
|
||||||
private final AtomicReference<Consumer<Throwable>> exceptionHandler = new AtomicReference<>();
|
private final AtomicReference<Consumer<Throwable>> exceptionHandler = new AtomicReference<>();
|
||||||
|
|
||||||
private WebClientStreamableHttpTransport(ObjectMapper objectMapper, WebClient.Builder webClientBuilder,
|
private WebClientStreamableHttpTransport(McpJsonMapper jsonMapper, WebClient.Builder webClientBuilder,
|
||||||
String endpoint, boolean resumableStreams, boolean openConnectionOnStartup) {
|
String endpoint, boolean resumableStreams, boolean openConnectionOnStartup) {
|
||||||
this.objectMapper = objectMapper;
|
this.jsonMapper = jsonMapper;
|
||||||
this.webClient = webClientBuilder.build();
|
this.webClient = webClientBuilder.build();
|
||||||
this.endpoint = endpoint;
|
this.endpoint = endpoint;
|
||||||
this.resumableStreams = resumableStreams;
|
this.resumableStreams = resumableStreams;
|
||||||
|
|
@ -366,8 +366,7 @@ public class WebClientStreamableHttpTransport implements McpClientTransport {
|
||||||
McpSchema.JSONRPCResponse.JSONRPCError jsonRpcError = null;
|
McpSchema.JSONRPCResponse.JSONRPCError jsonRpcError = null;
|
||||||
Exception toPropagate;
|
Exception toPropagate;
|
||||||
try {
|
try {
|
||||||
McpSchema.JSONRPCResponse jsonRpcResponse = objectMapper.readValue(body,
|
McpSchema.JSONRPCResponse jsonRpcResponse = jsonMapper.readValue(body, McpSchema.JSONRPCResponse.class);
|
||||||
McpSchema.JSONRPCResponse.class);
|
|
||||||
jsonRpcError = jsonRpcResponse.error();
|
jsonRpcError = jsonRpcResponse.error();
|
||||||
toPropagate = jsonRpcError != null ? new McpError(jsonRpcError)
|
toPropagate = jsonRpcError != null ? new McpError(jsonRpcError)
|
||||||
: new McpTransportException("Can't parse the jsonResponse " + jsonRpcResponse);
|
: new McpTransportException("Can't parse the jsonResponse " + jsonRpcResponse);
|
||||||
|
|
@ -427,7 +426,7 @@ public class WebClientStreamableHttpTransport implements McpClientTransport {
|
||||||
s.complete();
|
s.complete();
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
McpSchema.JSONRPCMessage jsonRpcResponse = McpSchema.deserializeJsonRpcMessage(objectMapper,
|
McpSchema.JSONRPCMessage jsonRpcResponse = McpSchema.deserializeJsonRpcMessage(jsonMapper,
|
||||||
responseMessage);
|
responseMessage);
|
||||||
s.next(List.of(jsonRpcResponse));
|
s.next(List.of(jsonRpcResponse));
|
||||||
}
|
}
|
||||||
|
|
@ -447,8 +446,8 @@ public class WebClientStreamableHttpTransport implements McpClientTransport {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public <T> T unmarshalFrom(Object data, TypeReference<T> typeRef) {
|
public <T> T unmarshalFrom(Object data, TypeRef<T> typeRef) {
|
||||||
return this.objectMapper.convertValue(data, typeRef);
|
return this.jsonMapper.convertValue(data, typeRef);
|
||||||
}
|
}
|
||||||
|
|
||||||
private Tuple2<Optional<String>, Iterable<McpSchema.JSONRPCMessage>> parse(ServerSentEvent<String> event) {
|
private Tuple2<Optional<String>, Iterable<McpSchema.JSONRPCMessage>> parse(ServerSentEvent<String> event) {
|
||||||
|
|
@ -456,7 +455,7 @@ public class WebClientStreamableHttpTransport implements McpClientTransport {
|
||||||
try {
|
try {
|
||||||
// We don't support batching ATM and probably won't since the next version
|
// We don't support batching ATM and probably won't since the next version
|
||||||
// considers removing it.
|
// considers removing it.
|
||||||
McpSchema.JSONRPCMessage message = McpSchema.deserializeJsonRpcMessage(this.objectMapper, event.data());
|
McpSchema.JSONRPCMessage message = McpSchema.deserializeJsonRpcMessage(this.jsonMapper, event.data());
|
||||||
return Tuples.of(Optional.ofNullable(event.id()), List.of(message));
|
return Tuples.of(Optional.ofNullable(event.id()), List.of(message));
|
||||||
}
|
}
|
||||||
catch (IOException ioException) {
|
catch (IOException ioException) {
|
||||||
|
|
@ -474,7 +473,7 @@ public class WebClientStreamableHttpTransport implements McpClientTransport {
|
||||||
*/
|
*/
|
||||||
public static class Builder {
|
public static class Builder {
|
||||||
|
|
||||||
private ObjectMapper objectMapper;
|
private McpJsonMapper jsonMapper;
|
||||||
|
|
||||||
private WebClient.Builder webClientBuilder;
|
private WebClient.Builder webClientBuilder;
|
||||||
|
|
||||||
|
|
@ -490,13 +489,13 @@ public class WebClientStreamableHttpTransport implements McpClientTransport {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Configure the {@link ObjectMapper} to use.
|
* Configure the {@link McpJsonMapper} to use.
|
||||||
* @param objectMapper instance to use
|
* @param jsonMapper instance to use
|
||||||
* @return the builder instance
|
* @return the builder instance
|
||||||
*/
|
*/
|
||||||
public Builder objectMapper(ObjectMapper objectMapper) {
|
public Builder jsonMapper(McpJsonMapper jsonMapper) {
|
||||||
Assert.notNull(objectMapper, "ObjectMapper must not be null");
|
Assert.notNull(jsonMapper, "JsonMapper must not be null");
|
||||||
this.objectMapper = objectMapper;
|
this.jsonMapper = jsonMapper;
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -555,10 +554,8 @@ public class WebClientStreamableHttpTransport implements McpClientTransport {
|
||||||
* @return a new instance of {@link WebClientStreamableHttpTransport}
|
* @return a new instance of {@link WebClientStreamableHttpTransport}
|
||||||
*/
|
*/
|
||||||
public WebClientStreamableHttpTransport build() {
|
public WebClientStreamableHttpTransport build() {
|
||||||
ObjectMapper objectMapper = this.objectMapper != null ? this.objectMapper : new ObjectMapper();
|
return new WebClientStreamableHttpTransport(jsonMapper == null ? McpJsonMapper.getDefault() : jsonMapper,
|
||||||
|
webClientBuilder, endpoint, resumableStreams, openConnectionOnStartup);
|
||||||
return new WebClientStreamableHttpTransport(objectMapper, this.webClientBuilder, endpoint, resumableStreams,
|
|
||||||
openConnectionOnStartup);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,8 +9,8 @@ import java.util.List;
|
||||||
import java.util.function.BiConsumer;
|
import java.util.function.BiConsumer;
|
||||||
import java.util.function.Function;
|
import java.util.function.Function;
|
||||||
|
|
||||||
import com.fasterxml.jackson.core.type.TypeReference;
|
import io.modelcontextprotocol.json.McpJsonMapper;
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import io.modelcontextprotocol.json.TypeRef;
|
||||||
|
|
||||||
import io.modelcontextprotocol.spec.HttpHeaders;
|
import io.modelcontextprotocol.spec.HttpHeaders;
|
||||||
import io.modelcontextprotocol.spec.McpClientTransport;
|
import io.modelcontextprotocol.spec.McpClientTransport;
|
||||||
|
|
@ -100,10 +100,10 @@ public class WebFluxSseClientTransport implements McpClientTransport {
|
||||||
private final WebClient webClient;
|
private final WebClient webClient;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ObjectMapper for serializing outbound messages and deserializing inbound messages.
|
* JSON mapper for serializing outbound messages and deserializing inbound messages.
|
||||||
* Handles conversion between JSON-RPC messages and their string representation.
|
* Handles conversion between JSON-RPC messages and their string representation.
|
||||||
*/
|
*/
|
||||||
protected ObjectMapper objectMapper;
|
protected McpJsonMapper jsonMapper;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Subscription for the SSE connection handling inbound messages. Used for cleanup
|
* Subscription for the SSE connection handling inbound messages. Used for cleanup
|
||||||
|
|
@ -129,27 +129,16 @@ public class WebFluxSseClientTransport implements McpClientTransport {
|
||||||
*/
|
*/
|
||||||
private String sseEndpoint;
|
private String sseEndpoint;
|
||||||
|
|
||||||
/**
|
|
||||||
* Constructs a new SseClientTransport with the specified WebClient builder. Uses a
|
|
||||||
* default ObjectMapper instance for JSON processing.
|
|
||||||
* @param webClientBuilder the WebClient.Builder to use for creating the WebClient
|
|
||||||
* instance
|
|
||||||
* @throws IllegalArgumentException if webClientBuilder is null
|
|
||||||
*/
|
|
||||||
public WebFluxSseClientTransport(WebClient.Builder webClientBuilder) {
|
|
||||||
this(webClientBuilder, new ObjectMapper());
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructs a new SseClientTransport with the specified WebClient builder and
|
* Constructs a new SseClientTransport with the specified WebClient builder and
|
||||||
* ObjectMapper. Initializes both inbound and outbound message processing pipelines.
|
* ObjectMapper. Initializes both inbound and outbound message processing pipelines.
|
||||||
* @param webClientBuilder the WebClient.Builder to use for creating the WebClient
|
* @param webClientBuilder the WebClient.Builder to use for creating the WebClient
|
||||||
* instance
|
* instance
|
||||||
* @param objectMapper the ObjectMapper to use for JSON processing
|
* @param jsonMapper the ObjectMapper to use for JSON processing
|
||||||
* @throws IllegalArgumentException if either parameter is null
|
* @throws IllegalArgumentException if either parameter is null
|
||||||
*/
|
*/
|
||||||
public WebFluxSseClientTransport(WebClient.Builder webClientBuilder, ObjectMapper objectMapper) {
|
public WebFluxSseClientTransport(WebClient.Builder webClientBuilder, McpJsonMapper jsonMapper) {
|
||||||
this(webClientBuilder, objectMapper, DEFAULT_SSE_ENDPOINT);
|
this(webClientBuilder, jsonMapper, DEFAULT_SSE_ENDPOINT);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -157,17 +146,16 @@ public class WebFluxSseClientTransport implements McpClientTransport {
|
||||||
* ObjectMapper. Initializes both inbound and outbound message processing pipelines.
|
* ObjectMapper. Initializes both inbound and outbound message processing pipelines.
|
||||||
* @param webClientBuilder the WebClient.Builder to use for creating the WebClient
|
* @param webClientBuilder the WebClient.Builder to use for creating the WebClient
|
||||||
* instance
|
* instance
|
||||||
* @param objectMapper the ObjectMapper to use for JSON processing
|
* @param jsonMapper the ObjectMapper to use for JSON processing
|
||||||
* @param sseEndpoint the SSE endpoint URI to use for establishing the connection
|
* @param sseEndpoint the SSE endpoint URI to use for establishing the connection
|
||||||
* @throws IllegalArgumentException if either parameter is null
|
* @throws IllegalArgumentException if either parameter is null
|
||||||
*/
|
*/
|
||||||
public WebFluxSseClientTransport(WebClient.Builder webClientBuilder, ObjectMapper objectMapper,
|
public WebFluxSseClientTransport(WebClient.Builder webClientBuilder, McpJsonMapper jsonMapper, String sseEndpoint) {
|
||||||
String sseEndpoint) {
|
Assert.notNull(jsonMapper, "jsonMapper must not be null");
|
||||||
Assert.notNull(objectMapper, "ObjectMapper must not be null");
|
|
||||||
Assert.notNull(webClientBuilder, "WebClient.Builder must not be null");
|
Assert.notNull(webClientBuilder, "WebClient.Builder must not be null");
|
||||||
Assert.hasText(sseEndpoint, "SSE endpoint must not be null or empty");
|
Assert.hasText(sseEndpoint, "SSE endpoint must not be null or empty");
|
||||||
|
|
||||||
this.objectMapper = objectMapper;
|
this.jsonMapper = jsonMapper;
|
||||||
this.webClient = webClientBuilder.build();
|
this.webClient = webClientBuilder.build();
|
||||||
this.sseEndpoint = sseEndpoint;
|
this.sseEndpoint = sseEndpoint;
|
||||||
}
|
}
|
||||||
|
|
@ -217,7 +205,7 @@ public class WebFluxSseClientTransport implements McpClientTransport {
|
||||||
}
|
}
|
||||||
else if (MESSAGE_EVENT_TYPE.equals(event.event())) {
|
else if (MESSAGE_EVENT_TYPE.equals(event.event())) {
|
||||||
try {
|
try {
|
||||||
JSONRPCMessage message = McpSchema.deserializeJsonRpcMessage(this.objectMapper, event.data());
|
JSONRPCMessage message = McpSchema.deserializeJsonRpcMessage(this.jsonMapper, event.data());
|
||||||
s.next(message);
|
s.next(message);
|
||||||
}
|
}
|
||||||
catch (IOException ioException) {
|
catch (IOException ioException) {
|
||||||
|
|
@ -255,7 +243,7 @@ public class WebFluxSseClientTransport implements McpClientTransport {
|
||||||
return Mono.empty();
|
return Mono.empty();
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
String jsonText = this.objectMapper.writeValueAsString(message);
|
String jsonText = this.jsonMapper.writeValueAsString(message);
|
||||||
return webClient.post()
|
return webClient.post()
|
||||||
.uri(messageEndpointUri)
|
.uri(messageEndpointUri)
|
||||||
.contentType(MediaType.APPLICATION_JSON)
|
.contentType(MediaType.APPLICATION_JSON)
|
||||||
|
|
@ -349,13 +337,13 @@ public class WebFluxSseClientTransport implements McpClientTransport {
|
||||||
* type conversion capabilities to handle complex object structures.
|
* type conversion capabilities to handle complex object structures.
|
||||||
* @param <T> the target type to convert the data into
|
* @param <T> the target type to convert the data into
|
||||||
* @param data the source object to convert
|
* @param data the source object to convert
|
||||||
* @param typeRef the TypeReference describing the target type
|
* @param typeRef the TypeRef describing the target type
|
||||||
* @return the unmarshalled object of type T
|
* @return the unmarshalled object of type T
|
||||||
* @throws IllegalArgumentException if the conversion cannot be performed
|
* @throws IllegalArgumentException if the conversion cannot be performed
|
||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
public <T> T unmarshalFrom(Object data, TypeReference<T> typeRef) {
|
public <T> T unmarshalFrom(Object data, TypeRef<T> typeRef) {
|
||||||
return this.objectMapper.convertValue(data, typeRef);
|
return this.jsonMapper.convertValue(data, typeRef);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -377,7 +365,7 @@ public class WebFluxSseClientTransport implements McpClientTransport {
|
||||||
|
|
||||||
private String sseEndpoint = DEFAULT_SSE_ENDPOINT;
|
private String sseEndpoint = DEFAULT_SSE_ENDPOINT;
|
||||||
|
|
||||||
private ObjectMapper objectMapper = new ObjectMapper();
|
private McpJsonMapper jsonMapper;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a new builder with the specified WebClient.Builder.
|
* Creates a new builder with the specified WebClient.Builder.
|
||||||
|
|
@ -400,13 +388,13 @@ public class WebFluxSseClientTransport implements McpClientTransport {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets the object mapper for JSON serialization/deserialization.
|
* Sets the JSON mapper for serialization/deserialization.
|
||||||
* @param objectMapper the object mapper
|
* @param jsonMapper the JsonMapper to use
|
||||||
* @return this builder
|
* @return this builder
|
||||||
*/
|
*/
|
||||||
public Builder objectMapper(ObjectMapper objectMapper) {
|
public Builder jsonMapper(McpJsonMapper jsonMapper) {
|
||||||
Assert.notNull(objectMapper, "objectMapper must not be null");
|
Assert.notNull(jsonMapper, "jsonMapper must not be null");
|
||||||
this.objectMapper = objectMapper;
|
this.jsonMapper = jsonMapper;
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -415,7 +403,8 @@ public class WebFluxSseClientTransport implements McpClientTransport {
|
||||||
* @return a new transport instance
|
* @return a new transport instance
|
||||||
*/
|
*/
|
||||||
public WebFluxSseClientTransport build() {
|
public WebFluxSseClientTransport build() {
|
||||||
return new WebFluxSseClientTransport(webClientBuilder, objectMapper, sseEndpoint);
|
return new WebFluxSseClientTransport(webClientBuilder,
|
||||||
|
jsonMapper == null ? McpJsonMapper.getDefault() : jsonMapper, sseEndpoint);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,8 +9,8 @@ import java.time.Duration;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
|
||||||
import com.fasterxml.jackson.core.type.TypeReference;
|
import io.modelcontextprotocol.json.McpJsonMapper;
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import io.modelcontextprotocol.json.TypeRef;
|
||||||
|
|
||||||
import io.modelcontextprotocol.common.McpTransportContext;
|
import io.modelcontextprotocol.common.McpTransportContext;
|
||||||
import io.modelcontextprotocol.server.McpTransportContextExtractor;
|
import io.modelcontextprotocol.server.McpTransportContextExtractor;
|
||||||
|
|
@ -97,7 +97,7 @@ public class WebFluxSseServerTransportProvider implements McpServerTransportProv
|
||||||
|
|
||||||
public static final String DEFAULT_BASE_URL = "";
|
public static final String DEFAULT_BASE_URL = "";
|
||||||
|
|
||||||
private final ObjectMapper objectMapper;
|
private final McpJsonMapper jsonMapper;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Base URL for the message endpoint. This is used to construct the full URL for
|
* Base URL for the message endpoint. This is used to construct the full URL for
|
||||||
|
|
@ -131,82 +131,10 @@ public class WebFluxSseServerTransportProvider implements McpServerTransportProv
|
||||||
*/
|
*/
|
||||||
private KeepAliveScheduler keepAliveScheduler;
|
private KeepAliveScheduler keepAliveScheduler;
|
||||||
|
|
||||||
/**
|
|
||||||
* Constructs a new WebFlux SSE server transport provider instance with the default
|
|
||||||
* SSE endpoint.
|
|
||||||
* @param objectMapper The ObjectMapper to use for JSON serialization/deserialization
|
|
||||||
* of MCP messages. Must not be null.
|
|
||||||
* @param messageEndpoint The endpoint URI where clients should send their JSON-RPC
|
|
||||||
* messages. This endpoint will be communicated to clients during SSE connection
|
|
||||||
* setup. Must not be null.
|
|
||||||
* @throws IllegalArgumentException if either parameter is null
|
|
||||||
* @deprecated Use the builder {@link #builder()} instead for better configuration
|
|
||||||
* options.
|
|
||||||
*/
|
|
||||||
@Deprecated
|
|
||||||
public WebFluxSseServerTransportProvider(ObjectMapper objectMapper, String messageEndpoint) {
|
|
||||||
this(objectMapper, messageEndpoint, DEFAULT_SSE_ENDPOINT);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructs a new WebFlux SSE server transport provider instance.
|
* Constructs a new WebFlux SSE server transport provider instance.
|
||||||
* @param objectMapper The ObjectMapper to use for JSON serialization/deserialization
|
* @param jsonMapper The ObjectMapper to use for JSON serialization/deserialization of
|
||||||
* of MCP messages. Must not be null.
|
* MCP messages. Must not be null.
|
||||||
* @param messageEndpoint The endpoint URI where clients should send their JSON-RPC
|
|
||||||
* messages. This endpoint will be communicated to clients during SSE connection
|
|
||||||
* setup. Must not be null.
|
|
||||||
* @throws IllegalArgumentException if either parameter is null
|
|
||||||
* @deprecated Use the builder {@link #builder()} instead for better configuration
|
|
||||||
* options.
|
|
||||||
*/
|
|
||||||
@Deprecated
|
|
||||||
public WebFluxSseServerTransportProvider(ObjectMapper objectMapper, String messageEndpoint, String sseEndpoint) {
|
|
||||||
this(objectMapper, DEFAULT_BASE_URL, messageEndpoint, sseEndpoint);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Constructs a new WebFlux SSE server transport provider instance.
|
|
||||||
* @param objectMapper The ObjectMapper to use for JSON serialization/deserialization
|
|
||||||
* of MCP messages. Must not be null.
|
|
||||||
* @param baseUrl webflux message base path
|
|
||||||
* @param messageEndpoint The endpoint URI where clients should send their JSON-RPC
|
|
||||||
* messages. This endpoint will be communicated to clients during SSE connection
|
|
||||||
* setup. Must not be null.
|
|
||||||
* @throws IllegalArgumentException if either parameter is null
|
|
||||||
* @deprecated Use the builder {@link #builder()} instead for better configuration
|
|
||||||
* options.
|
|
||||||
*/
|
|
||||||
@Deprecated
|
|
||||||
public WebFluxSseServerTransportProvider(ObjectMapper objectMapper, String baseUrl, String messageEndpoint,
|
|
||||||
String sseEndpoint) {
|
|
||||||
this(objectMapper, baseUrl, messageEndpoint, sseEndpoint, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Constructs a new WebFlux SSE server transport provider instance.
|
|
||||||
* @param objectMapper The ObjectMapper to use for JSON serialization/deserialization
|
|
||||||
* of MCP messages. Must not be null.
|
|
||||||
* @param baseUrl webflux message base path
|
|
||||||
* @param messageEndpoint The endpoint URI where clients should send their JSON-RPC
|
|
||||||
* messages. This endpoint will be communicated to clients during SSE connection
|
|
||||||
* setup. Must not be null.
|
|
||||||
* @param sseEndpoint The SSE endpoint path. Must not be null.
|
|
||||||
* @param keepAliveInterval The interval for sending keep-alive pings to clients.
|
|
||||||
* @throws IllegalArgumentException if either parameter is null
|
|
||||||
* @deprecated Use the builder {@link #builder()} instead for better configuration
|
|
||||||
* options.
|
|
||||||
*/
|
|
||||||
@Deprecated
|
|
||||||
public WebFluxSseServerTransportProvider(ObjectMapper objectMapper, String baseUrl, String messageEndpoint,
|
|
||||||
String sseEndpoint, Duration keepAliveInterval) {
|
|
||||||
this(objectMapper, baseUrl, messageEndpoint, sseEndpoint, keepAliveInterval,
|
|
||||||
(serverRequest) -> McpTransportContext.EMPTY);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Constructs a new WebFlux SSE server transport provider instance.
|
|
||||||
* @param objectMapper The ObjectMapper to use for JSON serialization/deserialization
|
|
||||||
* of MCP messages. Must not be null.
|
|
||||||
* @param baseUrl webflux message base path
|
* @param baseUrl webflux message base path
|
||||||
* @param messageEndpoint The endpoint URI where clients should send their JSON-RPC
|
* @param messageEndpoint The endpoint URI where clients should send their JSON-RPC
|
||||||
* messages. This endpoint will be communicated to clients during SSE connection
|
* messages. This endpoint will be communicated to clients during SSE connection
|
||||||
|
|
@ -217,16 +145,16 @@ public class WebFluxSseServerTransportProvider implements McpServerTransportProv
|
||||||
* context from HTTP requests. Must not be null.
|
* context from HTTP requests. Must not be null.
|
||||||
* @throws IllegalArgumentException if either parameter is null
|
* @throws IllegalArgumentException if either parameter is null
|
||||||
*/
|
*/
|
||||||
private WebFluxSseServerTransportProvider(ObjectMapper objectMapper, String baseUrl, String messageEndpoint,
|
private WebFluxSseServerTransportProvider(McpJsonMapper jsonMapper, String baseUrl, String messageEndpoint,
|
||||||
String sseEndpoint, Duration keepAliveInterval,
|
String sseEndpoint, Duration keepAliveInterval,
|
||||||
McpTransportContextExtractor<ServerRequest> contextExtractor) {
|
McpTransportContextExtractor<ServerRequest> contextExtractor) {
|
||||||
Assert.notNull(objectMapper, "ObjectMapper must not be null");
|
Assert.notNull(jsonMapper, "ObjectMapper must not be null");
|
||||||
Assert.notNull(baseUrl, "Message base path must not be null");
|
Assert.notNull(baseUrl, "Message base path must not be null");
|
||||||
Assert.notNull(messageEndpoint, "Message endpoint must not be null");
|
Assert.notNull(messageEndpoint, "Message endpoint must not be null");
|
||||||
Assert.notNull(sseEndpoint, "SSE endpoint must not be null");
|
Assert.notNull(sseEndpoint, "SSE endpoint must not be null");
|
||||||
Assert.notNull(contextExtractor, "Context extractor must not be null");
|
Assert.notNull(contextExtractor, "Context extractor must not be null");
|
||||||
|
|
||||||
this.objectMapper = objectMapper;
|
this.jsonMapper = jsonMapper;
|
||||||
this.baseUrl = baseUrl;
|
this.baseUrl = baseUrl;
|
||||||
this.messageEndpoint = messageEndpoint;
|
this.messageEndpoint = messageEndpoint;
|
||||||
this.sseEndpoint = sseEndpoint;
|
this.sseEndpoint = sseEndpoint;
|
||||||
|
|
@ -404,7 +332,7 @@ public class WebFluxSseServerTransportProvider implements McpServerTransportProv
|
||||||
|
|
||||||
return request.bodyToMono(String.class).flatMap(body -> {
|
return request.bodyToMono(String.class).flatMap(body -> {
|
||||||
try {
|
try {
|
||||||
McpSchema.JSONRPCMessage message = McpSchema.deserializeJsonRpcMessage(objectMapper, body);
|
McpSchema.JSONRPCMessage message = McpSchema.deserializeJsonRpcMessage(jsonMapper, body);
|
||||||
return session.handle(message).flatMap(response -> ServerResponse.ok().build()).onErrorResume(error -> {
|
return session.handle(message).flatMap(response -> ServerResponse.ok().build()).onErrorResume(error -> {
|
||||||
logger.error("Error processing message: {}", error.getMessage());
|
logger.error("Error processing message: {}", error.getMessage());
|
||||||
// TODO: instead of signalling the error, just respond with 200 OK
|
// TODO: instead of signalling the error, just respond with 200 OK
|
||||||
|
|
@ -433,7 +361,7 @@ public class WebFluxSseServerTransportProvider implements McpServerTransportProv
|
||||||
public Mono<Void> sendMessage(McpSchema.JSONRPCMessage message) {
|
public Mono<Void> sendMessage(McpSchema.JSONRPCMessage message) {
|
||||||
return Mono.fromSupplier(() -> {
|
return Mono.fromSupplier(() -> {
|
||||||
try {
|
try {
|
||||||
return objectMapper.writeValueAsString(message);
|
return jsonMapper.writeValueAsString(message);
|
||||||
}
|
}
|
||||||
catch (IOException e) {
|
catch (IOException e) {
|
||||||
throw Exceptions.propagate(e);
|
throw Exceptions.propagate(e);
|
||||||
|
|
@ -452,8 +380,8 @@ public class WebFluxSseServerTransportProvider implements McpServerTransportProv
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public <T> T unmarshalFrom(Object data, TypeReference<T> typeRef) {
|
public <T> T unmarshalFrom(Object data, TypeRef<T> typeRef) {
|
||||||
return objectMapper.convertValue(data, typeRef);
|
return jsonMapper.convertValue(data, typeRef);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
@ -480,7 +408,7 @@ public class WebFluxSseServerTransportProvider implements McpServerTransportProv
|
||||||
*/
|
*/
|
||||||
public static class Builder {
|
public static class Builder {
|
||||||
|
|
||||||
private ObjectMapper objectMapper;
|
private McpJsonMapper jsonMapper;
|
||||||
|
|
||||||
private String baseUrl = DEFAULT_BASE_URL;
|
private String baseUrl = DEFAULT_BASE_URL;
|
||||||
|
|
||||||
|
|
@ -494,15 +422,15 @@ public class WebFluxSseServerTransportProvider implements McpServerTransportProv
|
||||||
serverRequest) -> McpTransportContext.EMPTY;
|
serverRequest) -> McpTransportContext.EMPTY;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets the ObjectMapper to use for JSON serialization/deserialization of MCP
|
* Sets the McpJsonMapper to use for JSON serialization/deserialization of MCP
|
||||||
* messages.
|
* messages.
|
||||||
* @param objectMapper The ObjectMapper instance. Must not be null.
|
* @param jsonMapper The McpJsonMapper instance. Must not be null.
|
||||||
* @return this builder instance
|
* @return this builder instance
|
||||||
* @throws IllegalArgumentException if objectMapper is null
|
* @throws IllegalArgumentException if jsonMapper is null
|
||||||
*/
|
*/
|
||||||
public Builder objectMapper(ObjectMapper objectMapper) {
|
public Builder jsonMapper(McpJsonMapper jsonMapper) {
|
||||||
Assert.notNull(objectMapper, "ObjectMapper must not be null");
|
Assert.notNull(jsonMapper, "JsonMapper must not be null");
|
||||||
this.objectMapper = objectMapper;
|
this.jsonMapper = jsonMapper;
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -577,11 +505,9 @@ public class WebFluxSseServerTransportProvider implements McpServerTransportProv
|
||||||
* @throws IllegalStateException if required parameters are not set
|
* @throws IllegalStateException if required parameters are not set
|
||||||
*/
|
*/
|
||||||
public WebFluxSseServerTransportProvider build() {
|
public WebFluxSseServerTransportProvider build() {
|
||||||
Assert.notNull(objectMapper, "ObjectMapper must be set");
|
|
||||||
Assert.notNull(messageEndpoint, "Message endpoint must be set");
|
Assert.notNull(messageEndpoint, "Message endpoint must be set");
|
||||||
|
return new WebFluxSseServerTransportProvider(jsonMapper == null ? McpJsonMapper.getDefault() : jsonMapper,
|
||||||
return new WebFluxSseServerTransportProvider(objectMapper, baseUrl, messageEndpoint, sseEndpoint,
|
baseUrl, messageEndpoint, sseEndpoint, keepAliveInterval, contextExtractor);
|
||||||
keepAliveInterval, contextExtractor);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue