什么是MCP
Model Control Protocol(MCP)是由AI研究机构Anthropic在2023年第二季度首次提出的新型协议规范,旨在解决大语言模型LLM应用中的上下文管理难题。作为LLM交互领域的创新标准,MCP协议在发布后短短一年内已进行了多次更新,最近一次更新是在2025-03-26(Key Changes - Model Context Protocol),包含 添加了一个基于 OAuth 2.1 的全面的授权框架 等内容变更:
协议定义与核心价值
MCP是一套开放式的通信协议,它通过标准化:
- 上下文结构化表示(JSON Schema)
- 多轮对话状态跟踪机制
- 模型控制指令集
使开发者能够精准控制LLM的上下文窗口,解决传统对话系统中存在的:
- 上下文丢失(Context Bleeding)
- 指令冲突
- 长文本处理低效等痛点
成熟度
目前MCP已被Claude系列模型原生支持,并在Llama 3、Mistral等开源模型中实现兼容。行业分析显示,超过43%的企业级LLM应用已开始采用MCP作为首选上下文管理方案(数据来源:2024 ML Stack调查报告)。
MCP的目标对象
刚开始接触MCP的开发者可能会进入一个误区,以为有了MCP后,LLM是不是可以直接调用MCP Server的能力(API)了,其实并不是,LLM实际上是无法感知MCP的存在的。
MCP(Model Control Protocol)的核心目标对象是LLM应用开发者,而不是LLM(大语言模型)本身。要理解这一点,我们需要明确LLM和LLM应用的区别:
1. LLM vs. LLM应用
- LLM(大语言模型):指底层的大模型(如GPT-4、Claude、Llama等),它们负责接收输入并生成文本输出,但本身不具备复杂的状态管理或上下文控制能力。
- LLM应用:指基于LLM构建的完整系统,如聊天机器人、AI助手、代码生成工具等。这些应用需要管理多轮对话、维护上下文、处理用户指令,并可能集成外部数据或API。
2. 为什么LLM不能直接使用MCP
LLM本身是“无状态”的——它们仅对当前输入做出响应,而不会自动记住之前的交互。MCP的作用是让LLM应用能更高效地管理上下文,例如:
- 维护对话历史(避免超出模型的上下文窗口限制)
- 动态调整提示词(prompt),确保LLM获得正确的背景信息
- 控制模型行为(如切换模式、调整生成参数)
MCP协议定义了一套标准化的接口,让LLM应用能以结构化的方式与LLM交互,而不是让LLM自己去解析或执行这些逻辑。
MCP作为外部协议而非模型内置机制,还有两大更关键原因:
-
训练成本问题:若MCP逻辑固化到LLM中,协议每次更新都需重新训练模型,带来极高的计算和迭代成本;
-
安全与权限挑战:若LLM直接通过MCP调用三方应用,权限管理(如数据访问、API鉴权)将难以控制,增加滥用风险。
因此,MCP设计为应用层协议,由外部系统管理上下文和资源,既保持LLM的通用性,又避免强耦合带来的运维负担。
3. MCP的目标用户
MCP主要面向:
- LLM应用开发者:需要构建复杂对话系统或AI代理的工程师
- AI平台架构师:设计LLM基础设施,优化上下文管理和推理效率
- 企业级AI解决方案:需要稳定、可扩展的LLM交互协议
简而言之,MCP不是让LLM自己“学会”管理上下文,而是为LLM应用提供一套标准化的控制机制,使开发者能更高效地构建可靠的AI系统。
MCP的主要角色和架构
MCP最核心的两个角色就是MCP客户端和MCP服务端。参见:Model Context Protocol (MCP) :: Spring AI Reference
MCP Client
MCP 客户端是模型上下文协议 (MCP) 架构中的关键组件,负责建立和管理与 MCP 服务器的连接。对,你没听错,MCP体系中,客户端才是最复杂的,而且通常这个客户端也是由服务端应用(也就是前面说的LLM应用)来“使用”。
它实现了协议的客户端功能,处理以下操作:
- 协议版本协商以确保与服务器的兼容性
- 能力协商以确定可用功能
- 消息传输和 JSON-RPC 通信
- 工具发现和执行
- 资源访问和管理
- 提示系统交互
- 可选功能:
- 根部管理
- 采样支持
- 同步和异步操作
- 通信选择:
- 基于 Stdio 的传输,用于基于进程的通信
- 基于 SSE 客户端传输
你可以看到,通常是MCP客户端在“调用” LLM 和 MCP服务端 。
MCP Server
MCP 服务器是模型上下文协议 (MCP) 架构中的基础组件,为客户端提供:
- 资源:上下文和数据,供用户或 AI 模型使用。它们通常是 只读的,AI 模型可以读取资源数据,但不会直接修改或执行它们,所以一般是指数据库查询。
- 提示:为用户提供模板化的消息和工作流程
- 工具:AI模型执行的功能。它们通常是 动态的、有副作用的(如修改数据、发送消息、触发操作),所以一般是指AI调用。
它实现了协议的服务器端,负责:
-
服务器端协议操作实现
-
工具曝光和发现
-
基于 URI 访问的资源管理
-
及时提供和处理模板
-
与客户进行能力谈判
-
结构化日志记录和通知
-
-
并发客户端连接管理
-
同步和异步 API 支持
-
通信实施:
-
基于 Stdio 的传输,用于基于进程的通信
-
基于 SSE 服务器传输
-
通常来说MCP服务器是不直接使用LLM的(当然,如果你这个MCP服务器也是另一些MCP服务器的MCP客户端时也有可能需要直接使用LLM),它通常是我们常见的一个个业务系统。
这里需要特殊说明的是如果MCP客户端和MCP服务器是部署在一起的,那么可以使用Stdio( Standard Input/Output(标准输入/输出) 的缩写,指计算机程序与外部环境(如终端、文件或其他程序)进行数据交互的标准方式。它是操作系统提供的基础通信机制,几乎所有编程语言都支持 stdio 操作)的方式来进行通信。
一言以蔽之
通过MCP的角色、架构分析,大家应该有所感知,MCP就是通过定义一套标准协议,同时标准化通过LLM来使用应用能力来解决我们之前直接使用LLM去编排Function Call难度大、复杂度高以及效率低的问题。
实战:构建MCP客户端和服务器(for Java)
说的再多都是虚的,得眼见为实,这里我们通过构建两个Spring应用来演示如何在本地来使用MCP。Spring框架早已提供对调用LLM能力的封装:Spring AI API :: Spring AI Reference,这里我们就使用Spring框架来构建MCP客户端和MCP服务器。
构建MCP服务器
MCP服务器相对比较简单,它负责把业务功能封装成一个个原子能力,供MCP客户端来使用,新建一个Maven项目mcp-server,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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.manzhizhen.mcp</groupId>
<artifactId>mcp-study</artifactId>
<version>0.0.1-SNAPSHOT</version>
</parent>
<artifactId>mcp-server</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>jar</packaging>
<name>mcp-server</name>
<description>Demo project for mcp</description>
<properties>
<java.version>21</java.version>
<spring-ai.version>1.0.0-M7</spring-ai.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-mcp-server-webmvc</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>
这里和常规的Spring Boot项目不同的是加了spring-ai的依赖,而且我们计划使用SSE来进行通信(Stdio方式生产不常用):
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-mcp-server-webmvc</artifactId>
</dependency>
接着,我们Copy官方的天气预报例子,构建一个通过经纬度查询天气预报(getWeatherForecastByLocation),另一个通过美国州代码来查询天气预报的接口(getAlerts),注意,这属于上面提到的MCP服务器提供的“工具Tool”类型。
我们先通过 org.springframework.ai.tool.annotation.Tool 注解完成工具的定义(参考 https://github.com/spring-projects/spring-ai-examples/tree/main/model-context-protocol/weather/starter-webmvc-server/src/main/java/org/springframework/ai/mcp/sample/server):
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.tool.annotation.Tool;
import org.springframework.ai.tool.annotation.ToolParam;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestClient;
import org.springframework.web.client.RestClientException;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
@Slf4j
@Service
public class WeatherService {
private final RestClient restClient;
public WeatherService() {
this.restClient = RestClient.builder()
.baseUrl("https://api.weather.gov")
.defaultHeader("Accept", "application/geo+json")
.defaultHeader("User-Agent", "WeatherApiClient/1.0 (835576511@qq.com)")
.build();
}
/**
* Get forecast for a specific latitude/longitude
* @param latitude Latitude
* @param longitude Longitude
* @return The forecast for the given location
* @throws RestClientException if the request fails
*/
@Tool(description = "Get weather forecast for a specific latitude/longitude")
public String getWeatherForecastByLocation(
double latitude, // Latitude coordinate
double longitude // Longitude coordinate
) {
log.info("Fetching forecast for coordinates: {}, {}", latitude, longitude);
var points = restClient.get()
.uri("/points/{latitude},{longitude}", latitude, longitude)
.retrieve()
.body(Points.class);
var forecast = restClient.get().uri(points.properties().forecast()).retrieve().body(Forecast.class);
String forecastText = forecast.properties().periods().stream().map(p -> {
return String.format("""
%s:
Temperature: %s %s
Wind: %s %s
Forecast: %s
""", p.name(), p.temperature(), p.temperatureUnit(), p.windSpeed(), p.windDirection(),
p.detailedForecast());
}).collect(Collectors.joining());
return forecastText;
}
/**
* Get alerts for a specific area
* @param state Area code. Two-letter US state code (e.g. CA, NY)
* @return Human readable alert information
* @throws RestClientException if the request fails
*/
@Tool(description = "Get weather alerts for a US state")
public String getAlerts(
@ToolParam(description = "Two-letter US state code (e.g. CA, NY)") String state) {
log.info("Fetching alerts for state: {}", state);
Alert alert = restClient.get().uri("/alerts/active/area/{state}", state).retrieve().body(Alert.class);
return alert.features()
.stream()
.map(f -> String.format("""
Event: %s
Area: %s
Severity: %s
Description: %s
Instructions: %s
""", f.properties().event(), f.properties.areaDesc(), f.properties.severity(),
f.properties.description(), f.properties.instruction()))
.collect(Collectors.joining("\n"));
}
@JsonIgnoreProperties(ignoreUnknown = true)
public record Points(@JsonProperty("properties") Props properties) {
@JsonIgnoreProperties(ignoreUnknown = true)
public record Props(@JsonProperty("forecast") String forecast) {
}
}
@JsonIgnoreProperties(ignoreUnknown = true)
public record Forecast(@JsonProperty("properties") Props properties) {
@JsonIgnoreProperties(ignoreUnknown = true)
public record Props(@JsonProperty("periods") List<Period> periods) {
}
@JsonIgnoreProperties(ignoreUnknown = true)
public record Period(@JsonProperty("number") Integer number, @JsonProperty("name") String name,
@JsonProperty("startTime") String startTime, @JsonProperty("endTime") String endTime,
@JsonProperty("isDaytime") Boolean isDayTime,
@JsonProperty("temperature") Integer temperature,
@JsonProperty("temperatureUnit") String temperatureUnit,
@JsonProperty("temperatureTrend") String temperatureTrend,
@JsonProperty("probabilityOfPrecipitation") Map probabilityOfPrecipitation,
@JsonProperty("windSpeed") String windSpeed,
@JsonProperty("windDirection") String windDirection,
@JsonProperty("icon") String icon, @JsonProperty("shortForecast") String shortForecast,
@JsonProperty("detailedForecast") String detailedForecast) {
}
}
@JsonIgnoreProperties(ignoreUnknown = true)
public record Alert(@JsonProperty("features") List<Feature> features) {
@JsonIgnoreProperties(ignoreUnknown = true)
public record Feature(@JsonProperty("properties") Properties properties) {
}
@JsonIgnoreProperties(ignoreUnknown = true)
public record Properties(@JsonProperty("event") String event, @JsonProperty("areaDesc") String areaDesc,
@JsonProperty("severity") String severity,
@JsonProperty("description") String description,
@JsonProperty("instruction") String instruction) {
}
}
}
@Tool是 Spring AI 项目中的一个注解,用于将 Java 方法标记为可由 AI 模型(如 OpenAI、Azure OpenAI 或其他支持的 AI 服务)调用的工具(Tool)。它的主要作用是将你的方法暴露给 MCP客户端,使它够动态调用这些方法来完成特定任务。其中 @Tool 完成方法整体的功能描述,而@ToolParam 可以进一步完成参数的说明,如果你参数名足够清晰,也可以不用加@ToolParam。
同时在我们的Spring Boot启动类中增加ToolCallbackProvider,这样才能真正把Tool暴露出去给MCP Client调用:
import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.tool.ToolCallbackProvider;
import org.springframework.ai.tool.method.MethodToolCallbackProvider;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
@Slf4j
@SpringBootApplication
public class McpServerApplication {
public static void main(String[] args) {
log.info("McpServerApplication 启动啦");
SpringApplication.run(McpServerApplication.class, args);
}
/**
* Tools
* Allows servers to expose tools that can be invoked by language models.
* The auto-configuration will automatically register the tool callbacks as MCP tools.
* You can have multiple beans producing ToolCallbacks. The auto-configuration will merge them.
* @param weatherService
* @return
*/
@Bean
public ToolCallbackProvider weatherTools(WeatherService weatherService) {
return MethodToolCallbackProvider.builder().toolObjects(weatherService).build();
}
}
细心的朋友会发现,前面不是说有Stdio和SSE两种方式来暴露吗?目前看代码上没有体现?其实前面在pom.xml中我们依赖的spring-ai-starter-mcp-server-webmvc里面就依赖了:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
这里面就支持了SSE的实现,我们只需要在application.yml配置文件中配置一下即可:
server:
port: 8090
spring:
application:
name: mcp-server
main:
banner-mode: off
ai:
mcp:
server:
name: mcp-server
version: 1.0.0
type: SYNC
sse-message-endpoint: /mcp/messages
这里的 sse-message-endpoint 就是配置SSE的path。注意这里暴露的是8090端口。到这里,一个简单的MCP Server就构建完成了。
构建MCP客户端
前面说过通常MCP客户端是需要使用LLM的,这里我们首选DeepSeek,然后在DeepSeek开放平台官网创建Api Keys 再充10块钱,这样你就有了可用DeepSeek Api Key了:
同样,我们建一个Maven项目叫做mcp-client,pom.xml配置如下:
<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>com.manzhizhen.mcp</groupId>
<artifactId>mcp-study</artifactId>
<version>0.0.1-SNAPSHOT</version>
</parent>
<artifactId>mcp-client</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>jar</packaging>
<name>mcp-client</name>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<java.version>22</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-mcp-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-model-openai</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>3.8.1</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>33.3.1-jre</version>
<scope>compile</scope>
</dependency>
</dependencies>
</project>
其中和其他项目不同的是我们依赖了:
<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>
一个是为了支持mcp-client,一个是为了支持我们调用DeepSeek。由于只是做一个简单的演示,我们决定使用IDEA的控制台作为输入,来当做一个天气预报咨询平台。我们只需要修改启动类即可:
import io.modelcontextprotocol.client.McpSyncClient;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.tool.ToolCallbackProvider;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.Bean;
import java.util.List;
import java.util.Scanner;
@Slf4j
@SpringBootApplication
public class McpClientSseApplication {
public static void main(String[] args) {
SpringApplication.run(McpClientSseApplication.class, args);
}
@Bean
public CommandLineRunner interactiveChatRunner(
ChatClient.Builder chatClientBuilder,
ToolCallbackProvider tools,
ConfigurableApplicationContext context
) {
return args -> {
var chatClient = chatClientBuilder
.defaultTools(tools)
.build();
Scanner scanner = new Scanner(System.in); // 用于读取控制台输入
System.out.println("=== 聊天模式已启动(输入 'exit' 退出) ===");
while (true) {
System.out.print("\n>>> 你的问题: ");
String userInput = scanner.nextLine().trim();
// 输入 "exit" 退出聊天
if ("exit".equalsIgnoreCase(userInput)) {
System.out.println(">>> 聊天结束,程序退出。");
break;
}
// 空输入则跳过
if (userInput.isEmpty()) {
continue;
}
// 调用 AI 并打印回复
System.out.println("\n>>> AI 回复: " + chatClient.prompt(userInput).call().content());
}
context.close(); // 关闭 Spring 上下文
};
}
}
可以看到,为了能不断的在控制台提问,我们写了一个while循环,当你输入exit才退出。
这里有一个关键,就是不管我们mcp-client连接了多少个mcp-server以及使用了多少个mcp-server的能力(例如Tool),我们都不需要去太关心细节,一行代码“chatClient.prompt(userInput).call().content()”就搞定,mcp内置的库类会去调用AI来根据你的问题“合理”的使用mcp-server提供的一个或者多个能力来完成任务,之前用Function Call可没有这么丝滑。
对,mcp-client修改这一个类即可,接下来我们配置下application.yml:
spring:
application:
name: mcp-client
ai:
mcp:
client:
toolcallback:
enabled: true
enabled: true
name: my-mcp-client
version: 1.0.0
request-timeout: 30s
type: SYNC # or ASYNC for reactive applications
sse:
connections:
server1:
url: http://localhost:8090
openai:
api-key: 这里填写你的DeepSeek api key
base-url: https://api.deepseek.com
chat:
options:
model: deepseek-chat
temperature: 0.7
embedding:
enabled: false
可以看到,我们最重要的是要配置mcp-server的地址(这里由于mcp-server和mcp-client部署在一起,所以用的是localhost)以及我们的openai的类型和对于的api key。到这里,整个mcp-client示例我们也写完了。
看看效果
先运行mcp-server,再运行mcp-client,如果启动都没问题,那么我们在mcp-client的控制台可以进行天气预报咨询了:
>>> 你的问题: 帮我查下 纬度 (lat): 40.7128,经度 (lon): -74.0060 的天气
>>> AI 回复: 以下是纬度 40.7128,经度 -74.0060 的天气预报:
### 今天
- **温度**: 80°F
- **风速**: 10 到 23 mph,西南风
- **预报**: 上午8点前可能有零星小雨,随后可能有零星阵雨和雷暴。部分晴天,最高气温接近80°F。西南风10至23 mph,阵风高达39 mph。降水概率20%。
### 今晚
- **温度**: 57°F
- **风速**: 15 到 22 mph,西风
- **预报**: 凌晨2点前可能有零星阵雨和雷暴。大部分多云,最低气温约57°F,夜间气温升至62°F左右。西风15至22 mph,阵风高达38 mph。降水概率20%。
### 周日
- **温度**: 63°F
- **风速**: 15 到 20 mph,西北风
- **预报**: 大部分晴天,最高气温接近63°F。西北风15至20 mph。
...
可以看到我的问题携带了经纬度,所以最终是mcp-server的WeatherService#getWeatherForecastByLocation 被调用了。
>>> 你的问题: 美国 WA 州 天气预报
>>> AI 回复: 以下是美国华盛顿州(WA)的天气预报:
### 今晚
- **温度**: 44°F
- **风速**: 9 mph 西北风
- **预报**: 多云,最低气温约44°F。西北风约9 mph,阵风高达22 mph。
### 周六
- **温度**: 54°F
- **风速**: 9至13 mph 西北风
- **预报**: 大部分晴天,最高气温约54°F,下午气温降至约52°F。西北风9至13 mph,阵风高达29 mph。
### 周六晚
- **温度**: 38°F
- **风速**: 10 mph 西北风
- **预报**: 部分多云,最低气温约38°F。西北风约10 mph,阵风高达28 mph。
...
这个问题很明显想引导mcp-client去使用mcp-server的WeatherService#getAlerts,但由于getAlerts没有查到华盛顿州的数据,所以mcp-client最终通过DeepSeek拿到华盛顿州的经纬度47.7511, -120.7401 去调用了getWeatherForecastByLocation方法,真聪明!!!
总结
欢迎加作者WX sugarmq 一起来探讨技术话题。