本次迁移将网关/通道/工具/技能/脚本与协议资源集中到新结构,统一路径常量与脚本转发机制,减少顶层噪音并保证运行与测试行为一致。 Made-with: Cursor
22 KiB
本地 MCP 工具开发与接入指南(stdio / JSON-RPC)
本文档用于指导你在本地编写 MCP Server,并接入当前系统的 MCP 市场。
当前系统对 MCP 的主流程已统一为标准协议:
initializenotifications/initializedtools/listtools/call
兼容说明:系统内部仍保留对旧
op风格消息的回退兼容,但不建议新工具继续使用旧协议。
1. 你要实现什么
你写的本地工具不是直接写成 ToolSpec,而是写成一个 MCP Server 子进程,通过标准输入输出(stdio)和系统通信。
系统会:
- 启动你的进程(
entry_command + entry_args) - 发送
initialize - 接收
tools/list - 在用户调用时发送
tools/call
2. 最小可运行示例(Python)
保存为 mcp_echo_server.py:
from __future__ import annotations
import json
import sys
from typing import Any
def ok(rid: Any, result: dict[str, Any]) -> None:
sys.stdout.write(json.dumps({"jsonrpc": "2.0", "id": rid, "result": result}, ensure_ascii=False) + "\n")
sys.stdout.flush()
def err(rid: Any, code: int, message: str) -> None:
sys.stdout.write(
json.dumps(
{"jsonrpc": "2.0", "id": rid, "error": {"code": code, "message": message}},
ensure_ascii=False,
)
+ "\n"
)
sys.stdout.flush()
for raw in sys.stdin:
raw = raw.strip()
if not raw:
continue
try:
req = json.loads(raw)
except Exception:
continue
rid = req.get("id")
method = str(req.get("method") or "")
params = req.get("params") if isinstance(req.get("params"), dict) else {}
if method == "initialize":
ok(
rid,
{
"protocolVersion": "2024-11-05",
"capabilities": {"tools": {}},
"serverInfo": {"name": "echo-mcp", "version": "0.1.0"},
},
)
continue
if method == "notifications/initialized":
# 通知类消息不需要返回
continue
if method == "tools/list":
ok(
rid,
{
"tools": [
{
"name": "echo",
"description": "回显输入文本。",
"inputSchema": {
"type": "object",
"properties": {
"text": {"type": "string", "description": "要回显的文本"}
},
"required": ["text"],
"additionalProperties": False,
},
}
]
},
)
continue
if method == "tools/call":
tool_name = str(params.get("name") or "")
arguments = params.get("arguments") if isinstance(params.get("arguments"), dict) else {}
if tool_name != "echo":
err(rid, -32601, f"unknown tool: {tool_name}")
continue
text = str(arguments.get("text") or "")
ok(rid, {"content": [{"type": "text", "text": text}]})
continue
err(rid, -32601, f"method not found: {method}")
3. 在管理台中接入
3.0 已安装 MCP 从库里消失时(换库 / 表被清空)
mcp_server_registry 存在默认 SQLite(见 data/ai_ops.sqlite)中。若列表变成 0 条,可在仓库根执行 python scripts/seed_mcp_registry.py,从 data/mcp_registry.seed.json 写回示例条目(含 local-echo 与 mcp-context7,可按需编辑该 JSON 再执行)。写回后在管理台对各服务执行 Health → Sync Tools;若曾换过 OPS_ASSISTANT_DB_PATH,请确认网关与脚本指向同一库文件。
3.1 表单安装(推荐)
在 MCP 页面填:
source_type: 可自定义用于记录(如pypi)source_ref: 自定义来源标识(如local-echo)entry_command:pythonentry_args:D:/path/to/mcp_echo_server.py
然后执行:
InstallHealthSync Tools
工具数量 > 0 即接通成功。
3.2 JSON 安装
单条(在 Plugins 「3」MCP 安装 的 Install from JSON 中粘贴,或作 array 的其中一个元素):
{
"source_type": "pypi",
"source_ref": "local-echo",
"server_id": "local-echo",
"version": "",
"entry_command": "python",
"entry_args": ["D:/project/chatgpt/examples/mcp_echo_server.py"],
"required_permissions": [],
"risk_level": "low",
"enabled": true,
"timeout_s": 30
}
批量:以下三种写法 Install from JSON 都支持,会逐条 preflight + install(任一条失败会记结果并继续下一条,最后看结果 JSON):
- 数组:
[ { 上面一条的字段… }, { … } ] - 对象包一层
servers(与data/mcp_registry.seed.json、导出文件一致):{ "servers": [ { "source_type": "npm", "source_ref": "@upstash/context7-mcp", "server_id": "mcp-context7", "version": "", "entry_command": "npx", "entry_args": ["-y", "@upstash/context7-mcp"], "env_schema": {}, "required_permissions": [], "risk_level": "medium", "enabled": true, "timeout_s": 60, "dry_run": false } ] } - 带
payload的单条(如examples/mcp_install_context7.json):{ "payload": { "source_type": "npm", "source_ref": "…", "server_id": "…" } }
路径占位符 __REPO_ROOT__/… 在管理台安装 / preflight 中会与 scripts/seed_mcp_registry.py 一样展开为仓库根下的绝对路径(如 mcp-echo 的脚本路径、filesystem 的目录根、sqlite 的库文件路径)。若不用占位符,可直接写本机绝对路径。
导出与自动备份
- 在 【4】已安装 MCP 服务 使用 Export JSON (download),可下载当前库中全部已安装 MCP 的可重装 JSON(
servers包 +exported_at)。 - 每次 安装、重装、卸载且删除库记录、Delete 成功后,会刷新
oclaw/_local/mcp_registry_migrated.json(与导出内容同结构,便于换库/换机后把文件粘回 Install from JSON 或python scripts/seed_mcp_registry.py path/to/file.json注意 seed 会跑 npm/pypi 安装步骤,与dry_run等字段一致)。该文件建议加入.gitignore(如未忽略),避免本机差异被误提交;密钥仍放在oclaw/_local/mcp_local.env等环境变量,不在此 JSON 中。
3.3 MCP 工具线侧策略(上送压缩与惩罚)
在管理台 Plugins(插件) 页中的 「线侧策略」 折叠区块(【6】全局参数、【7】已安装工具)可配置发往 LLM 的 OpenAI 格式 tools[] 的分层压缩、全局闲置惩罚,以及按完整工具名 mcp__{server_id}__{tool_name} 的策略(与模型 base_url 解耦时,将 wire_policy 设为 always)。【7】 中每个工具的等级为数字输入框(任意整数 1–9998;留空表示未配置;0 与留空语义不同,见下表)。
【7】表格筛选与专家列(管理台)
- 表头第二行为各列筛选输入(子串匹配,不区分大小写):
server、tool、wire_name、专家、count(上下界)、last_ts、惩罚/解封说明、策略等级(上下界)。分页条数按筛选后结果计算。 - 专家列由本页当前 MCP 专家绑定草稿(
mapping)与后端返回的available_specialists合并推导:某server_id出现在哪些专家的绑定列表中,即显示为逗号分隔的专家 id;可按专家子串筛选。勾选「仅已勾选」时只显示当前勾选的行(勾选集合在筛选、翻页间保留)。 - 同页 【8】专家 MCP 绑定看板(自动):按当前草稿与已安装 MCP 各服务的
tools列表,汇总每个专家绑定的 MCP 个数 与 tool 条数(各已绑定 server 的tools长度之和);专家集合随available_specialists与mapping中的键自动扩展,无需写死。 - 【9】MCP 专家绑定(编辑) 为原绑定编辑区(勾选、反向视图、保存);与【8】看板联动,改绑定后看板即时刷新(无需单独保存看板)。
持久化(SQLite app_setting)
| 键 | 含义 |
|---|---|
mcp_tool_wire_admin_config |
JSON:全局参数 + wire_policy(inherit / always / never)、penalty_disable 等 |
mcp_tool_wire_tool_policies |
JSON:{ "mcp__sid__tool": 等级 } |
mcp_tool_wire_penalty_state |
JSON:各工具惩罚状态机(phase、omit_until、wave_ts、kind),由运行时维护,一般无需手改 |
wire_policy
inherit:与原先一致,默认在 DashScope 兼容 URL 上启用线侧策略;其它环境变量OPS_MCP_WIRE_*仍可作为默认值来源。always:不依赖 URL,始终启用分层与惩罚逻辑(适合非 DashScope 网关也要控 payload)。never:关闭分层/惩罚逻辑;等级9999永久封禁仍会过滤该工具(不上送)。
按工具等级(mcp_tool_wire_tool_policies)
| 配置 | 库中是否存在键 | 行为 |
|---|---|---|
未配置(管理台留空 / GET 中 policy_level 为 null、policy_in_db 为 false) |
否 | 自动走全局:参与用量排名与分层压缩;适用全局闲置小时与罚时长;可被 Top N 全量豁免全局闲置惩罚。新安装 MCP 在 Sync Tools 后出现新 wire_name,默认即为此状态,无需手工登记。 |
显式 0 |
是 | 不参与全局闲置 omission;仍参与用量分层。与「未配置」不同。 |
显式 1~9998 |
是 | 与 Top N 无关:距上次成功调用超过 N×10 分钟 视为闲置,进入罚时 N×10 分钟 的上送 omission;罚满后需再次闲置达到阈值才会再罚(状态与 last_ts / kind 对齐)。 |
显式 9999 |
是 | 永久从线侧 tools[] 中移除(彻底封禁)。 |
生效优先级(同一工具上的概念顺序,便于排障)
9999永久封禁(若已写入mcp_tool_wire_tool_policies):在组装tools[]的较早阶段即剔除,不进入后续分层与动态惩罚状态机。- 显式
1~9998:走按工具闲置/罚分钟逻辑,不享受 Top N 对「全局惩罚」的豁免。 - 显式
0:跳过全局闲置 omission,仍走压缩档位。 - 未配置:走全局线侧逻辑(含全局闲置与 Top N 豁免等),由
prepare_openai_tools_for_llm_api与mcp_tool_wire_admin_config/ 环境变量共同决定。
wire_policy=never 时关闭分层与动态惩罚,但 9999 仍会过滤 对应工具。
全局闲置小时、罚时长(分钟)、Top N 全量、medium 档位等,在 【6】 中可调;未写入 app_setting 的项继续沿用环境变量(见仓库根 data/mcp_local.env.example 中 OPS_MCP_WIRE_*)。
Admin HTTP API(需 admin:tenant:write,与 MCP 安装类接口一致)
GET /admin/api/mcp/tool-wire— 返回合并后的config、当前policies、penalty_state,以及已安装 MCP 工具列表(每条含policy_level:null表示未在库中配置,policy_in_db标明是否持久化过)及惩罚/解封说明。POST /admin/api/mcp/tool-wire/config— 保存全局参数(部分字段可增量合并)。POST /admin/api/mcp/tool-wire/policies— body:{ "policies": { "mcp__...": 整数等级 }, "clears": ["mcp__...", ...](可选) }。先按clears从已存策略中删除键(用于管理台留空后恢复「未配置」),再合并policies。POST /admin/api/mcp/tool-wire/policies/batch— body:{ "level": 等级, "wire_names": ["mcp__...", ...] },批量写入策略。
实现代码:oclaw/platform/llm/tool_wire_policy.py;在发 Chat Completions 前由 prepare_openai_tools_for_llm_api 应用。
4. 专家分配(谁能用这个工具)
当前系统支持“按专家组”分配 MCP 工具可见性:
- 在 MCP 页面
MCP specialists勾选可用专家 - 保存后立即生效(持久化在数据库)
注意:当前是“专家级分配”,不是“单工具级分配”。
5. 编写规范(强烈建议)
-
stdout 只输出 JSON-RPC 响应行
日志请写到stderr,否则容易触发protocol_mismatch。 -
tools/list返回稳定 schema
建议所有参数都写明type与required,并设additionalProperties: false。 -
tools/call出错要可解释
优先通过 JSON-RPCerror返回明确错误信息。 -
避免长时间阻塞
长任务应拆分或优化,否则会出现mcp_runtime_timeout。
6. 常见错误与排查
-
mcp_runtime_timeout
含义:子进程超时未返回。
排查:先本地单独运行 server,确认单次调用耗时;必要时调大timeout_s。 -
mcp_runtime_protocol_mismatch
含义:收到的不是 JSON-RPC 响应。
排查:检查是否把日志打印到了 stdout。 -
mcp_runtime_bad_json
含义:输出不是合法 JSON。
排查:检查编码、换行、对象结构。 -
mcp_tools_list_invalid
含义:tools/list返回结构不符合预期。
排查:确认返回result.tools为数组,元素包含name、inputSchema。
7. 建议开发流程
- 本地先用脚本手动跑通 JSON-RPC
- 管理台安装(建议先
dry_run) - 执行
Health、Sync Tools - 在
Check Installed做批量体检 - 按专家映射开放给目标专家
8. 与原有内置工具关系
MCP 工具是增量能力,不会替代原有内置工具体系。
最终都走统一 ToolExecutor 执行链(策略、超时、审计一致)。
Local MCP Server Guide (stdio)
Goal
Write local tools as a standard MCP server over stdio (JSON-RPC), then connect them from Admin MCP Market.
This project now uses MCP standard flow in runtime:
initializenotifications/initializedtools/listtools/call
Legacy custom op messages are only compatibility fallback.
Minimal Python MCP Server
Save as mcp_echo_server.py:
from __future__ import annotations
import json
import sys
from typing import Any
def _ok(rid: Any, result: dict[str, Any]) -> None:
sys.stdout.write(json.dumps({"jsonrpc": "2.0", "id": rid, "result": result}, ensure_ascii=False) + "\n")
sys.stdout.flush()
def _err(rid: Any, code: int, message: str) -> None:
sys.stdout.write(
json.dumps(
{"jsonrpc": "2.0", "id": rid, "error": {"code": code, "message": message}},
ensure_ascii=False,
)
+ "\n"
)
sys.stdout.flush()
for line in sys.stdin:
line = line.strip()
if not line:
continue
try:
req = json.loads(line)
except Exception:
continue
rid = req.get("id")
method = str(req.get("method") or "")
params = req.get("params") if isinstance(req.get("params"), dict) else {}
if method == "initialize":
_ok(rid, {"protocolVersion": "2024-11-05", "capabilities": {"tools": {}}, "serverInfo": {"name": "echo-mcp", "version": "0.1.0"}})
continue
if method == "notifications/initialized":
# Notification has no response.
continue
if method == "tools/list":
_ok(
rid,
{
"tools": [
{
"name": "echo",
"description": "Echo input text.",
"inputSchema": {
"type": "object",
"properties": {"text": {"type": "string"}},
"required": ["text"],
"additionalProperties": False,
},
}
]
},
)
continue
if method == "tools/call":
name = str(params.get("name") or "")
args = params.get("arguments") if isinstance(params.get("arguments"), dict) else {}
if name != "echo":
_err(rid, -32601, f"unknown tool: {name}")
continue
text = str(args.get("text") or "")
_ok(rid, {"content": [{"type": "text", "text": text}]})
continue
_err(rid, -32601, f"method not found: {method}")
Run manually (sanity check):
python mcp_echo_server.py
Then send one JSON-RPC request line from stdin to verify.
Install in Admin MCP Market
For local Python script:
source_type:pypi(or any source type you use for bookkeeping)source_ref: custom label (for examplelocal-echo)entry_command:pythonentry_args:<absolute-or-relative-path-to-script>
Example JSON install payload:
{
"source_type": "pypi",
"source_ref": "local-echo",
"server_id": "local-echo",
"version": "",
"entry_command": "python",
"entry_args": ["D:/project/chatgpt/examples/mcp_echo_server.py"],
"required_permissions": [],
"risk_level": "low",
"enabled": true,
"timeout_s": 30
}
After install:
HealthSync Tools- Verify tool count > 0
@modelcontextprotocol/server-filesystem 与网关工作区
官方 @modelcontextprotocol/server-filesystem 只在进程启动时把命令行里列出的目录当作可访问根;多装一个路径就要多传一个 argv,否则 list_directory 等工具无法列出该目录。
本仓库在启动该 MCP 时会自动合并与内置工作区一致的路径来源,并去重后追加到 entry_command + entry_args 之后(不改变你在管理台填写的主根,只追加额外根):
| 来源 | 说明 |
|---|---|
OPS_WORKSPACE_EXTRA_ROOTS |
环境变量,| 分隔 |
OPS_MCP_FILESYSTEM_EXTRA_ROOTS |
环境变量或 SQLite settings 表同名键,仅影响该 MCP |
| Admin「工作区路径」 | 当前用户聊天会话(ui_session_owner 绑定的 session_id)对应账号的 extra_roots(| 拆分);不会合并其他用户。若 ui_session_owner 行缺失,会从请求里携带的 tenant_id / user_id(metadata)再拉一份同一条 allowlist,与内置 resolve_workspace_path 及 MCP 追加 argv 对齐。 |
| Windows 路径 | 在网关侧与 MCP argv 中会对路径作规范化;若仍报「无权限」或子进程报路径不在根下,可对比管理台中保存的「绝对路径」与资源管理器里实际盘符/大小写是否一致,修改工作区后对该 MCP Health → Sync Tools。 |
与 allow_any_path 的关系:管理台里的 allow_any_path 只影响网关内置工具(走 resolve_workspace_path 的读文件、glob、run_command 等),相当于在 Python 侧跳过「必须在 workspace 根或 extra_roots 下」的检查。官方 server-filesystem 不认这个字段:子进程只认启动时写在 argv 里的具体目录列表,没有「允许任意路径」的等价开关,因此单靠 allow_any_path: true 不会把 D:\download 等路径自动加进 MCP。要让 MCP 列到这些目录,请把它们写进 extra_roots(或 OPS_WORKSPACE_EXTRA_ROOTS / OPS_MCP_FILESYSTEM_EXTRA_ROOTS),再 Health → Sync Tools。
运维注意:同一网关进程内,不同用户对话会各自 materialize 一套 MCP 工具绑定(argv 含该用户 extra_roots + 全局 env)。管理台 Health / Sync Tools 无用户会话上下文,此时仅合并 环境变量与 settings,不含任一用户的 DB extra_roots。
修改环境或 DB 后,请对相应 MCP 执行 Health → Sync Tools(或重启网关),以便新进程带上更新后的 argv。
Tool Result Format Recommendations
For tools/call result:
- success: return
{"content":[{"type":"text","text":"..."}]} - failure: return JSON-RPC
errororresultwithisError=true
Keep responses deterministic and JSON-serializable.
Common Errors
-
mcp_runtime_timeout
Server did not answer in time. Check blocking calls, raisetimeout_s, or optimize startup. -
mcp_runtime_protocol_mismatch
Output is not JSON-RPC response line. Ensure stdout only emits JSON-RPC lines (move logs to stderr). -
mcp_runtime_bad_json
Response line is malformed JSON. Validate serialization and newline framing. -
mcp_tools_list_invalid
tools/listdid not return validtoolsarray.
通识工具库与 Cursor / Claude Code / oclaw(能力对齐说明)
已能覆盖的常见编码助手能力:仓库读写与搜索(内置 workspace + MCP filesystem)、Git 本地与 GitHub 远端、网页抓取与浏览器自动化(fetch / playwright)、会话库 SQLite、日历与时间、PDF、顺序思考与 memory MCP 等。
单靠 MCP 无法等价的部分:IDE 内 LSP 实时红线(Cursor 编辑器集成)、oclaw 式 ACP 外接 Claude Code/Codex 子进程(需单独编排/通道产品化)。
Context7(库文档时效)
- 作用:按库名/版本拉取较新的官方文档片段,减少「API 记错版本」类幻觉。
- 安装:
python scripts/install_mcp_context7.py,或管理台POST /admin/api/mcp/install使用examples/mcp_install_context7.json中的payload。 - 密钥:在
oclaw/_local/mcp_local.env(推荐)或data/mcp_local.env(兼容)设置CONTEXT7_API_KEY(见 context7.com/dashboard)。两处都存在时同键以oclaw/_local/mcp_local.env为准(覆盖data中的同键)。未自定义OPS_MCP_ENV_ALLOWLIST时,网关默认 allowlist 已包含CONTEXT7_API_KEY(见oclaw/runtime/operations/mcp_env.py);若你自定义了 allowlist,请手动追加该键。 - 装完后:
Health→Sync Tools→ 将mcp-context7加入通识 specialist 的 MCP 绑定(若脚本已成功 Sync,会自动追加)。
通识侧终端能力(run_command)
与 Claude Code「在仓库里跑命令」类似的能力来自内置 run_command,但通识 lane 需同时满足:
- 环境
OPS_ENABLE_RUN_COMMAND=1(见oclaw/tools/catalog.py与oclaw/tools/experts/workspace/shell_tools.py门控)。 - 仅在 可信仓库 / 内网 开启;否则易误执行高危命令。
Postgres / Linear / Slack / Sentry 等
按实际业务栈再装对应 MCP 即可;无相关系统则不必安装,避免工具膨胀与误选。
Multi-specialist Assignment
MCP tools are assigned in Admin UI by specialist mapping.
Only selected specialists can see/use MCP tools at runtime.