1. 为什么我要手写一个 MCP Server 接进 Claude Code
MCP 协议(Model Context Protocol)是 Anthropic 提出的开放标准,用一句话说清楚:它让大模型像浏览器访问网页一样,用统一协议安全地访问外部工具和数据源。你写一次 MCP Server,所有支持 MCP 的客户端都能直接调用,不用再为每个框架重写一遍 Function Calling。Claude Code 是目前对 MCP 支持最完整的命令行 AI 编程助手,适合想把自己的脚本、数据库、内部 API 变成大模型可调用工具的开发者。
我试过把公司内部的日志查询脚本接进 Claude Code,一开始踩了不少坑:stdio 模式下 print 调试直接把协议通道污染了,Windows 下中文编码乱码,配置文件里 command 写了相对路径换个目录就找不到。这些问题教程里大多一笔带过,但对新手来说每一个都能卡半天。这篇就把从零手写 MCP Server 到接入 Claude Code 的全流程拆开讲,stdio 和 SSE 两种传输模式都覆盖,配置文件骨架、启动命令、鉴权通道配置片段全部给可复制的版本,最后附一份常见报错排查清单。
适合谁看:已经会用 Claude Code 或 Cursor,想把自己的工具接进去的开发者;被 Function Calling 各家格式不统一折磨过的人;想搞懂 MCP 协议到底怎么跑起来、不想只看概念科普的人。读完你能得到一个能跑通的 Note Server,以及一套可复用的配置和排障方法。
2. TaoToken 前置:统一 Key 与 API 通道准备
在动手写 Server 之前,先把模型调用通道理顺。Claude Code 本身需要模型 API 才能工作,如果你同时还在用其他支持 MCP 的客户端,每个都配一遍 Key 和地址会很乱。TaoToken 提供统一 Key 和 API 通道,把模型调用收敛到一个入口,后面 Claude Code 的配置里只需要填一次。
官网地址:https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content=
API 地址:https://taotoken.net/api
操作路径很直接:注册后在控制台创建 API Key,然后在 Claude Code 的环境变量或配置里指向这个 API 地址。具体入口:
- 模型对话体验:https://taotoken.net/models?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite
- Coding Plan(长期编码/Agent 场景):https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite
- 控制台:https://taotoken.net/console?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite
- API Keys 管理:https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite
- 接入文档:https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite
注意:MCP Server 本身不负责模型调用,它只负责暴露工具。模型调用通道是 Claude Code 这一侧的事。把这两层分清楚,后面排查问题时就不会混。
环境变量配置示例(Windows Git Bash / Linux / Mac 通用):
export ANTHROPIC_BASE_URL="https://taotoken.net/api" export ANTHROPIC_API_KEY="你的TaoToken Key"Windows PowerShell 用:
$env:ANTHROPIC_BASE_URL="https://taotoken.net/api" $env:ANTHROPIC_API_KEY="你的TaoToken Key"配好后先验证模型通道能通,再往下写 Server。这一步不通,后面 MCP 接得再好也没用。
3. 可复制配置:手写 stdio 版 MCP Server 并接入 Claude Code
3.1 环境准备与依赖安装
建议 Python 3.10+,建独立虚拟环境。不建虚拟环境的话,后面配置文件里 command 要写 Python 绝对路径,全局环境一升级就崩。
mkdir my-mcp-server && cd my-mcp-server python -m venv .venv # Windows Git Bash: source .venv/Scripts/activate # Linux/Mac: source .venv/bin/activate pip install mcpClaude Code 安装:
npm install -g @anthropic-ai/claude-code claude --version3.2 写一个 Note Server(stdio 模式)
需求:让大模型能增、查、搜本地笔记。新建note_server.py:
import json import os import sys import logging from mcp.server.fastmcp import FastMCP # 关键:日志输出到 stderr,绝不污染 stdout(stdio 协议通道) logging.basicConfig( level=logging.DEBUG, stream=sys.stderr, format="%(asctime)s [%(levelname)s] %(message)s" ) NOTE_FILE = "notes.json" def _load_notes(): if not os.path.exists(NOTE_FILE): return [] with open(NOTE_FILE, "r", encoding="utf-8") as f: return json.load(f) def _save_notes(notes): with open(NOTE_FILE, "w", encoding="utf-8") as f: json.dump(notes, f, ensure_ascii=False, indent=2) mcp = FastMCP("note-server") @mcp.tool() def list_notes() -> str: """列出所有笔记的标题和编号。无笔记时返回空列表提示。""" notes = _load_notes() if not notes: return "当前没有任何笔记。" return json.dumps( [{"id": i, "title": n["title"]} for i, n in enumerate(notes)], ensure_ascii=False ) @mcp.tool() def add_note(title: str, content: str) -> str: """添加一条新笔记。 Args: title: 笔记标题,简短概括 content: 笔记正文内容 """ notes = _load_notes() notes.append({"title": title, "content": content}) _save_notes(notes) return f"已添加笔记《{title}》,当前共 {len(notes)} 条。" @mcp.tool() def search_notes(keyword: str) -> str: """按关键词搜索笔记标题和正文,返回所有匹配的笔记。 当用户想查找"包含某内容的笔记""有没有关于XX的记录"时使用。 Args: keyword: 要搜索的关键词,单个词或短语 """ notes = _load_notes() results = [ {"id": i, "title": n["title"], "content": n["content"]} for i, n in enumerate(notes) if keyword in n["title"] or keyword in n["content"] ] if not results: return f"没有找到包含「{keyword}」的笔记。" return json.dumps(results, ensure_ascii=False) if __name__ == "__main__": mcp.run(transport="stdio")几个决定工具能不能被模型正确调用的细节:函数名语义化(list_notes比get_data好);docstring 写清做什么、参数含义、返回什么;参数加类型注解;返回值统一用字符串,复杂结构json.dumps。
本地验证语法:
python note_server.py # 无报错、阻塞等待输入,说明 stdio 服务就绪,Ctrl+C 退出3.3 Claude Code 配置文件骨架
在项目根目录创建.mcp.json:
{ "mcpServers": { "note-server": { "command": "C:/项目路径/my-mcp-server/.venv/Scripts/python.exe", "args": ["C:/项目路径/my-mcp-server/note_server.py"] } } }注意:Windows 路径用正斜杠
/最省心,JSON 里反斜杠要转义成\\。command 和 args 一律写绝对路径,Host 启动子进程时工作目录不固定,相对路径会解析失败。
命令行临时添加(等价写法):
claude mcp add note-server -- C:/项目路径/my-mcp-server/.venv/Scripts/python.exe C:/项目路径/my-mcp-server/note_server.py3.4 升级到 SSE 远程传输
stdio 每台机器都要装一份,团队共享不方便。改成 SSE 只需改一行:
if __name__ == "__main__": mcp.run(transport="sse", port=8765)客户端配置改为 URL 形式:
{ "mcpServers": { "note-server-remote": { "url": "http://your-server-ip:8765/sse" } } }命令行:
claude mcp add note-server-remote --transport sse http://localhost:8765/sse提示:MCP 规范后续引入了 Streamable HTTP 传输,对无状态部署更友好。SDK 版本较新可尝试
transport="streamable-http",过渡期 SSE 仍广泛兼容。
4. 验证请求与成功结果
4.1 用 MCP Inspector 先测 Server
改完代码先在 Inspector 里测,把"Server 的 bug"和"模型没调用对"区分开:
npx @modelcontextprotocol/inspector python note_server.py浏览器打开http://localhost:5173,左侧列出所有 Tools,点一个填参数点 Run 直接调用,底部显示完整 JSON-RPC 请求/响应。
4.2 在 Claude Code 里验证
项目目录下启动:
claude输入/mcp,看到note-server: connected且列出三个工具,说明接入成功。然后用自然语言测试:
帮我加一条笔记,标题"MCP学习计划",内容"本周跑通stdio版本,下周升级SSE" 我现在有哪些笔记? 搜一下有没有关于"SSE"的笔记模型自主调用对应工具并返回正确结果,就说明整条链路通了。
4.3 SSE 连通性验证
curl -N http://localhost:8765/sse能看到流式事件返回即正常。远程连不上时先确认监听地址是0.0.0.0而非127.0.0.1,再查防火墙端口。
5. 本篇常见错排查清单
| # | 坑点 | 现象 | 根因 | 解决方案 |
|---|---|---|---|---|
| 1 | stdio 下用 print 调试 | 连接后卡死、协议解析错误 | print 内容被当成协议消息污染通道 | 用logging输出到 stderr |
| 2 | Windows 中文编码乱码 | 中文变 ??? 或 UnicodeDecodeError | 默认 GBK 与 utf-8 不一致 | 文件操作显式encoding="utf-8" |
| 3 | command 写相对路径 | 换目录启动报 command not found | 子进程工作目录不固定 | command 和 args 写绝对路径 |
| 4 | docstring 太简略 | 模型不调用或乱传参 | 模型靠 docstring 理解语义 | 写清做什么、参数、返回 |
| 5 | 参数没类型注解 | 数字传成字符串、顺序乱 | JSON Schema 缺类型信息 | 每个参数加类型注解 |
| 6 | SSE 部署后连不上 | 本机能连远程连不上 | 监听 127.0.0.1 或防火墙 | 监听 0.0.0.0、开端口、配 CORS |
| 7 | 危险操作没护栏 | 模型删了不该删的数据 | 工具权限过大 | 加二次确认、只读隔离、操作日志 |
坑 1 的正确调试姿势:
import sys import logging logging.basicConfig( level=logging.DEBUG, stream=sys.stderr, format="%(asctime)s [%(levelname)s] %(message)s" ) @mcp.tool() def add_note(title: str, content: str) -> str: logging.debug(f"调用 add_note: title={title}") # ... 业务逻辑 ... return "ok"坑 4 的 docstring 对比:
# 模型大概率不调用,或乱传参 @mcp.tool() def search(keyword): """搜索""" ... # 模型能准确判断何时调用、怎么传参 @mcp.tool() def search_notes(keyword: str) -> str: """按关键词搜索笔记标题和正文,返回所有匹配的笔记。 当用户想查找"包含某内容的笔记""有没有关于XX的记录"时使用。 Args: keyword: 要搜索的关键词,单个词或短语 """ ...6. 下一步:把通道和工具都收敛好
Server 跑通后,接下来两件事值得做。一是把模型调用通道统一到 TaoToken,Claude Code 和其他 MCP 客户端共用一套 Key 和 API 地址,配置不再散落各处。API Keys 管理入口:https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite ,接入文档:https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite 。如果你长期用 Claude Code 做编码或 Agent 任务,Coding Plan 会更划算:https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite 。想先验证模型通道是否正常,可以直接在模型对话页试:https://taotoken.net/models?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite 。
二是把你自己工作里的真实脚本改造成 MCP Server。查日志、跑 SQL、生成报表,任何一个重复劳动都值得包一层工具。改的时候记住三条:docstring 当接口文档写、参数加类型注解、危险操作加护栏。这三条做到,工具被模型正确调用的概率会高很多。