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/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

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>