周三凌晨一点半,我盯着 Claude Desktop 的日志文件看了快四十分钟。
原因很蠢。我写了个 MCP server,命令行里 python server.py 敲下去风平浪静,但 Claude 那边就是死活刷不出工具。最后发现是 print() 的锅——我为了调试加了一行打印参数,那行输出混进了 stdout,把 JSON-RPC 的消息流冲烂了。MCP 走 stdio 传输的时候,stdout 是留给协议本身的,你所有的日志只能走 stderr。这条规则官方文档里写了,但我第一次读的时候直接跳过去了,因为它藏在“架构概览”那一大段后面,看着像废话。所以这篇不是教程,是我自己摸索的过程和结论,其中几个坑我觉得比怎么写代码更值得说。
先交代背景,免得你觉得这文章太空。我本职是做数据的,Python 能写但算不上工程师,最大的需求就是让 Claude 能直接查我本地几个 SQLite 文件:一份记了三年的记账表、读书笔记、还有个游戏销量的库。以前的做法是导成 CSV 再拖进对话框,超过两万行就卡,而且每次对话都要重来一遍。MCP 出来后我第一反应是“这不就是我想要的”,第二反应是“这玩意儿到底值不值得学”。现在两个星期过去,我本地跑着三个自写的 server、总共 11 个工具,可以给个阶段性的答案:值得,但多半不是你想的那个理由。
先把四种做法摆一起比
我把能想到的路子都试了一遍,下面这张表里的时间是我自己的实测,不保证对你有参考价值:
| 做法 | 从零到能用 | 上下文开销 | 换客户端 | 出错好查吗 |
|---|---|---|---|---|
| 贴 CSV 进对话 | 0 分钟 | 两万行约 60 万字符,直接爆 | —— | 不用查 |
| 自己写 function calling | 半天 | 工具定义照样要占位 | 换个客户端全部重写 | 好查,都在自己代码里 |
| 给它一个 bash 工具 | 10 分钟 | 极小,就一个工具定义 | 好迁移 | 差,SQL 写错了你根本看不出来 |
| 写 MCP server | 2 到 4 小时 | 中等,定义每次请求都带 | 好,Claude、Cursor 这些基本都认 | 中等,日志在客户端的目录里 |
第三行那个“bash 工具”我用了差不多半年,一开始觉得很爽——给模型一个 shell,理论上它能干任何事。但问题很快暴露:它查出来的数字你没法验证。有一次它把我记账表里的“餐饮”和“外卖”当成两个分类分别求和,我看了三遍才发现口径不对。这不是模型的错,是我没有给它任何约束。MCP 的价值有一大半在这里:工具的边界是你定义的,参数是模型填的,返回是确定性的,出问题你能定位到具体哪个工具。
最小可跑的东西长什么样
我用的 Python SDK,一个只读的记账查询工具,代码大概三十行:
from mcp.server.fastmcp import FastMCP
import sqlite3
mcp = FastMCP("ledger")

@mcp.tool()
def query(sql: str) -> str:
"""只读查询本地记账库。表名 transactions,字段 date/amount/category/note。仅支持 SELECT。"""
con = sqlite3.connect("/Users/chen/ledger.db")
try:
rows = con.execute(sql).fetchall()[:200]
return "\n".join(str(r) for r in rows)
finally:
con.close()
if __name__ == "__main__":
mcp.run(transport="stdio")
然后是在 Claude Desktop 的配置里注册。macOS 上的路径是 ~/Library/Application Support/Claude/claude_desktop_config.json,Windows 是 %APPDATA%\Claude\claude_desktop_config.json,内容大概这样:
{
"mcpServers": {
"ledger": {
"command": "/Users/chen/.venv/bin/python",
"args": ["/Users/chen/mcp/ledger/server.py"]
}
}
}
看着简单,但这里有两个我卡了很久的点。第一,command 和 args 里的路径必须是绝对路径,~ 不会展开,写相对路径也不行,因为客户端的工作目录不是你想的那个。第二,command 千万别写 python 或者 python3,要写你 venv 里那个解释器的绝对路径,否则客户端拉起来的是系统 Python,里面没装 mcp 这个包,表现就是进程秒退、界面毫无提示。
七个坑,按我踩到的顺序排
-
stdout 污染。上面说过了,任何
print()、任何没配置好的 logging 默认 handler,只要往 stdout 写东西就会破坏协议。我的做法是代码里所有调试输出统一走sys.stderr.write,或者干脆用logging.basicConfig(stream=sys.stderr)。 -
日志文件在哪。macOS 下 Claude Desktop 的 MCP 日志在
~/Library/Logs/Claude/目录,文件名是mcp-server-<你的服务名>.log。这个路径我是翻了好久才找到的,官方文档里没写得很显眼。找不到日志,你就是在盲调。 -
Node 版本。如果你用的是官方那些用 npx 起的 server,本机 Node 要 18 以上。我第一次跑的时候 Node 是 16,报错信息含糊得要命。另外首次启动 npx 要联网下载包,慢的时候客户端那边直接超时,我印象里启动等待差不多是 60 秒这个量级,超了就判定失败——这条我没去翻源码确认,反正别赌。
-
工具描述是每次请求都在交的税。我一开始四个工具,描述写得很啰嗦,加起来大概 3800 个 token。后来压到七个工具、总描述 900 多 token 左右(统计方法很土:在 server 里把
tools/list的返回打日志,字符数除以 3.5 估),长对话跑偏的情况明显少了。这个开销不会因为你不用工具就省掉,它是常驻的。
-
别把写权限交给模型。我现在读和写是两个 server,写那个平时关着,要改数据的时候手动在配置里打开、重启。听着很麻烦,但你想想
DELETE FROM transactions WHERE date < '2024-01-01'被执行是什么感觉。 -
改完代码必须重启客户端。它是进程启动的时候把 server 拉起来的,没有热更新这回事。我有半小时一直在改代码然后疑惑为什么没生效。
-
Windows 路径的反斜杠。JSON 里要写双反斜杠,这个属于基本功失误,但人在凌晨一点半的时候什么都会忘。
我的观点:MCP 解决的是分发,不是能力
这一点我想说得直白些。你的模型本来就能通过 function calling 调 SQLite,MCP 并没有给它新能力。它做的事是把“怎么调”从应用代码里剥出来,变成一个独立进程加一份标准协议。好处是别人能复用、你能跨客户端复用、换工具的时候不用重写。
所以判断标准就变得很粗暴:你会不会换客户端?有没有第二个人要用?两个答案都是否,那就老老实实写 function calling,或者干脆做个命令行脚本让模型去调,两小时能省下来。我见过不少人一上来就折腾 MCP,结果服务只有自己用、客户端也只用 Claude Desktop,最后收益就是个仪式感。
另一个观点:MCP server 应该做得薄。我见过有人把重试、限流、缓存、业务规则全塞进 server 里,把它写成了一个小型后端。这些逻辑放外面去,server 只干一件事——把模型的自然语言参数翻译成一个边界明确的确定性调用。反过来说,千万别写那种 do_anything(action, params) 的万能工具,模型在这种工具上的表现是最差的,因为你把参数校验的活全丢回给它了。宁可多写几个窄工具。
现在的状态
三个 server,11 个工具。跑了两周,没出过数据损坏,查询准确率比我以前贴 CSV 高得多,因为口径被工具定义锁死了。代价是 Claude Desktop 的启动时间从大概 2 秒变成了 7 秒左右,每次开机都能感觉到。
值不值,取决于你是不是每天都在干同一件事。如果你一周就用一次这个场景,真的别折腾,把表导成 500 行的样本丢进对话里,效果差不了多少,还省下一晚上。我那一晚上本来是想九点睡的。