从零构建MCP工具:基于mcp-run实现AI可调用的天气查询脚本
1. 项目概述从“AI调用工具”到“亲手造轮子”的转变最近在折腾AI Agent和各类大模型应用时我频繁地接触到“MCP”这个词。无论是Cursor里集成的各种MCP Server还是Claude Desktop中琳琅满目的工具市场MCPModel Context Protocol协议似乎正在成为连接大模型与外部工具、数据源的事实标准。但用着用着我发现了一个痛点市面上的MCP工具虽然多但要么功能太“重”要么不完全符合我某个特定的、细碎的工作流需求。比如我就想快速查一下某个API的实时状态或者把一段文本按照我自定义的规则格式化一下为了这点事去配置一个完整的、复杂的MCP Server感觉有点杀鸡用牛刀。于是我把目光投向了mcp-run。这个官方提供的轻量级工具其核心设计理念就是让你能用最简单的方式——通常就是写一个脚本——快速创建一个一次性的、或临时的MCP工具并直接暴露给AI使用。这简直完美契合了我“快速验证想法”、“解决特定小问题”的需求。但当我真正开始动手时发现关于如何从零编写一个mcp-run可用的工具中文社区的实践分享并不多大多停留在概念介绍。所以我决定结合自己踩坑和实现的过程写一篇详细的指南聊聊如何用mcp-run编写一个真正简单、可用的MCP工具。这不仅仅是调用AI而是让AI能调用“你亲手打造”的工具这种控制感和灵活性是单纯使用现成工具无法比拟的。2. MCP与mcp-run核心概念快速解析在动手之前我们得先统一一下认知理解我们到底在做什么。这能帮你避开很多后续的迷惑。2.1 MCP协议AI的“手和脚”你可以把大模型LLM想象成一个超级聪明但“瘫痪”在床的大脑。它知识渊博逻辑缜密但它没有手去操作电脑没有眼睛去浏览网页没有耳朵去听实时信息。MCP协议就是为这个“大脑”安装的一套标准化的“神经接口”和“外骨骼”。这套协议定义了一套清晰的通信规范工具Tools大脑可以发出的指令。比如“伸手拿水杯”、“睁开眼睛看网页”。在MCP里一个工具对应一个可供AI调用的函数有明确的名称、描述和参数格式。资源Resources大脑可以读取的信息。比如“眼前的书桌上有哪些物品”、“当前的天气数据”。在MCP里资源可以是文本、文件、数据库查询结果等通过URI来标识和读取。提示Prompts大脑可以调用的预设对话模板。比如“开始一次代码审查会话”。MCP Server就是实现了这套协议的“外骨骼”本体。它暴露出具体的工具和资源等待AIMCP Client来连接和调用。我们常说的“为Code添加MCP支持”本质上就是让Code作为Client能够连接到这些Server从而获得扩展能力。2.2 mcp-run你的“快速原型制造机”而mcp-run是MCP官方工具集里一个极其轻量的成员。它不是为了构建一个长期运行、功能完备的Server而生的。它的定位是“脚本运行器”。它的工作模式非常直接你写一个脚本可以是Python、Node.js、Bash等任何能输出JSON到标准输出的程序。这个脚本只需要做一件事根据接收到的指令执行对应的逻辑然后把结果按照MCP约定的JSON格式打印出来。mcp-run负责启动你的脚本并扮演一个“翻译官”和“接线员”的角色。它把来自AI Client如Claude Desktop的请求转换成对你的脚本的调用再把你的脚本输出的JSON转换成标准的MCP响应传回给AI。为什么选择mcp-run零框架依赖你不需要引入任何复杂的MCP SDK或框架一个能处理命令行参数和打印JSON的脚本足矣。开发速度快聚焦业务逻辑本身无需关心Server的生命周期管理、长连接维护等复杂问题。语言无绑定可以用你最熟悉的脚本语言快速实现。完美适配临时需求为某个一次性数据分析、某个特定的文件处理任务快速创建一个AI助手工具用完即弃没有负担。理解了这些我们就可以开始动手了。我们的目标是创建一个能查询指定城市当前天气的简单工具。3. 开发环境准备与项目初始化工欲善其事必先利其器。我们先来把环境搭好。3.1 基础环境配置首先确保你的系统已经安装了Node.jsmcp-run本身是一个Node.js工具和Python我们将用Python编写工具脚本。在终端中检查node --version python3 --version接下来全局安装modelcontextprotocol/sdk和mcp-run。虽然我们写工具脚本用不到SDK但安装它通常能确保相关依赖齐全。npm install -g modelcontextprotocol/sdk然后通过npx直接运行mcp-run推荐无需全局安装或者从源码构建。这里我们使用npx方式因为它最干净。# 尝试运行一下查看帮助信息这也会触发下载 npx modelcontextprotocol/mcp-run --help3.2 创建项目结构创建一个干净的项目目录这有助于管理我们的脚本和配置。mkdir simple-weather-mcp cd simple-weather-mcp在这个目录里我们主要需要两个文件weather_tool.py我们的Python工具脚本包含核心逻辑。server.py可选但推荐一个极简的Python HTTP服务器脚本用于模拟一个天气API。因为我们需要一个真实的数据源来演示但又不希望依赖不稳定的外部网络API所以自己模拟一个。我们先创建server.py用它来提供一个本地的、稳定的天气数据接口。# server.py - 一个简单的模拟天气API服务器 from http.server import HTTPServer, BaseHTTPRequestHandler import json import sys # 模拟的天气数据 WEATHER_DATA { beijing: {city: 北京, condition: 晴朗, temperature: 22, humidity: 45}, shanghai: {city: 上海, condition: 多云, temperature: 25, humidity: 70}, shenzhen: {city: 深圳, condition: 阵雨, temperature: 28, humidity: 85}, newyork: {city: 纽约, condition: 阴天, temperature: 18, humidity: 60}, } class SimpleWeatherHandler(BaseHTTPRequestHandler): def do_GET(self): # 从路径中提取城市名例如 /weather/beijing parts self.path.split(/) if len(parts) 3 and parts[1] weather: city_key parts[2].lower() weather WEATHER_DATA.get(city_key) self.send_response(200 if weather else 404) self.send_header(Content-type, application/json) self.end_headers() if weather: response json.dumps(weather).encode(utf-8) else: response json.dumps({error: City not found}).encode(utf-8) self.wfile.write(response) else: self.send_response(400) self.send_header(Content-type, application/json) self.end_headers() self.wfile.write(json.dumps({error: Invalid request path}).encode(utf-8)) def log_message(self, format, *args): # 静默日志避免干扰控制台输出 pass def run_server(port8080): server_address (, port) httpd HTTPServer(server_address, SimpleWeatherHandler) print(f模拟天气API服务器已在 http://localhost:{port} 启动) print(f支持的城市: {, .join(WEATHER_DATA.keys())}) print(访问示例: GET /weather/beijing) try: httpd.serve_forever() except KeyboardInterrupt: print(\n服务器已关闭) httpd.server_close() if __name__ __main__: run_server()打开一个新的终端窗口运行这个服务器python3 server.py保持这个终端运行我们的天气API就在http://localhost:8080上待命了。4. 核心工具脚本编写详解现在我们来编写核心的weather_tool.py。这个脚本必须遵循mcp-run的交互协议。4.1 理解mcp-run的STDIN/STDOUT协议mcp-run与你的脚本通过标准输入STDIN和标准输出STDOUT进行JSON通信。整个生命周期是这样的初始化mcp-run启动你的脚本并向其STDIN发送一个初始化请求表明MCP协议的版本等信息。你的脚本需要回复一个清单list_tools的结果告诉mcp-run“我提供了哪些工具”。空闲等待脚本进入循环从STDIN读取JSON行。调用工具当AI想要使用某个工具时mcp-run会向你的脚本发送一个call_tool请求其中包含工具名和参数。执行并返回你的脚本执行对应的逻辑然后将结果以JSON格式打印到STDOUT。结束当mcp-run退出时你的脚本进程也会被终止。我们的脚本需要处理两种请求list_tools和call_tool。4.2 编写weather_tool.py脚本下面是完整的脚本代码我会逐段解释。#!/usr/bin/env python3 # weather_tool.py - 一个简单的MCP天气查询工具脚本 import sys import json import requests def list_tools(): 返回此脚本提供的工具清单 tools [ { name: get_current_weather, description: 获取指定城市的当前天气情况。, inputSchema: { type: object, properties: { city: { type: string, description: 城市名称的英文拼音或小写字母如beijing, shanghai } }, required: [city] } } ] return tools def call_tool(tool_name, arguments): 根据工具名和参数调用具体的工具 if tool_name get_current_weather: city arguments.get(city, ).strip().lower() if not city: return {error: 参数 city 不能为空} # 调用我们本地运行的模拟天气API try: # 注意这里请求的是我们本地启动的 server.py response requests.get(fhttp://localhost:8080/weather/{city}, timeout5) response.raise_for_status() # 如果状态码不是200抛出异常 weather_data response.json() # 格式化输出使其对AI更友好 result_text ( f城市{weather_data[city]}\n f天气状况{weather_data[condition]}\n f温度{weather_data[temperature]}°C\n f湿度{weather_data[humidity]}% ) return {content: [{type: text, text: result_text}]} except requests.exceptions.ConnectionError: return {error: 无法连接到天气服务请确保模拟服务器server.py已启动。} except requests.exceptions.Timeout: return {error: 请求天气服务超时。} except requests.exceptions.HTTPError: return {error: f未找到城市 {city} 的天气信息。} except Exception as e: return {error: f获取天气数据时发生未知错误{str(e)}} else: return {error: f未知工具{tool_name}} def main(): 主循环处理来自mcp-run的JSON行请求 # 首次启动mcp-run会发送初始化请求我们需要回复工具清单 for line in sys.stdin: if not line.strip(): continue try: request json.loads(line) request_type request.get(method) params request.get(params, {}) if request_type initialize: # 回复初始化成功并附上工具清单 result { jsonrpc: 2.0, id: request.get(id), result: { protocolVersion: 2024-11-05, capabilities: { tools: {} }, serverInfo: { name: Simple Weather MCP Tool, version: 0.1.0 } } } # 在初始化响应后立即发送工具清单这是一个单独的notification tools list_tools() print(json.dumps(result)) sys.stdout.flush() # 发送 tools/list 通知 print(json.dumps({ jsonrpc: 2.0, method: tools/list, params: {tools: tools} })) sys.stdout.flush() elif request_type tools/call: # 调用工具 tool_call params.get(toolCall) tool_name tool_call.get(name) tool_args tool_call.get(arguments, {}) call_result call_tool(tool_name, tool_args) # 构建响应 response { jsonrpc: 2.0, id: request.get(id), result: { toolCalls: [ { callId: tool_call.get(callId), result: call_result } ] } } print(json.dumps(response)) sys.stdout.flush() # 可以处理其他类型的请求如 resources/list, resources/read 等 # 但本例中我们只实现工具相关功能 else: # 对于不支持的请求返回方法未找到错误可选 pass except json.JSONDecodeError: # 忽略无效的JSON输入 pass except Exception as e: # 捕获其他异常避免脚本崩溃 error_response { jsonrpc: 2.0, id: request.get(id) if request in locals() else None, error: { code: -32000, message: fServer error: {str(e)} } } print(json.dumps(error_response)) sys.stdout.flush() if __name__ __main__: main()关键点解析工具定义 (list_tools函数)我们定义了一个名为get_current_weather的工具。description字段至关重要AI如Claude会根据这个描述来决定是否以及何时调用这个工具。描述要清晰、具体。inputSchema定义了工具的参数。这里我们只要求一个city参数并说明了其格式。required字段指明了这是必填参数。工具实现 (call_tool函数)核心业务逻辑在这里。我们接收城市名向本地模拟API发起请求。错误处理是重中之重。网络请求可能失败连接错误、超时API可能返回错误HTTP 404我们的脚本必须妥善处理这些情况并返回结构化的错误信息而不是崩溃或输出混乱的文本。这能保证AI和用户获得明确的反馈。返回格式必须遵循MCP对工具调用结果的约定{content: [{type: text, text: ...}]}。这是一个包含文本内容的数组。主循环协议处理 (main函数)循环读取sys.stdin的每一行每行是一个JSON-RPC请求。初始化阶段收到initialize请求后除了回复协议版本等信息必须紧接着发送一个tools/list通知将工具清单告知客户端。这是很多初学者会漏掉的一步导致工具列表为空。调用阶段收到tools/call请求后解析出工具名和参数交给call_tool函数执行然后将结果包装成JSON-RPC响应格式输出。输出后务必刷新缓冲区sys.stdout.flush()确保数据立即发送出去避免因缓冲导致通信延迟或超时。5. 本地测试与调试技巧脚本写好了但在交给AI使用前我们必须先进行充分的手动测试确保协议通信和业务逻辑都正确。5.1 模拟mcp-run进行手动测试我们可以自己模拟mcp-run的行为向脚本发送JSON请求来测试。创建一个test_script.py文件# test_script.py import subprocess import json import time # 启动我们的工具脚本进程 proc subprocess.Popen( [python3, weather_tool.py], stdinsubprocess.PIPE, stdoutsubprocess.PIPE, stderrsubprocess.PIPE, textTrue, bufsize1 # 行缓冲 ) def send_request(req): 发送一个JSON-RPC请求 print(f 发送: {json.dumps(req)}) proc.stdin.write(json.dumps(req) \n) proc.stdin.flush() def read_response(): 读取一行响应 line proc.stdout.readline() if line: print(f 接收: {line.strip()}) return json.loads(line) return None # 1. 发送初始化请求 init_request { jsonrpc: 2.0, id: 1, method: initialize, params: { protocolVersion: 2024-11-05, clientInfo: {name: TestClient} } } send_request(init_request) # 等待并读取初始化响应和随后的tools/list通知 time.sleep(0.5) while True: try: resp read_response() if resp and resp.get(method) tools/list: print(工具清单已接收。) break except: break # 2. 发送工具调用请求 call_request { jsonrpc: 2.0, id: 2, method: tools/call, params: { toolCall: { callId: call_123, name: get_current_weather, arguments: {city: beijing} } } } send_request(call_request) # 读取调用结果 time.sleep(0.5) read_response() # 关闭进程 proc.terminate() proc.wait()运行这个测试脚本python3 test_script.py你应该能看到类似以下的输出表明通信成功 发送: {初始化请求...} 接收: {初始化响应...} 接收: {jsonrpc: 2.0, method: tools/list, params: {tools: [...]}} 工具清单已接收。 发送: {调用请求...} 接收: {调用结果...}检查调用结果中是否包含了正确的北京天气信息。5.2 集成到Claude Desktop进行真实测试手动测试通过后就可以进行终极测试了让真正的AI来调用它。配置Claude Desktop 找到Claude Desktop的配置文件。通常在以下位置macOS:~/Library/Application Support/Claude/claude_desktop_config.jsonWindows:%APPDATA%\Claude\claude_desktop_config.jsonLinux:~/.config/Claude/claude_desktop_config.json编辑配置文件 在配置文件中添加你的mcp-run工具配置。如果文件不存在就创建它。{ mcpServers: { simple-weather: { command: npx, args: [ modelcontextprotocol/mcp-run, python3, /ABSOLUTE/PATH/TO/YOUR/simple-weather-mcp/weather_tool.py ] } } }重要必须使用python3命令的绝对路径或者确保它在系统PATH中。weather_tool.py的路径也必须使用绝对路径。这是最常见的错误来源之一。重启Claude Desktop 完全退出并重新启动Claude Desktop使其加载新的配置。开始对话测试 在新的对话中直接询问“Can you check the weather in Shanghai for me?” 或者 “请帮我查询一下上海的天气。” 如果一切配置正确Claude应该会识别出可用的get_current_weather工具并自动调用它。你会在Claude的回复中看到“正在调用工具...”的提示然后显示出格式化后的天气信息。6. 进阶优化与生产级考量我们的基础工具已经能跑了但要把它变得更强壮、更实用还需要考虑以下几点。6.1 增强脚本的健壮性输入验证与清洗我们的脚本只做了简单的strip().lower()。在生产环境中需要对输入进行更严格的验证防止注入攻击或非法输入导致脚本异常。例如检查城市名是否只包含字母。超时与重试机制对于外部API调用除了设置请求超时还可以实现简单的重试逻辑如重试2次以提高在临时网络波动下的成功率。资源管理与清理如果工具涉及打开文件、数据库连接等确保在脚本生命周期内或异常情况下能正确关闭和清理。日志记录将重要的操作和错误记录到文件而不是仅仅打印到标准输出/错误流便于后期排查问题。可以引入Python的logging模块。6.2 扩展工具功能从单一到多元一个脚本可以提供多个工具。只需在list_tools函数中返回多个工具定义并在call_tool函数中根据tool_name进行分发即可。例如我们可以增加一个工具def list_tools(): tools [ { name: get_current_weather, description: 获取指定城市的当前天气情况。, inputSchema: {...} # 同上 }, { name: list_supported_cities, description: 列出当前天气服务支持查询的所有城市。, inputSchema: { # 这个工具不需要参数 type: object, properties: {}, required: [] } } ] return tools def call_tool(tool_name, arguments): if tool_name get_current_weather: # ... 原有逻辑 elif tool_name list_supported_cities: # 返回支持的城市列表 supported [北京 (beijing), 上海 (shanghai), 深圳 (shenzhen), 纽约 (newyork)] result_text 当前支持查询的城市有\n \n.join(f- {city} for city in supported) return {content: [{type: text, text: result_text}]} else: return {error: f未知工具{tool_name}}6.3 性能与部署思考mcp-run模式的特点是每次调用都可能启动一个新的脚本进程。对于简单的、调用不频繁的工具这没问题。但如果工具初始化成本高如加载大模型、连接数据库频繁启停会成为性能瓶颈。此时你有两个方向升级为标准MCP Server使用modelcontextprotocol/sdk或其他语言的SDK编写一个长期运行的Server。这样昂贵的初始化只需一次。这是功能复杂、调用频繁的工具的最终归宿。在mcp-run脚本内做缓存如果只是避免重复计算可以在脚本内使用全局变量或轻量级缓存注意进程隔离mcp-run不同会话可能不是同一进程。部署上如果你想让团队其他成员也能使用这个工具你需要将脚本和可能的配置文件打包。编写清晰的安装和配置说明尤其是Claude Desktop配置那一步。考虑使用Docker容器化以确保运行环境一致。7. 常见问题与排查实录在这一路上我踩过不少坑。这里把典型问题和解决方法列出来希望能帮你节省时间。问题现象可能原因排查步骤与解决方案Claude Desktop完全看不到新工具1. 配置文件路径错误。2. 配置文件格式错误JSON语法。3.mcp-run或脚本命令执行失败。1.检查配置文件路径和名称确保文件在正确位置且名称是claude_desktop_config.json。2.验证JSON格式使用jq . your_config.json或在线JSON校验工具。3.查看Claude Desktop日志在Claude Desktop设置中打开“调试模式”或查看其输出日志启动时在终端运行里面通常会有MCP Server加载失败的具体错误信息。Claude能看到工具但调用时失败或超时1. 脚本路径或解释器命令错误相对路径问题。2. 脚本本身有Bug启动后立即崩溃。3. 脚本协议实现错误未正确响应请求。1.使用绝对路径在配置文件的args中为python3和脚本路径都使用绝对路径。2.独立运行脚本在终端直接运行python3 /path/to/your_tool.py看是否有语法错误或导入错误。3.进行手动STDIN测试使用前面章节的test_script.py模拟调用这是最有效的调试手段能精准定位是协议问题还是业务逻辑问题。脚本被调用但返回“Internal server error”或AI收不到结果1. 脚本没有输出合法的JSON-RPC响应。2. 脚本输出后没有刷新缓冲区 (sys.stdout.flush())。3. 脚本抛出了未捕获的异常。1.检查输出格式确保打印到stdout的每一行都是完整的、格式正确的JSON。可以用print(json.dumps(obj), flushTrue)一步到位。2.添加异常捕获在main()函数最外层用try...except包裹将任何异常都转换为JSON-RPC错误响应输出避免脚本静默崩溃。3.查看脚本的stderr在Claude Desktop配置中可以将stderr重定向到文件查看运行时错误。工具描述已更新但Claude仍显示旧的Claude Desktop可能缓存了工具列表。1. 完全退出Claude Desktop并重新启动。2. 如果还不行尝试清除Claude Desktop的缓存数据位置因系统而异通常可在设置中找到或删除应用数据目录。调用工具时AI说“参数不正确”工具定义的inputSchema与AI传递的参数不匹配。1. 检查list_tools返回的schema确保required字段和properties定义正确。2. 在call_tool函数开始处打印收到的arguments确认AI传递的数据结构。有时AI可能会传递额外的参数或格式有细微差别。一个关键的实操心得始终先进行离线的、手动的协议测试。在集成到AI环境前用test_script.py这样的模拟器验证你的脚本能正确响应initialize和tools/call请求。这能将问题隔离在最小的范围内避免在Claude、Cursor等复杂环境中进行低效的黑盒调试。编写mcp-run工具是一个“麻雀虽小五脏俱全”的过程。它迫使你深入理解MCP协议最基本的通信单元这种理解在你未来构建更复杂的标准MCP Server时会是一笔宝贵的财富。从解决自己的一个小痛点开始亲手造一个AI能用的工具这种成就感远比单纯使用现成方案要大得多。当你看到AI流畅地调用着你写的代码并给出答案时你会感觉自己和AI的协作进入了一个新的层次——你不再是单纯的使用者而是其能力的塑造者之一。