Please do a quick search on GitHub issues first, there might be already a duplicate issue for the one you are about to create. If the bug is trivial, just go ahead and create the issue. Otherwise, please take a few moments and fill in the following sections:
Bug description
When using the MCP Server Starter, one of the supported protocols is SSE. If you are setting a custom context path using the property server.servlet.context-path, the relevant paths for MCP will change, but the path given when you look at the /sse endpoint will not. Therefore MCP clients will break. This was tested with Cursor MCP.
Environment JVM: OpenJDK Runtime Environment Temurin-21.0.5+11 (build 21.0.5+11-LTS) Spring AI version: "1.0.0-M6" (Used BOM)
Application was written in Kotlin but that shouldn't be relevant here.
Steps to reproduce
- Create any MCP server application that uses SSE
- Connect it to an MCP client, should work fine (I used /sse as the path in Cursor)
- Set the server.servlet.context-path, say to "/v1"
- Attempt to update your setup in your MCP client, it will always fail
If you look at the output of the /sse endpoint, the reason is that the path provided by the endpoint is incorrect. It will match what you set in spring.ai.mcp.server.sse-message-endpoint but it will not include the context path.
Expected behavior
In my opinion, this would be fixed if the endpoint that is returned in the call to the /sse endpoint also prepended the context path.
Minimal Complete Reproducible example Please provide a failing test or a minimal complete verifiable example that reproduces the issue. Bug reports that are reproducible will take priority in resolution over reports that are not reproducible.
Comment From: mlmw92
Same problem
Comment From: rod2k2
same problem, how to solve it if have to add server.servelt.context-path ?
Comment From: HollisChen1
I think there should be an issue of org.springframework.ai.mcp.server.autoconfigure.McpWebMvcServerAutoConfiguration(the same as McpWebFluxServerAutoConfiguration) by adding bean in our config class like below, using another constructor of WebMvcSseServerTransportProvider, make sure your servlet context path was added to spring.ai.mcp.server.base-url
@Bean
public WebMvcSseServerTransportProvider webMvcSseServerTransportProvider(
ObjectProvider<ObjectMapper> objectMapperProvider, McpServerProperties serverProperties) {
ObjectMapper objectMapper = objectMapperProvider.getIfAvailable(ObjectMapper::new);
return new WebMvcSseServerTransportProvider(objectMapper,serverProperties.getBaseUrl(), serverProperties.getSseMessageEndpoint(), serverProperties.getSseEndpoint());
}
@Bean
public RouterFunction<ServerResponse> mvcMcpRouterFunction(WebMvcSseServerTransportProvider transportProvider) {
return transportProvider.getRouterFunction();
}
Comment From: GregoireW
@HollisChen1 you mention:
make sure your servlet context path was added to spring.ai.mcp.server.base-url
But setting:
spring:
ai:
mcp:
server:
base-url: /myapp/
sse-endpoint: /sse
sse-message-endpoint: /mcp/message
on spring-ai 1.0.0-M7, I get on curl http://localhost:8080/sse a response containing: data:/mcp/message?sessio... so the base-url is not taken either I think
Comment From: GregoireW
I will answer myself: those beans are initialized with default configuration (base url and sseEndpoint (the /sse ) ) a small change to add the 2 others parameter should fix the issue
https://github.com/spring-projects/spring-ai/blob/687dea52e0ccf5ffe70dfbf6a7aa248d28c01bdc/auto-configurations/mcp/spring-ai-autoconfigure-mcp-server/src/main/java/org/springframework/ai/mcp/server/autoconfigure/McpWebMvcServerAutoConfiguration.java#L75
https://github.com/spring-projects/spring-ai/blob/687dea52e0ccf5ffe70dfbf6a7aa248d28c01bdc/auto-configurations/mcp/spring-ai-autoconfigure-mcp-server/src/main/java/org/springframework/ai/mcp/server/autoconfigure/McpWebFluxServerAutoConfiguration.java#L80
Comment From: GregoireW
looking for this issue, I found a related one: #2841
Comment From: weiwudu
@HollisChen1 you mention:
make sure your servlet context path was added to spring.ai.mcp.server.base-url
But setting:
spring: ai: mcp: server: base-url: /myapp/ sse-endpoint: /sse sse-message-endpoint: /mcp/messageon spring-ai 1.0.0-M7, I get on
curl http://localhost:8080/ssea response containing:data:/mcp/message?sessio...so the base-url is not taken either I think
good job! This solution solved my problem
Comment From: mauricio1990silva
has this been fixed? My mcp data endpoint is still going after /mcp/message as supposed to /api/mcp/message even though I have the base-url set to /api/
spring-ai 1.0.0-M7
Comment From: CloudSen
Currently, I can only set the baseUrl to the context path.
server:
port: 8080
servlet:
context-path: /datakits/ai/web
spring:
main:
web-application-type: servlet
ai:
mcp:
server:
name: webmvc-mcp-server
version: 1.0.0
type: SYNC
stdio: false
instructions: "This server provides system current time tools"
base-url: ${server.servlet.context-path}
sse-endpoint: /sse
sse-message-endpoint: /mcp/messages
capabilities:
tool: true
resource: false
prompt: false
completion: false
Comment From: godfather1103
The problem can be solved in 1.0.0-M6 in this way
@Configuration
public class McpConfig {
@Bean
public RouterFunction<ServerResponse> mvcMcpRouterFunction(WebMvcSseServerTransport transport) {
return transport.getRouterFunction();
}
@Bean
public WebMvcSseServerTransport webMvcSseServerTransport(
ObjectMapper objectMapper,
@Nullable ServerProperties serverProperties,
McpServerProperties mcpServerProperties
) {
final String contextPath = Optional.ofNullable(serverProperties)
.map(ServerProperties::getServlet)
.map(ServerProperties.Servlet::getContextPath)
.orElse(null);
if (StringUtils.isNotEmpty(contextPath)) {
return new WebMvcSseServerTransportProxy(objectMapper, mcpServerProperties.getSseMessageEndpoint(), contextPath);
}
return new WebMvcSseServerTransport(objectMapper, mcpServerProperties.getSseMessageEndpoint());
}
public static class WebMvcSseServerTransportProxy extends WebMvcSseServerTransport {
private final WebMvcSseServerTransport serverTransport;
private final RouterFunction<ServerResponse> routerFunction;
public WebMvcSseServerTransportProxy(ObjectMapper objectMapper, String messageEndpoint, String contextPath) {
super(objectMapper, contextPath + messageEndpoint);
// fix change event:endpoint data
this.serverTransport = new WebMvcSseServerTransport(objectMapper, contextPath + messageEndpoint);
this.routerFunction = RouterFunctions.route()
.GET(DEFAULT_SSE_ENDPOINT, this::handleSseConnection)
// fix Rewrite route
.POST(messageEndpoint, this::handleMessage)
.build();
}
@Override
public Mono<Void> connect(Function<Mono<McpSchema.JSONRPCMessage>, Mono<McpSchema.JSONRPCMessage>> connectionHandler) {
return this.serverTransport.connect(connectionHandler);
}
@Override
public Mono<Void> sendMessage(McpSchema.JSONRPCMessage message) {
return this.serverTransport.sendMessage(message);
}
@Override
public <T> T unmarshalFrom(Object data, TypeReference<T> typeRef) {
return this.serverTransport.unmarshalFrom(data, typeRef);
}
@Override
public Mono<Void> closeGracefully() {
return this.serverTransport.closeGracefully();
}
private ServerResponse handleSseConnection(ServerRequest request) {
try {
var handleMessage = WebMvcSseServerTransport.class.getDeclaredMethod("handleSseConnection", ServerRequest.class);
handleMessage.setAccessible(true);
return (ServerResponse) handleMessage.invoke(this.serverTransport, request);
} catch (NoSuchMethodException | InvocationTargetException | IllegalAccessException e) {
throw new RuntimeException(e);
}
}
private ServerResponse handleMessage(ServerRequest request) {
try {
var handleMessage = WebMvcSseServerTransport.class.getDeclaredMethod("handleMessage", ServerRequest.class);
handleMessage.setAccessible(true);
return (ServerResponse) handleMessage.invoke(this.serverTransport, request);
} catch (NoSuchMethodException | InvocationTargetException | IllegalAccessException e) {
throw new RuntimeException(e);
}
}
@Override
public RouterFunction<ServerResponse> getRouterFunction() {
return routerFunction;
}
}
}
Comment From: gdrouet
any workaround for webflux?
Comment From: godfather1103
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.modelcontextprotocol.server.transport.WebFluxSseServerTransport;
import io.modelcontextprotocol.spec.McpSchema;
import org.apache.commons.lang3.StringUtils;
import org.springframework.ai.autoconfigure.mcp.server.McpServerProperties;
import org.springframework.boot.autoconfigure.web.ServerProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.lang.Nullable;
import org.springframework.web.reactive.function.server.RouterFunction;
import org.springframework.web.reactive.function.server.RouterFunctions;
import org.springframework.web.reactive.function.server.ServerRequest;
import org.springframework.web.reactive.function.server.ServerResponse;
import reactor.core.publisher.Mono;
import java.lang.reflect.InvocationTargetException;
import java.util.Optional;
import java.util.function.Function;
@Configuration
public class McpConfig {
@Bean
public RouterFunction<?> webfluxMcpRouterFunction(WebFluxSseServerTransport webFluxTransport) {
return webFluxTransport.getRouterFunction();
}
@Bean
public WebFluxSseServerTransport webMvcSseServerTransport(
@Nullable ServerProperties serverProperties,
McpServerProperties mcpServerProperties
) {
final ObjectMapper objectMapper = new ObjectMapper();
final String contextPath = Optional.ofNullable(serverProperties)
.map(ServerProperties::getServlet)
.map(ServerProperties.Servlet::getContextPath)
.orElse(null);
if (StringUtils.isNotEmpty(contextPath)) {
return new WebFluxSseServerTransportProxy(objectMapper, mcpServerProperties.getSseMessageEndpoint(), contextPath);
}
return new WebFluxSseServerTransport(objectMapper, mcpServerProperties.getSseMessageEndpoint());
}
public static class WebFluxSseServerTransportProxy extends WebFluxSseServerTransport {
private final WebFluxSseServerTransport serverTransport;
private final RouterFunction<?> routerFunction;
public WebFluxSseServerTransportProxy(ObjectMapper objectMapper, String messageEndpoint, String contextPath) {
super(objectMapper, contextPath + messageEndpoint);
// fix change event:endpoint data
this.serverTransport = new WebFluxSseServerTransport(objectMapper, contextPath + messageEndpoint);
this.routerFunction = RouterFunctions.route()
.GET(DEFAULT_SSE_ENDPOINT, this::handleSseConnection)
// fix Rewrite route
.POST(messageEndpoint, this::handleMessage)
.build();
}
@Override
public Mono<Void> connect(Function<Mono<McpSchema.JSONRPCMessage>, Mono<McpSchema.JSONRPCMessage>> handler) {
return this.serverTransport.connect(handler);
}
@Override
public Mono<Void> sendMessage(McpSchema.JSONRPCMessage message) {
return this.serverTransport.sendMessage(message);
}
@Override
public <T> T unmarshalFrom(Object data, TypeReference<T> typeRef) {
return this.serverTransport.unmarshalFrom(data, typeRef);
}
@Override
public Mono<Void> closeGracefully() {
return this.serverTransport.closeGracefully();
}
private Mono<ServerResponse> handleSseConnection(ServerRequest request) {
try {
var handleMessage = WebFluxSseServerTransport.class.getDeclaredMethod("handleSseConnection", ServerRequest.class);
handleMessage.setAccessible(true);
return (Mono<ServerResponse>) handleMessage.invoke(this.serverTransport, request);
} catch (NoSuchMethodException | InvocationTargetException | IllegalAccessException e) {
throw new RuntimeException(e);
}
}
private Mono<ServerResponse> handleMessage(ServerRequest request) {
try {
var handleMessage = WebFluxSseServerTransport.class.getDeclaredMethod("handleMessage", ServerRequest.class);
handleMessage.setAccessible(true);
return (Mono<ServerResponse>) handleMessage.invoke(this.serverTransport, request);
} catch (NoSuchMethodException | InvocationTargetException | IllegalAccessException e) {
throw new RuntimeException(e);
}
}
@Override
public RouterFunction<?> getRouterFunction() {
return routerFunction;
}
}
}
Comment From: MISAKIGA
you can upgrade to spring ai at version 1.1.0
my mcp server config is
server.port=8094
spring.application.name=hello-world-demo
server.servlet.context-path=/v1
spring.ai.mcp.server.protocol=STATELESS
spring.ai.mcp.server.name=mcp-server
spring.ai.mcp.server.version=1.0.0
spring.ai.mcp.server.type=sync
spring.ai.mcp.server.streamable-http.mcp-endpoint=/mcp
spring.ai.mcp.server.base-url=${server.servlet.context-path}
and my mcp client is
public class TestMcp {
public static void main(String[] args) {
String serverUrl = "http://127.0.0.1:8094";
HttpClientStreamableHttpTransport transport = HttpClientStreamableHttpTransport.builder(serverUrl)
// this is important for endpoint
.endpoint("/v1/mcp")
.build();
System.out.println("HttpClientSseClientTransport created");
McpSyncClient client = McpClient.sync(transport).build();
System.out.println("McpSyncClient created");
client.initialize();
System.out.println("是否已经初始化:" + client.isInitialized());
McpSchema.ListToolsResult listToolsResult = client.listTools();
List<McpSchema.Tool> tools = listToolsResult.tools();
System.out.println("获取到的tools:" + tools.stream().map(McpSchema.Tool::name).collect(Collectors.joining(",")));
for (McpSchema.Tool tool : tools) {
McpSchema.JsonSchema jsonSchema = tool.inputSchema();
System.out.println(jsonSchema);
//McpSchema.CallToolResult callToolResult = client.callTool(new McpSchema.CallToolRequest(tool.name(), Map.of("city", "北京")));
//System.out.println("获取到的结果为:==========");
//callToolResult.content().forEach(System.out::println);
}
}
}
````
this is pom.xml content
```xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<!-- 基础坐标配置 -->
<groupId>com.msga</groupId>
<artifactId>ai-demo</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>jar</packaging>
<!-- Spring Boot父级依赖管理 -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.5.8</version> <!-- 2.x系列最终版本 -->
<relativePath/>
</parent>
<!-- 项目属性配置 -->
<properties>
<java.version>17</java.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<spring-ai.version>1.1.0</spring-ai.version>
<maven.compiler.parameters>true</maven.compiler.parameters>
<maven.compiler.compilerArgument>-parameters</maven.compiler.compilerArgument>
</properties>
<!-- 核心依赖配置 -->
<dependencies>
<!-- Web服务支持(含Tomcat、Spring MVC) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- spring ai -->
<!--<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-mcp-server-webflux</artifactId>
</dependency>-->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-mcp-server-webmvc</artifactId>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-bom</artifactId>
<version>${spring-ai.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<!-- 构建插件配置 -->
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>