MingJunDuan的博客
热爱可抵一切,探索未知之境
全站访问量

MCP 全面解析 —— 类型、原理与使用方式

你在聊天框里敲下:”帮我看看本地 ~/notes 目录下有哪些文件,整理成清单发给我。”

AI 不仅读懂了你的意图,还真的列出了文件、读取了内容、给出了整理结果——它明明只是一个没有操作系统的模型,凭什么能访问你的文件系统?

答案就是 MCP(Model Context Protocol,模型上下文协议)

2024 年 11 月,Anthropic 开源了 MCP,把它称作”AI 应用的 USB-C 接口”:一个开放标准,让大模型应用能以统一方式连接任意数据源与工具。两年过去,MCP 已经从一个”概念”长成了事实上的行业标准——OpenAI、Google、Microsoft、阿里等主流厂商全部宣布支持,GitHub 官方 MCP 服务器下载量破百万,规范也迭代到了 2026-07-28 版本。

这篇文章会用与之前 Dubbo Triple、ZooKeeper 系列相同的深度剖析风格,回答三个问题:

  1. 类型:MCP 到底有哪几类?角色、原语、传输、形态分别是什么?
  2. 原理:MCP 底层是怎么工作的?JSON-RPC 消息如何流转,生命周期如何演进?
  3. 使用方式:从配置客户端到写一个自己的 MCP Server,完整动手实践。

一、为什么需要 MCP?

1.1 大模型的”能力断层”

大语言模型本质上是一个函数:输入 token,输出 token。它没有任何”副作用”——不能读文件、不能查数据库、不能发 HTTP 请求、不能执行命令。

但真实世界的任务几乎都需要副作用:

用户意图 需要的副作用
“帮我看看这个项目的 README” 读文件
“统计一下这个月的订单量” 查数据库
“把这个 issue 指派给张三” 调用 GitHub API

于是业界催生了各种”工具调用”方案:OpenAI 的 Function Calling、Anthropic 的 Tool Use、各家 Agent 框架自己定义的插件体系。它们本质相同:把”外部能力”描述成一个个函数(工具),让模型在推理时决定调用哪个、传什么参数。

1.2 N×M 集成困境

问题在于:每个模型厂商的调用协议是私有的,每个工具也要单独适配。

              工具域                         模型域
        ┌──────────────┐
        │   MySQL 数据库 │
        └──────┬───────┘        ┌───────────┐
        ┌──────┴───────┐        │   LLM-A   │
        │   文件系统     ├────────┤   (OpenAI) │
        └──────┬───────┘        └─────┬─────┘
        ┌──────┴───────┐        ┌─────┴─────┐
        │   GitHub API │        │   LLM-B   │
        └──────┬───────┘        │ (Anthropic)│
        ┌──────┴───────┐        └─────┬─────┘
        │    浏览器      │        ┌─────┴─────┐
        └──────────────┘        │   LLM-C   │
                                │  (国产大模型) │
                                └───────────┘

    N 个工具 × M 个模型 = N×M 个定制适配器,全部要手工维护

每个工具都要为每个模型写一套适配代码,集成成本随规模平方级增长,而且数据与工具被锁定在特定模型生态里——换个模型,全部重来。

1.3 已有方案的局限

在 MCP 出现之前,”让 AI 用工具”有几种主流做法,各有硬伤:

方案 代表 局限
Function Calling OpenAI 私有协议,只服务自家模型,工具描述格式与调用链绑定 SDK
插件系统 ChatGPT Plugins、Copilot Extensions 平台绑定,插件生态由平台方控制,接入方话语权低
自研 Agent 框架 LangChain 等 框架层解决编排,但工具接入仍是每个框架一套 DSL,框架之间不互通
代码直连 直接调 API 每次需求变化都要改代码,模型无法动态发现能力

核心痛点是一致的:缺少一个中立、开放、双向的”工具连接层”标准

1.4 MCP 是什么

Model Context Protocol 是一个开放协议,定义了 AI 应用(Host)如何通过标准化的方式连接外部数据源和工具(Server),从而为模型提供上下文(Context)

三个关键词拆开看:

关键词 含义
Model 服务对象是模型——让模型获得它缺失的”上下文”(文件内容、数据库记录、工具能力)
Context MCP 连接的不仅是”工具”,还有数据——工具是动作,资源是数据,提示词是模板,三者共同构成模型的上下文
Protocol 它是一份协议规范,不是 SDK 也不是平台;任何人可实现自己的客户端与服务器,只要遵守规范就能互通

关键里程碑:

时间 事件
2024-11-25 Anthropic 开源 MCP,MIT 协议,首版规范 2024-11-05
2025-03 OpenAI 宣布支持 MCP;Anthropic 发布 Java/Kotlin SDK
2025-06 规范 2025-06-18:统一 Streamable HTTP 传输、引入 OAuth 2.1;MCP Registry 上线
2025-11 规范 2025-11-25:OIDC 发现、任务(Tasks)实验特性、JSON Schema 2020-12
2026-07 规范 2026-07-28:协议无状态化、Multi Round-Trip Requests、废弃 Roots/Sampling/Logging

MCP 的设计目标可以总结为四句话:

  1. 一个协议,处处可用:工具与数据源只需实现一次 MCP,任何 MCP 客户端都能接入。
  2. 双向通信:不只是客户端调服务端(工具调用),服务端也能向客户端要数据(如请求用户补充输入)。
  3. 能力自描述:服务器主动声明自己有哪些工具/资源/提示词,客户端动态发现,无需硬编码。
  4. 安全可审计:所有外部访问都经过协议边界,配合用户确认与权限模型,可观测、可控。

二、MCP 的类型

“类型”这个词在 MCP 语境下有四层含义,很多人混淆。我们一层层拆。

2.1 角色类型:Host / Client / Server

MCP 的架构里只有三种角色:

┌──────────────────────────────────────────────┐
│                   Host(宿主)                  │
│        Claude Desktop / IDE / Agent 应用       │
│                                              │
│   ┌──────────┐   ┌──────────┐   ┌──────────┐ │
│   │  Client 1 │   │  Client 2 │   │  Client 3 │ │
│   └────┬─────┘   └────┬─────┘   └────┬─────┘ │
└────────┼──────────────┼──────────────┼────────┘
         │              │              │
   ┌─────┴─────┐  ┌─────┴─────┐  ┌─────┴─────┐
   │  Server A  │  │  Server B  │  │  Server C  │
   │  文件系统   │  │  MySQL     │  │  GitHub    │
   └───────────┘  └───────────┘  └───────────┘
角色 职责 关键点
Host 用户直接面对的应用(Claude Desktop、Cursor、自研 Agent 等) 一个 Host 可以连接多个 Server;Host 决定”何时调用哪个工具”以及是否征求用户确认
Client Host 与某个 Server 之间的一对一协议连接,负责建立会话、收发 JSON-RPC 消息 注意:这里的 Client 是协议角色,不是”应用”。一个 Host 内为每个 Server 各维护一个 Client
Server 暴露工具/资源/提示词的一方,连接真实世界(文件系统、数据库、第三方 API) 一个 Server 可以同时被多个 Client 连接(远程场景);Server 不关心”哪个模型”在用自己

一个常见的误解:把 MCP Server 当成”插件”。更准确的说法是:MCP Server 是一个独立的进程/服务,Host 通过 MCP 客户端与之对话。模型本身并不直接连接 Server——中间隔着 Host 的编排逻辑(包括权限判断、用户确认)。

2.2 原语类型:Tools / Resources / Prompts

这是 MCP 最核心的类型划分。服务器通过三种原语(Primitive)向模型暴露能力,而模型通过它们获得”上下文”:

┌────────────────────────── MCP Server ──────────────────────────┐
│                                                                │
│   Tools(工具)       Resources(资源)      Prompts(提示词)      │
│   "我会做什么"          "我知道什么"           "我该怎么问"         │
│   · 可执行的函数         · 只读数据            · 可复用的模板       │
│   · 有副作用            · 无副作用            · 引导交互方式       │
│   · 由模型主动调用        · 由模型/用户按需读取   · 由用户/模型触发    │
│                                                                │
└────────────────────────────────────────────────────────────────┘

三者对比如下:

维度 Tools Resources Prompts
本质 动作(副作用) 数据(只读) 交互模板
典型例子 发送邮件、执行 SQL、创建 issue 数据库表内容、项目文件、文档 “代码评审”模板、”周报生成”模板
谁触发 模型自主决定调用(经用户确认) 模型发现后按需读取;用户也可直接指定 用户或模型选用模板,填入参数
协议方法 tools/listtools/call resources/listresources/readresources/templates/list prompts/listprompts/get
可变性 是(会改变世界状态) 否(纯读取) 否(只是拼字符串)
适合场景 一切”做事”的需求 一切”给模型喂上下文”的需求 一切”规范交互方式”的需求

Tools(工具)——最常用、最容易被理解的原语。服务器注册一组带 JSON Schema 入参声明的函数,模型在推理时选择调用。协议层只关心两件事:tools/list 拿到工具清单(名字、描述、参数 schema),tools/call 提交参数并拿回结果。工具可以声明自己的属性(注解):是否幂等(idempotentHint)、是否只读(readOnlyHint)、是否破坏性(destructiveHint)——这些会直接影响客户端的安全策略。

Resources(资源)——把”数据”暴露给模型的原语。文件内容、数据库查询结果、API 响应都可以包装成资源。资源用 URI 标识,支持资源模板(URI Template,如 file:///{path} 匹配任意文件),配合 MIME 类型声明格式。模型看到资源引用后,可以调用 resources/read 拉取内容。2026-07-28 版本引入了资源订阅(通过 subscriptions/listen),服务器可以在资源变化时主动推送变更通知。

Prompts(提示词)——服务器预定义的”提示词模板”。例如一个”代码评审”提示词:用户触发后,服务器用 prompts/get 返回一组带角色的消息(system + user),再交给模型执行。Prompts 是三者中唯一”不直接与工具交互”的原语,它本质上是协作规范:让第三方服务器能引导 Host 以特定方式与用户对话。

2026-07-28 之前还有两个原语级能力:Roots(客户端向服务器声明”我可访问哪些目录”)和 Sampling(服务器请求客户端调用模型生成文本)。它们在新版规范中已被废弃——前者建议改用工具参数/资源 URI 显式传路径,后者建议服务器直接集成 LLM 供应商 API。新实现不应再依赖它们。

2.3 传输类型:stdio 与 Streamable HTTP

MCP 定义了两种传输(Transport)方式,解决”本地进程”与”远程服务”两类连接需求:

维度 stdio Streamable HTTP
连接形态 客户端拉起服务器子进程,通过 stdin/stdout 通信 客户端通过 HTTP POST 调用远程 URL
消息格式 换行分隔的 JSON-RPC(每行一条消息) JSON-RPC 封装在 HTTP 请求/响应中;长连接用响应流
部署位置 本地(开发机、桌面端) 远程(云端、内网服务、SaaS)
鉴权 无(进程边界即信任边界) OAuth 2.1 等标准授权
适用场景 本地开发工具、桌面应用、安全敏感场景 多租户 SaaS、集中式工具网关、团队共享
日志/调试 日志走 stderr(不能污染 stdout 协议流) 标准 HTTP 日志、OpenTelemetry

stdio 是 MCP 最初的传输,也是”零配置安全”的典范:客户端直接 spawn 服务器进程,通信不经过网络,权限由操作系统进程隔离天然保证。代价是服务器必须随客户端一起部署(或至少能本地启动)。

Streamable HTTP 是 2025-06-18 版本引入的统一远程传输,取代了早期版本里”HTTP + SSE 双端点”的割裂设计。它允许服务器用响应流下发长任务进度、日志等通知,同时支持请求-响应语义。2026-07-28 进一步演进:移除了会话头 Mcp-Session-Id,服务端主动通知改为通过 subscriptions/listen 订阅流,让协议在 HTTP 层做到无状态、可水平扩展

2.4 服务器形态类型

按部署与组织方式,MCP Server 还可以分成几类形态:

形态 说明 例子
本地进程服务器 随客户端启动的独立进程(stdio) 文件系统、Git、SQLite、浏览器控制
远程托管服务器 部署在云端的 HTTP 服务,多用户共享 GitHub 官方 MCP、Sentry、Cloudflare
单工具服务器 一个服务器只暴露一个工具,职责单一 fetch 抓取、时间查询
聚合网关 把多个内部工具统一包装成一个 MCP 端点 企业内部”工具中台”、LangGraph/Spring AI 的 MCP 网关
参考服务器 官方/社区维护的开源示例实现 modelcontextprotocol/servers 仓库

形态选择没有对错:本地工具用 stdio 最简单安全;团队共享、需要集中治理的工具做成远程 HTTP 服务器;企业内部多个系统则适合先做聚合网关,再统一暴露。


三、MCP 的原理

这一章我们打开协议的黑盒,看消息是怎么流动的。

3.1 协议基础:JSON-RPC 2.0

MCP 的消息层基于 JSON-RPC 2.0——一个极简的远程调用规范,只有三种消息:

请求(Request):    有 id,期待响应
响应(Response):   与请求 id 对应,携带 result 或 error
通知(Notification):无 id,单向发出,不需要响应

一条完整的 tools/call 请求长这样:

{
  "jsonrpc": "2.0",
  "id": 3,
  "method": "tools/call",
  "params": {
    "name": "query_issues",
    "arguments": {
      "repo": "mingjunduan/mingjunduan.github.io",
      "query": "MCP"
    }
  }
}

对应的响应:

{
  "jsonrpc": "2.0",
  "id": 3,
  "result": {
    "content": [
      { "type": "text", "text": "共找到 2 个相关 issue:\n#12 MCP 介绍……" }
    ],
    "isError": false
  }
}

MCP 在此之上定义了两类扩展:

  1. 方法命名空间tools/*resources/*prompts/*initializeping 等,按前缀分组。
  2. 元数据约定params._meta 携带协议版本、客户端/服务器标识等协议级信息(2026-07-28 起成为版本协商的主通道)。

错误码也遵循 JSON-RPC 规范并做了 MCP 扩展:

错误码 含义
-32700 解析错误(非法 JSON)
-32600 无效请求
-32601 方法不存在
-32602 参数无效(2026-07-28 起,资源不存在也从 -32002 归入此码)
-32603 内部错误
-32020 ~ -32099 MCP 规范保留区(-32020 HeaderMismatch、-32021 缺少必需能力、-32022 协议版本不匹配等)

3.2 生命周期:从”握手”到”无状态”

MCP 的生命周期设计经历了一次重大演进,理解两个阶段才能真正看懂现状。

经典模型(2024-11-05 ~ 2025-11-25):显式握手 + 会话状态

Client                                        Server
  │                                                │
  │── initialize(协议版本 + 客户端能力 + 标识)─────→│
  │←─ result(协商后的协议版本 + 服务端能力 + 标识)───│
  │── notifications/initialized ────────────────→│
  │                                                │
  │            ……运行期:工具/资源/提示词交互……        │
  │                                                │
  │── 会话结束,关闭连接 ───────────────────────────→│

经典模型三步走:

  1. initialize 握手:客户端声明它支持的协议版本、自身能力(如 rootssampling)和标识;服务器回复双方协商一致的版本、自身能力(如 toolsresourcespromptslogging)和标识。
  2. notifications/initialized:客户端通知服务器初始化完成,可以开始正常交互。
  3. 运行期:双方按能力清单进行工具/资源/提示词交互,期间服务器可通过 notifications/messagenotifications/progress 推送日志与进度。

这个模型直观,但有明显的分布式系统缺陷:有状态。服务器需要为每个连接维护会话(Session ID、能力上下文),无法水平扩展,也不利于无状态网关与负载均衡。

无状态模型(2026-07-28 起):每请求自带版本与能力

2026-07-28 规范做出了 MCP 历史上最激进的一次重构——移除握手,协议无状态化

Client                                        Server
  │                                                │
  │── server/discover(可选,版本探测)─────────────→│
  │←─ 支持的协议版本、能力、身份 ────────────────────│
  │                                                │
  │── tools/list(_meta 携带版本+客户端能力)───────→│
  │←─ result(_meta 携带服务器信息)────────────────│
  │── tools/call(每个请求都带版本)────────────────→│
  │←─ result ────────────────────────────────────│

新模型的关键变化:

变化点 说明
没有 initialize 握手 每个请求的 _meta 携带 io.modelcontextprotocol/protocolVersion(协议版本)和 io.modelcontextprotocol/clientCapabilities(客户端能力),服务器按请求逐条处理
新增 server/discover 服务器必须实现该 RPC,声明自己支持的版本范围与能力,供客户端在业务请求前做版本选择或向后兼容探测
移除会话概念 HTTP 层不再有 Mcp-Session-Id 头;tools/list 等列表接口不再按连接区分
跨请求状态显式化 需要状态时使用服务器签发的”句柄”(handle),作为普通工具参数传递
版本不匹配 返回 -32022 UnsupportedProtocolVersion

这个演进与 RPC 领域的大趋势完全一致(从有状态会话到无状态幂等接口):牺牲一点点握手便利,换取水平扩展性、网关兼容性和多云可移植性。对新读者,直接按无状态模型理解即可;若你接触的 SDK 还保留 initialize() 调用,那是为兼容旧协议保留的,新版本会逐步迁移。

3.3 通信模式:请求-响应、通知与 MRTR

MCP 的通信模式比普通 RPC 更丰富,2026-07-28 规范将其归纳为几种模式:

通信模式 方向 说明
请求-响应(Request-Response) 客户端 → 服务器 客户端发请求、服务器回结果;工具调用、资源读取、列表查询都属于此类
服务端通知(Server Notifications) 服务器 → 客户端 分两类:请求作用域通知notifications/progressnotifications/message 跟随原请求的响应流下发);订阅通知(客户端通过 subscriptions/listen 订阅 toolsListChangedresourcesListChanged 等变更事件)
多轮请求(Multi Round-Trip Requests,MRTR) 客户端驱动的多轮往返 服务器返回 input_required 中间结果,客户端补齐 inputResponses 后重试原请求;2026-07-28 引入
Client                                        Server
  │── tools/call(需要额外信息)──────────────────→│
  │←─ result: InputRequiredResult                │
  │    { resultType: "input_required",           │
  │      inputRequests: [ "请提供用户确认密码" ] }  │
  │── tools/call(重试原请求,附上 inputResponses)→│
  │←─ result: 最终结果                            │

过去,服务器需要”额外输入”(比如工具执行到一半需要用户确认、需要更多参数)时,会反过来向客户端发起 sampling/elicitation 等反向请求——这在无状态模型里变得困难。MRTR 把”服务器要东西”统一改为:服务器返回 resultType: "input_required" 的中间结果,客户端补齐 inputResponses重试原请求。所有结果都带 resultType 字段("complete""input_required"),协议语义因此完全收敛为”客户端驱动的多轮往返”。

3.4 原语的协议级交互流程

以一次完整的工具调用为例,看看消息如何流动:

    Host(应用)              MCP Client              MCP Server            真实世界
       │                          │                      │                    │
       │  1. 用户提问:帮我统计     │                      │                    │
       │     仓库的 star 数         │                      │                    │
       │                          │                      │                    │
       │  2. 模型推理:需要调工具    │                      │                    │
       │     tools/list 发现能力    │                      │                    │
       │─────────────────────────→│── tools/list ───────→│                    │
       │                          │←── 工具清单 ──────────│                    │
       │  3. 模型选择 query_repo    │                      │                    │
       │─────────────────────────→│── tools/call ───────→│                    │
       │                          │                      │── 调用 GitHub API ─→│
       │                          │                      │←── 返回数据 ─────────│
       │                          │←── 工具结果 ──────────│                    │
       │  4. 用户确认并展示结果     │                      │                    │
       │←─────────────────────────│                      │                    │

对应到消息序列(经典协议表示,新版差异仅在元数据携带方式):

  1. tools/list(无参数)→ 返回工具数组,每个工具含 namedescriptioninputSchema(JSON Schema);
  2. 模型依据描述与 schema 生成参数;
  3. tools/call(name + arguments)→ 服务器执行 → 返回 content(文本/图片/结构化内容数组)与 isError 标志;
  4. Host 决定是否展示给用户、是否把结果回灌给模型继续推理。

资源读取与提示词获取的流程同理,只是方法不同:

资源:resources/list → resources/templates/list → resources/read(URI)→ 内容
提示词:prompts/list → prompts/get(name + arguments)→ messages[](含 role 的消息序列)

3.5 版本演进史

把规范版本串起来,能看到 MCP 两年的演进主线:

版本 核心变化
2024-11-05 首个公开版本:定义三大原语、stdio 与 HTTP+SSE 双传输、initialize 握手
2025-03-26 能力扩展:上下文管理(context management)、根目录 Roots、资源订阅、采样 Sampling 细化
2025-06-18 传输统一:Streamable HTTP 取代 HTTP+SSE;引入 OAuth 2.1 远程鉴权;Elicitation(服务器请求用户输入)
2025-11-25 治理与增强:OIDC 发现、工具/资源图标元数据、增量 scope 授权、URL 模式 Elicitation、实验性 Tasks、JSON Schema 2020-12 默认方言、正式化治理结构
2026-07-28(当前) 无状态化革命:移除握手与会话、server/discover、MRTR、subscriptions/listen、结果缓存提示(ttlMs/cacheScope);废弃 Roots/Sampling/Logging;Tasks 移入官方扩展

两条主线贯穿始终:

演进主线 具体表现
走向无状态与可扩展 从”每连接一个会话”到”每请求自描述”,让 MCP 服务器能像普通 HTTP 服务一样水平扩展
走向开放与安全 传输统一、OAuth 2.1、OIDC 发现、治理结构正式化——从”Anthropic 家的协议”变成”行业共同治理的标准”

3.6 安全模型

MCP 把”AI 访问外部世界”这件事从不可控变成了可控,靠的是多层安全设计:

安全层 核心机制
用户确认(Human-in-the-loop) Host 是安全决策中心:模型调用工具时先征求用户同意,尤其是破坏性工具(删除、写入、转账);工具注解(destructiveHintreadOnlyHint)是 Host 做决策的信号
权限最小化 stdio 场景服务器进程权限 = 启动用户权限;远程场景只暴露被授权的工具集合;2026-07-28 支持增量 scope 授权,只授予当前任务所需权限
标准鉴权(远程) Streamable HTTP 服务器必须支持 OAuth 2.1;客户端通过 OIDC 发现(2025-11-25 起)定位授权服务器、动态注册客户端,用 iss 校验防钓鱼(RFC 9207);凭证绑定签发方,禁止跨授权服务器复用
数据面与控制面分离 _meta 等控制信息与业务数据分离;日志走 stderr 或 OpenTelemetry,避免敏感日志混入协议流
风险提示 MCP 不阻止服务器在 tools/call 里做任何事——信任边界在 Host;恶意 prompt 注入下若 Host 不做确认风险依然存在,因此工具设计要窄、默认拒绝、一切可审计

3.7 性能与缓存

无状态化之后,MCP 在性能上也更友好。2026-07-28 引入 CacheableResulttools/listresources/listresources/read 等只读接口的响应携带 ttlMs(缓存有效期,毫秒)与 cacheScope"public" 可被共享代理缓存 / "private" 仅客户端缓存)两个字段,客户端可以据此缓存结果、减少轮询。同时规范要求服务器确定性排序返回工具列表,以提升 LLM 提示词缓存(prompt cache)的命中率——同一个列表每次顺序一致,模型拿到的上下文前缀才能复用 KV 缓存。


四、MCP 的使用方式

理论讲完,动手。这一章从”用户”到”开发者”两条路径完整走一遍。

4.1 作为用户:在客户端接入 MCP Server

以 Claude Desktop 为例(其他客户端如 Cursor、VS Code、Cherry Studio 大同小异),接入一个 MCP Server 只需编辑配置文件:

macOS~/Library/Application Support/Claude/claude_desktop_config.json Windows%APPDATA%\Claude\claude_desktop_config.json Linux~/.config/Claude/claude_desktop_config.json

{
  "mcpServers": {
    "filesystem": {
      "command": "npx",
      "args": [
        "-y",
        "@modelcontextprotocol/server-filesystem",
        "/Users/me/workspace"
      ]
    },
    "calculator": {
      "command": "java",
      "args": ["-jar", "/Users/me/mcp-servers/calculator/target/calculator-server-1.0.0.jar"]
    },
    "github": {
      "command": "npx",
      "args": ["-y", "@modelcontextprotocol/server-github"]
    }
  }
}

保存后重启客户端,左侧会出现新增的连接图标,点开后能看到该服务器暴露的工具列表——然后你就可以直接对模型说”帮我列出 workspace 目录下的文件”。

远程服务器的配置方式略有不同(以支持 HTTP 的客户端为例):

{
  "mcpServers": {
    "company-tools": {
      "url": "https://mcp.example.com/mcp",
      "headers": { "Authorization": "Bearer <token>" }
    }
  }
}

4.2 作为开发者:十分钟写一个 MCP Server(Java 17)

官方 MCP Java SDKio.modelcontextprotocol,要求 JDK 17+,已演进到 2.x)提供了完整的服务端与客户端实现。

第一步:加依赖(Maven)

<dependencyManagement>
  <dependencies>
    <dependency>
      <groupId>io.modelcontextprotocol.sdk</groupId>
      <artifactId>mcp-bom</artifactId>
      <version>2.0.0</version>
      <type>pom</type>
      <scope>import</scope>
    </dependency>
  </dependencies>
</dependencyManagement>

<dependencies>
  <!-- mcp = mcp-core + Jackson 3 JSON,开箱即用(默认含 stdio / Streamable HTTP 传输) -->
  <dependency>
    <groupId>io.modelcontextprotocol.sdk</groupId>
    <artifactId>mcp</artifactId>
  </dependency>
</dependencies>

第二步:写服务器。工具/资源/提示词各是一个”定义 + 处理器”的规格(Specification)对象,注册到 McpSyncServer

// CalculatorServer.java
import java.util.List;
import java.util.Map;

import io.modelcontextprotocol.json.McpJsonDefaults;
import io.modelcontextprotocol.server.McpServer;
import io.modelcontextprotocol.server.McpServerFeatures.SyncPromptSpecification;
import io.modelcontextprotocol.server.McpServerFeatures.SyncResourceSpecification;
import io.modelcontextprotocol.server.McpServerFeatures.SyncToolSpecification;
import io.modelcontextprotocol.server.McpSyncServer;
import io.modelcontextprotocol.server.transport.StdioServerTransportProvider;
import io.modelcontextprotocol.spec.McpSchema.CallToolResult;
import io.modelcontextprotocol.spec.McpSchema.GetPromptResult;
import io.modelcontextprotocol.spec.McpSchema.Prompt;
import io.modelcontextprotocol.spec.McpSchema.PromptArgument;
import io.modelcontextprotocol.spec.McpSchema.PromptMessage;
import io.modelcontextprotocol.spec.McpSchema.ReadResourceResult;
import io.modelcontextprotocol.spec.McpSchema.Resource;
import io.modelcontextprotocol.spec.McpSchema.Role;
import io.modelcontextprotocol.spec.McpSchema.ServerCapabilities;
import io.modelcontextprotocol.spec.McpSchema.TextContent;
import io.modelcontextprotocol.spec.McpSchema.TextResourceContents;
import io.modelcontextprotocol.spec.McpSchema.Tool;

public class CalculatorServer {

    public static void main(String[] args) {
        // 1. 选择 stdio 传输:本地进程,由客户端以子进程方式拉起
        var transport = new StdioServerTransportProvider(McpJsonDefaults.getMapper());

        // 2. 构建服务器:声明标识与能力(tools / resources / prompts)
        McpSyncServer server = McpServer.sync(transport)
            .serverInfo("calculator-demo", "1.0.0")
            .capabilities(ServerCapabilities.builder()
                .tools(true)              // 工具能力
                .resources(false, false)  // 资源能力(subscribe=false, listChanged=false)
                .prompts(true)            // 提示词能力
                .build())
            .build();

        // 3. 注册工具 / 资源 / 提示词
        server.addTool(addTool());
        server.addTool(fibTool());
        server.addResource(noteResource());
        server.addPrompt(codeReviewPrompt());

        // 4. 优雅关闭;stdio 传输的非守护线程会保持进程存活,等待客户端调用
        Runtime.getRuntime().addShutdownHook(new Thread(server::closeGracefully));
    }

    // ---- Tool:加法(JSON Schema 声明入参 + 处理器)----
    static SyncToolSpecification addTool() {
        Map<String, Object> schema = Map.of(
            "type", "object",
            "properties", Map.of(
                "a", Map.of("type", "integer"),
                "b", Map.of("type", "integer")),
            "required", List.of("a", "b"));

        return SyncToolSpecification.builder()
            .tool(Tool.builder("add", schema)
                .description("求两个整数之和")
                .build())
            .callHandler((exchange, request) -> {
                int a = ((Number) request.arguments().get("a")).intValue();
                int b = ((Number) request.arguments().get("b")).intValue();
                return CallToolResult.builder()
                    .content(List.of(new TextContent(String.valueOf(a + b))))
                    .isError(false)
                    .build();
            })
            .build();
    }

    // ---- Tool:斐波那契 ----
    static SyncToolSpecification fibTool() {
        Map<String, Object> schema = Map.of(
            "type", "object",
            "properties", Map.of("n", Map.of("type", "integer")),
            "required", List.of("n"));

        return SyncToolSpecification.builder()
            .tool(Tool.builder("fib", schema)
                .description("计算斐波那契数列的第 n 项(n >= 0)")
                .build())
            .callHandler((exchange, request) -> {
                int n = ((Number) request.arguments().get("n")).intValue();
                if (n < 0) {
                    return CallToolResult.builder()
                        .content(List.of(new TextContent("n 必须 >= 0")))
                        .isError(true)
                        .build();
                }
                int a = 0, b = 1;
                for (int i = 0; i < n; i++) {
                    int next = a + b;
                    a = b;
                    b = next;
                }
                return CallToolResult.builder()
                    .content(List.of(new TextContent(String.valueOf(a))))
                    .isError(false)
                    .build();
            })
            .build();
    }

    // ---- Resource:只读数据,用 URI 标识 ----
    static SyncResourceSpecification noteResource() {
        Resource resource = Resource.builder("notes://todo", "Todo 便签")
            .description("按名字读取便签内容")
            .mimeType("text/plain")
            .build();

        return new SyncResourceSpecification(resource, (exchange, request) -> {
            String content = switch (request.uri()) {
                case "notes://todo" -> "1. 写 MCP 博客\n2. 本地构建验证";
                case "notes://idea" -> "下一篇写 Agent 编排";
                default -> "未找到该便签";
            };
            return ReadResourceResult.builder(List.of(
                    TextResourceContents.builder(request.uri(), content)
                        .mimeType("text/plain")
                        .build()))
                .build();
        });
    }

    // ---- Prompt:可复用提示词模板 ----
    static SyncPromptSpecification codeReviewPrompt() {
        Prompt prompt = Prompt.builder("code_review")
            .description("生成一段代码评审提示词")
            .arguments(List.of(
                PromptArgument.builder("language")
                    .description("编程语言")
                    .required(false)
                    .build()))
            .build();

        return new SyncPromptSpecification(prompt, (exchange, request) -> {
            Object lang = request.arguments() == null ? null : request.arguments().get("language");
            String language = lang == null ? "Java" : lang.toString();
            String text = "请以资深 " + language + " 工程师的视角,从正确性、性能、可读性三个维度审查以下代码:";
            return GetPromptResult.builder(List.of(
                    PromptMessage.builder(Role.USER, new TextContent(text)).build()))
                .description("代码评审提示词")
                .build();
        });
    }
}

注意:与 Python 的类型推导不同,Java SDK 中工具入参 Schema 是显式声明的 JSON SchemaMap 形式)——更精确但更啰嗦。SDK 2.x 内置入参校验,参数不符合 Schema 时直接返回 isError 的工具结果,不会进入你的处理器。

第三步:启动

# 打包后以 stdio 模式运行:客户端会以子进程方式拉起本进程
mvn -q package
java -jar target/calculator-server-1.0.0.jar

远程(Streamable HTTP)模式:把传输换成 HttpServletStreamableServerTransportProvider,注册进任意 Servlet 容器(Spring Boot / Jetty 等)即可暴露 /mcp 端点,鉴权走 OAuth 2.1。

验证:HTTP 模式启动后,直接 curl 就能看到协议在跑(端口以容器实际配置为准):

curl -X POST http://localhost:8080/mcp \
  -H "Content-Type: application/json" \
  -H "Accept: application/json, text/event-stream" \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}'

4.3 更简洁的 Java 方式:Spring AI 注解版

Java 生态还有一个官方推荐的快捷路径——Spring AI 2.0+ 的 MCP 注解支持spring-ai-starter-mcp-server-webmvc / webflux 等 starter):一个 @Tool 注解就能把普通 Java 方法暴露成 MCP 工具,方法名、参数名、@ToolParam 描述会自动生成工具 Schema,体验接近 Python FastMCP 的”类型即 Schema”:

import org.springframework.ai.tool.annotation.Tool;
import org.springframework.ai.tool.annotation.ToolParam;
import org.springframework.context.annotation.Configuration;

@Configuration
public class CalculatorTools {

    @Tool(description = "求两个整数之和")
    public int add(@ToolParam(description = "第一个整数") int a,
                   @ToolParam(description = "第二个整数") int b) {
        return a + b;
    }

    @Tool(description = "计算斐波那契数列的第 n 项(n >= 0)")
    public int fib(@ToolParam(description = "项数 n") int n) {
        if (n < 0) {
            throw new IllegalArgumentException("n 必须 >= 0");
        }
        int a = 0, b = 1;
        for (int i = 0; i < n; i++) {
            int next = a + b;
            a = b;
            b = next;
        }
        return a;
    }
}

Spring 容器扫描到 @Tool Bean 后自动注册进 MCP 服务器,starter 直接暴露 /mcp 端点,无需手写 Specification。如何选择:需要精细控制传输、协议元数据,或不想引入 Spring 的项目,用官方 SDK;Spring 系项目用注解版,开发效率最高。

4.4 写一个 MCP Client 调用自己的服务器

客户端是协议的另一半。用官方 Java SDK 调用上面的服务器:

// McpClientDemo.java
import java.time.Duration;
import java.util.Map;

import io.modelcontextprotocol.client.McpClient;
import io.modelcontextprotocol.client.McpSyncClient;
import io.modelcontextprotocol.client.transport.ServerParameters;
import io.modelcontextprotocol.client.transport.StdioClientTransport;
import io.modelcontextprotocol.json.McpJsonDefaults;
import io.modelcontextprotocol.spec.McpSchema.CallToolRequest;
import io.modelcontextprotocol.spec.McpSchema.CallToolResult;
import io.modelcontextprotocol.spec.McpSchema.GetPromptRequest;
import io.modelcontextprotocol.spec.McpSchema.GetPromptResult;
import io.modelcontextprotocol.spec.McpSchema.ListToolsResult;
import io.modelcontextprotocol.spec.McpSchema.ReadResourceRequest;
import io.modelcontextprotocol.spec.McpSchema.ReadResourceResult;
import io.modelcontextprotocol.spec.McpSchema.TextContent;
import io.modelcontextprotocol.spec.McpSchema.TextResourceContents;
import io.modelcontextprotocol.spec.McpSchema.Tool;

public class McpClientDemo {

    public static void main(String[] args) {
        // 1. 声明要拉起的服务器进程(stdio)
        ServerParameters params = ServerParameters.builder("java")
            .args("-jar", "target/calculator-server-1.0.0.jar")
            .build();

        // 2. 创建 stdio 传输 + 同步客户端
        McpSyncClient client = McpClient.sync(
                new StdioClientTransport(params, McpJsonDefaults.getMapper()))
            .requestTimeout(Duration.ofSeconds(10))
            .build();

        try {
            // 3. 初始化握手(SDK 自动协商协议版本与能力)
            client.initialize();

            // 4. 发现工具
            ListToolsResult tools = client.listTools();
            System.out.println("可用工具: " + tools.tools().stream().map(Tool::name).toList());

            // 5. 调用工具
            CallToolResult result = client.callTool(CallToolRequest.builder("add")
                .arguments(Map.of("a", 40, "b", 2))
                .build());
            System.out.println("add(40, 2) = " + ((TextContent) result.content().get(0)).text());

            // 6. 读取资源
            ReadResourceResult note = client.readResource(
                ReadResourceRequest.builder("notes://todo").build());
            System.out.println("便签内容:\n" + ((TextResourceContents) note.contents().get(0)).text());

            // 7. 获取提示词模板
            GetPromptResult prompt = client.getPrompt(GetPromptRequest.builder("code_review")
                .arguments(Map.of("language", "Java"))
                .build());
            System.out.println("提示词: " + ((TextContent) prompt.messages().get(0).content()).text());
        } finally {
            client.closeGracefully();
        }
    }
}

运行:java -jar target/client-demo.jar,输出类似:

可用工具: [add, fib]
add(40, 2) = 42
便签内容:
1. 写 MCP 博客
2. 本地构建验证
提示词: 请以资深 Java 工程师的视角,从正确性、性能、可读性三个维度审查以下代码:

调用远程服务器时,把 StdioClientTransport 换成 HttpClientStreamableHttpTransport,传入服务器 URL 即可:

var transport = HttpClientStreamableHttpTransport.builder("http://localhost:8080")
    .endpoint("/mcp")
    .build();
McpSyncClient client = McpClient.sync(transport).build();

4.5 调试:MCP Inspector

官方提供 MCP Inspector 可视化调试工具,无需写代码就能与任意服务器交互:

# 调试 stdio 服务器(命令 + 参数)
npx @modelcontextprotocol/inspector java -jar target/calculator-server-1.0.0.jar

# 或显式指定传输
npx @modelcontextprotocol/inspector --transport stdio -- java -jar target/calculator-server-1.0.0.jar

启动后浏览器打开 http://localhost:6274,你可以:查看服务器能力、手动调用每个工具、读取资源、查看原始 JSON-RPC 消息、模拟客户端行为。写服务器时的第一道调试手段,永远是它。

调试远程 Streamable HTTP 服务器时,同样可以在 Inspector 中选择 HTTP 传输并填入服务器 URL 连接。

4.6 生态盘点:开箱即用的服务器

不想从零写?官方与社区已经提供了大量成熟服务器:

类别 代表服务器 接入方式
官方参考实现 filesystem、git、memory、fetch、everything、sequentialthinking、time npx -y @modelcontextprotocol/server-*
代码托管 GitHub(官方)、GitLab 远程/本地均支持,token 鉴权
数据库 PostgreSQL、SQLite、Redis(官方参考) npx -y @modelcontextprotocol/server-*
浏览器/爬虫 Puppeteer(官方参考)、Playwright npx -y @modelcontextprotocol/server-puppeteer
办公协作 Google Drive、Slack、Notion 各家官方发布
可观测 Sentry、Cloudflare、Grafana 远程 HTTP + OAuth
云厂商 AWS、Azure、阿里云百炼等 各云厂商 MCP 网关

官方还在 2025 年 6 月推出了 MCP Registryregistry.modelcontextprotocol.io),用于服务器的发现与共享——就像 npm 之于 JavaScript、Docker Hub 之于容器,MCP Registry 之于 AI 工具生态。

4.7 常见坑与最佳实践

分类 最佳实践 原因/说明
工具设计 一个工具只做一件事,参数少而明确;名字用动词开头(create_issue 优于 issue 避免歧义,符合规范的工具命名指导
工具设计 入参 schema 描述要详细 模型只靠 description 理解工具,描述模糊 = 模型不会用
工具设计 校验失败返回工具执行错误isError: true + 可读信息),而非协议错误 模型能根据错误信息自我纠正
健壮性 工具必须幂等或至少可安全重试 MRTR 会重试原请求,无状态 HTTP 下网络重试是常态
健壮性 长任务用 notifications/progress 汇报进度 避免客户端误判超时
健壮性 stdio 服务器不要往 stdout 打印日志,日志一律走 stderr 避免污染协议流
安全 默认拒绝一切不确定操作;破坏性工具声明 destructiveHint Host 端配合用户确认
安全 远程服务器必须实现 OAuth 2.1,不要自造 token 方案 标准鉴权,可审计
安全 不信任模型传来的路径/URL 服务器侧做路径穿越与 SSRF 防护
性能 只读接口合理设置 ttlMs 让客户端缓存 减少无谓轮询
性能 工具列表保持稳定顺序 提升 LLM 提示词缓存命中率

五、总结与展望

MCP 与 Function Calling 的关系

很多人问:有了 Function Calling,为什么还要 MCP?答案是层次不同

维度 Function Calling MCP
层次 模型 API 的一个参数(怎么把工具描述给模型) 模型之外的应用层协议(工具如何被发现、连接、授权)
谁来实现 模型厂商 任何工具方
绑定 绑定特定厂商的模型 API 中立,跨模型、跨厂商
可替换性 换模型就要换适配 工具实现一次,处处可用

事实上两者是互补的:MCP Server 暴露工具 → 客户端通过 MCP 拿到工具清单 → 翻译成某个模型厂商的 Function Calling 格式 → 模型推理调用。MCP 解决”连接”问题,Function Calling 解决”推理”问题。

上面是”精确版”答案;还没完全 get 到的话,直接看第六节的”大白话版”。

现状与未来方向

截至 2026 年中,MCP 已经走完”从概念到标准”的阶段,正在走”从标准到基础设施”的阶段:

方向 现状
协议侧 无状态化让 MCP 服务器可以像普通微服务一样部署运维;MRTR 让”服务器向人要输入”成为一等公民;Tasks 作为官方扩展支持异步长任务
生态侧 主流模型厂商、云厂商、开发工具全线接入;MCP Registry 成为工具分发的公共设施
治理侧 规范进入行业共同治理轨道,版本节奏稳定,废弃机制(feature lifecycle)成熟——Roots/Sampling 的优雅退役就是例证

可以预见的方向:MCP 会继续向 Agent 互操作(多个 Agent 通过 MCP 协作)、结构化输出(outputSchema 进一步收紧)、企业级治理(细粒度权限、审计、多租户)演进。对开发者而言,MCP 正在成为 AI 时代的”标准库接口”——就像当年 REST 统一了前后端,MCP 正在统一”模型与世界的对话方式”。

一句话总结

MCP 用一套基于 JSON-RPC 的开放协议,把”模型、数据、工具”三者解耦:类型上分清角色/原语/传输/形态,原理上依靠能力自描述与多轮消息模式完成动态集成,使用上十分钟就能写一个服务器接入任何客户端——它是 AI 应用连接真实世界的通用插座。

现在,打开你的编辑器,写一个属于自己的 MCP Server 吧。


六、番外:大白话讲清楚 MCP 与 Function Calling、SKILL 的关系

前面的对比表”精确但抽象”,这一节用大白话把两者讲透。先记住一句话:

Function Calling 是”教模型怎么用工具”,MCP 是”让工具谁都能接”。

6.1 一个比喻:插座与插头

想象你要给手机充电:

  • Function Calling插头规格——你的手机用 Type-C 还是 Lightning,决定了”电”(工具调用)怎么进到手机(模型)里。麻烦在于每家手机厂商的插头规格还不一样,换个牌子就得换线。
  • MCP插座标准——充电器(工具方)把电接到统一的国标插座上,不管什么手机,插上就能充。插座是墙上的公共基础设施,跟你是哪个牌子的手机没关系。

再换个更生活化的场景——点外卖

  • 没有 MCP 时:想让 AI 帮你点餐,得分别装美团、饿了么、肯德基三个 App,记住三套完全不同的操作流程——对应各家模型私有的 Function Calling 格式,每个工具都要单独适配;
  • 有了 MCP 后:三家店统一入驻外卖平台(MCP Server),AI 只需要学会平台这一套标准话术,就能点任何一家店。

6.2 一张图看清”谁管哪一层”

┌──────────────────────────────────────────────┐
│ 模型推理:选哪个工具、传什么参数(大脑做决策)      │
│       ↑ 这一层是 Function Calling 的地盘        │
├──────────────────────────────────────────────┤
│ 连接层:工具怎么注册、发现、鉴权、调用(管道)      │
│       ↑ 这一层是 MCP 的地盘                    │
├──────────────────────────────────────────────┤
│ 真实世界:文件、数据库、GitHub、业务系统          │
└──────────────────────────────────────────────┘
  • Function Calling 管最上面一层:把工具描述(名字、参数、用途)翻译成模型能理解的格式,让模型在推理时选对工具、填对参数。它是模型 API 自带的一个参数,跟模型厂商深度绑定。
  • MCP 管中间一层:工具怎么被发现、怎么连上、谁有权限用、结果怎么回来。它是模型之外的独立协议,跟用哪个模型无关。

6.3 大白话对比表

对比点 Function Calling MCP
一句话 教模型”怎么用工具” 让工具”谁都能接”
本质 模型 API 里的一个参数格式 一个独立的开放协议
谁说了算 模型厂商(OpenAI、Anthropic 各有一套) 开放社区共同治理的标准
换个模型 工具适配代码要重写 工具实现一次,处处可用
管的事 推理层:模型怎么理解工具、怎么决定调用 连接层:工具怎么注册、发现、授权、调用
类比 手机充电的插头规格 / 各家 App 的专属操作流程 统一的国标插座 / 统一的外卖平台

6.4 最常见的两个误区

误区一:”有了 MCP 就不需要 Function Calling 了”——错,两者是配合关系,不是替代关系。真实链路是这样的:

MCP Server 暴露工具
   → MCP Client 拿到工具清单
   → 翻译成当前模型的 Function Calling 格式
   → 模型推理并调用工具

MCP 解决”连接”问题,Function Calling 解决”推理”问题——一个管管道,一个管大脑,缺一不可。

误区二:”MCP 是 Anthropic 家的私有协议”——早期确实如此,现在早已不是。2025 年起 OpenAI、Google、Microsoft 等主流厂商全部宣布支持,规范进入行业共同治理,谁都能实现、谁都能接入。

6.5 那模型到底先调哪个?——实际调用顺序

一个常见追问:”模型是先调 Function Calling,再调 MCP 吗?”答案是:不是先后两步,而是”模型表态、宿主执行”的分工。模型根本不会直接碰 MCP,完整的调用时序是这样的:

① 用户提问
   ↓
② 宿主把"问题 + 工具清单"发给模型
   (工具清单是初始化时通过 MCP 的 tools/list 发现、
    再翻译成该模型 Function Calling 格式的结果)
   ↓
③ 模型推理,决定"我要用 add(40,2)"
   (返回 tool_calls —— 这是 Function Calling 的地盘,模型只"表态")
   ↓
④ 宿主收到调用意图,找到对应的 MCP Server
   (这是宿主应用干的活,不是模型干的)
   ↓
⑤ 宿主的 MCP Client 向 MCP Server 发送 tools/call(JSON-RPC)
   ↓
⑥ MCP Server 真正执行:读文件 / 查数据库 / 调 GitHub API
   ↓
⑦ 结果沿 MCP 返回宿主
   ↓
⑧ 宿主把结果按 Function Calling 的 tool result 格式回传给模型
   ↓
⑨ 模型拿着结果继续推理,可能再发起下一轮工具调用

一句话总结:Function Calling 负责”表态”(模型说要用什么工具),MCP 负责”干活”(宿主去找服务器真正执行)。一个发生在模型 API 里,一个发生在应用层,二者在每一轮”工具调用 → 结果回传”的循环里交替配合,而不是先后串联的两步。

6.6 顺带聊聊:SKILL 和 MCP 是什么关系?

经常有人把 SKILL 和 MCP 搞混——它们都是”增强 AI 能力”的东西,但层次完全不同。

先明确 SKILL 指什么:Anthropic 推出的 Claude Skills(Agent Skills),把”做某件事的方法论”打包成一个文件夹:SKILL.md(说明”这事该怎么按流程做、按什么标准做”)+ 可选脚本和参考文件。模型加载后,就像拿到一本老师傅的经验笔记,立刻知道该怎么干活。

维度 MCP SKILL
本质 通信协议:连接外部工具和数据 知识包:封装做事的流程与经验
组成 Server 暴露 tools / resources / prompts SKILL.md 指令 + 可选脚本/资源
解决什么 能力从哪来(读文件、查库、调 API) 方法是什么(先做什么、后做什么、按什么标准)
作用对象 外部世界(数据、动作、系统) 模型自身的行为(怎么思考、怎么执行)
类比 工具箱 / 插座 使用说明书 / 老师傅的经验笔记

一句话:MCP 给 AI”装手和眼睛”,SKILL 给 AI”塞操作手册”——MCP 解决”能不能连到外面的世界”,SKILL 解决”活儿该怎么干、干得好不好”。

两者经常配合使用:比如一个”写周报”的 SKILL 会写明”先收集本周 commit → 按模块归类 → 突出风险项 → 用固定模板输出”;其中”收集 commit”这一步,SKILL 会指示模型去调用 GitHub 的 MCP 工具。SKILL 是剧本,MCP 是舞台上的道具与通道——SKILL 教模型什么时候用、怎么用工具,MCP 负责把工具真正接进来。反过来,MCP 完全不依赖 SKILL:一个 MCP Server 没有 SKILL 也能被正常调用。

记忆口诀:MCP 让 AI 够得着世界(工具、数据),SKILL 让 AI 知道怎么做(流程、规范、经验)——一个管”手脚”,一个管”方法论”。


一句话收尾:Function Calling 让模型”会用手”,MCP 让模型”有手可用”——一个管大脑,一个管手脚,合在一起,AI 才算真正”手脚并用”。


参考资料

本文阅读量