前面两周,我把团队内部那个一直靠“人肉查库、手动推送”的订单状态查询工具,接进了公司自己的Agent里。做完之后最大的感受是:MCP Server 这个事,看着概念新,实际落地起来其实就是一套标准化的工具封装流程。网上讲 MCP 概念的文章已经很多了,但真正能照着手把手写一个 Server、跑起来、接进 Host 的实操教程还是偏少。所以这篇我就从开发者的角度,把从 0 到 1 构建自己的 MCP Server 的完整过程写一遍,包括架构理解、SDK 选型、代码实现、本地调试,以及接入 Claude Desktop、浏览器的真实用法和几个容易踩的坑。
这篇东西适合两种人:一种是对 MCP 只有模糊概念、想快速上手写一个能跑的 Server 的开发者;另一种是已经在用各种 MCP 工具,但不太清楚 Host、Client、Server 之间到底怎么协作,想搞明白原理好排查问题的人。内容偏实操,你最好手边有一个 Python 3.10+ 的环境,跟着代码敲一遍,比单纯看十篇文章都管用。
1. MCP 是干什么的:先理解 Host、Client、Server 这三层
1.1 没有 MCP 之前,工具接入有多痛苦
在 MCP 出现之前,给 AI 应用接外部工具基本上是一个项目一种接法。你用 OpenAI 的函数调用,就得按 function calling 的 JSON Schema 写工具定义;你用自己的 Agent 框架,又要按框架的 Tool 抽象来写。更麻烦的是,每个工具服务都要单独实现一套鉴权、调用、错误通知机制,工具多了之后全是重复劳动。
我当时最烦的就是:同一个组织里,业务部门有查询接口、数据部门有分析服务、运维那边还有告警系统,每一套都要为 AI 单独做适配。这就像每买一个新电器,都得带一条专用充电线,接口还不一样。MCP 想解决的就是这个问题——它把“工具接入”这件事标准化了。你的服务只要实现一次 MCP 协议,任何支持 MCP 的 AI 应用(也就是 Host)都能直接调用,不需要重复适配。
1.2 MCP 的三层架构:谁在说话、谁在翻译、谁在办事
MCP(Model Context Protocol,模型上下文协议)从架构上分成三层:MCP Host、MCP Client、MCP Server。
- MCP Host 是用户直接面对的那一层,比如 Claude Desktop、IDE 插件、自研的 Agent 应用。它负责和用户对话、调用大模型、决定什么时候去请求工具。它本身不直接和你的工具服务打交道。
- MCP Client 是 Host 内部内置的协议客户端,负责建立连接、发送请求、接收结果。你可以把它理解成 Host 的“翻译官”。一个 Host 可以同时连接多个 Server,也就意味着一个 Host 里可能有多个 Client 实例在同时工作。
- MCP Server 就是你要开发的部分。它对外暴露具体的工具、数据资源和提示词模板,被客户端调用。
很多人一开始搞混 Host 和 Server,其实记住一句话就行:Host 是“大脑”,Server 是“手”。大脑决定要做什么,手负责具体执行。Client 则是连接这两者的“神经”。当你在 Claude Desktop 里看到某个工具被调用,链路是:用户提问 → Host 让大模型判断需要调用工具 → Host 里的 Client 发起请求 → Server 执行逻辑 → 返回结构 → 大模型再把结果组织成自然语言回给用户。
1.3 Tools、Resources、Prompts:Server 对外暴露的三类能力
MCP Server 不是只提供“工具调用”这一种能力,它实际上有三类对外接口,搞清楚这三者的区别,你的 Server 设计会清晰很多。
- Tools:可执行的动作,一般是“让 AI 去做某件事”。比如查订单、发邮件、创建工单。Tool 需要显式调用,AI 决定调用哪个、传什么参数。
- Resources:可读取的数据,一般是“让 AI 获取上下文”。比如一个订单列表、一份配置文件的 JSON、一篇文章正文。Resource 通常以 URI 的形式暴露,有点像把文件系统能力开放给 AI。
- Prompts:可复用的提示词模板。比如“生成订单周报”“写一段会议纪要开头”。Prompt 能引导模型按固定格式做事,适合沉淀团队里的最佳实践。
实际开发中,Tools 是绝大多数场景的主角,但 Resources 也很常用——如果你的 Agent 需要先读到一批背景数据再回答问题,用 Resource 比让模型瞎猜强得多。后面实战部分我会三样都写一遍,让你看到它们在一个项目里怎么共存。
2. 动手前的准备:SDK 选型与项目初始化
2.1 用官方 SDK 还是 FastMCP
Python 生态里现在有两条主流路线:一是官方提供的mcpPython SDK,另一个是社区封装FastMCP。我实际两个都试过,结论是:如果你是想快速做出一个能用的服务,优先用 FastMCP;如果是要深度定制协议细节、造轮子,再看看官方 SDK。
原因很简单。官方 SDK 把协议层的细节暴露得很充分,灵活是灵活,但写起来啰嗦。你需要自己处理 initialize 握手、工具列表声明、请求路由这些事。而 FastMCP 把这些全部简化成了装饰器风格——你写一个普通 Python 函数,加一行@mcp.tool(),就完成了工具注册。底层还是走官方协议,但开发体验完全是现代 Python 框架的感觉。
FastMCP 是社区项目,不是 Anthropic 官方出品,但它目前维护活跃,协议兼容性也跟得比较及时。我自己的判断标准是:个人项目、内部工具、快速迭代的场景,FastMCP 完胜;你要是想给复杂分布式系统做底座,或者需要非常细的协议控制,再考虑官方 SDK。
2.2 创建项目目录和虚拟环境
我习惯用uv管 Python 环境,比 pip 干净利落。不过用venv + pip也一样,看个人偏好。下面是 FastMCP 路线的初始化步骤。
# 创建项目目录并初始化虚拟环境 mkdir order-mcp-server && cd order-mcp-server python -m venv .venv source .venv/bin/activate # 安装依赖 pip install "mcp[cli]" fastmcp # 装完最好确认一下版本 python -c "import fastmcp; print(fastmcp.__version__)"这里有必要解释一下为什么需要安装mcp[cli]。虽然 FastMCP 本身会依赖官方 SDK,但我们后面调试要用mcp官方提供的 Inspector,那部分需要以命令行形式跑起来,所以提前把官方 CLI 一起装了。常见的问题是:只装 fastmcp 后发现没有mcp命令,然后又回头补装,白折腾一圈。
2.3 两种传输方式怎么选
MCP 服务端目前最常用的传输方式是 stdio 和 Streamable HTTP。
- stdio:Server 作为子进程被 Host 拉起,双方通过标准输入输出通信。这种方式配置简单,适合本地运行,Claude Desktop 配置里最常见。缺点是服务不能被多个进程远程共享。
- Streamable HTTP:Server 作为一个 HTTP 服务运行,支持远程访问。适合部署到服务器上给多个 Host 用。新版协议里已经废弃了老旧的 SSE-only 方式,统一走 Streamable HTTP。
开发初期建议先用 stdio 模式跑通整条链路,因为调试起来最简单,不需要考虑端口、鉴权、跨域这些问题。等逻辑稳定了,再切换到 HTTP 模式部署。后面实战和问题排查的部分,我会按这个顺序来。
3. 写一个能用的 MCP Server:从工具定义到数据暴露
3.1 用 FastMCP 定义一个订单查询服务
实战部分我用一个“订单查询服务”做例子,背景是:团队内部有一个订单系统,经常需要让 AI 助手帮忙查订单状态、看物流轨迹、创建简单订单。按传统做法,我会写一个 REST API 然后让 Agent 去调,但现在有了 MCP,我直接把业务逻辑封装成 Server。
先看最基本的代码结构:
from datetime import datetime from fastmcp import FastMCP # 创建 Server 实例,名字会显示在 Host 的可用工具列表里 mcp = FastMCP("order-service") # 模拟数据库里的订单数据 ORDERS = { "SO-1001": { "customer": "张三", "status": "shipped", "items": ["机械键盘", "显示器支架"], "created_at": "2025-01-10 14:30:00", "tracking": "SF-987654321", }, "SO-1002": { "customer": "李四", "status": "pending", "items": ["USB-C 扩展坞"], "created_at": "2025-01-12 09:15:00", "tracking": None, }, } @mcp.tool() def get_order(order_id: str) -> dict: """按订单号查询订单状态,订单号格式为 SO- 加数字,例如 SO-1001""" order = ORDERS.get(order_id) if not order: raise ValueError(f"订单 {order_id} 不存在") return order if __name__ == "__main__": mcp.run()这个文件只要运行起来,一个最小可用的 MCP Server 就算建成了。@mcp.tool()会把函数名转成工具名,docstring 转成工具描述,参数注释和类型标注转成参数 Schema。所以你在写工具函数时,docstring 一定要认真写清楚“这个工具干什么、参数格式有什么特殊要求”,因为大模型全靠这段描述来决定什么时候调用、怎么传参。
比如get_order("SO-1001")返回的就是订单字典;如果传了不存在的单号,我会直接抛ValueError,Host 会把错误信息带回给大模型,模型会自己决定是换一个参数重试、还是向用户道歉。这就是 MCP 工具和普通 API 一个很大的不同:API 返回 404 就结束了,MCP 工具的错误会成为模型上下文的一部分,影响下一轮推理。
运行这个 Server:
python server.pyFastMCP 默认跑在 stdio 模式上。如果你直接执行,程序会挂起、等待从 stdin 读取协议消息,这一般说明启动成功了。这时候不要觉得“没反应就是坏了”,stdio 模式本来就不该有任何输出——如果有输出,说明你代码里有 print,那些内容会污染协议通信,后面我会重点讲这个坑。
3.2 参数校验和错误返回:不要放过这个环节
很多人开发 MCP Server 时会把参数校验省略,觉得“模型应该传对参数”。但实测下来,大模型传参远比想象中多奇怪。有一次我测试时,模型把日期传成了"周一",把订单号里的字母 O 传成了数字 0,如果服务端不做校验,错误会一路流到业务系统里。
FastMCP 支持 Pydantic 模型来做参数校验,强烈建议参数一多就上 Pydantic:
from pydantic import BaseModel, Field class CreateOrderInput(BaseModel): customer_name: str = Field(..., min_length=2, max_length=50, description="客户姓名") items: list[str] = Field(..., min_length=1, description="商品名称列表,至少有一个商品") @mcp.tool() def create_order(input: CreateOrderInput) -> dict: """创建新订单,返回订单号""" order_id = f"SO-{len(ORDERS) + 1002}" ORDERS[order_id] = { "customer": input.customer_name, "status": "pending", "items": input.items, "created_at": datetime.now().strftime("%Y-%m-%d %H:%M:%S"), "tracking": None, } return {"order_id": order_id, "status": "created"}用 FastMCP 时,把 Pydantic 模型作为参数类型,SDK 会自动生成 JSON Schema,并在调用时做校验。校验失败的信息会作为工具错误返回给 Host,模型看到后可以自己修正参数再调一次。这其实是 MCP 流程里被低估的一环——好的校验策略能明显减少“模型对着错误参数反复试错”的情况。
错误返回这块,我的建议是:业务性错误(比如订单不存在)直接抛异常即可,但错误信息要写得像给同事看的提示,不要写“系统错误”这种废话。比如 “订单 SO-9999 不存在,当前可查询的订单号范围是 SO-1001 到 SO-1002”,模型很可能会顺着这个提示改参数重试,体验会好很多。
3.3 再补上 Resources 和 Prompts
Tools 只是一个 Server 的起点。我再给这个订单服务加上一个 Resource 和一个 Prompt,让读者直观感受三类能力的差异。
from fastmcp import FastMCP mcp = FastMCP("order-service") @mcp.resource("order://{order_id}/timeline") def get_order_timeline(order_id: str) -> str: """读取订单的完整状态时间线""" order = ORDERS.get(order_id) if not order: raise ValueError(f"订单 {order_id} 不存在") lines = [ f"订单:{order_id}", f"客户:{order['customer']}", f"创建时间:{order['created_at']}", f"状态:{order['status']}", ] if order.get("tracking"): lines.append(f"物流单号:{order['tracking']}") return "\n".join(lines) @mcp.prompt() def order_summary(order_id: str) -> str: """为指定订单生成一段周报风格的状态摘要""" return f"请根据订单 {order_id} 的数据,生成一段适合在周报中使用的状态描述。"Resource 在这个场景里说得通的地方在于:它不需要模型“决定去调用”,对 Host 来说更像一种数据发现机制——只要 Client 端支持,模型读取上下文时可能主动去抓取它。Prompt 更简单,就是给用户一个可选的提示词模板,点击后自动填进对话框。
从设计角度说,如果你的工具服务是给公司内部 Agent 用的,我建议把“高频、静态的数据”设计成 Resource,“AI 判断后再执行的动作”设计成 Tool。划分清楚,Host 侧的行为会好预测很多。
4. 本地调试:Inspector 和小型客户端
4.1 用 MCP Inspector 快速验证
代码写完了别急着接 Claude Desktop,先用官方提供的 Inspector 跑一遍,能省掉后面 80% 的排查时间。Inspector 是一个可视化调试面板,可以手动选择工具、传参数、看返回结果和错误堆栈,相当于 MCP Server 的 Postman。
启动方式:
npx @modelcontextprotocol/inspector python server.py这段命令的意思是:用 npx 拉起 Inspector,然后 Inspector 再以 stdio 子进程的方式启动python server.py。启动后浏览器会打开一个本地页面,你在界面上能看到order-service里注册的get_order、create_order工具,点进去手工填参数就能调用。
我第一次用 Inspector 时觉得这工具太值了。以前写普通 API 可以用 curl 测,MCP Server 因为走的是协议通信,直接用 curl 很难模拟,Inspector 把中间层全部可视化,还能看到原始 JSON-RPC 消息。排查参数 Schema 问题、调试错误返回,几乎全靠它。
调试时一个实用技巧:在 Inspector 里切换不同的输入看校验效果。比如给create_order传一个空的items列表,正常情况会看到参数校验错误;传一个正常参数,能看到工具执行结果。这相当于把端到端的错误处理链路提前验证一遍。
4.2 自己写一个 MCP 客户端做集成测试
Inspector 能验证单个工具的调用,但如果你想把 Server 集成到自动化测试里,更靠谱的方式是写一个最小客户端。官方 SDK 提供了客户端能力,代码量很小:
import asyncio from mcp import ClientSession, StdioServerParameters from mcp.client.stdio import stdio_client async def main(): server_params = StdioServerParameters( command="python", args=["server.py"], cwd=None, # 如果 server 依赖相对路径,这里可以指定工作目录 ) async with stdio_client(server_params) as (read, write): async with ClientSession(read, write) as session: await session.initialize() # 列出服务器上所有工具 tools = await session.list_tools() print("可用工具:", [t.name for t in tools.tools]) # 调用工具 result = await session.call_tool("get_order", {"order_id": "SO-1001"}) print("调用结果:", result.content) asyncio.run(main())这个脚本的好处是,它和 Claude Desktop 访问 Server 的方式几乎一致,都走 stdio。测试时如果脚本能跑通,那基本可以断定 Server 本身没问题,问题通常出在 Host 侧的配置上。
我在多个项目里的习惯是:把小客户端脚本放在tests/目录下,配合 pytest 写成集成测试,保证每次改动工具定义后,至少能自动验证“工具可被发现、可被调用”。这一步投入不大,但对 Server 的长期维护帮助挺大的。
5. 接入真实场景:Claude Desktop 与 Chrome MCP Server
5.1 把 Server 配置进 Claude Desktop
本地验证通过后,下一步就是接进真正面向用户的 Host。这里以 Claude Desktop 为例,因为它是目前对 MCP 支持最顺滑的桌面客户端之一。
Claude Desktop 的配置文件在 macOS 上位于~/Library/Application Support/Claude/claude_desktop_config.json,Windows 在%APPDATA%\Claude\claude_desktop_config.json。往mcpServers字段里加一段你的服务:
{ "mcpServers": { "order-service": { "command": "python", "args": ["/Users/你的用户名/dev/order-mcp-server/server.py"] } } }配置完重启 Claude Desktop,在界面右下角或设置里能看到连接状态。如果连接成功,对话时它会自动识别可用的工具,在需要查询订单时会主动调用。
这里有几个特别容易踩的配置坑。第一,command里的python必须是命令行里能直接找到的那个解释器。如果你用的是虚拟环境,建议直接把 command 写成虚拟环境里的 python 绝对路径,比如/Users/xxx/dev/order-mcp-server/.venv/bin/python,避免 PATH 不一致导致启动失败。第二,args里 server 脚本要写绝对路径,别写相对路径,因为 Host 启动子进程时的工作目录不等于你的项目目录。第三,fastmcp 代码里如果 import 了项目内的其他模块,务必确保工作目录正确——必要时可以在 server.py 开头手动sys.path.insert(0, "/项目绝对路径"),这个操作不算优雅,但确实能救急。
5.2 Chrome MCP Server 使用教程:让 AI 操作浏览器的正确姿势
很多人问 Chrome MCP Server 到底怎么用,网上信息比较零散,我结合自己的试用经历整理一下。浏览器类 MCP 的思路,是把“打开网页、点击元素、提取文本、截图、读取控制台日志”这些浏览器操作,封装成一个个 MCP 工具,然后让 AI 根据任务自行组合调用。典型用途包括:让 AI 打开某个页面并总结内容、爬取一个列表页的数据、自动填表提交、前端页面回归测试。
使用方式是分三步走的:
第一步,安装并启动浏览器侧的 MCP 服务端。社区主流的实现方式有两种:一种是基于 Chrome DevTools Protocol(CDP),启动一个带调试端口的真实 Chrome,然后由 MCP Server 通过 CDP 去控制浏览器;另一种是安装浏览器扩展,扩展通过本地回环端口与 MCP Server 通信。不管哪种,装完后都需要你在浏览器侧做一次授权确认,之后 MCP Server 才能拿到标签页的控制权。
第二步,把服务端配置进你的 MCP Host。以 Claude Desktop 为例,同样是在mcpServers里加一个条目,command 通常是npx,args 里写对应的包名和参数。配置完成后,Host 里会出现navigate、click、extract_text、screenshot这类工具。
第三步,对话时让 AI 执行任务。比如你说“打开百度首页,搜索 MCP Server,把搜索结果第一页的标题列出来”,模型会依次调用导航、输入、提取文本等工具,最终返回你一份标题列表。
这条经验值的部分是:让 AI 操作浏览器,一定要做好权限控制。我个人的安全准则是:浏览器 MCP 只用于“读取信息”和“非敏感操作”,凡是涉及支付、发布、删除、发送敏感数据的动作,必须加入人工确认环节,否则宁可不用。另外建议给浏览器 MCP 单独分配一个 Chrome profile,别让它带着你的个人登录态去做自动化,防止脚本行为被混入个人账号的上下文。
如果你有定制化需求,其实自己写一个浏览器 MCP Server 也不难。核心就是用 Playwright 或 Puppeteer 驱动浏览器,把每个页面动作封装成一个 tool,返回值尽量给结构化文本——比如页面核心区域文本、DOM 快照摘要,不要直接返回整个 HTML,否则上下文很快被撑爆。
5.3 远程 MCP Server 的部署与鉴权
如果你的 Server 不只是本地用,需要部署到服务器给多个 Host 远程连接,那就该切成 Streamable HTTP 传输方式。FastMCP 切换起来很快:
if __name__ == "__main__": mcp.run(transport="streamable-http")这样 Server 启动后会监听一个本地端口,以 HTTP 协议提供 MCP 服务。你可以在前面挂一个反向代理做 TLS、加一层访问控制。
远程场景下,鉴权是绕不开的话题。新版 MCP 协议对远程 Server 的要求是支持 OAuth 2.0(Authorization Code + PKCE),官方 SDK 已经内置了这套流程,Host 连接时会自动拉起授权。如果你在内网或者完全信任的环境里跑,可以在 Server 注册时关闭强制鉴权,但我建议只在开发环境这么做。生产环境至少要有 Bearer Token 级别防护,毕竟暴露在网络上的是“能执行你业务操作的工具”,不是只读的静态页面。
6. 踩坑实录:常见问题与排查技巧
6.1 启动慢、超时、连接失败
MCP Server 在 stdio 模式下是一个子进程,Host 启动它是有超时等待的。如果你服务里导入了很重的依赖、或者启动时连了一堆外部服务,很可能 Host 已经判定超时了你的 Server 才初始化完。表现就是 Host 里看不到工具,配置页显示连接失败。
排查方式我自己会分三步:第一步,先在终端手跑一遍python server.py,看启动是否有报错、日志输出是否异常;第二步,用 Inspector 启动一次采集启动耗时;第三步,启动慢的话,把耗时操作挪到工具内首次调用时再做,不要在 import 阶段做初始化。另外所有启动过程的日志全部打进 stderr,未来排查时用2>&1就能看到完整日志。
6.2 数据返回报警与序列化问题
MCP 工具返回给 Host 的数据,最终要能被序列化成 JSON。如果你的代码里返回了datetime对象、bytes、或者自定义类的实例,Host 侧很容易收到序列化错误。
我遇到最典型的就是返回datetime没转字符串,结果在 Host 里看到工具调用失败,错误信息却不明显。解决办法是,养成在工具返回时做一层显式转换的习惯,所有时间字段先strftime,所有枚举先转str,所有可能为None的字段想清楚要不要带回去。这不是难事,但很容易被忽略。
另外,大模型对超长返回的处理能力有限。如果你一个工具返回了 10 万字的日志,模型很可能“看不过来”,反而影响回答质量。尽量只返回模型需要的那部分信息,大文件走 Resource 按需读取,别一股脑塞进 tool result。
6.3 工具描述和参数 Schema 对不准
这是最隐蔽的坑,程序不报错,但模型就是调不对工具。比如你在create_order的 docstring 里写“创建订单”,但没有写明订单号生成规则、不写客户姓名的格式要求,模型可能随手传一个“张”这种单字名字,被 Pydantic 的min_length=2拦回来;也可能在items参数里传一个字符串而不是字符串列表。
解决办法是,把 docstring 和 Pydantic 的 description 当成产品文案来写。具体说明格式规范、枚举值、边界条件。实测效果差距很明显:描述写得清楚的工具,模型一次调用成功的概率会高非常多。你要记住,MCP 的工具 Schema 是给“一个很聪明但对你的业务完全不了解的代理”看的,多给一个例子,胜过一百个抽象描述。
6.4 日志、并发和资源回收
最后说几个平时一不注意就会翻车的点。
日志污染 stdio 是我见过最多的问题。在 stdio 传输模式下,stdout 是协议通道,绝对不能 print。一旦 print 了普通日志,Host 解析协议消息就会失败,表现是工具列表加载不出来。排查时把日志全部改到 stderr,或者直接配置 logging 模块输出到 stderr。
并发和全局状态是另一个大坑。FastMCP 在处理并发请求时,如果你的 Server 内部维护了可变全局变量(比如我这个示例里的ORDERS字典),多线程环境下就可能出现脏数据。简单的解决办法是给全局状态加锁,或者改用数据库/缓存存储。我自己的习惯是:Server 无状态化,所有业务状态放外面,Server 只做转发和协议适配。
资源回收也要注意。如果你的工具内部需要连接数据库、Redis、外部 HTTP API,记得在工具里显式关闭连接,或者用连接池复用。Server 在本地跑时进程生命周期短,连接泄漏不致命;一旦部署成常驻 HTTP 服务,连接泄漏就会慢慢把文件描述符吃光,然后出现各种莫名其妙的“无法连接”“内存暴涨”。我自己线上最严重的一次就是遗漏了数据库连接关闭,跑了三天把连接池打满,整个服务假死。
还有一个小技巧:善用 Host 侧的工具调用记录。Claude Desktop 里每次工具调用都会显示输入输出摘要,排查问题时先看 Host 记录,再对照 Server 的 stderr 日志,基本能在 10 分钟内定位绝大多数问题。
我在实际操作中最深的一个体会是:写 MCP Server 的技术门槛其实不高,真正决定这个服务好不好用的,是工具边界的定义和描述的质量。你给 AI 的工具,如果说明写得够清楚、参数校验够严格、返回结果够简洁,整个调用链路会顺滑得超出预期;反过来,工具描述含糊,参数宽松,模型就会在调用阶段反复横跳,体验立刻崩掉。所以动手开发之前,先把“这个服务到底想给 AI 暴露哪些能力、每个能力的边界是什么”想清楚,这一步省下来的时间,远比写代码本身多得多。如果你按这篇把订单服务跑通了,下一步可以试试把自己经常用的小脚本往 MCP 里套一层,很快你就能体会到“让 AI 直接操控你手头工具”的感觉了。