Bug description 使用 stdio 方式时没问题的,但使用 sse 方式,上周还可以,今天再试突然就不行了,访问/sse没反应了。 我看了帖子 https://github.com/spring-projects/spring-ai/issues/2732 并不能解决我这个问题。能告诉我时什么原因吗?或者是一个BUG?

Environment

C:\Users\zhou_>java --version
Picked up JAVA_TOOL_OPTIONS: -Dfile.encoding=UTF-8 -Dsun.jnu.encoding=UTF-8
openjdk 17.0.9 2023-10-17 LTS
OpenJDK Runtime Environment Corretto-17.0.9.8.1 (build 17.0.9+8-LTS)
OpenJDK 64-Bit Server VM Corretto-17.0.9.8.1 (build 17.0.9+8-LTS, mixed mode, sharing)

pom.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>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.4.5</version>
        <relativePath /> <!-- lookup parent from repository -->
    </parent>

    <groupId>com.example</groupId>
    <artifactId>mcp-sampling-weather-server</artifactId>
    <version>0.0.1-SNAPSHOT</version>

    <name>Spring AI MCP Sampling - Weather Server</name>
    <description>Sample Spring Boot application demonstrating MCP client and server sampling usage</description>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.ai</groupId>
                <artifactId>spring-ai-bom</artifactId>
                <version>1.0.0</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <dependencies>
        <dependency>
            <groupId>org.springframework.ai</groupId>
            <artifactId>spring-ai-starter-mcp-server-webmvc</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

application.properties

# spring.main.web-application-type=none

# NOTE: You must disable the banner and the console logging 
# to allow the STDIO transport to work !!!
spring.main.banner-mode=off
logging.pattern.console=

#spring.ai.mcp.server.stdio=true

spring.ai.mcp.server.name=mcp-sampling-server
spring.ai.mcp.server.version=0.0.1
spring.ai.mcp.server.request-timeout=30s

logging.file.name=./sampling/mcp-weather-webmvc-server/target/mcp-sampling-server.log

Comment From: koradji2046

我尝试使用curl访问:

C:\Users\zhou_>curl http://127.0.0.1:8090/sse id:5f5036e3-c822-4a56-a752-872c724d75f3 event:endpoint data:/mcp/message?sessionId=5f5036e3-c822-4a56-a752-872c724d75f3

然后没反应了

Comment From: koradji2046

清理了本地仓库后重新编译运行mcp client发现 spring-ai-starter-model-openai 与 以下包不能同时存在,否则会发生循环依赖 - spring-ai-starter-model-deepseek - spring-ai-starter-model-ollama

但可以与 spring-ai-starter-model-anthropic 同时依赖! pom.xml

    <dependencies>

        <dependency>
            <groupId>org.springframework.ai</groupId>
            <artifactId>spring-ai-starter-mcp-client</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.ai</groupId>
            <artifactId>spring-ai-starter-model-openai</artifactId>
        </dependency>

<!--        <dependency>-->
<!--            <groupId>org.springframework.ai</groupId>-->
<!--            <artifactId>spring-ai-starter-model-anthropic</artifactId>-->
<!--        </dependency>-->

<!--        <dependency>-->
<!--            <groupId>org.springframework.ai</groupId>-->
<!--            <artifactId>spring-ai-starter-model-deepseek</artifactId>-->
<!--        </dependency>-->
        <dependency>
            <groupId>org.springframework.ai</groupId>
            <artifactId>spring-ai-starter-model-ollama</artifactId>
        </dependency>

    </dependencies>
***************************
APPLICATION FAILED TO START
***************************

Description:

The dependencies of some of the beans in the application context form a cycle:

   predefinedQuestions defined in org.springframework.ai.mcp.samples.client.McpClientApplication
      ↓
   openAiChatModel defined in class path resource [org/springframework/ai/model/openai/autoconfigure/OpenAiChatAutoConfiguration.class]
┌─────┐
|  toolCallingManager defined in class path resource [org/springframework/ai/model/tool/autoconfigure/ToolCallingAutoConfiguration.class]
↑     ↓
|  toolCallbackResolver defined in class path resource [org/springframework/ai/model/tool/autoconfigure/ToolCallingAutoConfiguration.class]
↑     ↓
|  mcpToolCallbacks defined in class path resource [org/springframework/ai/mcp/client/autoconfigure/McpToolCallbackAutoConfiguration.class]
↑     ↓
|  mcpSyncClients defined in class path resource [org/springframework/ai/mcp/client/autoconfigure/McpClientAutoConfiguration.class]
↑     ↓
|  mcpSyncClientConfigurer defined in class path resource [org/springframework/ai/mcp/client/autoconfigure/McpClientAutoConfiguration.class]
↑     ↓
|  samplingCustomizer defined in org.springframework.ai.mcp.samples.client.McpClientApplication
↑     ↓
|  chatClients defined in org.springframework.ai.mcp.samples.client.McpClientApplication
↑     ↓
|  ollamaChatModel defined in class path resource [org/springframework/ai/model/ollama/autoconfigure/OllamaChatAutoConfiguration.class]
└─────┘


Action:

Despite circular references being allowed, the dependency cycle between beans could not be broken. Update your application to remove the dependency cycle.

Comment From: sunyuhan1998

spring-ai-starter-model-openai 与 以下包不能同时存在,否则会发生循环依赖

  • spring-ai-starter-model-deepseek
  • spring-ai-starter-model-ollama

I have not reproduced the problem, can you provide a minimal project that would reproduce the problem?

Comment From: koradji2046

是我没有说清楚, 实际上我使用的是 https://github.com/spring-projects/spring-ai-examples/blob/main/model-context-protocol/sampling/mcp-sampling-client/pom.xml 在学习这个例子过程中,我做了以下几件事: - 将spring ai的版本从 1.0.0-SNAPSHOT 改成了1.0.0 - 将spring-ai-starter-model-anthropic替换成了spring-ai-starter-model-ollma - 修改了相关的pom.xml和application.properties如前面所述。

结合 https://github.com/spring-projects/spring-ai-examples/tree/main/model-context-protocol/sampling/mcp-weather-webmvc-server 进行调试,就是这样,开始发现 /sse无法访问,然后我检查了上周写过的另外一个程序,/sse也不能访问了,但当时是可以的。我考虑到过程中曾经在SANPSHOT和RC、以及RELEASE之间反复切换,因此清除了本地仓库,然后就发现循环依赖。今天定位发现可能循环依赖的原因与之前描述的不一致,是spring-ai-starter-mcp-client 与 spring-ai-starter-model-* 共存的情况下,使用McpSyncClientCustomizer 会导致循环依赖: https://github.com/spring-projects/spring-ai-examples/blob/main/model-context-protocol/sampling/mcp-sampling-client/src/main/java/org/springframework/ai/mcp/samples/client/McpClientApplication.java

@Bean
    McpSyncClientCustomizer samplingCustomizer(Map<String, ChatClient> chatClients) {

        return (name, mcpClientSpec) -> {

            mcpClientSpec = mcpClientSpec.loggingConsumer(logingMessage -> {            
                System.out.println("MCP LOGGING: [" + logingMessage.level() + "] " + logingMessage.data());         
            });

            mcpClientSpec.sampling(llmRequest -> {
                var userPrompt = ((McpSchema.TextContent) llmRequest.messages().get(0).content()).text();
                String modelHint = llmRequest.modelPreferences().hints().get(0).name();

                ChatClient hintedChatClient = chatClients.entrySet().stream()
                        .filter(e -> e.getKey().contains(modelHint)).findFirst()
                        .orElseThrow().getValue();

                String response = hintedChatClient.prompt()
                        .system(llmRequest.systemPrompt())
                        .user(userPrompt)
                        .call()
                        .content();

                return CreateMessageResult.builder().content(new McpSchema.TextContent(response)).build();
            });
            System.out.println("Customizing " + name);
        };
    }

但目前/sse依然无法访问。

嗯,这就是全部过程。问题有2个: 1. McpSyncClientCustomizer 在 spring-ai-starter-mcp-client 与 spring-ai-starter-model-* 共存时会导致循环依赖 2. 第三方工具接入MCP server stdio模式ok,而sse方式连接超时。

希望能指点迷津,谢谢。

Comment From: sunyuhan1998

OK

1. Regarding McpSyncClientCustomizer causing circular dependencies:

I tend to think that this is an issue with the demo itself and not with the Spring AI framework. the McpSyncClientCustomizer given by the demo expects to select the corresponding ChatClient based on the model's prompts and use it to generate thereplies.I think if we just want to break the circular dependency while maintaining this functionality, we just need to add a @Lazy annotation:

McpSyncClientCustomizer samplingCustomizer(@Lazy Map<String, ChatClient> chatClients)

2. Regarding the "SSE method of connecting to the timeout":

In fact, when accessing the /sse interface of a Spring AI-built MCP server via curl, the mcp server return:

C:\Users\zhou_>curl http://127.0.0.1:8090/sse id:5f5036e3-c822-4a56-a752-872c724d75f3 event:endpoint data:/mcp/message?sessionId=5f5036e3-c822-4a56-a752-872c724d75f3

and then no response, this is not a problem, this is precisely to prove that you are correctly connected to the MCP server, the return indicates that this is an endpoint event, and the client is instructed to continue by accessing /mcp/message?sessionId=5f5036e3-c822-4a56-a752- 872c724d75f-872c724d75f3 to continue the communication.This is actually a normal communication pattern for MCP services based on the SSE protocol.

3. Finally

if you still have any questions, I think the official demo content is slightly complicated, I wrote a minimized mcp server and client demo: spring-ai-repro-demo can be used for your reference, to use it you just need to install an ollama!

Comment From: koradji2046

OK

1. Regarding McpSyncClientCustomizer causing circular dependencies:

I tend to think that this is an issue with the demo itself and not with the Spring AI framework. the McpSyncClientCustomizer given by the demo expects to select the corresponding ChatClient based on the model's prompts and use it to generate thereplies.I think if we just want to break the circular dependency while maintaining this functionality, we just need to add a @Lazy annotation:

McpSyncClientCustomizer samplingCustomizer(@Lazy Map<String, ChatClient> chatClients)

2. Regarding the "SSE method of connecting to the timeout":

In fact, when accessing the /sse interface of a Spring AI-built MCP server via curl, the mcp server return:

C:\Users\zhou_>curl http://127.0.0.1:8090/sse id:5f5036e3-c822-4a56-a752-872c724d75f3 event:endpoint data:/mcp/message?sessionId=5f5036e3-c822-4a56-a752-872c724d75f3

and then no response, this is not a problem, this is precisely to prove that you are correctly connected to the MCP server, the return indicates that this is an endpoint event, and the client is instructed to continue by accessing /mcp/message?sessionId=5f5036e3-c822-4a56-a752- 872c724d75f-872c724d75f3 to continue the communication.This is actually a normal communication pattern for MCP services based on the SSE protocol.

3. Finally

if you still have any questions, I think the official demo content is slightly complicated, I wrote a minimized mcp server and client demo: spring-ai-repro-demo can be used for your reference, to use it you just need to install an ollama!

我下载了你的代码并测试,但依然如故,出错的几句依然很高。我怀疑问题是由于资源限制,当大模型给出连接或响应的时间会比较长,Spring AI 提供的默认的超时时间往往不够, ChatModel 出错导致整个请求出现错误。而Spring AI 的超时配置过于隐晦,我以为非流式接口使用的是 Spring 的 RestClient,流式接口使用的是 Spring 的 WebClient,因此添加了以下代码:

WebClient 设置超时

import io.netty.channel.ChannelOption;
import io.netty.handler.timeout.ReadTimeoutHandler;
import io.netty.handler.timeout.WriteTimeoutHandler;
import org.springframework.boot.web.reactive.function.client.WebClientCustomizer;
import org.springframework.http.client.reactive.ReactorClientHttpConnector;
import org.springframework.stereotype.Component;
import org.springframework.web.reactive.function.client.WebClient;
import reactor.netty.http.client.HttpClient;
import reactor.netty.tcp.TcpClient;

import java.time.Duration;

@Component
public class MyWebClientCustomizer implements WebClientCustomizer {
    @Override
    public void customize(WebClient.Builder webClientBuilder) {
        HttpClient httpClient = HttpClient.create()
                .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 3 * 60 * 1000) // 连接超时 3 分钟
                .responseTimeout(Duration.ofMinutes(3));  // 设置超时时间 3 分钟

        webClientBuilder.clientConnector(new ReactorClientHttpConnector(httpClient));
    }
}

RestClient设置超时

import org.springframework.boot.web.client.RestClientCustomizer;
import org.springframework.http.client.JdkClientHttpRequestFactory;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestClient;

import java.net.http.HttpClient;
import java.time.Duration;

@Component
public class MyRestClientCustomizer implements RestClientCustomizer {
    @Override
    public void customize(RestClient.Builder restClientBuilder) {
        HttpClient httpClient = HttpClient.newBuilder()
                .connectTimeout(Duration.ofMinutes(3)) // 设置超时时间 3 分钟
                .build();

        JdkClientHttpRequestFactory requestFactory = new JdkClientHttpRequestFactory(httpClient);
        requestFactory.setReadTimeout(Duration.ofMinutes(3)); // 读取超时 3 分钟

        RestClient.builder().requestFactory(requestFactory);
    }
}

但依然会时不时报错:

error

reactor.core.Exceptions$ReactiveException: java.util.concurrent.TimeoutException: Did not observe any item or terminal signal within 20000ms in 'Mono.deferContextual ⇢ at io.modelcontextprotocol.spec.McpClientSession.sendRequest(McpClientSession.java:233)' (and no fallback has been configured)

奇了怪,还是报错,为啥?请问这个超时要怎么处理呢?

Comment From: sunyuhan1998

我下载了你的代码并测试,但依然如故,出错的几句依然很高。我怀疑问题是由于资源限制,当大模型给出连接或响应的时间会比较长,Spring AI 提供的默认的超时时间往往不够, ChatModel 出错导致整个请求出现错误。

It seems to be difficult to reproduce the timeout issue you're talking about in my environment, but I think that if you want to adjust the timeout for the request, perhaps you could just construct the OllamaApi object yourself, either by passing RestClient.Builder and WebClient.Builder yourself via Constructor:

https://github.com/spring-projects/spring-ai/blob/694bb50be5825bb2d91f3ce0291020d9aadcb780/models/spring-ai-ollama/src/main/java/org/springframework/ai/ollama/api/OllamaApi.java#L78

or Builder:

https://github.com/spring-projects/spring-ai/blob/694bb50be5825bb2d91f3ce0291020d9aadcb780/models/spring-ai-ollama/src/main/java/org/springframework/ai/ollama/api/OllamaApi.java#L729-L739

Maybe you can adjust the timeout by defining the Builder.