一、什么是MCP
模型上下文协议(MCP)是一种标准化协议,使 AI 模型能够以结构化的方式与外部工具和资源进行交互。它支持多种传输机制,以在不同环境中提供灵活的使用方式。
二、MCP传输层协议
stdio传输层
stdio(标准输入输出)传输层是MCP最基本的传输实现方式。它通过进程间通信(IPC)实现,具体工作原理如下:
- 进程创建:MCP客户端会启动一个子进程来运行MCP服务器
- 通信机制:
- 使用标准输入(stdin)向MCP服务器发送请求
- 通过标准输出(stdout)接收MCP服务器的响应
- 标准错误(stderr)用于日志和错误信息
- 优点:
- 简单可靠,无需网络配置
- 适合本地部署场景
- 进程隔离,安全性好
- 缺点:
- 仅支持单机部署
- 不支持跨网络访问
- 每个客户端需要独立启动服务器进程
SSE传输层
SSE(Server-Sent Events)传输层是基于HTTP的单向通信机制,专门用于服务器向客户端推送数据。其工作原理如下:
- 连接建立:
- 客户端通过HTTP建立与服务器的持久连接
- 使用
text/event-stream内容类型
- 通信机制:
- 服务器可以主动向客户端推送消息
- 支持自动重连机制
- 支持事件ID和自定义事件类型
- 优点:
- 支持分布式部署
- 可跨网络访问
- 支持多客户端连接
- 轻量级,使用标准HTTP协议
- 缺点:
- 需要额外的网络配置
- 相比stdio实现略微复杂
- 需要考虑网络安全性
三、使用Spring AI编写MCP服务端
Studio协议
新建一个Spring MVC的工程,引入依赖
<!-- Spring Web -->
<spring-web.version>6.2.0</spring-web.version>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
<version>${spring-web.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-mcp-server-webmvc</artifactId>
</dependency>
新增配置文件
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.name=mcp-server
spring.ai.mcp.server.version=0.0.1
logging.file.name=D:/Projects/IdeaProjects/spring-ai-learn/mcp-server/target/mcp-stdio-server.log
新建一个Service,编写MCP能提供的服务,例子里面调用了网络的的今日简报、微博热搜的接口,提供查询新闻热点的能力
package com.renne.ai.learn.mcpserver.service;
import com.alibaba.fastjson2.JSONArray;
import com.alibaba.fastjson2.JSONObject;
import org.slf4j.Logger;
import org.springframework.ai.tool.annotation.Tool;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestClient;
/**
* @author LiuYu
* @since 2025-03-27 18:42
*/@Service
public class McpToolsService {
private static final Logger logger = org.slf4j.LoggerFactory.getLogger(McpToolsService.class);
private static final String BASE_URL = "https://whyta.cn/api";
private final RestClient restClient;
public McpToolsService() {
this.restClient = RestClient.builder()
.baseUrl(BASE_URL)
.defaultHeader("Accept", "application/json")
.defaultHeader("User-Agent", "OpenMeteoClient/1.0")
.build();
}
/**
* 获取今日简报
* 此方法调用外部API获取今日新闻,并返回新闻内容的字符串表示
*
* @return 今日新闻的字符串表示
*/
@Tool(description = "获取今日榜单数据")
public String getTodayBulletin() {
try {
String news = restClient.get()
.uri("/tx/bulletin?key=36de5db81215")
.retrieve()
.body(String.class);
return news != null ? formatBulletin(news) : "No news available today.";
} catch (Exception e) {
return "Failed to fetch news: " + e.getMessage();
}
}
/**
* 获取今日微博热搜
* 此方法调用外部API获取今日微博热搜,并返回新闻内容的字符串表示
*
* @return 今日新闻的字符串表示
*/
@Tool(description = "获取微博热搜数据")
public String getTodayWeiboHot() {
try {
String news = restClient.get()
.uri("/tx/weibohot?key=36de5db81215")
.retrieve()
.body(String.class);
return news != null ? formatWeiboHot(news) : "No news available today.";
} catch (Exception e) {
return "Failed to fetch news: " + e.getMessage();
}
}
/**
* 将数据格式化为指定的简报格式
*
* @param newsData 热搜数据
* @return 格式化后的字符串
*/
public String formatWeiboHot(String newsData) {
JSONObject jsonObject = JSONObject.parseObject(newsData);
JSONArray newsList = jsonObject.getJSONObject("result").getJSONArray("list");
StringBuilder bulletin = new StringBuilder("以下是今日微博热搜:\n");
bulletin.append("---------------------------------\n");
for (int i = 0; i < newsList.size(); i++) {
JSONObject news = newsList.getJSONObject(i);
bulletin.append(" - 热搜话题:").append(news.getString("hotword")).append("\n");
bulletin.append(" - 热搜指数:").append(news.getString("hotwordnum")).append("\n");
bulletin.append(" - 热搜标签:").append(news.getString("hottag")).append("\n");
if (i < newsList.size() - 1) {
bulletin.append("---------------------------------\n");
}
}
return bulletin.toString();
}
/**
* 将新闻数据格式化为指定的简报格式
*
* @param newsData 新闻数据,包含新闻列表
* @return 格式化后的简报字符串
*/
public String formatBulletin(String newsData) {
JSONObject jsonObject = JSONObject.parseObject(newsData);
JSONArray newsList = jsonObject.getJSONObject("result").getJSONArray("list");
StringBuilder bulletin = new StringBuilder("以下是今日简报:\n");
bulletin.append("---------------------------------\n");
for (int i = 0; i < newsList.size(); i++) {
JSONObject news = newsList.getJSONObject(i);
bulletin.append(" - 新闻标题:").append(news.getString("title")).append("\n");
bulletin.append(" - 新闻时间:").append(news.getString("mtime")).append("\n");
bulletin.append(" - 简报内容:").append(news.getString("digest")).append("\n");
if (i < newsList.size() - 1) {
bulletin.append("---------------------------------\n");
}
}
return bulletin.toString();
}
}
增加配置项,将编写的Service放入到MethodToolCallbackProvider的toolObjects中
package com.renne.ai.learn.mcpserver.config;
import com.renne.ai.learn.mcpserver.service.McpToolsService;
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;
/**
* @author LiuYu
* @since 2025-03-28 17:43
*/@Configuration
public class ToolCallbackConfig {
@Bean
public ToolCallbackProvider mcpTools(McpToolsService mcpToolsService) {
return MethodToolCallbackProvider.builder().toolObjects(mcpToolsService).build();
}
}
编译打包,得到一个jar包,这样我们完成了一个MCP的客户端,用来提供新闻热点的服务。 Cursor最近增加MCP Servers可以支持添加MCP客户端,我们将我们自己写的放入提供给Cursor调用一下; 在mcp.json中加入执行jar的命令
{
"mcpServers": {
"mcp-server": {
"command": "cmd",
"args": [
"/c",
"java",
"-Dspring.ai.mcp.server.stdio=true",
"-Dspring.main.web-application-type=none",
"-Dlogging.pattern.console=",
"-jar",
"D:\\Projects\\IdeaProjects\\spring-ai-learn\\mcp-server\\target\\mcp-server-0.0.1-SNAPSHOT.jar"
],
"env": {}
}
}
}
这里就可以看到我们的服务已经可用了
我们来调用一下,向Cursor提问今天有哪些新闻,这里可以看到,他调用了我们微博热点和今日简报的两个方法,进行总结之后进行了回答
在我们自己的Spring AI MCP的客户端中使用MCP,首先引入依赖
<!-- Spring AI -->
<spring-ai.version>1.0.0-M6</spring-ai.version>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-mcp-client-spring-boot-starter</artifactId>
<version>${spring-ai.version}</version>
</dependency>
在配置文件中增加mcp的相关配置
spring:
ai:
mcp:
client:
stdio:
servers-configuration: classpath:/mcp-servers-config.json
# 直接配置
# connections:
# server1:
# command: java
# args:
# - -jar
# - D:\Projects\IdeaProjects\spring-ai-learn\mcp-server\target\mcp-server-0.0.1-SNAPSHOT.jar # 放一个绝对路径,修改为server jar包所在位置
增加配置文件中mcp-servers-config.json
{
"mcpServers": {
"weather": {
"command": "java",
"args": [
"-Dspring.ai.mcp.server.stdio=true",
"-Dspring.main.web-application-type=none",
"-Dlogging.pattern.console=",
"-jar",
"D:\\Projects\\IdeaProjects\\spring-ai-learn\\mcp-server\\target\\mcp-server-0.0.1-SNAPSHOT.jar"
],
"env": {}
}
}
}
编写Controller调用,模型依然是前一期中使用硅基流动的7B的千问模型,相关配置可以去看前一期
package com.renne.ai.learn.mcp.controller;
import io.modelcontextprotocol.client.McpSyncClient;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.client.advisor.AbstractChatMemoryAdvisor;
import org.springframework.ai.chat.model.ChatModel;
import org.springframework.ai.mcp.SyncMcpToolCallbackProvider;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
/**
* @author LiuYu
* @since 2025-03-27 11:46
*/@RestController
@RequestMapping("/openai/mcp")
public class McpController {
private final ChatModel chatModel;
private final List<McpSyncClient> mcpSyncClients;
public McpController(ChatModel chatModel, List<McpSyncClient> mcpSyncClients) {
this.chatModel = chatModel;
this.mcpSyncClients = mcpSyncClients;
}
/**
* 调用mcp对话
* 该方法接收一个可选的查询参数prompt和一个chatId参数,然后使用这些参数通过OpenAI聊天客户端生成响应内容
*
* @param message 查询参数,用于提示聊天内容,可为空
* @param chatId 聊天会话ID,用于关联聊天记录
* @return 返回由OpenAI聊天客户端生成的响应内容
*/
@GetMapping("/chat")
public String get(@RequestParam(value = "message", required = false) String message, String chatId) {
// 使用OpenAI聊天客户端处理提示和聊天记录,并调用工具生成最终的响应内容
return ChatClient.create(chatModel)
.prompt(message) // 设置聊天提示内容
.advisors(a -> a
.param(AbstractChatMemoryAdvisor.CHAT_MEMORY_CONVERSATION_ID_KEY, chatId) // 设置聊天会话ID
.param(AbstractChatMemoryAdvisor.CHAT_MEMORY_RETRIEVE_SIZE_KEY, 100)) // 设置聊天记录检索数量
.tools(new SyncMcpToolCallbackProvider(mcpSyncClients))
.call() // 调用OpenAI聊天客户端生成响应
.content(); // 获取生成的响应内容
}
}
我们来调用一下,可以看到模型也调用我们MCP的服务成功了,根据微博热点信息进行总结返回了
SSE协议
之后会补充