一、概述
1.1 背景
目前,大模型(如 ChatGPT、DeepSeek)发展十分迅速,而它们需要基于一定的上下文来回答问题。我们与模型的每一次交互,都会携带一定的上下文,例如我们之前提过的问题、提问的方式、当前任务的进展等,当大模型有了充足的上下文时,它就能更好地回答我们的问题。
如果大模型的上下文内容不充足时,它就无法很精准地回答。例如,问大模型今天我们食堂做的菜大部分是荤菜还是素菜,它肯定不知道这个答案。而如果我们告诉它食堂今天有哪些菜,它就能依此分析出答案。
但是,我们每次都需要手动把上下文信息发给它,就没有一种自动化的方式,让大模型能够自己就获取上下文吗?
为了解决这一问题,我们可能会想开发一个程序来实现自动化地获取上下文,但这时又出现了一个新的问题,大模型要怎么去调用程序 ?目前,大模型本身是无法调用程序的,因此需要一个中介程序来完成调用的工作。
之后,有人尝试着使用如下的模式来调用外部程序:

(1)中介程序告诉大模型用户问题,以及工具的使用说明书。
(2)大模型告诉中介程序要使用的工具和参数。
(3)中介程序调用外部程序(这里示例为爬虫程序),并将调用结果返回给大模型。
(4)大模型返回最终结果给中介程序。
其实可以看出,这里的中介程序就相当于 CPU,编排着各个事情。
但是,这种模式也存在着一些问题,例如有时大模型不会按照说明书里的方式返回我们需要使用的工具和参数,以及如果外部程序比较复杂,我们需要编写很长的 prompt,这不仅会耗费大量的 token,还可能会使得大模型无法准确解析出说明书内容。
基于这些问题,Function Call、MCP 等应运而生,本篇内容以介绍 MCP 为主。
1.2 Function Call
在讲 MCP 之前,有必要先讲讲在 MCP 之前出现的 Function Call。
过去的大模型遵循指令的能力不强,即使告诉了它说明书,它也很可能并不会按照说明书上的方式和你对接,这就会使得中介程序无法完成后续的事情。针对这一问题,OpenAI 提出了 Function Call 技术,其中专门定义了规范,用于标准化过程。具体来说,Function Call 使用 JSON 格式来描述工具说明书和大模型返回的结果,并定义了专门的字段来指示各个信息,如使用哪些工具、使用什么参数等。例如:

此时,交互就变为如下:

具备 Function Call 能力的大模型,会依据着这样的规范来完成与外部的交互。而目前具备 Function Call 能力的大模型也越来越多,例如 ChatGPT、Claude 等。
这里就不继续展开讲述 Function Call,还是以 MCP 为主。
1.3 MCP
MCP的全称是模型上下文协议(Model Context Protoco l),由 Anthropic 在2024年11月推出(文章:Introducing the Model Context Protocol)。它是一个开源的通信标准,定义了应用程序和 AI 模型之间交换上下文信息的方式,使得开发者能以统一的协议将各种数据源、工具和功能连接到大模型中。
简单来说,MCP 就是大模型的标准化工具箱,大模型可以利用这些工具与外界互动,获取信息并完成具体任务。

实际上,MCP就类似于一个中间协议层,使得大模型能够通过标准的方式来使用不同的工具。

相比于 Function Call,MCP 的好处就是使得工具的开发更为简单,开发时只需遵循 MCP 协议,就能轻松接入大模型,类似于插件式开发。
二、原理
2.1 MCP架构
MCP 采用 C/S 架构:

- 主机:负责接收用户的提问,并与大模型进行交互。这里可以理解为调用大模型的客户端,如 Claude For Desktop、Cline、Cherry Studio 等。
- MCP客户端:负责与 MCP 服务器以 MCP 协议的方式进行交互。具备 MCP 能力的主机通常内置 MCP 客户端。
- MCP服务器:轻量级程序,提供特定的能力。
- 本地数据源:MCP 服务器可安全访问的计算机文件、数据库和服务。
- 远程服务:MCP 服务器可连接的互联网上的外部系统,如通过 API。
2.2 MCP调用过程

(1)用户向 Host 发起提问。
(2)Host 会把问题以及 MCP 的可用列表、使用方法等告诉 LLM。
(3)LLM 会自行决定使用哪个 MCP 工具,然后将使用的工具和参数返回给 Host。
(4)Host 启动 MCP Client。
(5)MCP Client 会向指定的 MCP Server 发起连接,然后将请求封装为标准的 MCP 协议格式信息,并发起请求。
(6)MCP Server 会访问指定的数据源获取信息,然后将响应封装为标准的 MCP 协议格式信息,并返回给 MCP Client。
(7)MCP Client 将响应内容返回给Host。
(8)Host 将用户问题、MCP 调用上下文发送给LLM。
(9)LLM 整理结果后,将最终结果返回给 Host,Host 再将结果返回给用户。
由上图可以看出,执行的动作其实还是由 Host 完成的,并不是由 LLM 完成。MCP 定义的 Host 和上文提到的中介程序,本质是相同的,都完成各个事情的编排。其次,MCP 这个协议只是规定了 MCP Client 与 MCP Server 之间的交互,但与 LLM 的交互无关,与 LLM 交互时,其实只是传递了相应的 prompt,而并不会要求 LLM 以 MCP 协议格式来传递信息。
2.3 MCP抓包分析
在之前讲述 Function Call 时说过,中介程序会把外部工具的使用说明书通过 JSON 的形式发送给 LLM。而在 MCP 中,则有所差别。
先来看看下面抓的包,这是 Host 向 LLM 发起请求时的包内容:

这里其实可以看出,MCP 并没有使用 Function Call 的规范,而是使用了最直接的方式,把所有外部工具的说明书直接写在了 prompt 里(内容长达 6w 个字符),并设定了角色 system。而用户的问题则通过 user 项分开给出。

其实,对于 MCP 来说,Host 请求给 LLM 的内容除了像这样直接塞到 prompt 里,也可以像 Function Call 那样以一种规范的形式呈现。这个具体依赖于 Host 的实现,而不是 MCP 的实现。 正如在调用过程中所讲述的一样,MCP这个协议只是规定了 MCP Client 与 MCP Server 之间的交互,但与 LLM 的交互无关。
这里补充一个知识点:在与 LLM 进行交互时,常包含三种角色:system、user、assistant。
● system:系统设定者,用于定义对话前的规则和语境,相当于给 LLM 一个前置的 prompt。
● user:用户,即与大模型交互的人类。
● assistant:由大模型生成的响应者,也就是 AI 自己。
一个完整的对话通常是:system -> user -> assistant -> user -> assistant -> ... 大部分 LLM 都采用了这个结构,且以 JSON 形式交互,示例:

由于 LLM 并不具备记忆能力,因此在每一轮与 LLM 对话时,不仅会带上当前问题,还会带上历史的 Q-A json 数据,即虽然看似我们每次都只向大模型提出一次问题,但实际上请求中会携带历史的所有数据,类似于上图展示的 json 列表。
结合上面的 MCP 抓包,可以看出有的 Host 会选择以 system 的方式把 MCP 工具的使用方法告诉 LLM,来让大模型提前掌握一些上下文。
2.4 MCP传输机制
目前,MCP 规范定义了两种标准的传输机制,即 Stdio 和 HTTP with SSE,消息的格式都为 JSON-RPC。
在 Stdio 中,MCP 客户端将 MCP 服务器作为一个子进程来启动,服务端通过标准输入(stdin)接收消息,并通过标准输出(stdout)响应消息。除此之外,服务端可以通过标准错误(stderr)来记录日志。

在 HTTP with SSE 中,服务端作为独立进程运行,可以处理多个客户端连接。服务端提供两个端点:一个 SSE 端点 ,用于客户端建立长连接并持续接收服务端推送的消息,另一个 HTTP POST 端点用于客户端主动向服务端发送消息。
SSE 端点可以理解为客户端与服务端之间的一个连接接口,可以用于服务端向客户端不断推送消息。但由于 SSE 是服务端到客户端的单向传输协议,因此这里还需要一个 HTTP POST 端点,用于客户端到服务端方向的传输接口。

Stdio 与 HTTP with SSE 的区别:
传输方式 | Stdio | HTTP with SSE |
---|---|---|
位置 | 本地,即客户端与服务端在一台机器上 | 本地或远程 |
客户端量 | 单客户端 | 多客户端 |
性能 | 低延迟 | 网络开销带来的较高延迟 |
复杂度 | 较简单 | 较复杂,需 HTTP 服务器 |
安全性 | 依赖本地权限,相对安全 | 需显式的安全措施,如 TLS |
网络访问 | 不需要 | 需要 |
可扩展性 | 有限 | 可网络扩展 |
部署 | 每个用户安装 | 中心化部署 |
资源使用 | 客户端资源 | 服务端资源 |
主要用途 | 本地工具、IDE 插件 | 远程服务、公共服务 |
三、实践
这里主要讲述使用 Spring AI 框架开发 MCP Client 与 MCP Server 的方式。
3.1 前置环境
配置名 | 说明 |
---|---|
JDK | 17 |
SpringBoot | 3.4.4 |
构建工具 | Maven |
LLM | Qwen2.5-72B-Instruct |
3.2 MCP Server开发
3.2.1 核心依赖
之前讲述了 MCP 规范中的传输机制有 Stdio 和 HTTP with SSE,在 Spring AI 中,不同的传输机制也有对应的依赖。
Stdio:
xml
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-mcp-server</artifactId>
<version>1.0.0-M7</version> <!-- 版本可以选择其他的 -->
</dependency>
Spring AI 提供了两种提供 SSE 的方式,分别是基于 Spring MVC 的 SSE 和基于 Spring WebFlux 的 SSE,两种对应的依赖如下:
xml
<!--Spring MVC-->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-mcp-server-webmvc</artifactId>
<version>1.0.0-M7</version> <!-- 版本可以选择其他的 -->
</dependency>
<!--Spring WebFlux-->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-mcp-server-webflux</artifactId>
<version>1.0.0-M7</version> <!-- 版本可以选择其他的 -->
</dependency>
这三个依赖任选一个即可,后面会讲述不同依赖对应的调用方式。
3.2.2 配置文件
不同的传输方式,配置文件也不一样,这里任选一种即可。
3.2.2.1 Stdio
yml
spring:
ai:
mcp:
server:
name: mcp-server
version: 1.0.0
type: SYNC # 这里可以赋值为SYNC/ASYNC,表示同步/异步
3.2.2.2 SSE
yml
spring:
ai:
mcp:
server:
name: mcp-server
version: 1.0.0
type: SYNC
sse-message-endpoint: /mcp/messages # SSE端点
3.2.3 开发
可以使用 @Tool 将一个方法声明为工具,这个注解的属性 description 用于表示这个工具的用途(建议为每个工具方法都写上 description 属性,因为在交互时 description 会被传递给 LLM,让 LLM 知道每个工具的用途)。示例:
java
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.ai.tool.annotation.Tool;
import org.springframework.context.i18n.LocaleContextHolder;
import org.springframework.stereotype.Service;
@Service
public class DateTimeTools {
private final Logger logger = LoggerFactory.getLogger(DateTimeTools.class);
@Tool(description = "获取当前时间")
String getCurrentDateTime() {
return LocalDateTime.now().atZone(LocaleContextHolder.getTimeZone().toZoneId()).toString();
}
@Tool(description = "设置闹钟,需要提供ISO-8601格式的时间")
void setAlarm(String time) {
LocalDateTime alarmTime = LocalDateTime.parse(time, DateTimeFormatter.ISO_DATE_TIME);
System.out.println("Alarm set for " + alarmTime);
}
}
接下来需要将 @Tool 注解的方法注册为可供 LLM 调用的工具,在 Spring AI 中,对应的是 ToolCallbackProvider 类。示例:
java
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 com.xllz.mcpserver.server.DateTimeTools;
@Configuration
public class ToolsConfig {
@Bean
public ToolCallbackProvider tools(DateTimeTools dateTimeTools) {
return MethodToolCallbackProvider.builder().toolObjects(dateTimeTools).build();
}
}
3.2.4 测试
这里先使用可以调用 MCP Server 的软件测试:Cherry Studio。
3.2.4.1 Stdio
Stdio 的配置实际上就是我们用 java 命令启动一个 jar 包,因此配置之前需要先用 maven 将 MCP Server 打成一个 jar 包(这种方式不需要我们在 IDEA 中启动服务,正如之前在 MCP 传输机制中说的,MCP Client 会自行开启一个子进程来启动 MCP Server,无需我们手动启动),然后配置如下内容:

之后需要在对话中选择配置好的 MCP Server。

然后测试:

可以看到,这里调用了刚才在 MCP Server 中定义的两个工具。
3.2.4.2 SSE
接下来讲述 SSE 的配置。在配置之前,需要我们先手动启动 MCP Server 的服务。

然后在 Cherry Studio 中进行如下的配置:

然后测试:


在服务的控制台中也能够看到其被调用的日志。

3.3 MCP Client 开发
下面使用 Spring AI 开发 MCP Client 来调用之前编写的 MCP Server。
3.3.1 核心依赖
Spring AI 提供的 MCP Client 的依赖有两个(任选一个即可):
xml
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-mcp-client</artifactId>
<version>1.0.0-M7</version> <!-- 版本可以选择其他的 -->
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-mcp-client-webflux</artifactId>
<version>1.0.0-M7</version> <!-- 版本可以选择其他的 -->
</dependency>
其中第一个既可以使用 Stdio,也可以使用 SSE。而第二个只适用于 SSE,但与第一个不同的是,第一个使用的 SSE 基于 HttpClient 实现,而第二个基于 WebFlux 实现。
需要注意的是,客户端侧除了与 MCP Server 交互,还需要与 LLM 交互,因此这里还需要添加 LLM 相关的依赖。这里以 OpenAI 为例:
xml
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-model-openai</artifactId>
<version>1.0.0-M7</version> <!-- 版本可以选择其他的 -->
</dependency>
此外,Spring AI 构建的 MCP Client 还需要加入 web 依赖,否则启动会报错。
xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
3.3.2 配置文件
同样的,这里也是任选一种传输方式即可。
3.3.2.1 Stdio
对于 Stdio 的传输方式,配置方式和之前使用 Cherry Studio 的配置很类似,如下:
yml
server:
port: 8081
spring:
ai:
mcp:
client:
toolcallback:
enabled: true
stdio:
root-change-notification: true
connections:
server1:
command: java
args:
- -jar
- /Users/zhanghaisen/Desktop/develop/code/mcp/mcp-server/target/mcp-server-0.0.1-SNAPSHOT.jar
注意:如果使用 Stdio 传输方式,在使用 Spring AI 编写 MCP Client 时,由于 Client 与 MCP Server 之间通过 Stdio 通信,因此 Server 启动时必须严格避免向标准输出写入非协议格式的数据。SpringBoot 在默认配置下会在启动时输出横幅信息(Banner)以及大量日志内容,这些信息会干扰 MCP 协议的正常解析,从而导致 Client 在启动阶段发生协议错误,进而启动失败。
因此,Server 必须在配置文件中显式关闭横幅输出和日志输出,配置如下:
yml
spring:
main:
banner-mode: off
logging:
pattern:
console:
level:
root: off
3.3.2.2 SSE
对于 SSE 的传输方式,配置方式也是类似的,如下:
yml
spring:
ai:
mcp:
client:
toolcallback:
enabled: true
sse:
connections:
server1:
url: http://localhost:8080
3.3.2.3 LLM
除了传输方式的配置外,还要对 LLM 进行配置,如密钥等。这里以 OpenAI 为例:
yml
spring:
ai:
openai:
base-url: [这里填url]
api-key: [这里填密钥]
chat:
options:
model: [这里填使用的模型]
3.3.3 开发
首先声明 ChatClient 类的 Bean,这个类用于与 LLM 交互。
java
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.tool.ToolCallbackProvider;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class ChatClientConfig {
@Bean
public ChatClient initChatClient(ChatClient.Builder chatClientBuilder, ToolCallbackProvider mcpTools) {
return chatClientBuilder.defaultTools(mcpTools).build();
}
}
然后编写一个 Controller,用于输入问题并调用 LLM。
java
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import jakarta.servlet.http.HttpServletResponse;
import reactor.core.publisher.Flux;
@RestController
public class DateTimeController {
@Autowired
private ChatClient chatClient;
/**
* 非流式传输
*/
@GetMapping("/chat")
public String chat(String input) {
return chatClient.prompt().user(input).call().content();
}
/**
* 流式传输
*/
@GetMapping("/chat/stream")
public Flux<String> streamChat(HttpServletResponse response, String input) {
response.setCharacterEncoding("UTF-8");
return chatClient.prompt().user(input).stream().content();
}
}
如果传输方式为 Stdio,需要将 MCP Server 中的标准输出方法注释掉,原因在之前讲述 Stdio 配置文件已经说过,这里就不再赘述。Server 中的工具方法需修改为如下:
java
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.ai.tool.annotation.Tool;
import org.springframework.context.i18n.LocaleContextHolder;
import org.springframework.stereotype.Service;
@Service
public class DateTimeTools {
private final Logger logger = LoggerFactory.getLogger(DateTimeTools.class);
@Tool(description = "获取当前时间")
String getCurrentDateTime() {
return LocalDateTime.now().atZone(LocaleContextHolder.getTimeZone().toZoneId()).toString();
}
@Tool(description = "设置闹钟,需要提供ISO-8601格式的时间")
void setAlarm(String time) {
LocalDateTime alarmTime = LocalDateTime.parse(time, DateTimeFormatter.ISO_DATE_TIME);
//System.out.println("Alarm set for " + alarmTime);
}
}
3.3.4 测试
3.3.4.1 Stdio
首先启动 MCP Client 服务。

然后在浏览器中输入 url 和问题。

这里可以看出,Client 调用了 MCP Server 中的获取时间和设置闹钟两个工具。
3.3.4.2 SSE
测试前注意把 MCP Server 的传输方式配置成 SSE。
这里由于不是 Stdio 传输方式,因此 Server 可以使用标准输出来输出一些内容在控制台上。
先启动 MCP Server。

然后启动 MCP Client。

启动 Client 后,可以在 Server 的控制台处看到客户端连接的日志信息。

然后在浏览器中输入 url 和问题。

这里结合 Server 的控制台输出,也可以看到 Client 成功调用了 Server 的工具。

四、展望
以个人角度来说,我认为 MCP 更像是连接传统后端系统与 AI 模型世界的一座桥梁,MCP Server 的开发其实很适合当下的后端开发,对于当前的后端开发而言,是一个具有前景的新方向,我相信随着 AI 技术的发展,MCP 有望成为未来 AI 应用后端开发的重要基础之一。不过,目前的 MCP 还处于初级阶段,存在许多问题,比如技术框架不成熟、token 消耗量较大等等。但也期待着后续这一技术会不断发展,未来能够在实际工程中应用 MCP。
附录
参考资料:
2.MCP (Model Context Protocol),一篇就够了
5.抓包分析了MCP,直接惊呆了!高端的技术往往只需要最朴素的实现方式~_哔哩哔哩_bilibili
6.MCP是怎么对接大模型的?抓取AI提示词,拆解MCP的底层原理_哔哩哔哩_bilibili
7.Model Context Protocol (MCP) :: Spring AI Reference