├── .gitattributes ├── mcp-json-jackson2 ├── src │ └── main │ │ ├── resources │ │ └── META-INF │ │ │ └── services │ │ │ ├── io.modelcontextprotocol.json.McpJsonMapperSupplier │ │ │ └── io.modelcontextprotocol.json.schema.JsonSchemaValidatorSupplier │ │ └── java │ │ └── io │ │ └── modelcontextprotocol │ │ └── json │ │ ├── schema │ │ └── jackson │ │ │ └── JacksonJsonSchemaValidatorSupplier.java │ │ └── jackson │ │ ├── JacksonMcpJsonMapperSupplier.java │ │ └── JacksonMcpJsonMapper.java └── pom.xml ├── mcp-core └── src │ ├── test │ ├── java │ │ └── io │ │ │ └── modelcontextprotocol │ │ │ ├── spec │ │ │ ├── ArgumentException.java │ │ │ ├── McpErrorTest.java │ │ │ ├── CompleteCompletionSerializationTest.java │ │ │ ├── json │ │ │ │ └── gson │ │ │ │ │ └── GsonMcpJsonMapper.java │ │ │ └── JSONRPCRequestMcpValidationTest.java │ │ │ ├── util │ │ │ ├── McpJsonMapperUtils.java │ │ │ ├── ToolsUtils.java │ │ │ ├── AssertTests.java │ │ │ └── UtilsTests.java │ │ │ ├── client │ │ │ ├── ServerParameterUtils.java │ │ │ ├── HttpClientStreamableHttpAsyncClientResiliencyTests.java │ │ │ ├── HttpClientStreamableHttpAsyncClientTests.java │ │ │ ├── StdioMcpAsyncClientTests.java │ │ │ ├── HttpSseMcpAsyncClientTests.java │ │ │ ├── transport │ │ │ │ ├── customizer │ │ │ │ │ ├── DelegatingMcpSyncHttpClientRequestCustomizerTest.java │ │ │ │ │ └── DelegatingMcpAsyncHttpClientRequestCustomizerTest.java │ │ │ │ └── HttpClientStreamableHttpTransportEmptyJsonResponseTest.java │ │ │ ├── HttpClientStreamableHttpSyncClientTests.java │ │ │ ├── StdioMcpSyncClientTests.java │ │ │ └── HttpSseMcpSyncClientTests.java │ │ │ ├── server │ │ │ ├── ServletSseMcpSyncServerTests.java │ │ │ ├── ServletSseMcpAsyncServerTests.java │ │ │ ├── StdioMcpAsyncServerTests.java │ │ │ ├── StdioMcpSyncServerTests.java │ │ │ ├── HttpServletStreamableSyncServerTests.java │ │ │ ├── HttpServletStreamableAsyncServerTests.java │ │ │ ├── transport │ │ │ │ ├── TomcatTestUtil.java │ │ │ │ └── HttpServletSseServerCustomContextPathTests.java │ │ │ └── HttpServletStreamableIntegrationTests.java │ │ │ ├── MockMcpServerTransportProvider.java │ │ │ ├── MockMcpServerTransport.java │ │ │ └── MockMcpClientTransport.java │ └── resources │ │ └── logback.xml │ └── main │ └── java │ └── io │ └── modelcontextprotocol │ ├── spec │ ├── McpServerTransport.java │ ├── ProtocolVersions.java │ ├── McpTransportSessionClosedException.java │ ├── McpStreamableServerTransport.java │ ├── McpServerTransportProvider.java │ ├── McpLoggableSession.java │ ├── McpStatelessServerTransport.java │ ├── McpTransportException.java │ ├── McpTransportSessionNotFoundException.java │ ├── HttpHeaders.java │ ├── McpClientTransport.java │ ├── ClosedMcpTransportSession.java │ ├── JsonSchemaValidator.java │ ├── McpTransportStream.java │ ├── MissingMcpTransportSession.java │ ├── DefaultMcpStreamableServerSessionFactory.java │ ├── McpTransportSession.java │ ├── DefaultMcpTransportSession.java │ ├── McpServerTransportProviderBase.java │ ├── McpTransport.java │ ├── McpSession.java │ ├── DefaultMcpTransportStream.java │ ├── McpStreamableServerTransportProvider.java │ └── McpError.java │ ├── server │ ├── McpNotificationHandler.java │ ├── McpInitRequestHandler.java │ ├── McpRequestHandler.java │ ├── McpStatelessNotificationHandler.java │ ├── McpStatelessRequestHandler.java │ ├── McpTransportContextExtractor.java │ ├── McpStatelessServerHandler.java │ └── DefaultMcpStatelessServerHandler.java │ ├── util │ ├── McpUriTemplateManagerFactory.java │ ├── DefaultMcpUriTemplateManagerFactory.java │ ├── McpUriTemplateManager.java │ └── Assert.java │ ├── client │ └── transport │ │ └── customizer │ │ ├── McpSyncHttpClientRequestCustomizer.java │ │ ├── DelegatingMcpSyncHttpClientRequestCustomizer.java │ │ ├── DelegatingMcpAsyncHttpClientRequestCustomizer.java │ │ └── McpAsyncHttpClientRequestCustomizer.java │ └── common │ ├── DefaultMcpTransportContext.java │ └── McpTransportContext.java ├── .github ├── ISSUE_TEMPLATE │ ├── config.yml │ ├── miscellaneous.md │ ├── feature_request.md │ └── bug_report.md └── workflows │ ├── ci.yml │ ├── maven-central-release.yml │ └── publish-snapshot.yml ├── mcp ├── README.md └── pom.xml ├── mcp-test ├── src │ └── main │ │ └── java │ │ └── io │ │ └── modelcontextprotocol │ │ ├── util │ │ ├── McpJsonMapperUtils.java │ │ └── ToolsUtils.java │ │ ├── server │ │ └── TestUtil.java │ │ └── MockMcpTransport.java └── pom.xml ├── mcp-spring ├── mcp-spring-webflux │ ├── src │ │ └── test │ │ │ ├── java │ │ │ └── io │ │ │ │ └── modelcontextprotocol │ │ │ │ ├── utils │ │ │ │ ├── McpJsonMapperUtils.java │ │ │ │ └── McpTestRequestRecordingExchangeFilterFunction.java │ │ │ │ ├── client │ │ │ │ ├── WebClientStreamableHttpAsyncClientResiliencyTests.java │ │ │ │ ├── WebClientStreamableHttpSyncClientTests.java │ │ │ │ ├── WebClientStreamableHttpAsyncClientTests.java │ │ │ │ ├── WebFluxSseMcpSyncClientTests.java │ │ │ │ ├── WebFluxSseMcpAsyncClientTests.java │ │ │ │ └── transport │ │ │ │ │ └── WebClientStreamableHttpTransportTest.java │ │ │ │ ├── server │ │ │ │ ├── transport │ │ │ │ │ └── BlockingInputStream.java │ │ │ │ ├── WebFluxSseMcpAsyncServerTests.java │ │ │ │ ├── WebFluxSseMcpSyncServerTests.java │ │ │ │ ├── WebFluxStreamableMcpSyncServerTests.java │ │ │ │ └── WebFluxStreamableMcpAsyncServerTests.java │ │ │ │ └── WebFluxStatelessIntegrationTests.java │ │ │ └── resources │ │ │ └── logback.xml │ └── README.md └── mcp-spring-webmvc │ ├── README.md │ └── src │ └── test │ ├── resources │ └── logback.xml │ └── java │ └── io │ └── modelcontextprotocol │ └── server │ └── TomcatTestUtil.java ├── mcp-json ├── src │ └── main │ │ └── java │ │ └── io │ │ └── modelcontextprotocol │ │ └── json │ │ ├── McpJsonMapperSupplier.java │ │ ├── schema │ │ ├── JsonSchemaValidatorSupplier.java │ │ ├── JsonSchemaValidator.java │ │ └── JsonSchemaInternal.java │ │ ├── TypeRef.java │ │ ├── McpJsonInternal.java │ │ └── McpJsonMapper.java └── pom.xml ├── SECURITY.md ├── .mvn └── wrapper │ └── maven-wrapper.properties ├── .gitignore ├── LICENSE └── CONTRIBUTING.md /.gitattributes: -------------------------------------------------------------------------------- 1 | /mvnw text eol=lf 2 | *.cmd text eol=crlf 3 | -------------------------------------------------------------------------------- /mcp-json-jackson2/src/main/resources/META-INF/services/io.modelcontextprotocol.json.McpJsonMapperSupplier: -------------------------------------------------------------------------------- 1 | io.modelcontextprotocol.json.jackson.JacksonMcpJsonMapperSupplier -------------------------------------------------------------------------------- /mcp-json-jackson2/src/main/resources/META-INF/services/io.modelcontextprotocol.json.schema.JsonSchemaValidatorSupplier: -------------------------------------------------------------------------------- 1 | io.modelcontextprotocol.json.schema.jackson.JacksonJsonSchemaValidatorSupplier -------------------------------------------------------------------------------- /mcp-core/src/test/java/io/modelcontextprotocol/spec/ArgumentException.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2024-2025 the original author or authors. 3 | */ 4 | 5 | package io.modelcontextprotocol.spec; 6 | 7 | public class ArgumentException { 8 | 9 | } 10 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: Questions and Community Support 4 | url: https://stackoverflow.com/questions/tagged/spring-ai-mcp 5 | about: Please ask and answer questions on StackOverflow with the spring-ai tag 6 | -------------------------------------------------------------------------------- /mcp/README.md: -------------------------------------------------------------------------------- 1 | # Java MCP SDK 2 | 3 | Java SDK implementation of the Model Context Protocol, enabling seamless integration with language models and AI tools. 4 | For comprehensive guides and API documentation, visit the [MCP Java SDK Reference Documentation](https://modelcontextprotocol.io/sdk/java/mcp-overview). 5 | 6 | -------------------------------------------------------------------------------- /mcp-test/src/main/java/io/modelcontextprotocol/util/McpJsonMapperUtils.java: -------------------------------------------------------------------------------- 1 | package io.modelcontextprotocol.util; 2 | 3 | import io.modelcontextprotocol.json.McpJsonMapper; 4 | 5 | public final class McpJsonMapperUtils { 6 | 7 | private McpJsonMapperUtils() { 8 | } 9 | 10 | public static final McpJsonMapper JSON_MAPPER = McpJsonMapper.getDefault(); 11 | 12 | } -------------------------------------------------------------------------------- /mcp-core/src/test/java/io/modelcontextprotocol/util/McpJsonMapperUtils.java: -------------------------------------------------------------------------------- 1 | package io.modelcontextprotocol.util; 2 | 3 | import io.modelcontextprotocol.json.McpJsonMapper; 4 | 5 | public final class McpJsonMapperUtils { 6 | 7 | private McpJsonMapperUtils() { 8 | } 9 | 10 | public static final McpJsonMapper JSON_MAPPER = McpJsonMapper.getDefault(); 11 | 12 | } 13 | -------------------------------------------------------------------------------- /mcp-spring/mcp-spring-webflux/src/test/java/io/modelcontextprotocol/utils/McpJsonMapperUtils.java: -------------------------------------------------------------------------------- 1 | package io.modelcontextprotocol.utils; 2 | 3 | import io.modelcontextprotocol.json.McpJsonMapper; 4 | 5 | public final class McpJsonMapperUtils { 6 | 7 | private McpJsonMapperUtils() { 8 | } 9 | 10 | public static final McpJsonMapper JSON_MAPPER = McpJsonMapper.createDefault(); 11 | 12 | } -------------------------------------------------------------------------------- /mcp-json/src/main/java/io/modelcontextprotocol/json/McpJsonMapperSupplier.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 - 2025 the original author or authors. 3 | */ 4 | 5 | package io.modelcontextprotocol.json; 6 | 7 | import java.util.function.Supplier; 8 | 9 | /** 10 | * Strategy interface for resolving a {@link McpJsonMapper}. 11 | */ 12 | public interface McpJsonMapperSupplier extends Supplier { 13 | 14 | } 15 | -------------------------------------------------------------------------------- /mcp-core/src/main/java/io/modelcontextprotocol/spec/McpServerTransport.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2024-2025 the original author or authors. 3 | */ 4 | 5 | package io.modelcontextprotocol.spec; 6 | 7 | /** 8 | * Marker interface for the server-side MCP transport. 9 | * 10 | * @author Christian Tzolov 11 | * @author Dariusz Jędrzejczyk 12 | */ 13 | public interface McpServerTransport extends McpTransport { 14 | 15 | } 16 | -------------------------------------------------------------------------------- /mcp-core/src/test/java/io/modelcontextprotocol/util/ToolsUtils.java: -------------------------------------------------------------------------------- 1 | package io.modelcontextprotocol.util; 2 | 3 | import io.modelcontextprotocol.spec.McpSchema; 4 | 5 | import java.util.Collections; 6 | 7 | public final class ToolsUtils { 8 | 9 | private ToolsUtils() { 10 | } 11 | 12 | public static final McpSchema.JsonSchema EMPTY_JSON_SCHEMA = new McpSchema.JsonSchema("object", 13 | Collections.emptyMap(), null, null, null, null); 14 | 15 | } 16 | -------------------------------------------------------------------------------- /mcp-test/src/main/java/io/modelcontextprotocol/util/ToolsUtils.java: -------------------------------------------------------------------------------- 1 | package io.modelcontextprotocol.util; 2 | 3 | import io.modelcontextprotocol.spec.McpSchema; 4 | 5 | import java.util.Collections; 6 | 7 | public final class ToolsUtils { 8 | 9 | private ToolsUtils() { 10 | } 11 | 12 | public static final McpSchema.JsonSchema EMPTY_JSON_SCHEMA = new McpSchema.JsonSchema("object", 13 | Collections.emptyMap(), null, null, null, null); 14 | 15 | } 16 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | pull_request: {} 5 | 6 | jobs: 7 | build: 8 | name: Build branch 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: Checkout source code 12 | uses: actions/checkout@v4 13 | 14 | - name: Set up JDK 17 15 | uses: actions/setup-java@v4 16 | with: 17 | java-version: '17' 18 | distribution: 'temurin' 19 | cache: 'maven' 20 | 21 | - name: Build 22 | run: mvn verify 23 | -------------------------------------------------------------------------------- /mcp-json/src/main/java/io/modelcontextprotocol/json/schema/JsonSchemaValidatorSupplier.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 - 2025 the original author or authors. 3 | */ 4 | 5 | package io.modelcontextprotocol.json.schema; 6 | 7 | import java.util.function.Supplier; 8 | 9 | /** 10 | * A supplier interface that provides a {@link JsonSchemaValidator} instance. 11 | * Implementations of this interface are expected to return a new or cached instance of 12 | * {@link JsonSchemaValidator} when {@link #get()} is invoked. 13 | * 14 | * @see JsonSchemaValidator 15 | * @see Supplier 16 | */ 17 | public interface JsonSchemaValidatorSupplier extends Supplier { 18 | 19 | } 20 | -------------------------------------------------------------------------------- /mcp-core/src/main/java/io/modelcontextprotocol/spec/ProtocolVersions.java: -------------------------------------------------------------------------------- 1 | package io.modelcontextprotocol.spec; 2 | 3 | public interface ProtocolVersions { 4 | 5 | /** 6 | * MCP protocol version for 2024-11-05. 7 | * https://modelcontextprotocol.io/specification/2024-11-05 8 | */ 9 | String MCP_2024_11_05 = "2024-11-05"; 10 | 11 | /** 12 | * MCP protocol version for 2025-03-26. 13 | * https://modelcontextprotocol.io/specification/2025-03-26 14 | */ 15 | String MCP_2025_03_26 = "2025-03-26"; 16 | 17 | /** 18 | * MCP protocol version for 2025-06-18. 19 | * https://modelcontextprotocol.io/specification/2025-06-18 20 | */ 21 | String MCP_2025_06_18 = "2025-06-18"; 22 | 23 | } 24 | -------------------------------------------------------------------------------- /mcp-core/src/main/java/io/modelcontextprotocol/server/McpNotificationHandler.java: -------------------------------------------------------------------------------- 1 | package io.modelcontextprotocol.server; 2 | 3 | import reactor.core.publisher.Mono; 4 | 5 | /** 6 | * A handler for client-initiated notifications. 7 | */ 8 | public interface McpNotificationHandler { 9 | 10 | /** 11 | * Handles a notification from the client. 12 | * @param exchange the exchange associated with the client that allows calling back to 13 | * the connected client or inspecting its capabilities. 14 | * @param params the parameters of the notification. 15 | * @return a Mono that completes once the notification is handled. 16 | */ 17 | Mono handle(McpAsyncServerExchange exchange, Object params); 18 | 19 | } 20 | -------------------------------------------------------------------------------- /mcp-core/src/main/java/io/modelcontextprotocol/server/McpInitRequestHandler.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2024-2025 the original author or authors. 3 | */ 4 | 5 | package io.modelcontextprotocol.server; 6 | 7 | import io.modelcontextprotocol.spec.McpSchema; 8 | import reactor.core.publisher.Mono; 9 | 10 | /** 11 | * Request handler for the initialization request. 12 | */ 13 | public interface McpInitRequestHandler { 14 | 15 | /** 16 | * Handles the initialization request. 17 | * @param initializeRequest the initialization request by the client 18 | * @return a Mono that will emit the result of the initialization 19 | */ 20 | Mono handle(McpSchema.InitializeRequest initializeRequest); 21 | 22 | } 23 | -------------------------------------------------------------------------------- /mcp-core/src/test/java/io/modelcontextprotocol/client/ServerParameterUtils.java: -------------------------------------------------------------------------------- 1 | package io.modelcontextprotocol.client; 2 | 3 | import io.modelcontextprotocol.client.transport.ServerParameters; 4 | 5 | public final class ServerParameterUtils { 6 | 7 | private ServerParameterUtils() { 8 | } 9 | 10 | public static ServerParameters createServerParameters() { 11 | if (System.getProperty("os.name").toLowerCase().contains("win")) { 12 | return ServerParameters.builder("cmd.exe") 13 | .args("/c", "npx.cmd", "-y", "@modelcontextprotocol/server-everything", "stdio") 14 | .build(); 15 | } 16 | return ServerParameters.builder("npx").args("-y", "@modelcontextprotocol/server-everything", "stdio").build(); 17 | } 18 | 19 | } 20 | -------------------------------------------------------------------------------- /mcp-spring/mcp-spring-webmvc/README.md: -------------------------------------------------------------------------------- 1 | # WebMVC SSE Server Transport 2 | 3 | ```xml 4 | 5 | io.modelcontextprotocol.sdk 6 | mcp-spring-webmvc 7 | 8 | ``` 9 | 10 | 11 | 12 | ```java 13 | String MESSAGE_ENDPOINT = "/mcp/message"; 14 | 15 | @Configuration 16 | @EnableWebMvc 17 | static class MyConfig { 18 | 19 | @Bean 20 | public WebMvcSseServerTransport webMvcSseServerTransport() { 21 | return new WebMvcSseServerTransport(new ObjectMapper(), MESSAGE_ENDPOINT); 22 | } 23 | 24 | @Bean 25 | public RouterFunction routerFunction(WebMvcSseServerTransport transport) { 26 | return transport.getRouterFunction(); 27 | } 28 | } 29 | ``` 30 | -------------------------------------------------------------------------------- /mcp-core/src/main/java/io/modelcontextprotocol/spec/McpTransportSessionClosedException.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025-2025 the original author or authors. 3 | */ 4 | 5 | package io.modelcontextprotocol.spec; 6 | 7 | import reactor.util.annotation.Nullable; 8 | 9 | /** 10 | * Exception thrown when trying to use an {@link McpTransportSession} that has been 11 | * closed. 12 | * 13 | * @see ClosedMcpTransportSession 14 | * @author Daniel Garnier-Moiroux 15 | */ 16 | public class McpTransportSessionClosedException extends RuntimeException { 17 | 18 | public McpTransportSessionClosedException(@Nullable String sessionId) { 19 | super(sessionId != null ? "MCP session with ID %s has been closed".formatted(sessionId) 20 | : "MCP session has been closed"); 21 | } 22 | 23 | } 24 | -------------------------------------------------------------------------------- /mcp-core/src/test/java/io/modelcontextprotocol/spec/McpErrorTest.java: -------------------------------------------------------------------------------- 1 | package io.modelcontextprotocol.spec; 2 | 3 | import org.junit.jupiter.api.Test; 4 | 5 | import java.util.Map; 6 | 7 | import static org.junit.jupiter.api.Assertions.assertNotNull; 8 | import static org.junit.jupiter.api.Assertions.assertEquals; 9 | 10 | class McpErrorTest { 11 | 12 | @Test 13 | void testNotFound() { 14 | String uri = "file:///nonexistent.txt"; 15 | McpError mcpError = McpError.RESOURCE_NOT_FOUND.apply(uri); 16 | assertNotNull(mcpError.getJsonRpcError()); 17 | assertEquals(-32002, mcpError.getJsonRpcError().code()); 18 | assertEquals("Resource not found", mcpError.getJsonRpcError().message()); 19 | assertEquals(Map.of("uri", uri), mcpError.getJsonRpcError().data()); 20 | } 21 | 22 | } -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/miscellaneous.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Miscellaneous 3 | about: Suggest an improvement for this project 4 | title: '' 5 | labels: 'status: waiting-for-triage' 6 | assignees: '' 7 | 8 | --- 9 | 10 | For anything other than bug reports and feature requests (performance, refactoring, etc), 11 | just go ahead and file the issue. Please provide as many details as possible. 12 | 13 | If you have a question or a support request, please open a new discussion on [GitHub Discussions](https://github.com/modelcontextprotocol/java-sdk/discussions) 14 | 15 | Please do **not** create issues on the [Issue Tracker](https://github.com/modelcontextprotocol/java-sdk/issues) for questions or support requests. 16 | We would like to keep the issue tracker **exclusively** for bug reports and feature requests. 17 | -------------------------------------------------------------------------------- /mcp-core/src/main/java/io/modelcontextprotocol/server/McpRequestHandler.java: -------------------------------------------------------------------------------- 1 | package io.modelcontextprotocol.server; 2 | 3 | import reactor.core.publisher.Mono; 4 | 5 | /** 6 | * A handler for client-initiated requests. 7 | * 8 | * @param the type of the response that is expected as a result of handling the 9 | * request. 10 | */ 11 | public interface McpRequestHandler { 12 | 13 | /** 14 | * Handles a request from the client. 15 | * @param exchange the exchange associated with the client that allows calling back to 16 | * the connected client or inspecting its capabilities. 17 | * @param params the parameters of the request. 18 | * @return a Mono that will emit the response to the request. 19 | */ 20 | Mono handle(McpAsyncServerExchange exchange, Object params); 21 | 22 | } 23 | -------------------------------------------------------------------------------- /mcp-core/src/main/java/io/modelcontextprotocol/spec/McpStreamableServerTransport.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2024-2025 the original author or authors. 3 | */ 4 | 5 | package io.modelcontextprotocol.spec; 6 | 7 | import reactor.core.publisher.Mono; 8 | 9 | /** 10 | * Streamable HTTP server transport representing an individual SSE stream. 11 | * 12 | * @author Dariusz Jędrzejczyk 13 | */ 14 | public interface McpStreamableServerTransport extends McpServerTransport { 15 | 16 | /** 17 | * Send a message to the client with a message ID for use in the SSE event payload 18 | * @param message the JSON-RPC payload 19 | * @param messageId message id for SSE events 20 | * @return Mono which completes when done 21 | */ 22 | Mono sendMessage(McpSchema.JSONRPCMessage message, String messageId); 23 | 24 | } 25 | -------------------------------------------------------------------------------- /mcp-core/src/main/java/io/modelcontextprotocol/util/McpUriTemplateManagerFactory.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 - 2025 the original author or authors. 3 | */ 4 | 5 | package io.modelcontextprotocol.util; 6 | 7 | /** 8 | * Factory interface for creating instances of {@link McpUriTemplateManager}. 9 | * 10 | * @author Christian Tzolov 11 | */ 12 | public interface McpUriTemplateManagerFactory { 13 | 14 | /** 15 | * Creates a new instance of {@link McpUriTemplateManager} with the specified URI 16 | * template. 17 | * @param uriTemplate The URI template to be used for variable extraction 18 | * @return A new instance of {@link McpUriTemplateManager} 19 | * @throws IllegalArgumentException if the URI template is null or empty 20 | */ 21 | McpUriTemplateManager create(String uriTemplate); 22 | 23 | } 24 | -------------------------------------------------------------------------------- /mcp-core/src/test/resources/logback.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | %d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /mcp-spring/mcp-spring-webflux/src/test/java/io/modelcontextprotocol/client/WebClientStreamableHttpAsyncClientResiliencyTests.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025-2025 the original author or authors. 3 | */ 4 | 5 | package io.modelcontextprotocol.client; 6 | 7 | import io.modelcontextprotocol.client.transport.WebClientStreamableHttpTransport; 8 | import io.modelcontextprotocol.spec.McpClientTransport; 9 | import org.junit.jupiter.api.Timeout; 10 | import org.springframework.web.reactive.function.client.WebClient; 11 | 12 | @Timeout(15) 13 | public class WebClientStreamableHttpAsyncClientResiliencyTests extends AbstractMcpAsyncClientResiliencyTests { 14 | 15 | @Override 16 | protected McpClientTransport createMcpTransport() { 17 | return WebClientStreamableHttpTransport.builder(WebClient.builder().baseUrl(host)).build(); 18 | } 19 | 20 | } 21 | -------------------------------------------------------------------------------- /mcp-spring/mcp-spring-webflux/src/test/resources/logback.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | %d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /mcp-core/src/main/java/io/modelcontextprotocol/util/DefaultMcpUriTemplateManagerFactory.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 - 2025 the original author or authors. 3 | */ 4 | 5 | package io.modelcontextprotocol.util; 6 | 7 | /** 8 | * @author Christian Tzolov 9 | */ 10 | public class DefaultMcpUriTemplateManagerFactory implements McpUriTemplateManagerFactory { 11 | 12 | /** 13 | * Creates a new instance of {@link McpUriTemplateManager} with the specified URI 14 | * template. 15 | * @param uriTemplate The URI template to be used for variable extraction 16 | * @return A new instance of {@link McpUriTemplateManager} 17 | * @throws IllegalArgumentException if the URI template is null or empty 18 | */ 19 | @Override 20 | public McpUriTemplateManager create(String uriTemplate) { 21 | return new DefaultMcpUriTemplateManager(uriTemplate); 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /mcp-core/src/main/java/io/modelcontextprotocol/server/McpStatelessNotificationHandler.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2024-2025 the original author or authors. 3 | */ 4 | 5 | package io.modelcontextprotocol.server; 6 | 7 | import io.modelcontextprotocol.common.McpTransportContext; 8 | import reactor.core.publisher.Mono; 9 | 10 | /** 11 | * Handler for MCP notifications in a stateless server. 12 | * 13 | * @author Dariusz Jędrzejczyk 14 | */ 15 | public interface McpStatelessNotificationHandler { 16 | 17 | /** 18 | * Handle to notification and complete once done. 19 | * @param transportContext {@link McpTransportContext} associated with the transport 20 | * @param params the payload of the MCP notification 21 | * @return Mono which completes once the processing is done 22 | */ 23 | Mono handle(McpTransportContext transportContext, Object params); 24 | 25 | } 26 | -------------------------------------------------------------------------------- /mcp-core/src/main/java/io/modelcontextprotocol/server/McpStatelessRequestHandler.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2024-2025 the original author or authors. 3 | */ 4 | 5 | package io.modelcontextprotocol.server; 6 | 7 | import io.modelcontextprotocol.common.McpTransportContext; 8 | import reactor.core.publisher.Mono; 9 | 10 | /** 11 | * Handler for MCP requests in a stateless server. 12 | * 13 | * @param type of the MCP response 14 | * @author Dariusz Jędrzejczyk 15 | */ 16 | public interface McpStatelessRequestHandler { 17 | 18 | /** 19 | * Handle the request and complete with a result. 20 | * @param transportContext {@link McpTransportContext} associated with the transport 21 | * @param params the payload of the MCP request 22 | * @return Mono which completes with the response object 23 | */ 24 | Mono handle(McpTransportContext transportContext, Object params); 25 | 26 | } 27 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | Thank you for helping us keep the SDKs and systems they interact with secure. 4 | 5 | ## Reporting Security Issues 6 | 7 | This SDK is maintained by [Anthropic](https://www.anthropic.com/) as part of the Model 8 | Context Protocol project. 9 | 10 | The security of our systems and user data is Anthropic’s top priority. We appreciate the 11 | work of security researchers acting in good faith in identifying and reporting potential 12 | vulnerabilities. 13 | 14 | Our security program is managed on HackerOne and we ask that any validated vulnerability 15 | in this functionality be reported through their 16 | [submission form](https://hackerone.com/anthropic-vdp/reports/new?type=team&report_type=vulnerability). 17 | 18 | ## Vulnerability Disclosure Program 19 | 20 | Our Vulnerability Program Guidelines are defined on our 21 | [HackerOne program page](https://hackerone.com/anthropic-vdp). -------------------------------------------------------------------------------- /mcp-core/src/main/java/io/modelcontextprotocol/spec/McpServerTransportProvider.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2024-2025 the original author or authors. 3 | */ 4 | 5 | package io.modelcontextprotocol.spec; 6 | 7 | /** 8 | * Classic implementation of {@link McpServerTransportProviderBase} for a single outgoing 9 | * stream in bidirectional communication (STDIO and the legacy HTTP SSE). 10 | * 11 | * @author Dariusz Jędrzejczyk 12 | */ 13 | public interface McpServerTransportProvider extends McpServerTransportProviderBase { 14 | 15 | /** 16 | * Sets the session factory that will be used to create sessions for new clients. An 17 | * implementation of the MCP server MUST call this method before any MCP interactions 18 | * take place. 19 | * @param sessionFactory the session factory to be used for initiating client sessions 20 | */ 21 | void setSessionFactory(McpServerSession.Factory sessionFactory); 22 | 23 | } 24 | -------------------------------------------------------------------------------- /mcp-test/src/main/java/io/modelcontextprotocol/server/TestUtil.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 - 2025 the original author or authors. 3 | */ 4 | 5 | package io.modelcontextprotocol.server; 6 | 7 | import java.io.IOException; 8 | import java.net.InetSocketAddress; 9 | import java.net.ServerSocket; 10 | 11 | public class TestUtil { 12 | 13 | TestUtil() { 14 | // Prevent instantiation 15 | } 16 | 17 | /** 18 | * Finds an available port on the local machine. 19 | * @return an available port number 20 | * @throws IllegalStateException if no available port can be found 21 | */ 22 | public static int findAvailablePort() { 23 | try (final ServerSocket socket = new ServerSocket()) { 24 | socket.bind(new InetSocketAddress(0)); 25 | return socket.getLocalPort(); 26 | } 27 | catch (final IOException e) { 28 | throw new IllegalStateException("Cannot bind to an available port!", e); 29 | } 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /mcp-spring/mcp-spring-webmvc/src/test/resources/logback.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | %d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: 'status: waiting-for-triage, type: feature' 6 | assignees: '' 7 | 8 | --- 9 | 10 | Please do a quick search on GitHub issues first, the feature you are about to request might have already been requested. 11 | 12 | **Expected Behavior** 13 | 14 | 15 | 16 | **Current Behavior** 17 | 18 | 19 | 20 | **Context** 21 | 22 | 28 | -------------------------------------------------------------------------------- /mcp-core/src/main/java/io/modelcontextprotocol/server/McpTransportContextExtractor.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2024-2025 the original author or authors. 3 | */ 4 | 5 | package io.modelcontextprotocol.server; 6 | 7 | import io.modelcontextprotocol.common.McpTransportContext; 8 | 9 | /** 10 | * The contract for extracting metadata from a generic transport request of type 11 | * {@link T}. 12 | * 13 | * @param transport-specific representation of the request which allows extracting 14 | * metadata for use in the MCP features implementations. 15 | * @author Dariusz Jędrzejczyk 16 | */ 17 | public interface McpTransportContextExtractor { 18 | 19 | /** 20 | * Extract transport-specific metadata from the request into an McpTransportContext. 21 | * @param request the generic representation for the request in the context of a 22 | * specific transport implementation 23 | * @return the context containing the metadata 24 | */ 25 | McpTransportContext extract(T request); 26 | 27 | } 28 | -------------------------------------------------------------------------------- /mcp-core/src/main/java/io/modelcontextprotocol/spec/McpLoggableSession.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2024-2025 the original author or authors. 3 | */ 4 | 5 | package io.modelcontextprotocol.spec; 6 | 7 | /** 8 | * An {@link McpSession} which is capable of processing logging notifications and keeping 9 | * track of a min logging level. 10 | * 11 | * @author Dariusz Jędrzejczyk 12 | */ 13 | public interface McpLoggableSession extends McpSession { 14 | 15 | /** 16 | * Set the minimum logging level for the client. Messages below this level will be 17 | * filtered out. 18 | * @param minLoggingLevel The minimum logging level 19 | */ 20 | void setMinLoggingLevel(McpSchema.LoggingLevel minLoggingLevel); 21 | 22 | /** 23 | * Allows checking whether a particular logging level is allowed. 24 | * @param loggingLevel the level to check 25 | * @return whether the logging at the specified level is permitted. 26 | */ 27 | boolean isNotificationForLevelAllowed(McpSchema.LoggingLevel loggingLevel); 28 | 29 | } 30 | -------------------------------------------------------------------------------- /.mvn/wrapper/maven-wrapper.properties: -------------------------------------------------------------------------------- 1 | # Licensed to the Apache Software Foundation (ASF) under one 2 | # or more contributor license agreements. See the NOTICE file 3 | # distributed with this work for additional information 4 | # regarding copyright ownership. The ASF licenses this file 5 | # to you under the Apache License, Version 2.0 (the 6 | # "License"); you may not use this file except in compliance 7 | # with the License. You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, 12 | # software distributed under the License is distributed on an 13 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 14 | # KIND, either express or implied. See the License for the 15 | # specific language governing permissions and limitations 16 | # under the License. 17 | wrapperVersion=3.3.2 18 | distributionType=only-script 19 | distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.9/apache-maven-3.9.9-bin.zip 20 | -------------------------------------------------------------------------------- /mcp-core/src/test/java/io/modelcontextprotocol/server/ServletSseMcpSyncServerTests.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2024-2024 the original author or authors. 3 | */ 4 | 5 | package io.modelcontextprotocol.server; 6 | 7 | import io.modelcontextprotocol.server.transport.HttpServletSseServerTransportProvider; 8 | import io.modelcontextprotocol.spec.McpServerTransportProvider; 9 | import org.junit.jupiter.api.Timeout; 10 | 11 | /** 12 | * Tests for {@link McpSyncServer} using {@link HttpServletSseServerTransportProvider}. 13 | * 14 | * @author Christian Tzolov 15 | */ 16 | @Timeout(15) // Giving extra time beyond the client timeout 17 | class ServletSseMcpSyncServerTests extends AbstractMcpSyncServerTests { 18 | 19 | protected McpServerTransportProvider createMcpTransportProvider() { 20 | return HttpServletSseServerTransportProvider.builder().messageEndpoint("/mcp/message").build(); 21 | } 22 | 23 | @Override 24 | protected McpServer.SyncSpecification prepareSyncServerBuilder() { 25 | return McpServer.sync(createMcpTransportProvider()); 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /mcp-core/src/test/java/io/modelcontextprotocol/server/ServletSseMcpAsyncServerTests.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2024-2024 the original author or authors. 3 | */ 4 | 5 | package io.modelcontextprotocol.server; 6 | 7 | import io.modelcontextprotocol.server.transport.HttpServletSseServerTransportProvider; 8 | import io.modelcontextprotocol.spec.McpServerTransportProvider; 9 | import org.junit.jupiter.api.Timeout; 10 | 11 | /** 12 | * Tests for {@link McpAsyncServer} using {@link HttpServletSseServerTransportProvider}. 13 | * 14 | * @author Christian Tzolov 15 | */ 16 | @Timeout(15) // Giving extra time beyond the client timeout 17 | class ServletSseMcpAsyncServerTests extends AbstractMcpAsyncServerTests { 18 | 19 | protected McpServerTransportProvider createMcpTransportProvider() { 20 | return HttpServletSseServerTransportProvider.builder().messageEndpoint("/mcp/message").build(); 21 | } 22 | 23 | @Override 24 | protected McpServer.AsyncSpecification prepareAsyncServerBuilder() { 25 | return McpServer.async(createMcpTransportProvider()); 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /mcp-core/src/test/java/io/modelcontextprotocol/server/StdioMcpAsyncServerTests.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2024-2024 the original author or authors. 3 | */ 4 | 5 | package io.modelcontextprotocol.server; 6 | 7 | import io.modelcontextprotocol.server.transport.StdioServerTransportProvider; 8 | import io.modelcontextprotocol.spec.McpServerTransportProvider; 9 | import org.junit.jupiter.api.Timeout; 10 | 11 | import static io.modelcontextprotocol.util.McpJsonMapperUtils.JSON_MAPPER; 12 | 13 | /** 14 | * Tests for {@link McpAsyncServer} using {@link StdioServerTransport}. 15 | * 16 | * @author Christian Tzolov 17 | */ 18 | @Timeout(15) // Giving extra time beyond the client timeout 19 | class StdioMcpAsyncServerTests extends AbstractMcpAsyncServerTests { 20 | 21 | protected McpServerTransportProvider createMcpTransportProvider() { 22 | return new StdioServerTransportProvider(JSON_MAPPER); 23 | } 24 | 25 | @Override 26 | protected McpServer.AsyncSpecification prepareAsyncServerBuilder() { 27 | return McpServer.async(createMcpTransportProvider()); 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /mcp-core/src/test/java/io/modelcontextprotocol/server/StdioMcpSyncServerTests.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2024-2024 the original author or authors. 3 | */ 4 | 5 | package io.modelcontextprotocol.server; 6 | 7 | import io.modelcontextprotocol.server.transport.StdioServerTransportProvider; 8 | import io.modelcontextprotocol.spec.McpServerTransportProvider; 9 | import org.junit.jupiter.api.Timeout; 10 | 11 | import static io.modelcontextprotocol.util.McpJsonMapperUtils.JSON_MAPPER; 12 | 13 | /** 14 | * Tests for {@link McpSyncServer} using {@link StdioServerTransportProvider}. 15 | * 16 | * @author Christian Tzolov 17 | */ 18 | @Timeout(15) // Giving extra time beyond the client timeout 19 | class StdioMcpSyncServerTests extends AbstractMcpSyncServerTests { 20 | 21 | protected McpServerTransportProvider createMcpTransportProvider() { 22 | return new StdioServerTransportProvider(JSON_MAPPER); 23 | } 24 | 25 | @Override 26 | protected McpServer.SyncSpecification prepareSyncServerBuilder() { 27 | return McpServer.sync(createMcpTransportProvider()); 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /mcp-core/src/test/java/io/modelcontextprotocol/server/HttpServletStreamableSyncServerTests.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2024-2024 the original author or authors. 3 | */ 4 | 5 | package io.modelcontextprotocol.server; 6 | 7 | import org.junit.jupiter.api.Timeout; 8 | 9 | import io.modelcontextprotocol.server.transport.HttpServletStreamableServerTransportProvider; 10 | import io.modelcontextprotocol.spec.McpStreamableServerTransportProvider; 11 | 12 | /** 13 | * Tests for {@link McpSyncServer} using 14 | * {@link HttpServletStreamableServerTransportProvider}. 15 | * 16 | * @author Christian Tzolov 17 | */ 18 | @Timeout(15) 19 | class HttpServletStreamableSyncServerTests extends AbstractMcpSyncServerTests { 20 | 21 | protected McpStreamableServerTransportProvider createMcpTransportProvider() { 22 | return HttpServletStreamableServerTransportProvider.builder().mcpEndpoint("/mcp/message").build(); 23 | } 24 | 25 | @Override 26 | protected McpServer.SyncSpecification prepareSyncServerBuilder() { 27 | return McpServer.sync(createMcpTransportProvider()); 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /mcp-json-jackson2/src/main/java/io/modelcontextprotocol/json/schema/jackson/JacksonJsonSchemaValidatorSupplier.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 - 2025 the original author or authors. 3 | */ 4 | 5 | package io.modelcontextprotocol.json.schema.jackson; 6 | 7 | import io.modelcontextprotocol.json.schema.JsonSchemaValidator; 8 | import io.modelcontextprotocol.json.schema.JsonSchemaValidatorSupplier; 9 | 10 | /** 11 | * A concrete implementation of {@link JsonSchemaValidatorSupplier} that provides a 12 | * {@link JsonSchemaValidator} instance based on the Jackson library. 13 | * 14 | * @see JsonSchemaValidatorSupplier 15 | * @see JsonSchemaValidator 16 | */ 17 | public class JacksonJsonSchemaValidatorSupplier implements JsonSchemaValidatorSupplier { 18 | 19 | /** 20 | * Returns a new instance of {@link JsonSchemaValidator} that uses the Jackson library 21 | * for JSON schema validation. 22 | * @return A {@link JsonSchemaValidator} instance. 23 | */ 24 | @Override 25 | public JsonSchemaValidator get() { 26 | return new DefaultJsonSchemaValidator(); 27 | } 28 | 29 | } 30 | -------------------------------------------------------------------------------- /mcp-core/src/test/java/io/modelcontextprotocol/server/HttpServletStreamableAsyncServerTests.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2024-2024 the original author or authors. 3 | */ 4 | 5 | package io.modelcontextprotocol.server; 6 | 7 | import org.junit.jupiter.api.Timeout; 8 | 9 | import io.modelcontextprotocol.server.transport.HttpServletStreamableServerTransportProvider; 10 | import io.modelcontextprotocol.spec.McpStreamableServerTransportProvider; 11 | 12 | /** 13 | * Tests for {@link McpAsyncServer} using 14 | * {@link HttpServletStreamableServerTransportProvider}. 15 | * 16 | * @author Christian Tzolov 17 | */ 18 | @Timeout(15) 19 | class HttpServletStreamableAsyncServerTests extends AbstractMcpAsyncServerTests { 20 | 21 | protected McpStreamableServerTransportProvider createMcpTransportProvider() { 22 | return HttpServletStreamableServerTransportProvider.builder().mcpEndpoint("/mcp/message").build(); 23 | } 24 | 25 | @Override 26 | protected McpServer.AsyncSpecification prepareAsyncServerBuilder() { 27 | return McpServer.async(createMcpTransportProvider()); 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /mcp-core/src/main/java/io/modelcontextprotocol/client/transport/customizer/McpSyncHttpClientRequestCustomizer.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2024-2025 the original author or authors. 3 | */ 4 | 5 | package io.modelcontextprotocol.client.transport.customizer; 6 | 7 | import java.net.URI; 8 | import java.net.http.HttpRequest; 9 | 10 | import reactor.util.annotation.Nullable; 11 | 12 | import io.modelcontextprotocol.client.McpClient.SyncSpec; 13 | import io.modelcontextprotocol.common.McpTransportContext; 14 | 15 | /** 16 | * Customize {@link HttpRequest.Builder} before executing the request, either in SSE or 17 | * Streamable HTTP transport. Do not rely on thread-locals in this implementation, instead 18 | * use {@link SyncSpec#transportContextProvider} to extract context, and then consume it 19 | * through {@link McpTransportContext}. 20 | * 21 | * @author Daniel Garnier-Moiroux 22 | */ 23 | public interface McpSyncHttpClientRequestCustomizer { 24 | 25 | void customize(HttpRequest.Builder builder, String method, URI endpoint, @Nullable String body, 26 | McpTransportContext context); 27 | 28 | } 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ### Maven/Gradle Builds ### 2 | target/ 3 | !.mvn/wrapper/maven-wrapper.jar 4 | !**/src/main/**/target/ 5 | !**/src/test/**/target/ 6 | build/ 7 | !**/src/main/**/build/ 8 | !**/src/test/**/build/ 9 | .gradle 10 | out 11 | /.gradletasknamecache 12 | **/*.flattened-pom.xml 13 | 14 | ### IDE - Eclipse/STS ### 15 | .apt_generated 16 | .classpath 17 | .factorypath 18 | .project 19 | .settings/ 20 | .springBeans 21 | .sts4-cache 22 | bin/ 23 | com.springsource.sts.config.flow.prefs 24 | 25 | ### IDE - IntelliJ IDEA ### 26 | .idea/ 27 | *.iml 28 | *.ipr 29 | *.iws 30 | 31 | ### IDE - NetBeans ### 32 | /nbproject/private/ 33 | /nbbuild/ 34 | /dist/ 35 | /nbdist/ 36 | /.nb-gradle/ 37 | 38 | ### IDE - VS Code ### 39 | .vscode/ 40 | vscode/ 41 | settings.json 42 | 43 | ### Logs and Databases ### 44 | build.log 45 | shell.log 46 | integration-repo 47 | ivy-cache 48 | spring-build 49 | derby-home 50 | derbydb 51 | derby.log 52 | 53 | ### Node.js ### 54 | node/ 55 | node_modules/ 56 | package-lock.json 57 | package.json 58 | 59 | ### Other ### 60 | .antlr/ 61 | .profiler/ 62 | s3.properties 63 | .*.swp 64 | .DS_Store -------------------------------------------------------------------------------- /mcp-core/src/main/java/io/modelcontextprotocol/spec/McpStatelessServerTransport.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2024-2025 the original author or authors. 3 | */ 4 | 5 | package io.modelcontextprotocol.spec; 6 | 7 | import java.util.List; 8 | 9 | import io.modelcontextprotocol.server.McpStatelessServerHandler; 10 | import reactor.core.publisher.Mono; 11 | 12 | public interface McpStatelessServerTransport { 13 | 14 | void setMcpHandler(McpStatelessServerHandler mcpHandler); 15 | 16 | /** 17 | * Immediately closes all the transports with connected clients and releases any 18 | * associated resources. 19 | */ 20 | default void close() { 21 | this.closeGracefully().subscribe(); 22 | } 23 | 24 | /** 25 | * Gracefully closes all the transports with connected clients and releases any 26 | * associated resources asynchronously. 27 | * @return a {@link Mono} that completes when the connections have been closed. 28 | */ 29 | Mono closeGracefully(); 30 | 31 | default List protocolVersions() { 32 | return List.of(ProtocolVersions.MCP_2025_03_26, ProtocolVersions.MCP_2025_06_18); 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a bug report to help us improve the project 4 | title: '' 5 | labels: 'type: bug, status: waiting-for-triage' 6 | assignees: '' 7 | 8 | --- 9 | 10 | Please do a quick search on GitHub issues first, there might be already a duplicate issue for the one you are about to create. 11 | If the bug is trivial, just go ahead and create the issue. Otherwise, please take a few moments and fill in the following sections: 12 | 13 | **Bug description** 14 | A clear and concise description of what the bug is about. 15 | 16 | **Environment** 17 | Please provide as many details as possible: Spring MCP version, Java version, which vector store you use if any, etc 18 | 19 | **Steps to reproduce** 20 | Steps to reproduce the issue. 21 | 22 | **Expected behavior** 23 | A clear and concise description of what you expected to happen. 24 | 25 | **Minimal Complete Reproducible example** 26 | Please provide a failing test or a minimal complete verifiable example that reproduces the issue. 27 | Bug reports that are reproducible will take priority in resolution over reports that are not reproducible. 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 the original author or authors. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /mcp-core/src/test/java/io/modelcontextprotocol/spec/CompleteCompletionSerializationTest.java: -------------------------------------------------------------------------------- 1 | package io.modelcontextprotocol.spec; 2 | 3 | import io.modelcontextprotocol.json.McpJsonMapper; 4 | import org.junit.jupiter.api.Test; 5 | import java.io.IOException; 6 | import java.util.Collections; 7 | import static org.junit.jupiter.api.Assertions.assertEquals; 8 | 9 | class CompleteCompletionSerializationTest { 10 | 11 | @Test 12 | void codeCompletionSerialization() throws IOException { 13 | McpJsonMapper jsonMapper = McpJsonMapper.getDefault(); 14 | McpSchema.CompleteResult.CompleteCompletion codeComplete = new McpSchema.CompleteResult.CompleteCompletion( 15 | Collections.emptyList(), 0, false); 16 | String json = jsonMapper.writeValueAsString(codeComplete); 17 | String expected = """ 18 | {"values":[],"total":0,"hasMore":false}"""; 19 | assertEquals(expected, json, json); 20 | 21 | McpSchema.CompleteResult completeResult = new McpSchema.CompleteResult(codeComplete); 22 | json = jsonMapper.writeValueAsString(completeResult); 23 | expected = """ 24 | {"completion":{"values":[],"total":0,"hasMore":false}}"""; 25 | assertEquals(expected, json, json); 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /mcp-core/src/main/java/io/modelcontextprotocol/spec/McpTransportException.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 - 2025 the original author or authors. 3 | */ 4 | package io.modelcontextprotocol.spec; 5 | 6 | /** 7 | * Exception thrown when there is an issue with the transport layer of the Model Context 8 | * Protocol (MCP). 9 | * 10 | *

11 | * This exception is used to indicate errors that occur during communication between the 12 | * MCP client and server, such as connection failures, protocol violations, or unexpected 13 | * responses. 14 | * 15 | * @author Christian Tzolov 16 | */ 17 | public class McpTransportException extends RuntimeException { 18 | 19 | private static final long serialVersionUID = 1L; 20 | 21 | public McpTransportException(String message) { 22 | super(message); 23 | } 24 | 25 | public McpTransportException(String message, Throwable cause) { 26 | super(message, cause); 27 | } 28 | 29 | public McpTransportException(Throwable cause) { 30 | super(cause); 31 | } 32 | 33 | public McpTransportException(String message, Throwable cause, boolean enableSuppression, 34 | boolean writableStackTrace) { 35 | super(message, cause, enableSuppression, writableStackTrace); 36 | } 37 | 38 | } -------------------------------------------------------------------------------- /mcp-core/src/main/java/io/modelcontextprotocol/spec/McpTransportSessionNotFoundException.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2024-2025 the original author or authors. 3 | */ 4 | 5 | package io.modelcontextprotocol.spec; 6 | 7 | /** 8 | * Exception that signifies that the server does not recognize the connecting client via 9 | * the presented transport session identifier. 10 | * 11 | * @author Dariusz Jędrzejczyk 12 | */ 13 | public class McpTransportSessionNotFoundException extends RuntimeException { 14 | 15 | /** 16 | * Construct an instance with a known {@link Exception cause}. 17 | * @param sessionId transport session identifier 18 | * @param cause the cause that was identified as a session not found error 19 | */ 20 | public McpTransportSessionNotFoundException(String sessionId, Exception cause) { 21 | super("Session " + sessionId + " not found on the server", cause); 22 | } 23 | 24 | /** 25 | * Construct an instance with the session identifier but without a {@link Exception 26 | * cause}. 27 | * @param sessionId transport session identifier 28 | */ 29 | public McpTransportSessionNotFoundException(String sessionId) { 30 | super("Session " + sessionId + " not found on the server"); 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /mcp-core/src/main/java/io/modelcontextprotocol/common/DefaultMcpTransportContext.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2024-2025 the original author or authors. 3 | */ 4 | 5 | package io.modelcontextprotocol.common; 6 | 7 | import java.util.Map; 8 | 9 | import io.modelcontextprotocol.util.Assert; 10 | 11 | /** 12 | * Default implementation for {@link McpTransportContext} which uses a map as storage. 13 | * 14 | * @author Dariusz Jędrzejczyk 15 | * @author Daniel Garnier-Moiroux 16 | */ 17 | class DefaultMcpTransportContext implements McpTransportContext { 18 | 19 | private final Map metadata; 20 | 21 | DefaultMcpTransportContext(Map metadata) { 22 | Assert.notNull(metadata, "The metadata cannot be null"); 23 | this.metadata = metadata; 24 | } 25 | 26 | @Override 27 | public Object get(String key) { 28 | return this.metadata.get(key); 29 | } 30 | 31 | @Override 32 | public boolean equals(Object o) { 33 | if (o == null || getClass() != o.getClass()) 34 | return false; 35 | 36 | DefaultMcpTransportContext that = (DefaultMcpTransportContext) o; 37 | return this.metadata.equals(that.metadata); 38 | } 39 | 40 | @Override 41 | public int hashCode() { 42 | return this.metadata.hashCode(); 43 | } 44 | 45 | } 46 | -------------------------------------------------------------------------------- /mcp-json-jackson2/src/main/java/io/modelcontextprotocol/json/jackson/JacksonMcpJsonMapperSupplier.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 - 2025 the original author or authors. 3 | */ 4 | 5 | package io.modelcontextprotocol.json.jackson; 6 | 7 | import io.modelcontextprotocol.json.McpJsonMapper; 8 | import io.modelcontextprotocol.json.McpJsonMapperSupplier; 9 | 10 | /** 11 | * A supplier of {@link McpJsonMapper} instances that uses the Jackson library for JSON 12 | * serialization and deserialization. 13 | *

14 | * This implementation provides a {@link McpJsonMapper} backed by a Jackson 15 | * {@link com.fasterxml.jackson.databind.ObjectMapper}. 16 | */ 17 | public class JacksonMcpJsonMapperSupplier implements McpJsonMapperSupplier { 18 | 19 | /** 20 | * Returns a new instance of {@link McpJsonMapper} that uses the Jackson library for 21 | * JSON serialization and deserialization. 22 | *

23 | * The returned {@link McpJsonMapper} is backed by a new instance of 24 | * {@link com.fasterxml.jackson.databind.ObjectMapper}. 25 | * @return a new {@link McpJsonMapper} instance 26 | */ 27 | @Override 28 | public McpJsonMapper get() { 29 | return new JacksonMcpJsonMapper(new com.fasterxml.jackson.databind.ObjectMapper()); 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /.github/workflows/maven-central-release.yml: -------------------------------------------------------------------------------- 1 | name: Release to Maven Central 2 | 3 | on: 4 | workflow_dispatch: 5 | 6 | jobs: 7 | publish: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v4 11 | 12 | - name: Set up Java 13 | uses: actions/setup-java@v4 14 | with: 15 | java-version: '17' 16 | distribution: 'temurin' 17 | cache: 'maven' 18 | server-id: central 19 | server-username: MAVEN_USERNAME 20 | server-password: MAVEN_PASSWORD 21 | gpg-private-key: ${{ secrets.GPG_SECRET_KEY }} 22 | gpg-passphrase: MAVEN_GPG_PASSPHRASE 23 | 24 | - name: Setup Node.js 25 | uses: actions/setup-node@v4 26 | with: 27 | node-version: '20' 28 | 29 | - name: Build and Test 30 | run: mvn clean verify 31 | 32 | - name: Publish to Maven Central 33 | run: | 34 | mvn --batch-mode \ 35 | -Prelease \ 36 | -Pjavadoc \ 37 | deploy 38 | env: 39 | MAVEN_USERNAME: ${{ secrets.OSSRH_USERNAME }} 40 | MAVEN_PASSWORD: ${{ secrets.OSSRH_TOKEN }} 41 | MAVEN_GPG_PASSPHRASE: ${{ secrets.SIGNING_PASSPHRASE }} 42 | -------------------------------------------------------------------------------- /mcp-core/src/test/java/io/modelcontextprotocol/client/HttpClientStreamableHttpAsyncClientResiliencyTests.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2024-2024 the original author or authors. 3 | */ 4 | 5 | package io.modelcontextprotocol.client; 6 | 7 | import java.io.IOException; 8 | 9 | import org.junit.jupiter.api.Test; 10 | import org.junit.jupiter.api.Timeout; 11 | 12 | import io.modelcontextprotocol.client.transport.HttpClientStreamableHttpTransport; 13 | import io.modelcontextprotocol.spec.McpClientTransport; 14 | import reactor.test.StepVerifier; 15 | 16 | @Timeout(15) 17 | public class HttpClientStreamableHttpAsyncClientResiliencyTests extends AbstractMcpAsyncClientResiliencyTests { 18 | 19 | @Override 20 | protected McpClientTransport createMcpTransport() { 21 | return HttpClientStreamableHttpTransport.builder(host).build(); 22 | } 23 | 24 | @Test 25 | void testPingWithExactExceptionType() { 26 | withClient(createMcpTransport(), mcpAsyncClient -> { 27 | StepVerifier.create(mcpAsyncClient.initialize()).expectNextCount(1).verifyComplete(); 28 | 29 | disconnect(); 30 | 31 | StepVerifier.create(mcpAsyncClient.ping()).expectError(IOException.class).verify(); 32 | 33 | reconnect(); 34 | 35 | StepVerifier.create(mcpAsyncClient.ping()).expectNextCount(1).verifyComplete(); 36 | }); 37 | } 38 | 39 | } 40 | -------------------------------------------------------------------------------- /mcp-core/src/main/java/io/modelcontextprotocol/client/transport/customizer/DelegatingMcpSyncHttpClientRequestCustomizer.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2024-2025 the original author or authors. 3 | */ 4 | 5 | package io.modelcontextprotocol.client.transport.customizer; 6 | 7 | import java.net.URI; 8 | import java.net.http.HttpRequest; 9 | import java.util.List; 10 | 11 | import io.modelcontextprotocol.common.McpTransportContext; 12 | import io.modelcontextprotocol.util.Assert; 13 | 14 | /** 15 | * Composable {@link McpSyncHttpClientRequestCustomizer} that applies multiple 16 | * customizers, in order. 17 | * 18 | * @author Daniel Garnier-Moiroux 19 | */ 20 | public class DelegatingMcpSyncHttpClientRequestCustomizer implements McpSyncHttpClientRequestCustomizer { 21 | 22 | private final List delegates; 23 | 24 | public DelegatingMcpSyncHttpClientRequestCustomizer(List customizers) { 25 | Assert.notNull(customizers, "Customizers must not be null"); 26 | this.delegates = customizers; 27 | } 28 | 29 | @Override 30 | public void customize(HttpRequest.Builder builder, String method, URI endpoint, String body, 31 | McpTransportContext context) { 32 | this.delegates.forEach(delegate -> delegate.customize(builder, method, endpoint, body, context)); 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /mcp-core/src/main/java/io/modelcontextprotocol/server/McpStatelessServerHandler.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2024-2025 the original author or authors. 3 | */ 4 | 5 | package io.modelcontextprotocol.server; 6 | 7 | import io.modelcontextprotocol.common.McpTransportContext; 8 | import io.modelcontextprotocol.spec.McpSchema; 9 | import reactor.core.publisher.Mono; 10 | 11 | /** 12 | * Handler for MCP requests and notifications in a Stateless Streamable HTTP Server 13 | * context. 14 | * 15 | * @author Dariusz Jędrzejczyk 16 | */ 17 | public interface McpStatelessServerHandler { 18 | 19 | /** 20 | * Handle the request using user-provided feature implementations. 21 | * @param transportContext {@link McpTransportContext} carrying transport layer 22 | * metadata 23 | * @param request the request JSON object 24 | * @return Mono containing the JSON response 25 | */ 26 | Mono handleRequest(McpTransportContext transportContext, 27 | McpSchema.JSONRPCRequest request); 28 | 29 | /** 30 | * Handle the notification. 31 | * @param transportContext {@link McpTransportContext} carrying transport layer 32 | * metadata 33 | * @param notification the notification JSON object 34 | * @return Mono that completes once handling is finished 35 | */ 36 | Mono handleNotification(McpTransportContext transportContext, McpSchema.JSONRPCNotification notification); 37 | 38 | } 39 | -------------------------------------------------------------------------------- /mcp-core/src/test/java/io/modelcontextprotocol/MockMcpServerTransportProvider.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025-2025 the original author or authors. 3 | */ 4 | 5 | package io.modelcontextprotocol; 6 | 7 | import io.modelcontextprotocol.spec.McpSchema; 8 | import io.modelcontextprotocol.spec.McpServerSession; 9 | import io.modelcontextprotocol.spec.McpServerSession.Factory; 10 | import io.modelcontextprotocol.spec.McpServerTransportProvider; 11 | import reactor.core.publisher.Mono; 12 | 13 | /** 14 | * @author Christian Tzolov 15 | */ 16 | public class MockMcpServerTransportProvider implements McpServerTransportProvider { 17 | 18 | private McpServerSession session; 19 | 20 | private final MockMcpServerTransport transport; 21 | 22 | public MockMcpServerTransportProvider(MockMcpServerTransport transport) { 23 | this.transport = transport; 24 | } 25 | 26 | public MockMcpServerTransport getTransport() { 27 | return transport; 28 | } 29 | 30 | @Override 31 | public void setSessionFactory(Factory sessionFactory) { 32 | 33 | session = sessionFactory.create(transport); 34 | } 35 | 36 | @Override 37 | public Mono notifyClients(String method, Object params) { 38 | return session.sendNotification(method, params); 39 | } 40 | 41 | @Override 42 | public Mono closeGracefully() { 43 | return session.closeGracefully(); 44 | } 45 | 46 | public void simulateIncomingMessage(McpSchema.JSONRPCMessage message) { 47 | session.handle(message).subscribe(); 48 | } 49 | 50 | } 51 | -------------------------------------------------------------------------------- /mcp-core/src/main/java/io/modelcontextprotocol/common/McpTransportContext.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2024-2025 the original author or authors. 3 | */ 4 | 5 | package io.modelcontextprotocol.common; 6 | 7 | import java.util.Collections; 8 | import java.util.Map; 9 | 10 | /** 11 | * Context associated with the transport layer. It allows to add transport-level metadata 12 | * for use further down the line. Specifically, it can be beneficial to extract HTTP 13 | * request metadata for use in MCP feature implementations. 14 | * 15 | * @author Dariusz Jędrzejczyk 16 | */ 17 | public interface McpTransportContext { 18 | 19 | /** 20 | * Key for use in Reactor Context to transport the context to user land. 21 | */ 22 | String KEY = "MCP_TRANSPORT_CONTEXT"; 23 | 24 | /** 25 | * An empty, unmodifiable context. 26 | */ 27 | @SuppressWarnings("unchecked") 28 | McpTransportContext EMPTY = new DefaultMcpTransportContext(Collections.EMPTY_MAP); 29 | 30 | /** 31 | * Create an unmodifiable context containing the given metadata. 32 | * @param metadata the transport metadata 33 | * @return the context containing the metadata 34 | */ 35 | static McpTransportContext create(Map metadata) { 36 | return new DefaultMcpTransportContext(metadata); 37 | } 38 | 39 | /** 40 | * Extract a value from the context. 41 | * @param key the key under the data is expected 42 | * @return the associated value or {@code null} if missing. 43 | */ 44 | Object get(String key); 45 | 46 | } 47 | -------------------------------------------------------------------------------- /mcp/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | io.modelcontextprotocol.sdk 8 | mcp-parent 9 | 0.18.0-SNAPSHOT 10 | 11 | mcp 12 | jar 13 | Java MCP SDK 14 | Java SDK implementation of the Model Context Protocol, enabling seamless integration with language models and AI tools 15 | https://github.com/modelcontextprotocol/java-sdk 16 | 17 | 18 | https://github.com/modelcontextprotocol/java-sdk 19 | git://github.com/modelcontextprotocol/java-sdk.git 20 | git@github.com/modelcontextprotocol/java-sdk.git 21 | 22 | 23 | 24 | 25 | 26 | io.modelcontextprotocol.sdk 27 | mcp-json-jackson2 28 | 0.18.0-SNAPSHOT 29 | 30 | 31 | 32 | io.modelcontextprotocol.sdk 33 | mcp-core 34 | 0.18.0-SNAPSHOT 35 | 36 | 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /.github/workflows/publish-snapshot.yml: -------------------------------------------------------------------------------- 1 | name: Publish Snapshot 2 | 3 | on: 4 | push: 5 | branches: [ "main" ] 6 | 7 | jobs: 8 | build: 9 | name: Build branch 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Checkout source code 13 | uses: actions/checkout@v4 14 | 15 | - name: Set up JDK 17 16 | uses: actions/setup-java@v4 17 | with: 18 | java-version: '17' 19 | distribution: 'temurin' 20 | cache: 'maven' 21 | server-id: central 22 | server-username: MAVEN_USERNAME 23 | server-password: MAVEN_PASSWORD 24 | gpg-private-key: ${{ secrets.GPG_SECRET_KEY }} 25 | gpg-passphrase: MAVEN_GPG_PASSPHRASE 26 | 27 | - name: Setup Node.js 28 | uses: actions/setup-node@v4 29 | with: 30 | node-version: '20' 31 | 32 | - name: Generate Java docs 33 | run: mvn -Pjavadoc -B javadoc:aggregate 34 | 35 | - name: Build with Maven and deploy to Sonatype snapshot repository 36 | env: 37 | MAVEN_USERNAME: ${{ secrets.OSSRH_USERNAME }} 38 | MAVEN_PASSWORD: ${{ secrets.OSSRH_TOKEN }} 39 | MAVEN_GPG_PASSPHRASE: ${{ secrets.SIGNING_PASSPHRASE }} 40 | run: | 41 | mvn -Pjavadoc -Prelease --batch-mode --update-snapshots deploy 42 | 43 | - name: Capture project version 44 | run: echo PROJECT_VERSION=$(mvn help:evaluate -Dexpression=project.version --quiet -DforceStdout) >> $GITHUB_ENV 45 | -------------------------------------------------------------------------------- /mcp-core/src/main/java/io/modelcontextprotocol/spec/HttpHeaders.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2024-2025 the original author or authors. 3 | */ 4 | 5 | package io.modelcontextprotocol.spec; 6 | 7 | /** 8 | * Names of HTTP headers in use by MCP HTTP transports. 9 | * 10 | * @author Dariusz Jędrzejczyk 11 | */ 12 | public interface HttpHeaders { 13 | 14 | /** 15 | * Identifies individual MCP sessions. 16 | */ 17 | String MCP_SESSION_ID = "Mcp-Session-Id"; 18 | 19 | /** 20 | * Identifies events within an SSE Stream. 21 | */ 22 | String LAST_EVENT_ID = "Last-Event-ID"; 23 | 24 | /** 25 | * Identifies the MCP protocol version. 26 | */ 27 | String PROTOCOL_VERSION = "MCP-Protocol-Version"; 28 | 29 | /** 30 | * The HTTP Content-Length header. 31 | * @see RFC9110 33 | */ 34 | String CONTENT_LENGTH = "Content-Length"; 35 | 36 | /** 37 | * The HTTP Content-Type header. 38 | * @see RFC9110 40 | */ 41 | String CONTENT_TYPE = "Content-Type"; 42 | 43 | /** 44 | * The HTTP Accept header. 45 | * @see RFC9110 46 | */ 47 | String ACCEPT = "Accept"; 48 | 49 | /** 50 | * The HTTP Cache-Control header. 51 | * @see RFC9111 53 | */ 54 | String CACHE_CONTROL = "Cache-Control"; 55 | 56 | } 57 | -------------------------------------------------------------------------------- /mcp-json/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | io.modelcontextprotocol.sdk 8 | mcp-parent 9 | 0.18.0-SNAPSHOT 10 | 11 | mcp-json 12 | jar 13 | Java MCP SDK JSON Support 14 | Java MCP SDK JSON Support API 15 | https://github.com/modelcontextprotocol/java-sdk 16 | 17 | https://github.com/modelcontextprotocol/java-sdk 18 | git://github.com/modelcontextprotocol/java-sdk.git 19 | git@github.com/modelcontextprotocol/java-sdk.git 20 | 21 | 22 | 23 | 24 | org.apache.maven.plugins 25 | maven-jar-plugin 26 | 27 | 28 | 29 | true 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /mcp-json/src/main/java/io/modelcontextprotocol/json/TypeRef.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 - 2025 the original author or authors. 3 | */ 4 | 5 | package io.modelcontextprotocol.json; 6 | 7 | import java.lang.reflect.ParameterizedType; 8 | import java.lang.reflect.Type; 9 | 10 | /** 11 | * Captures generic type information at runtime for parameterized JSON (de)serialization. 12 | * Usage: TypeRef<List<Foo>> ref = new TypeRef<>(){}; 13 | */ 14 | public abstract class TypeRef { 15 | 16 | private final Type type; 17 | 18 | /** 19 | * Constructs a new TypeRef instance, capturing the generic type information of the 20 | * subclass. This constructor should be called from an anonymous subclass to capture 21 | * the actual type arguments. For example:

22 | 	 * TypeRef<List<Foo>> ref = new TypeRef<>(){};
23 | 	 * 
24 | * @throws IllegalStateException if TypeRef is not subclassed with actual type 25 | * information 26 | */ 27 | protected TypeRef() { 28 | Type superClass = getClass().getGenericSuperclass(); 29 | if (superClass instanceof Class) { 30 | throw new IllegalStateException("TypeRef constructed without actual type information"); 31 | } 32 | this.type = ((ParameterizedType) superClass).getActualTypeArguments()[0]; 33 | } 34 | 35 | /** 36 | * Returns the captured type information. 37 | * @return the Type representing the actual type argument captured by this TypeRef 38 | * instance 39 | */ 40 | public Type getType() { 41 | return type; 42 | } 43 | 44 | } 45 | -------------------------------------------------------------------------------- /mcp-core/src/main/java/io/modelcontextprotocol/client/transport/customizer/DelegatingMcpAsyncHttpClientRequestCustomizer.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2024-2025 the original author or authors. 3 | */ 4 | package io.modelcontextprotocol.client.transport.customizer; 5 | 6 | import java.net.URI; 7 | import java.net.http.HttpRequest; 8 | import java.util.List; 9 | 10 | import org.reactivestreams.Publisher; 11 | 12 | import io.modelcontextprotocol.common.McpTransportContext; 13 | import io.modelcontextprotocol.util.Assert; 14 | 15 | import reactor.core.publisher.Mono; 16 | 17 | /** 18 | * Composable {@link McpAsyncHttpClientRequestCustomizer} that applies multiple 19 | * customizers, in order. 20 | * 21 | * @author Daniel Garnier-Moiroux 22 | */ 23 | public class DelegatingMcpAsyncHttpClientRequestCustomizer implements McpAsyncHttpClientRequestCustomizer { 24 | 25 | private final List customizers; 26 | 27 | public DelegatingMcpAsyncHttpClientRequestCustomizer(List customizers) { 28 | Assert.notNull(customizers, "Customizers must not be null"); 29 | this.customizers = customizers; 30 | } 31 | 32 | @Override 33 | public Publisher customize(HttpRequest.Builder builder, String method, URI endpoint, 34 | String body, McpTransportContext context) { 35 | var result = Mono.just(builder); 36 | for (var customizer : this.customizers) { 37 | result = result.flatMap(b -> Mono.from(customizer.customize(b, method, endpoint, body, context))); 38 | } 39 | return result; 40 | } 41 | 42 | } 43 | -------------------------------------------------------------------------------- /mcp-core/src/main/java/io/modelcontextprotocol/spec/McpClientTransport.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2024 - 2024 the original author or authors. 3 | */ 4 | 5 | package io.modelcontextprotocol.spec; 6 | 7 | import java.util.function.Consumer; 8 | import java.util.function.Function; 9 | 10 | import reactor.core.publisher.Mono; 11 | 12 | /** 13 | * Interface for the client side of the {@link McpTransport}. It allows setting handlers 14 | * for messages that are incoming from the MCP server and hooking in to exceptions raised 15 | * on the transport layer. 16 | * 17 | * @author Christian Tzolov 18 | * @author Dariusz Jędrzejczyk 19 | */ 20 | public interface McpClientTransport extends McpTransport { 21 | 22 | /** 23 | * Used to register the incoming messages' handler and potentially (eagerly) connect 24 | * to the server. 25 | * @param handler a transformer for incoming messages 26 | * @return a {@link Mono} that terminates upon successful client setup. It can mean 27 | * establishing a connection (which can be later disposed) but it doesn't have to, 28 | * depending on the transport type. The successful termination of the returned 29 | * {@link Mono} simply means the client can now be used. An error can be retried 30 | * according to the application requirements. 31 | */ 32 | Mono connect(Function, Mono> handler); 33 | 34 | /** 35 | * Sets the exception handler for exceptions raised on the transport layer. 36 | * @param handler Allows reacting to transport level exceptions by the higher layers 37 | */ 38 | default void setExceptionHandler(Consumer handler) { 39 | } 40 | 41 | } 42 | -------------------------------------------------------------------------------- /mcp-core/src/main/java/io/modelcontextprotocol/spec/ClosedMcpTransportSession.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025-2025 the original author or authors. 3 | */ 4 | package io.modelcontextprotocol.spec; 5 | 6 | import java.util.Optional; 7 | 8 | import org.reactivestreams.Publisher; 9 | import reactor.core.publisher.Mono; 10 | import reactor.util.annotation.Nullable; 11 | 12 | /** 13 | * Represents a closed MCP session, which may not be reused. All calls will throw a 14 | * {@link McpTransportSessionClosedException}. 15 | * 16 | * @param the resource representing the connection that the transport 17 | * manages. 18 | * @author Daniel Garnier-Moiroux 19 | */ 20 | public class ClosedMcpTransportSession implements McpTransportSession { 21 | 22 | private final String sessionId; 23 | 24 | public ClosedMcpTransportSession(@Nullable String sessionId) { 25 | this.sessionId = sessionId; 26 | } 27 | 28 | @Override 29 | public Optional sessionId() { 30 | throw new McpTransportSessionClosedException(sessionId); 31 | } 32 | 33 | @Override 34 | public boolean markInitialized(String sessionId) { 35 | throw new McpTransportSessionClosedException(sessionId); 36 | } 37 | 38 | @Override 39 | public void addConnection(CONNECTION connection) { 40 | throw new McpTransportSessionClosedException(sessionId); 41 | } 42 | 43 | @Override 44 | public void removeConnection(CONNECTION connection) { 45 | throw new McpTransportSessionClosedException(sessionId); 46 | } 47 | 48 | @Override 49 | public void close() { 50 | 51 | } 52 | 53 | @Override 54 | public Publisher closeGracefully() { 55 | return Mono.empty(); 56 | } 57 | 58 | } 59 | -------------------------------------------------------------------------------- /mcp-core/src/test/java/io/modelcontextprotocol/client/HttpClientStreamableHttpAsyncClientTests.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2024-2025 the original author or authors. 3 | */ 4 | 5 | package io.modelcontextprotocol.client; 6 | 7 | import io.modelcontextprotocol.client.transport.HttpClientStreamableHttpTransport; 8 | import io.modelcontextprotocol.spec.McpClientTransport; 9 | import org.junit.jupiter.api.AfterAll; 10 | import org.junit.jupiter.api.BeforeAll; 11 | import org.junit.jupiter.api.Timeout; 12 | import org.testcontainers.containers.GenericContainer; 13 | import org.testcontainers.containers.wait.strategy.Wait; 14 | 15 | @Timeout(15) 16 | public class HttpClientStreamableHttpAsyncClientTests extends AbstractMcpAsyncClientTests { 17 | 18 | private static String host = "http://localhost:3001"; 19 | 20 | // Uses the https://github.com/tzolov/mcp-everything-server-docker-image 21 | @SuppressWarnings("resource") 22 | static GenericContainer container = new GenericContainer<>("docker.io/tzolov/mcp-everything-server:v3") 23 | .withCommand("node dist/index.js streamableHttp") 24 | .withLogConsumer(outputFrame -> System.out.println(outputFrame.getUtf8String())) 25 | .withExposedPorts(3001) 26 | .waitingFor(Wait.forHttp("/").forStatusCode(404)); 27 | 28 | @Override 29 | protected McpClientTransport createMcpTransport() { 30 | return HttpClientStreamableHttpTransport.builder(host).build(); 31 | } 32 | 33 | @BeforeAll 34 | static void startContainer() { 35 | container.start(); 36 | int port = container.getMappedPort(3001); 37 | host = "http://" + container.getHost() + ":" + port; 38 | } 39 | 40 | @AfterAll 41 | static void stopContainer() { 42 | container.stop(); 43 | } 44 | 45 | } 46 | -------------------------------------------------------------------------------- /mcp-core/src/main/java/io/modelcontextprotocol/spec/JsonSchemaValidator.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2024-2024 the original author or authors. 3 | */ 4 | 5 | package io.modelcontextprotocol.spec; 6 | 7 | import java.util.Map; 8 | 9 | /** 10 | * Interface for validating structured content against a JSON schema. This interface 11 | * defines a method to validate structured content based on the provided output schema. 12 | * 13 | * @author Christian Tzolov 14 | */ 15 | public interface JsonSchemaValidator { 16 | 17 | /** 18 | * Represents the result of a validation operation. 19 | * 20 | * @param valid Indicates whether the validation was successful. 21 | * @param errorMessage An error message if the validation failed, otherwise null. 22 | * @param jsonStructuredOutput The text structured content in JSON format if the 23 | * validation was successful, otherwise null. 24 | */ 25 | public record ValidationResponse(boolean valid, String errorMessage, String jsonStructuredOutput) { 26 | 27 | public static ValidationResponse asValid(String jsonStructuredOutput) { 28 | return new ValidationResponse(true, null, jsonStructuredOutput); 29 | } 30 | 31 | public static ValidationResponse asInvalid(String message) { 32 | return new ValidationResponse(false, message, null); 33 | } 34 | } 35 | 36 | /** 37 | * Validates the structured content against the provided JSON schema. 38 | * @param schema The JSON schema to validate against. 39 | * @param structuredContent The structured content to validate. 40 | * @return A ValidationResponse indicating whether the validation was successful or 41 | * not. 42 | */ 43 | ValidationResponse validate(Map schema, Object structuredContent); 44 | 45 | } 46 | -------------------------------------------------------------------------------- /mcp-core/src/test/java/io/modelcontextprotocol/util/AssertTests.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2024-2024 the original author or authors. 3 | */ 4 | 5 | package io.modelcontextprotocol.util; 6 | 7 | import org.junit.jupiter.api.Test; 8 | 9 | import java.util.List; 10 | 11 | import static org.junit.jupiter.api.Assertions.assertThrows; 12 | import static org.junit.jupiter.api.Assertions.assertEquals; 13 | import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; 14 | 15 | class AssertTests { 16 | 17 | @Test 18 | void testCollectionNotEmpty() { 19 | IllegalArgumentException e1 = assertThrows(IllegalArgumentException.class, 20 | () -> Assert.notEmpty(null, "collection is null")); 21 | assertEquals("collection is null", e1.getMessage()); 22 | 23 | IllegalArgumentException e2 = assertThrows(IllegalArgumentException.class, 24 | () -> Assert.notEmpty(List.of(), "collection is empty")); 25 | assertEquals("collection is empty", e2.getMessage()); 26 | 27 | assertDoesNotThrow(() -> Assert.notEmpty(List.of("test"), "collection is not empty")); 28 | } 29 | 30 | @Test 31 | void testObjectNotNull() { 32 | IllegalArgumentException e = assertThrows(IllegalArgumentException.class, 33 | () -> Assert.notNull(null, "object is null")); 34 | assertEquals("object is null", e.getMessage()); 35 | 36 | assertDoesNotThrow(() -> Assert.notNull("test", "object is not null")); 37 | } 38 | 39 | @Test 40 | void testStringHasText() { 41 | IllegalArgumentException e = assertThrows(IllegalArgumentException.class, 42 | () -> Assert.hasText(null, "string is null")); 43 | assertEquals("string is null", e.getMessage()); 44 | 45 | assertDoesNotThrow(() -> Assert.hasText("test", "string is not empty")); 46 | } 47 | 48 | } -------------------------------------------------------------------------------- /mcp-core/src/test/java/io/modelcontextprotocol/client/StdioMcpAsyncClientTests.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2024-2024 the original author or authors. 3 | */ 4 | 5 | package io.modelcontextprotocol.client; 6 | 7 | import java.time.Duration; 8 | 9 | import io.modelcontextprotocol.client.transport.ServerParameters; 10 | import io.modelcontextprotocol.client.transport.StdioClientTransport; 11 | import io.modelcontextprotocol.spec.McpClientTransport; 12 | import org.junit.jupiter.api.Timeout; 13 | 14 | import static io.modelcontextprotocol.client.ServerParameterUtils.createServerParameters; 15 | import static io.modelcontextprotocol.util.McpJsonMapperUtils.JSON_MAPPER; 16 | 17 | /** 18 | * Tests for the {@link McpAsyncClient} with {@link StdioClientTransport}. 19 | * 20 | *

21 | * These tests use npx to download and run the MCP "everything" server locally. The first 22 | * test execution will download the everything server scripts and cache them locally, 23 | * which can take more than 15 seconds. Subsequent test runs will use the cached version 24 | * and execute faster. 25 | * 26 | * @author Christian Tzolov 27 | * @author Dariusz Jędrzejczyk 28 | */ 29 | @Timeout(25) // Giving extra time beyond the client timeout to account for initial server 30 | // download 31 | class StdioMcpAsyncClientTests extends AbstractMcpAsyncClientTests { 32 | 33 | @Override 34 | protected McpClientTransport createMcpTransport() { 35 | return new StdioClientTransport(createServerParameters(), JSON_MAPPER); 36 | } 37 | 38 | protected Duration getInitializationTimeout() { 39 | return Duration.ofSeconds(20); 40 | } 41 | 42 | @Override 43 | protected Duration getRequestTimeout() { 44 | return Duration.ofSeconds(25); 45 | } 46 | 47 | } 48 | -------------------------------------------------------------------------------- /mcp-spring/mcp-spring-webflux/README.md: -------------------------------------------------------------------------------- 1 | # WebFlux SSE Transport 2 | 3 | ```xml 4 | 5 | io.modelcontextprotocol.sdk 6 | mcp-spring-webflux 7 | 8 | ``` 9 | 10 | ```java 11 | String MESSAGE_ENDPOINT = "/mcp/message"; 12 | 13 | @Configuration 14 | static class MyConfig { 15 | 16 | // SSE transport 17 | @Bean 18 | public WebFluxSseServerTransport sseServerTransport() { 19 | return new WebFluxSseServerTransport(new ObjectMapper(), "/mcp/message"); 20 | } 21 | 22 | // Router function for SSE transport used by Spring WebFlux to start an HTTP 23 | // server. 24 | @Bean 25 | public RouterFunction mcpRouterFunction(WebFluxSseServerTransport transport) { 26 | return transport.getRouterFunction(); 27 | } 28 | 29 | @Bean 30 | public McpAsyncServer mcpServer(ServerMcpTransport transport, OpenLibrary openLibrary) { 31 | 32 | // Configure server capabilities with resource support 33 | var capabilities = McpSchema.ServerCapabilities.builder() 34 | .resources(false, true) // No subscribe support, but list changes notifications 35 | .tools(true) // Tool support with list changes notifications 36 | .prompts(true) // Prompt support with list changes notifications 37 | .logging() // Logging support 38 | .build(); 39 | 40 | // Create the server with both tool and resource capabilities 41 | var server = McpServer.using(transport) 42 | .serverInfo("MCP Demo Server", "1.0.0") 43 | .capabilities(capabilities) 44 | .resources(systemInfoResourceRegistration()) 45 | .prompts(greetingPromptRegistration()) 46 | .tools(openLibraryToolRegistrations(openLibrary)) 47 | .async(); 48 | 49 | return server; 50 | } 51 | 52 | // ... 53 | 54 | } 55 | ``` 56 | -------------------------------------------------------------------------------- /mcp-spring/mcp-spring-webflux/src/test/java/io/modelcontextprotocol/server/transport/BlockingInputStream.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2024 - 2024 the original author or authors. 3 | */ 4 | 5 | package io.modelcontextprotocol.server.transport; 6 | 7 | import java.io.IOException; 8 | import java.io.InputStream; 9 | import java.util.concurrent.BlockingQueue; 10 | import java.util.concurrent.LinkedBlockingQueue; 11 | 12 | public class BlockingInputStream extends InputStream { 13 | 14 | private final BlockingQueue queue = new LinkedBlockingQueue<>(); 15 | 16 | private volatile boolean completed = false; 17 | 18 | private volatile boolean closed = false; 19 | 20 | @Override 21 | public int read() throws IOException { 22 | if (closed) { 23 | throw new IOException("Stream is closed"); 24 | } 25 | 26 | try { 27 | Integer value = queue.poll(); 28 | if (value == null) { 29 | if (completed) { 30 | return -1; 31 | } 32 | value = queue.take(); // Blocks until data is available 33 | if (value == null && completed) { 34 | return -1; 35 | } 36 | } 37 | return value; 38 | } 39 | catch (InterruptedException e) { 40 | Thread.currentThread().interrupt(); 41 | throw new IOException("Read interrupted", e); 42 | } 43 | } 44 | 45 | public void write(int b) { 46 | if (!closed && !completed) { 47 | queue.offer(b); 48 | } 49 | } 50 | 51 | public void write(byte[] data) { 52 | if (!closed && !completed) { 53 | for (byte b : data) { 54 | queue.offer((int) b & 0xFF); 55 | } 56 | } 57 | } 58 | 59 | public void complete() { 60 | this.completed = true; 61 | } 62 | 63 | @Override 64 | public void close() { 65 | this.closed = true; 66 | this.completed = true; 67 | this.queue.clear(); 68 | } 69 | 70 | } -------------------------------------------------------------------------------- /mcp-core/src/test/java/io/modelcontextprotocol/client/HttpSseMcpAsyncClientTests.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2024-2024 the original author or authors. 3 | */ 4 | 5 | package io.modelcontextprotocol.client; 6 | 7 | import org.junit.jupiter.api.AfterAll; 8 | import org.junit.jupiter.api.BeforeAll; 9 | import org.junit.jupiter.api.Timeout; 10 | import org.testcontainers.containers.GenericContainer; 11 | import org.testcontainers.containers.wait.strategy.Wait; 12 | 13 | import io.modelcontextprotocol.client.transport.HttpClientSseClientTransport; 14 | import io.modelcontextprotocol.spec.McpClientTransport; 15 | 16 | /** 17 | * Tests for the {@link McpSyncClient} with {@link HttpClientSseClientTransport}. 18 | * 19 | * @author Christian Tzolov 20 | */ 21 | @Timeout(15) 22 | class HttpSseMcpAsyncClientTests extends AbstractMcpAsyncClientTests { 23 | 24 | private static String host = "http://localhost:3004"; 25 | 26 | // Uses the https://github.com/tzolov/mcp-everything-server-docker-image 27 | @SuppressWarnings("resource") 28 | static GenericContainer container = new GenericContainer<>("docker.io/tzolov/mcp-everything-server:v3") 29 | .withCommand("node dist/index.js sse") 30 | .withLogConsumer(outputFrame -> System.out.println(outputFrame.getUtf8String())) 31 | .withExposedPorts(3001) 32 | .waitingFor(Wait.forHttp("/").forStatusCode(404)); 33 | 34 | @Override 35 | protected McpClientTransport createMcpTransport() { 36 | return HttpClientSseClientTransport.builder(host).build(); 37 | } 38 | 39 | @BeforeAll 40 | static void startContainer() { 41 | container.start(); 42 | int port = container.getMappedPort(3001); 43 | host = "http://" + container.getHost() + ":" + port; 44 | } 45 | 46 | @AfterAll 47 | static void stopContainer() { 48 | container.stop(); 49 | } 50 | 51 | } 52 | -------------------------------------------------------------------------------- /mcp-core/src/main/java/io/modelcontextprotocol/util/McpUriTemplateManager.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025-2025 the original author or authors. 3 | */ 4 | 5 | package io.modelcontextprotocol.util; 6 | 7 | import java.util.List; 8 | import java.util.Map; 9 | 10 | /** 11 | * Interface for working with URI templates. 12 | *

13 | * This interface provides methods for extracting variables from URI templates and 14 | * matching them against actual URIs. 15 | * 16 | * @author Christian Tzolov 17 | */ 18 | public interface McpUriTemplateManager { 19 | 20 | /** 21 | * Extract URI variable names from this URI template. 22 | * @return A list of variable names extracted from the template 23 | * @throws IllegalArgumentException if duplicate variable names are found 24 | */ 25 | List getVariableNames(); 26 | 27 | /** 28 | * Extract URI variable values from the actual request URI. 29 | *

30 | * This method converts the URI template into a regex pattern, then uses that pattern 31 | * to extract variable values from the request URI. 32 | * @param uri The actual URI from the request 33 | * @return A map of variable names to their values 34 | * @throws IllegalArgumentException if the URI template is invalid or the request URI 35 | * doesn't match the template pattern 36 | */ 37 | Map extractVariableValues(String uri); 38 | 39 | /** 40 | * Indicate whether the given URI matches this template. 41 | * @param uri the URI to match to 42 | * @return {@code true} if it matches; {@code false} otherwise 43 | */ 44 | boolean matches(String uri); 45 | 46 | /** 47 | * Check if the given URI is a URI template. 48 | * @return Returns true if the URI contains variables in the format {variableName} 49 | */ 50 | public boolean isUriTemplate(String uri); 51 | 52 | } 53 | -------------------------------------------------------------------------------- /mcp-spring/mcp-spring-webflux/src/test/java/io/modelcontextprotocol/client/WebClientStreamableHttpSyncClientTests.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025-2025 the original author or authors. 3 | */ 4 | 5 | package io.modelcontextprotocol.client; 6 | 7 | import org.junit.jupiter.api.AfterAll; 8 | import org.junit.jupiter.api.BeforeAll; 9 | import org.junit.jupiter.api.Timeout; 10 | import org.springframework.web.reactive.function.client.WebClient; 11 | import org.testcontainers.containers.GenericContainer; 12 | import org.testcontainers.containers.wait.strategy.Wait; 13 | 14 | import io.modelcontextprotocol.client.transport.WebClientStreamableHttpTransport; 15 | import io.modelcontextprotocol.spec.McpClientTransport; 16 | 17 | @Timeout(15) 18 | public class WebClientStreamableHttpSyncClientTests extends AbstractMcpSyncClientTests { 19 | 20 | static String host = "http://localhost:3001"; 21 | 22 | // Uses the https://github.com/tzolov/mcp-everything-server-docker-image 23 | @SuppressWarnings("resource") 24 | static GenericContainer container = new GenericContainer<>("docker.io/tzolov/mcp-everything-server:v3") 25 | .withCommand("node dist/index.js streamableHttp") 26 | .withLogConsumer(outputFrame -> System.out.println(outputFrame.getUtf8String())) 27 | .withExposedPorts(3001) 28 | .waitingFor(Wait.forHttp("/").forStatusCode(404)); 29 | 30 | @Override 31 | protected McpClientTransport createMcpTransport() { 32 | return WebClientStreamableHttpTransport.builder(WebClient.builder().baseUrl(host)).build(); 33 | } 34 | 35 | @BeforeAll 36 | static void startContainer() { 37 | container.start(); 38 | int port = container.getMappedPort(3001); 39 | host = "http://" + container.getHost() + ":" + port; 40 | } 41 | 42 | @AfterAll 43 | static void stopContainer() { 44 | container.stop(); 45 | } 46 | 47 | } 48 | -------------------------------------------------------------------------------- /mcp-spring/mcp-spring-webflux/src/test/java/io/modelcontextprotocol/client/WebClientStreamableHttpAsyncClientTests.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025-2025 the original author or authors. 3 | */ 4 | 5 | package io.modelcontextprotocol.client; 6 | 7 | import org.junit.jupiter.api.AfterAll; 8 | import org.junit.jupiter.api.BeforeAll; 9 | import org.junit.jupiter.api.Timeout; 10 | import org.springframework.web.reactive.function.client.WebClient; 11 | import org.testcontainers.containers.GenericContainer; 12 | import org.testcontainers.containers.wait.strategy.Wait; 13 | 14 | import io.modelcontextprotocol.client.transport.WebClientStreamableHttpTransport; 15 | import io.modelcontextprotocol.spec.McpClientTransport; 16 | 17 | @Timeout(15) 18 | public class WebClientStreamableHttpAsyncClientTests extends AbstractMcpAsyncClientTests { 19 | 20 | static String host = "http://localhost:3001"; 21 | 22 | // Uses the https://github.com/tzolov/mcp-everything-server-docker-image 23 | @SuppressWarnings("resource") 24 | static GenericContainer container = new GenericContainer<>("docker.io/tzolov/mcp-everything-server:v3") 25 | .withCommand("node dist/index.js streamableHttp") 26 | .withLogConsumer(outputFrame -> System.out.println(outputFrame.getUtf8String())) 27 | .withExposedPorts(3001) 28 | .waitingFor(Wait.forHttp("/").forStatusCode(404)); 29 | 30 | @Override 31 | protected McpClientTransport createMcpTransport() { 32 | return WebClientStreamableHttpTransport.builder(WebClient.builder().baseUrl(host)).build(); 33 | } 34 | 35 | @BeforeAll 36 | static void startContainer() { 37 | container.start(); 38 | int port = container.getMappedPort(3001); 39 | host = "http://" + container.getHost() + ":" + port; 40 | } 41 | 42 | @AfterAll 43 | static void stopContainer() { 44 | container.stop(); 45 | } 46 | 47 | } 48 | -------------------------------------------------------------------------------- /mcp-spring/mcp-spring-webflux/src/test/java/io/modelcontextprotocol/utils/McpTestRequestRecordingExchangeFilterFunction.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025-2025 the original author or authors. 3 | */ 4 | 5 | package io.modelcontextprotocol.utils; 6 | 7 | import java.util.ArrayList; 8 | import java.util.List; 9 | import java.util.Map; 10 | import java.util.stream.Collectors; 11 | 12 | import reactor.core.publisher.Mono; 13 | 14 | import org.springframework.http.HttpMethod; 15 | import org.springframework.web.reactive.function.server.HandlerFilterFunction; 16 | import org.springframework.web.reactive.function.server.HandlerFunction; 17 | import org.springframework.web.reactive.function.server.ServerRequest; 18 | import org.springframework.web.reactive.function.server.ServerResponse; 19 | 20 | /** 21 | * Simple {@link HandlerFilterFunction} which records calls made to an MCP server. 22 | * 23 | * @author Daniel Garnier-Moiroux 24 | */ 25 | public class McpTestRequestRecordingExchangeFilterFunction implements HandlerFilterFunction { 26 | 27 | private final List calls = new ArrayList<>(); 28 | 29 | @Override 30 | public Mono filter(ServerRequest request, HandlerFunction next) { 31 | Map headers = request.headers() 32 | .asHttpHeaders() 33 | .keySet() 34 | .stream() 35 | .collect(Collectors.toMap(String::toLowerCase, k -> String.join(",", request.headers().header(k)))); 36 | 37 | var cr = request.bodyToMono(String.class).defaultIfEmpty("").map(body -> { 38 | this.calls.add(new Call(request.method(), headers, body)); 39 | return ServerRequest.from(request).body(body).build(); 40 | }); 41 | 42 | return cr.flatMap(next::handle); 43 | 44 | } 45 | 46 | public List getCalls() { 47 | return List.copyOf(calls); 48 | } 49 | 50 | public record Call(HttpMethod method, Map headers, String body) { 51 | 52 | } 53 | 54 | } 55 | -------------------------------------------------------------------------------- /mcp-core/src/main/java/io/modelcontextprotocol/spec/McpTransportStream.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2024-2025 the original author or authors. 3 | */ 4 | 5 | package io.modelcontextprotocol.spec; 6 | 7 | import org.reactivestreams.Publisher; 8 | import reactor.util.function.Tuple2; 9 | 10 | import java.util.Optional; 11 | 12 | /** 13 | * A representation of a stream at the transport layer of the MCP protocol. In particular, 14 | * it is currently used in the Streamable HTTP implementation to potentially be able to 15 | * resume a broken connection from where it left off by optionally keeping track of 16 | * attached SSE event ids. 17 | * 18 | * @param the resource on which the stream is being served and consumed via 19 | * this mechanism 20 | * @author Dariusz Jędrzejczyk 21 | */ 22 | public interface McpTransportStream { 23 | 24 | /** 25 | * The last observed event identifier. 26 | * @return if not empty, contains the most recent event that was consumed 27 | */ 28 | Optional lastId(); 29 | 30 | /** 31 | * An internal stream identifier used to distinguish streams while debugging. 32 | * @return a {@code long} stream identifier value 33 | */ 34 | long streamId(); 35 | 36 | /** 37 | * Allows keeping track of the transport stream of events (currently an SSE stream 38 | * from Streamable HTTP specification) and enable resumability and reconnects in case 39 | * of stream errors. 40 | * @param eventStream a {@link Publisher} of tuples (pairs) of an optional identifier 41 | * associated with a collection of messages 42 | * @return a flattened {@link Publisher} of 43 | * {@link io.modelcontextprotocol.spec.McpSchema.JSONRPCMessage JSON-RPC messages} 44 | * with the identifier stripped away 45 | */ 46 | Publisher consumeSseStream( 47 | Publisher, Iterable>> eventStream); 48 | 49 | } 50 | -------------------------------------------------------------------------------- /mcp-core/src/main/java/io/modelcontextprotocol/client/transport/customizer/McpAsyncHttpClientRequestCustomizer.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2024-2025 the original author or authors. 3 | */ 4 | 5 | package io.modelcontextprotocol.client.transport.customizer; 6 | 7 | import java.net.URI; 8 | import java.net.http.HttpRequest; 9 | 10 | import org.reactivestreams.Publisher; 11 | import reactor.core.publisher.Mono; 12 | import reactor.core.scheduler.Schedulers; 13 | import reactor.util.annotation.Nullable; 14 | 15 | import io.modelcontextprotocol.common.McpTransportContext; 16 | 17 | /** 18 | * Customize {@link HttpRequest.Builder} before executing the request, in either SSE or 19 | * Streamable HTTP transport. 20 | *

21 | * When used in a non-blocking context, implementations MUST be non-blocking. 22 | * 23 | * @author Daniel Garnier-Moiroux 24 | */ 25 | public interface McpAsyncHttpClientRequestCustomizer { 26 | 27 | Publisher customize(HttpRequest.Builder builder, String method, URI endpoint, 28 | @Nullable String body, McpTransportContext context); 29 | 30 | McpAsyncHttpClientRequestCustomizer NOOP = new Noop(); 31 | 32 | /** 33 | * Wrap a sync implementation in an async wrapper. 34 | *

35 | * Do NOT wrap a blocking implementation for use in a non-blocking context. For a 36 | * blocking implementation, consider using {@link Schedulers#boundedElastic()}. 37 | */ 38 | static McpAsyncHttpClientRequestCustomizer fromSync(McpSyncHttpClientRequestCustomizer customizer) { 39 | return (builder, method, uri, body, context) -> Mono.fromSupplier(() -> { 40 | customizer.customize(builder, method, uri, body, context); 41 | return builder; 42 | }); 43 | } 44 | 45 | class Noop implements McpAsyncHttpClientRequestCustomizer { 46 | 47 | @Override 48 | public Publisher customize(HttpRequest.Builder builder, String method, URI endpoint, 49 | String body, McpTransportContext context) { 50 | return Mono.just(builder); 51 | } 52 | 53 | } 54 | 55 | } 56 | -------------------------------------------------------------------------------- /mcp-spring/mcp-spring-webflux/src/test/java/io/modelcontextprotocol/client/WebFluxSseMcpSyncClientTests.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2024-2024 the original author or authors. 3 | */ 4 | 5 | package io.modelcontextprotocol.client; 6 | 7 | import java.time.Duration; 8 | 9 | import io.modelcontextprotocol.client.transport.WebFluxSseClientTransport; 10 | import io.modelcontextprotocol.spec.McpClientTransport; 11 | import org.junit.jupiter.api.AfterAll; 12 | import org.junit.jupiter.api.BeforeAll; 13 | import org.junit.jupiter.api.Timeout; 14 | import org.testcontainers.containers.GenericContainer; 15 | import org.testcontainers.containers.wait.strategy.Wait; 16 | import org.springframework.web.reactive.function.client.WebClient; 17 | 18 | /** 19 | * Tests for the {@link McpSyncClient} with {@link WebFluxSseClientTransport}. 20 | * 21 | * @author Christian Tzolov 22 | */ 23 | @Timeout(15) // Giving extra time beyond the client timeout 24 | class WebFluxSseMcpSyncClientTests extends AbstractMcpSyncClientTests { 25 | 26 | static String host = "http://localhost:3001"; 27 | 28 | // Uses the https://github.com/tzolov/mcp-everything-server-docker-image 29 | @SuppressWarnings("resource") 30 | static GenericContainer container = new GenericContainer<>("docker.io/tzolov/mcp-everything-server:v3") 31 | .withCommand("node dist/index.js sse") 32 | .withLogConsumer(outputFrame -> System.out.println(outputFrame.getUtf8String())) 33 | .withExposedPorts(3001) 34 | .waitingFor(Wait.forHttp("/").forStatusCode(404)); 35 | 36 | @Override 37 | protected McpClientTransport createMcpTransport() { 38 | return WebFluxSseClientTransport.builder(WebClient.builder().baseUrl(host)).build(); 39 | } 40 | 41 | @BeforeAll 42 | static void startContainer() { 43 | container.start(); 44 | int port = container.getMappedPort(3001); 45 | host = "http://" + container.getHost() + ":" + port; 46 | } 47 | 48 | @AfterAll 49 | static void stopContainer() { 50 | container.stop(); 51 | } 52 | 53 | protected Duration getInitializationTimeout() { 54 | return Duration.ofSeconds(1); 55 | } 56 | 57 | } 58 | -------------------------------------------------------------------------------- /mcp-spring/mcp-spring-webflux/src/test/java/io/modelcontextprotocol/client/WebFluxSseMcpAsyncClientTests.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2024-2024 the original author or authors. 3 | */ 4 | 5 | package io.modelcontextprotocol.client; 6 | 7 | import java.time.Duration; 8 | 9 | import org.junit.jupiter.api.AfterAll; 10 | import org.junit.jupiter.api.BeforeAll; 11 | import org.junit.jupiter.api.Timeout; 12 | import org.springframework.web.reactive.function.client.WebClient; 13 | import org.testcontainers.containers.GenericContainer; 14 | import org.testcontainers.containers.wait.strategy.Wait; 15 | 16 | import io.modelcontextprotocol.client.transport.WebFluxSseClientTransport; 17 | import io.modelcontextprotocol.spec.McpClientTransport; 18 | 19 | /** 20 | * Tests for the {@link McpAsyncClient} with {@link WebFluxSseClientTransport}. 21 | * 22 | * @author Christian Tzolov 23 | */ 24 | @Timeout(15) // Giving extra time beyond the client timeout 25 | class WebFluxSseMcpAsyncClientTests extends AbstractMcpAsyncClientTests { 26 | 27 | static String host = "http://localhost:3001"; 28 | 29 | // Uses the https://github.com/tzolov/mcp-everything-server-docker-image 30 | @SuppressWarnings("resource") 31 | static GenericContainer container = new GenericContainer<>("docker.io/tzolov/mcp-everything-server:v3") 32 | .withCommand("node dist/index.js sse") 33 | .withLogConsumer(outputFrame -> System.out.println(outputFrame.getUtf8String())) 34 | .withExposedPorts(3001) 35 | .waitingFor(Wait.forHttp("/").forStatusCode(404)); 36 | 37 | @Override 38 | protected McpClientTransport createMcpTransport() { 39 | return WebFluxSseClientTransport.builder(WebClient.builder().baseUrl(host)).build(); 40 | } 41 | 42 | @BeforeAll 43 | static void startContainer() { 44 | container.start(); 45 | int port = container.getMappedPort(3001); 46 | host = "http://" + container.getHost() + ":" + port; 47 | } 48 | 49 | @AfterAll 50 | static void stopContainer() { 51 | container.stop(); 52 | } 53 | 54 | protected Duration getInitializationTimeout() { 55 | return Duration.ofSeconds(1); 56 | } 57 | 58 | } 59 | -------------------------------------------------------------------------------- /mcp-spring/mcp-spring-webflux/src/test/java/io/modelcontextprotocol/server/WebFluxSseMcpAsyncServerTests.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2024-2024 the original author or authors. 3 | */ 4 | 5 | package io.modelcontextprotocol.server; 6 | 7 | import io.modelcontextprotocol.server.transport.WebFluxSseServerTransportProvider; 8 | import io.modelcontextprotocol.spec.McpServerTransportProvider; 9 | import org.junit.jupiter.api.Timeout; 10 | import reactor.netty.DisposableServer; 11 | import reactor.netty.http.server.HttpServer; 12 | 13 | import org.springframework.http.server.reactive.HttpHandler; 14 | import org.springframework.http.server.reactive.ReactorHttpHandlerAdapter; 15 | import org.springframework.web.reactive.function.server.RouterFunctions; 16 | 17 | /** 18 | * Tests for {@link McpAsyncServer} using {@link WebFluxSseServerTransportProvider}. 19 | * 20 | * @author Christian Tzolov 21 | */ 22 | @Timeout(15) // Giving extra time beyond the client timeout 23 | class WebFluxSseMcpAsyncServerTests extends AbstractMcpAsyncServerTests { 24 | 25 | private static final int PORT = TestUtil.findAvailablePort(); 26 | 27 | private static final String MESSAGE_ENDPOINT = "/mcp/message"; 28 | 29 | private DisposableServer httpServer; 30 | 31 | private McpServerTransportProvider createMcpTransportProvider() { 32 | var transportProvider = new WebFluxSseServerTransportProvider.Builder().messageEndpoint(MESSAGE_ENDPOINT) 33 | .build(); 34 | 35 | HttpHandler httpHandler = RouterFunctions.toHttpHandler(transportProvider.getRouterFunction()); 36 | ReactorHttpHandlerAdapter adapter = new ReactorHttpHandlerAdapter(httpHandler); 37 | httpServer = HttpServer.create().port(PORT).handle(adapter).bindNow(); 38 | return transportProvider; 39 | } 40 | 41 | @Override 42 | protected McpServer.AsyncSpecification prepareAsyncServerBuilder() { 43 | return McpServer.async(createMcpTransportProvider()); 44 | } 45 | 46 | @Override 47 | protected void onStart() { 48 | } 49 | 50 | @Override 51 | protected void onClose() { 52 | if (httpServer != null) { 53 | httpServer.disposeNow(); 54 | } 55 | } 56 | 57 | } 58 | -------------------------------------------------------------------------------- /mcp-spring/mcp-spring-webflux/src/test/java/io/modelcontextprotocol/server/WebFluxSseMcpSyncServerTests.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2024-2024 the original author or authors. 3 | */ 4 | 5 | package io.modelcontextprotocol.server; 6 | 7 | import io.modelcontextprotocol.server.transport.WebFluxSseServerTransportProvider; 8 | import io.modelcontextprotocol.spec.McpServerTransportProvider; 9 | import org.junit.jupiter.api.Timeout; 10 | import reactor.netty.DisposableServer; 11 | import reactor.netty.http.server.HttpServer; 12 | 13 | import org.springframework.http.server.reactive.HttpHandler; 14 | import org.springframework.http.server.reactive.ReactorHttpHandlerAdapter; 15 | import org.springframework.web.reactive.function.server.RouterFunctions; 16 | 17 | /** 18 | * Tests for {@link McpSyncServer} using {@link WebFluxSseServerTransportProvider}. 19 | * 20 | * @author Christian Tzolov 21 | */ 22 | @Timeout(15) // Giving extra time beyond the client timeout 23 | class WebFluxSseMcpSyncServerTests extends AbstractMcpSyncServerTests { 24 | 25 | private static final int PORT = TestUtil.findAvailablePort(); 26 | 27 | private static final String MESSAGE_ENDPOINT = "/mcp/message"; 28 | 29 | private DisposableServer httpServer; 30 | 31 | private WebFluxSseServerTransportProvider transportProvider; 32 | 33 | @Override 34 | protected McpServer.SyncSpecification prepareSyncServerBuilder() { 35 | return McpServer.sync(createMcpTransportProvider()); 36 | } 37 | 38 | private McpServerTransportProvider createMcpTransportProvider() { 39 | transportProvider = new WebFluxSseServerTransportProvider.Builder().messageEndpoint(MESSAGE_ENDPOINT).build(); 40 | return transportProvider; 41 | } 42 | 43 | @Override 44 | protected void onStart() { 45 | HttpHandler httpHandler = RouterFunctions.toHttpHandler(transportProvider.getRouterFunction()); 46 | ReactorHttpHandlerAdapter adapter = new ReactorHttpHandlerAdapter(httpHandler); 47 | httpServer = HttpServer.create().port(PORT).handle(adapter).bindNow(); 48 | } 49 | 50 | @Override 51 | protected void onClose() { 52 | if (httpServer != null) { 53 | httpServer.disposeNow(); 54 | } 55 | } 56 | 57 | } 58 | -------------------------------------------------------------------------------- /mcp-core/src/main/java/io/modelcontextprotocol/spec/MissingMcpTransportSession.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2024-2025 the original author or authors. 3 | */ 4 | 5 | package io.modelcontextprotocol.spec; 6 | 7 | import io.modelcontextprotocol.json.TypeRef; 8 | import io.modelcontextprotocol.util.Assert; 9 | import reactor.core.publisher.Mono; 10 | 11 | /** 12 | * A {@link McpLoggableSession} which represents a missing stream that would allow the 13 | * server to communicate with the client. Specifically, it can be used when a Streamable 14 | * HTTP client has not opened a listening SSE stream to accept messages for interactions 15 | * unrelated with concurrently running client-initiated requests. 16 | * 17 | * @author Dariusz Jędrzejczyk 18 | */ 19 | public class MissingMcpTransportSession implements McpLoggableSession { 20 | 21 | private final String sessionId; 22 | 23 | private volatile McpSchema.LoggingLevel minLoggingLevel = McpSchema.LoggingLevel.INFO; 24 | 25 | /** 26 | * Create an instance with the Session ID specified. 27 | * @param sessionId session ID 28 | */ 29 | public MissingMcpTransportSession(String sessionId) { 30 | this.sessionId = sessionId; 31 | } 32 | 33 | @Override 34 | public Mono sendRequest(String method, Object requestParams, TypeRef typeRef) { 35 | return Mono.error(new IllegalStateException("Stream unavailable for session " + this.sessionId)); 36 | } 37 | 38 | @Override 39 | public Mono sendNotification(String method, Object params) { 40 | return Mono.error(new IllegalStateException("Stream unavailable for session " + this.sessionId)); 41 | } 42 | 43 | @Override 44 | public Mono closeGracefully() { 45 | return Mono.empty(); 46 | } 47 | 48 | @Override 49 | public void close() { 50 | } 51 | 52 | @Override 53 | public void setMinLoggingLevel(McpSchema.LoggingLevel minLoggingLevel) { 54 | Assert.notNull(minLoggingLevel, "minLoggingLevel must not be null"); 55 | this.minLoggingLevel = minLoggingLevel; 56 | } 57 | 58 | @Override 59 | public boolean isNotificationForLevelAllowed(McpSchema.LoggingLevel loggingLevel) { 60 | return loggingLevel.level() >= this.minLoggingLevel.level(); 61 | } 62 | 63 | } 64 | -------------------------------------------------------------------------------- /mcp-spring/mcp-spring-webflux/src/test/java/io/modelcontextprotocol/server/WebFluxStreamableMcpSyncServerTests.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2024-2024 the original author or authors. 3 | */ 4 | 5 | package io.modelcontextprotocol.server; 6 | 7 | import io.modelcontextprotocol.server.transport.WebFluxStreamableServerTransportProvider; 8 | import io.modelcontextprotocol.spec.McpStreamableServerTransportProvider; 9 | import org.junit.jupiter.api.Timeout; 10 | import org.springframework.http.server.reactive.HttpHandler; 11 | import org.springframework.http.server.reactive.ReactorHttpHandlerAdapter; 12 | import org.springframework.web.reactive.function.server.RouterFunctions; 13 | import reactor.netty.DisposableServer; 14 | import reactor.netty.http.server.HttpServer; 15 | 16 | /** 17 | * Tests for {@link McpAsyncServer} using 18 | * {@link WebFluxStreamableServerTransportProvider}. 19 | * 20 | * @author Christian Tzolov 21 | * @author Dariusz Jędrzejczyk 22 | */ 23 | @Timeout(15) // Giving extra time beyond the client timeout 24 | class WebFluxStreamableMcpSyncServerTests extends AbstractMcpSyncServerTests { 25 | 26 | private static final int PORT = TestUtil.findAvailablePort(); 27 | 28 | private static final String MESSAGE_ENDPOINT = "/mcp/message"; 29 | 30 | private DisposableServer httpServer; 31 | 32 | private McpStreamableServerTransportProvider createMcpTransportProvider() { 33 | var transportProvider = WebFluxStreamableServerTransportProvider.builder() 34 | .messageEndpoint(MESSAGE_ENDPOINT) 35 | .build(); 36 | 37 | HttpHandler httpHandler = RouterFunctions.toHttpHandler(transportProvider.getRouterFunction()); 38 | ReactorHttpHandlerAdapter adapter = new ReactorHttpHandlerAdapter(httpHandler); 39 | httpServer = HttpServer.create().port(PORT).handle(adapter).bindNow(); 40 | return transportProvider; 41 | } 42 | 43 | @Override 44 | protected McpServer.SyncSpecification prepareSyncServerBuilder() { 45 | return McpServer.sync(createMcpTransportProvider()); 46 | } 47 | 48 | @Override 49 | protected void onStart() { 50 | } 51 | 52 | @Override 53 | protected void onClose() { 54 | if (httpServer != null) { 55 | httpServer.disposeNow(); 56 | } 57 | } 58 | 59 | } 60 | -------------------------------------------------------------------------------- /mcp-spring/mcp-spring-webflux/src/test/java/io/modelcontextprotocol/server/WebFluxStreamableMcpAsyncServerTests.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2024-2024 the original author or authors. 3 | */ 4 | 5 | package io.modelcontextprotocol.server; 6 | 7 | import io.modelcontextprotocol.server.transport.WebFluxStreamableServerTransportProvider; 8 | import io.modelcontextprotocol.spec.McpStreamableServerTransportProvider; 9 | import org.junit.jupiter.api.Timeout; 10 | import org.springframework.http.server.reactive.HttpHandler; 11 | import org.springframework.http.server.reactive.ReactorHttpHandlerAdapter; 12 | import org.springframework.web.reactive.function.server.RouterFunctions; 13 | import reactor.netty.DisposableServer; 14 | import reactor.netty.http.server.HttpServer; 15 | 16 | /** 17 | * Tests for {@link McpAsyncServer} using 18 | * {@link WebFluxStreamableServerTransportProvider}. 19 | * 20 | * @author Christian Tzolov 21 | * @author Dariusz Jędrzejczyk 22 | */ 23 | @Timeout(15) // Giving extra time beyond the client timeout 24 | class WebFluxStreamableMcpAsyncServerTests extends AbstractMcpAsyncServerTests { 25 | 26 | private static final int PORT = TestUtil.findAvailablePort(); 27 | 28 | private static final String MESSAGE_ENDPOINT = "/mcp/message"; 29 | 30 | private DisposableServer httpServer; 31 | 32 | private McpStreamableServerTransportProvider createMcpTransportProvider() { 33 | var transportProvider = WebFluxStreamableServerTransportProvider.builder() 34 | .messageEndpoint(MESSAGE_ENDPOINT) 35 | .build(); 36 | 37 | HttpHandler httpHandler = RouterFunctions.toHttpHandler(transportProvider.getRouterFunction()); 38 | ReactorHttpHandlerAdapter adapter = new ReactorHttpHandlerAdapter(httpHandler); 39 | httpServer = HttpServer.create().port(PORT).handle(adapter).bindNow(); 40 | return transportProvider; 41 | } 42 | 43 | @Override 44 | protected McpServer.AsyncSpecification prepareAsyncServerBuilder() { 45 | return McpServer.async(createMcpTransportProvider()); 46 | } 47 | 48 | @Override 49 | protected void onStart() { 50 | } 51 | 52 | @Override 53 | protected void onClose() { 54 | if (httpServer != null) { 55 | httpServer.disposeNow(); 56 | } 57 | } 58 | 59 | } 60 | -------------------------------------------------------------------------------- /mcp-core/src/main/java/io/modelcontextprotocol/spec/DefaultMcpStreamableServerSessionFactory.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2024-2025 the original author or authors. 3 | */ 4 | 5 | package io.modelcontextprotocol.spec; 6 | 7 | import io.modelcontextprotocol.server.McpNotificationHandler; 8 | import io.modelcontextprotocol.server.McpRequestHandler; 9 | 10 | import java.time.Duration; 11 | import java.util.Map; 12 | import java.util.UUID; 13 | 14 | /** 15 | * A default implementation of {@link McpStreamableServerSession.Factory}. 16 | * 17 | * @author Dariusz Jędrzejczyk 18 | */ 19 | public class DefaultMcpStreamableServerSessionFactory implements McpStreamableServerSession.Factory { 20 | 21 | Duration requestTimeout; 22 | 23 | McpStreamableServerSession.InitRequestHandler initRequestHandler; 24 | 25 | Map> requestHandlers; 26 | 27 | Map notificationHandlers; 28 | 29 | /** 30 | * Constructs an instance 31 | * @param requestTimeout timeout for requests 32 | * @param initRequestHandler initialization request handler 33 | * @param requestHandlers map of MCP request handlers keyed by method name 34 | * @param notificationHandlers map of MCP notification handlers keyed by method name 35 | */ 36 | public DefaultMcpStreamableServerSessionFactory(Duration requestTimeout, 37 | McpStreamableServerSession.InitRequestHandler initRequestHandler, 38 | Map> requestHandlers, 39 | Map notificationHandlers) { 40 | this.requestTimeout = requestTimeout; 41 | this.initRequestHandler = initRequestHandler; 42 | this.requestHandlers = requestHandlers; 43 | this.notificationHandlers = notificationHandlers; 44 | } 45 | 46 | @Override 47 | public McpStreamableServerSession.McpStreamableServerSessionInit startSession( 48 | McpSchema.InitializeRequest initializeRequest) { 49 | return new McpStreamableServerSession.McpStreamableServerSessionInit( 50 | new McpStreamableServerSession(UUID.randomUUID().toString(), initializeRequest.capabilities(), 51 | initializeRequest.clientInfo(), requestTimeout, requestHandlers, notificationHandlers), 52 | this.initRequestHandler.handle(initializeRequest)); 53 | } 54 | 55 | } 56 | -------------------------------------------------------------------------------- /mcp-spring/mcp-spring-webmvc/src/test/java/io/modelcontextprotocol/server/TomcatTestUtil.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 - 2025 the original author or authors. 3 | */ 4 | package io.modelcontextprotocol.server; 5 | 6 | import org.apache.catalina.Context; 7 | import org.apache.catalina.startup.Tomcat; 8 | 9 | import org.springframework.web.context.support.AnnotationConfigWebApplicationContext; 10 | import org.springframework.web.servlet.DispatcherServlet; 11 | 12 | /** 13 | * @author Christian Tzolov 14 | */ 15 | public class TomcatTestUtil { 16 | 17 | TomcatTestUtil() { 18 | // Prevent instantiation 19 | } 20 | 21 | public record TomcatServer(Tomcat tomcat, AnnotationConfigWebApplicationContext appContext) { 22 | } 23 | 24 | public static TomcatServer createTomcatServer(String contextPath, int port, Class componentClass) { 25 | 26 | // Set up Tomcat first 27 | var tomcat = new Tomcat(); 28 | tomcat.setPort(port); 29 | 30 | // Set Tomcat base directory to java.io.tmpdir to avoid permission issues 31 | String baseDir = System.getProperty("java.io.tmpdir"); 32 | tomcat.setBaseDir(baseDir); 33 | 34 | // Use the same directory for document base 35 | Context context = tomcat.addContext(contextPath, baseDir); 36 | 37 | // Create and configure Spring WebMvc context 38 | var appContext = new AnnotationConfigWebApplicationContext(); 39 | appContext.register(componentClass); 40 | appContext.setServletContext(context.getServletContext()); 41 | appContext.refresh(); 42 | 43 | // Create DispatcherServlet with our Spring context 44 | DispatcherServlet dispatcherServlet = new DispatcherServlet(appContext); 45 | 46 | // Add servlet to Tomcat and get the wrapper 47 | var wrapper = Tomcat.addServlet(context, "dispatcherServlet", dispatcherServlet); 48 | wrapper.setLoadOnStartup(1); 49 | wrapper.setAsyncSupported(true); 50 | context.addServletMappingDecoded("/*", "dispatcherServlet"); 51 | 52 | try { 53 | // Configure and start the connector with async support 54 | var connector = tomcat.getConnector(); 55 | connector.setAsyncTimeout(3000); // 3 seconds timeout for async requests 56 | } 57 | catch (Exception e) { 58 | throw new RuntimeException("Failed to start Tomcat", e); 59 | } 60 | 61 | return new TomcatServer(tomcat, appContext); 62 | } 63 | 64 | } 65 | -------------------------------------------------------------------------------- /mcp-core/src/main/java/io/modelcontextprotocol/spec/McpTransportSession.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2024-2025 the original author or authors. 3 | */ 4 | 5 | package io.modelcontextprotocol.spec; 6 | 7 | import java.util.Optional; 8 | 9 | import org.reactivestreams.Publisher; 10 | 11 | /** 12 | * An abstraction of the session as perceived from the MCP transport layer. Not to be 13 | * confused with the {@link McpSession} type that operates at the level of the JSON-RPC 14 | * communication protocol and matches asynchronous responses with previously issued 15 | * requests. 16 | * 17 | * @param the resource representing the connection that the transport 18 | * manages. 19 | * @author Dariusz Jędrzejczyk 20 | */ 21 | public interface McpTransportSession { 22 | 23 | /** 24 | * In case of stateful MCP servers, the value is present and contains the String 25 | * identifier for the transport-level session. 26 | * @return optional session id 27 | */ 28 | Optional sessionId(); 29 | 30 | /** 31 | * Stateful operation that flips the un-initialized state to initialized if this is 32 | * the first call. If the transport provides a session id for the communication, 33 | * argument should not be null to record the current identifier. 34 | * @param sessionId session identifier as provided by the server 35 | * @return if successful, this method returns {@code true} and means that a 36 | * post-initialization step can be performed 37 | */ 38 | boolean markInitialized(String sessionId); 39 | 40 | /** 41 | * Adds a resource that this transport session can monitor and dismiss when needed. 42 | * @param connection the managed resource 43 | */ 44 | void addConnection(CONNECTION connection); 45 | 46 | /** 47 | * Called when the resource is terminating by itself and the transport session does 48 | * not need to track it anymore. 49 | * @param connection the resource to remove from the monitored collection 50 | */ 51 | void removeConnection(CONNECTION connection); 52 | 53 | /** 54 | * Close and clear the monitored resources. Potentially asynchronous. 55 | */ 56 | void close(); 57 | 58 | /** 59 | * Close and clear the monitored resources in a graceful manner. 60 | * @return completes once all resources have been dismissed 61 | */ 62 | Publisher closeGracefully(); 63 | 64 | } 65 | -------------------------------------------------------------------------------- /mcp-core/src/test/java/io/modelcontextprotocol/MockMcpServerTransport.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2024-2024 the original author or authors. 3 | */ 4 | 5 | package io.modelcontextprotocol; 6 | 7 | import java.util.ArrayList; 8 | import java.util.List; 9 | import java.util.function.BiConsumer; 10 | 11 | import io.modelcontextprotocol.json.McpJsonMapper; 12 | import io.modelcontextprotocol.json.TypeRef; 13 | import io.modelcontextprotocol.spec.McpSchema; 14 | import io.modelcontextprotocol.spec.McpSchema.JSONRPCNotification; 15 | import io.modelcontextprotocol.spec.McpSchema.JSONRPCRequest; 16 | import io.modelcontextprotocol.spec.McpServerTransport; 17 | import reactor.core.publisher.Mono; 18 | 19 | /** 20 | * A mock implementation of the {@link McpServerTransport} interfaces. 21 | */ 22 | public class MockMcpServerTransport implements McpServerTransport { 23 | 24 | private final List sent = new ArrayList<>(); 25 | 26 | private final BiConsumer interceptor; 27 | 28 | public MockMcpServerTransport() { 29 | this((t, msg) -> { 30 | }); 31 | } 32 | 33 | public MockMcpServerTransport(BiConsumer interceptor) { 34 | this.interceptor = interceptor; 35 | } 36 | 37 | @Override 38 | public Mono sendMessage(McpSchema.JSONRPCMessage message) { 39 | sent.add(message); 40 | interceptor.accept(this, message); 41 | return Mono.empty(); 42 | } 43 | 44 | public McpSchema.JSONRPCRequest getLastSentMessageAsRequest() { 45 | return (JSONRPCRequest) getLastSentMessage(); 46 | } 47 | 48 | public McpSchema.JSONRPCNotification getLastSentMessageAsNotification() { 49 | return (JSONRPCNotification) getLastSentMessage(); 50 | } 51 | 52 | public McpSchema.JSONRPCMessage getLastSentMessage() { 53 | return !sent.isEmpty() ? sent.get(sent.size() - 1) : null; 54 | } 55 | 56 | public void clearSentMessages() { 57 | sent.clear(); 58 | } 59 | 60 | public List getAllSentMessages() { 61 | return new ArrayList<>(sent); 62 | } 63 | 64 | @Override 65 | public Mono closeGracefully() { 66 | return Mono.empty(); 67 | } 68 | 69 | @Override 70 | public T unmarshalFrom(Object data, TypeRef typeRef) { 71 | return McpJsonMapper.getDefault().convertValue(data, typeRef); 72 | } 73 | 74 | } 75 | -------------------------------------------------------------------------------- /mcp-core/src/test/java/io/modelcontextprotocol/client/transport/customizer/DelegatingMcpSyncHttpClientRequestCustomizerTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2024-2025 the original author or authors. 3 | */ 4 | 5 | package io.modelcontextprotocol.client.transport.customizer; 6 | 7 | import java.net.URI; 8 | import java.net.http.HttpRequest; 9 | import java.util.List; 10 | import org.junit.jupiter.api.Test; 11 | import org.mockito.Mockito; 12 | 13 | import io.modelcontextprotocol.common.McpTransportContext; 14 | 15 | import static org.assertj.core.api.Assertions.assertThat; 16 | import static org.assertj.core.api.Assertions.assertThatThrownBy; 17 | import static org.mockito.Mockito.verify; 18 | 19 | /** 20 | * Tests for {@link DelegatingMcpSyncHttpClientRequestCustomizer}. 21 | * 22 | * @author Daniel Garnier-Moiroux 23 | */ 24 | class DelegatingMcpSyncHttpClientRequestCustomizerTest { 25 | 26 | private static final URI TEST_URI = URI.create("https://example.com"); 27 | 28 | private final HttpRequest.Builder TEST_BUILDER = HttpRequest.newBuilder(TEST_URI); 29 | 30 | @Test 31 | void delegates() { 32 | var mockCustomizer = Mockito.mock(McpSyncHttpClientRequestCustomizer.class); 33 | var customizer = new DelegatingMcpSyncHttpClientRequestCustomizer(List.of(mockCustomizer)); 34 | 35 | var context = McpTransportContext.EMPTY; 36 | customizer.customize(TEST_BUILDER, "GET", TEST_URI, "{\"everybody\": \"needs somebody\"}", context); 37 | 38 | verify(mockCustomizer).customize(TEST_BUILDER, "GET", TEST_URI, "{\"everybody\": \"needs somebody\"}", context); 39 | } 40 | 41 | @Test 42 | void delegatesInOrder() { 43 | var testHeaderName = "x-test"; 44 | var customizer = new DelegatingMcpSyncHttpClientRequestCustomizer( 45 | List.of((builder, method, uri, body, ctx) -> builder.header(testHeaderName, "one"), 46 | (builder, method, uri, body, ctx) -> builder.header(testHeaderName, "two"))); 47 | 48 | customizer.customize(TEST_BUILDER, "GET", TEST_URI, null, McpTransportContext.EMPTY); 49 | var request = TEST_BUILDER.build(); 50 | 51 | assertThat(request.headers().allValues(testHeaderName)).containsExactly("one", "two"); 52 | } 53 | 54 | @Test 55 | void constructorRequiresNonNull() { 56 | assertThatThrownBy(() -> new DelegatingMcpAsyncHttpClientRequestCustomizer(null)) 57 | .isInstanceOf(IllegalArgumentException.class) 58 | .hasMessage("Customizers must not be null"); 59 | } 60 | 61 | } 62 | -------------------------------------------------------------------------------- /mcp-json/src/main/java/io/modelcontextprotocol/json/schema/JsonSchemaValidator.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2024-2024 the original author or authors. 3 | */ 4 | package io.modelcontextprotocol.json.schema; 5 | 6 | import java.util.Map; 7 | 8 | /** 9 | * Interface for validating structured content against a JSON schema. This interface 10 | * defines a method to validate structured content based on the provided output schema. 11 | * 12 | * @author Christian Tzolov 13 | */ 14 | public interface JsonSchemaValidator { 15 | 16 | /** 17 | * Represents the result of a validation operation. 18 | * 19 | * @param valid Indicates whether the validation was successful. 20 | * @param errorMessage An error message if the validation failed, otherwise null. 21 | * @param jsonStructuredOutput The text structured content in JSON format if the 22 | * validation was successful, otherwise null. 23 | */ 24 | record ValidationResponse(boolean valid, String errorMessage, String jsonStructuredOutput) { 25 | 26 | public static ValidationResponse asValid(String jsonStructuredOutput) { 27 | return new ValidationResponse(true, null, jsonStructuredOutput); 28 | } 29 | 30 | public static ValidationResponse asInvalid(String message) { 31 | return new ValidationResponse(false, message, null); 32 | } 33 | } 34 | 35 | /** 36 | * Validates the structured content against the provided JSON schema. 37 | * @param schema The JSON schema to validate against. 38 | * @param structuredContent The structured content to validate. 39 | * @return A ValidationResponse indicating whether the validation was successful or 40 | * not. 41 | */ 42 | ValidationResponse validate(Map schema, Object structuredContent); 43 | 44 | /** 45 | * Creates the default {@link JsonSchemaValidator}. 46 | * @return The default {@link JsonSchemaValidator} 47 | * @throws IllegalStateException If no {@link JsonSchemaValidator} implementation 48 | * exists on the classpath. 49 | */ 50 | static JsonSchemaValidator createDefault() { 51 | return JsonSchemaInternal.createDefaultValidator(); 52 | } 53 | 54 | /** 55 | * Returns the default {@link JsonSchemaValidator}. 56 | * @return The default {@link JsonSchemaValidator} 57 | * @throws IllegalStateException If no {@link JsonSchemaValidator} implementation 58 | * exists on the classpath. 59 | */ 60 | static JsonSchemaValidator getDefault() { 61 | return JsonSchemaInternal.getDefaultValidator(); 62 | } 63 | 64 | } 65 | -------------------------------------------------------------------------------- /mcp-core/src/test/java/io/modelcontextprotocol/util/UtilsTests.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2024-2024 the original author or authors. 3 | */ 4 | 5 | package io.modelcontextprotocol.util; 6 | 7 | import org.junit.jupiter.api.Test; 8 | 9 | import java.net.URI; 10 | import java.util.Collection; 11 | import java.util.List; 12 | import java.util.Map; 13 | 14 | import static org.assertj.core.api.Assertions.assertThat; 15 | import static org.assertj.core.api.Assertions.assertThatThrownBy; 16 | import static org.junit.jupiter.api.Assertions.assertFalse; 17 | import static org.junit.jupiter.api.Assertions.assertTrue; 18 | import org.junit.jupiter.params.ParameterizedTest; 19 | import org.junit.jupiter.params.provider.CsvSource; 20 | 21 | class UtilsTests { 22 | 23 | @Test 24 | void testHasText() { 25 | assertFalse(Utils.hasText(null)); 26 | assertFalse(Utils.hasText("")); 27 | assertFalse(Utils.hasText(" ")); 28 | assertTrue(Utils.hasText("test")); 29 | } 30 | 31 | @Test 32 | void testCollectionIsEmpty() { 33 | assertTrue(Utils.isEmpty((Collection) null)); 34 | assertTrue(Utils.isEmpty(List.of())); 35 | assertFalse(Utils.isEmpty(List.of("test"))); 36 | } 37 | 38 | @Test 39 | void testMapIsEmpty() { 40 | assertTrue(Utils.isEmpty((Map) null)); 41 | assertTrue(Utils.isEmpty(Map.of())); 42 | assertFalse(Utils.isEmpty(Map.of("key", "value"))); 43 | } 44 | 45 | @ParameterizedTest 46 | @CsvSource({ 47 | // relative endpoints 48 | "http://localhost:8080/root, /api/v1, http://localhost:8080/api/v1", 49 | "http://localhost:8080/root/, api, http://localhost:8080/root/api", 50 | "http://localhost:8080, /api, http://localhost:8080/api", 51 | // absolute endpoints matching base 52 | "http://localhost:8080/root, http://localhost:8080/root/api/v1, http://localhost:8080/root/api/v1", 53 | "http://localhost:8080/root, http://localhost:8080/root, http://localhost:8080/root" }) 54 | void testValidUriResolution(String baseUrl, String endpoint, String expectedResult) { 55 | URI result = Utils.resolveUri(URI.create(baseUrl), endpoint); 56 | assertThat(result.toString()).isEqualTo(expectedResult); 57 | } 58 | 59 | @ParameterizedTest 60 | @CsvSource({ "http://localhost:8080/root, http://localhost:8080/other/api", 61 | "http://localhost:8080/root, http://otherhost/api", 62 | "http://localhost:8080/root, http://localhost:9090/root/api" }) 63 | void testAbsoluteUriNotMatchingBase(String baseUrl, String endpoint) { 64 | assertThatThrownBy(() -> Utils.resolveUri(URI.create(baseUrl), endpoint)) 65 | .isInstanceOf(IllegalArgumentException.class) 66 | .hasMessageContaining("does not match the base URL"); 67 | } 68 | 69 | } -------------------------------------------------------------------------------- /mcp-core/src/test/java/io/modelcontextprotocol/server/transport/TomcatTestUtil.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 - 2025 the original author or authors. 3 | */ 4 | 5 | package io.modelcontextprotocol.server.transport; 6 | 7 | import java.io.IOException; 8 | import java.net.InetSocketAddress; 9 | import java.net.ServerSocket; 10 | 11 | import jakarta.servlet.Filter; 12 | import jakarta.servlet.Servlet; 13 | import org.apache.catalina.Context; 14 | import org.apache.catalina.startup.Tomcat; 15 | import org.apache.tomcat.util.descriptor.web.FilterDef; 16 | import org.apache.tomcat.util.descriptor.web.FilterMap; 17 | 18 | /** 19 | * @author Christian Tzolov 20 | * @author Daniel Garnier-Moiroux 21 | */ 22 | public class TomcatTestUtil { 23 | 24 | TomcatTestUtil() { 25 | // Prevent instantiation 26 | } 27 | 28 | public static Tomcat createTomcatServer(String contextPath, int port, Servlet servlet, 29 | Filter... additionalFilters) { 30 | 31 | var tomcat = new Tomcat(); 32 | tomcat.setPort(port); 33 | 34 | String baseDir = System.getProperty("java.io.tmpdir"); 35 | tomcat.setBaseDir(baseDir); 36 | 37 | Context context = tomcat.addContext(contextPath, baseDir); 38 | 39 | // Add transport servlet to Tomcat 40 | org.apache.catalina.Wrapper wrapper = context.createWrapper(); 41 | wrapper.setName("mcpServlet"); 42 | wrapper.setServlet(servlet); 43 | wrapper.setLoadOnStartup(1); 44 | wrapper.setAsyncSupported(true); 45 | context.addChild(wrapper); 46 | context.addServletMappingDecoded("/*", "mcpServlet"); 47 | 48 | for (var filter : additionalFilters) { 49 | var filterDef = new FilterDef(); 50 | filterDef.setFilter(filter); 51 | filterDef.setFilterName(McpTestRequestRecordingServletFilter.class.getSimpleName()); 52 | context.addFilterDef(filterDef); 53 | 54 | var filterMap = new FilterMap(); 55 | filterMap.setFilterName(McpTestRequestRecordingServletFilter.class.getSimpleName()); 56 | filterMap.addURLPattern("/*"); 57 | context.addFilterMap(filterMap); 58 | } 59 | 60 | var connector = tomcat.getConnector(); 61 | connector.setAsyncTimeout(3000); 62 | 63 | return tomcat; 64 | } 65 | 66 | /** 67 | * Finds an available port on the local machine. 68 | * @return an available port number 69 | * @throws IllegalStateException if no available port can be found 70 | */ 71 | public static int findAvailablePort() { 72 | try (final ServerSocket socket = new ServerSocket()) { 73 | socket.bind(new InetSocketAddress(0)); 74 | return socket.getLocalPort(); 75 | } 76 | catch (final IOException e) { 77 | throw new IllegalStateException("Cannot bind to an available port!", e); 78 | } 79 | } 80 | 81 | } 82 | -------------------------------------------------------------------------------- /mcp-core/src/main/java/io/modelcontextprotocol/server/DefaultMcpStatelessServerHandler.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2024-2025 the original author or authors. 3 | */ 4 | 5 | package io.modelcontextprotocol.server; 6 | 7 | import io.modelcontextprotocol.common.McpTransportContext; 8 | import io.modelcontextprotocol.spec.McpError; 9 | import io.modelcontextprotocol.spec.McpSchema; 10 | import org.slf4j.Logger; 11 | import org.slf4j.LoggerFactory; 12 | import reactor.core.publisher.Mono; 13 | 14 | import java.util.Map; 15 | 16 | class DefaultMcpStatelessServerHandler implements McpStatelessServerHandler { 17 | 18 | private static final Logger logger = LoggerFactory.getLogger(DefaultMcpStatelessServerHandler.class); 19 | 20 | Map> requestHandlers; 21 | 22 | Map notificationHandlers; 23 | 24 | public DefaultMcpStatelessServerHandler(Map> requestHandlers, 25 | Map notificationHandlers) { 26 | this.requestHandlers = requestHandlers; 27 | this.notificationHandlers = notificationHandlers; 28 | } 29 | 30 | @Override 31 | public Mono handleRequest(McpTransportContext transportContext, 32 | McpSchema.JSONRPCRequest request) { 33 | McpStatelessRequestHandler requestHandler = this.requestHandlers.get(request.method()); 34 | if (requestHandler == null) { 35 | return Mono.error(new McpError("Missing handler for request type: " + request.method())); 36 | } 37 | return requestHandler.handle(transportContext, request.params()) 38 | .map(result -> new McpSchema.JSONRPCResponse(McpSchema.JSONRPC_VERSION, request.id(), result, null)) 39 | .onErrorResume(t -> { 40 | McpSchema.JSONRPCResponse.JSONRPCError error; 41 | if (t instanceof McpError mcpError && mcpError.getJsonRpcError() != null) { 42 | error = mcpError.getJsonRpcError(); 43 | } 44 | else { 45 | error = new McpSchema.JSONRPCResponse.JSONRPCError(McpSchema.ErrorCodes.INTERNAL_ERROR, 46 | t.getMessage(), null); 47 | } 48 | return Mono.just(new McpSchema.JSONRPCResponse(McpSchema.JSONRPC_VERSION, request.id(), null, error)); 49 | }); 50 | } 51 | 52 | @Override 53 | public Mono handleNotification(McpTransportContext transportContext, 54 | McpSchema.JSONRPCNotification notification) { 55 | McpStatelessNotificationHandler notificationHandler = this.notificationHandlers.get(notification.method()); 56 | if (notificationHandler == null) { 57 | logger.warn("Missing handler for notification type: {}", notification.method()); 58 | return Mono.empty(); 59 | } 60 | return notificationHandler.handle(transportContext, notification.params()); 61 | } 62 | 63 | } 64 | -------------------------------------------------------------------------------- /mcp-core/src/main/java/io/modelcontextprotocol/spec/DefaultMcpTransportSession.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2024-2025 the original author or authors. 3 | */ 4 | 5 | package io.modelcontextprotocol.spec; 6 | 7 | import org.reactivestreams.Publisher; 8 | import org.slf4j.Logger; 9 | import org.slf4j.LoggerFactory; 10 | import reactor.core.Disposable; 11 | import reactor.core.Disposables; 12 | import reactor.core.publisher.Mono; 13 | 14 | import java.util.Optional; 15 | import java.util.concurrent.atomic.AtomicBoolean; 16 | import java.util.concurrent.atomic.AtomicReference; 17 | import java.util.function.Function; 18 | 19 | /** 20 | * Default implementation of {@link McpTransportSession} which manages the open 21 | * connections using tye {@link Disposable} type and allows to perform clean up using the 22 | * {@link Disposable#dispose()} method. 23 | * 24 | * @author Dariusz Jędrzejczyk 25 | */ 26 | public class DefaultMcpTransportSession implements McpTransportSession { 27 | 28 | private static final Logger logger = LoggerFactory.getLogger(DefaultMcpTransportSession.class); 29 | 30 | private final Disposable.Composite openConnections = Disposables.composite(); 31 | 32 | private final AtomicBoolean initialized = new AtomicBoolean(false); 33 | 34 | private final AtomicReference sessionId = new AtomicReference<>(); 35 | 36 | private final Function> onClose; 37 | 38 | public DefaultMcpTransportSession(Function> onClose) { 39 | this.onClose = onClose; 40 | } 41 | 42 | @Override 43 | public Optional sessionId() { 44 | return Optional.ofNullable(this.sessionId.get()); 45 | } 46 | 47 | @Override 48 | public boolean markInitialized(String sessionId) { 49 | boolean flipped = this.initialized.compareAndSet(false, true); 50 | if (flipped) { 51 | this.sessionId.set(sessionId); 52 | logger.debug("Established session with id {}", sessionId); 53 | } 54 | else { 55 | if (sessionId != null && !sessionId.equals(this.sessionId.get())) { 56 | logger.warn("Different session id provided in response. Expecting {} but server returned {}", 57 | this.sessionId.get(), sessionId); 58 | } 59 | } 60 | return flipped; 61 | } 62 | 63 | @Override 64 | public void addConnection(Disposable connection) { 65 | this.openConnections.add(connection); 66 | } 67 | 68 | @Override 69 | public void removeConnection(Disposable connection) { 70 | this.openConnections.remove(connection); 71 | } 72 | 73 | @Override 74 | public void close() { 75 | this.closeGracefully().subscribe(); 76 | } 77 | 78 | @Override 79 | public Mono closeGracefully() { 80 | return Mono.from(this.onClose.apply(this.sessionId.get())) 81 | .then(Mono.fromRunnable(this.openConnections::dispose)); 82 | } 83 | 84 | } 85 | -------------------------------------------------------------------------------- /mcp-core/src/test/java/io/modelcontextprotocol/client/transport/customizer/DelegatingMcpAsyncHttpClientRequestCustomizerTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2024-2025 the original author or authors. 3 | */ 4 | 5 | package io.modelcontextprotocol.client.transport.customizer; 6 | 7 | import java.net.URI; 8 | import java.net.http.HttpRequest; 9 | import java.util.List; 10 | import org.junit.jupiter.api.Test; 11 | import reactor.core.publisher.Mono; 12 | import reactor.test.StepVerifier; 13 | 14 | import io.modelcontextprotocol.common.McpTransportContext; 15 | 16 | import static org.assertj.core.api.Assertions.assertThatThrownBy; 17 | import static org.mockito.ArgumentMatchers.any; 18 | import static org.mockito.Mockito.mock; 19 | import static org.mockito.Mockito.verify; 20 | import static org.mockito.Mockito.when; 21 | 22 | /** 23 | * Tests for {@link DelegatingMcpAsyncHttpClientRequestCustomizer}. 24 | * 25 | * @author Daniel Garnier-Moiroux 26 | */ 27 | class DelegatingMcpAsyncHttpClientRequestCustomizerTest { 28 | 29 | private static final URI TEST_URI = URI.create("https://example.com"); 30 | 31 | private final HttpRequest.Builder TEST_BUILDER = HttpRequest.newBuilder(TEST_URI); 32 | 33 | @Test 34 | void delegates() { 35 | var mockCustomizer = mock(McpAsyncHttpClientRequestCustomizer.class); 36 | when(mockCustomizer.customize(any(), any(), any(), any(), any())) 37 | .thenAnswer(invocation -> Mono.just(invocation.getArguments()[0])); 38 | var customizer = new DelegatingMcpAsyncHttpClientRequestCustomizer(List.of(mockCustomizer)); 39 | 40 | var context = McpTransportContext.EMPTY; 41 | StepVerifier 42 | .create(customizer.customize(TEST_BUILDER, "GET", TEST_URI, "{\"everybody\": \"needs somebody\"}", context)) 43 | .expectNext(TEST_BUILDER) 44 | .verifyComplete(); 45 | 46 | verify(mockCustomizer).customize(TEST_BUILDER, "GET", TEST_URI, "{\"everybody\": \"needs somebody\"}", context); 47 | } 48 | 49 | @Test 50 | void delegatesInOrder() { 51 | var customizer = new DelegatingMcpAsyncHttpClientRequestCustomizer( 52 | List.of((builder, method, uri, body, ctx) -> Mono.just(builder.copy().header("x-test", "one")), 53 | (builder, method, uri, body, ctx) -> Mono.just(builder.copy().header("x-test", "two")))); 54 | 55 | var headers = Mono 56 | .from(customizer.customize(TEST_BUILDER, "GET", TEST_URI, "{\"everybody\": \"needs somebody\"}", 57 | McpTransportContext.EMPTY)) 58 | .map(HttpRequest.Builder::build) 59 | .map(HttpRequest::headers) 60 | .flatMapIterable(h -> h.allValues("x-test")); 61 | 62 | StepVerifier.create(headers).expectNext("one").expectNext("two").verifyComplete(); 63 | } 64 | 65 | @Test 66 | void constructorRequiresNonNull() { 67 | assertThatThrownBy(() -> new DelegatingMcpAsyncHttpClientRequestCustomizer(null)) 68 | .isInstanceOf(IllegalArgumentException.class) 69 | .hasMessage("Customizers must not be null"); 70 | } 71 | 72 | } 73 | -------------------------------------------------------------------------------- /mcp-json/src/main/java/io/modelcontextprotocol/json/McpJsonInternal.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 - 2025 the original author or authors. 3 | */ 4 | 5 | package io.modelcontextprotocol.json; 6 | 7 | import java.util.ServiceLoader; 8 | import java.util.concurrent.atomic.AtomicReference; 9 | import java.util.stream.Stream; 10 | 11 | /** 12 | * Utility class for creating a default {@link McpJsonMapper} instance. This class 13 | * provides a single method to create a default mapper using the {@link ServiceLoader} 14 | * mechanism. 15 | */ 16 | final class McpJsonInternal { 17 | 18 | private static McpJsonMapper defaultJsonMapper = null; 19 | 20 | /** 21 | * Returns the cached default {@link McpJsonMapper} instance. If the default mapper 22 | * has not been created yet, it will be initialized using the 23 | * {@link #createDefaultMapper()} method. 24 | * @return the default {@link McpJsonMapper} instance 25 | * @throws IllegalStateException if no default {@link McpJsonMapper} implementation is 26 | * found 27 | */ 28 | static McpJsonMapper getDefaultMapper() { 29 | if (defaultJsonMapper == null) { 30 | defaultJsonMapper = McpJsonInternal.createDefaultMapper(); 31 | } 32 | return defaultJsonMapper; 33 | } 34 | 35 | /** 36 | * Creates a default {@link McpJsonMapper} instance using the {@link ServiceLoader} 37 | * mechanism. The default mapper is resolved by loading the first available 38 | * {@link McpJsonMapperSupplier} implementation on the classpath. 39 | * @return the default {@link McpJsonMapper} instance 40 | * @throws IllegalStateException if no default {@link McpJsonMapper} implementation is 41 | * found 42 | */ 43 | static McpJsonMapper createDefaultMapper() { 44 | AtomicReference ex = new AtomicReference<>(); 45 | return ServiceLoader.load(McpJsonMapperSupplier.class).stream().flatMap(p -> { 46 | try { 47 | McpJsonMapperSupplier supplier = p.get(); 48 | return Stream.ofNullable(supplier); 49 | } 50 | catch (Exception e) { 51 | addException(ex, e); 52 | return Stream.empty(); 53 | } 54 | }).flatMap(jsonMapperSupplier -> { 55 | try { 56 | return Stream.ofNullable(jsonMapperSupplier.get()); 57 | } 58 | catch (Exception e) { 59 | addException(ex, e); 60 | return Stream.empty(); 61 | } 62 | }).findFirst().orElseThrow(() -> { 63 | if (ex.get() != null) { 64 | return ex.get(); 65 | } 66 | else { 67 | return new IllegalStateException("No default McpJsonMapper implementation found"); 68 | } 69 | }); 70 | } 71 | 72 | private static void addException(AtomicReference ref, Exception toAdd) { 73 | ref.updateAndGet(existing -> { 74 | if (existing == null) { 75 | return new IllegalStateException("Failed to initialize default McpJsonMapper", toAdd); 76 | } 77 | else { 78 | existing.addSuppressed(toAdd); 79 | return existing; 80 | } 81 | }); 82 | } 83 | 84 | } 85 | -------------------------------------------------------------------------------- /mcp-core/src/test/java/io/modelcontextprotocol/client/HttpClientStreamableHttpSyncClientTests.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2024-2025 the original author or authors. 3 | */ 4 | 5 | package io.modelcontextprotocol.client; 6 | 7 | import java.net.URI; 8 | import java.util.Map; 9 | 10 | import org.junit.jupiter.api.AfterAll; 11 | import org.junit.jupiter.api.BeforeAll; 12 | import org.junit.jupiter.api.Test; 13 | import org.junit.jupiter.api.Timeout; 14 | import org.testcontainers.containers.GenericContainer; 15 | import org.testcontainers.containers.wait.strategy.Wait; 16 | 17 | import io.modelcontextprotocol.client.transport.HttpClientStreamableHttpTransport; 18 | import io.modelcontextprotocol.client.transport.customizer.McpSyncHttpClientRequestCustomizer; 19 | import io.modelcontextprotocol.common.McpTransportContext; 20 | import io.modelcontextprotocol.spec.McpClientTransport; 21 | 22 | import static org.mockito.ArgumentMatchers.any; 23 | import static org.mockito.ArgumentMatchers.eq; 24 | import static org.mockito.Mockito.atLeastOnce; 25 | import static org.mockito.Mockito.mock; 26 | import static org.mockito.Mockito.verify; 27 | 28 | @Timeout(15) 29 | public class HttpClientStreamableHttpSyncClientTests extends AbstractMcpSyncClientTests { 30 | 31 | static String host = "http://localhost:3001"; 32 | 33 | // Uses the https://github.com/tzolov/mcp-everything-server-docker-image 34 | @SuppressWarnings("resource") 35 | static GenericContainer container = new GenericContainer<>("docker.io/tzolov/mcp-everything-server:v3") 36 | .withCommand("node dist/index.js streamableHttp") 37 | .withLogConsumer(outputFrame -> System.out.println(outputFrame.getUtf8String())) 38 | .withExposedPorts(3001) 39 | .waitingFor(Wait.forHttp("/").forStatusCode(404)); 40 | 41 | private final McpSyncHttpClientRequestCustomizer requestCustomizer = mock(McpSyncHttpClientRequestCustomizer.class); 42 | 43 | @Override 44 | protected McpClientTransport createMcpTransport() { 45 | return HttpClientStreamableHttpTransport.builder(host).httpRequestCustomizer(requestCustomizer).build(); 46 | } 47 | 48 | @BeforeAll 49 | static void startContainer() { 50 | container.start(); 51 | int port = container.getMappedPort(3001); 52 | host = "http://" + container.getHost() + ":" + port; 53 | } 54 | 55 | @AfterAll 56 | static void stopContainer() { 57 | container.stop(); 58 | } 59 | 60 | @Test 61 | void customizesRequests() { 62 | var mcpTransportContext = McpTransportContext.create(Map.of("some-key", "some-value")); 63 | withClient(createMcpTransport(), syncSpec -> syncSpec.transportContextProvider(() -> mcpTransportContext), 64 | mcpSyncClient -> { 65 | mcpSyncClient.initialize(); 66 | 67 | verify(requestCustomizer, atLeastOnce()).customize(any(), eq("POST"), eq(URI.create(host + "/mcp")), 68 | eq("{\"jsonrpc\":\"2.0\",\"method\":\"notifications/initialized\"}"), 69 | eq(mcpTransportContext)); 70 | }); 71 | } 72 | 73 | } 74 | -------------------------------------------------------------------------------- /mcp-core/src/main/java/io/modelcontextprotocol/spec/McpServerTransportProviderBase.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2024-2025 the original author or authors. 3 | */ 4 | 5 | package io.modelcontextprotocol.spec; 6 | 7 | import java.util.List; 8 | import java.util.Map; 9 | 10 | import reactor.core.publisher.Mono; 11 | 12 | /** 13 | * The core building block providing the server-side MCP transport. Implement this 14 | * interface to bridge between a particular server-side technology and the MCP server 15 | * transport layer. 16 | * 17 | *

18 | * The lifecycle of the provider dictates that it be created first, upon application 19 | * startup, and then passed into either 20 | * {@link io.modelcontextprotocol.server.McpServer#sync(McpServerTransportProvider)} or 21 | * {@link io.modelcontextprotocol.server.McpServer#async(McpServerTransportProvider)}. As 22 | * a result of the MCP server creation, the provider will be notified of a 23 | * {@link McpServerSession.Factory} which will be used to handle a 1:1 communication 24 | * between a newly connected client and the server. The provider's responsibility is to 25 | * create instances of {@link McpServerTransport} that the session will utilise during the 26 | * session lifetime. 27 | * 28 | *

29 | * Finally, the {@link McpServerTransport}s can be closed in bulk when {@link #close()} or 30 | * {@link #closeGracefully()} are called as part of the normal application shutdown event. 31 | * Individual {@link McpServerTransport}s can also be closed on a per-session basis, where 32 | * the {@link McpServerSession#close()} or {@link McpServerSession#closeGracefully()} 33 | * closes the provided transport. 34 | * 35 | * @author Dariusz Jędrzejczyk 36 | */ 37 | public interface McpServerTransportProviderBase { 38 | 39 | /** 40 | * Sends a notification to all connected clients. 41 | * @param method the name of the notification method to be called on the clients 42 | * @param params parameters to be sent with the notification 43 | * @return a Mono that completes when the notification has been broadcast 44 | * @see McpSession#sendNotification(String, Map) 45 | */ 46 | Mono notifyClients(String method, Object params); 47 | 48 | /** 49 | * Immediately closes all the transports with connected clients and releases any 50 | * associated resources. 51 | */ 52 | default void close() { 53 | this.closeGracefully().subscribe(); 54 | } 55 | 56 | /** 57 | * Gracefully closes all the transports with connected clients and releases any 58 | * associated resources asynchronously. 59 | * @return a {@link Mono} that completes when the connections have been closed. 60 | */ 61 | Mono closeGracefully(); 62 | 63 | /** 64 | * Returns the protocol version supported by this transport provider. 65 | * @return the protocol version as a string 66 | */ 67 | default List protocolVersions() { 68 | return List.of(ProtocolVersions.MCP_2024_11_05); 69 | } 70 | 71 | } 72 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Model Context Protocol Java SDK 2 | 3 | Thank you for your interest in contributing to the Model Context Protocol Java SDK! 4 | This document outlines how to contribute to this project. 5 | 6 | ## Prerequisites 7 | 8 | The following software is required to work on the codebase: 9 | 10 | - `Java 17` or above 11 | - `Docker` 12 | - `npx` 13 | 14 | ## Getting Started 15 | 16 | 1. Fork the repository 17 | 2. Clone your fork: 18 | 19 | ```bash 20 | git clone https://github.com/YOUR-USERNAME/java-sdk.git 21 | cd java-sdk 22 | ``` 23 | 24 | 3. Build from source: 25 | 26 | ```bash 27 | ./mvnw clean install -DskipTests # skip the tests 28 | ./mvnw test # run tests 29 | ``` 30 | 31 | ## Reporting Issues 32 | 33 | Please create an issue in the repository if you discover a bug or would like to 34 | propose an enhancement. Bug reports should have a reproducer in the form of a code 35 | sample or a repository attached that the maintainers or contributors can work with to 36 | address the problem. 37 | 38 | ## Making Changes 39 | 40 | 1. Create a new branch: 41 | 42 | ```bash 43 | git checkout -b feature/your-feature-name 44 | ``` 45 | 46 | 2. Make your changes 47 | 3. Validate your changes: 48 | 49 | ```bash 50 | ./mvnw clean test 51 | ``` 52 | 53 | ### Change Proposal Guidelines 54 | 55 | #### Principles of MCP 56 | 57 | 1. **Simple + Minimal**: It is much easier to add things to the codebase than it is to 58 | remove them. To maintain simplicity, we keep a high bar for adding new concepts and 59 | primitives as each addition requires maintenance and compatibility consideration. 60 | 2. **Concrete**: Code changes need to be based on specific usage and implementation 61 | challenges and not on speculative ideas. Most importantly, the SDK is meant to 62 | implement the MCP specification. 63 | 64 | ## Submitting Changes 65 | 66 | 1. For non-trivial changes, please clarify with the maintainers in an issue whether 67 | you can contribute the change and the desired scope of the change. 68 | 2. For trivial changes (for example a couple of lines or documentation changes) there 69 | is no need to open an issue first. 70 | 3. Push your changes to your fork. 71 | 4. Submit a pull request to the main repository. 72 | 5. Follow the pull request template. 73 | 6. Wait for review. 74 | 7. For any follow-up work, please add new commits instead of force-pushing. This will 75 | allow the reviewer to focus on incremental changes instead of having to restart the 76 | review process. 77 | 78 | ## Code of Conduct 79 | 80 | This project follows a Code of Conduct. Please review it in 81 | [CODE_OF_CONDUCT.md](CODE_OF_CONDUCT.md). 82 | 83 | ## Questions 84 | 85 | If you have questions, please create a discussion in the repository. 86 | 87 | ## License 88 | 89 | By contributing, you agree that your contributions will be licensed under the MIT 90 | License. 91 | 92 | ## Security 93 | 94 | Please review our [Security Policy](SECURITY.md) for reporting security issues. -------------------------------------------------------------------------------- /mcp-core/src/test/java/io/modelcontextprotocol/client/StdioMcpSyncClientTests.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2024-2024 the original author or authors. 3 | */ 4 | 5 | package io.modelcontextprotocol.client; 6 | 7 | import java.time.Duration; 8 | import java.util.concurrent.CountDownLatch; 9 | import java.util.concurrent.TimeUnit; 10 | import java.util.concurrent.atomic.AtomicReference; 11 | 12 | import io.modelcontextprotocol.client.transport.ServerParameters; 13 | import io.modelcontextprotocol.client.transport.StdioClientTransport; 14 | import io.modelcontextprotocol.spec.McpClientTransport; 15 | import org.junit.jupiter.api.Test; 16 | import org.junit.jupiter.api.Timeout; 17 | import reactor.core.publisher.Sinks; 18 | import reactor.test.StepVerifier; 19 | 20 | import static io.modelcontextprotocol.client.ServerParameterUtils.createServerParameters; 21 | import static io.modelcontextprotocol.util.McpJsonMapperUtils.JSON_MAPPER; 22 | import static org.assertj.core.api.Assertions.assertThat; 23 | 24 | /** 25 | * Tests for the {@link McpSyncClient} with {@link StdioClientTransport}. 26 | * 27 | *

28 | * These tests use npx to download and run the MCP "everything" server locally. The first 29 | * test execution will download the everything server scripts and cache them locally, 30 | * which can take more than 15 seconds. Subsequent test runs will use the cached version 31 | * and execute faster. 32 | * 33 | * @author Christian Tzolov 34 | * @author Dariusz Jędrzejczyk 35 | */ 36 | @Timeout(25) // Giving extra time beyond the client timeout to account for initial server 37 | // download 38 | class StdioMcpSyncClientTests extends AbstractMcpSyncClientTests { 39 | 40 | @Override 41 | protected McpClientTransport createMcpTransport() { 42 | ServerParameters stdioParams = createServerParameters(); 43 | return new StdioClientTransport(stdioParams, JSON_MAPPER); 44 | } 45 | 46 | @Test 47 | void customErrorHandlerShouldReceiveErrors() throws InterruptedException { 48 | CountDownLatch latch = new CountDownLatch(1); 49 | AtomicReference receivedError = new AtomicReference<>(); 50 | 51 | McpClientTransport transport = createMcpTransport(); 52 | StepVerifier.create(transport.connect(msg -> msg)).verifyComplete(); 53 | 54 | ((StdioClientTransport) transport).setStdErrorHandler(error -> { 55 | receivedError.set(error); 56 | latch.countDown(); 57 | }); 58 | 59 | String errorMessage = "Test error"; 60 | ((StdioClientTransport) transport).getErrorSink().emitNext(errorMessage, Sinks.EmitFailureHandler.FAIL_FAST); 61 | 62 | assertThat(latch.await(5, TimeUnit.SECONDS)).isTrue(); 63 | 64 | assertThat(receivedError.get()).isNotNull().isEqualTo(errorMessage); 65 | 66 | StepVerifier.create(transport.closeGracefully()).expectComplete().verify(Duration.ofSeconds(5)); 67 | } 68 | 69 | protected Duration getInitializationTimeout() { 70 | return Duration.ofSeconds(10); 71 | } 72 | 73 | @Override 74 | protected Duration getRequestTimeout() { 75 | return Duration.ofSeconds(25); 76 | } 77 | 78 | } 79 | -------------------------------------------------------------------------------- /mcp-json-jackson2/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | io.modelcontextprotocol.sdk 8 | mcp-parent 9 | 0.18.0-SNAPSHOT 10 | 11 | mcp-json-jackson2 12 | jar 13 | Java MCP SDK JSON Jackson 14 | Java MCP SDK JSON implementation based on Jackson 15 | https://github.com/modelcontextprotocol/java-sdk 16 | 17 | https://github.com/modelcontextprotocol/java-sdk 18 | git://github.com/modelcontextprotocol/java-sdk.git 19 | git@github.com/modelcontextprotocol/java-sdk.git 20 | 21 | 22 | 23 | 24 | org.apache.maven.plugins 25 | maven-jar-plugin 26 | 27 | 28 | 29 | true 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | io.modelcontextprotocol.sdk 39 | mcp-json 40 | 0.18.0-SNAPSHOT 41 | 42 | 43 | com.fasterxml.jackson.core 44 | jackson-databind 45 | ${jackson.version} 46 | 47 | 48 | com.networknt 49 | json-schema-validator 50 | ${json-schema-validator.version} 51 | 52 | 53 | 54 | org.assertj 55 | assertj-core 56 | ${assert4j.version} 57 | test 58 | 59 | 60 | org.junit.jupiter 61 | junit-jupiter-api 62 | ${junit.version} 63 | test 64 | 65 | 66 | org.junit.jupiter 67 | junit-jupiter-params 68 | ${junit.version} 69 | test 70 | 71 | 72 | org.mockito 73 | mockito-core 74 | ${mockito.version} 75 | test 76 | 77 | 78 | 79 | 80 | -------------------------------------------------------------------------------- /mcp-core/src/main/java/io/modelcontextprotocol/spec/McpTransport.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2024-2024 the original author or authors. 3 | */ 4 | 5 | package io.modelcontextprotocol.spec; 6 | 7 | import java.util.List; 8 | 9 | import io.modelcontextprotocol.spec.McpSchema.JSONRPCMessage; 10 | import io.modelcontextprotocol.json.TypeRef; 11 | import reactor.core.publisher.Mono; 12 | 13 | /** 14 | * Defines the asynchronous transport layer for the Model Context Protocol (MCP). 15 | * 16 | *

17 | * The McpTransport interface provides the foundation for implementing custom transport 18 | * mechanisms in the Model Context Protocol. It handles the bidirectional communication 19 | * between the client and server components, supporting asynchronous message exchange 20 | * using JSON-RPC format. 21 | *

22 | * 23 | *

24 | * Implementations of this interface are responsible for: 25 | *

26 | *
    27 | *
  • Managing the lifecycle of the transport connection
  • 28 | *
  • Handling incoming messages and errors from the server
  • 29 | *
  • Sending outbound messages to the server
  • 30 | *
31 | * 32 | *

33 | * The transport layer is designed to be protocol-agnostic, allowing for various 34 | * implementations such as WebSocket, HTTP, or custom protocols. 35 | *

36 | * 37 | * @author Christian Tzolov 38 | * @author Dariusz Jędrzejczyk 39 | */ 40 | public interface McpTransport { 41 | 42 | /** 43 | * Closes the transport connection and releases any associated resources. 44 | * 45 | *

46 | * This method ensures proper cleanup of resources when the transport is no longer 47 | * needed. It should handle the graceful shutdown of any active connections. 48 | *

49 | */ 50 | default void close() { 51 | this.closeGracefully().subscribe(); 52 | } 53 | 54 | /** 55 | * Closes the transport connection and releases any associated resources 56 | * asynchronously. 57 | * @return a {@link Mono} that completes when the connection has been closed. 58 | */ 59 | Mono closeGracefully(); 60 | 61 | /** 62 | * Sends a message to the peer asynchronously. 63 | * 64 | *

65 | * This method handles the transmission of messages to the server in an asynchronous 66 | * manner. Messages are sent in JSON-RPC format as specified by the MCP protocol. 67 | *

68 | * @param message the {@link JSONRPCMessage} to be sent to the server 69 | * @return a {@link Mono} that completes when the message has been sent 70 | */ 71 | Mono sendMessage(JSONRPCMessage message); 72 | 73 | /** 74 | * Unmarshals the given data into an object of the specified type. 75 | * @param the type of the object to unmarshal 76 | * @param data the data to unmarshal 77 | * @param typeRef the type reference for the object to unmarshal 78 | * @return the unmarshalled object 79 | */ 80 | T unmarshalFrom(Object data, TypeRef typeRef); 81 | 82 | default List protocolVersions() { 83 | return List.of(ProtocolVersions.MCP_2024_11_05); 84 | } 85 | 86 | } 87 | -------------------------------------------------------------------------------- /mcp-core/src/test/java/io/modelcontextprotocol/spec/json/gson/GsonMcpJsonMapper.java: -------------------------------------------------------------------------------- 1 | package io.modelcontextprotocol.spec.json.gson; 2 | 3 | import com.google.gson.Gson; 4 | import com.google.gson.GsonBuilder; 5 | import com.google.gson.ToNumberPolicy; 6 | import io.modelcontextprotocol.json.McpJsonMapper; 7 | import io.modelcontextprotocol.json.TypeRef; 8 | 9 | import java.io.IOException; 10 | import java.nio.charset.StandardCharsets; 11 | 12 | /** 13 | * Test-only Gson-based implementation of McpJsonMapper. This lives under src/test/java so 14 | * it doesn't affect production code or dependencies. 15 | */ 16 | public final class GsonMcpJsonMapper implements McpJsonMapper { 17 | 18 | private final Gson gson; 19 | 20 | public GsonMcpJsonMapper() { 21 | this(new GsonBuilder().serializeNulls() 22 | // Ensure numeric values in untyped (Object) fields preserve integral numbers 23 | // as Long 24 | .setObjectToNumberStrategy(ToNumberPolicy.LONG_OR_DOUBLE) 25 | .setNumberToNumberStrategy(ToNumberPolicy.LONG_OR_DOUBLE) 26 | .create()); 27 | } 28 | 29 | public GsonMcpJsonMapper(Gson gson) { 30 | if (gson == null) { 31 | throw new IllegalArgumentException("Gson must not be null"); 32 | } 33 | this.gson = gson; 34 | } 35 | 36 | public Gson getGson() { 37 | return gson; 38 | } 39 | 40 | @Override 41 | public T readValue(String content, Class type) throws IOException { 42 | try { 43 | return gson.fromJson(content, type); 44 | } 45 | catch (Exception e) { 46 | throw new IOException("Failed to deserialize JSON", e); 47 | } 48 | } 49 | 50 | @Override 51 | public T readValue(byte[] content, Class type) throws IOException { 52 | return readValue(new String(content, StandardCharsets.UTF_8), type); 53 | } 54 | 55 | @Override 56 | public T readValue(String content, TypeRef type) throws IOException { 57 | try { 58 | return gson.fromJson(content, type.getType()); 59 | } 60 | catch (Exception e) { 61 | throw new IOException("Failed to deserialize JSON", e); 62 | } 63 | } 64 | 65 | @Override 66 | public T readValue(byte[] content, TypeRef type) throws IOException { 67 | return readValue(new String(content, StandardCharsets.UTF_8), type); 68 | } 69 | 70 | @Override 71 | public T convertValue(Object fromValue, Class type) { 72 | String json = gson.toJson(fromValue); 73 | return gson.fromJson(json, type); 74 | } 75 | 76 | @Override 77 | public T convertValue(Object fromValue, TypeRef type) { 78 | String json = gson.toJson(fromValue); 79 | return gson.fromJson(json, type.getType()); 80 | } 81 | 82 | @Override 83 | public String writeValueAsString(Object value) throws IOException { 84 | try { 85 | return gson.toJson(value); 86 | } 87 | catch (Exception e) { 88 | throw new IOException("Failed to serialize to JSON", e); 89 | } 90 | } 91 | 92 | @Override 93 | public byte[] writeValueAsBytes(Object value) throws IOException { 94 | return writeValueAsString(value).getBytes(StandardCharsets.UTF_8); 95 | } 96 | 97 | } 98 | -------------------------------------------------------------------------------- /mcp-core/src/test/java/io/modelcontextprotocol/client/HttpSseMcpSyncClientTests.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2024-2024 the original author or authors. 3 | */ 4 | 5 | package io.modelcontextprotocol.client; 6 | 7 | import java.net.URI; 8 | import java.util.Map; 9 | 10 | import org.junit.jupiter.api.AfterAll; 11 | import org.junit.jupiter.api.BeforeAll; 12 | import org.junit.jupiter.api.Test; 13 | import org.junit.jupiter.api.Timeout; 14 | import org.testcontainers.containers.GenericContainer; 15 | import org.testcontainers.containers.wait.strategy.Wait; 16 | 17 | import io.modelcontextprotocol.client.transport.HttpClientSseClientTransport; 18 | import io.modelcontextprotocol.client.transport.customizer.McpSyncHttpClientRequestCustomizer; 19 | import io.modelcontextprotocol.common.McpTransportContext; 20 | import io.modelcontextprotocol.spec.McpClientTransport; 21 | 22 | import static org.mockito.ArgumentMatchers.any; 23 | import static org.mockito.ArgumentMatchers.eq; 24 | import static org.mockito.ArgumentMatchers.isNull; 25 | import static org.mockito.Mockito.atLeastOnce; 26 | import static org.mockito.Mockito.mock; 27 | import static org.mockito.Mockito.verify; 28 | 29 | /** 30 | * Tests for the {@link McpSyncClient} with {@link HttpClientSseClientTransport}. 31 | * 32 | * @author Christian Tzolov 33 | */ 34 | @Timeout(15) // Giving extra time beyond the client timeout 35 | class HttpSseMcpSyncClientTests extends AbstractMcpSyncClientTests { 36 | 37 | static String host = "http://localhost:3003"; 38 | 39 | // Uses the https://github.com/tzolov/mcp-everything-server-docker-image 40 | @SuppressWarnings("resource") 41 | static GenericContainer container = new GenericContainer<>("docker.io/tzolov/mcp-everything-server:v3") 42 | .withCommand("node dist/index.js sse") 43 | .withLogConsumer(outputFrame -> System.out.println(outputFrame.getUtf8String())) 44 | .withExposedPorts(3001) 45 | .waitingFor(Wait.forHttp("/").forStatusCode(404)); 46 | 47 | private final McpSyncHttpClientRequestCustomizer requestCustomizer = mock(McpSyncHttpClientRequestCustomizer.class); 48 | 49 | @Override 50 | protected McpClientTransport createMcpTransport() { 51 | return HttpClientSseClientTransport.builder(host).httpRequestCustomizer(requestCustomizer).build(); 52 | } 53 | 54 | @BeforeAll 55 | static void startContainer() { 56 | container.start(); 57 | int port = container.getMappedPort(3001); 58 | host = "http://" + container.getHost() + ":" + port; 59 | } 60 | 61 | @AfterAll 62 | static void stopContainer() { 63 | container.stop(); 64 | } 65 | 66 | @Test 67 | void customizesRequests() { 68 | var mcpTransportContext = McpTransportContext.create(Map.of("some-key", "some-value")); 69 | withClient(createMcpTransport(), syncSpec -> syncSpec.transportContextProvider(() -> mcpTransportContext), 70 | mcpSyncClient -> { 71 | mcpSyncClient.initialize(); 72 | 73 | verify(requestCustomizer, atLeastOnce()).customize(any(), eq("GET"), eq(URI.create(host + "/sse")), 74 | isNull(), eq(mcpTransportContext)); 75 | }); 76 | } 77 | 78 | } 79 | -------------------------------------------------------------------------------- /mcp-core/src/test/java/io/modelcontextprotocol/server/transport/HttpServletSseServerCustomContextPathTests.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2024 - 2024 the original author or authors. 3 | */ 4 | 5 | package io.modelcontextprotocol.server.transport; 6 | 7 | import io.modelcontextprotocol.client.McpClient; 8 | import io.modelcontextprotocol.client.transport.HttpClientSseClientTransport; 9 | import io.modelcontextprotocol.server.McpServer; 10 | import io.modelcontextprotocol.spec.McpSchema; 11 | import org.apache.catalina.LifecycleException; 12 | import org.apache.catalina.LifecycleState; 13 | import org.apache.catalina.startup.Tomcat; 14 | import org.junit.jupiter.api.AfterEach; 15 | import org.junit.jupiter.api.BeforeEach; 16 | import org.junit.jupiter.api.Test; 17 | 18 | import static org.assertj.core.api.Assertions.assertThat; 19 | 20 | class HttpServletSseServerCustomContextPathTests { 21 | 22 | private static final int PORT = TomcatTestUtil.findAvailablePort(); 23 | 24 | private static final String CUSTOM_CONTEXT_PATH = "/api/v1"; 25 | 26 | private static final String CUSTOM_SSE_ENDPOINT = "/somePath/sse"; 27 | 28 | private static final String CUSTOM_MESSAGE_ENDPOINT = "/otherPath/mcp/message"; 29 | 30 | private HttpServletSseServerTransportProvider mcpServerTransportProvider; 31 | 32 | McpClient.SyncSpec clientBuilder; 33 | 34 | private Tomcat tomcat; 35 | 36 | @BeforeEach 37 | public void before() { 38 | 39 | // Create and configure the transport provider 40 | mcpServerTransportProvider = HttpServletSseServerTransportProvider.builder() 41 | .baseUrl(CUSTOM_CONTEXT_PATH) 42 | .messageEndpoint(CUSTOM_MESSAGE_ENDPOINT) 43 | .sseEndpoint(CUSTOM_SSE_ENDPOINT) 44 | .build(); 45 | 46 | tomcat = TomcatTestUtil.createTomcatServer(CUSTOM_CONTEXT_PATH, PORT, mcpServerTransportProvider); 47 | 48 | try { 49 | tomcat.start(); 50 | assertThat(tomcat.getServer().getState()).isEqualTo(LifecycleState.STARTED); 51 | } 52 | catch (Exception e) { 53 | throw new RuntimeException("Failed to start Tomcat", e); 54 | } 55 | 56 | this.clientBuilder = McpClient.sync(HttpClientSseClientTransport.builder("http://localhost:" + PORT) 57 | .sseEndpoint(CUSTOM_CONTEXT_PATH + CUSTOM_SSE_ENDPOINT) 58 | .build()); 59 | } 60 | 61 | @AfterEach 62 | public void after() { 63 | if (mcpServerTransportProvider != null) { 64 | mcpServerTransportProvider.closeGracefully().block(); 65 | } 66 | if (tomcat != null) { 67 | try { 68 | tomcat.stop(); 69 | tomcat.destroy(); 70 | } 71 | catch (LifecycleException e) { 72 | throw new RuntimeException("Failed to stop Tomcat", e); 73 | } 74 | } 75 | } 76 | 77 | @Test 78 | void testCustomContextPath() { 79 | var server = McpServer.async(mcpServerTransportProvider).serverInfo("test-server", "1.0.0").build(); 80 | try (//@formatter:off 81 | var client = clientBuilder.clientInfo(new McpSchema.Implementation("Sample " + "client", "0.0.0")) .build()) { //@formatter:on 82 | 83 | assertThat(client.initialize()).isNotNull(); 84 | } 85 | server.close(); 86 | } 87 | 88 | } 89 | -------------------------------------------------------------------------------- /mcp-json-jackson2/src/main/java/io/modelcontextprotocol/json/jackson/JacksonMcpJsonMapper.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 - 2025 the original author or authors. 3 | */ 4 | 5 | package io.modelcontextprotocol.json.jackson; 6 | 7 | import com.fasterxml.jackson.databind.JavaType; 8 | import com.fasterxml.jackson.databind.ObjectMapper; 9 | import io.modelcontextprotocol.json.McpJsonMapper; 10 | import io.modelcontextprotocol.json.TypeRef; 11 | 12 | import java.io.IOException; 13 | 14 | /** 15 | * Jackson-based implementation of JsonMapper. Wraps a Jackson ObjectMapper but keeps the 16 | * SDK decoupled from Jackson at the API level. 17 | */ 18 | public final class JacksonMcpJsonMapper implements McpJsonMapper { 19 | 20 | private final ObjectMapper objectMapper; 21 | 22 | /** 23 | * Constructs a new JacksonMcpJsonMapper instance with the given ObjectMapper. 24 | * @param objectMapper the ObjectMapper to be used for JSON serialization and 25 | * deserialization. Must not be null. 26 | * @throws IllegalArgumentException if the provided ObjectMapper is null. 27 | */ 28 | public JacksonMcpJsonMapper(ObjectMapper objectMapper) { 29 | if (objectMapper == null) { 30 | throw new IllegalArgumentException("ObjectMapper must not be null"); 31 | } 32 | this.objectMapper = objectMapper; 33 | } 34 | 35 | /** 36 | * Returns the underlying Jackson {@link ObjectMapper} used for JSON serialization and 37 | * deserialization. 38 | * @return the ObjectMapper instance 39 | */ 40 | public ObjectMapper getObjectMapper() { 41 | return objectMapper; 42 | } 43 | 44 | @Override 45 | public T readValue(String content, Class type) throws IOException { 46 | return objectMapper.readValue(content, type); 47 | } 48 | 49 | @Override 50 | public T readValue(byte[] content, Class type) throws IOException { 51 | return objectMapper.readValue(content, type); 52 | } 53 | 54 | @Override 55 | public T readValue(String content, TypeRef type) throws IOException { 56 | JavaType javaType = objectMapper.getTypeFactory().constructType(type.getType()); 57 | return objectMapper.readValue(content, javaType); 58 | } 59 | 60 | @Override 61 | public T readValue(byte[] content, TypeRef type) throws IOException { 62 | JavaType javaType = objectMapper.getTypeFactory().constructType(type.getType()); 63 | return objectMapper.readValue(content, javaType); 64 | } 65 | 66 | @Override 67 | public T convertValue(Object fromValue, Class type) { 68 | return objectMapper.convertValue(fromValue, type); 69 | } 70 | 71 | @Override 72 | public T convertValue(Object fromValue, TypeRef type) { 73 | JavaType javaType = objectMapper.getTypeFactory().constructType(type.getType()); 74 | return objectMapper.convertValue(fromValue, javaType); 75 | } 76 | 77 | @Override 78 | public String writeValueAsString(Object value) throws IOException { 79 | return objectMapper.writeValueAsString(value); 80 | } 81 | 82 | @Override 83 | public byte[] writeValueAsBytes(Object value) throws IOException { 84 | return objectMapper.writeValueAsBytes(value); 85 | } 86 | 87 | } 88 | -------------------------------------------------------------------------------- /mcp-core/src/main/java/io/modelcontextprotocol/spec/McpSession.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2024-2024 the original author or authors. 3 | */ 4 | 5 | package io.modelcontextprotocol.spec; 6 | 7 | import io.modelcontextprotocol.json.TypeRef; 8 | import reactor.core.publisher.Mono; 9 | 10 | /** 11 | * Represents a Model Context Protocol (MCP) session that handles communication between 12 | * clients and the server. This interface provides methods for sending requests and 13 | * notifications, as well as managing the session lifecycle. 14 | * 15 | *

16 | * The session operates asynchronously using Project Reactor's {@link Mono} type for 17 | * non-blocking operations. It supports both request-response patterns and one-way 18 | * notifications. 19 | *

20 | * 21 | * @author Christian Tzolov 22 | * @author Dariusz Jędrzejczyk 23 | */ 24 | public interface McpSession { 25 | 26 | /** 27 | * Sends a request to the model counterparty and expects a response of type T. 28 | * 29 | *

30 | * This method handles the request-response pattern where a response is expected from 31 | * the client or server. The response type is determined by the provided 32 | * TypeReference. 33 | *

34 | * @param the type of the expected response 35 | * @param method the name of the method to be called on the counterparty 36 | * @param requestParams the parameters to be sent with the request 37 | * @param typeRef the TypeReference describing the expected response type 38 | * @return a Mono that will emit the response when received 39 | */ 40 | Mono sendRequest(String method, Object requestParams, TypeRef typeRef); 41 | 42 | /** 43 | * Sends a notification to the model client or server without parameters. 44 | * 45 | *

46 | * This method implements the notification pattern where no response is expected from 47 | * the counterparty. It's useful for fire-and-forget scenarios. 48 | *

49 | * @param method the name of the notification method to be called on the server 50 | * @return a Mono that completes when the notification has been sent 51 | */ 52 | default Mono sendNotification(String method) { 53 | return sendNotification(method, null); 54 | } 55 | 56 | /** 57 | * Sends a notification to the model client or server with parameters. 58 | * 59 | *

60 | * Similar to {@link #sendNotification(String)} but allows sending additional 61 | * parameters with the notification. 62 | *

63 | * @param method the name of the notification method to be sent to the counterparty 64 | * @param params parameters to be sent with the notification 65 | * @return a Mono that completes when the notification has been sent 66 | */ 67 | Mono sendNotification(String method, Object params); 68 | 69 | /** 70 | * Closes the session and releases any associated resources asynchronously. 71 | * @return a {@link Mono} that completes when the session has been closed. 72 | */ 73 | Mono closeGracefully(); 74 | 75 | /** 76 | * Closes the session and releases any associated resources. 77 | */ 78 | void close(); 79 | 80 | } 81 | -------------------------------------------------------------------------------- /mcp-core/src/main/java/io/modelcontextprotocol/spec/DefaultMcpTransportStream.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2024-2025 the original author or authors. 3 | */ 4 | 5 | package io.modelcontextprotocol.spec; 6 | 7 | import org.reactivestreams.Publisher; 8 | import org.slf4j.Logger; 9 | import org.slf4j.LoggerFactory; 10 | import reactor.core.publisher.Flux; 11 | import reactor.core.publisher.Mono; 12 | import reactor.util.function.Tuple2; 13 | 14 | import java.util.Optional; 15 | import java.util.concurrent.atomic.AtomicLong; 16 | import java.util.concurrent.atomic.AtomicReference; 17 | import java.util.function.Function; 18 | 19 | /** 20 | * An implementation of {@link McpTransportStream} using Project Reactor types. 21 | * 22 | * @param the resource serving the stream 23 | * @author Dariusz Jędrzejczyk 24 | */ 25 | public class DefaultMcpTransportStream implements McpTransportStream { 26 | 27 | private static final Logger logger = LoggerFactory.getLogger(DefaultMcpTransportStream.class); 28 | 29 | private static final AtomicLong counter = new AtomicLong(); 30 | 31 | private final AtomicReference lastId = new AtomicReference<>(); 32 | 33 | // Used only for internal accounting 34 | private final long streamId; 35 | 36 | private final boolean resumable; 37 | 38 | private final Function, Publisher> reconnect; 39 | 40 | /** 41 | * Constructs a new instance representing a particular stream that can resume using 42 | * the provided reconnect mechanism. 43 | * @param resumable whether the stream is resumable and should try to reconnect 44 | * @param reconnect the mechanism to use in case an error is observed on the current 45 | * event stream to asynchronously kick off a resumed stream consumption, potentially 46 | * using the stored {@link #lastId()}. 47 | */ 48 | public DefaultMcpTransportStream(boolean resumable, 49 | Function, Publisher> reconnect) { 50 | this.reconnect = reconnect; 51 | this.streamId = counter.getAndIncrement(); 52 | this.resumable = resumable; 53 | } 54 | 55 | @Override 56 | public Optional lastId() { 57 | return Optional.ofNullable(this.lastId.get()); 58 | } 59 | 60 | @Override 61 | public long streamId() { 62 | return this.streamId; 63 | } 64 | 65 | @Override 66 | public Publisher consumeSseStream( 67 | Publisher, Iterable>> eventStream) { 68 | 69 | // @formatter:off 70 | return Flux.deferContextual(ctx -> Flux.from(eventStream) 71 | .doOnNext(idAndMessage -> idAndMessage.getT1().ifPresent(id -> { 72 | String previousId = this.lastId.getAndSet(id); 73 | logger.debug("Updating last id {} -> {} for stream {}", previousId, id, this.streamId); 74 | })) 75 | .doOnError(e -> { 76 | if (resumable && !(e instanceof McpTransportSessionNotFoundException)) { 77 | Mono.from(reconnect.apply(this)).contextWrite(ctx).subscribe(); 78 | } 79 | }) 80 | .flatMapIterable(Tuple2::getT2)); // @formatter:on 81 | } 82 | 83 | } 84 | -------------------------------------------------------------------------------- /mcp-core/src/main/java/io/modelcontextprotocol/spec/McpStreamableServerTransportProvider.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2024-2025 the original author or authors. 3 | */ 4 | 5 | package io.modelcontextprotocol.spec; 6 | 7 | import reactor.core.publisher.Mono; 8 | 9 | /** 10 | * The core building block providing the server-side MCP transport for Streamable HTTP 11 | * servers. Implement this interface to bridge between a particular server-side technology 12 | * and the MCP server transport layer. 13 | * 14 | *

15 | * The lifecycle of the provider dictates that it be created first, upon application 16 | * startup, and then passed into either 17 | * {@link io.modelcontextprotocol.server.McpServer#sync(McpStreamableServerTransportProvider)} 18 | * or 19 | * {@link io.modelcontextprotocol.server.McpServer#async(McpStreamableServerTransportProvider)}. 20 | * As a result of the MCP server creation, the provider will be notified of a 21 | * {@link McpStreamableServerSession.Factory} which will be used to handle a 1:1 22 | * communication between a newly connected client and the server using a session concept. 23 | * The provider's responsibility is to create instances of 24 | * {@link McpStreamableServerTransport} that the session will utilise during the session 25 | * lifetime. 26 | * 27 | *

28 | * Finally, the {@link McpStreamableServerTransport}s can be closed in bulk when 29 | * {@link #close()} or {@link #closeGracefully()} are called as part of the normal 30 | * application shutdown event. Individual {@link McpStreamableServerTransport}s can also 31 | * be closed on a per-session basis, where the {@link McpServerSession#close()} or 32 | * {@link McpServerSession#closeGracefully()} closes the provided transport. 33 | * 34 | * @author Dariusz Jędrzejczyk 35 | */ 36 | public interface McpStreamableServerTransportProvider extends McpServerTransportProviderBase { 37 | 38 | /** 39 | * Sets the session factory that will be used to create sessions for new clients. An 40 | * implementation of the MCP server MUST call this method before any MCP interactions 41 | * take place. 42 | * @param sessionFactory the session factory to be used for initiating client sessions 43 | */ 44 | void setSessionFactory(McpStreamableServerSession.Factory sessionFactory); 45 | 46 | /** 47 | * Sends a notification to all connected clients. 48 | * @param method the name of the notification method to be called on the clients 49 | * @param params parameters to be sent with the notification 50 | * @return a Mono that completes when the notification has been broadcast 51 | */ 52 | Mono notifyClients(String method, Object params); 53 | 54 | /** 55 | * Immediately closes all the transports with connected clients and releases any 56 | * associated resources. 57 | */ 58 | default void close() { 59 | this.closeGracefully().subscribe(); 60 | } 61 | 62 | /** 63 | * Gracefully closes all the transports with connected clients and releases any 64 | * associated resources asynchronously. 65 | * @return a {@link Mono} that completes when the connections have been closed. 66 | */ 67 | Mono closeGracefully(); 68 | 69 | } 70 | -------------------------------------------------------------------------------- /mcp-spring/mcp-spring-webflux/src/test/java/io/modelcontextprotocol/client/transport/WebClientStreamableHttpTransportTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2024-2025 the original author or authors. 3 | */ 4 | package io.modelcontextprotocol.client.transport; 5 | 6 | import io.modelcontextprotocol.spec.McpSchema; 7 | import org.junit.jupiter.api.AfterAll; 8 | import org.junit.jupiter.api.BeforeAll; 9 | import org.junit.jupiter.api.Test; 10 | import org.testcontainers.containers.GenericContainer; 11 | import org.testcontainers.containers.wait.strategy.Wait; 12 | import reactor.test.StepVerifier; 13 | 14 | import org.springframework.web.reactive.function.client.WebClient; 15 | 16 | class WebClientStreamableHttpTransportTest { 17 | 18 | static String host = "http://localhost:3001"; 19 | 20 | static WebClient.Builder builder; 21 | 22 | @SuppressWarnings("resource") 23 | static GenericContainer container = new GenericContainer<>("docker.io/tzolov/mcp-everything-server:v3") 24 | .withCommand("node dist/index.js streamableHttp") 25 | .withLogConsumer(outputFrame -> System.out.println(outputFrame.getUtf8String())) 26 | .withExposedPorts(3001) 27 | .waitingFor(Wait.forHttp("/").forStatusCode(404)); 28 | 29 | @BeforeAll 30 | static void startContainer() { 31 | container.start(); 32 | int port = container.getMappedPort(3001); 33 | host = "http://" + container.getHost() + ":" + port; 34 | builder = WebClient.builder().baseUrl(host); 35 | } 36 | 37 | @AfterAll 38 | static void stopContainer() { 39 | container.stop(); 40 | } 41 | 42 | @Test 43 | void testCloseUninitialized() { 44 | var transport = WebClientStreamableHttpTransport.builder(builder).build(); 45 | 46 | StepVerifier.create(transport.closeGracefully()).verifyComplete(); 47 | 48 | var initializeRequest = new McpSchema.InitializeRequest(McpSchema.LATEST_PROTOCOL_VERSION, 49 | McpSchema.ClientCapabilities.builder().roots(true).build(), 50 | new McpSchema.Implementation("MCP Client", "0.3.1")); 51 | var testMessage = new McpSchema.JSONRPCRequest(McpSchema.JSONRPC_VERSION, McpSchema.METHOD_INITIALIZE, 52 | "test-id", initializeRequest); 53 | 54 | StepVerifier.create(transport.sendMessage(testMessage)) 55 | .expectErrorMessage("MCP session has been closed") 56 | .verify(); 57 | } 58 | 59 | @Test 60 | void testCloseInitialized() { 61 | var transport = WebClientStreamableHttpTransport.builder(builder).build(); 62 | 63 | var initializeRequest = new McpSchema.InitializeRequest(McpSchema.LATEST_PROTOCOL_VERSION, 64 | McpSchema.ClientCapabilities.builder().roots(true).build(), 65 | new McpSchema.Implementation("MCP Client", "0.3.1")); 66 | var testMessage = new McpSchema.JSONRPCRequest(McpSchema.JSONRPC_VERSION, McpSchema.METHOD_INITIALIZE, 67 | "test-id", initializeRequest); 68 | 69 | StepVerifier.create(transport.sendMessage(testMessage)).verifyComplete(); 70 | StepVerifier.create(transport.closeGracefully()).verifyComplete(); 71 | 72 | StepVerifier.create(transport.sendMessage(testMessage)) 73 | .expectErrorMatches(err -> err.getMessage().matches("MCP session with ID [a-zA-Z0-9-]* has been closed")) 74 | .verify(); 75 | } 76 | 77 | } 78 | -------------------------------------------------------------------------------- /mcp-json/src/main/java/io/modelcontextprotocol/json/schema/JsonSchemaInternal.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 - 2025 the original author or authors. 3 | */ 4 | 5 | package io.modelcontextprotocol.json.schema; 6 | 7 | import java.util.ServiceLoader; 8 | import java.util.concurrent.atomic.AtomicReference; 9 | import java.util.stream.Stream; 10 | 11 | /** 12 | * Internal utility class for creating a default {@link JsonSchemaValidator} instance. 13 | * This class uses the {@link ServiceLoader} to discover and instantiate a 14 | * {@link JsonSchemaValidatorSupplier} implementation. 15 | */ 16 | final class JsonSchemaInternal { 17 | 18 | private static JsonSchemaValidator defaultValidator = null; 19 | 20 | /** 21 | * Returns the default {@link JsonSchemaValidator} instance. If the default validator 22 | * has not been initialized, it will be created using the {@link ServiceLoader} to 23 | * discover and instantiate a {@link JsonSchemaValidatorSupplier} implementation. 24 | * @return The default {@link JsonSchemaValidator} instance. 25 | * @throws IllegalStateException If no {@link JsonSchemaValidatorSupplier} 26 | * implementation exists on the classpath or if an error occurs during instantiation. 27 | */ 28 | static JsonSchemaValidator getDefaultValidator() { 29 | if (defaultValidator == null) { 30 | defaultValidator = JsonSchemaInternal.createDefaultValidator(); 31 | } 32 | return defaultValidator; 33 | } 34 | 35 | /** 36 | * Creates a default {@link JsonSchemaValidator} instance by loading a 37 | * {@link JsonSchemaValidatorSupplier} implementation using the {@link ServiceLoader}. 38 | * @return A default {@link JsonSchemaValidator} instance. 39 | * @throws IllegalStateException If no {@link JsonSchemaValidatorSupplier} 40 | * implementation is found or if an error occurs during instantiation. 41 | */ 42 | static JsonSchemaValidator createDefaultValidator() { 43 | AtomicReference ex = new AtomicReference<>(); 44 | return ServiceLoader.load(JsonSchemaValidatorSupplier.class).stream().flatMap(p -> { 45 | try { 46 | JsonSchemaValidatorSupplier supplier = p.get(); 47 | return Stream.ofNullable(supplier); 48 | } 49 | catch (Exception e) { 50 | addException(ex, e); 51 | return Stream.empty(); 52 | } 53 | }).flatMap(jsonMapperSupplier -> { 54 | try { 55 | return Stream.of(jsonMapperSupplier.get()); 56 | } 57 | catch (Exception e) { 58 | addException(ex, e); 59 | return Stream.empty(); 60 | } 61 | }).findFirst().orElseThrow(() -> { 62 | if (ex.get() != null) { 63 | return ex.get(); 64 | } 65 | else { 66 | return new IllegalStateException("No default JsonSchemaValidatorSupplier implementation found"); 67 | } 68 | }); 69 | } 70 | 71 | private static void addException(AtomicReference ref, Exception toAdd) { 72 | ref.updateAndGet(existing -> { 73 | if (existing == null) { 74 | return new IllegalStateException("Failed to initialize default JsonSchemaValidatorSupplier", toAdd); 75 | } 76 | else { 77 | existing.addSuppressed(toAdd); 78 | return existing; 79 | } 80 | }); 81 | } 82 | 83 | } 84 | -------------------------------------------------------------------------------- /mcp-core/src/test/java/io/modelcontextprotocol/spec/JSONRPCRequestMcpValidationTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2024-2024 the original author or authors. 3 | */ 4 | 5 | package io.modelcontextprotocol.spec; 6 | 7 | import org.junit.jupiter.api.Test; 8 | import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; 9 | import static org.junit.jupiter.api.Assertions.assertEquals; 10 | import static org.junit.jupiter.api.Assertions.assertThrows; 11 | import static org.junit.jupiter.api.Assertions.assertTrue; 12 | 13 | /** 14 | * Tests for MCP-specific validation of JSONRPCRequest ID requirements. 15 | * 16 | * @author Christian Tzolov 17 | */ 18 | public class JSONRPCRequestMcpValidationTest { 19 | 20 | @Test 21 | public void testValidStringId() { 22 | assertDoesNotThrow(() -> { 23 | var request = new McpSchema.JSONRPCRequest("2.0", "test/method", "string-id", null); 24 | assertEquals("string-id", request.id()); 25 | }); 26 | } 27 | 28 | @Test 29 | public void testValidIntegerId() { 30 | assertDoesNotThrow(() -> { 31 | var request = new McpSchema.JSONRPCRequest("2.0", "test/method", 123, null); 32 | assertEquals(123, request.id()); 33 | }); 34 | } 35 | 36 | @Test 37 | public void testValidLongId() { 38 | assertDoesNotThrow(() -> { 39 | var request = new McpSchema.JSONRPCRequest("2.0", "test/method", 123L, null); 40 | assertEquals(123L, request.id()); 41 | }); 42 | } 43 | 44 | @Test 45 | public void testNullIdThrowsException() { 46 | IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> { 47 | new McpSchema.JSONRPCRequest("2.0", "test/method", null, null); 48 | }); 49 | 50 | assertTrue(exception.getMessage().contains("MCP requests MUST include an ID")); 51 | assertTrue(exception.getMessage().contains("null IDs are not allowed")); 52 | } 53 | 54 | @Test 55 | public void testDoubleIdTypeThrowsException() { 56 | IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> { 57 | new McpSchema.JSONRPCRequest("2.0", "test/method", 123.45, null); 58 | }); 59 | 60 | assertTrue(exception.getMessage().contains("MCP requests MUST have an ID that is either a string or integer")); 61 | } 62 | 63 | @Test 64 | public void testBooleanIdThrowsException() { 65 | IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> { 66 | new McpSchema.JSONRPCRequest("2.0", "test/method", true, null); 67 | }); 68 | 69 | assertTrue(exception.getMessage().contains("MCP requests MUST have an ID that is either a string or integer")); 70 | } 71 | 72 | @Test 73 | public void testArrayIdThrowsException() { 74 | IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> { 75 | new McpSchema.JSONRPCRequest("2.0", "test/method", new String[] { "array" }, null); 76 | }); 77 | 78 | assertTrue(exception.getMessage().contains("MCP requests MUST have an ID that is either a string or integer")); 79 | } 80 | 81 | @Test 82 | public void testObjectIdThrowsException() { 83 | IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> { 84 | new McpSchema.JSONRPCRequest("2.0", "test/method", new Object(), null); 85 | }); 86 | 87 | assertTrue(exception.getMessage().contains("MCP requests MUST have an ID that is either a string or integer")); 88 | } 89 | 90 | } 91 | -------------------------------------------------------------------------------- /mcp-core/src/main/java/io/modelcontextprotocol/spec/McpError.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2024 - 2024 the original author or authors. 3 | */ 4 | 5 | package io.modelcontextprotocol.spec; 6 | 7 | import io.modelcontextprotocol.spec.McpSchema.JSONRPCResponse.JSONRPCError; 8 | import io.modelcontextprotocol.util.Assert; 9 | 10 | import java.util.Map; 11 | import java.util.function.Function; 12 | 13 | public class McpError extends RuntimeException { 14 | 15 | /** 16 | * Resource 18 | * Error Handling 19 | */ 20 | public static final Function RESOURCE_NOT_FOUND = resourceUri -> new McpError(new JSONRPCError( 21 | McpSchema.ErrorCodes.RESOURCE_NOT_FOUND, "Resource not found", Map.of("uri", resourceUri))); 22 | 23 | private JSONRPCError jsonRpcError; 24 | 25 | public McpError(JSONRPCError jsonRpcError) { 26 | super(jsonRpcError.message()); 27 | this.jsonRpcError = jsonRpcError; 28 | } 29 | 30 | @Deprecated 31 | public McpError(Object error) { 32 | super(error.toString()); 33 | } 34 | 35 | public JSONRPCError getJsonRpcError() { 36 | return jsonRpcError; 37 | } 38 | 39 | @Override 40 | public String toString() { 41 | var builder = new StringBuilder(super.toString()); 42 | if (jsonRpcError != null) { 43 | builder.append("\n"); 44 | builder.append(jsonRpcError.toString()); 45 | } 46 | return builder.toString(); 47 | } 48 | 49 | public static Builder builder(int errorCode) { 50 | return new Builder(errorCode); 51 | } 52 | 53 | public static class Builder { 54 | 55 | private final int code; 56 | 57 | private String message; 58 | 59 | private Object data; 60 | 61 | private Builder(int code) { 62 | this.code = code; 63 | } 64 | 65 | public Builder message(String message) { 66 | this.message = message; 67 | return this; 68 | } 69 | 70 | public Builder data(Object data) { 71 | this.data = data; 72 | return this; 73 | } 74 | 75 | public McpError build() { 76 | Assert.hasText(message, "message must not be empty"); 77 | return new McpError(new JSONRPCError(code, message, data)); 78 | } 79 | 80 | } 81 | 82 | public static Throwable findRootCause(Throwable throwable) { 83 | Assert.notNull(throwable, "throwable must not be null"); 84 | Throwable rootCause = throwable; 85 | while (rootCause.getCause() != null && rootCause.getCause() != rootCause) { 86 | rootCause = rootCause.getCause(); 87 | } 88 | return rootCause; 89 | } 90 | 91 | public static String aggregateExceptionMessages(Throwable throwable) { 92 | Assert.notNull(throwable, "throwable must not be null"); 93 | 94 | StringBuilder messages = new StringBuilder(); 95 | Throwable current = throwable; 96 | 97 | while (current != null) { 98 | if (messages.length() > 0) { 99 | messages.append("\n Caused by: "); 100 | } 101 | 102 | messages.append(current.getClass().getSimpleName()); 103 | if (current.getMessage() != null) { 104 | messages.append(": ").append(current.getMessage()); 105 | } 106 | 107 | if (current.getCause() == current) { 108 | break; 109 | } 110 | current = current.getCause(); 111 | } 112 | 113 | return messages.toString(); 114 | } 115 | 116 | } -------------------------------------------------------------------------------- /mcp-core/src/main/java/io/modelcontextprotocol/util/Assert.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2024-2024 the original author or authors. 3 | */ 4 | 5 | package io.modelcontextprotocol.util; 6 | 7 | import java.util.Collection; 8 | 9 | import reactor.util.annotation.Nullable; 10 | 11 | /** 12 | * Assertion utility class that assists in validating arguments. 13 | * 14 | * @author Christian Tzolov 15 | */ 16 | 17 | /** 18 | * Utility class providing assertion methods for parameter validation. 19 | */ 20 | public final class Assert { 21 | 22 | /** 23 | * Assert that the collection is not {@code null} and not empty. 24 | * @param collection the collection to check 25 | * @param message the exception message to use if the assertion fails 26 | * @throws IllegalArgumentException if the collection is {@code null} or empty 27 | */ 28 | public static void notEmpty(@Nullable Collection collection, String message) { 29 | if (collection == null || collection.isEmpty()) { 30 | throw new IllegalArgumentException(message); 31 | } 32 | } 33 | 34 | /** 35 | * Assert that an object is not {@code null}. 36 | * 37 | *

38 | 	 * Assert.notNull(clazz, "The class must not be null");
39 | 	 * 
40 | * @param object the object to check 41 | * @param message the exception message to use if the assertion fails 42 | * @throws IllegalArgumentException if the object is {@code null} 43 | */ 44 | public static void notNull(@Nullable Object object, String message) { 45 | if (object == null) { 46 | throw new IllegalArgumentException(message); 47 | } 48 | } 49 | 50 | /** 51 | * Assert that the given String contains valid text content; that is, it must not be 52 | * {@code null} and must contain at least one non-whitespace character. 53 | *
Assert.hasText(name, "'name' must not be empty");
54 | * @param text the String to check 55 | * @param message the exception message to use if the assertion fails 56 | * @throws IllegalArgumentException if the text does not contain valid text content 57 | */ 58 | public static void hasText(@Nullable String text, String message) { 59 | if (!hasText(text)) { 60 | throw new IllegalArgumentException(message); 61 | } 62 | } 63 | 64 | /** 65 | * Check whether the given {@code String} contains actual text. 66 | *

67 | * More specifically, this method returns {@code true} if the {@code String} is not 68 | * {@code null}, its length is greater than 0, and it contains at least one 69 | * non-whitespace character. 70 | * @param str the {@code String} to check (may be {@code null}) 71 | * @return {@code true} if the {@code String} is not {@code null}, its length is 72 | * greater than 0, and it does not contain whitespace only 73 | * @see Character#isWhitespace 74 | */ 75 | public static boolean hasText(@Nullable String str) { 76 | return (str != null && !str.isBlank()); 77 | } 78 | 79 | /** 80 | * Assert a boolean expression, throwing an {@code IllegalArgumentException} if the 81 | * expression evaluates to {@code false}. 82 | * @param expression a boolean expression 83 | * @param message the exception message to use if the assertion fails 84 | * @throws IllegalArgumentException if {@code expression} is {@code false} 85 | */ 86 | public static void isTrue(boolean expression, String message) { 87 | if (!expression) { 88 | throw new IllegalArgumentException(message); 89 | } 90 | } 91 | 92 | } 93 | -------------------------------------------------------------------------------- /mcp-test/src/main/java/io/modelcontextprotocol/MockMcpTransport.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2024-2024 the original author or authors. 3 | */ 4 | 5 | package io.modelcontextprotocol; 6 | 7 | import java.util.ArrayList; 8 | import java.util.List; 9 | import java.util.function.BiConsumer; 10 | import java.util.function.Function; 11 | 12 | import io.modelcontextprotocol.json.McpJsonMapper; 13 | import io.modelcontextprotocol.json.TypeRef; 14 | import io.modelcontextprotocol.spec.McpClientTransport; 15 | import io.modelcontextprotocol.spec.McpSchema; 16 | import io.modelcontextprotocol.spec.McpSchema.JSONRPCNotification; 17 | import io.modelcontextprotocol.spec.McpSchema.JSONRPCRequest; 18 | import io.modelcontextprotocol.spec.McpServerTransport; 19 | import reactor.core.publisher.Mono; 20 | import reactor.core.publisher.Sinks; 21 | 22 | /** 23 | * A mock implementation of the {@link McpClientTransport} and {@link McpServerTransport} 24 | * interfaces. 25 | * 26 | * @deprecated not used. to be removed in the future. 27 | */ 28 | @Deprecated 29 | public class MockMcpTransport implements McpClientTransport, McpServerTransport { 30 | 31 | private final Sinks.Many inbound = Sinks.many().unicast().onBackpressureBuffer(); 32 | 33 | private final List sent = new ArrayList<>(); 34 | 35 | private final BiConsumer interceptor; 36 | 37 | public MockMcpTransport() { 38 | this((t, msg) -> { 39 | }); 40 | } 41 | 42 | public MockMcpTransport(BiConsumer interceptor) { 43 | this.interceptor = interceptor; 44 | } 45 | 46 | public void simulateIncomingMessage(McpSchema.JSONRPCMessage message) { 47 | if (inbound.tryEmitNext(message).isFailure()) { 48 | throw new RuntimeException("Failed to process incoming message " + message); 49 | } 50 | } 51 | 52 | @Override 53 | public Mono sendMessage(McpSchema.JSONRPCMessage message) { 54 | sent.add(message); 55 | interceptor.accept(this, message); 56 | return Mono.empty(); 57 | } 58 | 59 | public McpSchema.JSONRPCRequest getLastSentMessageAsRequest() { 60 | return (JSONRPCRequest) getLastSentMessage(); 61 | } 62 | 63 | public McpSchema.JSONRPCNotification getLastSentMessageAsNotification() { 64 | return (JSONRPCNotification) getLastSentMessage(); 65 | } 66 | 67 | public McpSchema.JSONRPCMessage getLastSentMessage() { 68 | return !sent.isEmpty() ? sent.get(sent.size() - 1) : null; 69 | } 70 | 71 | private volatile boolean connected = false; 72 | 73 | @Override 74 | public Mono connect(Function, Mono> handler) { 75 | if (connected) { 76 | return Mono.error(new IllegalStateException("Already connected")); 77 | } 78 | connected = true; 79 | return inbound.asFlux() 80 | .flatMap(message -> Mono.just(message).transform(handler)) 81 | .doFinally(signal -> connected = false) 82 | .then(); 83 | } 84 | 85 | @Override 86 | public Mono closeGracefully() { 87 | return Mono.defer(() -> { 88 | connected = false; 89 | inbound.tryEmitComplete(); 90 | // Wait for all subscribers to complete 91 | return Mono.empty(); 92 | }); 93 | } 94 | 95 | @Override 96 | public T unmarshalFrom(Object data, TypeRef typeRef) { 97 | return McpJsonMapper.getDefault().convertValue(data, typeRef); 98 | } 99 | 100 | } 101 | -------------------------------------------------------------------------------- /mcp-core/src/test/java/io/modelcontextprotocol/client/transport/HttpClientStreamableHttpTransportEmptyJsonResponseTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2024-2025 the original author or authors. 3 | */ 4 | 5 | package io.modelcontextprotocol.client.transport; 6 | 7 | import static org.mockito.ArgumentMatchers.any; 8 | import static org.mockito.ArgumentMatchers.eq; 9 | import static org.mockito.Mockito.atLeastOnce; 10 | import static org.mockito.Mockito.mock; 11 | import static org.mockito.Mockito.verify; 12 | 13 | import java.io.IOException; 14 | import java.net.InetSocketAddress; 15 | import java.net.URI; 16 | import java.net.URISyntaxException; 17 | 18 | import org.junit.jupiter.api.AfterAll; 19 | import org.junit.jupiter.api.BeforeAll; 20 | import org.junit.jupiter.api.Test; 21 | import org.junit.jupiter.api.Timeout; 22 | 23 | import com.sun.net.httpserver.HttpServer; 24 | 25 | import io.modelcontextprotocol.client.transport.customizer.McpSyncHttpClientRequestCustomizer; 26 | import io.modelcontextprotocol.server.transport.TomcatTestUtil; 27 | import io.modelcontextprotocol.spec.McpSchema; 28 | import io.modelcontextprotocol.spec.ProtocolVersions; 29 | import reactor.test.StepVerifier; 30 | 31 | /** 32 | * Handles emplty application/json response with 200 OK status code. 33 | * 34 | * @author codezkk 35 | */ 36 | public class HttpClientStreamableHttpTransportEmptyJsonResponseTest { 37 | 38 | static int PORT = TomcatTestUtil.findAvailablePort(); 39 | 40 | static String host = "http://localhost:" + PORT; 41 | 42 | static HttpServer server; 43 | 44 | @BeforeAll 45 | static void startContainer() throws IOException { 46 | 47 | server = HttpServer.create(new InetSocketAddress(PORT), 0); 48 | 49 | // Empty, 200 OK response for the /mcp endpoint 50 | server.createContext("/mcp", exchange -> { 51 | exchange.getResponseHeaders().set("Content-Type", "application/json"); 52 | exchange.sendResponseHeaders(200, 0); 53 | exchange.close(); 54 | }); 55 | 56 | server.setExecutor(null); 57 | server.start(); 58 | } 59 | 60 | @AfterAll 61 | static void stopContainer() { 62 | server.stop(1); 63 | } 64 | 65 | /** 66 | * Regardless of the response (even if the response is null and the content-type is 67 | * present), notify should handle it correctly. 68 | */ 69 | @Test 70 | @Timeout(3) 71 | void testNotificationInitialized() throws URISyntaxException { 72 | 73 | var uri = new URI(host + "/mcp"); 74 | var mockRequestCustomizer = mock(McpSyncHttpClientRequestCustomizer.class); 75 | var transport = HttpClientStreamableHttpTransport.builder(host) 76 | .httpRequestCustomizer(mockRequestCustomizer) 77 | .build(); 78 | 79 | var initializeRequest = new McpSchema.InitializeRequest(ProtocolVersions.MCP_2025_03_26, 80 | McpSchema.ClientCapabilities.builder().roots(true).build(), 81 | new McpSchema.Implementation("MCP Client", "0.3.1")); 82 | var testMessage = new McpSchema.JSONRPCRequest(McpSchema.JSONRPC_VERSION, McpSchema.METHOD_INITIALIZE, 83 | "test-id", initializeRequest); 84 | 85 | StepVerifier.create(transport.sendMessage(testMessage)).verifyComplete(); 86 | 87 | // Verify the customizer was called 88 | verify(mockRequestCustomizer, atLeastOnce()).customize(any(), eq("POST"), eq(uri), eq( 89 | "{\"jsonrpc\":\"2.0\",\"method\":\"initialize\",\"id\":\"test-id\",\"params\":{\"protocolVersion\":\"2025-03-26\",\"capabilities\":{\"roots\":{\"listChanged\":true}},\"clientInfo\":{\"name\":\"MCP Client\",\"version\":\"0.3.1\"}}}"), 90 | any()); 91 | 92 | } 93 | 94 | } 95 | -------------------------------------------------------------------------------- /mcp-test/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | io.modelcontextprotocol.sdk 8 | mcp-parent 9 | 0.18.0-SNAPSHOT 10 | 11 | mcp-test 12 | jar 13 | Tests for the Java MCP SDK 14 | Provides some shared test fasilities for the MCP Java SDK 15 | https://github.com/modelcontextprotocol/java-sdk 16 | 17 | 18 | https://github.com/modelcontextprotocol/java-sdk 19 | git://github.com/modelcontextprotocol/java-sdk.git 20 | git@github.com/modelcontextprotocol/java-sdk.git 21 | 22 | 23 | 24 | 25 | io.modelcontextprotocol.sdk 26 | mcp 27 | 0.18.0-SNAPSHOT 28 | 29 | 30 | 31 | org.slf4j 32 | slf4j-api 33 | ${slf4j-api.version} 34 | 35 | 36 | 37 | com.fasterxml.jackson.core 38 | jackson-databind 39 | ${jackson.version} 40 | 41 | 42 | 43 | io.projectreactor 44 | reactor-core 45 | 46 | 47 | 48 | org.assertj 49 | assertj-core 50 | ${assert4j.version} 51 | 52 | 53 | org.junit.jupiter 54 | junit-jupiter-api 55 | ${junit.version} 56 | 57 | 58 | org.junit.jupiter 59 | junit-jupiter-params 60 | ${junit.version} 61 | 62 | 63 | org.mockito 64 | mockito-core 65 | ${mockito.version} 66 | 67 | 68 | io.projectreactor 69 | reactor-test 70 | 71 | 72 | org.testcontainers 73 | junit-jupiter 74 | ${testcontainers.version} 75 | 76 | 77 | org.testcontainers 78 | toxiproxy 79 | ${toxiproxy.version} 80 | 81 | 82 | 83 | org.awaitility 84 | awaitility 85 | ${awaitility.version} 86 | 87 | 88 | 89 | ch.qos.logback 90 | logback-classic 91 | ${logback.version} 92 | 93 | 94 | 95 | net.javacrumbs.json-unit 96 | json-unit-assertj 97 | ${json-unit-assertj.version} 98 | 99 | 100 | 101 | 102 | 103 | 104 | -------------------------------------------------------------------------------- /mcp-core/src/test/java/io/modelcontextprotocol/MockMcpClientTransport.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2024-2024 the original author or authors. 3 | */ 4 | 5 | package io.modelcontextprotocol; 6 | 7 | import java.util.ArrayList; 8 | import java.util.List; 9 | import java.util.function.BiConsumer; 10 | import java.util.function.Function; 11 | 12 | import io.modelcontextprotocol.json.McpJsonMapper; 13 | import io.modelcontextprotocol.json.TypeRef; 14 | import io.modelcontextprotocol.spec.McpClientTransport; 15 | import io.modelcontextprotocol.spec.McpSchema; 16 | import io.modelcontextprotocol.spec.McpSchema.JSONRPCNotification; 17 | import io.modelcontextprotocol.spec.McpSchema.JSONRPCRequest; 18 | import reactor.core.publisher.Mono; 19 | import reactor.core.publisher.Sinks; 20 | 21 | /** 22 | * A mock implementation of the {@link McpClientTransport} interfaces. 23 | */ 24 | public class MockMcpClientTransport implements McpClientTransport { 25 | 26 | private final Sinks.Many inbound = Sinks.many().unicast().onBackpressureBuffer(); 27 | 28 | private final List sent = new ArrayList<>(); 29 | 30 | private final BiConsumer interceptor; 31 | 32 | private String protocolVersion = McpSchema.LATEST_PROTOCOL_VERSION; 33 | 34 | public MockMcpClientTransport() { 35 | this((t, msg) -> { 36 | }); 37 | } 38 | 39 | public MockMcpClientTransport(BiConsumer interceptor) { 40 | this.interceptor = interceptor; 41 | } 42 | 43 | public MockMcpClientTransport withProtocolVersion(String protocolVersion) { 44 | return this; 45 | } 46 | 47 | @Override 48 | public List protocolVersions() { 49 | return List.of(protocolVersion); 50 | } 51 | 52 | public void simulateIncomingMessage(McpSchema.JSONRPCMessage message) { 53 | if (inbound.tryEmitNext(message).isFailure()) { 54 | throw new RuntimeException("Failed to process incoming message " + message); 55 | } 56 | } 57 | 58 | @Override 59 | public Mono sendMessage(McpSchema.JSONRPCMessage message) { 60 | sent.add(message); 61 | interceptor.accept(this, message); 62 | return Mono.empty(); 63 | } 64 | 65 | public McpSchema.JSONRPCRequest getLastSentMessageAsRequest() { 66 | return (JSONRPCRequest) getLastSentMessage(); 67 | } 68 | 69 | public McpSchema.JSONRPCNotification getLastSentMessageAsNotification() { 70 | return (JSONRPCNotification) getLastSentMessage(); 71 | } 72 | 73 | public McpSchema.JSONRPCMessage getLastSentMessage() { 74 | return !sent.isEmpty() ? sent.get(sent.size() - 1) : null; 75 | } 76 | 77 | private volatile boolean connected = false; 78 | 79 | @Override 80 | public Mono connect(Function, Mono> handler) { 81 | if (connected) { 82 | return Mono.error(new IllegalStateException("Already connected")); 83 | } 84 | connected = true; 85 | return inbound.asFlux() 86 | .flatMap(message -> Mono.just(message).transform(handler)) 87 | .doFinally(signal -> connected = false) 88 | .then(); 89 | } 90 | 91 | @Override 92 | public Mono closeGracefully() { 93 | return Mono.defer(() -> { 94 | connected = false; 95 | inbound.tryEmitComplete(); 96 | // Wait for all subscribers to complete 97 | return Mono.empty(); 98 | }); 99 | } 100 | 101 | @Override 102 | public T unmarshalFrom(Object data, TypeRef typeRef) { 103 | return McpJsonMapper.getDefault().convertValue(data, typeRef); 104 | } 105 | 106 | } 107 | -------------------------------------------------------------------------------- /mcp-spring/mcp-spring-webflux/src/test/java/io/modelcontextprotocol/WebFluxStatelessIntegrationTests.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2024 - 2024 the original author or authors. 3 | */ 4 | 5 | package io.modelcontextprotocol; 6 | 7 | import java.time.Duration; 8 | import java.util.stream.Stream; 9 | 10 | import org.junit.jupiter.api.AfterEach; 11 | import org.junit.jupiter.api.BeforeEach; 12 | import org.junit.jupiter.api.Timeout; 13 | import org.junit.jupiter.params.provider.Arguments; 14 | 15 | import org.springframework.http.server.reactive.HttpHandler; 16 | import org.springframework.http.server.reactive.ReactorHttpHandlerAdapter; 17 | import org.springframework.web.reactive.function.client.WebClient; 18 | import org.springframework.web.reactive.function.server.RouterFunctions; 19 | import io.modelcontextprotocol.client.McpClient; 20 | import io.modelcontextprotocol.client.transport.HttpClientStreamableHttpTransport; 21 | import io.modelcontextprotocol.client.transport.WebClientStreamableHttpTransport; 22 | import io.modelcontextprotocol.server.McpServer; 23 | import io.modelcontextprotocol.server.McpServer.StatelessAsyncSpecification; 24 | import io.modelcontextprotocol.server.McpServer.StatelessSyncSpecification; 25 | import io.modelcontextprotocol.server.TestUtil; 26 | import io.modelcontextprotocol.server.transport.WebFluxStatelessServerTransport; 27 | import reactor.netty.DisposableServer; 28 | import reactor.netty.http.server.HttpServer; 29 | 30 | @Timeout(15) 31 | class WebFluxStatelessIntegrationTests extends AbstractStatelessIntegrationTests { 32 | 33 | private static final int PORT = TestUtil.findAvailablePort(); 34 | 35 | private static final String CUSTOM_MESSAGE_ENDPOINT = "/otherPath/mcp/message"; 36 | 37 | private DisposableServer httpServer; 38 | 39 | private WebFluxStatelessServerTransport mcpStreamableServerTransport; 40 | 41 | static Stream clientsForTesting() { 42 | return Stream.of(Arguments.of("httpclient"), Arguments.of("webflux")); 43 | } 44 | 45 | @Override 46 | protected void prepareClients(int port, String mcpEndpoint) { 47 | clientBuilders 48 | .put("httpclient", 49 | McpClient.sync(HttpClientStreamableHttpTransport.builder("http://localhost:" + PORT) 50 | .endpoint(CUSTOM_MESSAGE_ENDPOINT) 51 | .build()).initializationTimeout(Duration.ofHours(10)).requestTimeout(Duration.ofHours(10))); 52 | clientBuilders 53 | .put("webflux", McpClient 54 | .sync(WebClientStreamableHttpTransport.builder(WebClient.builder().baseUrl("http://localhost:" + PORT)) 55 | .endpoint(CUSTOM_MESSAGE_ENDPOINT) 56 | .build()) 57 | .initializationTimeout(Duration.ofHours(10)) 58 | .requestTimeout(Duration.ofHours(10))); 59 | } 60 | 61 | @Override 62 | protected StatelessAsyncSpecification prepareAsyncServerBuilder() { 63 | return McpServer.async(this.mcpStreamableServerTransport); 64 | } 65 | 66 | @Override 67 | protected StatelessSyncSpecification prepareSyncServerBuilder() { 68 | return McpServer.sync(this.mcpStreamableServerTransport); 69 | } 70 | 71 | @BeforeEach 72 | public void before() { 73 | this.mcpStreamableServerTransport = WebFluxStatelessServerTransport.builder() 74 | .messageEndpoint(CUSTOM_MESSAGE_ENDPOINT) 75 | .build(); 76 | 77 | HttpHandler httpHandler = RouterFunctions.toHttpHandler(mcpStreamableServerTransport.getRouterFunction()); 78 | ReactorHttpHandlerAdapter adapter = new ReactorHttpHandlerAdapter(httpHandler); 79 | this.httpServer = HttpServer.create().port(PORT).handle(adapter).bindNow(); 80 | 81 | prepareClients(PORT, null); 82 | } 83 | 84 | @AfterEach 85 | public void after() { 86 | if (httpServer != null) { 87 | httpServer.disposeNow(); 88 | } 89 | } 90 | 91 | } 92 | -------------------------------------------------------------------------------- /mcp-core/src/test/java/io/modelcontextprotocol/server/HttpServletStreamableIntegrationTests.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2024 - 2024 the original author or authors. 3 | */ 4 | 5 | package io.modelcontextprotocol.server; 6 | 7 | import java.time.Duration; 8 | import java.util.Map; 9 | import java.util.stream.Stream; 10 | 11 | import io.modelcontextprotocol.client.McpClient; 12 | import io.modelcontextprotocol.client.transport.HttpClientStreamableHttpTransport; 13 | import io.modelcontextprotocol.common.McpTransportContext; 14 | import io.modelcontextprotocol.server.McpServer.AsyncSpecification; 15 | import io.modelcontextprotocol.server.McpServer.SyncSpecification; 16 | import io.modelcontextprotocol.server.transport.HttpServletStreamableServerTransportProvider; 17 | import io.modelcontextprotocol.server.transport.TomcatTestUtil; 18 | import jakarta.servlet.http.HttpServletRequest; 19 | import org.apache.catalina.LifecycleException; 20 | import org.apache.catalina.LifecycleState; 21 | import org.apache.catalina.startup.Tomcat; 22 | import org.junit.jupiter.api.AfterEach; 23 | import org.junit.jupiter.api.BeforeEach; 24 | import org.junit.jupiter.api.Timeout; 25 | import org.junit.jupiter.params.provider.Arguments; 26 | 27 | import static org.assertj.core.api.Assertions.assertThat; 28 | 29 | @Timeout(15) 30 | class HttpServletStreamableIntegrationTests extends AbstractMcpClientServerIntegrationTests { 31 | 32 | private static final int PORT = TomcatTestUtil.findAvailablePort(); 33 | 34 | private static final String MESSAGE_ENDPOINT = "/mcp/message"; 35 | 36 | private HttpServletStreamableServerTransportProvider mcpServerTransportProvider; 37 | 38 | private Tomcat tomcat; 39 | 40 | static Stream clientsForTesting() { 41 | return Stream.of(Arguments.of("httpclient")); 42 | } 43 | 44 | @BeforeEach 45 | public void before() { 46 | // Create and configure the transport provider 47 | mcpServerTransportProvider = HttpServletStreamableServerTransportProvider.builder() 48 | .contextExtractor(TEST_CONTEXT_EXTRACTOR) 49 | .mcpEndpoint(MESSAGE_ENDPOINT) 50 | .keepAliveInterval(Duration.ofSeconds(1)) 51 | .build(); 52 | 53 | tomcat = TomcatTestUtil.createTomcatServer("", PORT, mcpServerTransportProvider); 54 | try { 55 | tomcat.start(); 56 | assertThat(tomcat.getServer().getState()).isEqualTo(LifecycleState.STARTED); 57 | } 58 | catch (Exception e) { 59 | throw new RuntimeException("Failed to start Tomcat", e); 60 | } 61 | 62 | clientBuilders 63 | .put("httpclient", 64 | McpClient.sync(HttpClientStreamableHttpTransport.builder("http://localhost:" + PORT) 65 | .endpoint(MESSAGE_ENDPOINT) 66 | .build()).requestTimeout(Duration.ofHours(10))); 67 | } 68 | 69 | @Override 70 | protected AsyncSpecification prepareAsyncServerBuilder() { 71 | return McpServer.async(this.mcpServerTransportProvider); 72 | } 73 | 74 | @Override 75 | protected SyncSpecification prepareSyncServerBuilder() { 76 | return McpServer.sync(this.mcpServerTransportProvider); 77 | } 78 | 79 | @AfterEach 80 | public void after() { 81 | if (mcpServerTransportProvider != null) { 82 | mcpServerTransportProvider.closeGracefully().block(); 83 | } 84 | if (tomcat != null) { 85 | try { 86 | tomcat.stop(); 87 | tomcat.destroy(); 88 | } 89 | catch (LifecycleException e) { 90 | throw new RuntimeException("Failed to stop Tomcat", e); 91 | } 92 | } 93 | } 94 | 95 | @Override 96 | protected void prepareClients(int port, String mcpEndpoint) { 97 | } 98 | 99 | static McpTransportContextExtractor TEST_CONTEXT_EXTRACTOR = (r) -> McpTransportContext 100 | .create(Map.of("important", "value")); 101 | 102 | } 103 | -------------------------------------------------------------------------------- /mcp-json/src/main/java/io/modelcontextprotocol/json/McpJsonMapper.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 - 2025 the original author or authors. 3 | */ 4 | 5 | package io.modelcontextprotocol.json; 6 | 7 | import java.io.IOException; 8 | 9 | /** 10 | * Abstraction for JSON serialization/deserialization to decouple the SDK from any 11 | * specific JSON library. A default implementation backed by Jackson is provided in 12 | * io.modelcontextprotocol.spec.json.jackson.JacksonJsonMapper. 13 | */ 14 | public interface McpJsonMapper { 15 | 16 | /** 17 | * Deserialize JSON string into a target type. 18 | * @param content JSON as String 19 | * @param type target class 20 | * @return deserialized instance 21 | * @param generic type 22 | * @throws IOException on parse errors 23 | */ 24 | T readValue(String content, Class type) throws IOException; 25 | 26 | /** 27 | * Deserialize JSON bytes into a target type. 28 | * @param content JSON as bytes 29 | * @param type target class 30 | * @return deserialized instance 31 | * @param generic type 32 | * @throws IOException on parse errors 33 | */ 34 | T readValue(byte[] content, Class type) throws IOException; 35 | 36 | /** 37 | * Deserialize JSON string into a parameterized target type. 38 | * @param content JSON as String 39 | * @param type parameterized type reference 40 | * @return deserialized instance 41 | * @param generic type 42 | * @throws IOException on parse errors 43 | */ 44 | T readValue(String content, TypeRef type) throws IOException; 45 | 46 | /** 47 | * Deserialize JSON bytes into a parameterized target type. 48 | * @param content JSON as bytes 49 | * @param type parameterized type reference 50 | * @return deserialized instance 51 | * @param generic type 52 | * @throws IOException on parse errors 53 | */ 54 | T readValue(byte[] content, TypeRef type) throws IOException; 55 | 56 | /** 57 | * Convert a value to a given type, useful for mapping nested JSON structures. 58 | * @param fromValue source value 59 | * @param type target class 60 | * @return converted value 61 | * @param generic type 62 | */ 63 | T convertValue(Object fromValue, Class type); 64 | 65 | /** 66 | * Convert a value to a given parameterized type. 67 | * @param fromValue source value 68 | * @param type target type reference 69 | * @return converted value 70 | * @param generic type 71 | */ 72 | T convertValue(Object fromValue, TypeRef type); 73 | 74 | /** 75 | * Serialize an object to JSON string. 76 | * @param value object to serialize 77 | * @return JSON as String 78 | * @throws IOException on serialization errors 79 | */ 80 | String writeValueAsString(Object value) throws IOException; 81 | 82 | /** 83 | * Serialize an object to JSON bytes. 84 | * @param value object to serialize 85 | * @return JSON as bytes 86 | * @throws IOException on serialization errors 87 | */ 88 | byte[] writeValueAsBytes(Object value) throws IOException; 89 | 90 | /** 91 | * Returns the default {@link McpJsonMapper}. 92 | * @return The default {@link McpJsonMapper} 93 | * @throws IllegalStateException If no {@link McpJsonMapper} implementation exists on 94 | * the classpath. 95 | */ 96 | static McpJsonMapper getDefault() { 97 | return McpJsonInternal.getDefaultMapper(); 98 | } 99 | 100 | /** 101 | * Creates a new default {@link McpJsonMapper}. 102 | * @return The default {@link McpJsonMapper} 103 | * @throws IllegalStateException If no {@link McpJsonMapper} implementation exists on 104 | * the classpath. 105 | */ 106 | static McpJsonMapper createDefault() { 107 | return McpJsonInternal.createDefaultMapper(); 108 | } 109 | 110 | } 111 | --------------------------------------------------------------------------------