尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

从零手写MCP Server:让AI工具接入像USB-C一样统一

从零手写MCP Server:让AI工具接入像USB-C一样统一 前一阵子在本地折腾 AI 工作流发现一个特别头疼的事每次让模型去读项目文件、查数据库、调第三方服务都要按不同厂商的 function calling 格式写一遍适配代码换个 AI 客户端就得重来。后来我把这套能力用 MCPModel Context Protocol模型上下文协议重写了一遍才发现工具接入这件事原本可以这么清爽。这篇文章我会从协议设计思路讲到代码实战带你在本地从零写一个 MCP Server并把它接到支持 MCP 的 AI 客户端里真正跑起来。适合第一次接触 MCP 的开发者也适合用了 MCP 但一直没亲手写过 Server 的朋友。1. 先想清楚MCP 解决的是「工具接入」不是「模型本身」1.1 没有 MCP 之前工具接入为什么这么「碎」我做本地项目里的文档问答功能时最初的方案是让模型能读取指定目录的 Markdown 文件。听起来很简单实际落地要经历这么几步先选定底层模型查它支持的 function calling 格式接着给文件读取写一段工具描述 JSON把参数名、类型、用途都规定清楚然后处理模型的调用请求执行完再把结果拼回对话上下文最后如果你发现这个方案明天要换一个 AI 客户端上述所有代码基本作废。这种“碎”不只是麻烦而是每个工具、每个数据源都要重复一遍同样性质的胶水工作。一个典型项目里本地文件、SQLite 数据库、任务清单、内部 Web 服务每一个都要单独写接入逻辑。时间久了你会发现真正业务逻辑没写多少代码库反而长满了各类“接线器”。更难受的是这些接线器彼此格式还不一样今天接 A 模型写一套明天接 B 应用又得写一套。我去翻了很多团队的开源项目发现大家痛苦高度一致工具接入是非标准化的每换一层就重写一遍。MCP 就是在这样的背景下出现的——它想解决的问题不是让某个模型变聪明而是把“工具怎么暴露、怎么被发现、怎么被调用”这几件事变标准。1.2 MCP 的思路把能力暴露协议化MCP 给出的方案很朴素所有提供能力的服务都用同一个协议暴露自己的工具和数据所有消费能力的 AI 应用都用同一个协议去发现和调用。换句话说它把“能力接入”这件事从每对关系的私聊变成了公共标准下的群聊。我习惯把它类比成 USB-C 接口。USB-C 能取代一堆杂线不是因为它做了多惊天动地的事而是把供电、数据传输、协议协商统一到了同一个接口上。MCP 也一样它统一的是“模型怎么发现工具、怎么发起调用、怎么拿回结果”这段完整的交互流程。你写好的一个 MCP Server可以在任何支持 MCP 的客户端里复用不用为每个客户端改代码。这里要强调一个边界MCP 改变不了模型本身的智商它不负责让回答更流畅也不参与模型训练或微调。它管的是连接层。换句话说你把一个文件系统封装成 MCP ServerAI 客户端就能用标准方式读取文件但能不能从文件里总结出好观点那是模型能力的事。理解这个边界很重要能避免你对 MCP 抱有不切实际的预期。1.3 和 function calling、RAG 的区别一句话就能说清我在社区里看到不少新手把 MCP、function calling、RAG 混在一起其实它们的定位完全不同。function calling 是模型服务内部的一种函数调用机制解决的是“怎么把参数结构化和结果返回给模型”这件事但它绑定特定厂商实现RAG 是给模型补充外部知识它的目标是“让模型回答它原本不知道的内容”MCP 是连接层标准它让工具和数据的暴露方式协议化方便同一套能力在不同应用和模型之间复用。我对三者的记忆口诀是RAG 是给厨师递菜谱function calling 是告诉厨师某台特定烤箱的按钮怎么按MCP 则是把所有烤箱统一成同一个操作台。实际项目里三者并不冲突反而经常一起用。例如我可以在 MCP Server 里实现一个“语义搜索”工具工具内部用向量检索去找到相关文档再把结果包装成标准工具调用返回给 AI 应用。RAG 负责找MCP 负责把“找”这件事变成标准能力。2. 拆开协议骨架三个角色、三种能力、一条消息链路2.1 Host / Client / Server谁在组织、谁在转发、谁在干活MCP 的拓扑里有三个角色Host、Client、Server。Host 是用户直接面对的应用层比如你用的桌面 AI 客户端、IDE 插件、Agent 框架。它负责承载整个会话决定“什么时候该调用工具”“调用结果怎么展示”。Client 是 Host 内部的协议组件专门负责与 Server 建立连接、收发 JSON-RPC 消息、维护会话状态。Server 则是能力提供方它连接真实的文件系统、数据库或第三方服务并把它们包装成协议可描述的能力。一个 Host 内部可以同时跑多个 Client每个 Client 连接不同的 Server。例如一个 IDE 插件可以同时连接项目文件 MCP Server、数据库 MCP Server 和浏览器调试 MCP Server。Server 本身不感知模型用什么也不感知 Host 是谁它只对 Client 负责。这也正是 MCP 能复用的关键Server 只需要写一遍换个 Host 照样能接。2.2 JSON-RPC 2.0从 initialize 到 tools/call 的完整会话MCP 的消息层基于 JSON-RPC 2.0这是一种轻量的远程调用协议。一个典型会话会经历下面几个阶段。首先是 initialize。Client 向 Server 发送一条 initialize 请求带上自己支持的协议版本和客户端信息Server 回应自己的协议版本、能力列表和服务名称。这个阶段相当于两个人在自我介绍并确认“我们用同一个版本的语言交流”。接着 Client 发送notifications/initialized通知表示初始化完成。之后 Client 就可以调用tools/list获取 Server 暴露的工具清单Server 会返回每个工具的名称、描述、参数 JSON Schema。再往后当 Host 决定让模型调用某个工具时Client 会发送tools/call请求Server 执行对应逻辑并返回结果。我手动给本地 Server 发过一条简单的 initialize 消息大致长这样{jsonrpc:2.0,id:1,method:initialize,params:{protocolVersion:2025-06-18,capabilities:{},clientInfo:{name:manual-test,version:1.0}}}Server 返回的响应里会包含协议版本、capabilities 和 serverInfo。看到这个响应基本可以确认 Server 进程活着且协议握手成功。整个交互都是双向的一次请求对应一次响应超时、错误也都有标准字段可以表达。2.3 Tools / Resources / Prompts三种原语的边界MCP 协议定义了三种核心原语Tools、Resources、Prompts。Tools 是最常用的它表示一个可被模型调用的函数通常带有参数、会执行操作、可能产生副作用比如“读取文件”“发送请求”“写入数据库”。模型在推理过程中可以主动发起调用。Resources 是暴露给模型的数据内容比如项目里的某个文档、数据库中的某条记录。它更像“文件”模型不是去调用它而是在需要的时候引用它作为上下文。实际实现中Server 可以通过resources/list和resources/read让客户端浏览和读取这些数据。Prompts 则有点类似模板命令。Server 可以为用户交互提供可复用的提示词模板例如“总结某个文档”“分析当前目录代码结构”。用户在 Host 界面里触发这些模板就能快速发起一个预定义的交互过程。三者的差别可以这样看Tools 是“动词”模型发起Resources 是“名词”模型引用Prompts 是“剧本”用户触发。动手写 MCP Server 时多数场景先聚焦 Tools 就够了。2.4 stdio 与 HTTP传输层应该怎么选MCP 支持多种传输方式主流的是 stdio 和 HTTP。stdio 模式适用于本地开发场景Server 以子进程方式启动Client 通过标准输入、标准输出与 Server 通信。它的好处是简单、隔离性好、不涉及网络端口非常适合个人本地工具和桌面应用。我的本地项目默认都用 stdio。另一种是通过 HTTP 承载的远程传输适用于部署在一台服务器上、供多个客户端共享的工具服务。2025 年之后协议主推的 Streamable HTTP 模式支持流式与非流式请求双向通信能力比早期的 SSE 方案更强。选型的建议很简单只在本机用选 stdio要部署成团队共享服务选 HTTP。别一上来就把本地工具做成网络服务网络化意味着要补认证、限流、防攻击这些成本大多数本地场景根本不需要付。3. 从零写一个本地 MCP Server项目文档助手实战3.1 实战目标让 AI 能看懂项目里所有 Markdown 文档我们现在做一个叫“项目文档助手”的本地 MCP Server。目标场景是我的项目里有大量.md文档包括架构说明、开发规范、发布记录。我希望 AI 客户端能自动发现这些文档、按需读取内容、还能根据关键词定位相关内容。为此Server 需要暴露三个能力列出项目里所有 Markdown 文件、读取指定文件内容、在文档里搜索关键词。这三个能力对应三个工具函数足够覆盖多数实际使用场景又不会因为工具太多把模型搞得无所适从。项目结构长这样project-doc-mcp/ ├── .venv/ ├── server.py ├── README.md └── docs/ ├── 架构说明.md ├── 开发规范.md └── 发布记录.md3.2 环境准备虚拟环境与官方 Python SDKMCP 的官方 Python SDK 包名就是mcp。先建虚拟环境再安装依赖mkdir project-doc-mcp cd project-doc-mcp python -m venv .venv source .venv/bin/activate pip install mcp建议 Python 版本用 3.10 以上SDK 内部大量使用类型注解和异步语法版本太低容易出兼容问题。装完后可以确认一下版本号python -c from importlib.metadata import version; print(version(mcp))3.3 用 FastMCP 实现 Server三个工具函数官方 SDK 提供了一个高级封装叫 FastMCP它的设计思路是你只需要写普通函数加个装饰器FastMCP 会自动把函数签名转换成工具调用的 JSON Schema并处理底层的协议交互。下面是完整的server.pyfrom pathlib import Path from mcp.server.fastmcp import FastMCP PROJECT_ROOT Path(__file__).parent.resolve() mcp FastMCP( project-doc-mcp, instructions你是项目文档助手可以帮用户列出、读取和搜索项目中的 Markdown 文档。, ) mcp.tool() def list_markdown_files() - list[str]: 列出项目目录下所有 Markdown 文件用于了解项目文档结构。 return sorted( str(p.relative_to(PROJECT_ROOT)) for p in PROJECT_ROOT.rglob(*.md) ) mcp.tool() def read_file(relative_path: str) - str: 读取项目目录下的一个文件返回其内容。相对路径必须指向项目内存在的文件。 path (PROJECT_ROOT / relative_path).resolve() if not path.is_file(): return f文件不存在{relative_path} try: content path.read_text(encodingutf-8, errorsreplace) except Exception as exc: return f读取文件失败{exc} return content[:8000] mcp.tool() def search_docs(keyword: str) - list[str]: 在项目内 Markdown 文件中搜索关键词返回文件路径、行号和匹配行内容。 results [] for p in PROJECT_ROOT.rglob(*.md): try: for line_no, line in enumerate( p.read_text(encodingutf-8, errorsignore).splitlines(), start1, ): if keyword in line: results.append( f{p.relative_to(PROJECT_ROOT)}:{line_no}: {line.strip()[:120]} ) except Exception: continue return results[:50] if __name__ __main__: mcp.run()三个函数的定位很清晰先列目录让模型知道有哪些文档再按需读取拿到完整内容最后是关键词搜索用于快速定位某个概念出现在哪些文档里。代码里有几个细节值得解释一下。PROJECT_ROOT用的是Path(__file__).parent.resolve()确保无论 Server 被从哪里启动根目录都固定为脚本所在目录不会因为当前工作目录变化导致路径错乱。每个工具函数都写了完整的中文描述这非常关键因为模型看不到函数内部实现它在推理时依赖的就是函数名、参数名和描述信息。read_file返回时截断到 8000 字符是为了防止单个文件过大导致上下文膨胀。你可以按自己项目的文档规模调整这个值。search_docs里对每一行做了匹配而不是把整个文件放进内存这样即使文档很大也不会把 Server 拖垮。3.4 启动自测手动发一条 JSON-RPC 消息验证握手写完 Server 后先用最简单的方式测一下协议能不能通。在终端里运行python server.py这个命令默认以 stdio 模式启动。你可以手动给它喂一条 JSON-RPC 消息验证握手比如在另一个终端里执行echo {jsonrpc:2.0,id:1,method:initialize,params:{protocolVersion:2025-06-18,capabilities:{},clientInfo:{name:manual-test,version:1.0}}} | timeout 3 python server.py如果一切正常你会看到 Server 返回包含serverInfo和capabilities的 JSON 响应。这就说明进程启动成功、协议握手没问题。看到响应后进程会因为等待后续输入而挂着用 CtrlC 结束即可。这个手动测试在什么情况下特别有价值当你的 AI 客户端连不上 Server、报“无法连接”时先手动发 initialize 消息能够快速区分问题是出在 Server 本身还是出在 Host 配置。这比把全部报错甩给 AI 客户端要高效得多。4. 接进 AI 客户端配置、推理与工具调用的完整链路4.1 客户端配置把 mcpServers 指向本地进程要让 AI 客户端用上我们刚写的 Server需要在客户端的 MCP 配置里声明这个服务器。不同客户端的配置入口略有区别但本质上都是维护一份mcpServers列表把 Server 名称、启动命令、参数和当前目录告诉 Host。以配置文件方式为例核心配置段长这样{ mcpServers: { project-doc: { command: /path/to/project-doc-mcp/.venv/bin/python, args: [server.py], cwd: /path/to/project-doc-mcp } } }注意command我写的是虚拟环境里的 Python 解释器绝对路径。这一步很容易踩坑如果你直接写pythonHost 子进程可能用的是系统全局 Python那个环境里未必装了mcp库结果就是这个 Server 起不来。配置完成后重启客户端让配置生效。4.2 从用户提问到工具结果回传一次完整调用的 6 个节点平台配置好了很多开发者却不知道模型到底是怎么一步步用上工具的。我把完整链路拆成 6 个节点你就明白“MCP 怎么被调用”了。用户在 Host 里提问比如“README 里写了什么”。Host 获取当前会话可用的 MCP 工具列表这些工具的描述会被拼进发给模型的上下文里。模型在推理时判断需要调用某个工具输出一个结构化的工具调用意图比如调用list_markdown_files。Host 里的 MCP Client 收到这个意图把它翻译成一条tools/call协议消息发送给对应的 Server 进程。Server 执行函数逻辑把结果以 JSON 形式返回给 Client。Client 把工具结果回传给模型模型基于结果生成最终回答Host 展示给用户。整个过程中模型没有直接访问文件系统它只看到“工具描述 工具返回结果”具体执行完全由 Server 完成。这种隔离带来一个好处你可以在不改变任何工具逻辑的前提下把同一套能力接入不同模型、不同客户端。4.3 联调手段Inspector、stderr 日志与重启注意事项初次联调时几乎一定会遇到问题。我最推荐的调试工具是 MCP 官方配套的 Inspector它能可视化地查看工具列表、手动触发工具、回看协议消息。启动方式也很简单npx modelcontextprotocol/inspector python server.pyInspector 启动后会在浏览器里打开一个调试面板左侧显示工具列表右侧可以填参数并发送调用。这个工具最大的价值是“绕过模型直接测试 Server 本身”帮你确认问题到底在 Server 逻辑还是模型选择。另一个关键实践是日志输出位置。stdio 模式下标准输出是协议通道绝对不能用来打印业务日志日志必须走标准错误。我通常会在代码里加一行import sys print(server started, project root:, PROJECT_ROOT, filesys.stderr)这样做能保证 Server 的调试信息可见又不污染协议消息。很多新手在 Server 里用print输出调试信息接进 Host 后流量全部损坏连接报错排查半天才发现是这个原因。最后是重启问题MCP Server 以子进程方式运行修改 Server 代码后必须重启客户端或者确保客户端能重新拉起子进程。我实测时经常写完代码不重启就一遍遍测试白白浪费很多时间。现在我会先确认配置中进程不存在再重新连接。5. 一个「好」的本地 MCP Server还要处理这些边界5.1 工具描述与参数 SchemaAI 用不用的准先看「自解释」工具能不能被模型正确调用很大程度上不取决于代码逻辑而取决于工具的“自解释”能力。模型在推理时只能看到工具名、参数名、描述和 JSON Schema它看不到你的实现。所以写工具描述时要站在“一个不了解内部实现的 AI 模型”的视角来写。好的工具描述应该包含三部分这个工具是做什么的、什么场景应该用、什么场景不要用。举个例子search_docs的描述如果只写“搜索文本”模型就不知道这个工具是搜项目文档还是搜整个文件系统。像代码里那样写“在项目内 Markdown 文件中搜索关键词返回文件路径、行号和匹配行内容”模型就清楚它的边界。参数设计也要尽量精简。如果一个工具需要 5 个以上参数模型选择错误参数的概率会明显上升。我建议把工具拆细一个工具只负责一件事必要参数控制在 3 个以内可选参数给默认值。实在复杂的场景就用多个工具组合完成。5.2 路径与权限控制别让 Server 变成任意文件读取接口本地 MCP Server 看似只给自己用但依然要做好路径校验。最典型的攻击路径是路径穿越也就是模型在被诱导后传入类似../../etc/passwd这样的路径。如果 Server 直接拼接路径去读就可能读到项目目录外的敏感文件。我的防护方式是先做路径归一化再用relative_to校验path (PROJECT_ROOT / relative_path).resolve() if PROJECT_ROOT not in path.parents and path ! PROJECT_ROOT: return f不允许访问项目目录之外的文件{relative_path}这个写法先把路径解析成绝对路径再确认它确实位于项目根目录之下。校验失败就返回提示字符串不做任何拼路径的补救。虽然这个 MCP Server 是以本地可信场景为主但把防护写在前面之后如果要把同样代码部署成 HTTP 服务就不用再补课了。权限控制还要考虑另一层本地文件服务应默认只读。像我们提供的工具只读文件内容、搜索关键词不动任何写入操作。需要写文件时我会单独提供一个带明确描述的 write 工具而不是让 read 工具同时承担写的能力。5.3 错误返回要「模型可读」日志要「人可读」很多开发者在写工具时遇到异常就直接抛出让 FastMCP 把异常包装成协议错误。这样做的问题是模型拿到的是一段晦涩的异常信息它没法判断下一步应该怎么做。更好的做法是预期内的错误直接作为字符串结果返回。我在read_file里写的 “文件不存在xxx” 就是一个典型例子。模型拿到这个字符串就能理解是路径传错了会在后续回答里直接告诉用户“这个文件不存在”而不是抛出一个让人摸不着头脑的调用失败。而真正需要人工排查的错误比如调用栈、依赖缺失、启动失败应该由 Server 在启动阶段用日志输出通常是写进 stderr 或日志文件。我习惯给每个工具加一个最小化日志print(f[tool] search_docs keyword{keyword}, filesys.stderr)这样的好处是当工具行为不符合预期时能从日志里看到模型实际传入了什么参数快速判断是模型选错工具还是参数被截断。模型可读的错误面向“自动恢复”人可读的日志面向“事后定位”两者职责不同缺一不可。6. 复盘自研 Server 前的判断、取舍与我的体会6.1 先翻现成生态再决定要不要自己写动手写任何 MCP Server 之前我强烈建议先花半小时翻社区里已经存在的 MCP Server 集合。MCP 生态从 2024 年底开始爆发式增长到现在已经有非常丰富的现成实现。数据库连接、GitHub 操作、图片处理、设计稿导出、股票行情、交通查询等场景大概率已经有人做好了而且多数是开源项目。我见过不少开发者在没有调研的情况下从零写了一个功能与现成项目重复的 Server最后还要花大量时间维护和修 bug。正确路径是先用现成的跑通核心流程真要自定义也优先在已有项目上改而不是推翻重来。自己写 Server 前先问一句“这世界上是不是已经有人解决过类似问题”能帮你省下很多时间。6.2 什么时候适合自研 MCP Server现成生态虽多但自研依然有它的价值我归纳为三种典型情况。一种是你有高度私有化的工具或数据源。公司内部平台、特殊格式的本地数据库、自研系统这类东西没有公开 MCP 实现只能自己做协议封装。第二种是场景非常轻、不值得引入外部依赖比如我们要给 AI 暴露一个只有三个工具的文档接口自己写几十行代码比接一个几百行的通用工具更划算。第三种是你需要精确控制工具的权限边界和错误行为这时候自己写的逻辑最可控。反过来说如果你的需求正好落在某个成熟 Server 的覆盖范围且对方项目活跃度不错就别重复造轮子了。我的一个本地项目依赖某个第三方工具我一开始坚持自研最后发现对方在细节上比我处理得更完善果断切换成了现成方案整体效果反而更好。6.3 我踩过几次坑之后的选择逻辑复盘我自己的踩坑经历有三条经验值得分享。第一条是先明确这个 MCP Server 的消费端。如果只是本机用stdio 就够扎实地支撑全部需求别提前引入 HTTP、认证等复杂度。我是被“网络化”三个字忽悠过的人给一个本地小工具加了 HTTP 服务结果额外维护了一堆东西价值却没增加。第二条是工具越少越好。写 Server 时总想把所有能力都暴露出去但工具太多会让模型难以选择经常调用错工具。我现在的做法是先用最小工具集跑通流程等模型确实经常需要某个能力时再加。第三条是协议细节比工具逻辑更容易出错。MCP 是建立在 JSON-RPC 之上的如果你不了解 initialize、tools/list、tools/call 这个生命周期可能连配置怎么排查都无从下手。所以我建议所有人动手写 Server 前至少手动发一次 initialize 消息把协议的“手感”建立起来。我现在做本地 AI 工具链MCP 已经成了标配。无论是把项目文档喂给 IDE 助手还是让 Agent 操作本地的脚本工具我都尽量用同一个标准去封装。如果你正准备给自己的项目接入 AI 能力我建议也从这样一个小小的本地 Server 开始跑通之后再向更多场景延伸。
返回列表