14:00 痛点先摆在台面上
Claude 只能读写文件,连个数据库都查不了,这怎么当生产力工具?
我下午被这事儿卡了快二十分钟。想让 Claude 帮我跑一段用户留存分析,数据在远端,结果它一句"我没有数据库访问能力"把我噎回来。Bash 倒是能跑 psql,可那不叫 Agent,那叫让我自己写脚本再让它执行。
翻了翻《Claude Code 实战:Harness 工程之道》第 6 章,作者把这事说得很透:Claude 本身只有 Read/Bash/Edit 这类基础工具,外部系统全靠 MCP(Model Context Protocol)扩展。MCP 是个客户端-服务器协议——Claude 当客户端,你写的或者别人写好的服务当服务器,中间走 stdio 或 HTTP。书里管这叫把 M×N 问题压成 M+N:M 个模型接 N 个工具,原本要 M×N 个适配,现在大家遵守同一协议,模型方写 M 个客户端、工具方写 N 个服务器就完事。
定位我理解了——MCP 扩展的就是 Agentic Loop 里"工具执行"那一格的工具池。说白了,给 Claude 加挂件。
14:20 配置先跑通
MCP 配置两条路,都简单到离谱。
第一条是 CLI:
# 加一个本地 stdio 服务器claude mcpaddfilesystem npx-y@modelcontextprotocol/server-filesystem /workspace# 加一个远程 HTTP 服务器,带认证头claude mcpadd--transporthttp--header"Authorization: Bearer${TOKEN}"\company-jira https://jira.company.com/mcp# 加到用户级别,所有项目都能用claude mcpadd--scopeuser github -- npx-y@modelcontextprotocol/server-github# 管理claude mcp list# 看哪些挂上了claude mcptestgithub# 测连接claude mcp remove github# 删掉第二条是项目根放一个.mcp.json,团队共享配置走这个:
{"mcpServers":{"filesystem":{"command":"npx","args":["-y","@modelcontextprotocol/server-filesystem","."]},"fetch":{"command":"npx","args":["-y","@modelcontextprotocol/server-fetch"]},"github":{"type":"http","url":"https://api.githubcopilot.com/mcp/","headers":{"Authorization":"Bearer ${GITHUB_TOKEN}"}},"database":{"command":"npx","args":["-y","@bytebase/dbhub","--dsn","${DATABASE_URL}"]}}}我一开始选了 CLI,图快。后来换台机器又得敲一遍,老老实实迁回.mcp.json。这是个真实教训:临时验证用 CLI,团队/长期用配置文件。环境变量那种${VAR:-default}兜底写法是书里抄的,挺好用。
14:40 实战一:内置 MCP 挂数据库
先上最痛的一刀——数据库。官方有现成的@modelcontextprotocol/server-postgres,npm 直接跑:
{"mcpServers":{"postgres":{"command":"npx","args":["-y","@modelcontextprotocol/server-postgres"],"env":{"DATABASE_URL":"${DATABASE_URL:-postgresql://localhost:5432/mydb}"}}}}存到.mcp.json,claude mcp list看一眼状态是 connected,问它"这张表有哪些字段"——它真去查了。这一下午就值了。
中途踩了个坑:DATABASE_URL我开始写死成 localhost,结果 Claude 跑去连我本地的空库,问啥都回"表不存在"。改成${DATABASE_URL:-...}兜底之后才顺。
15:10 实战二:自定义 MCP 服务器
内置的能用,但我想要的不止这些。我想给 Claude 一个 todo 工具,让它自己记任务。这得自己写 MCP 服务器。
书里给了 Python 和 TS 两个版本。我先抄的 Python,因为快。
Python 版(mcp-servers/todo-server.py,配套requirements.txt写mcp>=1.0.0,pip install -r requirements.txt):
frommcp.serverimportServerfrommcp.server.stdioimportstdio_server server=Server("todo-server")todos=[]defgenerate_id():returnstr(len(todos)+1)@server.tool("todo_add")asyncdefadd_todo(text:str)->str:"""Add a new todo item"""todo_id=generate_id()todos.append({"id":todo_id,"text":text,"done":False})returnf"Added:{todo_id}-{text}"@server.tool("todo_list")asyncdeflist_todos()->str:"""List all todo items"""ifnottodos:return"No todos found."return"\n".join(f"[{'x'ift['done']else' '}]{t['id']}:{t['text']}"fortintodos)asyncdefmain():asyncwithstdio_server()as(read_stream,write_stream):awaitserver.run(read_stream,write_stream)if__name__=="__main__":importasyncio asyncio.run(main())跑起来一报错——generate_id没定义。书里这段省了 ID 生成函数,我自己补了一个generate_id()返回len(todos)+1,能用就行。这是抄书必经环节,别指望开箱即用。
后来我想顺便试 TS 版,因为 TS 版多了个 resource(资源)概念,能暴露stats://current这种 URI 给 Claude 读统计。
TS 版(mcp-servers/todo-server.ts,配套package.json装@modelcontextprotocol/sdk和zod,记得写"type": "module"):
import{McpServer}from"@modelcontextprotocol/sdk/server/mcp.js";import{StdioServerTransport}from"@modelcontextprotocol/sdk/server/stdio.js";import{z}from"zod";// 内存存储consttodos:{id:string;text:string;done:boolean}[]=[];// 创建 MCP 服务器constserver=newMcpServer({name:"todo-server",version:"1.0.0",});// 工具:添加待办server.tool("todo_add","Add a new todo item",// Claude 用这个判断何时调用{text:z.string().describe("The todo text")},// 输入参数 schemaasync({text})=>{consttodo={id:Math.random().toString(36).substring(2,9),text,done:false,};todos.push(todo);return{content:[{type:"text",text:`Added:${todo.id}-${todo.text}`}],};});// 工具:列出待办server.tool("todo_list","List all todo items",{},async()=>{consttext=todos.length===0?"No todos found.":todos.map((t)=>`[${t.done?"x":" "}]${t.id}:${t.text}`).join("\n");return{content:[{type:"text",text}]};});// 资源:统计信息server.resource("stats","Server statistics",async()=>{return{contents:[{uri:"stats://current",mimeType:"application/json",text:JSON.stringify({total:todos.length,completed:todos.filter((t)=>t.done).length,pending:todos.filter((t)=>!t.done).length,},null,2),}],};});// 启动asyncfunctionmain(){consttransport=newStdioServerTransport();awaitserver.connect(transport);console.error("MCP Server started");// 日志走 stderr,别污染 stdout}main().catch(console.error);这里有个细节我踩了一脚:console.error不是写错。MCP 走 stdio 通信,stdout 是协议通道,你 print 一条调试日志进去 Claude 就懵了。日志必须吐到 stderr。
挂上之后,Claude 调用工具的命名规则是mcp__todo-server__todo_add——双下划线分段。这名字可以直接写进 Skills 的allowed-tools,让 Skill(菜谱)指挥 MCP 工具(食材)干活,书里管这叫"厨房与菜谱"。
15:50 调试三板斧
跑不通的时候三招,从轻到重:
claude mcp list# 看哪些挂上了、状态对不对claude mcptesttodo-server# 单测某一个服务器claude --mcp-debug# 全局开 debug,看协议层报文--mcp-debug是压箱底那招,输出会非常啰嗦,但能看到 Claude 到底发什么、服务器回什么。我 TS 版第一次跑不通就是靠它看出package.json里漏写"type": "module",import 路径全炸。
16:20 收尾
一下午跑下来,MCP 这套东西的体感是:配置简单、协议干净、坑都集中在「抄书少抄了一行」「stdout/stderr 搞混」这种小事上。M+N 的设计是真省事——我今天写的 todo-server,明天接个 Cursor、后天接别的客户端,服务器代码一行不用改。
顺便一提,雷达鸭后台数据查询我后来就是用这套自定义 MCP 服务器接 UniCloud 的 HTTP 接口写的,Claude 直接问留存、问日活,比我自己开控制台点快多了。
那问题来了——既然 MCP 把 Claude 的手脚放开了,数据库能连、API 能调、自定义逻辑能跑,那它和你自己写个 CLI 脚本让 Claude 用 Bash 调,到底差在哪?仅仅是多了层协议包装吗?
标签
MCP、Claude Code、Model Context Protocol、AI Agent、数据库集成
个人介绍
雷达鸭 App 独立开发者,10+ 年软件开发经验,软件设计师、人工智能应用工程师,专注鸿蒙 ArkTS + Web 前端,正在探索 AI 自动化。雷达鸭在华为应用市场和微信小程序都能搜到,收录中国一人公司赚钱案例。
声明
本文基于《Claude Code 实战:Harness 工程之道》(黄佳 著)第 6 章内容整理与实战记录,代码示例与配置参考原书。遵循 MIT 协议。