
Java MCP SDK 开发笔记(一)
在开发前,首先带大家熟悉一下MCP协议和所有可能需要进行协商的功能。通过这些机制,MCP协议实现了高效、灵活且可扩展的通信能力,适用于多样化的应用场景。
MCP 简介
AI 大模型诞生之初,其高度模拟人的对话之能力惊为天人。但我们肯定不希望止步于此—— 工具化就是我们希望 AI 能够完成的目标,由此可以从单纯的对话发展为代替繁复人力的“干活”。这条道路上毋庸置疑 AI 大模型任重道远。而 MCP(Model Control Protocol) 的应运而生正好为大模型工具化铺平了道路。
了解 MCP
MCP 是一个协议,怎么理解的 MCP 呢?首先概念上讲,Host 是宿主的概念,它一般伴随 Client 客户端一起出现。在终端用户上讲,Client 往往看不见——因为它被 Host 整合到其中里面去了。而 Host 则是指我们“看得见摸得着”的软件,比如代码编辑器 Cursor 就是一个典型的 Host,——你甚至不需要知道 MCP Client 而直接使用 Host Cursor,在 Cursor 中配置一下连接 MCP Server 即可。Server 肯定是重要的概念,但我们先按下不表,——先说说 Client。Client 虽然被 Host 暂时“遮蔽”了,但不意味它并不重要。相反 Client 在 MCP 里面跟 Server 同等重要——反倒 Host 是在前面忽悠人的东东,甚至有朋友在 Host 与 Client 之间的概念纠结了很久……
如果你能 LLM 那样嚼蜡般死磕文档,那也未尝不可,这里有份中文文档读起来更容易……
如果不是,那么笔者推荐你去开源中国 gitee 搞个他们的 gitee-mcp 是个可执行 exe,他就是个 MCP Server,你用 MCP Client 跟它交互,这样通过例子学习 MCP 或许更友好。
接着你还要搞个 MCP Inspector,可视化的,学习更方便!
MCP Inspector
这是官方出品的调试 MCP Server 的可视化工具,也可以作为了解 MCP 原理的工具(它本身是一个 MCP Client)。其实无非就是跟 Server 如何通讯,JSON-RPC 报文如何定义的等问题,对于了解 MCP 机制很有帮助,——好过去看一堆忽悠的文章。
该 app 由 next.js 写成,系 nodejs 工程,如果是前端那么比较熟悉安装了。
注意,如果出现Cannot find module @rollup/rollup-win32-x64-msvc
报错,可以尝试先安装npm install rollup
。
通过源码学习 MCP
MCP 不少乃是 Python 或 TypeScript 的实现,Java 岂能缺席?搜索了一下发现 Java MCP 的不多,比较有代表性如下两个:
- 官方 SDK(源码),系基于 Reactor 的实现。官方默认的就是绑定 Spring 框架。响应式虽然应用合理且强大,但不免又要学习一番,简单的通讯不可以?Java 自带的
CompleteFuture
或许刚好够用。不过后来看,不用 WebFlux 那套也行,官方提供了mcp-spring-webmvc
也可以为传统的 SpringMVC 程序所服务。然后,最主要的问题是,这 SDK 要求 Java 17,太高大上了……
教程、例子。 - Quarkus MCP servers,一个更陌生的框架,好像在 K8S 上用的。
- langchain4j-mcp。谈到 AI 工具不能不提 langchain——而这个 Java 移植版本提供了 MCP Client 的实现,但 Server 好像未看到。这个可比 Spring 的下里巴人多了,免其他框架依赖,简单朴实,唯一美中不足就是要求 Java11,而且依赖 langchain-core 模块(其实就几个 POJO 类)
协议规范一览
MCP 协议概览的几个点:
- Client/Host/Server 架构,比传统 C/S 多个 Host,但这个 Host 不那么重要。——为什么不只做一个 MCP 组件就好?因为安全问题和关注点不同,分多一层自然能够应付复杂的问题
- 基于 JSON-RPC 协议。这是一个轻量级、简单的协议。说白了就是 JSON 规范一下内容你再去调。这个有状态的、可以双工的(例如 Sampling)
组件简单说说:
- Host 发起连接的 LLM 应用程序,例如 Desktop App 或 IDE,负责管理 MCP Client,整合 AI/LLM 的
- Client,在 Host 与 Server 之间的连接器,作为 SDK 整合到 Host 中
- Server,暴露工具 Tool、资源等给 Host/Client 用的,有 API 和本地通讯两种方式
简单的架构图如下:
既然是 C/S 架构自然有个请求与响应,用 JSON-RPC 定义(其实是 TypeScript 定义,这事情 ts 擅长)
请求
双向的请求都有,就是说客户端请求服务端之外还有服务端请求客户端的。每次请求都有 id 字段且不能为null
。
{
"jsonrpc": "2.0",
"id": "string | number",
"method": "string",
"param?": {
"key": "value"
}
}
响应
响应通过 id 与请求关联。result 或 error 选其一出现。也可以没有响应的(result
为空)
{
"jsonrpc": "2.0",
"id": "string | number",
"result?": {
"[key: string]": "unknown"
},
"error?": {
"code": "number",
"message": "string",
"data?": "unknown"
}
}
通知
通知没有响应,没有 id 字段,用于事件通知或者更新状态,异步的,这样可以减少通信开销。
{
"jsonrpc": "2.0",
"method": "string",
"params?": {
"[key: string]": "unknown"
}
}
MCP 生存周期
初始化阶段
初始化阶段必须是客户端和服务器之间的第一次交互。在此阶段,双方需要完成以下任务:
- 建立协议版本兼容性:确认客户端和服务器都支持的协议版本,以确保双方能够理解彼此的消息格式和通信规则。
- 交换并协商能力(capabilities) :双方互相通报各自支持的功能和服务,并就将要使用的能力达成一致。这可能包括但不限于支持的数据格式、认证方法、加密算法等。
- 共享实现细节:提供必要的实现细节以便于优化通信或处理特定的情况。这些细节可以包含特殊的行为、限制条件或者是对标准的扩展。
具体流程如下
- 客户端发送初始化请求:客户端首先发送一个包含协议版本和功能的初始化请求。
- 服务器响应:服务器接收到请求后,会返回自己的协议版本和功能。
- 客户端确认:客户端接收到服务器的响应后,发送一个初始化完成的通知,表示双方已经准备好开始正常的消息交换。
- 正常消息交换开始:初始化完成后,双方开始进行正常的消息交换。
请求例子:
{
"jsonrpc": "2.0",
"id": 1,
"method": "initialize",
"params": {
"protocolVersion": "2024-11-05",
"capabilities": {
"roots": {
"listChanged": true
},
"sampling": {}
},
"clientInfo": {
"name": "ExampleClient",
"version": "1.0.0"
}
}
}
响应例子
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"protocolVersion": "2024-11-05",
"capabilities": {
"logging": {},
"prompts": {
"listChanged": true
},
"resources": {
"subscribe": true,
"listChanged": true
},
"tools": {
"listChanged": true
}
},
"serverInfo": {
"name": "ExampleServer",
"version": "1.0.0"
}
}
}
通知例子
{
"jsonrpc": "2.0",
"method": "initialized"
}
版本协商
在初始化请求中,客户端必须发送它所支持的协议版本。
- 客户端应当发送其支持的最新版本:这样可以确保尽可能使用最新的功能和改进。
- 服务器必须响应相同的版本或另一个它支持的版本:这表示服务器同意使用某个双方都支持的版本进行通信。
- 如果客户端不支持服务器的版本,它应该断开连接:这意味着没有找到兼容的协议版本,继续通信可能会导致不一致或错误。
能力协商
客户端和服务器的能力决定了会话期间哪些可选的协议特性是可用的。
客户端能力(Client Capabilities)
客户端能力描述了客户端在会话中支持的可选功能或特性。以下是一些常见的能力及其含义:
能力名称 | 描述 | 示例用途 |
---|---|---|
roots |
提供文件系统根目录的能力 | 文件操作、项目管理 |
sampling |
支持 LLM 采样请求的能力 | 文本生成、自然语言处理 |
experimental |
支持非标准实验性功能的能力 | 测试新特性、探索未来功能 |
服务器能力(Server Capabilities)
服务器能力(Server Capabilities)定义了服务器在会话期间能够提供的功能和服务。这些能力帮助客户端了解服务器支持哪些特性,从而可以更有效地利用服务器的功能。
能力名称 | 描述 | 示例用途 |
---|---|---|
prompts |
提供提示模板 | 文本生成、任务指令 |
resources |
提供可读资源 | 获取文档、配置文件等 |
tools |
提供可调用工具 | 编译代码、分析数据等 |
logging |
发出结构化日志消息 | 监控、调试、审计 |
experimental |
支持非标准实验性功能 | 测试新特性、探索未来功能 |
操作阶段(Operation)
在操作阶段,客户端和服务器依据已协商的能力进行消息交换。
- 遵循已协商的协议版本。
- 仅使用成功协商后的功能(capabilities)。
这意味着在操作阶段,所有通信都应严格基于之前协商确定的规则和功能集。例如,如果在初始连接建立时双方协商确定了特定的协议版本和一组支持的功能,则在后续的操作过程中,双方应当遵守这些约定,确保使用正确的协议版本以及仅启用那些已经确认双方都支持的功能。这样可以保证通信的兼容性和稳定性。
错误处理
MCP 定义了以下标准错误代码
enum ErrorCode {
// 标准 JSON-RPC 错误代码
ParseError = -32700, // 解析错误
InvalidRequest = -32600, // 无效请求
MethodNotFound = -32601, // 方法未找到
InvalidParams = -32602, // 无效参数
InternalError = -32603 // 内部错误
}
SDK 和应用程序可以定义自己的错误代码,范围在 -32000 以上。
关闭连接
正常关闭:任何一方都可以通过调用 close() 方法来正常关闭连接,需要做:1、客户端发送断开连接的通知。2、服务器关闭连接。3、清理相关资源。
连接可能会因为传输层的问题(如网络中断)而意外断开。在某些错误情况下,连接也可能被终止。
服务端功能
服务器通过 MCP(Model Context Protocol,模型上下文协议)提供了为语言模型添加上下文的基本构建块,并提供了三种基本组件来管理上下文:工具(tools)、提示(prompts)和资源(resources)。
工具(Tools)
工具是 MCP 中的强大组件,是模型上下文协议(MCP)中的一个基本元素,它允许服务器向客户端暴露可执行功能,使 LLM 能够与外部系统交互、执行计算或采取现实世界的行动。工具设计为模型控制(model-controlled),即服务器将工具暴露给客户端,AI 模型可以自动调用这些工具。
工具的核心功能
- 发现(Discovery):客户端可以通过tools/list端点列出可用的工具。
- 调用(Invocation):工具通过tools/call端点被调用,服务器执行请求的操作并返回结果。
- 灵活性(Flexibility):工具的范围从简单的计算到复杂的API交互,具有高度的灵活性。
工具与资源(Resources)不同,资源通常是静态的,而工具代表动态操作,可以修改状态或与外部系统交互。
工具的定义结构
每个工具的定义结构如下:
{
"name": "string", // 工具的唯一标识符
"description": "string", // 工具的人类可读描述
"inputSchema": { // 工具参数的JSON Schema
"type": "object",
"properties": { ... } // 工具特定的参数
}
}
工具的实现示例
以下是一个在 MCP 服务器中实现基本工具的示例:
const server = new Server({
name: "example-server",
version: "1.0.0"
}, {
capabilities: {
tools: {}
}
});
// 定义可用的工具
server.setRequestHandler(ListToolsRequestSchema, async () => {
return {
tools: [{
name: "calculate_sum",
description: "Add two numbers together",
inputSchema: {
type: "object",
properties: {
a: { type: "number" },
b: { type: "number" }
},
required: ["a", "b"]
}
}]
};
});
// 处理工具执行
server.setRequestHandler(CallToolRequestSchema, async (request) => {
if (request.params.name === "calculate_sum") {
const { a, b } = request.params.arguments;
return {
toolResult: a + b
};
}
throw new Error("Tool not found");
});
工具的模式示例
系统操作:与本地系统交互的工具。
{
"name": "execute_command",
"description": "Run a shell command",
"inputSchema": {
"type": "object",
"properties": {
"command": { "type": "string" },
"args": { "type": "array", "items": { "type": "string" } }
}
}
}
API 集成:封装外部 API 的工具。
{
"name": "github_create_issue",
"description": "Create a GitHub issue",
"inputSchema": {
"type": "object",
"properties": {
"title": { "type": "string" },
"body": { "type": "string" },
"labels": { "type": "array", "items": { "type": "string" } }
}
}
}
数据处理:用于数据转换或分析的工具。
{
"name": "analyze_csv",
"description": "Analyze a CSV file",
"inputSchema": {
"type": "object",
"properties": {
"filepath": { "type": "string" },
"operations": {
"type": "array",
"items": {
"enum": ["sum", "average", "count"]
}
}
}
}
}
总结
工具(Tools)在 MCP 中是一个核心概念,它通过服务器暴露可执行功能,使 LLMs 能够与外部系统交互并执行复杂的操作。工具的设计具有高度的灵活性和可扩展性,能够支持从简单的计算到复杂的 API 集成。通过定义工具的结构和实现方式,开发者可以轻松地将各种功能集成到 LLMs 中,从而增强模型的能力。
工具的模型控制特性确保了工具的调用是可控的,可以额外加入人工批准作为限制,这为安全性和可控性提供了保障。工具的发现和调用机制使得 LLMs 能够动态地与服务器交互,执行各种任务,极大地扩展了模型的应用场景。
资源(Resources)
资源是模型上下文协议(MCP)中的核心组件,允许服务器向客户端暴露数据和内容,供大语言模型(LLM)交互时使用。资源设计为应用控制(application-controlled),即客户端应用决定何时以及如何使用这些资源。不同客户端对资源的使用方式可能不同,例如,Claude Desktop 要求用户明确选择资源,而其他客户端可能基于启发式自动选择资源,甚至允许 AI 模型自行决定使用哪些资源。服务器开发者应准备好处理这些不同的交互模式。
服务器作者在实现资源支持时应准备好处理这些交互模式。为了自动向模型暴露数据,服务器作者应使用模型控制的工具(如 Tools)。
概述
资源代表 MCP 服务器希望向客户端提供的任何类型的数据。这可以包括:
- 文件内容
- 数据库记录
- API 响应
- 实时系统数据
- 截图和图像
- 日志文件
- 以及其他类型的数据
每个资源由一个唯一的 URI 标识,并且可以包含文本或二进制数据。
资源URI
资源使用以下格式的 URI 进行标识:
[协议]://[主机]/[路径]
例如:
file:///home/user/documents/report.pdf
postgres://database/customers/schema
screen://localhost/display1
协议和路径结构由 MCP 服务器实现定义。服务器可以定义自己的自定义 URI 方案。
资源类型
资源可以包含两种类型的内容:、
文本资源
文本资源包含 UTF-8 编码的文本数据,适用于:
- 源代码
- 配置文件
- 日志文件
- JSON/XML 数据
- 纯文本
二进制资源
二进制资源包含以 base64 编码的原始二进制数据,适用于:
- 图像
- PDF 文件
- 音频文件
- 视频文件
- 其他非文本格式
资源发现
客户端可以通过两种主要方法发现可用资源:
直接资源
服务器通过 resources/list
端点暴露具体的资源列表。每个资源包括:
{
"uri": "string", // 资源的唯一标识符
"name": "string", // 人类可读的名称
"description": "string", // 可选的描述
"mimeType": "string" // 可选的MIME类型
}
资源模板
对于动态资源,服务器可以暴露 URI 模板,客户端可以使用这些模板构建有效的资源 URI:
{
"uriTemplate": "string", // 遵循RFC 6570的URI模板
"name": "string", // 此类型的名称
"description": "string", // 可选的描述
"mimeType": "string" // 所有匹配资源的可选MIME类型
}
读取资源
客户端通过发送resources/read
请求并附带资源URI来读取资源。服务器响应包含资源内容列表:
{
"contents": [
{
"uri": "string", // 资源的URI
"mimeType": "string", // 可选的MIME类型
"text": "string", // 文本资源的内容
"blob": "string" // 二进制资源的内容(base64编码)
}
]
}
服务器可能会在一次resources/read
请求中返回多个资源,例如读取目录时返回目录中的所有文件。
资源更新
MCP支持通过两种机制进行资源的实时更新:
列表变化
服务器可以通过notifications/resources/list_changed
通知客户端可用资源列表的变化。
内容变化
客户端可以订阅特定资源的更新:
- 客户端发送
resources/subscribe
请求并附带资源URI。 - 服务器在资源更新时发送notifications/resources/updated通知。
- 客户端可以通过resources/read获取最新内容。
- 客户端可以通过resources/unsubscribe取消订阅。
示例实现
以下是一个简单的 MCP 服务器资源支持实现示例:
const server = new Server({
name: "example-server",
version: "1.0.0"
}, {
capabilities: {
resources: {}
}
});
// 列出可用资源
server.setRequestHandler(ListResourcesRequestSchema, async () => {
return {
resources: [
{
uri: "file:///logs/app.log",
name: "Application Logs",
mimeType: "text/plain"
}
]
};
});
// 读取资源内容
server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
const uri = request.params.uri;
if (uri === "file:///logs/app.log") {
const logContents = await readLogFile();
return {
contents: [
{
uri,
mimeType: "text/plain",
text: logContents
}
]
};
}
throw new Error("Resource not found");
});
总结
资源是 MCP 中的核心概念,允许服务器将数据和内容暴露给客户端,并作为 LLM 交互的上下文。资源的设计灵活,支持多种数据类型和交互模式。通过 URI 标识资源,客户端可以发现、读取和订阅资源的变化。服务器作者应根据不同的客户端需求实现资源支持,并考虑使用模型控制的工具来自动暴露数据。
更多推荐
所有评论(0)