From 286b0cef5ea9720ff054c25c59f495d061831e3b Mon Sep 17 00:00:00 2001 From: Sergio del Amo Date: Tue, 23 Sep 2025 13:59:48 +0200 Subject: [PATCH 1/5] Add MCP Server TCK Technology Compatibility Kit This pull-request adds a TCK and an implementation of MCP Server for the built-in Java HTTP Server. Maven Surefire plugin version: 3.5.4 JUnit Version 5.13.4 --- mcp-bom/pom.xml | 7 + .../server/McpHttpServer.java | 58 ++++++ .../server/McpHttpServerInternal.java | 95 +++++++++ .../server/McpHttpServerSupplier.java | 25 +++ ...pExchangeMcpTransportContextExtractor.java | 55 ++++++ .../httpserver/McpSimpleHttpServer.java | 185 ++++++++++++++++++ ...etRequestMcpTransportContextExtractor.java | 42 ++++ .../server/transport/HttpJsonRpcResponse.java | 101 ++++++++++ ...HttpServerMcpStatelessServerTransport.java | 173 ++++++++++++++++ ...HttpServletSseServerTransportProvider.java | 7 +- .../HttpServletStatelessServerTransport.java | 135 ++++--------- ...vletStreamableServerTransportProvider.java | 8 +- ...erMcpTransportContextIntegrationTests.java | 15 +- ...erMcpTransportContextIntegrationTests.java | 15 +- .../HttpServletSseIntegrationTests.java | 11 +- ...HttpServletStreamableIntegrationTests.java | 11 +- ...angeMcpTransportContextExtractorTests.java | 72 +++++++ ...uestMcpTransportContextExtractorTests.java | 81 ++++++++ mcp-tck-http-httpserver-async/pom.xml | 62 ++++++ .../async/AsyncMcpHttpServerSupplier.java | 89 +++++++++ .../async/HttpServerAsyncSuite.java | 12 ++ ...ntextprotocol.server.McpHttpServerSupplier | 1 + mcp-tck-http-httpserver-sync/pom.xml | 62 ++++++ .../httpserver/sync/HttpServerSyncSuite.java | 12 ++ .../sync/SyncMcpHttpServerSupplier.java | 87 ++++++++ ...ntextprotocol.server.McpHttpServerSupplier | 2 + .../src/test/resources/logback.xml | 11 ++ mcp-tck-http/pom.xml | 54 +++++ .../server/http/tck/HttpRequestUtils.java | 44 +++++ .../server/http/tck/InitializeTest.java | 71 +++++++ .../server/http/tck/PingTest.java | 50 +++++ .../server/http/tck/PromptsGetTest.java | 77 ++++++++ .../server/http/tck/PromptsListTest.java | 74 +++++++ .../server/http/tck/ResourcesGetTest.java | 73 +++++++ .../server/http/tck/ResourcesListTest.java | 70 +++++++ .../http/tck/SuiteShutdownExtension.java | 56 ++++++ .../server/http/tck/ToolsCallTest.java | 73 +++++++ .../server/http/tck/ToolsTest.java | 72 +++++++ .../org.junit.jupiter.api.extension.Extension | 2 + .../main/resources/junit-platform.properties | 2 + pom.xml | 9 +- 41 files changed, 2037 insertions(+), 124 deletions(-) create mode 100644 mcp-core/src/main/java/io/modelcontextprotocol/server/McpHttpServer.java create mode 100644 mcp-core/src/main/java/io/modelcontextprotocol/server/McpHttpServerInternal.java create mode 100644 mcp-core/src/main/java/io/modelcontextprotocol/server/McpHttpServerSupplier.java create mode 100644 mcp-core/src/main/java/io/modelcontextprotocol/server/httpserver/HttpExchangeMcpTransportContextExtractor.java create mode 100644 mcp-core/src/main/java/io/modelcontextprotocol/server/httpserver/McpSimpleHttpServer.java create mode 100644 mcp-core/src/main/java/io/modelcontextprotocol/server/servlet/HttpServletRequestMcpTransportContextExtractor.java create mode 100644 mcp-core/src/main/java/io/modelcontextprotocol/server/transport/HttpJsonRpcResponse.java create mode 100644 mcp-core/src/main/java/io/modelcontextprotocol/server/transport/HttpServerMcpStatelessServerTransport.java create mode 100644 mcp-core/src/test/java/io/modelcontextprotocol/server/httpserver/HttpExchangeMcpTransportContextExtractorTests.java create mode 100644 mcp-core/src/test/java/io/modelcontextprotocol/server/servlet/HttpServletRequestMcpTransportContextExtractorTests.java create mode 100644 mcp-tck-http-httpserver-async/pom.xml create mode 100644 mcp-tck-http-httpserver-async/src/test/java/io/modelcontextprotocol/server/http/tck/httpserver/async/AsyncMcpHttpServerSupplier.java create mode 100644 mcp-tck-http-httpserver-async/src/test/java/io/modelcontextprotocol/server/http/tck/httpserver/async/HttpServerAsyncSuite.java create mode 100644 mcp-tck-http-httpserver-async/src/test/resources/META-INF/services/io.modelcontextprotocol.server.McpHttpServerSupplier create mode 100644 mcp-tck-http-httpserver-sync/pom.xml create mode 100644 mcp-tck-http-httpserver-sync/src/test/java/io/modelcontextprotocol/server/httpserver/sync/HttpServerSyncSuite.java create mode 100644 mcp-tck-http-httpserver-sync/src/test/java/io/modelcontextprotocol/server/httpserver/sync/SyncMcpHttpServerSupplier.java create mode 100644 mcp-tck-http-httpserver-sync/src/test/resources/META-INF/services/io.modelcontextprotocol.server.McpHttpServerSupplier create mode 100644 mcp-tck-http-httpserver-sync/src/test/resources/logback.xml create mode 100644 mcp-tck-http/pom.xml create mode 100644 mcp-tck-http/src/main/java/io/modelcontextprotocol/server/http/tck/HttpRequestUtils.java create mode 100644 mcp-tck-http/src/main/java/io/modelcontextprotocol/server/http/tck/InitializeTest.java create mode 100644 mcp-tck-http/src/main/java/io/modelcontextprotocol/server/http/tck/PingTest.java create mode 100644 mcp-tck-http/src/main/java/io/modelcontextprotocol/server/http/tck/PromptsGetTest.java create mode 100644 mcp-tck-http/src/main/java/io/modelcontextprotocol/server/http/tck/PromptsListTest.java create mode 100644 mcp-tck-http/src/main/java/io/modelcontextprotocol/server/http/tck/ResourcesGetTest.java create mode 100644 mcp-tck-http/src/main/java/io/modelcontextprotocol/server/http/tck/ResourcesListTest.java create mode 100644 mcp-tck-http/src/main/java/io/modelcontextprotocol/server/http/tck/SuiteShutdownExtension.java create mode 100644 mcp-tck-http/src/main/java/io/modelcontextprotocol/server/http/tck/ToolsCallTest.java create mode 100644 mcp-tck-http/src/main/java/io/modelcontextprotocol/server/http/tck/ToolsTest.java create mode 100644 mcp-tck-http/src/main/resources/META-INF/services/org.junit.jupiter.api.extension.Extension create mode 100644 mcp-tck-http/src/main/resources/junit-platform.properties diff --git a/mcp-bom/pom.xml b/mcp-bom/pom.xml index 6b1027a21..40faeb6e0 100644 --- a/mcp-bom/pom.xml +++ b/mcp-bom/pom.xml @@ -61,6 +61,13 @@ ${project.version} + + + io.modelcontextprotocol.sdk + mcp-tck-http + ${project.version} + + io.modelcontextprotocol.sdk diff --git a/mcp-core/src/main/java/io/modelcontextprotocol/server/McpHttpServer.java b/mcp-core/src/main/java/io/modelcontextprotocol/server/McpHttpServer.java new file mode 100644 index 000000000..3ec4df78a --- /dev/null +++ b/mcp-core/src/main/java/io/modelcontextprotocol/server/McpHttpServer.java @@ -0,0 +1,58 @@ +/* + * Copyright 2017-2025 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.modelcontextprotocol.server; + +/** + * An HTTP MCP Server. + */ +public interface McpHttpServer extends AutoCloseable { + + /** + * Starts the MCP Server. + */ + void start(); + + /** + * @return The Port the server is running at + */ + int getPort(); + + /** + * @return the MCP endpoint path. For example, `/mcp` + */ + String getEndpoint(); + + /** + * Returns the default {@link McpHttpServer}. + * @return The default {@link McpHttpServer} + * @throws IllegalStateException If no {@link McpHttpServer} implementation exists on + * the classpath. + */ + static McpHttpServer getDefault() { + return McpHttpServerInternal.getDefaultMapper(); + } + + /** + * Creates a new default {@link McpHttpServer}. + * @return The default {@link McpHttpServer} + * @throws IllegalStateException If no {@link McpHttpServer} implementation exists on + * the classpath. + */ + static McpHttpServer createDefault() { + return McpHttpServerInternal.createDefaultMapper(); + } + +} diff --git a/mcp-core/src/main/java/io/modelcontextprotocol/server/McpHttpServerInternal.java b/mcp-core/src/main/java/io/modelcontextprotocol/server/McpHttpServerInternal.java new file mode 100644 index 000000000..7d9e19e8e --- /dev/null +++ b/mcp-core/src/main/java/io/modelcontextprotocol/server/McpHttpServerInternal.java @@ -0,0 +1,95 @@ +/* + * Copyright 2017-2025 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.modelcontextprotocol.server; + +import java.util.ServiceLoader; +import java.util.concurrent.atomic.AtomicReference; +import java.util.stream.Stream; + +/** + * Utility class for creating a default {@link McpHttpServer} instance. This can be used + * by TCK (Technology Compatibility Kit) suites. This class provides a single method to + * create a default mapper using the {@link ServiceLoader} mechanism. + */ +final class McpHttpServerInternal { + + private static McpHttpServer defaultJsonMapper = null; + + /** + * Returns the cached default {@link McpHttpServer} instance. If the default mapper + * has not been created yet, it will be initialized using the + * {@link #createDefaultMapper()} method. + * @return the default {@link McpHttpServer} instance + * @throws IllegalStateException if no default {@link McpHttpServer} implementation is + * found + */ + static McpHttpServer getDefaultMapper() { + if (defaultJsonMapper == null) { + defaultJsonMapper = McpHttpServerInternal.createDefaultMapper(); + } + return defaultJsonMapper; + } + + /** + * Creates a default {@link McpHttpServer} instance using the {@link ServiceLoader} + * mechanism. The default mapper is resolved by loading the first available + * {@link McpHttpServerSupplier} implementation on the classpath. + * @return the default {@link McpHttpServer} instance + * @throws IllegalStateException if no default {@link McpHttpServer} implementation is + * found + */ + static McpHttpServer createDefaultMapper() { + AtomicReference ex = new AtomicReference<>(); + return ServiceLoader.load(McpHttpServerSupplier.class).stream().flatMap(p -> { + try { + McpHttpServerSupplier supplier = p.get(); + return Stream.ofNullable(supplier); + } + catch (Exception e) { + addException(ex, e); + return Stream.empty(); + } + }).flatMap(jsonMapperSupplier -> { + try { + return Stream.ofNullable(jsonMapperSupplier.get()); + } + catch (Exception e) { + addException(ex, e); + return Stream.empty(); + } + }).findFirst().orElseThrow(() -> { + if (ex.get() != null) { + return ex.get(); + } + else { + return new IllegalStateException("No default McpHttpServer implementation found"); + } + }); + } + + private static void addException(AtomicReference ref, Exception toAdd) { + ref.updateAndGet(existing -> { + if (existing == null) { + return new IllegalStateException("Failed to initialize default McpHttpServer", toAdd); + } + else { + existing.addSuppressed(toAdd); + return existing; + } + }); + } + +} diff --git a/mcp-core/src/main/java/io/modelcontextprotocol/server/McpHttpServerSupplier.java b/mcp-core/src/main/java/io/modelcontextprotocol/server/McpHttpServerSupplier.java new file mode 100644 index 000000000..01a6a7ade --- /dev/null +++ b/mcp-core/src/main/java/io/modelcontextprotocol/server/McpHttpServerSupplier.java @@ -0,0 +1,25 @@ +/* + * Copyright 2017-2025 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.modelcontextprotocol.server; + +import java.util.function.Supplier; + +/** + * Strategy interface for resolving a {@link McpHttpServer}. + */ +public interface McpHttpServerSupplier extends Supplier { + +} diff --git a/mcp-core/src/main/java/io/modelcontextprotocol/server/httpserver/HttpExchangeMcpTransportContextExtractor.java b/mcp-core/src/main/java/io/modelcontextprotocol/server/httpserver/HttpExchangeMcpTransportContextExtractor.java new file mode 100644 index 000000000..f160a00a7 --- /dev/null +++ b/mcp-core/src/main/java/io/modelcontextprotocol/server/httpserver/HttpExchangeMcpTransportContextExtractor.java @@ -0,0 +1,55 @@ +/* + * Copyright 2017-2025 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.modelcontextprotocol.server.httpserver; + +import com.sun.net.httpserver.Headers; +import com.sun.net.httpserver.HttpExchange; +import io.modelcontextprotocol.common.McpTransportContext; +import io.modelcontextprotocol.server.McpTransportContextExtractor; +import io.modelcontextprotocol.spec.ProtocolVersions; + +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; + +/** + * {@link McpTransportContextExtractor} implementation for {@link HttpExchange}. + */ +public class HttpExchangeMcpTransportContextExtractor implements McpTransportContextExtractor { + + @Override + public McpTransportContext extract(HttpExchange httpExchange) { + return McpTransportContext.create(metadata(httpExchange)); + } + + private Map metadata(HttpExchange httpExchange) { + Headers headers = httpExchange.getRequestHeaders(); + return metadata(headers); + } + + private Map metadata(Headers headers) { + Map metadata = new HashMap<>(); + metadata.put(io.modelcontextprotocol.spec.HttpHeaders.PROTOCOL_VERSION, + Optional.ofNullable(headers.getFirst(io.modelcontextprotocol.spec.HttpHeaders.PROTOCOL_VERSION)) + .orElse(ProtocolVersions.MCP_2025_03_26)); + Optional.ofNullable(headers.getFirst(io.modelcontextprotocol.spec.HttpHeaders.MCP_SESSION_ID)) + .ifPresent(v -> metadata.put(io.modelcontextprotocol.spec.HttpHeaders.MCP_SESSION_ID, v)); + Optional.ofNullable(headers.getFirst(io.modelcontextprotocol.spec.HttpHeaders.LAST_EVENT_ID)) + .ifPresent(v -> metadata.put(io.modelcontextprotocol.spec.HttpHeaders.LAST_EVENT_ID, v)); + return metadata; + } + +} diff --git a/mcp-core/src/main/java/io/modelcontextprotocol/server/httpserver/McpSimpleHttpServer.java b/mcp-core/src/main/java/io/modelcontextprotocol/server/httpserver/McpSimpleHttpServer.java new file mode 100644 index 000000000..c1c5cdfed --- /dev/null +++ b/mcp-core/src/main/java/io/modelcontextprotocol/server/httpserver/McpSimpleHttpServer.java @@ -0,0 +1,185 @@ +/* + * Copyright 2017-2025 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.modelcontextprotocol.server.httpserver; + +import com.sun.net.httpserver.HttpExchange; +import com.sun.net.httpserver.HttpHandler; +import com.sun.net.httpserver.HttpServer; +import io.modelcontextprotocol.server.transport.HttpJsonRpcResponse; +import io.modelcontextprotocol.server.transport.HttpServerMcpStatelessServerTransport; +import io.modelcontextprotocol.server.McpHttpServer; +import io.modelcontextprotocol.json.McpJsonMapper; +import io.modelcontextprotocol.json.TypeRef; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.net.InetSocketAddress; +import java.util.Map; + +/** + * Mcp HTTP Server class which uses a {@link HttpServer}. + */ +public class McpSimpleHttpServer implements McpHttpServer { + + private static final Logger LOG = LoggerFactory.getLogger(McpSimpleHttpServer.class); + + private static final String METHOD_POST = "POST"; + + private static final String HEADER_CONTENT_TYPE = "Content-Type"; + + private static final String MEDIA_TYPE_APPLICATION_JSON = "application/json"; + + private static final String DEFAULT_ENDPOINT = "/mcp"; + + protected final HttpServer server; + + protected final String endpoint; + + protected final McpJsonMapper jsonMapper; + + protected final HttpServerMcpStatelessServerTransport transport; + + /** + * @param transport Transport + * @throws IOException IO Exception while invoking + * {@link HttpServer#create(InetSocketAddress, int)} + */ + public McpSimpleHttpServer(HttpServerMcpStatelessServerTransport transport) throws IOException { + this(new InetSocketAddress(0), DEFAULT_ENDPOINT, transport, McpJsonMapper.getDefault()); + } + + /** + * @param transport Transport + * @param jsonMapper JSON Mapper + * @throws IOException IO Exception while invoking + * {@link HttpServer#create(InetSocketAddress, int)} + */ + public McpSimpleHttpServer(HttpServerMcpStatelessServerTransport transport, McpJsonMapper jsonMapper) + throws IOException { + this(new InetSocketAddress(0), DEFAULT_ENDPOINT, transport, jsonMapper); + } + + /** + * @param inetSocketAddress address + * @param endpoint endpoint + * @param transport transport + * @param jsonMapper JSON Mapper + * @throws IOException IO Exception while invoking + * {@link HttpServer#create(InetSocketAddress, int)} + */ + public McpSimpleHttpServer(InetSocketAddress inetSocketAddress, String endpoint, + HttpServerMcpStatelessServerTransport transport, McpJsonMapper jsonMapper) + throws IOException { + this.endpoint = endpoint; + this.jsonMapper = jsonMapper; + this.transport = transport; + this.server = HttpServer.create(inetSocketAddress, 0); + server.createContext(endpoint, createHttpHandler()); + } + + @Override + public int getPort() { + return getAddress().getPort(); + } + + @Override + public void start() { + server.start(); + } + + @Override + public String getEndpoint() { + return this.endpoint; + } + + @Override + public void close() { + stop(); + } + + /** + * Stop this server. + */ + public void stop() { + server.stop(0); + } + + /** + * Stop this server. + * @param delay the maximum time in seconds to wait until exchanges have finished + */ + public void stop(int delay) { + server.stop(delay); + } + + private HttpHandler createHttpHandler() { + return exchange -> { + try { + if (exchange.getRequestMethod().equalsIgnoreCase(METHOD_POST)) { + if (hasJsonContentType(exchange)) { + Map body = body(exchange); + HttpJsonRpcResponse rsp = transport.handlePost(exchange, body).block(); + sendResponse(rsp, exchange); + } + else { + exchange.sendResponseHeaders(422, -1); + } + } + else { + exchange.sendResponseHeaders(405, -1); + } + } + catch (IOException e) { + if (LOG.isErrorEnabled()) { + LOG.error(e.getMessage(), e); + } + } + finally { + exchange.close(); + } + }; + } + + private void sendResponse(HttpJsonRpcResponse rsp, HttpExchange exchange) throws IOException { + if (rsp == null || rsp.body() == null) { + exchange.sendResponseHeaders(rsp != null ? rsp.statusCode() : 202, -1); + return; + } + byte[] responseBytes = jsonMapper.writeValueAsBytes(rsp.body()); + exchange.getResponseHeaders().add(HEADER_CONTENT_TYPE, MEDIA_TYPE_APPLICATION_JSON); + exchange.sendResponseHeaders(rsp.statusCode(), responseBytes.length); + exchange.getResponseBody().write(responseBytes); + } + + private boolean hasJsonContentType(HttpExchange exchange) { + return exchange.getRequestHeaders().containsKey(HEADER_CONTENT_TYPE) + && exchange.getRequestHeaders().getFirst(HEADER_CONTENT_TYPE).equals(MEDIA_TYPE_APPLICATION_JSON); + } + + private Map body(HttpExchange exchange) throws IOException { + TypeRef> typeRef = new TypeRef<>() { + + }; + byte[] requestBytes = exchange.getRequestBody().readAllBytes(); + return jsonMapper.readValue(requestBytes, typeRef); + } + + private InetSocketAddress getAddress() { + return server.getAddress(); + } + +} diff --git a/mcp-core/src/main/java/io/modelcontextprotocol/server/servlet/HttpServletRequestMcpTransportContextExtractor.java b/mcp-core/src/main/java/io/modelcontextprotocol/server/servlet/HttpServletRequestMcpTransportContextExtractor.java new file mode 100644 index 000000000..d8129646c --- /dev/null +++ b/mcp-core/src/main/java/io/modelcontextprotocol/server/servlet/HttpServletRequestMcpTransportContextExtractor.java @@ -0,0 +1,42 @@ +/* + * Copyright 2024-2024 the original author or authors. + */ +package io.modelcontextprotocol.server.servlet; + +import io.modelcontextprotocol.common.McpTransportContext; +import io.modelcontextprotocol.server.McpTransportContextExtractor; +import io.modelcontextprotocol.spec.ProtocolVersions; +import jakarta.servlet.http.HttpServletRequest; + +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; + +/** + * {@link McpTransportContextExtractor} implementation for {@link HttpServletRequest}. + */ +public class HttpServletRequestMcpTransportContextExtractor + implements McpTransportContextExtractor { + + @Override + public McpTransportContext extract(HttpServletRequest request) { + return McpTransportContext.create(metadata(request)); + } + + /** + * @param request Servlet Request + * @return Extracts Map for MCP Transport Context + */ + protected Map metadata(HttpServletRequest request) { + Map metadata = new HashMap<>(); + metadata.put(io.modelcontextprotocol.spec.HttpHeaders.PROTOCOL_VERSION, + Optional.ofNullable(request.getHeader(io.modelcontextprotocol.spec.HttpHeaders.PROTOCOL_VERSION)) + .orElse(ProtocolVersions.MCP_2025_03_26)); + Optional.ofNullable(request.getHeader(io.modelcontextprotocol.spec.HttpHeaders.MCP_SESSION_ID)) + .ifPresent(v -> metadata.put(io.modelcontextprotocol.spec.HttpHeaders.MCP_SESSION_ID, v)); + Optional.ofNullable(request.getHeader(io.modelcontextprotocol.spec.HttpHeaders.LAST_EVENT_ID)) + .ifPresent(v -> metadata.put(io.modelcontextprotocol.spec.HttpHeaders.LAST_EVENT_ID, v)); + return metadata; + } + +} diff --git a/mcp-core/src/main/java/io/modelcontextprotocol/server/transport/HttpJsonRpcResponse.java b/mcp-core/src/main/java/io/modelcontextprotocol/server/transport/HttpJsonRpcResponse.java new file mode 100644 index 000000000..b4d8d9e3d --- /dev/null +++ b/mcp-core/src/main/java/io/modelcontextprotocol/server/transport/HttpJsonRpcResponse.java @@ -0,0 +1,101 @@ +/* + * Copyright 2024-2024 the original author or authors. + */ +package io.modelcontextprotocol.server.transport; + +import io.modelcontextprotocol.spec.McpSchema; + +/** + * HTTP Response. + * + * @param statusCode HTTP Status code + * @param statusReason HTTP Status Reason + * @param body response Body + */ +public record HttpJsonRpcResponse(int statusCode, String statusReason, McpSchema.JSONRPCResponse body) { + + public HttpJsonRpcResponse(int statusCode, String statusReason, McpSchema.JSONRPCResponse body) { + this.statusCode = statusCode; + this.statusReason = statusReason; + this.body = body; + } + + public HttpJsonRpcResponse(int status, McpSchema.JSONRPCResponse rsp) { + this(status, reason(status), rsp); + } + + static String reason(int statusCode) { + return switch (statusCode) { + case 100 -> "Continue"; + case 101 -> "Switching Protocols"; + case 102 -> "Processing"; + case 103 -> "Early Hints"; + case 200 -> "Ok"; + case 201 -> "Created"; + case 202 -> "Accepted"; + case 203 -> "Non-Authoritative Information"; + case 204 -> "No Content"; + case 205 -> "Reset Content"; + case 206 -> "Partial Content"; + case 207 -> "Multi Status"; + case 208 -> "Already imported"; + case 226 -> "IM Used"; + case 300 -> "Multiple Choices"; + case 301 -> "Moved Permanently"; + case 302 -> "Found"; + case 303 -> "See Other"; + case 304 -> "Not Modified"; + case 305 -> "Use Proxy"; + case 306 -> "Switch Proxy"; + case 307 -> "Temporary Redirect"; + case 308 -> "Permanent Redirect"; + case 400 -> "Bad Request"; + case 401 -> "Unauthorized"; + case 402 -> "Payment Required"; + case 403 -> "Forbidden"; + case 404 -> "Not Found"; + case 405 -> "Method Not Allowed"; + case 406 -> "Not Acceptable"; + case 407 -> "Proxy Authentication Required"; + case 408 -> "Request Timeout"; + case 409 -> "Conflict"; + case 410 -> "Gone"; + case 411 -> "Length Required"; + case 412 -> "Precondition Failed"; + case 413 -> "Request Entity Too Large"; + case 414 -> "Request-URI Too Long"; + case 415 -> "Unsupported Media Type"; + case 416 -> "Requested Range Not Satisfiable"; + case 417 -> "Expectation Failed"; + case 418 -> "I am a teapot"; + case 420 -> "Enhance your calm"; + case 421 -> "Misdirected Request"; + case 422 -> "Unprocessable Entity"; + case 423 -> "Locked"; + case 424 -> "Failed Dependency"; + case 425 -> "Too Early"; + case 426 -> "Upgrade Required"; + case 428 -> "Precondition Required"; + case 429 -> "Too Many Requests"; + case 431 -> "Request Header Fields Too Large"; + case 444 -> "No Response"; + case 450 -> "Blocked by Windows Parental Controls"; + case 451 -> "Unavailable For Legal Reasons"; + case 494 -> "Request Header Too Large"; + case 500 -> "Internal Server Error"; + case 501 -> "Not Implemented"; + case 502 -> "Bad Gateway"; + case 503 -> "Service Unavailable"; + case 504 -> "Gateway Timeout"; + case 505 -> "HTTP Version Not Supported"; + case 506 -> "Variant Also Negotiates"; + case 507 -> "Insufficient Storage"; + case 508 -> "Loop Detected"; + case 509 -> "Bandwidth Limit Exceeded"; + case 510 -> "Not Extended"; + case 511 -> "Network Authentication Required"; + case 522 -> "Connection Timed Out"; + default -> null; + }; + } +} diff --git a/mcp-core/src/main/java/io/modelcontextprotocol/server/transport/HttpServerMcpStatelessServerTransport.java b/mcp-core/src/main/java/io/modelcontextprotocol/server/transport/HttpServerMcpStatelessServerTransport.java new file mode 100644 index 000000000..98ba519ad --- /dev/null +++ b/mcp-core/src/main/java/io/modelcontextprotocol/server/transport/HttpServerMcpStatelessServerTransport.java @@ -0,0 +1,173 @@ +/* + * Copyright 2024-2024 the original author or authors. + */ +package io.modelcontextprotocol.server.transport; + +import io.modelcontextprotocol.common.McpTransportContext; +import io.modelcontextprotocol.server.McpStatelessServerHandler; +import io.modelcontextprotocol.server.McpTransportContextExtractor; +import io.modelcontextprotocol.spec.McpError; +import io.modelcontextprotocol.spec.McpSchema; +import io.modelcontextprotocol.spec.McpStatelessServerTransport; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import reactor.core.publisher.Mono; + +import java.util.Map; +import java.util.function.Function; + +import static io.modelcontextprotocol.spec.McpSchema.JSONRPC_VERSION; + +/** + * Generic {@link McpStatelessServerTransport} implementation which can be used by + * different HTTP Servers implementations. + * + * @see {@link io.modelcontextprotocol.server.httpserver.McpSimpleHttpServer} + * @see {@link io.modelcontextprotocol.server.transport.HttpServletStatelessServerTransport} + * @param Request Type + */ +public class HttpServerMcpStatelessServerTransport implements McpStatelessServerTransport { + + private static final String KEY_METHOD = "method"; + + private static final String KEY_ID = "id"; + + private static final String KEY_JSONRPC = "jsonrpc"; + + private static final String KEY_PARAMS = "params"; + + private static final Logger LOG = LoggerFactory.getLogger(HttpServerMcpStatelessServerTransport.class); + + private final McpTransportContextExtractor contextExtractor; + + private McpStatelessServerHandler mcpHandler; + + public HttpServerMcpStatelessServerTransport(McpTransportContextExtractor contextExtractor) { + this.contextExtractor = contextExtractor; + } + + @Override + public void setMcpHandler(McpStatelessServerHandler mcpHandler) { + this.mcpHandler = mcpHandler; + } + + /** + * Handle POST request to MCP Endpoint. + * @param request HTTP Request + * @param body HTTP Request Body + * @return HTTP Response + */ + public Mono handlePost(T request, Map body) { + McpTransportContext transportContext = contextExtractor.extract(request); + McpSchema.JSONRPCMessage jsonRpcMessage = jsonRpcMessage(body); + if (jsonRpcMessage instanceof McpSchema.JSONRPCRequest jsonrpcRequest) { + return handleJsonRpcRequest(jsonrpcRequest, transportContext); + } + else if (jsonRpcMessage instanceof McpSchema.JSONRPCNotification notification) { + return handleJsonRpcNotification(notification, transportContext); + } + throw mcpError(McpSchema.ErrorCodes.INVALID_REQUEST, "The server accepts either requests or notifications"); + } + + @Override + public Mono closeGracefully() { + return Mono.empty(); + } + + @SuppressWarnings("java:S3740") + private Mono handleJsonRpcNotification(McpSchema.JSONRPCNotification jsonrpcNotification, + McpTransportContext transportContext) { + HttpJsonRpcResponse accepted = new HttpJsonRpcResponse(202, null); + Mono acceptedMono = Mono.just(accepted); + Mono voidMono = mcpHandler.handleNotification(transportContext, jsonrpcNotification); + Mono jsonrpcResponseMono = voidMono.then(Mono.empty()); + jsonrpcResponseMono = onError(transportContext, jsonrpcNotification, jsonrpcResponseMono); + return jsonrpcResponseMono.map(rsp -> { + int status = status(rsp); + if (status >= 400) { + return new HttpJsonRpcResponse(status, rsp); + } + return accepted; + }).switchIfEmpty(acceptedMono); + } + + @SuppressWarnings("java:S3740") + private Mono handleJsonRpcRequest(McpSchema.JSONRPCRequest jsonrpcRequest, + McpTransportContext transportContext) { + Mono jsonrpcResponse = mcpHandler.handleRequest(transportContext, jsonrpcRequest); + jsonrpcResponse = onError(transportContext, jsonrpcRequest, jsonrpcResponse); + return jsonrpcResponse.map(rsp -> new HttpJsonRpcResponse(status(rsp), rsp)); + } + + private Mono onError(McpTransportContext transportContext, + McpSchema.JSONRPCMessage jsonrpcMessage, Mono response) { + return response.contextWrite(ctx -> ctx.put(McpTransportContext.KEY, transportContext)) + .onErrorResume(McpError.class, e -> { + if (LOG.isDebugEnabled()) { + LOG.debug("Failed to handle JSON RPC Message: {}", e.getMessage()); + } + return Mono.just(errorJsonrpcResponse(jsonrpcMessage, e)); + }) + .onErrorResume(throwable -> { + if (LOG.isErrorEnabled()) { + LOG.error("Failed to handle JSON RPC Message: {}", throwable.getMessage()); + } + return Mono.just(errorJsonrpcResponse(jsonrpcMessage, mcpError(McpSchema.ErrorCodes.INTERNAL_ERROR, + "Failed to handle request: " + throwable.getMessage()))); + }); + } + + private McpSchema.JSONRPCMessage jsonRpcMessage(Map body) { + if (body.containsKey(KEY_METHOD) && body.containsKey(KEY_ID)) { + return new McpSchema.JSONRPCRequest(body.get(KEY_JSONRPC).toString(), body.get(KEY_METHOD).toString(), + body.get(KEY_ID), body.get(KEY_PARAMS)); + } + else if (body.containsKey(KEY_METHOD) && !body.containsKey(KEY_ID)) { + return new McpSchema.JSONRPCNotification(body.get(KEY_JSONRPC).toString(), body.get(KEY_METHOD).toString(), + body.get(KEY_PARAMS)); + } + return null; + } + + static McpSchema.JSONRPCResponse errorJsonrpcResponse(McpSchema.JSONRPCMessage jsonrpcMessage, McpError error) { + McpSchema.JSONRPCResponse.JSONRPCError jsonrpcError = error.getJsonRpcError(); + if (jsonrpcError == null) { + jsonrpcError = new McpSchema.JSONRPCResponse.JSONRPCError(McpSchema.ErrorCodes.INTERNAL_ERROR, + error.getMessage(), null); + } + return new McpSchema.JSONRPCResponse(JSONRPC_VERSION, + jsonrpcMessage instanceof McpSchema.JSONRPCRequest jsonrpcRequest ? jsonrpcRequest.id() : null, null, + jsonrpcError); + } + + private static int status(McpSchema.JSONRPCResponse response) { + if (response == null || response.error() == null) { + return 200; + } + return status(response.error()); + } + + static int status(McpSchema.JSONRPCResponse.JSONRPCError error) { + if (error.code() == McpSchema.ErrorCodes.PARSE_ERROR) { + return 400; + } + else if (error.code() == McpSchema.ErrorCodes.INVALID_REQUEST) { + return 400; + } + else if (error.code() == McpSchema.ErrorCodes.METHOD_NOT_FOUND) { + return 400; + } + else if (error.code() == McpSchema.ErrorCodes.INVALID_PARAMS) { + return 400; + } + else if (error.code() == McpSchema.ErrorCodes.INTERNAL_ERROR) { + return 500; + } + return 500; + } + + private static McpError mcpError(int error, String message) { + return McpError.builder(error).message(message).build(); + } + +} diff --git a/mcp-core/src/main/java/io/modelcontextprotocol/server/transport/HttpServletSseServerTransportProvider.java b/mcp-core/src/main/java/io/modelcontextprotocol/server/transport/HttpServletSseServerTransportProvider.java index 4739e231a..2914f0b1b 100644 --- a/mcp-core/src/main/java/io/modelcontextprotocol/server/transport/HttpServletSseServerTransportProvider.java +++ b/mcp-core/src/main/java/io/modelcontextprotocol/server/transport/HttpServletSseServerTransportProvider.java @@ -18,6 +18,7 @@ import io.modelcontextprotocol.json.TypeRef; import io.modelcontextprotocol.common.McpTransportContext; import io.modelcontextprotocol.server.McpTransportContextExtractor; +import io.modelcontextprotocol.server.servlet.HttpServletRequestMcpTransportContextExtractor; import io.modelcontextprotocol.spec.McpError; import io.modelcontextprotocol.spec.McpSchema; import io.modelcontextprotocol.spec.McpServerSession; @@ -503,8 +504,7 @@ public static class Builder { private String sseEndpoint = DEFAULT_SSE_ENDPOINT; - private McpTransportContextExtractor contextExtractor = ( - serverRequest) -> McpTransportContext.EMPTY; + private McpTransportContextExtractor contextExtractor; private Duration keepAliveInterval; @@ -594,7 +594,8 @@ public HttpServletSseServerTransportProvider build() { } return new HttpServletSseServerTransportProvider( jsonMapper == null ? McpJsonMapper.getDefault() : jsonMapper, baseUrl, messageEndpoint, sseEndpoint, - keepAliveInterval, contextExtractor); + keepAliveInterval, + contextExtractor == null ? new HttpServletRequestMcpTransportContextExtractor() : contextExtractor); } } diff --git a/mcp-core/src/main/java/io/modelcontextprotocol/server/transport/HttpServletStatelessServerTransport.java b/mcp-core/src/main/java/io/modelcontextprotocol/server/transport/HttpServletStatelessServerTransport.java index 40767f416..a916fd28b 100644 --- a/mcp-core/src/main/java/io/modelcontextprotocol/server/transport/HttpServletStatelessServerTransport.java +++ b/mcp-core/src/main/java/io/modelcontextprotocol/server/transport/HttpServletStatelessServerTransport.java @@ -4,10 +4,12 @@ package io.modelcontextprotocol.server.transport; -import java.io.BufferedReader; import java.io.IOException; import java.io.PrintWriter; +import java.util.Map; +import io.modelcontextprotocol.json.TypeRef; +import io.modelcontextprotocol.server.servlet.HttpServletRequestMcpTransportContextExtractor; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -16,8 +18,6 @@ import io.modelcontextprotocol.common.McpTransportContext; import io.modelcontextprotocol.server.McpStatelessServerHandler; import io.modelcontextprotocol.server.McpTransportContextExtractor; -import io.modelcontextprotocol.spec.McpError; -import io.modelcontextprotocol.spec.McpSchema; import io.modelcontextprotocol.spec.McpStatelessServerTransport; import io.modelcontextprotocol.util.Assert; import jakarta.servlet.ServletException; @@ -36,6 +36,9 @@ @WebServlet(asyncSupported = true) public class HttpServletStatelessServerTransport extends HttpServlet implements McpStatelessServerTransport { + private static final TypeRef> MAP_TYPE_REF = new TypeRef<>() { + }; + private static final Logger logger = LoggerFactory.getLogger(HttpServletStatelessServerTransport.class); public static final String UTF_8 = "UTF-8"; @@ -52,26 +55,22 @@ public class HttpServletStatelessServerTransport extends HttpServlet implements private final String mcpEndpoint; - private McpStatelessServerHandler mcpHandler; - - private McpTransportContextExtractor contextExtractor; - private volatile boolean isClosing = false; - private HttpServletStatelessServerTransport(McpJsonMapper jsonMapper, String mcpEndpoint, - McpTransportContextExtractor contextExtractor) { + private HttpServerMcpStatelessServerTransport delegate; + + private HttpServletStatelessServerTransport(HttpServerMcpStatelessServerTransport delegate, + McpJsonMapper jsonMapper, String mcpEndpoint) { Assert.notNull(jsonMapper, "jsonMapper must not be null"); Assert.notNull(mcpEndpoint, "mcpEndpoint must not be null"); - Assert.notNull(contextExtractor, "contextExtractor must not be null"); - + this.delegate = delegate; this.jsonMapper = jsonMapper; this.mcpEndpoint = mcpEndpoint; - this.contextExtractor = contextExtractor; } @Override public void setMcpHandler(McpStatelessServerHandler mcpHandler) { - this.mcpHandler = mcpHandler; + this.delegate.setMcpHandler(mcpHandler); } @Override @@ -108,105 +107,36 @@ protected void doGet(HttpServletRequest request, HttpServletResponse response) * @throws IOException If an I/O error occurs */ @Override - protected void doPost(HttpServletRequest request, HttpServletResponse response) - throws ServletException, IOException { - + protected void doPost(HttpServletRequest request, HttpServletResponse response) throws IOException { String requestURI = request.getRequestURI(); if (!requestURI.endsWith(mcpEndpoint)) { response.sendError(HttpServletResponse.SC_NOT_FOUND); return; } - - if (isClosing) { - response.sendError(HttpServletResponse.SC_SERVICE_UNAVAILABLE, "Server is shutting down"); - return; - } - - McpTransportContext transportContext = this.contextExtractor.extract(request); - - String accept = request.getHeader(ACCEPT); - if (accept == null || !(accept.contains(APPLICATION_JSON) && accept.contains(TEXT_EVENT_STREAM))) { - this.responseError(response, HttpServletResponse.SC_BAD_REQUEST, - new McpError("Both application/json and text/event-stream required in Accept header")); - return; - } - - try { - BufferedReader reader = request.getReader(); - StringBuilder body = new StringBuilder(); - String line; - while ((line = reader.readLine()) != null) { - body.append(line); - } - - McpSchema.JSONRPCMessage message = McpSchema.deserializeJsonRpcMessage(jsonMapper, body.toString()); - - if (message instanceof McpSchema.JSONRPCRequest jsonrpcRequest) { - try { - McpSchema.JSONRPCResponse jsonrpcResponse = this.mcpHandler - .handleRequest(transportContext, jsonrpcRequest) - .contextWrite(ctx -> ctx.put(McpTransportContext.KEY, transportContext)) - .block(); - - response.setContentType(APPLICATION_JSON); - response.setCharacterEncoding(UTF_8); - response.setStatus(HttpServletResponse.SC_OK); - - String jsonResponseText = jsonMapper.writeValueAsString(jsonrpcResponse); - PrintWriter writer = response.getWriter(); - writer.write(jsonResponseText); - writer.flush(); - } - catch (Exception e) { - logger.error("Failed to handle request: {}", e.getMessage()); - this.responseError(response, HttpServletResponse.SC_INTERNAL_SERVER_ERROR, - new McpError("Failed to handle request: " + e.getMessage())); - } - } - else if (message instanceof McpSchema.JSONRPCNotification jsonrpcNotification) { - try { - this.mcpHandler.handleNotification(transportContext, jsonrpcNotification) - .contextWrite(ctx -> ctx.put(McpTransportContext.KEY, transportContext)) - .block(); - response.setStatus(HttpServletResponse.SC_ACCEPTED); - } - catch (Exception e) { - logger.error("Failed to handle notification: {}", e.getMessage()); - this.responseError(response, HttpServletResponse.SC_INTERNAL_SERVER_ERROR, - new McpError("Failed to handle notification: " + e.getMessage())); - } - } - else { - this.responseError(response, HttpServletResponse.SC_BAD_REQUEST, - new McpError("The server accepts either requests or notifications")); - } - } - catch (IllegalArgumentException | IOException e) { - logger.error("Failed to deserialize message: {}", e.getMessage()); - this.responseError(response, HttpServletResponse.SC_BAD_REQUEST, new McpError("Invalid message format")); - } - catch (Exception e) { - logger.error("Unexpected error handling message: {}", e.getMessage()); - this.responseError(response, HttpServletResponse.SC_INTERNAL_SERVER_ERROR, - new McpError("Unexpected error: " + e.getMessage())); - } + Map body = jsonMapper.readValue(request.getInputStream().readAllBytes(), MAP_TYPE_REF); + HttpJsonRpcResponse jsonRpcResponse = delegate.handlePost(request, body).block(); + respond(jsonRpcResponse, response); } /** * Sends an error response to the client. + * @param jsonRpcResponse The HTTP JSON RPC response * @param response The HTTP servlet response - * @param httpCode The HTTP status code - * @param mcpError The MCP error to send * @throws IOException If an I/O error occurs */ - private void responseError(HttpServletResponse response, int httpCode, McpError mcpError) throws IOException { + private void respond(HttpJsonRpcResponse jsonRpcResponse, HttpServletResponse response) throws IOException { response.setContentType(APPLICATION_JSON); response.setCharacterEncoding(UTF_8); - response.setStatus(httpCode); - String jsonError = jsonMapper.writeValueAsString(mcpError); - PrintWriter writer = response.getWriter(); - writer.write(jsonError); - writer.flush(); + if (jsonRpcResponse != null) { + response.setStatus(jsonRpcResponse.statusCode()); + String jsonResponseText = jsonMapper.writeValueAsString(jsonRpcResponse.body()); + PrintWriter writer = response.getWriter(); + writer.write(jsonResponseText); + writer.flush(); + } + else { + response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); + } } /** @@ -240,8 +170,7 @@ public static class Builder { private String mcpEndpoint = "/mcp"; - private McpTransportContextExtractor contextExtractor = ( - serverRequest) -> McpTransportContext.EMPTY; + private McpTransportContextExtractor contextExtractor; private Builder() { // used by a static method @@ -296,8 +225,10 @@ public Builder contextExtractor(McpTransportContextExtractor */ public HttpServletStatelessServerTransport build() { Assert.notNull(mcpEndpoint, "Message endpoint must be set"); - return new HttpServletStatelessServerTransport(jsonMapper == null ? McpJsonMapper.getDefault() : jsonMapper, - mcpEndpoint, contextExtractor); + return new HttpServletStatelessServerTransport( + new HttpServerMcpStatelessServerTransport<>(contextExtractor == null + ? new HttpServletRequestMcpTransportContextExtractor() : contextExtractor), + jsonMapper == null ? McpJsonMapper.getDefault() : jsonMapper, mcpEndpoint); } } diff --git a/mcp-core/src/main/java/io/modelcontextprotocol/server/transport/HttpServletStreamableServerTransportProvider.java b/mcp-core/src/main/java/io/modelcontextprotocol/server/transport/HttpServletStreamableServerTransportProvider.java index 34671c105..58da9cae8 100644 --- a/mcp-core/src/main/java/io/modelcontextprotocol/server/transport/HttpServletStreamableServerTransportProvider.java +++ b/mcp-core/src/main/java/io/modelcontextprotocol/server/transport/HttpServletStreamableServerTransportProvider.java @@ -1,7 +1,6 @@ /* * Copyright 2024-2024 the original author or authors. */ - package io.modelcontextprotocol.server.transport; import java.io.BufferedReader; @@ -13,6 +12,7 @@ import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.locks.ReentrantLock; +import io.modelcontextprotocol.server.servlet.HttpServletRequestMcpTransportContextExtractor; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -769,8 +769,7 @@ public static class Builder { private boolean disallowDelete = false; - private McpTransportContextExtractor contextExtractor = ( - serverRequest) -> McpTransportContext.EMPTY; + private McpTransportContextExtractor contextExtractor; private Duration keepAliveInterval; @@ -843,7 +842,8 @@ public HttpServletStreamableServerTransportProvider build() { Assert.notNull(this.mcpEndpoint, "MCP endpoint must be set"); return new HttpServletStreamableServerTransportProvider( jsonMapper == null ? McpJsonMapper.getDefault() : jsonMapper, mcpEndpoint, disallowDelete, - contextExtractor, keepAliveInterval); + contextExtractor == null ? new HttpServletRequestMcpTransportContextExtractor() : contextExtractor, + keepAliveInterval); } } diff --git a/mcp-core/src/test/java/io/modelcontextprotocol/common/AsyncServerMcpTransportContextIntegrationTests.java b/mcp-core/src/test/java/io/modelcontextprotocol/common/AsyncServerMcpTransportContextIntegrationTests.java index 8b2dea462..1faaaa32e 100644 --- a/mcp-core/src/test/java/io/modelcontextprotocol/common/AsyncServerMcpTransportContextIntegrationTests.java +++ b/mcp-core/src/test/java/io/modelcontextprotocol/common/AsyncServerMcpTransportContextIntegrationTests.java @@ -18,6 +18,7 @@ import io.modelcontextprotocol.server.McpServerFeatures; import io.modelcontextprotocol.server.McpStatelessServerFeatures; import io.modelcontextprotocol.server.McpTransportContextExtractor; +import io.modelcontextprotocol.server.servlet.HttpServletRequestMcpTransportContextExtractor; import io.modelcontextprotocol.server.transport.HttpServletSseServerTransportProvider; import io.modelcontextprotocol.server.transport.HttpServletStatelessServerTransport; import io.modelcontextprotocol.server.transport.HttpServletStreamableServerTransportProvider; @@ -91,10 +92,16 @@ public class AsyncServerMcpTransportContextIntegrationTests { return Mono.just(builder); }; - private final McpTransportContextExtractor serverContextExtractor = (HttpServletRequest r) -> { - var headerValue = r.getHeader(HEADER_NAME); - return headerValue != null ? McpTransportContext.create(Map.of("server-side-header-value", headerValue)) - : McpTransportContext.EMPTY; + private final McpTransportContextExtractor serverContextExtractor = new HttpServletRequestMcpTransportContextExtractor() { + @Override + protected Map metadata(HttpServletRequest r) { + Map m = super.metadata(r); + var headerValue = r.getHeader(HEADER_NAME); + if (headerValue != null) { + m.put("server-side-header-value", headerValue); + } + return m; + } }; private final HttpServletStatelessServerTransport statelessServerTransport = HttpServletStatelessServerTransport diff --git a/mcp-core/src/test/java/io/modelcontextprotocol/common/SyncServerMcpTransportContextIntegrationTests.java b/mcp-core/src/test/java/io/modelcontextprotocol/common/SyncServerMcpTransportContextIntegrationTests.java index cc8f4c4be..a4d0b646a 100644 --- a/mcp-core/src/test/java/io/modelcontextprotocol/common/SyncServerMcpTransportContextIntegrationTests.java +++ b/mcp-core/src/test/java/io/modelcontextprotocol/common/SyncServerMcpTransportContextIntegrationTests.java @@ -15,6 +15,7 @@ import io.modelcontextprotocol.server.McpStatelessServerFeatures; import io.modelcontextprotocol.server.McpSyncServerExchange; import io.modelcontextprotocol.server.McpTransportContextExtractor; +import io.modelcontextprotocol.server.servlet.HttpServletRequestMcpTransportContextExtractor; import io.modelcontextprotocol.server.transport.HttpServletSseServerTransportProvider; import io.modelcontextprotocol.server.transport.HttpServletStatelessServerTransport; import io.modelcontextprotocol.server.transport.HttpServletStreamableServerTransportProvider; @@ -71,10 +72,16 @@ public class SyncServerMcpTransportContextIntegrationTests { } }; - private final McpTransportContextExtractor serverContextExtractor = (HttpServletRequest r) -> { - var headerValue = r.getHeader(HEADER_NAME); - return headerValue != null ? McpTransportContext.create(Map.of("server-side-header-value", headerValue)) - : McpTransportContext.EMPTY; + private final McpTransportContextExtractor serverContextExtractor = new HttpServletRequestMcpTransportContextExtractor() { + @Override + protected Map metadata(HttpServletRequest r) { + Map m = super.metadata(r); + var headerValue = r.getHeader(HEADER_NAME); + if (headerValue != null) { + m.put("server-side-header-value", headerValue); + } + return m; + } }; private final BiFunction statelessHandler = ( diff --git a/mcp-core/src/test/java/io/modelcontextprotocol/server/HttpServletSseIntegrationTests.java b/mcp-core/src/test/java/io/modelcontextprotocol/server/HttpServletSseIntegrationTests.java index d2b9d14d0..43c419f5c 100644 --- a/mcp-core/src/test/java/io/modelcontextprotocol/server/HttpServletSseIntegrationTests.java +++ b/mcp-core/src/test/java/io/modelcontextprotocol/server/HttpServletSseIntegrationTests.java @@ -13,6 +13,7 @@ import io.modelcontextprotocol.common.McpTransportContext; import io.modelcontextprotocol.server.McpServer.AsyncSpecification; import io.modelcontextprotocol.server.McpServer.SyncSpecification; +import io.modelcontextprotocol.server.servlet.HttpServletRequestMcpTransportContextExtractor; import io.modelcontextprotocol.server.transport.HttpServletSseServerTransportProvider; import io.modelcontextprotocol.server.transport.TomcatTestUtil; import jakarta.servlet.http.HttpServletRequest; @@ -98,7 +99,13 @@ public void after() { protected void prepareClients(int port, String mcpEndpoint) { } - static McpTransportContextExtractor TEST_CONTEXT_EXTRACTOR = (r) -> McpTransportContext - .create(Map.of("important", "value")); + static McpTransportContextExtractor TEST_CONTEXT_EXTRACTOR = new HttpServletRequestMcpTransportContextExtractor() { + @Override + protected Map metadata(HttpServletRequest r) { + Map m = super.metadata(r); + m.putAll(Map.of("important", "value")); + return m; + } + }; } diff --git a/mcp-core/src/test/java/io/modelcontextprotocol/server/HttpServletStreamableIntegrationTests.java b/mcp-core/src/test/java/io/modelcontextprotocol/server/HttpServletStreamableIntegrationTests.java index 81423e0c5..f13e151fd 100644 --- a/mcp-core/src/test/java/io/modelcontextprotocol/server/HttpServletStreamableIntegrationTests.java +++ b/mcp-core/src/test/java/io/modelcontextprotocol/server/HttpServletStreamableIntegrationTests.java @@ -13,6 +13,7 @@ import io.modelcontextprotocol.common.McpTransportContext; import io.modelcontextprotocol.server.McpServer.AsyncSpecification; import io.modelcontextprotocol.server.McpServer.SyncSpecification; +import io.modelcontextprotocol.server.servlet.HttpServletRequestMcpTransportContextExtractor; import io.modelcontextprotocol.server.transport.HttpServletStreamableServerTransportProvider; import io.modelcontextprotocol.server.transport.TomcatTestUtil; import jakarta.servlet.http.HttpServletRequest; @@ -96,7 +97,13 @@ public void after() { protected void prepareClients(int port, String mcpEndpoint) { } - static McpTransportContextExtractor TEST_CONTEXT_EXTRACTOR = (r) -> McpTransportContext - .create(Map.of("important", "value")); + static McpTransportContextExtractor TEST_CONTEXT_EXTRACTOR = new HttpServletRequestMcpTransportContextExtractor() { + @Override + protected Map metadata(HttpServletRequest r) { + Map m = super.metadata(r); + m.putAll(Map.of("important", "value")); + return m; + } + }; } diff --git a/mcp-core/src/test/java/io/modelcontextprotocol/server/httpserver/HttpExchangeMcpTransportContextExtractorTests.java b/mcp-core/src/test/java/io/modelcontextprotocol/server/httpserver/HttpExchangeMcpTransportContextExtractorTests.java new file mode 100644 index 000000000..e8999149e --- /dev/null +++ b/mcp-core/src/test/java/io/modelcontextprotocol/server/httpserver/HttpExchangeMcpTransportContextExtractorTests.java @@ -0,0 +1,72 @@ +package io.modelcontextprotocol.server.httpserver; + +import com.sun.net.httpserver.Headers; +import com.sun.net.httpserver.HttpExchange; +import io.modelcontextprotocol.common.McpTransportContext; +import io.modelcontextprotocol.spec.HttpHeaders; +import io.modelcontextprotocol.spec.ProtocolVersions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +class HttpExchangeMcpTransportContextExtractorTests { + + @Test + @DisplayName("extract() includes all provided headers in metadata") + void extractIncludesAllHeaders() { + Headers headers = new Headers(); + headers.add(HttpHeaders.PROTOCOL_VERSION, "2025-03-26"); + headers.add(HttpHeaders.MCP_SESSION_ID, "session-123"); + headers.add(HttpHeaders.LAST_EVENT_ID, "evt-9"); + + HttpExchange exchange = mock(HttpExchange.class); + when(exchange.getRequestHeaders()).thenReturn(headers); + HttpExchangeMcpTransportContextExtractor extractor = new HttpExchangeMcpTransportContextExtractor(); + + McpTransportContext ctx = extractor.extract(exchange); + + assertEquals("2025-03-26", ctx.get(HttpHeaders.PROTOCOL_VERSION)); + assertEquals("session-123", ctx.get(HttpHeaders.MCP_SESSION_ID)); + assertEquals("evt-9", ctx.get(HttpHeaders.LAST_EVENT_ID)); + + verify(exchange, times(1)).getRequestHeaders(); + verifyNoMoreInteractions(exchange); + } + + @Test + @DisplayName("extract() defaults protocol version when header missing") + void extractDefaultsProtocolVersion() { + Headers headers = new Headers(); + HttpExchange exchange = mock(HttpExchange.class); + when(exchange.getRequestHeaders()).thenReturn(headers); + HttpExchangeMcpTransportContextExtractor extractor = new HttpExchangeMcpTransportContextExtractor(); + + McpTransportContext ctx = extractor.extract(exchange); + + assertEquals(ProtocolVersions.MCP_2025_03_26, ctx.get(HttpHeaders.PROTOCOL_VERSION)); + + verify(exchange, times(1)).getRequestHeaders(); + verifyNoMoreInteractions(exchange); + } + + @Test + @DisplayName("extract() omits optional headers when not present") + void extractOmitsOptionalHeaders() { + Headers headers = new Headers(); + headers.add(HttpHeaders.PROTOCOL_VERSION, ProtocolVersions.MCP_2025_03_26); + HttpExchange exchange = mock(HttpExchange.class); + when(exchange.getRequestHeaders()).thenReturn(headers); + HttpExchangeMcpTransportContextExtractor extractor = new HttpExchangeMcpTransportContextExtractor(); + + McpTransportContext ctx = extractor.extract(exchange); + + assertNull(ctx.get(HttpHeaders.MCP_SESSION_ID)); + assertNull(ctx.get(HttpHeaders.LAST_EVENT_ID)); + + verify(exchange, times(1)).getRequestHeaders(); + verifyNoMoreInteractions(exchange); + } + +} \ No newline at end of file diff --git a/mcp-core/src/test/java/io/modelcontextprotocol/server/servlet/HttpServletRequestMcpTransportContextExtractorTests.java b/mcp-core/src/test/java/io/modelcontextprotocol/server/servlet/HttpServletRequestMcpTransportContextExtractorTests.java new file mode 100644 index 000000000..a248c1000 --- /dev/null +++ b/mcp-core/src/test/java/io/modelcontextprotocol/server/servlet/HttpServletRequestMcpTransportContextExtractorTests.java @@ -0,0 +1,81 @@ +package io.modelcontextprotocol.server.servlet; + +import io.modelcontextprotocol.common.McpTransportContext; +import io.modelcontextprotocol.spec.HttpHeaders; +import io.modelcontextprotocol.spec.ProtocolVersions; +import jakarta.servlet.http.HttpServletRequest; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +class HttpServletRequestMcpTransportContextExtractorTests { + + @Test + @DisplayName("extract() includes all provided headers in metadata") + void extractIncludesAllHeaders() { + HttpServletRequest request = mock(HttpServletRequest.class); + when(request.getHeader(HttpHeaders.PROTOCOL_VERSION)).thenReturn("2025-03-26"); + when(request.getHeader(HttpHeaders.MCP_SESSION_ID)).thenReturn("session-abc"); + when(request.getHeader(HttpHeaders.LAST_EVENT_ID)).thenReturn("evt-42"); + + HttpServletRequestMcpTransportContextExtractor extractor = new HttpServletRequestMcpTransportContextExtractor(); + + McpTransportContext ctx = extractor.extract(request); + + assertEquals("2025-03-26", ctx.get(HttpHeaders.PROTOCOL_VERSION)); + assertEquals("session-abc", ctx.get(HttpHeaders.MCP_SESSION_ID)); + assertEquals("evt-42", ctx.get(HttpHeaders.LAST_EVENT_ID)); + + verify(request, times(1)).getHeader(HttpHeaders.PROTOCOL_VERSION); + verify(request, times(1)).getHeader(HttpHeaders.MCP_SESSION_ID); + verify(request, times(1)).getHeader(HttpHeaders.LAST_EVENT_ID); + verifyNoMoreInteractions(request); + } + + @Test + @DisplayName("extract() defaults protocol version when header missing") + void extractDefaultsProtocolVersion() { + HttpServletRequest request = mock(HttpServletRequest.class); + when(request.getHeader(HttpHeaders.PROTOCOL_VERSION)).thenReturn(null); + when(request.getHeader(HttpHeaders.MCP_SESSION_ID)).thenReturn(null); + when(request.getHeader(HttpHeaders.LAST_EVENT_ID)).thenReturn(null); + + HttpServletRequestMcpTransportContextExtractor extractor = new HttpServletRequestMcpTransportContextExtractor(); + + McpTransportContext ctx = extractor.extract(request); + + assertEquals(ProtocolVersions.MCP_2025_03_26, ctx.get(HttpHeaders.PROTOCOL_VERSION)); + assertNull(ctx.get(HttpHeaders.MCP_SESSION_ID)); + assertNull(ctx.get(HttpHeaders.LAST_EVENT_ID)); + + verify(request, times(1)).getHeader(HttpHeaders.PROTOCOL_VERSION); + verify(request, times(1)).getHeader(HttpHeaders.MCP_SESSION_ID); + verify(request, times(1)).getHeader(HttpHeaders.LAST_EVENT_ID); + verifyNoMoreInteractions(request); + } + + @Test + @DisplayName("extract() omits optional headers when not present") + void extractOmitsOptionalHeaders() { + HttpServletRequest request = mock(HttpServletRequest.class); + when(request.getHeader(HttpHeaders.PROTOCOL_VERSION)).thenReturn(ProtocolVersions.MCP_2025_03_26); + when(request.getHeader(HttpHeaders.MCP_SESSION_ID)).thenReturn(null); + when(request.getHeader(HttpHeaders.LAST_EVENT_ID)).thenReturn(null); + + HttpServletRequestMcpTransportContextExtractor extractor = new HttpServletRequestMcpTransportContextExtractor(); + + McpTransportContext ctx = extractor.extract(request); + + assertEquals(ProtocolVersions.MCP_2025_03_26, ctx.get(HttpHeaders.PROTOCOL_VERSION)); + assertNull(ctx.get(HttpHeaders.MCP_SESSION_ID)); + assertNull(ctx.get(HttpHeaders.LAST_EVENT_ID)); + + verify(request, times(1)).getHeader(HttpHeaders.PROTOCOL_VERSION); + verify(request, times(1)).getHeader(HttpHeaders.MCP_SESSION_ID); + verify(request, times(1)).getHeader(HttpHeaders.LAST_EVENT_ID); + verifyNoMoreInteractions(request); + } + +} diff --git a/mcp-tck-http-httpserver-async/pom.xml b/mcp-tck-http-httpserver-async/pom.xml new file mode 100644 index 000000000..9772c0ec6 --- /dev/null +++ b/mcp-tck-http-httpserver-async/pom.xml @@ -0,0 +1,62 @@ + + + 4.0.0 + + io.modelcontextprotocol.sdk + mcp-parent + 0.14.0-SNAPSHOT + + mcp-tck-http-httpserver-async + MCP TCK HTTP Built-in HTTP Server Async + https://github.com/modelcontextprotocol/java-sdk + + https://github.com/modelcontextprotocol/java-sdk + git://github.com/modelcontextprotocol/java-sdk.git + git@github.com/modelcontextprotocol/java-sdk.git + + + + + org.apache.maven.plugins + maven-surefire-plugin + ${maven-surefire-plugin.version} + + + **/*Suite.java + + + + + + + + io.modelcontextprotocol.sdk + mcp-tck-http + 0.14.0-SNAPSHOT + + + io.modelcontextprotocol.sdk + mcp + 0.14.0-SNAPSHOT + + + org.junit.platform + junit-platform-suite + ${junit.platform.version} + test + + + org.junit.jupiter + junit-jupiter + ${junit.version} + + + org.mockito + mockito-core + ${mockito.version} + test + + + diff --git a/mcp-tck-http-httpserver-async/src/test/java/io/modelcontextprotocol/server/http/tck/httpserver/async/AsyncMcpHttpServerSupplier.java b/mcp-tck-http-httpserver-async/src/test/java/io/modelcontextprotocol/server/http/tck/httpserver/async/AsyncMcpHttpServerSupplier.java new file mode 100644 index 000000000..d1905e79b --- /dev/null +++ b/mcp-tck-http-httpserver-async/src/test/java/io/modelcontextprotocol/server/http/tck/httpserver/async/AsyncMcpHttpServerSupplier.java @@ -0,0 +1,89 @@ +package io.modelcontextprotocol.server.http.tck.httpserver.async; + +import com.sun.net.httpserver.HttpExchange; +import io.modelcontextprotocol.server.transport.HttpServerMcpStatelessServerTransport; +import io.modelcontextprotocol.server.McpHttpServer; +import io.modelcontextprotocol.server.McpHttpServerSupplier; +import io.modelcontextprotocol.server.httpserver.HttpExchangeMcpTransportContextExtractor; +import io.modelcontextprotocol.server.httpserver.McpSimpleHttpServer; +import io.modelcontextprotocol.common.McpTransportContext; +import io.modelcontextprotocol.json.McpJsonMapper; +import io.modelcontextprotocol.json.schema.JsonSchemaValidator; +import io.modelcontextprotocol.server.McpStatelessServerFeatures; +import io.modelcontextprotocol.spec.McpSchema; +import io.modelcontextprotocol.server.McpServer; +import reactor.core.publisher.Mono; + +import java.io.IOException; +import java.util.List; +import java.util.Map; +import java.util.function.BiFunction; + +import static io.modelcontextprotocol.spec.McpSchema.*; +import static io.modelcontextprotocol.spec.McpSchema.Role.USER; + +public class AsyncMcpHttpServerSupplier implements McpHttpServerSupplier { + + @Override + public McpHttpServer get() { + try { + McpJsonMapper jsonMapper = McpJsonMapper.getDefault(); + HttpServerMcpStatelessServerTransport transport = new HttpServerMcpStatelessServerTransport( + new HttpExchangeMcpTransportContextExtractor()); + McpServer.StatelessAsyncSpecification spec = McpServer.async(transport) + .jsonSchemaValidator(JsonSchemaValidator.getDefault()) + .jsonMapper(jsonMapper) + .tools(McpStatelessServerFeatures.AsyncToolSpecification.builder() + .callHandler(new BiFunction>() { + @Override + public Mono apply(McpTransportContext mcpTransportContext, + CallToolRequest callToolRequest) { + return Mono.just(new McpSchema.CallToolResult("Sunny", false)); + } + }) + .tool(Tool.builder() + .name("get_weather") + .title("Weather Information Provider") + .description("Get current weather information for a location") + .inputSchema(new JsonSchema("object", + Map.of("location", Map.of("type", "string", "description", "City name or zip code")), + List.of("location"), null, null, null)) + .build()) + .build()) + .serverInfo("mcp-server", "0.0.1") + .prompts(new McpStatelessServerFeatures.AsyncPromptSpecification( + new McpSchema.Prompt("code_review", "Request Code Review", + "Asks the LLM to analyze code quality and suggest improvements", + List.of(new McpSchema.PromptArgument("code", "The code to review", true))), + (mcpTransportContext, + getPromptRequest) -> Mono.just(new McpSchema.GetPromptResult("Code review prompt", + List.of(new McpSchema.PromptMessage(USER, + new McpSchema.TextContent("Please review this Python code"))))))) + .resources(new McpStatelessServerFeatures.AsyncResourceSpecification( + McpSchema.Resource.builder() + .uri("file:///project/src/main.rs") + .name("main.rs") + .title("Rust Software Application Main File") + .description("Primary application entry point") + .mimeType("text/x-rust") + .build(), + (mcpTransportContext, + readResourceRequest) -> Mono.just(new McpSchema.ReadResourceResult( + List.of(new McpSchema.TextResourceContents("file:///project/src/main.rs", + "text/x-rust", "fn main() {\n println!(\"Hello world!\");\n}")))))) + .capabilities(McpSchema.ServerCapabilities.builder() + .tools(false) + .prompts(false) + .resources(false, false) + .build()); + spec.build(); + McpHttpServer server = new McpSimpleHttpServer(transport, jsonMapper); + server.start(); + return server; + } + catch (IOException e) { + throw new RuntimeException(e); + } + } + +} diff --git a/mcp-tck-http-httpserver-async/src/test/java/io/modelcontextprotocol/server/http/tck/httpserver/async/HttpServerAsyncSuite.java b/mcp-tck-http-httpserver-async/src/test/java/io/modelcontextprotocol/server/http/tck/httpserver/async/HttpServerAsyncSuite.java new file mode 100644 index 000000000..414af62ef --- /dev/null +++ b/mcp-tck-http-httpserver-async/src/test/java/io/modelcontextprotocol/server/http/tck/httpserver/async/HttpServerAsyncSuite.java @@ -0,0 +1,12 @@ +package io.modelcontextprotocol.server.http.tck.httpserver.async; + +import org.junit.platform.suite.api.SelectPackages; +import org.junit.platform.suite.api.Suite; +import org.junit.platform.suite.api.SuiteDisplayName; + +@SelectPackages({ "io.modelcontextprotocol.server.http.tck" }) +@Suite +@SuiteDisplayName("MCP HTTP Server TCK for Java built-in HTTP Server Async") +public class HttpServerAsyncSuite { + +} diff --git a/mcp-tck-http-httpserver-async/src/test/resources/META-INF/services/io.modelcontextprotocol.server.McpHttpServerSupplier b/mcp-tck-http-httpserver-async/src/test/resources/META-INF/services/io.modelcontextprotocol.server.McpHttpServerSupplier new file mode 100644 index 000000000..6722e50ec --- /dev/null +++ b/mcp-tck-http-httpserver-async/src/test/resources/META-INF/services/io.modelcontextprotocol.server.McpHttpServerSupplier @@ -0,0 +1 @@ +io.modelcontextprotocol.server.http.tck.httpserver.async.AsyncMcpHttpServerSupplier diff --git a/mcp-tck-http-httpserver-sync/pom.xml b/mcp-tck-http-httpserver-sync/pom.xml new file mode 100644 index 000000000..545089480 --- /dev/null +++ b/mcp-tck-http-httpserver-sync/pom.xml @@ -0,0 +1,62 @@ + + + 4.0.0 + + io.modelcontextprotocol.sdk + mcp-parent + 0.14.0-SNAPSHOT + + mcp-tck-http-httpserver-sync + MCP TCK HTTP Built-in HTTP Server Sync + https://github.com/modelcontextprotocol/java-sdk + + https://github.com/modelcontextprotocol/java-sdk + git://github.com/modelcontextprotocol/java-sdk.git + git@github.com/modelcontextprotocol/java-sdk.git + + + + + org.apache.maven.plugins + maven-surefire-plugin + ${maven-surefire-plugin.version} + + + **/*Suite.java + + + + + + + + io.modelcontextprotocol.sdk + mcp-tck-http + 0.14.0-SNAPSHOT + + + io.modelcontextprotocol.sdk + mcp + 0.14.0-SNAPSHOT + + + org.junit.platform + junit-platform-suite + ${junit.platform.version} + test + + + org.junit.jupiter + junit-jupiter + ${junit.version} + + + org.mockito + mockito-core + ${mockito.version} + test + + + diff --git a/mcp-tck-http-httpserver-sync/src/test/java/io/modelcontextprotocol/server/httpserver/sync/HttpServerSyncSuite.java b/mcp-tck-http-httpserver-sync/src/test/java/io/modelcontextprotocol/server/httpserver/sync/HttpServerSyncSuite.java new file mode 100644 index 000000000..85541b5b3 --- /dev/null +++ b/mcp-tck-http-httpserver-sync/src/test/java/io/modelcontextprotocol/server/httpserver/sync/HttpServerSyncSuite.java @@ -0,0 +1,12 @@ +package io.modelcontextprotocol.server.httpserver.sync; + +import org.junit.platform.suite.api.SelectPackages; +import org.junit.platform.suite.api.Suite; +import org.junit.platform.suite.api.SuiteDisplayName; + +@SelectPackages("io.modelcontextprotocol.server.http.tck") +@Suite +@SuiteDisplayName("MCP HTTP Server TCK for Java built-in HTTP Server Sync") +public class HttpServerSyncSuite { + +} diff --git a/mcp-tck-http-httpserver-sync/src/test/java/io/modelcontextprotocol/server/httpserver/sync/SyncMcpHttpServerSupplier.java b/mcp-tck-http-httpserver-sync/src/test/java/io/modelcontextprotocol/server/httpserver/sync/SyncMcpHttpServerSupplier.java new file mode 100644 index 000000000..dc5e8ba82 --- /dev/null +++ b/mcp-tck-http-httpserver-sync/src/test/java/io/modelcontextprotocol/server/httpserver/sync/SyncMcpHttpServerSupplier.java @@ -0,0 +1,87 @@ +package io.modelcontextprotocol.server.httpserver.sync; + +import com.sun.net.httpserver.HttpExchange; +import io.modelcontextprotocol.server.transport.HttpServerMcpStatelessServerTransport; +import io.modelcontextprotocol.server.McpHttpServer; +import io.modelcontextprotocol.server.McpHttpServerSupplier; +import io.modelcontextprotocol.server.httpserver.HttpExchangeMcpTransportContextExtractor; +import io.modelcontextprotocol.server.httpserver.McpSimpleHttpServer; +import io.modelcontextprotocol.common.McpTransportContext; +import io.modelcontextprotocol.json.McpJsonMapper; +import io.modelcontextprotocol.json.schema.JsonSchemaValidator; +import io.modelcontextprotocol.server.McpStatelessServerFeatures; +import io.modelcontextprotocol.spec.McpSchema; +import io.modelcontextprotocol.server.McpServer; + +import java.io.IOException; +import java.util.Map; +import java.util.List; +import java.util.function.BiFunction; + +import static io.modelcontextprotocol.spec.McpSchema.Role.USER; + +public class SyncMcpHttpServerSupplier implements McpHttpServerSupplier { + + @Override + public McpHttpServer get() { + try { + McpJsonMapper jsonMapper = McpJsonMapper.getDefault(); + HttpServerMcpStatelessServerTransport transport = new HttpServerMcpStatelessServerTransport( + new HttpExchangeMcpTransportContextExtractor()); + McpServer.StatelessSyncSpecification spec = McpServer.sync(transport) + .jsonSchemaValidator(JsonSchemaValidator.getDefault()) + .jsonMapper(jsonMapper) + .tools(McpStatelessServerFeatures.SyncToolSpecification.builder() + .callHandler( + new BiFunction() { + @Override + public McpSchema.CallToolResult apply(McpTransportContext mcpTransportContext, + McpSchema.CallToolRequest callToolRequest) { + return new McpSchema.CallToolResult("Sunny", false); + } + }) + .tool(McpSchema.Tool.builder() + .name("get_weather") + .title("Weather Information Provider") + .description("Get current weather information for a location") + .inputSchema(new McpSchema.JsonSchema("object", + Map.of("location", Map.of("type", "string", "description", "City name or zip code")), + List.of("location"), null, null, null)) + .build()) + .build()) + .serverInfo("mcp-server", "0.0.1") + .prompts(new McpStatelessServerFeatures.SyncPromptSpecification( + new McpSchema.Prompt("code_review", "Request Code Review", + "Asks the LLM to analyze code quality and suggest improvements", + List.of(new McpSchema.PromptArgument("code", "The code to review", true))), + (mcpTransportContext, getPromptRequest) -> new McpSchema.GetPromptResult("Code review prompt", + List.of(new McpSchema.PromptMessage(USER, + new McpSchema.TextContent("Please review this Python code")))))) + .resources(new McpStatelessServerFeatures.SyncResourceSpecification( + McpSchema.Resource.builder() + .uri("file:///project/src/main.rs") + .name("main.rs") + .title("Rust Software Application Main File") + .description("Primary application entry point") + .mimeType("text/x-rust") + .build(), + (mcpTransportContext, readResourceRequest) -> new McpSchema.ReadResourceResult( + List.of(new McpSchema.TextResourceContents("file:///project/src/main.rs", "text/x-rust", + "fn main() {\n println!(\"Hello world!\");\n}"))))) + .capabilities(McpSchema.ServerCapabilities.builder() + .tools(false) + .prompts(false) + .resources(false, false) + .build()); + spec.build(); + + McpHttpServer server = new McpSimpleHttpServer(transport, jsonMapper); + server.start(); + return server; + } + catch (IOException e) { + throw new RuntimeException(e); + } + } + +} diff --git a/mcp-tck-http-httpserver-sync/src/test/resources/META-INF/services/io.modelcontextprotocol.server.McpHttpServerSupplier b/mcp-tck-http-httpserver-sync/src/test/resources/META-INF/services/io.modelcontextprotocol.server.McpHttpServerSupplier new file mode 100644 index 000000000..887a24edc --- /dev/null +++ b/mcp-tck-http-httpserver-sync/src/test/resources/META-INF/services/io.modelcontextprotocol.server.McpHttpServerSupplier @@ -0,0 +1,2 @@ +io.modelcontextprotocol.server.httpserver.sync.SyncMcpHttpServerSupplier + diff --git a/mcp-tck-http-httpserver-sync/src/test/resources/logback.xml b/mcp-tck-http-httpserver-sync/src/test/resources/logback.xml new file mode 100644 index 000000000..89e7cdfc2 --- /dev/null +++ b/mcp-tck-http-httpserver-sync/src/test/resources/logback.xml @@ -0,0 +1,11 @@ + + + + %cyan(%d{HH:mm:ss.SSS}) %gray([%thread]) %highlight(%-5level) %magenta(%logger{36}) - %msg%n + + + + + + + diff --git a/mcp-tck-http/pom.xml b/mcp-tck-http/pom.xml new file mode 100644 index 000000000..57dc80dc5 --- /dev/null +++ b/mcp-tck-http/pom.xml @@ -0,0 +1,54 @@ + + + 4.0.0 + + io.modelcontextprotocol.sdk + mcp-parent + 0.14.0-SNAPSHOT + + mcp-tck-http + jar + MCP Server TCK + MCP Server Technology Compatibility Kit (TCK) + https://github.com/modelcontextprotocol/java-sdk + + https://github.com/modelcontextprotocol/java-sdk + git://github.com/modelcontextprotocol/java-sdk.git + git@github.com/modelcontextprotocol/java-sdk.git + + + + + org.apache.maven.plugins + maven-jar-plugin + + + + true + + + + + + + + + io.modelcontextprotocol.sdk + mcp-core + 0.14.0-SNAPSHOT + + + org.junit.jupiter + junit-jupiter-api + ${junit.version} + + + org.skyscreamer + jsonassert + 1.5.3 + + + + diff --git a/mcp-tck-http/src/main/java/io/modelcontextprotocol/server/http/tck/HttpRequestUtils.java b/mcp-tck-http/src/main/java/io/modelcontextprotocol/server/http/tck/HttpRequestUtils.java new file mode 100644 index 000000000..511afffe7 --- /dev/null +++ b/mcp-tck-http/src/main/java/io/modelcontextprotocol/server/http/tck/HttpRequestUtils.java @@ -0,0 +1,44 @@ +/* + * Copyright 2017-2025 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.modelcontextprotocol.server.http.tck; + +import io.modelcontextprotocol.server.McpHttpServer; + +import java.net.URI; +import java.net.URISyntaxException; +import java.net.http.HttpRequest; +import java.nio.charset.StandardCharsets; + +/** + * Utils class to instantiate {@link HttpRequest}s. + */ +public final class HttpRequestUtils { + + private HttpRequestUtils() { + + } + + @SuppressWarnings("MethodName") + public static HttpRequest POST(McpHttpServer server, String body) throws URISyntaxException { + URI uri = new URI("http://localhost:" + server.getPort() + server.getEndpoint()); + return HttpRequest.newBuilder(uri) + .POST(HttpRequest.BodyPublishers.ofString(body, StandardCharsets.UTF_8)) + .header("Accept", "application/json") + .header("Content-Type", "application/json") + .build(); + } + +} diff --git a/mcp-tck-http/src/main/java/io/modelcontextprotocol/server/http/tck/InitializeTest.java b/mcp-tck-http/src/main/java/io/modelcontextprotocol/server/http/tck/InitializeTest.java new file mode 100644 index 000000000..a8a474da2 --- /dev/null +++ b/mcp-tck-http/src/main/java/io/modelcontextprotocol/server/http/tck/InitializeTest.java @@ -0,0 +1,71 @@ +/* + * Copyright 2017-2025 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.modelcontextprotocol.server.http.tck; + +import io.modelcontextprotocol.server.McpHttpServer; +import org.junit.jupiter.api.Test; +import org.skyscreamer.jsonassert.JSONAssert; + +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * MCP Server Initialization Phase. Initialization + */ +public class InitializeTest { + + private static final String INITIALIZE = """ + {"jsonrpc":"2.0","id":0,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{"sampling":{},"elicitation":{},"roots":{"listChanged":true}},"clientInfo":{"name":"mcp-inspector","version":"0.16.3"}}}"""; + + private static final String EXPECTED_INITIALIZATION = """ + { + "jsonrpc":"2.0", + "id":0, + "result": { + "protocolVersion":"2025-06-18", + "capabilities": { + "prompts": { + "listChanged": false + }, + "resources": { + "subscribe": false, "listChanged": false + }, + "tools": { + "listChanged": false + } + }, + "serverInfo": { + "name": "mcp-server", + "version": "0.0.1" + } + } + }"""; + + @Test + public void initializeTest() throws Exception { + McpHttpServer server = McpHttpServer.getDefault(); + HttpRequest request = HttpRequestUtils.POST(server, INITIALIZE); + HttpClient client = HttpClient.newBuilder().build(); + HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); + assertEquals(200, response.statusCode()); + JSONAssert.assertEquals(EXPECTED_INITIALIZATION, response.body(), true); + } + +} diff --git a/mcp-tck-http/src/main/java/io/modelcontextprotocol/server/http/tck/PingTest.java b/mcp-tck-http/src/main/java/io/modelcontextprotocol/server/http/tck/PingTest.java new file mode 100644 index 000000000..709863ded --- /dev/null +++ b/mcp-tck-http/src/main/java/io/modelcontextprotocol/server/http/tck/PingTest.java @@ -0,0 +1,50 @@ +/* + * Copyright 2017-2025 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.modelcontextprotocol.server.http.tck; + +import io.modelcontextprotocol.server.McpHttpServer; +import org.junit.jupiter.api.Test; +import org.skyscreamer.jsonassert.JSONAssert; + +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * MCP Server Ping. Ping + */ +public class PingTest { + + public static final String PING = """ + {"jsonrpc":"2.0","method":"ping","id":123}"""; + + public static final String PONG = """ + {"jsonrpc":"2.0","result":{},"id":123}"""; + + @Test + public void pingTest() throws Exception { + McpHttpServer server = McpHttpServer.getDefault(); + HttpRequest request = HttpRequestUtils.POST(server, PING); + HttpClient client = HttpClient.newBuilder().build(); + HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); + assertEquals(200, response.statusCode()); + JSONAssert.assertEquals(PONG, response.body(), true); + } + +} diff --git a/mcp-tck-http/src/main/java/io/modelcontextprotocol/server/http/tck/PromptsGetTest.java b/mcp-tck-http/src/main/java/io/modelcontextprotocol/server/http/tck/PromptsGetTest.java new file mode 100644 index 000000000..c1b98c6d6 --- /dev/null +++ b/mcp-tck-http/src/main/java/io/modelcontextprotocol/server/http/tck/PromptsGetTest.java @@ -0,0 +1,77 @@ +/* + * Copyright 2017-2025 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.modelcontextprotocol.server.http.tck; + +import io.modelcontextprotocol.server.McpHttpServer; +import org.junit.jupiter.api.Test; +import org.skyscreamer.jsonassert.JSONAssert; + +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * MCP Server Getting a Prompt. Getting + * a Prompt + */ +public class PromptsGetTest { + + public static final String PROMPTS_GET = """ + { + "jsonrpc": "2.0", + "id": 2, + "method": "prompts/get", + "params": { + "name": "code_review", + "arguments": { + "code": "def hello():\\n print('world')" + } + } + }"""; + + public static final String PROMPTS_GET_RESULT = """ + { + "jsonrpc": "2.0", + "id": 2, + "result": { + "description": "Code review prompt", + "messages": [ + { + "role": "user", + "content": { + "type": "text", + "text": "Please review this Python code" + } + } + ] + } + }"""; + + @Test + public void promptsListTest() throws Exception { + McpHttpServer server = McpHttpServer.getDefault(); + HttpRequest request = HttpRequestUtils.POST(server, PROMPTS_GET); + HttpClient client = HttpClient.newBuilder().build(); + HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); + assertEquals(200, response.statusCode()); + String json = response.body(); + JSONAssert.assertEquals(PROMPTS_GET_RESULT, json, true); + } + +} diff --git a/mcp-tck-http/src/main/java/io/modelcontextprotocol/server/http/tck/PromptsListTest.java b/mcp-tck-http/src/main/java/io/modelcontextprotocol/server/http/tck/PromptsListTest.java new file mode 100644 index 000000000..19621e7e1 --- /dev/null +++ b/mcp-tck-http/src/main/java/io/modelcontextprotocol/server/http/tck/PromptsListTest.java @@ -0,0 +1,74 @@ +/* + * Copyright 2017-2025 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.modelcontextprotocol.server.http.tck; + +import io.modelcontextprotocol.server.McpHttpServer; +import org.junit.jupiter.api.Test; +import org.skyscreamer.jsonassert.JSONAssert; + +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * MCP Server Prompts. Prompts + */ +public class PromptsListTest { + + public static final String PROMPTS_LIST = """ + { + "jsonrpc": "2.0", + "id": 1, + "method": "prompts/list" + }"""; + + public static final String PROMPTS_LIST_RESULT = """ + { + "jsonrpc": "2.0", + "id": 1, + "result": { + "prompts": [ + { + "name": "code_review", + "title": "Request Code Review", + "description": "Asks the LLM to analyze code quality and suggest improvements", + "arguments": [ + { + "name": "code", + "description": "The code to review", + "required": true + } + ] + } + ] + } + }"""; + + @Test + public void promptsListTest() throws Exception { + McpHttpServer server = McpHttpServer.getDefault(); + HttpRequest request = HttpRequestUtils.POST(server, PROMPTS_LIST); + HttpClient client = HttpClient.newBuilder().build(); + HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); + assertEquals(200, response.statusCode()); + String json = response.body(); + JSONAssert.assertEquals(PROMPTS_LIST_RESULT, json, true); + } + +} diff --git a/mcp-tck-http/src/main/java/io/modelcontextprotocol/server/http/tck/ResourcesGetTest.java b/mcp-tck-http/src/main/java/io/modelcontextprotocol/server/http/tck/ResourcesGetTest.java new file mode 100644 index 000000000..1a2d682bb --- /dev/null +++ b/mcp-tck-http/src/main/java/io/modelcontextprotocol/server/http/tck/ResourcesGetTest.java @@ -0,0 +1,73 @@ +/* + * Copyright 2017-2025 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.modelcontextprotocol.server.http.tck; + +import io.modelcontextprotocol.server.McpHttpServer; +import org.junit.jupiter.api.Test; +import org.skyscreamer.jsonassert.JSONAssert; + +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * MCP Server Resources List. Listing + * resources + */ +public class ResourcesGetTest { + + public static final String RESOURCES_GET = """ + { + "jsonrpc": "2.0", + "id": 2, + "method": "resources/read", + "params": { + "uri": "file:///project/src/main.rs" + } + }"""; + + public static final String RESOURCES_GET_RESULT = """ + { + "jsonrpc": "2.0", + "id": 2, + "result": { + "contents": [ + { + "uri": "file:///project/src/main.rs", + //"name": "main.rs", + //"title": "Rust Software Application Main File", + "mimeType": "text/x-rust", + "text": "fn main() {\\n println!(\\"Hello world!\\");\\n}" + } + ] + } + }"""; + + @Test + public void resourcesGet() throws Exception { + McpHttpServer server = McpHttpServer.getDefault(); + HttpRequest request = HttpRequestUtils.POST(server, RESOURCES_GET); + HttpClient client = HttpClient.newBuilder().build(); + HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); + assertEquals(200, response.statusCode()); + String json = response.body(); + JSONAssert.assertEquals(RESOURCES_GET_RESULT, json, true); + } + +} diff --git a/mcp-tck-http/src/main/java/io/modelcontextprotocol/server/http/tck/ResourcesListTest.java b/mcp-tck-http/src/main/java/io/modelcontextprotocol/server/http/tck/ResourcesListTest.java new file mode 100644 index 000000000..25e0cfd44 --- /dev/null +++ b/mcp-tck-http/src/main/java/io/modelcontextprotocol/server/http/tck/ResourcesListTest.java @@ -0,0 +1,70 @@ +/* + * Copyright 2017-2025 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.modelcontextprotocol.server.http.tck; + +import io.modelcontextprotocol.server.McpHttpServer; +import org.junit.jupiter.api.Test; +import org.skyscreamer.jsonassert.JSONAssert; + +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * MCP Server Resources List. Listing + * resources + */ +public class ResourcesListTest { + + public static final String RESOURCES_LIST = """ + { + "jsonrpc": "2.0", + "id": 1, + "method": "resources/list" + }"""; + + public static final String RESOURCES_LIST_RESULT = """ + { + "jsonrpc": "2.0", + "id": 1, + "result": { + "resources": [ + { + "uri": "file:///project/src/main.rs", + "name": "main.rs", + "title": "Rust Software Application Main File", + "description": "Primary application entry point", + "mimeType": "text/x-rust" + } + ] + } + }"""; + + @Test + public void resourcesListTest() throws Exception { + McpHttpServer server = McpHttpServer.getDefault(); + HttpRequest request = HttpRequestUtils.POST(server, RESOURCES_LIST); + HttpClient client = HttpClient.newBuilder().build(); + HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); + assertEquals(200, response.statusCode()); + String json = response.body(); + JSONAssert.assertEquals(RESOURCES_LIST_RESULT, json, true); + } + +} diff --git a/mcp-tck-http/src/main/java/io/modelcontextprotocol/server/http/tck/SuiteShutdownExtension.java b/mcp-tck-http/src/main/java/io/modelcontextprotocol/server/http/tck/SuiteShutdownExtension.java new file mode 100644 index 000000000..bcb41af15 --- /dev/null +++ b/mcp-tck-http/src/main/java/io/modelcontextprotocol/server/http/tck/SuiteShutdownExtension.java @@ -0,0 +1,56 @@ +/* + * Copyright 2017-2025 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.modelcontextprotocol.server.http.tck; + +import io.modelcontextprotocol.server.McpHttpServer; +import org.junit.jupiter.api.extension.AfterAllCallback; +import org.junit.jupiter.api.extension.ExtensionContext; + +/** + * Global JUnit Jupiter extension that closes the default McpHttpServer once the entire + * test plan (suite) finishes. + */ +public final class SuiteShutdownExtension implements AfterAllCallback { + + private static final ExtensionContext.Namespace NS = ExtensionContext.Namespace + .create(SuiteShutdownExtension.class); + + @Override + public void afterAll(ExtensionContext context) { + context.getRoot().getStore(NS).getOrComputeIfAbsent(Closer.class, key -> new Closer()); + } + + static final class Closer implements ExtensionContext.Store.CloseableResource { + + private static volatile boolean closed; + + @Override + public void close() { + if (closed) { + return; + } + closed = true; + try { + McpHttpServer.getDefault().close(); + } + catch (Exception e) { + throw new RuntimeException("Failed to close McpHttpServer", e); + } + } + + } + +} diff --git a/mcp-tck-http/src/main/java/io/modelcontextprotocol/server/http/tck/ToolsCallTest.java b/mcp-tck-http/src/main/java/io/modelcontextprotocol/server/http/tck/ToolsCallTest.java new file mode 100644 index 000000000..2cb7ba1bd --- /dev/null +++ b/mcp-tck-http/src/main/java/io/modelcontextprotocol/server/http/tck/ToolsCallTest.java @@ -0,0 +1,73 @@ +/* + * Copyright 2017-2025 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.modelcontextprotocol.server.http.tck; + +import io.modelcontextprotocol.server.McpHttpServer; +import org.junit.jupiter.api.Test; +import org.skyscreamer.jsonassert.JSONAssert; + +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * MCP Server Tools Call. Calling + * Tools + */ +public class ToolsCallTest { + + public static final String TOOLS_CALL = """ + { + "jsonrpc": "2.0", + "id": 2, + "method": "tools/call", + "params": { + "name": "get_weather", + "arguments": { + "location": "New York" + } + } + }"""; + + public static final String TOOLS_CALL_RESULT = """ + { + "jsonrpc": "2.0", + "id": 2, + "result": { + "content": [ + { + "type": "text", + "text": "Sunny" + } + ], + "isError": false + } + }"""; + + @Test + public void toolsCallTest() throws Exception { + McpHttpServer server = McpHttpServer.getDefault(); + HttpRequest request = HttpRequestUtils.POST(server, TOOLS_CALL); + HttpClient client = HttpClient.newBuilder().build(); + HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); + assertEquals(200, response.statusCode()); + JSONAssert.assertEquals(TOOLS_CALL_RESULT, response.body(), true); + } + +} diff --git a/mcp-tck-http/src/main/java/io/modelcontextprotocol/server/http/tck/ToolsTest.java b/mcp-tck-http/src/main/java/io/modelcontextprotocol/server/http/tck/ToolsTest.java new file mode 100644 index 000000000..8b390581e --- /dev/null +++ b/mcp-tck-http/src/main/java/io/modelcontextprotocol/server/http/tck/ToolsTest.java @@ -0,0 +1,72 @@ +/* + * Copyright 2017-2025 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.modelcontextprotocol.server.http.tck; + +import io.modelcontextprotocol.server.McpHttpServer; +import org.junit.jupiter.api.Test; +import org.skyscreamer.jsonassert.JSONAssert; + +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * MCP Server Tools. Tools + */ +public class ToolsTest { + + public static final String TOOLS_LIST = """ + {"jsonrpc":"2.0","method":"tools/list","id":123}"""; + + public static final String TOOLS_LIST_RESULT = """ + { + "jsonrpc": "2.0", + "id": 123, + "result": { + "tools": [ + { + "name": "get_weather", + "title": "Weather Information Provider", + "description": "Get current weather information for a location", + "inputSchema": { + "type": "object", + "properties": { + "location": { + "type": "string", + "description": "City name or zip code" + } + }, + "required": ["location"] + } + } + ] + } + }"""; + + @Test + public void toolsListTest() throws Exception { + McpHttpServer server = McpHttpServer.getDefault(); + HttpRequest request = HttpRequestUtils.POST(server, TOOLS_LIST); + HttpClient client = HttpClient.newBuilder().build(); + HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); + assertEquals(200, response.statusCode()); + JSONAssert.assertEquals(TOOLS_LIST_RESULT, response.body(), true); + } + +} diff --git a/mcp-tck-http/src/main/resources/META-INF/services/org.junit.jupiter.api.extension.Extension b/mcp-tck-http/src/main/resources/META-INF/services/org.junit.jupiter.api.extension.Extension new file mode 100644 index 000000000..3f6556106 --- /dev/null +++ b/mcp-tck-http/src/main/resources/META-INF/services/org.junit.jupiter.api.extension.Extension @@ -0,0 +1,2 @@ +io.modelcontextprotocol.server.http.tck.SuiteShutdownExtension + diff --git a/mcp-tck-http/src/main/resources/junit-platform.properties b/mcp-tck-http/src/main/resources/junit-platform.properties new file mode 100644 index 000000000..49ab57183 --- /dev/null +++ b/mcp-tck-http/src/main/resources/junit-platform.properties @@ -0,0 +1,2 @@ +junit.jupiter.extensions.autodetection.enabled=true + diff --git a/pom.xml b/pom.xml index a5fd98b7f..a3f2be951 100644 --- a/pom.xml +++ b/pom.xml @@ -60,7 +60,8 @@ 3.26.3 - 5.10.2 + 5.13.4 + 1.13.4 5.17.0 1.20.4 1.17.5 @@ -73,7 +74,7 @@ 3.11.0 - 3.1.2 + 3.5.4 3.5.2 3.5.0 3.3.0 @@ -106,6 +107,9 @@ mcp-core mcp-json-jackson2 mcp-json + mcp-tck-http + mcp-tck-http-httpserver-async + mcp-tck-http-httpserver-sync mcp-spring/mcp-spring-webflux mcp-spring/mcp-spring-webmvc mcp-test @@ -194,7 +198,6 @@ false - From e10eb62c72e1814da1c6f0e5486819efde12cf5e Mon Sep 17 00:00:00 2001 From: Sergio del Amo Date: Wed, 24 Sep 2025 16:05:35 +0200 Subject: [PATCH 2/5] delete trasnport context extractor --- ...pExchangeMcpTransportContextExtractor.java | 55 -------------- ...angeMcpTransportContextExtractorTests.java | 72 ------------------- .../async/AsyncMcpHttpServerSupplier.java | 1 - .../sync/SyncMcpHttpServerSupplier.java | 1 - 4 files changed, 129 deletions(-) delete mode 100644 mcp-core/src/main/java/io/modelcontextprotocol/server/httpserver/HttpExchangeMcpTransportContextExtractor.java delete mode 100644 mcp-core/src/test/java/io/modelcontextprotocol/server/httpserver/HttpExchangeMcpTransportContextExtractorTests.java diff --git a/mcp-core/src/main/java/io/modelcontextprotocol/server/httpserver/HttpExchangeMcpTransportContextExtractor.java b/mcp-core/src/main/java/io/modelcontextprotocol/server/httpserver/HttpExchangeMcpTransportContextExtractor.java deleted file mode 100644 index f160a00a7..000000000 --- a/mcp-core/src/main/java/io/modelcontextprotocol/server/httpserver/HttpExchangeMcpTransportContextExtractor.java +++ /dev/null @@ -1,55 +0,0 @@ -/* - * Copyright 2017-2025 original authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.modelcontextprotocol.server.httpserver; - -import com.sun.net.httpserver.Headers; -import com.sun.net.httpserver.HttpExchange; -import io.modelcontextprotocol.common.McpTransportContext; -import io.modelcontextprotocol.server.McpTransportContextExtractor; -import io.modelcontextprotocol.spec.ProtocolVersions; - -import java.util.HashMap; -import java.util.Map; -import java.util.Optional; - -/** - * {@link McpTransportContextExtractor} implementation for {@link HttpExchange}. - */ -public class HttpExchangeMcpTransportContextExtractor implements McpTransportContextExtractor { - - @Override - public McpTransportContext extract(HttpExchange httpExchange) { - return McpTransportContext.create(metadata(httpExchange)); - } - - private Map metadata(HttpExchange httpExchange) { - Headers headers = httpExchange.getRequestHeaders(); - return metadata(headers); - } - - private Map metadata(Headers headers) { - Map metadata = new HashMap<>(); - metadata.put(io.modelcontextprotocol.spec.HttpHeaders.PROTOCOL_VERSION, - Optional.ofNullable(headers.getFirst(io.modelcontextprotocol.spec.HttpHeaders.PROTOCOL_VERSION)) - .orElse(ProtocolVersions.MCP_2025_03_26)); - Optional.ofNullable(headers.getFirst(io.modelcontextprotocol.spec.HttpHeaders.MCP_SESSION_ID)) - .ifPresent(v -> metadata.put(io.modelcontextprotocol.spec.HttpHeaders.MCP_SESSION_ID, v)); - Optional.ofNullable(headers.getFirst(io.modelcontextprotocol.spec.HttpHeaders.LAST_EVENT_ID)) - .ifPresent(v -> metadata.put(io.modelcontextprotocol.spec.HttpHeaders.LAST_EVENT_ID, v)); - return metadata; - } - -} diff --git a/mcp-core/src/test/java/io/modelcontextprotocol/server/httpserver/HttpExchangeMcpTransportContextExtractorTests.java b/mcp-core/src/test/java/io/modelcontextprotocol/server/httpserver/HttpExchangeMcpTransportContextExtractorTests.java deleted file mode 100644 index e8999149e..000000000 --- a/mcp-core/src/test/java/io/modelcontextprotocol/server/httpserver/HttpExchangeMcpTransportContextExtractorTests.java +++ /dev/null @@ -1,72 +0,0 @@ -package io.modelcontextprotocol.server.httpserver; - -import com.sun.net.httpserver.Headers; -import com.sun.net.httpserver.HttpExchange; -import io.modelcontextprotocol.common.McpTransportContext; -import io.modelcontextprotocol.spec.HttpHeaders; -import io.modelcontextprotocol.spec.ProtocolVersions; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; - -import static org.junit.jupiter.api.Assertions.*; -import static org.mockito.Mockito.*; - -class HttpExchangeMcpTransportContextExtractorTests { - - @Test - @DisplayName("extract() includes all provided headers in metadata") - void extractIncludesAllHeaders() { - Headers headers = new Headers(); - headers.add(HttpHeaders.PROTOCOL_VERSION, "2025-03-26"); - headers.add(HttpHeaders.MCP_SESSION_ID, "session-123"); - headers.add(HttpHeaders.LAST_EVENT_ID, "evt-9"); - - HttpExchange exchange = mock(HttpExchange.class); - when(exchange.getRequestHeaders()).thenReturn(headers); - HttpExchangeMcpTransportContextExtractor extractor = new HttpExchangeMcpTransportContextExtractor(); - - McpTransportContext ctx = extractor.extract(exchange); - - assertEquals("2025-03-26", ctx.get(HttpHeaders.PROTOCOL_VERSION)); - assertEquals("session-123", ctx.get(HttpHeaders.MCP_SESSION_ID)); - assertEquals("evt-9", ctx.get(HttpHeaders.LAST_EVENT_ID)); - - verify(exchange, times(1)).getRequestHeaders(); - verifyNoMoreInteractions(exchange); - } - - @Test - @DisplayName("extract() defaults protocol version when header missing") - void extractDefaultsProtocolVersion() { - Headers headers = new Headers(); - HttpExchange exchange = mock(HttpExchange.class); - when(exchange.getRequestHeaders()).thenReturn(headers); - HttpExchangeMcpTransportContextExtractor extractor = new HttpExchangeMcpTransportContextExtractor(); - - McpTransportContext ctx = extractor.extract(exchange); - - assertEquals(ProtocolVersions.MCP_2025_03_26, ctx.get(HttpHeaders.PROTOCOL_VERSION)); - - verify(exchange, times(1)).getRequestHeaders(); - verifyNoMoreInteractions(exchange); - } - - @Test - @DisplayName("extract() omits optional headers when not present") - void extractOmitsOptionalHeaders() { - Headers headers = new Headers(); - headers.add(HttpHeaders.PROTOCOL_VERSION, ProtocolVersions.MCP_2025_03_26); - HttpExchange exchange = mock(HttpExchange.class); - when(exchange.getRequestHeaders()).thenReturn(headers); - HttpExchangeMcpTransportContextExtractor extractor = new HttpExchangeMcpTransportContextExtractor(); - - McpTransportContext ctx = extractor.extract(exchange); - - assertNull(ctx.get(HttpHeaders.MCP_SESSION_ID)); - assertNull(ctx.get(HttpHeaders.LAST_EVENT_ID)); - - verify(exchange, times(1)).getRequestHeaders(); - verifyNoMoreInteractions(exchange); - } - -} \ No newline at end of file diff --git a/mcp-tck-http-httpserver-async/src/test/java/io/modelcontextprotocol/server/http/tck/httpserver/async/AsyncMcpHttpServerSupplier.java b/mcp-tck-http-httpserver-async/src/test/java/io/modelcontextprotocol/server/http/tck/httpserver/async/AsyncMcpHttpServerSupplier.java index d1905e79b..0b7f3e6f8 100644 --- a/mcp-tck-http-httpserver-async/src/test/java/io/modelcontextprotocol/server/http/tck/httpserver/async/AsyncMcpHttpServerSupplier.java +++ b/mcp-tck-http-httpserver-async/src/test/java/io/modelcontextprotocol/server/http/tck/httpserver/async/AsyncMcpHttpServerSupplier.java @@ -4,7 +4,6 @@ import io.modelcontextprotocol.server.transport.HttpServerMcpStatelessServerTransport; import io.modelcontextprotocol.server.McpHttpServer; import io.modelcontextprotocol.server.McpHttpServerSupplier; -import io.modelcontextprotocol.server.httpserver.HttpExchangeMcpTransportContextExtractor; import io.modelcontextprotocol.server.httpserver.McpSimpleHttpServer; import io.modelcontextprotocol.common.McpTransportContext; import io.modelcontextprotocol.json.McpJsonMapper; diff --git a/mcp-tck-http-httpserver-sync/src/test/java/io/modelcontextprotocol/server/httpserver/sync/SyncMcpHttpServerSupplier.java b/mcp-tck-http-httpserver-sync/src/test/java/io/modelcontextprotocol/server/httpserver/sync/SyncMcpHttpServerSupplier.java index dc5e8ba82..601b1f40a 100644 --- a/mcp-tck-http-httpserver-sync/src/test/java/io/modelcontextprotocol/server/httpserver/sync/SyncMcpHttpServerSupplier.java +++ b/mcp-tck-http-httpserver-sync/src/test/java/io/modelcontextprotocol/server/httpserver/sync/SyncMcpHttpServerSupplier.java @@ -4,7 +4,6 @@ import io.modelcontextprotocol.server.transport.HttpServerMcpStatelessServerTransport; import io.modelcontextprotocol.server.McpHttpServer; import io.modelcontextprotocol.server.McpHttpServerSupplier; -import io.modelcontextprotocol.server.httpserver.HttpExchangeMcpTransportContextExtractor; import io.modelcontextprotocol.server.httpserver.McpSimpleHttpServer; import io.modelcontextprotocol.common.McpTransportContext; import io.modelcontextprotocol.json.McpJsonMapper; From 030b9e3ed43a8cd8477bc8720923cf55af12ef4f Mon Sep 17 00:00:00 2001 From: Sergio del Amo Date: Wed, 24 Sep 2025 16:06:59 +0200 Subject: [PATCH 3/5] remove extractor --- ...etRequestMcpTransportContextExtractor.java | 42 ---------- ...HttpServletSseServerTransportProvider.java | 1 - .../HttpServletStatelessServerTransport.java | 1 - ...vletStreamableServerTransportProvider.java | 1 - ...erMcpTransportContextIntegrationTests.java | 1 - ...erMcpTransportContextIntegrationTests.java | 1 - .../HttpServletSseIntegrationTests.java | 2 - ...HttpServletStreamableIntegrationTests.java | 2 - ...uestMcpTransportContextExtractorTests.java | 81 ------------------- 9 files changed, 132 deletions(-) delete mode 100644 mcp-core/src/main/java/io/modelcontextprotocol/server/servlet/HttpServletRequestMcpTransportContextExtractor.java delete mode 100644 mcp-core/src/test/java/io/modelcontextprotocol/server/servlet/HttpServletRequestMcpTransportContextExtractorTests.java diff --git a/mcp-core/src/main/java/io/modelcontextprotocol/server/servlet/HttpServletRequestMcpTransportContextExtractor.java b/mcp-core/src/main/java/io/modelcontextprotocol/server/servlet/HttpServletRequestMcpTransportContextExtractor.java deleted file mode 100644 index d8129646c..000000000 --- a/mcp-core/src/main/java/io/modelcontextprotocol/server/servlet/HttpServletRequestMcpTransportContextExtractor.java +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Copyright 2024-2024 the original author or authors. - */ -package io.modelcontextprotocol.server.servlet; - -import io.modelcontextprotocol.common.McpTransportContext; -import io.modelcontextprotocol.server.McpTransportContextExtractor; -import io.modelcontextprotocol.spec.ProtocolVersions; -import jakarta.servlet.http.HttpServletRequest; - -import java.util.HashMap; -import java.util.Map; -import java.util.Optional; - -/** - * {@link McpTransportContextExtractor} implementation for {@link HttpServletRequest}. - */ -public class HttpServletRequestMcpTransportContextExtractor - implements McpTransportContextExtractor { - - @Override - public McpTransportContext extract(HttpServletRequest request) { - return McpTransportContext.create(metadata(request)); - } - - /** - * @param request Servlet Request - * @return Extracts Map for MCP Transport Context - */ - protected Map metadata(HttpServletRequest request) { - Map metadata = new HashMap<>(); - metadata.put(io.modelcontextprotocol.spec.HttpHeaders.PROTOCOL_VERSION, - Optional.ofNullable(request.getHeader(io.modelcontextprotocol.spec.HttpHeaders.PROTOCOL_VERSION)) - .orElse(ProtocolVersions.MCP_2025_03_26)); - Optional.ofNullable(request.getHeader(io.modelcontextprotocol.spec.HttpHeaders.MCP_SESSION_ID)) - .ifPresent(v -> metadata.put(io.modelcontextprotocol.spec.HttpHeaders.MCP_SESSION_ID, v)); - Optional.ofNullable(request.getHeader(io.modelcontextprotocol.spec.HttpHeaders.LAST_EVENT_ID)) - .ifPresent(v -> metadata.put(io.modelcontextprotocol.spec.HttpHeaders.LAST_EVENT_ID, v)); - return metadata; - } - -} diff --git a/mcp-core/src/main/java/io/modelcontextprotocol/server/transport/HttpServletSseServerTransportProvider.java b/mcp-core/src/main/java/io/modelcontextprotocol/server/transport/HttpServletSseServerTransportProvider.java index 2914f0b1b..fecfb23db 100644 --- a/mcp-core/src/main/java/io/modelcontextprotocol/server/transport/HttpServletSseServerTransportProvider.java +++ b/mcp-core/src/main/java/io/modelcontextprotocol/server/transport/HttpServletSseServerTransportProvider.java @@ -18,7 +18,6 @@ import io.modelcontextprotocol.json.TypeRef; import io.modelcontextprotocol.common.McpTransportContext; import io.modelcontextprotocol.server.McpTransportContextExtractor; -import io.modelcontextprotocol.server.servlet.HttpServletRequestMcpTransportContextExtractor; import io.modelcontextprotocol.spec.McpError; import io.modelcontextprotocol.spec.McpSchema; import io.modelcontextprotocol.spec.McpServerSession; diff --git a/mcp-core/src/main/java/io/modelcontextprotocol/server/transport/HttpServletStatelessServerTransport.java b/mcp-core/src/main/java/io/modelcontextprotocol/server/transport/HttpServletStatelessServerTransport.java index a916fd28b..23aa396bd 100644 --- a/mcp-core/src/main/java/io/modelcontextprotocol/server/transport/HttpServletStatelessServerTransport.java +++ b/mcp-core/src/main/java/io/modelcontextprotocol/server/transport/HttpServletStatelessServerTransport.java @@ -9,7 +9,6 @@ import java.util.Map; import io.modelcontextprotocol.json.TypeRef; -import io.modelcontextprotocol.server.servlet.HttpServletRequestMcpTransportContextExtractor; import org.slf4j.Logger; import org.slf4j.LoggerFactory; diff --git a/mcp-core/src/main/java/io/modelcontextprotocol/server/transport/HttpServletStreamableServerTransportProvider.java b/mcp-core/src/main/java/io/modelcontextprotocol/server/transport/HttpServletStreamableServerTransportProvider.java index 58da9cae8..2cfc782d5 100644 --- a/mcp-core/src/main/java/io/modelcontextprotocol/server/transport/HttpServletStreamableServerTransportProvider.java +++ b/mcp-core/src/main/java/io/modelcontextprotocol/server/transport/HttpServletStreamableServerTransportProvider.java @@ -12,7 +12,6 @@ import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.locks.ReentrantLock; -import io.modelcontextprotocol.server.servlet.HttpServletRequestMcpTransportContextExtractor; import org.slf4j.Logger; import org.slf4j.LoggerFactory; diff --git a/mcp-core/src/test/java/io/modelcontextprotocol/common/AsyncServerMcpTransportContextIntegrationTests.java b/mcp-core/src/test/java/io/modelcontextprotocol/common/AsyncServerMcpTransportContextIntegrationTests.java index 1faaaa32e..dc11eaf08 100644 --- a/mcp-core/src/test/java/io/modelcontextprotocol/common/AsyncServerMcpTransportContextIntegrationTests.java +++ b/mcp-core/src/test/java/io/modelcontextprotocol/common/AsyncServerMcpTransportContextIntegrationTests.java @@ -18,7 +18,6 @@ import io.modelcontextprotocol.server.McpServerFeatures; import io.modelcontextprotocol.server.McpStatelessServerFeatures; import io.modelcontextprotocol.server.McpTransportContextExtractor; -import io.modelcontextprotocol.server.servlet.HttpServletRequestMcpTransportContextExtractor; import io.modelcontextprotocol.server.transport.HttpServletSseServerTransportProvider; import io.modelcontextprotocol.server.transport.HttpServletStatelessServerTransport; import io.modelcontextprotocol.server.transport.HttpServletStreamableServerTransportProvider; diff --git a/mcp-core/src/test/java/io/modelcontextprotocol/common/SyncServerMcpTransportContextIntegrationTests.java b/mcp-core/src/test/java/io/modelcontextprotocol/common/SyncServerMcpTransportContextIntegrationTests.java index a4d0b646a..70a469433 100644 --- a/mcp-core/src/test/java/io/modelcontextprotocol/common/SyncServerMcpTransportContextIntegrationTests.java +++ b/mcp-core/src/test/java/io/modelcontextprotocol/common/SyncServerMcpTransportContextIntegrationTests.java @@ -15,7 +15,6 @@ import io.modelcontextprotocol.server.McpStatelessServerFeatures; import io.modelcontextprotocol.server.McpSyncServerExchange; import io.modelcontextprotocol.server.McpTransportContextExtractor; -import io.modelcontextprotocol.server.servlet.HttpServletRequestMcpTransportContextExtractor; import io.modelcontextprotocol.server.transport.HttpServletSseServerTransportProvider; import io.modelcontextprotocol.server.transport.HttpServletStatelessServerTransport; import io.modelcontextprotocol.server.transport.HttpServletStreamableServerTransportProvider; diff --git a/mcp-core/src/test/java/io/modelcontextprotocol/server/HttpServletSseIntegrationTests.java b/mcp-core/src/test/java/io/modelcontextprotocol/server/HttpServletSseIntegrationTests.java index 43c419f5c..4d4c6b5da 100644 --- a/mcp-core/src/test/java/io/modelcontextprotocol/server/HttpServletSseIntegrationTests.java +++ b/mcp-core/src/test/java/io/modelcontextprotocol/server/HttpServletSseIntegrationTests.java @@ -10,10 +10,8 @@ import io.modelcontextprotocol.client.McpClient; import io.modelcontextprotocol.client.transport.HttpClientSseClientTransport; -import io.modelcontextprotocol.common.McpTransportContext; import io.modelcontextprotocol.server.McpServer.AsyncSpecification; import io.modelcontextprotocol.server.McpServer.SyncSpecification; -import io.modelcontextprotocol.server.servlet.HttpServletRequestMcpTransportContextExtractor; import io.modelcontextprotocol.server.transport.HttpServletSseServerTransportProvider; import io.modelcontextprotocol.server.transport.TomcatTestUtil; import jakarta.servlet.http.HttpServletRequest; diff --git a/mcp-core/src/test/java/io/modelcontextprotocol/server/HttpServletStreamableIntegrationTests.java b/mcp-core/src/test/java/io/modelcontextprotocol/server/HttpServletStreamableIntegrationTests.java index f13e151fd..2f6522cd9 100644 --- a/mcp-core/src/test/java/io/modelcontextprotocol/server/HttpServletStreamableIntegrationTests.java +++ b/mcp-core/src/test/java/io/modelcontextprotocol/server/HttpServletStreamableIntegrationTests.java @@ -10,10 +10,8 @@ import io.modelcontextprotocol.client.McpClient; import io.modelcontextprotocol.client.transport.HttpClientStreamableHttpTransport; -import io.modelcontextprotocol.common.McpTransportContext; import io.modelcontextprotocol.server.McpServer.AsyncSpecification; import io.modelcontextprotocol.server.McpServer.SyncSpecification; -import io.modelcontextprotocol.server.servlet.HttpServletRequestMcpTransportContextExtractor; import io.modelcontextprotocol.server.transport.HttpServletStreamableServerTransportProvider; import io.modelcontextprotocol.server.transport.TomcatTestUtil; import jakarta.servlet.http.HttpServletRequest; diff --git a/mcp-core/src/test/java/io/modelcontextprotocol/server/servlet/HttpServletRequestMcpTransportContextExtractorTests.java b/mcp-core/src/test/java/io/modelcontextprotocol/server/servlet/HttpServletRequestMcpTransportContextExtractorTests.java deleted file mode 100644 index a248c1000..000000000 --- a/mcp-core/src/test/java/io/modelcontextprotocol/server/servlet/HttpServletRequestMcpTransportContextExtractorTests.java +++ /dev/null @@ -1,81 +0,0 @@ -package io.modelcontextprotocol.server.servlet; - -import io.modelcontextprotocol.common.McpTransportContext; -import io.modelcontextprotocol.spec.HttpHeaders; -import io.modelcontextprotocol.spec.ProtocolVersions; -import jakarta.servlet.http.HttpServletRequest; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; - -import static org.junit.jupiter.api.Assertions.*; -import static org.mockito.Mockito.*; - -class HttpServletRequestMcpTransportContextExtractorTests { - - @Test - @DisplayName("extract() includes all provided headers in metadata") - void extractIncludesAllHeaders() { - HttpServletRequest request = mock(HttpServletRequest.class); - when(request.getHeader(HttpHeaders.PROTOCOL_VERSION)).thenReturn("2025-03-26"); - when(request.getHeader(HttpHeaders.MCP_SESSION_ID)).thenReturn("session-abc"); - when(request.getHeader(HttpHeaders.LAST_EVENT_ID)).thenReturn("evt-42"); - - HttpServletRequestMcpTransportContextExtractor extractor = new HttpServletRequestMcpTransportContextExtractor(); - - McpTransportContext ctx = extractor.extract(request); - - assertEquals("2025-03-26", ctx.get(HttpHeaders.PROTOCOL_VERSION)); - assertEquals("session-abc", ctx.get(HttpHeaders.MCP_SESSION_ID)); - assertEquals("evt-42", ctx.get(HttpHeaders.LAST_EVENT_ID)); - - verify(request, times(1)).getHeader(HttpHeaders.PROTOCOL_VERSION); - verify(request, times(1)).getHeader(HttpHeaders.MCP_SESSION_ID); - verify(request, times(1)).getHeader(HttpHeaders.LAST_EVENT_ID); - verifyNoMoreInteractions(request); - } - - @Test - @DisplayName("extract() defaults protocol version when header missing") - void extractDefaultsProtocolVersion() { - HttpServletRequest request = mock(HttpServletRequest.class); - when(request.getHeader(HttpHeaders.PROTOCOL_VERSION)).thenReturn(null); - when(request.getHeader(HttpHeaders.MCP_SESSION_ID)).thenReturn(null); - when(request.getHeader(HttpHeaders.LAST_EVENT_ID)).thenReturn(null); - - HttpServletRequestMcpTransportContextExtractor extractor = new HttpServletRequestMcpTransportContextExtractor(); - - McpTransportContext ctx = extractor.extract(request); - - assertEquals(ProtocolVersions.MCP_2025_03_26, ctx.get(HttpHeaders.PROTOCOL_VERSION)); - assertNull(ctx.get(HttpHeaders.MCP_SESSION_ID)); - assertNull(ctx.get(HttpHeaders.LAST_EVENT_ID)); - - verify(request, times(1)).getHeader(HttpHeaders.PROTOCOL_VERSION); - verify(request, times(1)).getHeader(HttpHeaders.MCP_SESSION_ID); - verify(request, times(1)).getHeader(HttpHeaders.LAST_EVENT_ID); - verifyNoMoreInteractions(request); - } - - @Test - @DisplayName("extract() omits optional headers when not present") - void extractOmitsOptionalHeaders() { - HttpServletRequest request = mock(HttpServletRequest.class); - when(request.getHeader(HttpHeaders.PROTOCOL_VERSION)).thenReturn(ProtocolVersions.MCP_2025_03_26); - when(request.getHeader(HttpHeaders.MCP_SESSION_ID)).thenReturn(null); - when(request.getHeader(HttpHeaders.LAST_EVENT_ID)).thenReturn(null); - - HttpServletRequestMcpTransportContextExtractor extractor = new HttpServletRequestMcpTransportContextExtractor(); - - McpTransportContext ctx = extractor.extract(request); - - assertEquals(ProtocolVersions.MCP_2025_03_26, ctx.get(HttpHeaders.PROTOCOL_VERSION)); - assertNull(ctx.get(HttpHeaders.MCP_SESSION_ID)); - assertNull(ctx.get(HttpHeaders.LAST_EVENT_ID)); - - verify(request, times(1)).getHeader(HttpHeaders.PROTOCOL_VERSION); - verify(request, times(1)).getHeader(HttpHeaders.MCP_SESSION_ID); - verify(request, times(1)).getHeader(HttpHeaders.LAST_EVENT_ID); - verifyNoMoreInteractions(request); - } - -} From 112df3640b8e5ddb0c135e1049466af80d9a6304 Mon Sep 17 00:00:00 2001 From: Sergio del Amo Date: Wed, 24 Sep 2025 16:09:15 +0200 Subject: [PATCH 4/5] revert empty --- .../transport/HttpServletSseServerTransportProvider.java | 5 +++-- .../transport/HttpServletStatelessServerTransport.java | 6 +++--- .../HttpServletStreamableServerTransportProvider.java | 5 +++-- 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/mcp-core/src/main/java/io/modelcontextprotocol/server/transport/HttpServletSseServerTransportProvider.java b/mcp-core/src/main/java/io/modelcontextprotocol/server/transport/HttpServletSseServerTransportProvider.java index fecfb23db..8f9cd6689 100644 --- a/mcp-core/src/main/java/io/modelcontextprotocol/server/transport/HttpServletSseServerTransportProvider.java +++ b/mcp-core/src/main/java/io/modelcontextprotocol/server/transport/HttpServletSseServerTransportProvider.java @@ -503,7 +503,8 @@ public static class Builder { private String sseEndpoint = DEFAULT_SSE_ENDPOINT; - private McpTransportContextExtractor contextExtractor; + private McpTransportContextExtractor contextExtractor = ( + serverRequest) -> McpTransportContext.EMPTY; private Duration keepAliveInterval; @@ -594,7 +595,7 @@ public HttpServletSseServerTransportProvider build() { return new HttpServletSseServerTransportProvider( jsonMapper == null ? McpJsonMapper.getDefault() : jsonMapper, baseUrl, messageEndpoint, sseEndpoint, keepAliveInterval, - contextExtractor == null ? new HttpServletRequestMcpTransportContextExtractor() : contextExtractor); + contextExtractor); } } diff --git a/mcp-core/src/main/java/io/modelcontextprotocol/server/transport/HttpServletStatelessServerTransport.java b/mcp-core/src/main/java/io/modelcontextprotocol/server/transport/HttpServletStatelessServerTransport.java index 23aa396bd..be75962f7 100644 --- a/mcp-core/src/main/java/io/modelcontextprotocol/server/transport/HttpServletStatelessServerTransport.java +++ b/mcp-core/src/main/java/io/modelcontextprotocol/server/transport/HttpServletStatelessServerTransport.java @@ -169,7 +169,8 @@ public static class Builder { private String mcpEndpoint = "/mcp"; - private McpTransportContextExtractor contextExtractor; + private McpTransportContextExtractor contextExtractor = ( + serverRequest) -> McpTransportContext.EMPTY; private Builder() { // used by a static method @@ -225,8 +226,7 @@ public Builder contextExtractor(McpTransportContextExtractor public HttpServletStatelessServerTransport build() { Assert.notNull(mcpEndpoint, "Message endpoint must be set"); return new HttpServletStatelessServerTransport( - new HttpServerMcpStatelessServerTransport<>(contextExtractor == null - ? new HttpServletRequestMcpTransportContextExtractor() : contextExtractor), + new HttpServerMcpStatelessServerTransport<>(contextExtractor), jsonMapper == null ? McpJsonMapper.getDefault() : jsonMapper, mcpEndpoint); } diff --git a/mcp-core/src/main/java/io/modelcontextprotocol/server/transport/HttpServletStreamableServerTransportProvider.java b/mcp-core/src/main/java/io/modelcontextprotocol/server/transport/HttpServletStreamableServerTransportProvider.java index 2cfc782d5..5035d418d 100644 --- a/mcp-core/src/main/java/io/modelcontextprotocol/server/transport/HttpServletStreamableServerTransportProvider.java +++ b/mcp-core/src/main/java/io/modelcontextprotocol/server/transport/HttpServletStreamableServerTransportProvider.java @@ -768,7 +768,8 @@ public static class Builder { private boolean disallowDelete = false; - private McpTransportContextExtractor contextExtractor; + private McpTransportContextExtractor contextExtractor = ( + serverRequest) -> McpTransportContext.EMPTY; private Duration keepAliveInterval; @@ -841,7 +842,7 @@ public HttpServletStreamableServerTransportProvider build() { Assert.notNull(this.mcpEndpoint, "MCP endpoint must be set"); return new HttpServletStreamableServerTransportProvider( jsonMapper == null ? McpJsonMapper.getDefault() : jsonMapper, mcpEndpoint, disallowDelete, - contextExtractor == null ? new HttpServletRequestMcpTransportContextExtractor() : contextExtractor, + contextExtractor, keepAliveInterval); } From 8ab48c630a744a10ca16e5787527c5d10860ab7d Mon Sep 17 00:00:00 2001 From: Sergio del Amo Date: Wed, 24 Sep 2025 16:14:40 +0200 Subject: [PATCH 5/5] fix --- .../HttpServerMcpStatelessServerTransport.java | 1 - .../HttpServletSseServerTransportProvider.java | 7 +++---- .../HttpServletStatelessServerTransport.java | 4 ++-- ...tpServletStreamableServerTransportProvider.java | 7 +++---- ...cServerMcpTransportContextIntegrationTests.java | 14 ++++---------- ...cServerMcpTransportContextIntegrationTests.java | 14 ++++---------- .../server/HttpServletSseIntegrationTests.java | 11 +++-------- .../HttpServletStreamableIntegrationTests.java | 11 +++-------- .../async/AsyncMcpHttpServerSupplier.java | 4 ++-- .../httpserver/sync/SyncMcpHttpServerSupplier.java | 4 ++-- 10 files changed, 26 insertions(+), 51 deletions(-) diff --git a/mcp-core/src/main/java/io/modelcontextprotocol/server/transport/HttpServerMcpStatelessServerTransport.java b/mcp-core/src/main/java/io/modelcontextprotocol/server/transport/HttpServerMcpStatelessServerTransport.java index 98ba519ad..b4ddde8ac 100644 --- a/mcp-core/src/main/java/io/modelcontextprotocol/server/transport/HttpServerMcpStatelessServerTransport.java +++ b/mcp-core/src/main/java/io/modelcontextprotocol/server/transport/HttpServerMcpStatelessServerTransport.java @@ -14,7 +14,6 @@ import reactor.core.publisher.Mono; import java.util.Map; -import java.util.function.Function; import static io.modelcontextprotocol.spec.McpSchema.JSONRPC_VERSION; diff --git a/mcp-core/src/main/java/io/modelcontextprotocol/server/transport/HttpServletSseServerTransportProvider.java b/mcp-core/src/main/java/io/modelcontextprotocol/server/transport/HttpServletSseServerTransportProvider.java index 8f9cd6689..4739e231a 100644 --- a/mcp-core/src/main/java/io/modelcontextprotocol/server/transport/HttpServletSseServerTransportProvider.java +++ b/mcp-core/src/main/java/io/modelcontextprotocol/server/transport/HttpServletSseServerTransportProvider.java @@ -503,8 +503,8 @@ public static class Builder { private String sseEndpoint = DEFAULT_SSE_ENDPOINT; - private McpTransportContextExtractor contextExtractor = ( - serverRequest) -> McpTransportContext.EMPTY; + private McpTransportContextExtractor contextExtractor = ( + serverRequest) -> McpTransportContext.EMPTY; private Duration keepAliveInterval; @@ -594,8 +594,7 @@ public HttpServletSseServerTransportProvider build() { } return new HttpServletSseServerTransportProvider( jsonMapper == null ? McpJsonMapper.getDefault() : jsonMapper, baseUrl, messageEndpoint, sseEndpoint, - keepAliveInterval, - contextExtractor); + keepAliveInterval, contextExtractor); } } diff --git a/mcp-core/src/main/java/io/modelcontextprotocol/server/transport/HttpServletStatelessServerTransport.java b/mcp-core/src/main/java/io/modelcontextprotocol/server/transport/HttpServletStatelessServerTransport.java index be75962f7..d73f16940 100644 --- a/mcp-core/src/main/java/io/modelcontextprotocol/server/transport/HttpServletStatelessServerTransport.java +++ b/mcp-core/src/main/java/io/modelcontextprotocol/server/transport/HttpServletStatelessServerTransport.java @@ -169,8 +169,8 @@ public static class Builder { private String mcpEndpoint = "/mcp"; - private McpTransportContextExtractor contextExtractor = ( - serverRequest) -> McpTransportContext.EMPTY; + private McpTransportContextExtractor contextExtractor = ( + serverRequest) -> McpTransportContext.EMPTY; private Builder() { // used by a static method diff --git a/mcp-core/src/main/java/io/modelcontextprotocol/server/transport/HttpServletStreamableServerTransportProvider.java b/mcp-core/src/main/java/io/modelcontextprotocol/server/transport/HttpServletStreamableServerTransportProvider.java index 5035d418d..8d03fcfd4 100644 --- a/mcp-core/src/main/java/io/modelcontextprotocol/server/transport/HttpServletStreamableServerTransportProvider.java +++ b/mcp-core/src/main/java/io/modelcontextprotocol/server/transport/HttpServletStreamableServerTransportProvider.java @@ -768,8 +768,8 @@ public static class Builder { private boolean disallowDelete = false; - private McpTransportContextExtractor contextExtractor = ( - serverRequest) -> McpTransportContext.EMPTY; + private McpTransportContextExtractor contextExtractor = ( + serverRequest) -> McpTransportContext.EMPTY; private Duration keepAliveInterval; @@ -842,8 +842,7 @@ public HttpServletStreamableServerTransportProvider build() { Assert.notNull(this.mcpEndpoint, "MCP endpoint must be set"); return new HttpServletStreamableServerTransportProvider( jsonMapper == null ? McpJsonMapper.getDefault() : jsonMapper, mcpEndpoint, disallowDelete, - contextExtractor, - keepAliveInterval); + contextExtractor, keepAliveInterval); } } diff --git a/mcp-core/src/test/java/io/modelcontextprotocol/common/AsyncServerMcpTransportContextIntegrationTests.java b/mcp-core/src/test/java/io/modelcontextprotocol/common/AsyncServerMcpTransportContextIntegrationTests.java index dc11eaf08..8b2dea462 100644 --- a/mcp-core/src/test/java/io/modelcontextprotocol/common/AsyncServerMcpTransportContextIntegrationTests.java +++ b/mcp-core/src/test/java/io/modelcontextprotocol/common/AsyncServerMcpTransportContextIntegrationTests.java @@ -91,16 +91,10 @@ public class AsyncServerMcpTransportContextIntegrationTests { return Mono.just(builder); }; - private final McpTransportContextExtractor serverContextExtractor = new HttpServletRequestMcpTransportContextExtractor() { - @Override - protected Map metadata(HttpServletRequest r) { - Map m = super.metadata(r); - var headerValue = r.getHeader(HEADER_NAME); - if (headerValue != null) { - m.put("server-side-header-value", headerValue); - } - return m; - } + private final McpTransportContextExtractor serverContextExtractor = (HttpServletRequest r) -> { + var headerValue = r.getHeader(HEADER_NAME); + return headerValue != null ? McpTransportContext.create(Map.of("server-side-header-value", headerValue)) + : McpTransportContext.EMPTY; }; private final HttpServletStatelessServerTransport statelessServerTransport = HttpServletStatelessServerTransport diff --git a/mcp-core/src/test/java/io/modelcontextprotocol/common/SyncServerMcpTransportContextIntegrationTests.java b/mcp-core/src/test/java/io/modelcontextprotocol/common/SyncServerMcpTransportContextIntegrationTests.java index 70a469433..cc8f4c4be 100644 --- a/mcp-core/src/test/java/io/modelcontextprotocol/common/SyncServerMcpTransportContextIntegrationTests.java +++ b/mcp-core/src/test/java/io/modelcontextprotocol/common/SyncServerMcpTransportContextIntegrationTests.java @@ -71,16 +71,10 @@ public class SyncServerMcpTransportContextIntegrationTests { } }; - private final McpTransportContextExtractor serverContextExtractor = new HttpServletRequestMcpTransportContextExtractor() { - @Override - protected Map metadata(HttpServletRequest r) { - Map m = super.metadata(r); - var headerValue = r.getHeader(HEADER_NAME); - if (headerValue != null) { - m.put("server-side-header-value", headerValue); - } - return m; - } + private final McpTransportContextExtractor serverContextExtractor = (HttpServletRequest r) -> { + var headerValue = r.getHeader(HEADER_NAME); + return headerValue != null ? McpTransportContext.create(Map.of("server-side-header-value", headerValue)) + : McpTransportContext.EMPTY; }; private final BiFunction statelessHandler = ( diff --git a/mcp-core/src/test/java/io/modelcontextprotocol/server/HttpServletSseIntegrationTests.java b/mcp-core/src/test/java/io/modelcontextprotocol/server/HttpServletSseIntegrationTests.java index 4d4c6b5da..d2b9d14d0 100644 --- a/mcp-core/src/test/java/io/modelcontextprotocol/server/HttpServletSseIntegrationTests.java +++ b/mcp-core/src/test/java/io/modelcontextprotocol/server/HttpServletSseIntegrationTests.java @@ -10,6 +10,7 @@ import io.modelcontextprotocol.client.McpClient; import io.modelcontextprotocol.client.transport.HttpClientSseClientTransport; +import io.modelcontextprotocol.common.McpTransportContext; import io.modelcontextprotocol.server.McpServer.AsyncSpecification; import io.modelcontextprotocol.server.McpServer.SyncSpecification; import io.modelcontextprotocol.server.transport.HttpServletSseServerTransportProvider; @@ -97,13 +98,7 @@ public void after() { protected void prepareClients(int port, String mcpEndpoint) { } - static McpTransportContextExtractor TEST_CONTEXT_EXTRACTOR = new HttpServletRequestMcpTransportContextExtractor() { - @Override - protected Map metadata(HttpServletRequest r) { - Map m = super.metadata(r); - m.putAll(Map.of("important", "value")); - return m; - } - }; + static McpTransportContextExtractor TEST_CONTEXT_EXTRACTOR = (r) -> McpTransportContext + .create(Map.of("important", "value")); } diff --git a/mcp-core/src/test/java/io/modelcontextprotocol/server/HttpServletStreamableIntegrationTests.java b/mcp-core/src/test/java/io/modelcontextprotocol/server/HttpServletStreamableIntegrationTests.java index 2f6522cd9..81423e0c5 100644 --- a/mcp-core/src/test/java/io/modelcontextprotocol/server/HttpServletStreamableIntegrationTests.java +++ b/mcp-core/src/test/java/io/modelcontextprotocol/server/HttpServletStreamableIntegrationTests.java @@ -10,6 +10,7 @@ import io.modelcontextprotocol.client.McpClient; import io.modelcontextprotocol.client.transport.HttpClientStreamableHttpTransport; +import io.modelcontextprotocol.common.McpTransportContext; import io.modelcontextprotocol.server.McpServer.AsyncSpecification; import io.modelcontextprotocol.server.McpServer.SyncSpecification; import io.modelcontextprotocol.server.transport.HttpServletStreamableServerTransportProvider; @@ -95,13 +96,7 @@ public void after() { protected void prepareClients(int port, String mcpEndpoint) { } - static McpTransportContextExtractor TEST_CONTEXT_EXTRACTOR = new HttpServletRequestMcpTransportContextExtractor() { - @Override - protected Map metadata(HttpServletRequest r) { - Map m = super.metadata(r); - m.putAll(Map.of("important", "value")); - return m; - } - }; + static McpTransportContextExtractor TEST_CONTEXT_EXTRACTOR = (r) -> McpTransportContext + .create(Map.of("important", "value")); } diff --git a/mcp-tck-http-httpserver-async/src/test/java/io/modelcontextprotocol/server/http/tck/httpserver/async/AsyncMcpHttpServerSupplier.java b/mcp-tck-http-httpserver-async/src/test/java/io/modelcontextprotocol/server/http/tck/httpserver/async/AsyncMcpHttpServerSupplier.java index 0b7f3e6f8..6fb32ab85 100644 --- a/mcp-tck-http-httpserver-async/src/test/java/io/modelcontextprotocol/server/http/tck/httpserver/async/AsyncMcpHttpServerSupplier.java +++ b/mcp-tck-http-httpserver-async/src/test/java/io/modelcontextprotocol/server/http/tck/httpserver/async/AsyncMcpHttpServerSupplier.java @@ -27,8 +27,8 @@ public class AsyncMcpHttpServerSupplier implements McpHttpServerSupplier { public McpHttpServer get() { try { McpJsonMapper jsonMapper = McpJsonMapper.getDefault(); - HttpServerMcpStatelessServerTransport transport = new HttpServerMcpStatelessServerTransport( - new HttpExchangeMcpTransportContextExtractor()); + HttpServerMcpStatelessServerTransport transport = new HttpServerMcpStatelessServerTransport<>( + (serverRequest) -> McpTransportContext.EMPTY); McpServer.StatelessAsyncSpecification spec = McpServer.async(transport) .jsonSchemaValidator(JsonSchemaValidator.getDefault()) .jsonMapper(jsonMapper) diff --git a/mcp-tck-http-httpserver-sync/src/test/java/io/modelcontextprotocol/server/httpserver/sync/SyncMcpHttpServerSupplier.java b/mcp-tck-http-httpserver-sync/src/test/java/io/modelcontextprotocol/server/httpserver/sync/SyncMcpHttpServerSupplier.java index 601b1f40a..9b3c73c29 100644 --- a/mcp-tck-http-httpserver-sync/src/test/java/io/modelcontextprotocol/server/httpserver/sync/SyncMcpHttpServerSupplier.java +++ b/mcp-tck-http-httpserver-sync/src/test/java/io/modelcontextprotocol/server/httpserver/sync/SyncMcpHttpServerSupplier.java @@ -25,8 +25,8 @@ public class SyncMcpHttpServerSupplier implements McpHttpServerSupplier { public McpHttpServer get() { try { McpJsonMapper jsonMapper = McpJsonMapper.getDefault(); - HttpServerMcpStatelessServerTransport transport = new HttpServerMcpStatelessServerTransport( - new HttpExchangeMcpTransportContextExtractor()); + HttpServerMcpStatelessServerTransport transport = new HttpServerMcpStatelessServerTransport<>( + (serverRequest) -> McpTransportContext.EMPTY); McpServer.StatelessSyncSpecification spec = McpServer.sync(transport) .jsonSchemaValidator(JsonSchemaValidator.getDefault()) .jsonMapper(jsonMapper)