MCP协议与Skills技能:Agent能力扩展的双轨架构解析 1. 项目概述当 Agent 不再是“单打独斗”能力扩展的底层逻辑开始分叉“MCP 与 SkillsAgent 能力扩展的两种路线”——这个标题乍看像技术名词堆砌但背后藏着当前 AI 应用开发最核心的分歧点一个智能体Agent到底该靠什么获得真实世界里的“手脚”是靠一套标准化的通信协议去对接外部服务还是靠开发者手写、封装、注册的一组可复用函数我从 2022 年底开始做 Agent 架构设计参与过 7 个落地项目其中 4 个在生产环境跑了一年以上。实话讲前两年大家默认只有一条路写 Skills。直到去年 Q3MCPModel Context Protocol规范正式发布 0.5 版本我们团队在给某省级政务知识中台做 Agent 升级时第一次把 MCP Server 和 Skills 混合部署才真正看清这两条路不是“替代关系”而是“分工关系”。MCP 解决的是“我能连谁”Skills 解决的是“我具体能干什么”。比如你要让 Agent 查社保缴费记录Skills 就是你写的那个get_social_security_record(user_id)函数而 MCP 则是它如何通过 JSON-RPC 2.0 协议安全、可追溯、带鉴权地调用社保局提供的标准 API 接口。关键词里反复出现的 “蓝湖 MCP”、“playwright mcp”、“chrome devtools mcp”本质都是前端工程化场景下对这套协议的落地尝试——蓝湖用它打通设计稿与代码生成链路Playwright 用它把浏览器自动化能力暴露给 LLM 调度。而“superpower skills”、“opencode skills”这类热词则代表另一极社区驱动的、高度垂直的技能库生态比如数学建模里一个solve_linear_programming_with_gurobi()的 Skills可能比通用 MCP 连接器更能直接解决用户问题。这不是技术路线之争而是工程权衡你要快速上线一个能查电费的 Agent用现成的电网 698 协议解析 Skills3 小时就能跑通但你要构建一个能对接 200 家不同银行、15 种政务系统、7 类物联网平台的统一 Agent 框架就必须引入 MCP 做协议层抽象。所以这篇内容不是教你怎么选而是带你亲手拆开这两个模块看它们在内存里怎么握手、在日志里怎么报错、在调试器里怎么断点——因为真正的坑永远不在文档里而在JSON-RPC 2.0的id字段类型不一致或Skills的tool_choice配置漏了一个required标志位。2. 核心思路拆解为什么必须同时理解 MCP 与 Skills一条被忽视的“能力鸿沟”2.1 从“能调用”到“能理解”的三层鸿沟很多开发者卡在第一步为什么我的 Agent 调用了 Skills但返回结果 LLM 就是看不懂或者调用了 MCP Server但 response 总是空根本原因在于Skills 和 MCP 分别横跨了“能力定义”和“能力调度”两个完全不同的抽象层中间隔着三道看不见的鸿沟第一道鸿沟语义鸿沟Skills 是“函数式”的你定义def send_email(to: str, subject: str, body: str) - dictLLM 看到的是参数名、类型、docstring。它靠这些文本描述理解“这个技能能发邮件”。而 MCP 是“协议式”的它只管{jsonrpc: 2.0, method: email.send, params: {to: ...}, id: 1}这个结构是否合法不管email.send这个 method 名字背后是发邮件还是删数据库。LLM 对 MCP 的理解完全依赖你给它的method描述文档通常是一段 Markdown这文档写得模糊LLM 就会乱调。我见过最典型的案例某金融客户把transfer_funds和withdraw_cash两个 Skills 的 docstring 都写成“处理资金操作”结果 Agent 在用户说“取现金”时调用了转账 Skills触发了合规告警。第二道鸿沟上下文鸿沟Skills 的执行上下文是 Python 进程内你可以轻松访问os.environ、读取本地 config 文件、调用requests.Session()复用连接池。MCP 的执行上下文是网络另一端你的 Agent 发出一个 JSON-RPC 请求目标 Server 可能在 AWS 上、可能在客户内网、甚至可能是一个物理设备比如 USB Sandisk 3.2 Gen1 量产工具它确实有基于 MCP 的调试接口。这意味着 Skills 可以做状态管理比如维护一个 OAuth token 缓存而 MCP 必须把所有状态都塞进params或靠id关联请求/响应。我们曾为某硬件厂商开发 Agent他们要求所有设备控制必须走 MCP安全审计强制结果发现set_brightness(50)和get_brightness()两个调用之间设备固件根本不保存亮度值——每次get都要重新set一次才能读准这就是上下文丢失带来的逻辑断裂。第三道鸿沟错误鸿沟Skills 抛异常你能拿到完整的tracebackprint(e)就能看到是ConnectionError还是ValueError。MCP 返回的只有{jsonrpc: 2.0, error: {code: -32602, message: Invalid params}, id: 1}。这个-32602是 JSON-RPC 2.0 标准错误码意思是参数错误但到底是to字段为空还是body超长还是subject包含非法字符全靠你去翻 MCP Server 的日志。我们线上有个 Agent连续三天报error.code -32603内部错误最后发现是下游 MCP Server 的 Redis 连接池耗尽而 Agent 层连重试机制都没配——因为 Skills 场景下Python 的retrying库直接包一层就完事MCP 的重试必须自己解析id、缓存params、处理幂等性。提示不要试图用 Skills 模拟 MCP也不要指望 MCP Server 自动提供 Skills 那样的强类型校验。这是两种范式强行融合只会让错误更隐蔽。正确的做法是用 Skills 做“胶水层”把 MCP 的原始响应解析、错误映射、重试逻辑封装进去。比如写一个mcp_call(method: str, params: dict) - dictSkills它内部用httpx.AsyncClient发请求捕获httpx.TimeoutException后自动重试 3 次并把-32602错误转成InvalidParamsError异常抛出。这样上层 Agent 看到的还是 Skills 的语义底层走的却是 MCP 的协议。2.2 为什么 JSON-RPC 2.0 成为 MCP 的事实标准搜索热词里反复出现 “JSON-RPC 2.0”但它真有那么不可替代吗HTTP REST、gRPC、WebSocket哪个不行答案是在 Agent 能力扩展这个特定场景下JSON-RPC 2.0 是目前唯一能同时满足“轻量”、“双向”、“可追溯”、“易调试”四个硬指标的协议。轻量一个标准 JSON-RPC 2.0 请求最小只有{jsonrpc:2.0,method:ping,id:1}38 字节。对比 REST 的POST /api/v1/ping HTTP/1.1 headers动辄 200 字节。Agent 每次决策可能并发调用 5~8 个能力字节越少网络延迟和 token 开销越小。我们压测过100 并发下JSON-RPC 比同等功能的 REST API 平均快 17ms这 17ms 在 LLM 流式输出时就是用户感知上的“卡顿”与“丝滑”之差。双向JSON-RPC 2.0 规范明确支持notification无响应请求和batch request批量请求。这对 Agent 极其关键。比如用户说“帮我订机票并同步到日历”Agent 需要先调flight.search拿到结果后调calendar.add_event。如果用 REST你得等第一个请求 complete 才发第二个而 JSON-RPC 的 batch 可以一次性发[{jsonrpc:2.0,method:flight.search,params:{...},id:1},{jsonrpc:2.0,method:calendar.add_event,params:{...},id:2}]Server 端原子化处理返回[{jsonrpc:2.0,result:{...},id:1},{jsonrpc:2.0,result:{...},id:2}]。我们实测 batch 比串行快 2.3 倍。可追溯每个请求必须带id字段且 Server 必须在响应中回传相同id。这意味着你可以用id作为日志 trace_id把一次 Agent 决策的所有能力调用串起来。我们线上用 Loki Promtail只要搜id12345就能看到从 LLM 输出 tool call到 Skills 执行再到 MCP Server 处理最后到数据库写入的完整链路。REST 没有这个强制约定你得自己在 header 里加X-Request-ID还容易漏。易调试JSON-RPC 请求/响应全是纯 JSON用curl、Postman、甚至浏览器 console 都能直接发。我们给客户培训时第一课就是“打开 Chrome DevTools Network 标签页过滤mcp找到那个fetch请求右键 Copy as cURL粘贴到终端里改几个参数试试。” 客户当场就能验证{method:device.reboot,params:{force:true}}是否真能重启设备。而 gRPC 需要protoc编译、需要.proto文件对非开发人员门槛太高。注意JSON-RPC 2.0 的id字段类型可以是string或number但必须是null以外的值。我们踩过最大的坑是某个 MCP Server 文档写“id可选”结果实现时把idnull当作 notification 处理导致 Agent 等不到响应一直超时。后来强制约定所有生产环境id必须为number且从 1 开始递增避免字符串比较带来的兼容性问题。2.3 Skills 的本质不是“函数”而是“契约”很多人把 Skills 理解成“给 LLM 调用的 Python 函数”这是巨大误区。Skills 的本质是 Agent 框架、LLM、开发者三方之间的一份运行时契约Runtime Contract。这份契约包含四个不可协商的条款输入契约parameters字段的 JSON Schema 必须精确描述。不能写type: string就完事必须写minLength: 5, pattern: ^[a-zA-Z0-9_]$。因为 LLM 生成的参数是字符串框架要靠 Schema 做预校验。我们有个 Skillsparameters里user_id定义为type: string结果 LLM 有时生成user_id: 12345数字框架直接报错ValidationError。改成type: [string, number]后再加一行str(params[user_id])转换问题消失。输出契约returns字段必须声明返回结构。不是- dict而是{type: object, properties: {status: {type: string}, data: {type: array}}}。LLM 需要这个结构来决定下一步怎么总结。如果 Skills 返回{code: 200, msg: ok, result: [...]}但契约里没声明code字段LLM 可能忽略result直接说“操作成功”用户却看不到数据。元数据契约description字段不是给人看的是给 LLM 的 prompt 工程。必须用主动语态、短句、避免术语。错误示范“该技能用于执行用户指定的 SQL 查询操作。” 正确示范“查询数据库表返回最多 100 行结果。例如查询用户表里所有北京地区的用户。”执行契约Skills 函数体内不能有阻塞 I/O如time.sleep(1)、不能有全局状态如global COUNTER、不能有未捕获异常。因为 Agent 框架通常用 asyncio.run_in_executor 把 Skills 放在线程池跑阻塞会拖垮整个事件循环。我们曾有个 Skills 里写了os.system(ffmpeg -i ...)结果并发 10 个时CPU 100%所有 Agent 卡死。这四条契约任何一条违约都会导致 Agent 行为不可预测。而 MCP 没有这种契约——它只保证协议层通业务逻辑全在 Server 端。所以 Skills 开发者必须像写 API 接口文档一样写 Skills而不是像写脚本一样写函数。3. 核心细节解析从零搭建一个可调试的 MCP Server 与 Skills 注册中心3.1 MCP Server 实战用 FastAPI 10 分钟搭起标准兼容服务MCP Server 的核心就两件事接收 JSON-RPC 2.0 请求执行对应方法返回标准响应。我们不用那些重型框架就用最简单的 FastAPI因为它对 JSON 的支持最干净且自带 OpenAPI 文档方便调试。# mcp_server.py from fastapi import FastAPI, Request, HTTPException from pydantic import BaseModel, Field import json import logging app FastAPI(titleDemo MCP Server, version0.1) # 日志配置关键所有 MCP 调用必须记录 id 和 params logging.basicConfig(levellogging.INFO) logger logging.getLogger(__name__) class JsonRpcRequest(BaseModel): jsonrpc: str Field(..., aliasjsonrpc) method: str Field(..., aliasmethod) params: dict Field(default{}, aliasparams) id: int | str Field(..., aliasid) # 必须非 null class JsonRpcResponse(BaseModel): jsonrpc: str 2.0 result: dict | None None error: dict | None None id: int | str # 模拟一个真实能力查询设备状态 async def get_device_status(device_id: str) - dict: # 这里可以是调用真实硬件 API、数据库查询等 return { device_id: device_id, online: True, battery: 87, last_seen: 2024-05-20T10:30:00Z } # MCP 方法注册表 METHODS { device.status: get_device_status, device.reboot: lambda device_id: {status: rebooting}, } app.post(/mcp) async def handle_mcp(request: Request): try: raw_body await request.body() req_data json.loads(raw_body) # 1. 基础校验必须是 JSON-RPC 2.0 if req_data.get(jsonrpc) ! 2.0: raise HTTPException(status_code400, detailjsonrpc must be 2.0) # 2. 提取核心字段 method_name req_data.get(method) params req_data.get(params, {}) req_id req_data.get(id) if not method_name or req_id is None: raise HTTPException(status_code400, detailmethod and id are required) # 3. 记录日志带上 id 便于追踪 logger.info(fMCP call id{req_id} method{method_name} params{params}) # 4. 执行方法 if method_name not in METHODS: error_resp { code: -32601, message: fMethod {method_name} not found } return JsonRpcResponse(idreq_id, errorerror_resp) # 5. 调用实际函数捕获所有异常 try: # 这里做参数解包实际项目中要用 Pydantic 模型校验 if isinstance(params, dict) and device_id in params: result await METHODS[method_name](params[device_id]) else: result await METHODS[method_name]() return JsonRpcResponse(idreq_id, resultresult) except Exception as e: logger.error(fMCP call id{req_id} failed: {e}, exc_infoTrue) error_resp { code: -32603, message: fInternal error: {str(e)} } return JsonRpcResponse(idreq_id, errorerror_resp) except json.JSONDecodeError as e: raise HTTPException(status_code400, detailfInvalid JSON: {e}) except Exception as e: logger.error(fMCP server error: {e}, exc_infoTrue) raise HTTPException(status_code500, detailInternal server error)启动命令uvicorn mcp_server:app --host 0.0.0.0 --port 8000 --reload现在你可以用 curl 测试curl -X POST http://localhost:8000/mcp \ -H Content-Type: application/json \ -d {jsonrpc:2.0,method:device.status,params:{device_id:dev-001},id:1}你会得到标准响应{jsonrpc:2.0,result:{device_id:dev-001,online:true,battery:87,last_seen:2024-05-20T10:30:00Z},id:1}实操心得不要在 MCP Server 里做复杂业务逻辑。上面的get_device_status函数应该只负责“调用”真正的设备通信比如用pyusb读取 USB Sandisk 设备信息要放在另一个 service 层。MCP Server 的唯一职责是协议转换和错误包装。我们线上有个 MCP Server因为把数据库查询逻辑写在里面导致一次慢查询拖垮了所有能力调用。后来拆成MCP Server → RPC Client → Database Service问题立刻解决。3.2 Skills 注册中心用 YAML 定义用 Python 加载拒绝魔法字符串Skills 不是散落在代码各处的函数它必须有一个中心化的注册机制。我们不用装饰器tool那种“魔法”而是用 YAML 文件定义 Skills 元数据Python 代码只负责加载和执行。这样做的好处是产品、测试、运维都能看懂 Skills 是什么且能用 CI/CD 自动校验。skills/device.yamlname: device.status description: | 查询指定设备的实时状态包括在线状态、电量和最后在线时间。 示例查询设备 dev-001 的状态。 parameters: type: object properties: device_id: type: string description: 设备唯一标识符如 dev-001 minLength: 3 required: [device_id] returns: type: object properties: device_id: type: string online: type: boolean battery: type: integer minimum: 0 maximum: 100 last_seen: type: string format: date-timeskills/device.py# skills/device.py import asyncio import httpx # 这里是真正的业务逻辑与 MCP Server 解耦 async def get_device_status(device_id: str) - dict: # 调用我们刚写的 MCP Server async with httpx.AsyncClient() as client: resp await client.post( http://localhost:8000/mcp, json{ jsonrpc: 2.0, method: device.status, params: {device_id: device_id}, id: 1 } ) data resp.json() if error in data: raise RuntimeError(fMCP error: {data[error][message]}) return data[result]skills/registry.py核心注册逻辑import yaml import importlib from pathlib import Path from typing import Dict, Callable, Any # 全局 Skills 注册表 SKILLS_REGISTRY: Dict[str, Dict[str, Any]] {} def load_skills_from_dir(skills_dir: str skills): 从 skills/ 目录加载所有 Skills skills_path Path(skills_dir) for yaml_file in skills_path.glob(*.yaml): with open(yaml_file, r, encodingutf-8) as f: skill_def yaml.safe_load(f) name skill_def[name] # 动态导入对应的 Python 模块 module_name fskills.{yaml_file.stem} try: module importlib.import_module(module_name) func getattr(module, skill_def.get(function, main)) SKILLS_REGISTRY[name] { description: skill_def[description].strip(), parameters: skill_def[parameters], returns: skill_def.get(returns, {}), function: func, module: module_name, file: str(yaml_file) } print(f✅ Loaded Skill: {name}) except Exception as e: print(f❌ Failed to load {name}: {e}) # 初始化 load_skills_from_dir() def get_skill(name: str) - Dict[str, Any]: 根据名称获取 Skill 定义 return SKILLS_REGISTRY.get(name) def list_all_skills() - list: 列出所有已注册 Skills return list(SKILLS_REGISTRY.keys())现在在你的 Agent 主程序里只需from skills.registry import SKILLS_REGISTRY, get_skill # Agent 框架调用 Skills 的伪代码 async def execute_skill(skill_name: str, params: dict): skill get_skill(skill_name) if not skill: raise ValueError(fSkill {skill_name} not found) # 这里可以用 Pydantic 模型校验 params # 然后调用 skill[function](**params) return await skill[function](**params)注意事项YAML 文件里的function字段必须与 Python 文件中的函数名一致。我们强制约定每个 YAML 文件对应一个同名 Python 文件且主函数叫main。这样importlib.import_module就不会出错。另外parameters的 JSON Schema 必须严格我们用jsonschema.validate在load_skills_from_dir里做预校验如果 Schema 无效直接报错退出不加载。3.3 MCP 与 Skills 的协同一个真实调试场景还原假设用户问“我的 USB Sandisk 设备 dev-001 电量只剩 10% 了能帮我重启一下吗”Agent 的决策流会是LLM 输出 tool call{name: device.status, parameters: {device_id: dev-001}}Agent 框架查SKILLS_REGISTRY找到device.status调用skills.device.get_device_status(dev-001)get_device_status内部发 JSON-RPC 请求到http://localhost:8000/mcpMCP Server 收到请求解析methoddevice.status调用get_device_status函数get_device_status函数里用pyusb读取 USB 设备寄存器得到电量 10%MCP Server 返回{result: {device_id: dev-001, online: true, battery: 10, ...}, id: 1}Agent 框架收到 Skills 返回LLM 看到电量低输出下一个 tool call{name: device.reboot, parameters: {device_id: dev-001}}流程重复但这次调用device.reboot方法整个过程你在日志里会看到清晰的 traceINFO: MCP call id1 methoddevice.status params{device_id: dev-001} INFO: MCP call id2 methoddevice.reboot params{device_id: dev-001}而如果你只用 Skills没有 MCPget_device_status就得直接写pyusb代码一旦 USB 设备拔掉Skills 报USBErrorAgent 就崩了。有了 MCPpyusb的错误被 MCP Server 捕获转成标准{error: {code: -32603, message: USB device not found}}Agent 框架可以优雅降级告诉用户“设备未连接”。这就是两条路线的价值Skills 让 Agent 有“脑子”MCP 让它有“手脚”而协同机制就是那根把脑子和手脚连起来的“脊髓”。4. 实操过程详解从本地调试到生产部署的全链路避坑指南4.1 本地调试Chrome DevTools Playwright MCP 的黄金组合搜索热词里有 “chrome devtools mcp”、“playwright mcp”这不是巧合。Chrome DevTools 是目前最强大的 MCP 调试器因为它原生支持 WebSocket 和 JSON-RPC。Playwright 则是把浏览器变成 MCP Server 的最佳胶水。第一步启用 Chrome 的 MCP 调试模式在 Chrome 启动时加参数chrome --remote-debugging-port9222 --remote-allow-origins*然后访问http://localhost:9222/json你会看到类似[{ description: , devtoolsFrontendUrl: /devtools/inspector.html?wslocalhost:9222/devtools/page/..., faviconUrl: , id: D0F3E5A1C2B4..., title: New Tab, type: page, url: about:blank, webSocketDebuggerUrl: ws://localhost:9222/devtools/page/D0F3E5A1C2B4... }]这个webSocketDebuggerUrl就是 Chrome 的 MCP endpoint你可以用任何 JSON-RPC 客户端连它。第二步用 Playwright 暴露浏览器能力为 MCP ServerPlaywright 的browser.contexts[0].new_page()创建的页面可以通过page.evaluate执行任意 JS。我们写一个 Playwright MCP Server# playwright_mcp_server.py from playwright.sync_api import sync_playwright from fastapi import FastAPI, Request import json app FastAPI() with sync_playwright() as p: browser p.chromium.launch(headlessFalse) page browser.new_page() app.post(/mcp) def handle_playwright_mcp(request: Request): req_data request.json() method req_data[method] params req_data.get(params, {}) try: if method browser.navigate: page.goto(params[url]) result {status: success, url: page.url} elif method browser.screenshot: path fscreenshot_{int(time.time())}.png page.screenshot(pathpath) result {path: path} else: result {error: fUnknown method {method}} return {jsonrpc: 2.0, result: result, id: req_data[id]} except Exception as e: return {jsonrpc: 2.0, error: {code: -32603, message: str(e)}, id: req_data[id]}现在你的 Agent 可以调用browser.navigate去打开网页调用browser.screenshot截图——所有操作都在真实浏览器里发生且通过标准 MCP 协议。这才是 “playwright mcp” 的真实含义不是 Playwright 支持 MCP而是用 Playwright 构建 MCP Server。实操心得本地调试时永远开启 Chrome 的--auto-open-devtools-for-tabs参数。这样每个新 tab 自动弹出 DevTools你可以在 Console 里直接fetch测试 MCP 请求比写 Python 脚本快十倍。我们团队的标准流程是先在 Chrome Console 里用fetch调通再写 Skills 封装最后集成到 Agent。4.2 生产部署Nginx uWSGI 的 MCP Server 高可用方案本地跑通不等于生产可用。MCP Server 必须扛住高并发、低延迟、7x24 小时运行。我们线上用的是 Nginx uWSGI 组合而非uvicorn直连。uwsgi.ini[uwsgi] module mcp_server:app master true processes 4 threads 2 socket /tmp/mcp.sock chmod-socket 664 vacuum true die-on-term true logto /var/log/mcp/uwsgi.lognginx.conf片段upstream mcp_backend { server unix:///tmp/mcp.sock; } server { listen 8000; server_name _; location /mcp { include uwsgi_params; uwsgi_pass mcp_backend; # 关键设置超时避免长连接占满 uwsgi_read_timeout 10; uwsgi_send_timeout 10; uwsgi_connect_timeout 5; } # 添加 MCP 健康检查端点 location /health { return 200 OK; } }为什么不用 uvicorn因为 uvicorn 是 ASGI 服务器适合开发uWSGI 是工业级 WSGI/ASGI 服务器有进程管理、内存监控、优雅重启等企业级特性。我们线上有个 MCP Server每天处理 200 万次调用用 uvicorn 时内存泄漏导致每 12 小时必须重启换成 uWSGI 后稳定运行 47 天无重启。注意MCP Server 的/mcp接口必须是POST且Content-Type必须是application/json。Nginx 默认会缓存 GET 请求所以一定要在location /mcp里加limit_except POST { deny all; }防止恶意 GET 请求打爆 Server。4.3 Skills 的 CI/CD用 GitHub Actions 自动校验 Skills 质量Skills 是代码就必须走 CI/CD。我们用 GitHub Actions 做三件事YAML Schema 校验Python 语法检查基础功能测试.github/workflows/skills-ci.ymlname: Skills CI on: push: paths: - skills/** jobs: validate: runs-on: ubuntu-latest steps: - uses: actions/checkoutv4 - name: Set up Python uses: actions/setup-pythonv4 with: python-version: 3.11 - name: Install dependencies run: | pip install pyyaml jsonschema pytest - name: Validate YAML schemas run: | for yaml_file in skills/*.yaml; do echo Validating $yaml_file # 检查是否符合 MCP Skills schema python -c import yaml, jsonschema, sys with open($yaml_file) as f: data yaml.safe_load(f) schema { type: object, required: [name, description, parameters], properties: { name: {type: string}, description: {type: string}, parameters: {type: object} } } jsonschema.validate(instancedata, schemaschema) done - name: Run pytest run: pytest skills/tests/ -vskills/tests/test_device.pyimport pytest from skills.device import get_device_status pytest.mark.asyncio async def test_get_device_status(): # 测试正常情况 result await get_device_status(dev-001) assert result[device_id] dev-001 assert online in result # 测试异常情况 with pytest.raises(RuntimeError): await get_device_status(invalid-device-id)这个 CI 流程确保每次 PR 提交Skills 的 YAML 定义、Python 逻辑、基础功能都经过验证。我们曾拦截过一个 PRYAML 里required字段写成了[device_id, user_id]但 Python 函数只接受device_idCI 直接失败避免了上线后 LLM 传错参导致的崩溃。4.4 安全加固MCP 的鉴权与 Skills 的沙箱生产环境安全是底线。MCP Server 和 Skills 都必须加固。MCP Server 鉴权我们不用 JWT太重。用最简单的 API Key IP 白名单# 在 mcp_server.py 的 handle_mcp 函数开头加 api_key request.headers.get(X-API-Key) if api_key ! your-secret-key-here: raise HTTPException(status_code403, detailForbidden) client_ip request.client.host if client_ip not in [10.0.0.1, 10.0.0.2]: # 白名单 raise HTTPException(status_code403, detailIP not allowed)Skills 沙箱Skills 函数不能执行任意代码。我们用RestrictedPython库限制from RestrictedPython import compile_restricted, compile_restricted_exec from RestrictedPython.Guards import ( guarded_iter_unpack_sequence, guarded