fix(mcp): align SDK and wire streamable server manually
- align all io.modelcontextprotocol.sdk artifacts to 0.18.1 via dependencyManagement so Spring AI transitives no longer pull mcp 0.10.0 - exclude Spring AI's legacy MCP server/webmvc auto-config, which is binary- incompatible with the 0.18.1 streamable transport APIs - build McpSyncServer directly against WebMvcStreamableServerTransportProvider and adapt Spring AI ToolCallbacks to MCP SyncToolSpecifications manually - keep /mcp as the sole Streamable HTTP endpoint for both initialize/tool calls and optional SSE event streams - update MCP transport documentation to match the new runtime Validated locally with: - POST /mcp initialize -> HTTP 200 + Mcp-Session-Id - POST /mcp tools/list -> returns resolve-library-id + get-library-docs
This commit is contained in:
@@ -198,7 +198,7 @@ End-to-end smoke after first assembly:
|
||||
- `POST /api/repos` + `GET /api/repos` — round-trips a repo.
|
||||
- `GET /swagger-ui.html` → 302 redirect (to `/swagger-ui/index.html`), `GET /v3/api-docs` → 200.
|
||||
- `GET /` → 200 (SvelteKit SPA served from Spring static resources).
|
||||
- `POST /mcp` one-shot JSON-RPC returns HTTP 500 — expected, the WebMVC MCP transport requires an SSE session established by `GET /sse` first; MCP clients that implement the Streamable-HTTP spec do this automatically. Verified MCP tools register: `tools/list` handler is reached (error thrown is transport-level session lookup, not bean wiring).
|
||||
- Historical note: at this point the server still used the legacy WebMVC SSE transport, so `POST /mcp` without an established `GET /sse` session returned HTTP 500. This was later replaced by the Streamable HTTP transport on `GET`/`POST /mcp`.
|
||||
|
||||
Fixes landed during smoke:
|
||||
- `V1__init_schema.sql`: H2 in PostgreSQL mode rejects `AUTO_INCREMENT`. Switched `job_log_events.id` to `BIGINT GENERATED BY DEFAULT AS IDENTITY` and removed the explicit `NULL` constraint.
|
||||
|
||||
24
pom.xml
24
pom.xml
@@ -33,6 +33,7 @@
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
|
||||
<spring-ai.version>1.0.0</spring-ai.version>
|
||||
<mcp.sdk.version>0.18.1</mcp.sdk.version>
|
||||
<springdoc.version>2.8.6</springdoc.version>
|
||||
<jgit.version>7.3.0.202506031305-r</jgit.version>
|
||||
<lucene.version>10.4.0</lucene.version>
|
||||
@@ -83,6 +84,29 @@
|
||||
<scope>import</scope>
|
||||
</dependency>
|
||||
|
||||
<!-- MCP Java SDK: keep Spring AI's transitives and our direct transport/json
|
||||
dependencies on one compatible version line. -->
|
||||
<dependency>
|
||||
<groupId>io.modelcontextprotocol.sdk</groupId>
|
||||
<artifactId>mcp</artifactId>
|
||||
<version>${mcp.sdk.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.modelcontextprotocol.sdk</groupId>
|
||||
<artifactId>mcp-core</artifactId>
|
||||
<version>${mcp.sdk.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.modelcontextprotocol.sdk</groupId>
|
||||
<artifactId>mcp-spring-webmvc</artifactId>
|
||||
<version>${mcp.sdk.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.modelcontextprotocol.sdk</groupId>
|
||||
<artifactId>mcp-json-jackson2</artifactId>
|
||||
<version>${mcp.sdk.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- 3rd-party -->
|
||||
<dependency>
|
||||
<groupId>org.springdoc</groupId>
|
||||
|
||||
@@ -57,12 +57,10 @@
|
||||
<dependency>
|
||||
<groupId>io.modelcontextprotocol.sdk</groupId>
|
||||
<artifactId>mcp-spring-webmvc</artifactId>
|
||||
<version>0.18.1</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.modelcontextprotocol.sdk</groupId>
|
||||
<artifactId>mcp-json-jackson2</artifactId>
|
||||
<version>0.18.1</version>
|
||||
</dependency>
|
||||
|
||||
<!-- H2 + Flyway -->
|
||||
|
||||
@@ -1,10 +1,20 @@
|
||||
package com.trueref.adapter.in.mcp;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import io.modelcontextprotocol.server.McpServer;
|
||||
import io.modelcontextprotocol.server.McpServerFeatures;
|
||||
import io.modelcontextprotocol.server.McpSyncServer;
|
||||
import io.modelcontextprotocol.json.jackson2.JacksonMcpJsonMapper;
|
||||
import io.modelcontextprotocol.spec.McpSchema;
|
||||
import io.modelcontextprotocol.server.transport.WebMvcStreamableServerTransportProvider;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import org.springframework.ai.mcp.server.autoconfigure.McpServerProperties;
|
||||
import org.springframework.ai.tool.ToolCallback;
|
||||
import org.springframework.ai.tool.ToolCallbackProvider;
|
||||
import org.springframework.ai.tool.method.MethodToolCallbackProvider;
|
||||
import org.springframework.beans.factory.ObjectProvider;
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
@@ -14,16 +24,16 @@ import org.springframework.web.servlet.function.ServerResponse;
|
||||
/**
|
||||
* Wires the MCP Streamable HTTP transport (2025-03-26 spec) and registers trueref tool callbacks.
|
||||
*
|
||||
* <p>Spring AI 1.0.0 only ships {@link io.modelcontextprotocol.server.transport.WebMvcSseServerTransportProvider}
|
||||
* (legacy SSE). We exclude {@code McpWebMvcServerAutoConfiguration} in {@code application.yml} and
|
||||
* register {@link WebMvcStreamableServerTransportProvider} directly — Spring AI's
|
||||
* {@code McpServerAutoConfiguration} accepts any {@code McpServerTransportProvider} bean.
|
||||
* <p>Spring AI 1.0.0 only auto-configures the legacy SSE/stdIO MCP server path. The current
|
||||
* Streamable HTTP provider in MCP SDK 0.18.1 uses the newer
|
||||
* {@code McpStreamableServerTransportProvider} interface, so we exclude Spring AI's MCP server
|
||||
* auto-configuration in {@code application.yml} and build the {@link McpSyncServer} manually.
|
||||
*
|
||||
* <p>Clients connect with {@code type: http} at {@code POST /mcp} (JSON-RPC) and optionally open
|
||||
* a long-poll GET {@code /mcp} stream for server-initiated notifications.
|
||||
*/
|
||||
@Configuration
|
||||
@EnableConfigurationProperties(McpProperties.class)
|
||||
@EnableConfigurationProperties({McpProperties.class, McpServerProperties.class})
|
||||
public class McpConfig {
|
||||
|
||||
/** Streamable HTTP transport — handles both POST (JSON-RPC) and GET (SSE stream) on /mcp. */
|
||||
@@ -42,8 +52,85 @@ public class McpConfig {
|
||||
return transport.getRouterFunction();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public McpSyncServer mcpSyncServer(
|
||||
WebMvcStreamableServerTransportProvider transport,
|
||||
ObjectMapper objectMapper,
|
||||
McpServerProperties serverProperties,
|
||||
ObjectProvider<List<ToolCallback>> toolCallbacks,
|
||||
List<ToolCallbackProvider> toolCallbackProviders) {
|
||||
|
||||
var jsonMapper = new JacksonMcpJsonMapper(objectMapper);
|
||||
var capabilitiesBuilder = McpSchema.ServerCapabilities.builder();
|
||||
var serverBuilder = McpServer.sync(transport)
|
||||
.serverInfo(serverProperties.getName(), serverProperties.getVersion());
|
||||
|
||||
if (serverProperties.getCapabilities().isTool()) {
|
||||
capabilitiesBuilder.tools(serverProperties.isToolChangeNotification());
|
||||
|
||||
List<McpServerFeatures.SyncToolSpecification> toolSpecifications = new ArrayList<>();
|
||||
toolCallbacks.stream()
|
||||
.flatMap(List::stream)
|
||||
.map(toolCallback -> toSyncToolSpecification(toolCallback, objectMapper, jsonMapper))
|
||||
.forEach(toolSpecifications::add);
|
||||
|
||||
toolCallbackProviders.stream()
|
||||
.flatMap(provider -> Arrays.stream(provider.getToolCallbacks()))
|
||||
.map(toolCallback -> toSyncToolSpecification(toolCallback, objectMapper, jsonMapper))
|
||||
.forEach(toolSpecifications::add);
|
||||
|
||||
if (!toolSpecifications.isEmpty()) {
|
||||
serverBuilder.tools(toolSpecifications);
|
||||
}
|
||||
}
|
||||
|
||||
if (serverProperties.getCapabilities().isResource()) {
|
||||
capabilitiesBuilder.resources(false, serverProperties.isResourceChangeNotification());
|
||||
}
|
||||
if (serverProperties.getCapabilities().isPrompt()) {
|
||||
capabilitiesBuilder.prompts(serverProperties.isPromptChangeNotification());
|
||||
}
|
||||
if (serverProperties.getCapabilities().isCompletion()) {
|
||||
capabilitiesBuilder.completions();
|
||||
}
|
||||
|
||||
if (serverProperties.getInstructions() != null) {
|
||||
serverBuilder.instructions(serverProperties.getInstructions());
|
||||
}
|
||||
if (serverProperties.getRequestTimeout() != null) {
|
||||
serverBuilder.requestTimeout(serverProperties.getRequestTimeout());
|
||||
}
|
||||
|
||||
return serverBuilder.capabilities(capabilitiesBuilder.build()).build();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public ToolCallbackProvider trueRefMcpToolCallbacks(TrueRefMcpTools tools) {
|
||||
return MethodToolCallbackProvider.builder().toolObjects(tools).build();
|
||||
}
|
||||
|
||||
private McpServerFeatures.SyncToolSpecification toSyncToolSpecification(
|
||||
ToolCallback toolCallback,
|
||||
ObjectMapper objectMapper,
|
||||
JacksonMcpJsonMapper jsonMapper) {
|
||||
var tool = McpSchema.Tool.builder()
|
||||
.name(toolCallback.getToolDefinition().name())
|
||||
.description(toolCallback.getToolDefinition().description())
|
||||
.inputSchema(jsonMapper, toolCallback.getToolDefinition().inputSchema())
|
||||
.build();
|
||||
|
||||
return new McpServerFeatures.SyncToolSpecification(tool, (exchange, arguments) -> {
|
||||
try {
|
||||
String result = toolCallback.call(objectMapper.writeValueAsString(arguments));
|
||||
return new McpSchema.CallToolResult(result, false);
|
||||
}
|
||||
catch (Exception ex) {
|
||||
String message = ex.getMessage();
|
||||
if (message == null || message.isBlank()) {
|
||||
message = ex.getClass().getSimpleName();
|
||||
}
|
||||
return new McpSchema.CallToolResult(message, true);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,16 +1,12 @@
|
||||
/**
|
||||
* Driving adapter: Model Context Protocol (MCP) server exposing the two Context7-compatible
|
||||
* tools ({@code resolve-library-id}, {@code get-library-docs}) over Spring AI's MCP WebMVC
|
||||
* transport. The HTTP message endpoint is wired to {@code POST /mcp} via
|
||||
* {@code spring.ai.mcp.server.sse-message-endpoint}.
|
||||
* transport. The adapter publishes the current Streamable HTTP transport on {@code /mcp}.
|
||||
*
|
||||
* <p>Spring AI 1.0.0 ships the SSE-based WebMVC transport
|
||||
* ({@code WebMvcSseServerTransportProvider}); the 2025-03-26 "Streamable HTTP" transport is
|
||||
* not a separate selectable property in this version. Clients that POST JSON-RPC to the
|
||||
* configured message endpoint receive JSON-RPC responses; the server additionally opens an
|
||||
* SSE stream on the configured {@code sse-endpoint} for server-initiated notifications. This
|
||||
* is the closest equivalent Spring AI 1.0.0 provides to Streamable HTTP and is the
|
||||
* intended/only transport of this adapter.
|
||||
* <p>Spring AI 1.0.0 still auto-configures only the legacy SSE WebMVC transport, so this adapter
|
||||
* wires {@code WebMvcStreamableServerTransportProvider} manually. Clients send JSON-RPC requests
|
||||
* to {@code POST /mcp}; server-initiated notifications may be streamed from {@code GET /mcp}
|
||||
* using the MCP Streamable HTTP transport.
|
||||
*/
|
||||
@org.jspecify.annotations.NullMarked
|
||||
package com.trueref.adapter.in.mcp;
|
||||
|
||||
@@ -21,11 +21,14 @@ spring:
|
||||
async:
|
||||
request-timeout: 0 # MCP GET streams must not time out
|
||||
# Spring AI MCP server — Streamable HTTP transport (MCP spec 2025-03-26).
|
||||
# McpWebMvcServerAutoConfiguration (SSE transport) is excluded below;
|
||||
# WebMvcStreamableServerTransportProvider is wired manually in McpConfig.
|
||||
# Spring AI 1.0.0's MCP auto-config only knows the legacy SSE/stdIO transport interfaces,
|
||||
# so both MCP auto-config classes are excluded below and the Streamable HTTP transport plus
|
||||
# McpSyncServer are wired manually in McpConfig.
|
||||
# Clients connect with type: http at POST /mcp.
|
||||
autoconfigure:
|
||||
exclude: org.springframework.ai.mcp.server.autoconfigure.McpWebMvcServerAutoConfiguration
|
||||
exclude:
|
||||
- org.springframework.ai.mcp.server.autoconfigure.McpWebMvcServerAutoConfiguration
|
||||
- org.springframework.ai.mcp.server.autoconfigure.McpServerAutoConfiguration
|
||||
ai:
|
||||
mcp:
|
||||
server:
|
||||
|
||||
Reference in New Issue
Block a user