mcp-run:快速构建AI工具,让大模型安全调用本地脚本
1. 项目概述为什么我们需要 mcp-run如果你最近在折腾 AI 编程特别是想让 Claude、Cursor 这类智能助手帮你处理一些本地操作比如读取文件、执行脚本或者查询系统状态那你大概率已经接触过 MCPModel Context Protocol这个概念了。简单来说MCP 是一个让大模型安全、可控地使用外部工具和数据的协议。但问题来了官方提供的 MCP Server 开发框架对于只是想快速验证一个想法、或者做一个一次性小工具的人来说有点“杀鸡用牛刀”的感觉。你需要配置环境、理解复杂的项目结构、处理繁琐的启动参数……还没开始写核心逻辑热情可能就被消耗了一半。这就是mcp-run这类工具出现的背景。它的核心目标就一个让你能用最简单、最直接的方式把一个想法变成一个 AI 可用的工具。你可以把它想象成 MCP 世界里的“脚本运行器”。不需要复杂的项目脚手架不需要理解完整的 Server 生命周期甚至对协议细节一知半解也没关系。你只需要关注工具本身的逻辑输入是什么处理过程是什么输出是什么。mcp-run负责帮你处理剩下的所有“脏活累活”比如启动一个符合 MCP 协议的服务器、处理与 AI 客户端的通信、管理工具的生命周期等等。我最初想做一个能让我用自然语言查询服务器磁盘空间的小工具如果走标准流程我可能得花半天时间。但用mcp-run的思路我写了一个不到 50 行的 Python 脚本五分钟就让它跑起来了并且立刻就能在 Claude Desktop 里调用。这种快速将想法落地的体验对于探索 AI 能力的边界至关重要。2. mcp-run 的核心设计思路与工作原理2.1 化繁为简从标准 MCP Server 到轻量脚本要理解mcp-run的设计首先得看看一个标准的 MCP Server 有多“重”。一个典型的 MCP 服务器比如用官方 TypeScript SDK 创建的项目通常包含以下部分协议实现必须实现initialize,tools/list,tools/call等核心 MCP 协议端点。工具注册与管理需要显式地定义工具Tool的输入输出 Schema通常用 JSON Schema并注册到服务器实例中。生命周期管理需要处理服务器的启动、信号监听、优雅关闭。传输层配置需要配置 STDIO标准输入输出或 SSE服务器发送事件等传输方式以便与 AI 客户端如 Claude Desktop通信。依赖与构建一个完整的package.json或pyproject.toml以及可能的构建步骤。这对于一个成熟的、需要长期维护的工具集是必要的。但对于一个“简单工具”呢我们可能只想写一个函数。mcp-run的设计哲学就是面向函数编程。它假设你的工具核心就是一个函数接收一些参数执行一些操作返回一个结果。至于这个函数如何被包装成 MCP 工具、如何启动服务器、如何与客户端握手这些统统交给mcp-run来处理。2.2 底层工作原理一个精妙的封装器mcp-run本身并不是一个 MCP 协议的完整实现者而是一个封装器和胶水层。它的工作流程可以拆解为以下几个步骤脚本加载与解析mcp-run读取你提供的脚本文件比如my_tool.py。它会通过约定的方式例如查找特定的函数名、装饰器或者解析脚本的导出对象来识别出你想要暴露为 MCP 工具的函数。动态工具包装对于识别出的每个函数mcp-run会在内存中动态创建一个符合 MCP 协议规范的Tool对象。它会自动分析函数的参数通过类型注解或默认值并尝试将其映射为 JSON Schema作为工具的输入描述。函数的文档字符串docstring则会被用作工具的“描述”。内嵌服务器启动mcp-run内部启动了一个轻量级的、符合 MCP 协议的服务器。这个服务器只做最少的事情在初始化时向客户端宣告它动态包装好的那几个工具在收到tools/call调用时找到对应的函数传入参数执行它并将返回值格式化成 MCP 要求的响应格式。进程与通信管理mcp-run负责管理这个内嵌服务器的整个进程生命周期。它通常通过 STDIO 与 AI 客户端通信这意味着 AI 客户端只需要像启动一个子进程一样启动mcp-run并通过标准输入输出流交换 JSON 消息即可。mcp-run处理了所有消息的解析、路由和序列化。这种设计带来的最大好处是关注点分离。作为工具开发者你只需要关心你的业务逻辑函数写得对不对。作为工具使用者你只需要知道如何运行mcp-run命令。中间的协议复杂性被完全隐藏了。2.3 与同类方案的对比为什么不是直接写脚本你可能会问我直接写个脚本让 AI 去调用系统命令执行这个脚本不也一样吗这里有几个关键区别安全性直接执行任意脚本是极高风险的行为。MCP 协议要求工具必须预先声明其输入参数和类型AI 客户端在调用前可以进行校验并且工具的执行是在一个受控的、预先定义好的上下文中进行的。mcp-run继承了这种安全模型。结构化交互通过 MCP 调用工具输入和输出都是结构化的 JSON 数据。你的脚本函数可以直接接收字典、列表等复杂对象并返回同样结构化的数据。而通过系统命令调用你通常只能传递字符串参数并且需要自己解析标准输出。发现与集成MCP 工具可以被 AI 客户端自动发现和描述。在 Claude Desktop 中连接后AI 就能知道你有“查询磁盘空间”、“格式化文档”等工具并理解它们的用途和参数。这是通过系统命令调用无法实现的体验。状态与性能mcp-run启动的服务器是常驻进程。如果你的工具需要加载大型模型或建立数据库连接这个成本只需要在启动时支付一次。后续的每次调用都非常快速。而每次通过系统命令调用脚本都需要启动一个新的 Python 解释器进程重复加载资源效率低下。3. 手把手实战从零编写并运行你的第一个 mcp-run 工具理论说得再多不如动手做一遍。我们以 Python 环境为例创建一个最简单的工具一个能够对两个数进行加减乘除运算的计算器工具。3.1 环境准备与依赖安装首先你需要一个 Python 环境3.8。然后安装mcp-run。目前它可能不是一个通过pip install mcp-run就能直接获取的包因为它更像一个概念或一个社区工具的原型。我们可以模拟实现一个最简单的版本。为了理解原理我们不直接使用某个特定的mcp-run实现而是利用现有的、最接近的库来搭建mcp官方 Python SDK 的底层库以及asyncio来处理异步通信。# 创建一个新的项目目录 mkdir my-mcp-tools cd my-mcp-tools # 创建虚拟环境推荐 python -m venv .venv # 激活虚拟环境 # Windows: .venv\Scripts\activate # macOS/Linux: source .venv/bin/activate # 安装 MCP 协议的基础库和必要的依赖 pip install mcp注意这里安装的mcp包是协议底层库它提供了构建 MCP 服务器和客户端的基础组件但并不包含一个现成的mcp-run命令行工具。我们正是要用它来理解mcp-run是如何工作的。3.2 编写工具函数脚本现在我们创建一个名为calculator_tool.py的脚本。这个脚本将包含我们想要暴露的工具逻辑。# calculator_tool.py import asyncio from typing import Literal # 这是我们的核心工具函数 async def calculate( a: float, b: float, operation: Literal[add, subtract, multiply, divide] ) - str: 执行简单的算术运算。 Args: a: 第一个操作数。 b: 第二个操作数。 operation: 要执行的运算可选值add加, subtract减, multiply乘, divide除。 Returns: 运算结果的字符串表示。 if operation add: result a b elif operation subtract: result a - b elif operation multiply: result a * b elif operation divide: if b 0: return 错误除数不能为零。 result a / b else: return 错误未知的运算类型。 return f{a} {operation} {b} {result} # 为了让“mcp-run”类工具能发现这个函数我们需要以某种方式导出它。 # 一个常见的约定是提供一个 tools 列表或字典。 __all__ [calculate] tools [calculate] # 将函数放入一个列表中关键点解析异步函数我们使用了async def。这是因为 MCP 服务器通常是异步的以高效处理并发请求。你的工具函数最好也是异步的特别是当它可能涉及 I/O 操作如读写文件、网络请求时。类型注解a: float,b: float,operation: Literal[...]。这些类型注解至关重要它们是我们自动生成工具输入 Schema 的依据。Literal明确指出了operation参数只能取那几个特定的字符串值。文档字符串Docstring函数下的三引号注释。这将成为 AI 客户端中看到的工具描述帮助 AI 理解何时以及如何使用这个工具。工具导出我们创建了一个tools列表。这是我们的脚本与“运行器”之间的一个简单约定“运行器”会来查找这个变量并将其中的每个函数注册为一个 MCP 工具。3.3 实现一个简易的 mcp-run 启动器由于没有现成的mcp-run我们来写一个简化的启动脚本simple_mcp_runner.py模拟它的核心行为。# simple_mcp_runner.py import sys import asyncio import inspect import json from typing import Any, Dict, List import importlib.util from mcp import ClientSession, StdioServerParameters from mcp.client.stdio import stdio_client async def main(): if len(sys.argv) ! 2: print(用法: python simple_mcp_runner.py 工具脚本路径, filesys.stderr) sys.exit(1) script_path sys.argv[1] # 1. 动态加载工具脚本 spec importlib.util.spec_from_file_location(tool_module, script_path) tool_module importlib.util.module_from_spec(spec) spec.loader.exec_module(tool_module) # 2. 从模块中获取工具函数列表遵循我们的约定 tool_functions getattr(tool_module, tools, []) if not tool_functions: print(f错误在 {script_path} 中未找到 tools 列表。, filesys.stderr) sys.exit(1) # 3. 为每个工具函数创建 MCP 工具描述 mcp_tools [] for func in tool_functions: # 解析函数签名以生成 JSON Schema sig inspect.signature(func) parameters_schema { type: object, properties: {}, required: [] } for param_name, param in sig.parameters.items(): param_schema {} # 处理类型注解简化版实际需要更复杂的类型映射 if param.annotation ! inspect.Parameter.empty: # 这里只是一个简单演示实际需要将 Python 类型映射为 JSON Schema 类型 if param.annotation in (int, float): param_schema[type] number elif param.annotation is str: param_schema[type] string elif hasattr(param.annotation, __origin__) and param.annotation.__origin__ is Literal: param_schema[type] string param_schema[enum] list(param.annotation.__args__) else: param_schema[type] string # 默认回退到字符串 else: param_schema[type] string # 从文档字符串或参数默认值获取描述此处简化 param_schema[description] f参数 {param_name} parameters_schema[properties][param_name] param_schema if param.default inspect.Parameter.empty: parameters_schema[required].append(param_name) tool_def { name: func.__name__, description: func.__doc__ or f执行 {func.__name__} 操作, inputSchema: parameters_schema } mcp_tools.append(tool_def) # 4. 创建并运行 MCP 服务器这里我们实际上创建一个客户端会话并通过自定义逻辑模拟服务器 # 这是最简化的演示真实实现需要实现完整的 MCP 服务器协议。 # 我们使用 mcp 库的底层客户端来模拟一个“反向”服务我们主动连接到一个 Stdio 流。 server_params StdioServerParameters( commandsys.executable, # 这里是个技巧我们用自己作为“命令” args[-c, print(MCP stdio server ready)] # 实际上我们需要一个真正的服务器进程 ) # 注意以下是一个概念性代码真实环境需要更复杂的处理。 # 为了演示我们直接打印出工具定义并进入一个简单的读取-求值-打印循环。 print(json.dumps({ jsonrpc: 2.0, method: notify, params: { method: tools/list, params: {tools: mcp_tools} } }), flushTrue) # 简单循环读取 stdin 的调用请求执行函数打印结果 print(简易 MCP 工具运行器已启动等待调用..., filesys.stderr) while True: try: line sys.stdin.readline() if not line: break request json.loads(line.strip()) if request.get(method) tools/call: tool_name request[params][name] arguments request[params].get(arguments, {}) # 查找对应的函数 target_func None for f in tool_functions: if f.__name__ tool_name: target_func f break if target_func: # 执行函数 try: # 注意这里需要异步执行我们简化用 asyncio.run # 实际应在异步上下文中 result await target_func(**arguments) response { jsonrpc: 2.0, id: request.get(id), result: { content: [{type: text, text: str(result)}] } } except Exception as e: response { jsonrpc: 2.0, id: request.get(id), error: {message: str(e)} } print(json.dumps(response), flushTrue) else: print(json.dumps({ jsonrpc: 2.0, id: request.get(id), error: {message: fTool not found: {tool_name}} }), flushTrue) except json.JSONDecodeError: continue except KeyboardInterrupt: break if __name__ __main__: asyncio.run(main())这个simple_mcp_runner.py脚本做了以下几件事加载用户指定的工具脚本。从脚本中提取tools列表。通过反射inspect模块分析每个函数的签名和文档动态生成 MCP 协议要求的工具定义inputSchema。启动一个简单的循环从标准输入读取 JSON-RPC 格式的调用请求找到对应的函数执行并将结果通过标准输出以 JSON-RPC 格式返回。这本质上就是一个极度简化的mcp-run。3.4 连接与测试要测试这个工具我们需要一个 MCP 客户端。最方便的就是 Claude Desktop。配置 Claude Desktop找到 Claude Desktop 的 MCP 配置文件。通常在以下位置macOS:~/Library/Application Support/Claude/claude_desktop_config.jsonWindows:%APPDATA%\Claude\claude_desktop_config.jsonLinux:~/.config/Claude/claude_desktop_config.json编辑配置文件在mcpServers部分添加我们的“服务器”。{ mcpServers: { my-calculator: { command: python, args: [ /ABSOLUTE/PATH/TO/YOUR/simple_mcp_runner.py, /ABSOLUTE/PATH/TO/YOUR/calculator_tool.py ], env: { PYTHONPATH: /ABSOLUTE/PATH/TO/YOUR/PROJECT } } } }command: 我们使用python解释器。args: 第一个参数是我们的运行器脚本第二个参数是工具脚本。env: 确保 Python 能找到你的脚本和可能安装的mcp库。重启 Claude Desktop保存配置文件并完全重启 Claude Desktop。测试重启后在 Claude 的聊天框中你应该能看到它已经加载了新工具。你可以尝试输入“请用计算器工具计算一下 15.7 乘以 4.2。” Claude 应该会识别出calculate工具并请求你提供operation参数或者它可能直接推断出使用multiply然后调用工具并返回结果。实操心得第一次配置时最常见的失败原因是路径错误或权限问题。务必使用绝对路径。可以在终端中先手动运行一下配置的命令看看脚本是否能正常启动、有无报错如模块导入错误。另外Claude Desktop 的配置是热加载的但有时需要彻底重启完全退出再打开才能生效。4. 进阶技巧打造更实用的 mcp-run 工具掌握了基础之后我们可以让工具变得更强大、更健壮。4.1 处理复杂参数与类型映射上面的简易运行器对类型的处理非常粗糙。一个健壮的mcp-run应该能更好地处理复杂的 Python 类型到 JSON Schema 的映射。例如处理List[str]、Dict[str, int]、Optional[float]等。我们可以利用pydantic库来极大地简化这个过程。首先安装pydanticpip install pydantic然后我们可以用 Pydantic 的BaseModel来定义工具的输入这样类型检查和 Schema 生成都会变得非常简单和准确。# advanced_tool.py from typing import List, Optional from pydantic import BaseModel, Field import asyncio # 使用 Pydantic Model 定义输入结构 class SummarizeInput(BaseModel): text: str Field(..., description需要总结的文本内容) max_length: Optional[int] Field(100, description总结的最大长度默认为100字符) keywords: List[str] Field(default_factorylist, description需要重点关注的关键词列表) async def summarize_text(input_data: SummarizeInput) - str: 对提供的文本进行智能总结。 该工具会提取文本的核心内容并根据可选的关键词进行侧重。 # 这里是一个简单的模拟实现 words input_data.text.split() if input_data.keywords: # 简单模拟如果有关键词在总结中提及 summary f本文涉及{, .join(input_data.keywords)}等概念。核心内容{ .join(words[:20])}... else: summary f核心内容{ .join(words[:20])}... if input_data.max_length and len(summary) input_data.max_length: summary summary[:input_data.max_length-3] ... return summary # 导出工具 tools [summarize_text]在我们的simple_mcp_runner.py中需要增加对 Pydantic Model 的检测。如果函数的参数是一个 Pydantic Model那么直接使用model.schema()或model.model_json_schema()来生成inputSchema这比手动解析inspect.signature要可靠和强大得多。4.2 工具的多功能与组合一个脚本可以暴露多个工具函数。mcp-run应该能自动将它们全部注册。只需确保你的tools列表包含了所有你想暴露的函数。# multi_tool.py import asyncio import os from datetime import datetime async def get_current_time(timezone: str UTC) - str: 获取指定时区的当前时间。 # 简化处理实际应使用pytz等库 now datetime.utcnow() return fCurrent UTC time is: {now.isoformat()} (Timezone: {timezone}) async def list_files(directory: str .) - str: 列出指定目录下的文件和文件夹。 try: files os.listdir(directory) return fFiles in {directory}:\n \n.join(files) except FileNotFoundError: return f错误目录 {directory} 不存在。 except PermissionError: return f错误没有权限访问目录 {directory}。 # 导出多个工具 tools [get_current_time, list_files]这样当你运行这个脚本时AI 客户端就能同时看到“获取当前时间”和“列出文件”两个工具。4.3 错误处理与日志输出在工具函数中良好的错误处理非常重要。不要因为一个异常导致整个 MCP 服务器崩溃。应该用try...except捕获异常并返回友好的错误信息。此外工具执行过程中的日志信息不应该污染返回给 AI 的结构化内容。通常日志应该输出到标准错误stderr而工具的结果通过return返回由运行器包装后从标准输出stdout以 JSON-RPC 格式发送。async def safe_file_operation(filepath: str) - str: 执行一个安全的文件操作示例。 import logging logging.basicConfig(levellogging.INFO) logger logging.getLogger(__name__) logger.info(f开始处理文件: {filepath}) try: with open(filepath, r, encodingutf-8) as f: content f.read() # 模拟一些处理 processed content.upper()[:500] # 取前500字符并转大写 logger.info(文件处理成功。) return processed except FileNotFoundError: error_msg f文件未找到: {filepath} logger.error(error_msg) return error_msg except Exception as e: error_msg f处理文件时发生未知错误: {e} logger.exception(error_msg) # 这会打印完整的堆栈跟踪到 stderr return error_msg注意事项在真正的mcp-run环境中标准错误输出可能会被 AI 客户端的日志系统捕获。确保你的工具日志是清晰且有用的便于在出现问题时进行调试。同时返回给 AI 的错误信息应当简洁、明确指导用户或 AI下一步该怎么做。5. 常见问题与排查技巧实录在实际使用和模拟实现mcp-run的过程中我遇到了不少坑。这里记录下最常见的问题和解决方法。5.1 连接与通信问题问题现象可能原因排查步骤与解决方案Claude Desktop 启动后提示“无法连接 MCP 服务器”或工具列表为空。1. 配置文件路径或语法错误。2. 命令或参数错误特别是路径不是绝对路径。3. Python 环境问题依赖未安装或使用了错误的 Python 解释器。4. 脚本本身有语法错误导致进程立即崩溃。1.检查配置文件使用 JSON 验证工具检查claude_desktop_config.json的语法。确保mcpServers对象格式正确。2.手动测试命令在终端中切换到配置中指定的工作目录如果有cwd设置然后完整地运行command和args组成的命令。观察输出看脚本是否能正常启动并停留在等待输入的状态还是报错退出。3.检查环境变量确保PYTHONPATH或虚拟环境已正确配置。在配置中显式设置env字段可能更可靠。4.查看客户端日志Claude Desktop 通常有日志文件。在 macOS 上可以在~/Library/Logs/Claude/找到Windows 在%APPDATA%\Claude\logs。查看日志中的错误信息。AI 客户端能连接但调用工具时超时或无响应。1. 工具函数是同步的但被放在异步上下文中执行导致阻塞。2. 工具函数执行时间过长。3. 运行器的通信循环逻辑有 bug没有正确返回响应。1.确保工具函数是异步的除非你的运行器明确支持同步函数并在独立线程中运行它们否则最好始终使用async def定义工具函数并在内部使用await进行 I/O 操作。2.为长时间运行的任务添加超时在工具函数内部实现超时逻辑或者考虑将任务拆分为更小的步骤。3.调试运行器在运行器的通信循环中添加调试打印语句输出到 stderr查看是否收到了调用请求以及是否发送了响应。5.2 工具定义与调用问题问题现象可能原因排查步骤与解决方案AI 无法正确识别工具参数或调用时参数类型错误。1. 工具输入 Schema 生成不正确。2. 函数类型注解不明确或不被运行器支持。3. AI 客户端对 Schema 的解析有差异。1.简化类型初期尽量使用基础类型str,int,float,bool和Literal。避免使用复杂的泛型如List[Dict[str, Any]]除非你的运行器能完美处理。2.使用 Pydantic如前所述使用 PydanticBaseModel是生成准确 Schema 的最可靠方法。它能明确地定义字段类型、默认值、描述和验证规则。3.检查生成的 Schema修改你的运行器在启动时将生成的工具定义inputSchema打印到 stderr。用这个 JSON 去 JSON Schema 验证网站 检查其正确性。工具被调用但返回的结果 AI 无法理解或格式错误。1. 返回的数据类型不是字符串或可序列化的简单结构。2. 运行器没有按照 MCP 协议格式化响应。3. 返回了过多无关信息如调试日志。1.返回文本或简单结构MCP 工具的content字段通常期望是文本。确保你的函数返回一个字符串。如果需要返回结构化数据可以返回一个 JSON 字符串并在工具描述中说明。2.遵循协议确保运行器返回的 JSON-RPC 响应格式正确特别是result.content是一个包含type和text的对象列表。3.净化输出确保工具函数的所有输出都通过return语句而不是print。将调试信息输出到logging或sys.stderr。5.3 性能与资源管理问题现象可能原因排查步骤与解决方案工具调用速度慢尤其是首次调用。1. 工具函数内部有昂贵的初始化操作如加载机器学习模型。2. 每次调用都启动新的进程或连接。1.利用缓存或全局状态在脚本的全局作用域或模块级别进行一次性初始化。例如将模型加载放在函数外部。mcp-run服务器是常驻进程全局变量会在多次调用间保持。2.连接池如果需要访问数据库或外部 API考虑在服务器启动时创建连接池而不是每次调用都新建连接。内存使用量随时间增长。1. 工具函数导致内存泄漏如不断追加到全局列表。2. 运行器本身有资源未释放。1.审查工具代码避免在全局范围内无限制地累积数据。对于需要缓存的数据设置大小限制或过期策略。2.使用轻量级运行器如果你实现的运行器有复杂逻辑确保没有不当的引用循环。对于长时间运行的服务这是正常挑战需要按常规服务进行内存剖析。我个人在实际操作中的体会是mcp-run这类工具的价值在于极大地降低了为 AI 构建工具的门槛。它把“与 MCP 协议对接”这个复杂的工程问题简化成了“写一个 Python 函数”的简单问题。这使得产品经理、数据分析师甚至是有编程兴趣的用户都能快速将自己的专业知识封装成 AI 可用的能力。虽然我们上面实现的是一个非常简易的版本但它清晰地揭示了其核心原理。社区中已经出现了一些更成熟的实现它们提供了更友好的 CLI、更完善的类型系统支持和错误处理。探索这些工具或者基于这个思路去构建更适合自己工作流的工具是拥抱 AI 时代编程方式的一个非常有趣的切入点。