文章摘要
土豆丝 GPT

需要的环境

jdk版本:21,建议新项目无脑选择21,之后的AI框架猜测21起步。

开发框架:springBoot 4.0.x,ai迭代贼快,练习项目能新就新,提前感受新特性。

AI框架:Spring AI 2.0.0-M5(不是稳定版),也是迭代的飞快,学新不学旧,好多api都是破坏性的更新,不兼容老格式。

本文的项目源码:https://github.com/tdsay-cn/spring-ai-mcp-demo

MCP官网:https://modelcontextprotocol.io/

MCP中文站:https://mcp.programnotes.cn/zh/docs/introduction

springAI:https://docs.spring.io/spring-ai/reference/api/mcp/mcp-server-boot-starter-docs.html

JSON-RPC 2.0官网:https://www.jsonrpc.org/specification#request_object

VS CODE:https://code.visualstudio.com/

名词解释

MCP的定义可以访问上述网站进行查阅,接下来将以白话的形式进行介绍,可能不是完全准确,但是通俗易懂。

MCP全称Model Context Protocol ,直译是模型上下文协议。啊根本不懂是啥意思 = =。先拆开来看,

  • “模型”那就是给大模型用的东西。

  • “上下文”就是操作范畴,可以触达的部分。

  • “协议”就是规范,类比http协议、接口定义。既然是协议,就会有提供方和调用方。无非就是规定了怎么提供、怎么调用。

连起来就是大模型在操作范畴使用的协议。最后的落点在“协议”, 也即是MCP本质是一个协议规范,依据这个规范所开发出的东西,广义都属于MCP的内容。

可能看到这里大家还是云里雾里的,没关系,下面开始进行实践操作,跟着本文走一遍,你一定会有所收获!

产生的背景

大模型LLM是一个大脑,本身没有动手的能力,只有思考推理的能力。只能以token或者简单理解为文字的形式与外界交互,也就是一问一答的方式。之后agent的出现为了实现AI的动手能力,开发出了Function Call,也就是函数(方法)调用,函数中和之前的编程一样,实现动手的能力,只需要在与LLM交互时,提供这个函数的作用以及调用方法,LLM就可以在需要的时候,告诉agent去调用相关的方法,从而实现动手的能力。

上面的解决方法虽然解决了大模型的动手能力,但是如果开发多个agent都需要相同的函数,岂不是需要在每一个agent里都实现一遍,为了解决这个问题,MCP就产生了。mcp本质上就是提供了不同场景需要的工具。就好比我们之前开发的接口,调用不同的api接口就能实现不同的能力。而MCP就是定义了api接口的规范。

白话解释:就是调用Mcp Server的东西。

MCP Server

白话解释

提供Tool工具的服务。暂时这么理解,除了工具还有其他比如Resources资源等,本文主要介绍工具。

交互协议

MCP SERVER使用的交互协议是JSON-RPC 2.0,协议非常简单,首先定义交互数据类型为json,里面包含4个字段。

1
2
3
4
5
6
{
"jsonrpc": // 定义协议版本 2.0
"method": // 定义调用的方法
"params": // 定义调用方法传入的参数
"id": // 定义消息id
}

MCP SERVER有两种交互方式,使用的协议都是JSON-RPC 2.0

  • 本地交互:stdio。简单理解为在本地调用。调用过程是客户端启动一个子进程,这个子进程就是mcp server的进程,然后客户端就可以通过定义好的交互协议方式,传入所需要的参数对mcp server进行调用。
  • 远程交互:SSE(已弃用)、Streamable HTTP。简单理解为远程调用,mcp server启动服务,对外提供http服务,客户端通过http的方式进行调用,就和调用restful第三方接口一样,唯一区别就是协议和传输方式。

实现一个stdio的MCP SERVER

首先在idea中创建一个springboot项目,type选择maven,jdk版本选择21,配置文件类型选择yml。下一步依赖输入“mcp”,选择Model Context Protocol Server,创建项目。spring ai 版本 2.0.0-M5.

编写yml文件,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
spring:
application:
name: mcp-server-stdio # 应用名称
main:
web-application-type: none # 不启动Web容器,
banner-mode: off # 关闭 Spring Boot 启动横幅(让日志更干净)
ai:
mcp:
server:
name: tdsay-student-mcp # MCP 服务器的逻辑名称,客户端会看到这个名字
version: 1.0.0 # 服务器版本,便于管理和兼容性

logging:
level:
root: OFF # 关闭日志输出

编写一个tool服务service,模拟查询“土豆丝学院学生信息”的工具

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
/**
* 提供土土豆丝学院学生查询功能
*/
@Service
public class StudentToolService {

/**
* 初始化数据
*/
private static final List<Student> STUDENT_LIST = List.of(
new Student("小明", 100),
new Student("小帅", 90),
new Student("小美", 80),
new Student("小丽", 70)
);

/**
* 查询土豆丝学院的所有学生
*/
@McpTool(
name = "tdsay_get_all_students",
description = "Tdsay Academy: Retrieve the complete list of all students."
)
public List<Student> queryAll() {
return STUDENT_LIST;
}

/**
* 查询土豆丝学院的分数高于score的所有学生
*/
@McpTool(
name = "tdsay_get_students_with_min_score",
description = "Tdsay Academy: Retrieve students whose score is greater than or equal to the specified value."
)
public List<Student> getStudentsWithScoreAbove(int score) {
return STUDENT_LIST.stream()
.filter(s -> s.score() >= score)
.toList();
}

/**
* record是java新特性,简单理解为定义一个pojo类
* @param name 学生姓名
* @param score 分数
*/
public record Student(String name, int score) {}
}

代码写完了,就是这么简单。对java项目进行打包,执行mvn clean package -DskipTests,记住打包的地址,一般工具使用stdio都会使用一个json的配置文件。如果是windows用户就像我这么配置,jar包的路径换成自己的,mac的路径改为/,可以参考mcp官网。如果有多个工具就在“tdsay-student-mcp”平级继续配置,下面会用到这个配置。这样一个stdio的mcp server就准备好了。为下面的调用做准备。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
{
"mcpServers": { // 固定字段
"tdsay-student-mcp": {
"disabled": false, // 是否禁用
"timeout": 60, // 连接超时时间
"type": "stdio", // 交互类型,stdio是本地交互
"command": "java", // 启动服务命令,py可以通过uv, node可以通过npx
"args": [ // 启动参数
"-jar", // java启动固定参数
"F:\\WorkSpace\\mcp-server\\stdio\\target\\mcp-server-0.0.1-SNAPSHOT.jar" // jar包路径
]
}
}
}

实现一个http的MCP SERVER

首先在idea中创建一个springboot项目,type选择maven,jdk版本选择21,配置文件类型选择yml。下一步依赖输入“mcp”,选择Model Context Protocol Server,创建项目。spring ai 版本 2.0.0-M5。

进入pom文件,mcp-server改为下面这个带webmvc的,默认的不支持http的方式。

1
2
3
4
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-mcp-server-webmvc</artifactId>
</dependency>

配置yml文件,参数以及含义见注释。其中重点说下protocol协议配置,webmvc目前支持三种协议,

  • sse:已经被mcp官方弃用了,如果使用这种方式,可以配置初始化端点和调用方法端点,所谓端点就是接口地址。
  • streamable:有状态的,适合需要长链接进行多次连续的请求响应,优点是可以保持会话,缺点是占用资源较高。
  • stateless,无状态的就和微服务架构一样,适合单次调用就可以处理完逻辑的场景,优点是水平扩展能力很强。

streamable和stateless都属于 Streamable-HTTP,默认端点/mcp。协议根据mcp的使用场景自行选择,本文演示比较简单的场景,所以选择了stateless。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
spring:
application:
name: mcp-server-streamable #应用名称
ai:
mcp:
server:
enabled: true #启用/禁用MCP服务器
name: tdsay-math-mcp #mcp名称
version: 1.0.0 #版本
protocol: stateless #通信协议

logging: #配置服务器接受的请求数据日志打印
level:
io.modelcontextprotocol: trace
org.springframework.ai.mcp: trace

编写一个工具类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package com.tdsay.mcpserverhttp.tool;

import org.springframework.ai.mcp.annotation.McpTool;
import org.springframework.stereotype.Service;

@Service
public class MathToolService {
@McpTool(name = "tdsay_math_add", description = "两数相加")
public Integer add(int a, int b) {
return a + b;
}

@McpTool(name = "tdsay_math_sub", description = "两数相减")
public Integer sub(int a, int b) {
return a - b;
}
}

大功告成了,又是这么简单= =。启动服务,默认服务端口8080。

MCP Clinet

白话解释

Client是MCP协议的具体实现者,负责与MCP Server的所有通信,包括协议转换,连接管理、消息路由等。

使用PostMan实现MCP Client

启动postman作为Clint进行调用,选择mcp,如果没有这个选项就更新下postman。

image-20260504211550478

调用方式选择http,可以看到不仅支持stdio和http两种方式。

image-20260504211631884

调用stdio

将上面stdio的json粘贴到地址框,postman会自动格式化路径。

image-20260505115311204

点击连接,显示连接成功,并且我们手写mcp server的两个工具也展示出来了。

image-20260505115458519

点击tdsay_get_students_with_min_score工具,输入参数,点击run,可以看到我们mcp server做出了正确的响应。

image-20260505115645709

调用http

模式选择http,地址输入:localhost:8080/mcp,点击执行,可以看到连接成功了,并且出现了工具列表。点击工具输入参数,点击执行可以看到执行结果,以及调用过程。

如果能到这里,表示我们的MCP Server已经开发成功了。

image-20260504211835082

image-20260504212136456

接下来我们可以看一下postman在第一次点击run也就是执行connected和第二次调用tool都做了什么,可以通过抓包工具看下

第一次,其实调用了两次mcp server,发送了初始化请求和查询工具列表请求。

image-20260504212900770

第二次,根据json schema的约束方式对工具进行了调用。

image-20260504213108737

手动实现MCP Clint

如果上面通过postman实现MCP Clint觉得不过瘾,可以自己写代码来实现。stdio方式本质就是在主程序之外启动一个子线程,读取json配置的指定参数,通过命令启动和调用mcp server,根据这个思路去编写一个clinet程序不算太难。http的方式就更简单了,根据上面抓包的数据,先调用init获取工具列表,在调用工具,每一步请求格式是知道的,就像平常调用三方的api一样。

我们也可以使用框架进行实现,框架为我们封装好了一些底层处理逻辑,实现起来也更方便,下面通过spring ai进行实现。

实现stdio clint

依旧在idea中创建一个springboot项目,type选择maven,jdk版本选择21,配置文件类型选择yml。下一步依赖输入“mcp”,选择Model Context Protocol Client,创建项目。spring ai 版本 2.0.0-M5。

因为clint实现比较简单,就以SpringBootTest的方式进行调用实现,比较直观。

代码里写参数的方式实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
@SpringBootTest
class McpClientApplicationTests {

/**
* 手写调用参数
*/
@Test
void callStdioToolWithParameter() {
// MCP Server的调用参数配置
ServerParameters stdioParams = ServerParameters.builder("java")
.args("-jar", "F:\\WorkSpace\\mcp\\mcp-server-stdio\\target\\mcp-server-stdio-0.0.1-SNAPSHOT.jar")
.build();

// 创建Stdio的Client运输通道,用标准输入/输出(stdin/stdout)来和 MCP Server 通信的客户端传输层实现。
StdioClientTransport stdioClientTransport = new StdioClientTransport(stdioParams,McpJsonDefaults.getMapper());

// 使用同步的方式创建MCP Clint , 配置连接超时时间
McpSyncClient mcpClient = McpClient.sync(stdioClientTransport).requestTimeout(Duration.ofSeconds(60)).build();

// 初始化,创建连接
mcpClient.initialize();

// 获取工具列表
McpSchema.ListToolsResult listToolsResult = mcpClient.listTools();
System.out.println("mcp server 集合: " + listToolsResult.tools());

// 创建调用工具的请求,CallToolRequest(String name, Map<String, Object> arguments) name是工具列表返回的工具名称,map是参数
McpSchema.CallToolRequest callToolRequest = new McpSchema.CallToolRequest("tdsay_get_students_with_min_score", Map.of("score", 88));

//调用MCP Server的工具
McpSchema.CallToolResult callToolResult = mcpClient.callTool(callToolRequest);
System.out.println("调用结果:" + callToolResult.content());

// 关闭会话,结束子进程,释放资源
mcpClient.closeGracefully();
}

}
配置的方式实现

编写yml,在resources目录下新增一个mcp-servers.json,内容和上面MCP Server的json配置一样。目前版本的sping ai配置的方式注入Client的方式不太好,之后肯定会改,暂时先这么用把。

1
2
3
4
5
6
7
8
spring:
application:
name: mcp-client
ai:
mcp:
client:
stdio:
servers-configuration: classpath:mcp-servers.json
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
@SpringBootTest
class McpClientApplicationTests {

/**
* 使用配置文件的方式使用,spring帮我们配置并注册好了client
* ps: 目前的版本不支持Map注入,也不支持选择性的注入, 如果有多个只能获取指定下标或者根据参数过滤获取了。
*/
@Autowired
private List<McpSyncClient> mcpSyncClients;

/**
* 通过配置的方式调用
*/
@Test
void callStdioToolWithConfig() {
// 因为只配置了一个,所以直接获取第一个。
McpSyncClient mcpClient = mcpSyncClients.getFirst();

// 获取工具集合
McpSchema.ListToolsResult listToolsResult = mcpClient.listTools();
System.out.println("mcp server 工具集合: " + listToolsResult.tools());

// 构建调用工具以及参数
McpSchema.CallToolRequest callToolRequest = new McpSchema.CallToolRequest("tdsay_get_students_with_min_score", Map.of("score", 88));

// 用MCP Server的工具
McpSchema.CallToolResult callToolResult = mcpClient.callTool(callToolRequest);
System.out.println("调用结果:" + callToolResult.content());

// 关闭会话,结束子进程,释放资源
mcpClient.closeGracefully();
}

}

实现http client

在yml配置里增加streamable-http,也可以使用sse,配置参考官网文档,本文MCP Server使用的是streamable-http,就以streamable-http为例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
spring:
application:
name: mcp-client
ai:
mcp:
client:
stdio:
servers-configuration: classpath:mcp-servers.json
streamable-http:
connections:
server1:
url: http://localhost:8080
# server2:
# url: http://otherserver:8081
# endpoint: /custom-sse

启动前面写好的MCP Server,在刚才的测试类里增加一个http client方法,spring ai为我们封装了交互细节,所以client的使用方式和stdio没有什么差别。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/**
* 通过配置的方式调用
*/
@Test
void callHttpTool() {
// http client在配置里是第2个
McpSyncClient mcpClient = mcpSyncClients.get(1);

// 获取工具集合
McpSchema.ListToolsResult listToolsResult = mcpClient.listTools();
System.out.println("mcp server 工具集合: " + listToolsResult.tools());

// 构建调用工具以及参数
McpSchema.CallToolRequest callToolRequest = new McpSchema.CallToolRequest("tdsay_math_add", Map.of("a", 2,"b",6));

// 用MCP Server的工具
McpSchema.CallToolResult callToolResult = mcpClient.callTool(callToolRequest);
System.out.println("调用结果:" + callToolResult.content());

// 关闭会话,释放资源
mcpClient.closeGracefully();
}

MCP HOST

介绍

白话解释:Host是MCP生态系统的控制中心,就像一个智能指挥官,可以处理用户请求,并且可以调用MCP Server的agent。

MCP Client只负责调用Mcp Server,Mcp Sever只提供服务实现,就和原本的编程范式一样,有请求有响应,但是无法根据用户输入的口语化问题,来解决用户的问题。

常见的host有:Claude Desktop、Cursor、Cline等。本文使用的是Cline。

实现原理:Cline本身也是一个普通程序,本身是不具备推理能力的!和我们之前编写的程序并没有什么不同,也是接收请求进行响应,接受的请求参数是固定格式的,是需要标准化的,比如定义一个get方法入参是int类型的数据,那么调用的时候就必须请求get方法,get1、gett方法名对不上或者传入字符串的参数就是调不了的。基于这个情况,既然agent本身改不了,那就只能让LLM的响应是结构化的。

实现方式:Cline内部内置了大量的系统提示词,提示词强制交互的数据格式采用的是xml,大概4万字符左右!这可都是白花花的token那,不过正是有了这些提示词约束了结构化的响应数据,才使得cline可以通过响应数据来实现各种功能。提示词大概包括角色定位、内置的工具列表、使用格式、使用示例,已连接的MCP服务器等等。

比如LLM想要调用一个读工具,就会返回下面这种xml数据,read_file标签标识调用read_file工具,path是参数名,value是代表调用的路径

1
2
3
<read_file>
<path>src/main.js</path>
<read_file>

MCP服务器的提示词中会包含工具调用的JSON Schema,定义方法名称、描述、出入参的字段、描述、数据类型、是否必填。LLM拿到这个约束后,会根据约束想应调用的实际参数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
{
"name": "tdsay_get_students_with_min_score", // 方法名
"description": "Tdsay Academy: Retrieve students whose score is greater than or equal to the specified value.",// 方法描述
"inputSchema": { // 入参格式约束
"type": "object", // 表示工具接收的参数必须是一个对象,也就是一个JSON 对象
"properties": { // 入参
"score": { // 入参字段名
"type": "integer", // 入参类型
"description": "The minimum score threshold (inclusive)." // 入参描述
}
},
"required": ["score"],// 必填的参数
"additionalProperties": false // 禁止对象包含未在 properties 中显式定义的属性,也就是json里只能有properties定义的字段
},
"outputSchema": {
...............和上面一样
}
}

再比如提示词中会强制LLM先对问题进行思考,并且把思考过程放入标签中,这样做的原因是,有些LLM不支持思考模式,Cline相当于使用外力让模型具备了这种功能。cline通过这种方式固定了交互模式,LLM接受到了用户的问题之后,首先会思考,并且告诉cline需要调用什么工具,也就是行动。cline调用工具后把结果发送给LLM后,LLM对这个结果进行观察,之后在进行思考、行动、观察…一直循环,只到最后一次观察结果已经完成,在进行思考并给出最终答案。整个步骤是不是很熟悉,没错,这就是actor模型,Cline通过这种方式实现了actor模型,使得回答的结果的准确率大幅提升。

安装与配置cline

安装VSCODE并且安装cline插件,我本地已经安装好了,没安装过的,插件后面会有安装按钮进行安装,安装好之后左侧会有一个如图图标,点击它。

image-20260503142904018

image-20260503143112971

点击右上角齿轮图标,配置模型参数,我使用的是小米的mimo,前两天刚得的免费token,其他平台就使用对应的参数配置就好。配置完成点击Done。

image-20260503143348130

在对话框里输入“你好”,有回应就代表配置成功了。

配置MCP Server

点击如图cline的这个按钮,在market里进行安装,也可以点击Configure自行安装。

image-20260503145556355~

点击Configure后,会出现mcp的配置文件,如果是windows用户就像我这么配置,jar包的路径换成自己的,mac的路径改为/,可以参考mcp官网。配置完成后观察是否显示绿灯,如果是就代表配置成功,如果不是绿灯会有错误提示,一般是路径配置错误,或者jar包有问题,jar包有问题的话可以自行通过命令java -jar进行启动,观察是否报错,输出日志是否有问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
{
"mcpServers": {
"tdsay-student-mcp": {
"disabled": false, // 是否禁用
"timeout": 60, // 连接超时时间
"type": "stdio", // 交互类型,stdio是本地交互
"command": "java", // 启动服务命令,py可以通过uv, node可以通过npx
"args": [ // 启动参数
"-jar", // java启动固定参数
"F:\\WorkSpace\\mcp-server\\stdio\\target\\mcp-server-0.0.1-SNAPSHOT.jar" // jar包路径
]
}
}
}

在对话框里进行提问 “查询土豆丝学院分数超过88的学生”,如图已经完成MCP Server的调用并且得到了正确答案。

image-20260504133251908

总结

以上,就是关于MCP的全部知识分享,希望对你有所帮助!