首页/新闻资讯/正文详情

手搓 MCP 服务:从零实现 Model Context Protocol 的实践记录(TaoToken 统一 Key 接入版)

发布时间:2026/9/27 12:53:03 来源:云帆数科 栏目:资讯中心
手搓 MCP 服务:从零实现 Model Context Protocol 的实践记录(TaoToken 统一 Key 接入版)
1. 为什么我要手搓一个 MCP 服务MCPModel Context Protocol是 Anthropic 提出的开放协议它定义了 AI 客户端Claude Desktop、OpenCode、Trae CN 这类工具和外部数据源之间的标准交互方式。你可以把它理解成「AI 界的 USB-C」客户端只管按协议发请求服务端只管按协议回数据两边不用互相认识。MCP 服务能做什么简单说就是把你的本地文档、数据库、内部 API 包装成 AI 能直接调用的工具和资源。适合谁适合手上有私有数据、想让 AI 真正读进去、又不想把数据传到第三方平台的开发者。我这次的目标很具体用 FastAPI 从零实现一个 MCP 服务走 JSON-RPC 2.0 协议、SSE 传输最后通过 TaoToken 的统一 Key 通道接进 AI 工具跑通一次完整的tools/list和tools/call调用。为什么不用现成 SDK因为我想把协议每一层都摸清楚——消息怎么分发、会话怎么管理、SSE 双通道怎么保活这些只有自己写一遍才真正理解。下面这份记录里你可以直接复制config.toml、settings.json骨架和启动命令跟着做就能跑通。2. TaoToken 前置统一 Key 与 API 通道准备在动手写服务之前先把 AI 侧的接入通道准备好。我选择 TaoToken 作为统一入口原因是它把模型对话、Coding Plan、API Key 管理收敛到一个控制台里MCP 服务调试时不用在多个平台之间来回切 Key。你需要做三件事注册账号、创建 API Key、确认接入文档里的请求格式。2.1 创建 API Key登录控制台后进入 API Keys 页面新建一个 Key 并复制保存。这个 Key 后面会同时用在两个地方一是 MCP 服务自身的 Bearer Token 认证保护你的服务端二是 AI 客户端调用模型时的鉴权。建议开发阶段用两个不同的 Key避免混淆。控制台入口https://taotoken.net/console?utm_sourcetaotoken_aicg_blog_endutm_contentmcp_fastapiutm_campaignrewriteAPI Key 管理https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentmcp_fastapiutm_campaignrewrite接入文档https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentmcp_fastapiutm_campaignrewrite2.2 确认 API 通道地址TaoToken 的 API 基础地址是https://taotoken.net/api兼容 OpenAI 风格的/v1/chat/completions调用。MCP 服务本身不直接调模型但你的 AI 客户端比如 OpenCode需要配置这个地址来发对话请求。把下面这段先记下来第 4 节的settings.json会用到# config.toml —— AI 客户端模型通道配置骨架 [model] provider openai-compatible base_url https://taotoken.net/api api_key sk-你的TaoToken密钥 model claude-sonnet-4-20250514如果你打算长期跑编码类 Agent 任务可以了解下 Coding Plan它按周期计费比单次调用更适合高频场景https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentmcp_fastapiutm_campaignrewrite3. FastAPI 搭建 MCP 服务JSON-RPC 与 SSE 双通道MCP over SSE 的核心架构是「GET 连 SSE 收消息POST 发命令」的双通道模式。服务端只暴露两个路由GET /mcp维持长连接推送事件POST /mcp接收 JSON-RPC 消息。初学者最容易搞混的就是这一点——以为所有通信都走 SSE其实客户端请求是普通 HTTP POST。3.1 项目结构与依赖pip install fastapi uvicorn sse-starlette pydantic目录结构建议这样组织后面排查问题时定位快mcp-server/ ├── server.py # FastAPI 入口 路由 ├── sse_manager.py # SSE 连接与会话管理 ├── handler.py # JSON-RPC 方法分发 ├── uri_parser.py # 资源 URI 解析 └── config.toml # 服务配置3.2 两个核心路由# server.py from fastapi import FastAPI, Request from fastapi.middleware.cors import CORSMiddleware from sse_starlette.sse import EventSourceResponse from sse_manager import SSEConnectionManager from handler import MCPSSEHandler app FastAPI(titleUni-Index MCP Server) sse_manager SSEConnectionManager() handler MCPSSEHandler(sse_manager) app.get(/mcp) async def sse_endpoint(request: Request): # 长连接分配 session_id推送 endpoint 事件 session_id sse_manager.create_session() async def event_generator(): yield {event: endpoint, data: f/mcp?session_id{session_id}} async for msg in sse_manager.listen(session_id): yield {event: message, data: msg} return EventSourceResponse(event_generator()) app.post(/mcp) async def jsonrpc_endpoint(request: Request, session_id: str None): body await request.json() if session_id and session_id in sse_manager.active_sessions: # SSE 模式异步处理通过 SSE 推送响应 import asyncio asyncio.create_task(handler.process_sse_request(session_id, body)) return {status: processing, session_id: session_id} # 无状态模式直接返回 JSON-RPC 响应 return await handler.handle_message(body, session_id, sse_manager) app.get(/health) async def health(): return {status: ok}3.3 JSON-RPC 消息结构与 Pydantic 模型所有 MCP 请求都遵循 JSON-RPC 2.0。一个tools/call请求长这样{ jsonrpc: 2.0, id: 1, method: tools/call, params: { name: search_document, arguments: {query: JSON-RPC, max_results: 5} } }字段里jsonrpc必须严格等于2.0少一个字符就返回-32600错误id用于请求响应匹配通知类消息可以为空。用 Pydantic 定义模型时我踩过一个坑不同客户端传参格式不统一有的用params.name有的用params.tool有的把参数塞在params.args里。所以我在 handler 里做了兼容层统一归一化成namearguments再分发。3.4 方法分发路由表handler.handle_message()里用字典做 method 分发比一长串 if-else 清晰得多ROUTES { ping: handle_ping, initialize: handle_initialize, notifications/initialized: handle_initialized, tools/list: handle_tools_list, tools/call: handle_tools_call, resources/templates/list: handle_templates_list, resources/read: handle_resources_read, prompts/list: handle_prompts_list, prompts/get: handle_prompts_get, }resources/list我故意没实现返回-32601方法不存在。原因是文档数量可能很大全量枚举没意义改成用resources/templates/list告诉客户端 URI 模板再通过search_document工具定位具体文档最后用resources/read精读——这更符合「先搜索、再精读」的使用模式。3.5 会话状态机与保活会话有三个状态NOT_INITIALIZED→AWAITING_INIT→ACTIVE。所有方法在处理前检查状态未就绪就返回-32000。保活用双重策略SSE 每 30 秒发一行注释心跳防代理超时服务端空闲 60 秒主动发ping并等待 30 秒响应。这里有个细节asyncio.Queue.get()默认无限阻塞没法做心跳得配合asyncio.Eventasyncio.wait_for实现可超时等待。4. 可复制配置config.toml 与 settings.json 骨架配置分两份config.toml管 MCP 服务自身settings.json管 AI 客户端怎么连过来。这两份骨架你可以直接抄改掉 Key 和路径就能用。4.1 config.toml# MCP 服务端配置 [server] host 0.0.0.0 port 8080 api_key uni-index-dev-key-2026 # Bearer Token客户端需带上 session_timeout 300 # 僵尸连接清理阈值秒 ping_interval 60 # 服务端主动 ping 间隔 heartbeat_interval 30 # SSE 注释心跳间隔 [index] doc_root ./docs # 本地文档根目录 uri_scheme uni-index # 资源 URI 前缀 [model] provider openai-compatible base_url https://taotoken.net/api api_key sk-你的TaoToken密钥 model claude-sonnet-4-202505144.2 settings.jsonAI 客户端侧以 OpenCode / Claude Desktop 这类支持 MCP 的客户端为例配置里声明一个 MCP server指向你的 FastAPI 服务{ mcpServers: { uni-index: { url: http://127.0.0.1:8080/mcp, headers: { Authorization: Bearer uni-index-dev-key-2026 } } }, model: { baseUrl: https://taotoken.net/api, apiKey: sk-你的TaoToken密钥, model: claude-sonnet-4-20250514 } }注意url只配一个/mcpGET 和 POST 共用这个路径靠 HTTP Method 区分。这是我把官方推荐的/sse/messages双路径合并后的设计好处是客户端配置极简代价是路由逻辑依赖 Method 判断。4.3 启动命令# 开发模式带热重载 uvicorn server:app --host 0.0.0.0 --port 8080 --reload # 生产模式 uvicorn server:app --host 0.0.0.0 --port 8080 --workers 2启动后先访问http://127.0.0.1:8080/health返回{status:ok}说明服务起来了。这一步别跳过我见过好几次服务没起就急着配客户端结果排查半天发现是端口占用。5. 验证请求跑通一次完整 JSON-RPC 调用配置就绪后用 curl 手动走一遍握手和工具调用确认链路通了再接客户端。整个过程分四步SSE 连接拿 session_id、initialize 握手、tools/list 列工具、tools/call 调工具。5.1 建立 SSE 连接curl -N -H Authorization: Bearer uni-index-dev-key-2026 \ -H Accept: text/event-stream \ http://127.0.0.1:8080/mcp你会看到服务端先推一个endpoint事件里面带着session_idevent: endpoint data: /mcp?session_idabc-1235.2 initialize 握手拿到 session_id 后用 POST 发 initializecurl -X POST http://127.0.0.1:8080/mcp?session_idabc-123 \ -H Authorization: Bearer uni-index-dev-key-2026 \ -H Content-Type: application/json \ -d {jsonrpc:2.0,id:1,method:initialize,params:{protocolVersion:2024-11-05,capabilities:{},clientInfo:{name:curl-test,version:1.0}}}响应会通过 SSE 通道推回来包含协议版本和服务器能力列表。接着发notifications/initialized通知会话进入 ACTIVE 状态。5.3 tools/list 列工具curl -X POST http://127.0.0.1:8080/mcp?session_idabc-123 \ -H Authorization: Bearer uni-index-dev-key-2026 \ -H Content-Type: application/json \ -d {jsonrpc:2.0,id:2,method:tools/list}预期返回两个工具get_server_status和search_document。5.4 tools/call 实际调用curl -X POST http://127.0.0.1:8080/mcp?session_idabc-123 \ -H Authorization: Bearer uni-index-dev-key-2026 \ -H Content-Type: application/json \ -d {jsonrpc:2.0,id:3,method:tools/call,params:{name:search_document,arguments:{query:JSON-RPC,max_results:5}}}成功的话SSE 通道会推回搜索结果格式类似event: message data: {jsonrpc:2.0,id:3,result:{content:[{type:text,text:找到 3 条结果: [docs_concepts:42]协议基础 (匹配度: 1.20)...}]}}看到这个返回说明从 FastAPI 服务到 JSON-RPC 分发到 SSE 推送的整条链路都通了。接下来把settings.json配进 AI 客户端就能在对话里直接让模型调用你的工具。想先在网页端验证模型通道是否正常可以打开模型对话页面发一条测试消息https://taotoken.net/model-chat?utm_sourcetaotoken_aicg_blog_endutm_contentmcp_fastapiutm_campaignrewrite6. 常见报错排查6.1 CORS preflight 被 401 拦截浏览器发 OPTIONS 预检请求时被 Auth 中间件拦下返回 401。原因是中间件注册顺序错了。FastAPI 里后注册的中间件先执行所以 CORSMiddleware 要放在最后注册让它最先处理 OPTIONSapp.add_middleware(MCPAuthMiddleware, api_keyapi_key) # 内层 app.add_middleware(CORSMiddleware, allow_origins[*]) # 外层6.2 ping 响应和客户端请求混淆服务端发 ping 后客户端返回{jsonrpc:2.0,id:srv-ping-xxx,result:{}}这个结构和一个无 method 的正常请求长得一样。我用两层校验区分第一层看字段特征无 method 有 result/error 有 id第二层查_pending_pings集合确认这个 ping_id 确实发过。只有两层都过才判定为 ping 响应否则忽略。6.3 SSE 连接空闲被代理切断asyncio.Queue.get()无限阻塞导致没法发心跳。改成asyncio.Eventasyncio.wait_for组合超时后先发心跳再继续等。同时每 30 秒发一行 SSE 注释: heartbeat很多反向代理看到有数据流动就不会断连。6.4 资源 URI 行号非法客户端可能传start end、start 0或非数字。在 URI 解析阶段就用正则加校验拦住返回规范的 JSON-RPC 错误别让非法参数流到业务层if start 1: raise UriParseError(f起始行号必须为正整数: {start}) if start end: raise UriParseError(f起始行号 {start} 不能大于结束行号 {end})6.5 僵尸连接堆积SSE 没有断开通知机制客户端异常退出后服务端不知道。加一个后台定时任务每 60 秒扫描所有会话last_heartbeat超过 300 秒的直接清理。这个阈值别设太小网络抖动时容易误杀正常连接。6.6 客户端参数格式不统一有的客户端传params.name有的传params.tool参数有的在arguments有的在args。在 handler 入口做一层归一化别在每个工具函数里各写一套兼容逻辑否则维护起来很痛苦。7. 接入 AI 工具与后续方向服务跑通后把settings.json放进 AI 客户端的配置目录重启客户端在对话里问一句「列出你可用的工具」如果模型能报出search_document说明 MCP 接入成功。这时候你的本地文档就真正变成 AI 能调用的上下文了。如果你打算把这个服务长期挂在后台给团队用建议走 Coding Plan 通道按周期计费比单次调用省心适合高频 Agent 场景https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentmcp_fastapiutm_campaignrewrite后续我准备把传输层从 SSE 迁到 Streamable HTTP。SSE 的长连接对代理不友好而 Streamable HTTP 用短连接按需响应在POST /mcp上加个?streamtrue参数就能兼容新旧客户端实现复杂度还更低。另外通知机制和 Sampling 也在规划里。手搓 MCP 最有意思的地方在于你不是在调 API而是在理解一种协议为什么这么设计——JSON-RPC 为什么用 id、SSE 双通道为什么比纯 WebSocket 更适合 AI 场景、资源为什么用 URI 模板而不是枚举列表。这些问题的答案只有自己写一遍才会真正明白。

相关推荐

大模型接入的认证与计费:TaoToken 统一网关设计中的 settings.json 配置骨架
大模型接入的认证与计费:TaoToken 统一网关设计中的 settings.json 配置骨架

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/9/27 12:53:02

用 yo generator-code 脚手架新建 VSCode 插件:TaoToken 统一 Key 接入配置骨架
用 yo generator-code 脚手架新建 VSCode 插件:TaoToken 统一 Key 接入配置骨架

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/9/27 12:52:56

小白零代码搭建 OpenClaw 本地智能体:Windows/Mac 双平台部署与 TaoToken 配置实战
小白零代码搭建 OpenClaw 本地智能体:Windows/Mac 双平台部署与 TaoToken 配置实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/9/27 12:52:56

南方医科大学精品课程建设网站域名选型对比评测
南方医科大学精品课程建设网站域名选型对比评测

南方医科大学精品课程建设网站域名选型对比评测 域名服务器搞不懂,是卡在南方医科大学精品课程建设网站上线前的最大拦路虎。很多老师拿到学校任务,第一反应是找个建站公司,结果被各种技术名词绕晕:什么DNS、SSL、备案,听得一头雾水。今天不聊虚的… · 2026/9/28 0:01:01

建设网站北京市进阶技巧
建设网站北京市进阶技巧

北京建设网站选错技术栈,流量归零?3个对比评测帮你避坑 网站做好了没人访问,比没做还让人焦虑。很多北京本地的站长,明明代码写得漂亮,UI也在线,结果上线一个月,百度收录寥寥无几,自然流量几乎为零。这时候再回头找开发团队,对方只会甩锅说“内容… · 2026/9/28 0:01:01

济南做网站多少钱:3个案例拆解,防黑源码下载全攻略
济南做网站多少钱:3个案例拆解,防黑源码下载全攻略

济南做网站多少钱:3个案例拆解,防黑源码下载全攻略 上周济南一个做建材的老板找我,脸都绿了。他的官网首页弹出了赌博广告,后台被植入了挖矿脚本。他慌得问我:“网站被黑挂马不知道怎么办?能不能直接找之前的外包公司要源码下载,看看哪里被动了手脚?… · 2026/9/28 0:00:25

婚恋网站实战案例:避开3个高价坑,省钱50%还能跑赢流量
婚恋网站实战案例:避开3个高价坑,省钱50%还能跑赢流量

婚恋网站实战案例:避开3个高价坑,省钱50%还能跑赢流量 找婚恋网站建站公司,最怕的就是被坑高价。很多同行跟我吐槽,报价单上写得模棱两可,功能栏里全是“高级定制”、“专属UI”,结果落地全是套壳。今天不聊虚的,直接甩几个我经手的 实战案例… · 2026/9/28 0:00:19

制作网页比较方便的软件怎么选?一文搞懂避坑指南
制作网页比较方便的软件怎么选?一文搞懂避坑指南

制作网页比较方便的软件怎么选?一文搞懂避坑指南 很多老板一上来就问:做个网站多少钱?但我反问他:你的域名买了吗?服务器租了吗?他一脸懵。这就是典型的“域名服务器搞不懂”。别急,今天咱们不聊虚的,直接 一文搞懂 那些让你头秃的技术名词。… · 2026/9/28 0:00:06

Java IEC 62056-21 C模式主站协议库:从串口到TCP的能源数据采集实践
Java IEC 62056-21 C模式主站协议库:从串口到TCP的能源数据采集实践

简介:面向Java开发者的IEC 62056-21 C模式主站协议库实现,可用于通过串口或网络从燃气表、水表、热量表、电表等能源计量装置读取标准化数据,解决多设备数据采集与协议解析难题,适合能源管理、智能家居及远程监控系统的集成开发。… · 2026/9/27 23:59:42

MATLAB雷达信号脉冲压缩仿真:LFM线性调频、匹配滤波与距离分辨率实现
MATLAB雷达信号脉冲压缩仿真:LFM线性调频、匹配滤波与距离分辨率实现

简介:这套Matlab仿真工具完整呈现雷达信号脉冲压缩过程,从线性调频(LFM)信号生成、目标回波仿真到匹配滤波压缩处理均有可运行代码支撑,面向电子信息工程、计算机、数学等专业学生,适用于课程设计、期末大作… · 2026/9/27 0:00:01

汕头网站建设制作厂家避坑指南:5大注意事项救急
汕头网站建设制作厂家避坑指南:5大注意事项救急

汕头网站建设制作厂家避坑指南:5大注意事项救急 改个需求建站公司拖一周,这种憋屈事我见得太多了。 很多汕头老板找本地建站团队,签合同前看着方案挺美,一上线就变脸。 今天不聊虚的,直接拆解找 汕头网站建设制作厂家 时的5个核心 注意事项… · 2026/9/27 0:00:01

多模态虚假新闻检测实战:BERT+ResNet双塔与对比学习
多模态虚假新闻检测实战:BERT+ResNet双塔与对比学习

简介:基于PyTorch的多模态虚假新闻检测项目完整代码包,面向自然语言处理与计算机视觉交叉方向的开发者、科研人员及毕业设计选题者,解决社交媒体中文本与图像联合识别虚假新闻的问题。系统以BERT预训练模型提取文本语义特征,以Res… · 2026/9/27 0:00:01

制作网页比较方便的软件怎么选?一文搞懂避坑指南
制作网页比较方便的软件怎么选?一文搞懂避坑指南

制作网页比较方便的软件怎么选?一文搞懂避坑指南 很多老板一上来就问:做个网站多少钱?但我反问他:你的域名买了吗?服务器租了吗?他一脸懵。这就是典型的“域名服务器搞不懂”。别急,今天咱们不聊虚的,直接 一文搞懂 那些让你头秃的技术名词。… · 2026/9/28 0:00:06

婚恋网站实战案例:避开3个高价坑,省钱50%还能跑赢流量
婚恋网站实战案例:避开3个高价坑,省钱50%还能跑赢流量

婚恋网站实战案例:避开3个高价坑,省钱50%还能跑赢流量 找婚恋网站建站公司,最怕的就是被坑高价。很多同行跟我吐槽,报价单上写得模棱两可,功能栏里全是“高级定制”、“专属UI”,结果落地全是套壳。今天不聊虚的,直接甩几个我经手的 实战案例… · 2026/9/28 0:00:19

济南做网站多少钱:3个案例拆解,防黑源码下载全攻略
济南做网站多少钱:3个案例拆解,防黑源码下载全攻略

济南做网站多少钱:3个案例拆解,防黑源码下载全攻略 上周济南一个做建材的老板找我,脸都绿了。他的官网首页弹出了赌博广告,后台被植入了挖矿脚本。他慌得问我:“网站被黑挂马不知道怎么办?能不能直接找之前的外包公司要源码下载,看看哪里被动了手脚?… · 2026/9/28 0:00:25

了解更多?预约专属演示

我们的顾问将为您一对一讲解产品与方案

企业微信二维码