MCP 工具开发入门:给 Claude 装上自定义技能
上周有个需求让 Claude 能直接查我们内部的工单系统。以前的做法是把工单内容复制粘贴给 Claude效率很低。研究了一下 MCP发现这个问题用 MCP 解决特别优雅写一个 server 就搞定了。这篇从头讲怎么写一个 MCP server然后接进 Claude Desktop。MCP 是什么MCPModel Context Protocol是 Anthropic 搞的一个开放协议让大模型能够调用外部工具和访问外部数据。简单说就是你写一个 MCP server声明好有哪些工具Claude 就能在对话中调用这些工具。跟 OpenAI 的 function calling 思路类似但 MCP 是协议层面的标准不绑定特定的模型而且 server 和 client 是独立进程通过标准 I/O 通信。开发环境pipinstallmcp anthropicMCP SDK 会帮你处理协议细节你只需要关心业务逻辑。写一个最简单的 MCP server从一个查天气的例子开始#!/usr/bin/env python3 简单的天气查询 MCP server importasyncioimportjsonfrommcp.serverimportServerfrommcp.server.modelsimportInitializationOptionsfrommcp.server.stdioimportstdio_serverfrommcpimporttypesimporthttpx# 创建 server 实例appServer(weather-server)app.list_tools()asyncdefhandle_list_tools()-list[types.Tool]: 声明这个 server 提供哪些工具 Claude 会读取这个列表知道有哪些工具可以调用 return[types.Tool(nameget_weather,description获取指定城市的天气信息,inputSchema{type:object,properties:{city:{type:string,description:城市名称例如北京、上海}},required:[city]})]app.call_tool()asyncdefhandle_call_tool(name:str,arguments:dict)-list[types.TextContent]: 工具调用的实际执行逻辑 ifnameget_weather:cityarguments.get(city,)# 这里调用真实的天气 API演示用随便写的# 实际替换成你用的天气服务asyncwithhttpx.AsyncClient()asclient:try:# 用 wttr.in 这个免费天气服务responseawaitclient.get(fhttps://wttr.in/{city}?formatj1,timeout10.0)dataresponse.json()currentdata[current_condition][0]temp_ccurrent[temp_C]weather_desccurrent[weatherDesc][0][value]humiditycurrent[humidity]resultf{city}天气{weather_desc}温度{temp_c}°C湿度{humidity}%exceptExceptionase:resultf获取天气失败{str(e)}return[types.TextContent(typetext,textresult)]raiseValueError(f未知工具{name})asyncdefmain():asyncwithstdio_server()as(read_stream,write_stream):awaitapp.run(read_stream,write_stream,InitializationOptions(server_nameweather-server,server_version1.0.0,capabilitiesapp.get_capabilities(notification_optionsNone,experimental_capabilities{})))if__name____main__:asyncio.run(main())更实用的例子查工单系统来个有实际价值的——查公司内部工单系统#!/usr/bin/env python3 工单系统 MCP server importasyncioimportjsonfromdatetimeimportdatetimefrommcp.serverimportServerfrommcp.server.modelsimportInitializationOptionsfrommcp.server.stdioimportstdio_serverfrommcpimporttypes appServer(ticket-server)# 模拟工单数据库实际替换成真实 API 调用MOCK_TICKETS{TICKET-001:{id:TICKET-001,title:登录页面 500 错误,status:处理中,priority:高,assignee:张三,created_at:2026-03-10 09:00,description:用户反馈登录时偶发 500 错误发生频率约 2%},TICKET-002:{id:TICKET-002,title:导出功能优化,status:待处理,priority:中,assignee:李四,created_at:2026-03-11 14:00,description:大数据量导出时速度很慢超过 10 万条会超时}}app.list_tools()asyncdefhandle_list_tools()-list[types.Tool]:return[types.Tool(nameget_ticket,description根据工单 ID 查询工单详情,inputSchema{type:object,properties:{ticket_id:{type:string,description:工单 ID格式如 TICKET-001}},required:[ticket_id]}),types.Tool(namelist_tickets,description列出工单支持按状态和优先级过滤,inputSchema{type:object,properties:{status:{type:string,description:工单状态待处理、处理中、已完成,enum:[待处理,处理中,已完成]},priority:{type:string,description:优先级高、中、低,enum:[高,中,低]}}}),types.Tool(nameupdate_ticket_status,description更新工单状态,inputSchema{type:object,properties:{ticket_id:{type:string,description:工单 ID},new_status:{type:string,description:新状态,enum:[待处理,处理中,已完成]}},required:[ticket_id,new_status]})]app.call_tool()asyncdefhandle_call_tool(name:str,arguments:dict)-list[types.TextContent]:ifnameget_ticket:ticket_idarguments[ticket_id]ticketMOCK_TICKETS.get(ticket_id)ifnotticket:return[types.TextContent(typetext,textf工单{ticket_id}不存在)]resultjson.dumps(ticket,ensure_asciiFalse,indent2)return[types.TextContent(typetext,textresult)]elifnamelist_tickets:status_filterarguments.get(status)priority_filterarguments.get(priority)ticketslist(MOCK_TICKETS.values())ifstatus_filter:tickets[tfortinticketsift[status]status_filter]ifpriority_filter:tickets[tfortinticketsift[priority]priority_filter]ifnottickets:return[types.TextContent(typetext,text没有找到符合条件的工单)]summary[]fortintickets:summary.append(f[{t[id]}]{t[title]}-{t[status]}({t[priority]}优先级) - 负责人{t[assignee]})return[types.TextContent(typetext,text\n.join(summary))]elifnameupdate_ticket_status:ticket_idarguments[ticket_id]new_statusarguments[new_status]ifticket_idnotinMOCK_TICKETS:return[types.TextContent(typetext,textf工单{ticket_id}不存在)]old_statusMOCK_TICKETS[ticket_id][status]MOCK_TICKETS[ticket_id][status]new_statusreturn[types.TextContent(typetext,textf工单{ticket_id}状态已更新{old_status}→{new_status})]raiseValueError(f未知工具{name})asyncdefmain():asyncwithstdio_server()as(read_stream,write_stream):awaitapp.run(read_stream,write_stream,InitializationOptions(server_nameticket-server,server_version1.0.0,capabilitiesapp.get_capabilities(notification_optionsNone,experimental_capabilities{})))if__name____main__:asyncio.run(main())接入 Claude Desktop把 server 写好之后需要在 Claude Desktop 的配置文件里注册。macOS 的配置文件位置~/Library/Application Support/Claude/claude_desktop_config.json{mcpServers:{ticket-server:{command:python,args:[/path/to/your/ticket_server.py],env:{TICKET_API_KEY:your_api_key_here}}}}重启 Claude Desktop在对话界面右下角会出现工具图标点开可以看到已注册的工具列表。然后直接跟 Claude 说「帮我查一下 TICKET-001 的状态」它就会自动调用对应工具。接入 CursorCursor 也支持 MCP配置方式类似。在.cursor/mcp.json里{mcpServers:{ticket-server:{command:python,args:[/path/to/ticket_server.py]}}}在 server 里调用大模型 API如果你的 MCP 工具本身也需要调用大模型比如做一些 AI 处理直接用 OpenAI 兼容的 SDK 就行fromopenaiimportAsyncOpenAI ai_clientAsyncOpenAI(api_keyos.environ.get(API_KEY),base_urlhttps://api.ofox.ai/v1)# 在工具实现里调用asyncdefsummarize_ticket(ticket_content:str)-str:responseawaitai_client.chat.completions.create(modelgpt-4o-mini,messages[{role:user,content:f用一句话总结这个工单的核心问题\n{ticket_content}}])returnresponse.choices[0].message.content坑点记录坑一server 启动失败不报错。Claude Desktop 加载 server 失败时不会弹提示你就是看不到工具列表。排查方法手动在命令行运行 python server.py看有没有报错通常是依赖没装或者路径写错了。坑二工具描述要写清楚。Claude 是靠读 description 来判断什么时候调用哪个工具的。如果描述写得含糊它可能压根不会想到调用或者调错工具。描述越精确调用越准确。坑三参数类型要匹配。inputSchema 里定义的类型和 Claude 实际传进来的类型可能不一致做好 type conversion。坑四异步 vs 同步。MCP SDK 是异步的如果你的工具里有同步 I/O 操作比如读文件包一层asyncio.to_thread避免阻塞。MCP 的生态现在还在快速发展社区里已经有很多现成的 server 可以直接用比如文件系统、GitHub、数据库等。自己写 server 主要是对接自家内部系统的时候用得上。
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2416357.html
如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!