我们将构建两个独立的 Spring Boot 应用:
- MCP Server:提供天气查询工具,支持 Stdio、SSE、Streamable HTTP 三种模式。
- MCP Client:连接 Server,获取工具并供 AI 调用。
核心依赖 (通用)
无论 Server 还是 Client,首先确保 pom.xml 包含以下基础依赖(版本号以 Spring AI 1.0.0-M6+ 或 1.1.x 为准):
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Spring AI MCP 核心 SDK -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-mcp-server-spring-boot-starter</artifactId>
<version>1.0.0-SNAPSHOT</version> <!-- 请根据实际情况调整 -->
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-mcp-client-spring-boot-starter</artifactId>
<version>1.0.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-chat-model</artifactId> <!-- 用于 Client 调用大模型 -->
</dependency>
</dependencies>
第一部分:MCP Server (服务端)
我们要在一个项目中同时支持三种协议。通常 Stdio 和 HTTP (SSE/Streamable) 是互斥的(因为 Stdio 需要独占控制台),但我们可以通过配置不同的 Bean 来演示如何定义它们。
1. 定义业务工具 (WeatherTool.java)
package com.example.mcpserver;
import org.springframework.ai.tool.annotation.Tool;
import org.springframework.ai.tool.annotation.ToolParam;
import org.springframework.stereotype.Service;
@Service
public class WeatherTool {
@Tool(description = "根据城市名称查询当前天气")
public String getWeather(@ToolParam(description = "城市名称,例如:北京、上海") String city) {
// 模拟耗时操作
try { Thread.sleep(500); } catch (InterruptedException e) {}
return String.format("🌤️ %s 的天气是:晴朗,气温 26°C,湿度 30%%。", city);
}
}
2. 纯 Java 配置 (McpServerConfig.java)
这里我们不使用 application.yml,而是手动装配 Bean。
package com.example.mcpserver;
import org.springframework.ai.mcp.server.McpServer;
import org.springframework.ai.mcp.server.McpSyncServer;
import org.springframework.ai.mcp.server.transport.StdioServerTransport;
import org.springframework.ai.mcp.server.transport.SseServerTransport;
import org.springframework.ai.mcp.server.transport.StreamableHttpServerTransport;
import org.springframework.ai.tool.ToolCallbackProvider;
import org.springframework.ai.tool.method.MethodToolCallbackProvider;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
@Configuration
@EnableWebMvc
public class McpServerConfig {
// 1. 注册工具提供者
@Bean
public ToolCallbackProvider weatherTools(WeatherTool weatherTool) {
return MethodToolCallbackProvider.builder()
.toolObjects(weatherTool)
.build();
}
// 2. 创建 MCP Server 核心实例
@Bean
public McpSyncServer mcpSyncServer(ToolCallbackProvider toolCallbackProvider) {
return McpServer.sync()
.name("weather-service")
.version("1.0.0")
.tools(toolCallbackProvider)
.build();
}
// --- 模式 A: Streamable HTTP (推荐) ---
// 对应 Profile: streamable
@Bean
@Profile("streamable")
public StreamableHttpServerTransport streamableHttpTransport(McpSyncServer mcpSyncServer) {
// 监听 /mcp 路径
return new StreamableHttpServerTransport("/mcp", mcpSyncServer);
}
// --- 模式 B: SSE ---
// 对应 Profile: sse
@Bean
@Profile("sse")
public SseServerTransport sseServerTransport(McpSyncServer mcpSyncServer) {
// SSE 通常需要两个端点:/sse (连接) 和 /message (通信)
return new SseServerTransport("/sse", "/message", mcpSyncServer);
}
// --- 模式 C: Stdio ---
// 对应 Profile: stdio
// 注意:Stdio 模式下通常不能同时开启 Web Server (Tomcat),否则会抢占控制台
@Bean
@Profile("stdio")
public StdioServerTransport stdioServerTransport(McpSyncServer mcpSyncServer) {
System.err.println("Starting MCP Server in Stdio mode...");
return new StdioServerTransport(System.in, System.out, mcpSyncServer);
}
}
3. 启动类 (McpServerApplication.java)
package com.example.mcpserver;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class McpServerApplication {
public static void main(String[] args) {
SpringApplication.run(McpServerApplication.class, args);
}
}
第二部分:MCP Client (客户端)
客户端需要连接上面的 Server。我们同样使用 Java Config 来定义连接方式。
1. 定义 ChatClient (McpClientConfig.java)
这里的核心是将 MCP 发现的工具注入到 ChatClient 中。
package com.example.mcpclient;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.model.ChatModel;
import org.springframework.ai.mcp.client.McpClient;
import org.springframework.ai.mcp.client.transport.stdio.StdioClientTransport;
import org.springframework.ai.mcp.client.transport.sse.SseClientTransport;
import org.springframework.ai.mcp.client.transport.streamablehttp.StreamableHttpClientTransport;
import org.springframework.ai.tool.ToolCallbackProvider;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;
import org.springframework.web.reactive.function.client.WebClient;
import java.util.List;
@Configuration
public class McpClientConfig {
// --- 模式 A: Streamable HTTP Client ---
@Bean
@Profile("streamable")
public McpClient streamableMcpClient() {
var transport = new StreamableHttpClientTransport(
WebClient.builder().baseUrl("http://localhost:8080").build(),
"/mcp"
);
return McpClient.sync().transport(transport).build();
}
// --- 模式 B: SSE Client ---
@Bean
@Profile("sse")
public McpClient sseMcpClient() {
var transport = new SseClientTransport(
WebClient.builder().baseUrl("http://localhost:8080").build(),
"/sse", "/message"
);
return McpClient.sync().transport(transport).build();
}
// --- 模式 C: Stdio Client ---
// 这种方式用于让 Client 自己去启动一个本地进程作为 Server
@Bean
@Profile("stdio")
public McpClient stdioMcpClient() {
// 假设你已经打包好了 server.jar
var transport = new StdioClientTransport("java", List.of("-jar", "path/to/server.jar"));
return McpClient.sync().transport(transport).build();
}
// --- 统一封装为 ChatClient ---
@Bean
public ChatClient chatClient(ChatModel chatModel, List<McpClient> mcpClients) {
// 获取第一个可用的 MCP Client (实际生产中建议用 Map 区分)
if (mcpClients.isEmpty()) throw new RuntimeException("No MCP Client found");
McpClient client = mcpClients.get(0);
// 将 MCP 工具转换为 ChatClient 可用的 ToolCallback
var tools = client.getToolCallbacks();
return ChatClient.builder(chatModel)
.defaultTools(tools)
.build();
}
}
2. 控制器测试 (ChatController.java)
package com.example.mcpclient;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class ChatController {
private final ChatClient chatClient;
public ChatController(ChatClient chatClient) {
this.chatClient = chatClient;
}
@GetMapping("/chat")
public String chat(@RequestParam(defaultValue = "北京今天天气怎么样?") String msg) {
// 这里的调用会自动触发 MCP Server 中的 getWeather 工具
return chatClient.prompt(msg).call().content();
}
}
第三部分:怎么发布与运行
由于没有配置文件,我们通过 JVM 启动参数 (-Dspring.profiles.active=...) 来切换模式。
1. 打包
在两个项目的根目录执行:
mvn clean package -DskipTests
2. 运行场景演示
场景一:HTTP Streamable 模式 (生产环境推荐)
-
启动 Server
java -jar target/mcp-server.jar --spring.profiles.active=streamableServer 启动 Tomcat,监听 8080 端口,暴露
/mcp接口。 -
启动 Client
java -jar target/mcp-client.jar --spring.profiles.active=streamableClient 初始化 HTTP 连接指向 localhost:8080/mcp。
-
测试 :
访问
http://localhost:8081/chat?msg=上海天气,Client 会通过 HTTP 调用 Server 的工具。
场景二:SSE 模式 (兼容旧版)
- 启动 Server :
--spring.profiles.active=sse - 启动 Client :
--spring.profiles.active=sse
场景三:Stdio 模式 (本地调试/IDE插件)
这种模式比较特殊,通常 Client 不需要独立运行,或者 Client 负责拉起 Server。
-
单独运行 Server (作为子进程)
java -jar target/mcp-server.jar --spring.profiles.active=stdio此时控制台会挂起,等待输入。
-
Client 调用 :
如果你在 Client 代码里配置了
StdioClientTransport指向这个 jar 包,Client 启动时会自动执行命令拉起 Server 进程,并通过管道通信。
总结
表格
| 特性 | Stdio | SSE | Streamable HTTP |
|---|---|---|---|
| Java Config 关键类 | StdioServerTransport |
SseServerTransport |
StreamableHttpServerTransport |
| Client Transport | StdioClientTransport |
SseClientTransport |
StreamableHttpClientTransport |
| 启动参数 | -Dspring.profiles.active=stdio |
-Dspring.profiles.active=sse |
-Dspring.profiles.active=streamable |
| 网络依赖 | 无 (本地进程) | 需要 Web容器 (Tomcat) | 需要 Web容器 (Tomcat) |
| 发布形式 | 命令行工具/Jar | Web服务 | Web服务 |