dsh-mcpp
把 Python 版 mcpp 网关(一个 MCP 聚合网关)完整重实现为 DSH 宿主侧 Cordis 插件。旧服务退场后,127.0.0.1:9020 上回答 /api/*、/admin 与 /{toolset}/{naming}/mcp 的就是这个插件。
对客户端是 drop-in:同一批路径、同一个 config.yaml、同一个 auth.token。Claude Code / Cursor / OpenCode / Codex 等现有配置一行都不用改。
对 dsh 自己是原生的(v0.2):聚合工具以 mcp__<toolset>__<tool> 的公开名直接注册进 ctx.tools,调用直连上游(不再经第二层 MCP 转发);默认全关,在 dsh 设置页「MCP 工具」里按 toolset / 单工具勾选后才注册给主 agent。管理页以同源 iframe 进 dsh-better-sidebar 侧栏「MCP 网关」页签。其他插件可经 ctx.mcpp 服务直接取目录、调用任意已暴露工具。
它做什么
对外 HTTP 网关(v0.1,保留)
- 读取旧服务的 YAML(
upstreams/expose/server_name/auth/clients),把上游 MCP 服务器(Streamable HTTP 与 stdio 子进程)聚合成若干 toolset,每个 toolset 按多种 naming mode 暴露:/{toolset}/claude/mcp→mcp__{toolset}__{tool}/{toolset}/cursor/mcp→mcp_{toolset}_{tool}/{toolset}/opencode/mcp→{toolset}_{tool}/{toolset}/coder/mcp→{toolset}__{tool}/{toolset}/codex/mcp→{toolset}::{tool}/{toolset}/default/mcp→{tool}(另有 legacy/mcp)
- 工具面改写:
as:改名、hide: true隐藏、toolset分组、描述里`upstream/tool`反引号交叉引用按客户端格式展开、params的map_from/enum/preset/hidden+default/required覆盖。 - 上游密钥池:轮转 + 401/403/429 标记不健康 +
/api/keys管理。 - 管理接口与页面:
/api/config、/api/tools、/api/upstreams、/api/upstreams/{n}/test、/api/toolsets、/api/expose、/api/client-presets、/api/keys、/api/calls、/api/add-server,以及原样的/admin页面。 - 网关 bearer 鉴权(header /
?token=/mcpp_tokencookie),clients:可逐 naming 关停端点。
dsh 原生工具面(v0.2)
- 选择文档
<DSH_HOME>/storages/mcpp/selection.json:{ "version": 1, "enabled": ["search/searxng_search", …] },默认全关;保存即差量注册/注销,无需重启。 - 每个被选中的工具注册一个
ToolDefinition:parameters= 变换后的 inputSchema,execute= 参数变换 → 上游tools/call(isError抛错、content 投影为文本、按上游read_timeout+15s设timeoutMs)。 - 同名冲突(例如另配了 mcp-client 指向同一上游)只记入设置页的「注册失败」列表,不影响其它工具。
- 同源路由(走 dsh 宿主的 webServer,受 dsh 的浏览器信任围栏保护,不暴露网关 token):
GET /plugins/mcpp/admin— 管理页(注入 API base / 公开 origin / token)/plugins/mcpp/api/*— 与 9020 同一套进程内 admin handlerGET /plugins/mcpp/ui/catalog、POST /plugins/mcpp/ui/selection、POST /plugins/mcpp/ui/sync
- 浏览器半区:侧栏「MCP 网关」页签(iframe 管理页)+ 设置页「MCP 工具」分区(勾选/保存/重新同步/失败原因)。
ctx.mcpp服务:catalog()/call(name, args, signal?)/upstreams()/toolsets();调用不要求工具被选中。- 预设作用域工具面(v0.3):
dsh-mcpp/scoped入口让 agent preset 行按tools/toolsets/exclude过滤,只把选中的mcp__*工具注册进该预设自己的 scope(主 agent 看不到),调用复用宿主网关的同一批上游连接,不再走第二道 MCP。
代码结构
index.mjs bundle 入口(re-export src/plugin.ts)
scoped.mjs 预设作用域入口(re-export src/scoped.ts)
src/plugin.ts Cordis 插件:Config + apply(网关、原生工具、同源路由、ctx.mcpp 服务)
src/scoped.ts 预设作用域插件:按 tools/toolsets/exclude 把工具注册进调用方 scope
src/bootstrap.ts composition root:读配置、建上游/密钥池、预热 stdio、reload 路径
src/server.ts 9020 的 node:http 监听:路由表、鉴权、cookie、静态 admin 页
src/gateway.ts MCP JSON-RPC 协议层(initialize / tools/list / tools/call / …)
src/admin.ts /api/* 全部管理接口
src/native-tools.ts ToolDefinition 适配器 + 按 publicName 的代际差量注册
src/selection.ts 选择文档原子读写(默认全关)
src/dsh-routes.ts /plugins/mcpp/** 同源路由(管理页 + admin API + ui API)
src/dsh.ts dsh 宿主服务的最小本地类型(tools/webServer/connection/ctx)
src/service.ts ctx.mcpp 服务与 catalog 构建
src/config.ts YAML 模型、默认值、交叉校验、序列化
src/transform.ts 下游工具面与参数变换
src/upstream-http.ts Streamable HTTP 上游(握手、session 恢复、重试一次)
src/upstream-stdio.ts stdio 上游(两种帧、超时隔离、EOF 重生成)
src/keypool.ts 密钥轮转池
src/presets.ts naming modes / connect modes / client 目录
src/types.ts 共享契约
src/client/ 浏览器半区(侧栏页签 + 设置页分区)
static/index.html 旧服务管理页(最小改动以支持同源注入)
scripts/run-standalone.mjs 脱离 dsh 直接起网关(验证用)
scripts/parity/ 与旧服务响应对齐的抓取/比较工具
test/ node --test 单测与端到端测试
零运行时依赖:产物只 import 宿主提供的 @deepseek-ai/schemastery;js-yaml 被打进 bundle,客户端 bundle 只依赖 module table 里的 react。
构建与测试
便携 Node 24(D:\softwares\node-v24.21.0-win-x64)下:
cd L:\dsh\.local-deploy\mcpp-plugin
node --test --experimental-strip-types test/*.test.mjs # 单测 + 端到端
node ../../node_modules/tsdown/dist/run.mjs --config tsdown.config.mjs --config-loader native # lib/index.js + lib/client.js
node L:\dsh\node_modules\typescript\bin\tsc -p tsconfig.json # 类型检查
脱离 dsh 验证(候选端口 9021,不打扰 9020 上的旧服务):
node --experimental-strip-types scripts/run-standalone.mjs C:\Users\joe\Documents\repo\skill\mcpp\config.yaml 9021
与旧服务逐端点对齐:
node scripts/parity/capture-golden.mjs # 从旧服务抓 golden(token 运行时读取,不落盘)
node scripts/parity/compare.mjs --candidate http://127.0.0.1:9021 # 新旧逐端点对比
node scripts/parity/verify-against-golden.mjs --candidate http://127.0.0.1:9020 # 旧服务退役后只对 golden 验证
node --experimental-strip-types scripts/verify-live.mjs --base http://127.0.0.1:9020 # 真实 tools/call
已验证(2026-11-08):
- v0.1:207/207 单测与端到端用例通过;91 个 golden 与旧服务逐端点 91/91 identical;同一套检查在「插件装进 dsh 宿主」的实例上复跑同样 91/91,
verify-live12 项全过。 - v0.2:全量 255/255 测试通过(新增 24 个 native/selection + 21 个同源路由 + 3 个接线端到端);在真实 dsh web 宿主上实测:选择往返
POST /plugins/mcpp/ui/selection→registered出现/消失、设置页「MCP 工具」分区渲染 20 个工具、侧栏「MCP 网关」页签加载管理页、/plugins/mcpp/*全部受 dsh 浏览器会话围栏保护(未登录 401);9020 外部端点重跑 golden 仍 91/91。 - v0.3(2026-11-10):全量 280/280 测试通过(新增 25 项 preset-scoped 与 service specs);新增
dsh-mcpp/scoped预设作用域入口,package.json的./scoped子路径导出lib/scoped.js,构建与 subpath 解析一并纳入测试。
打包约定(踩过的坑)
package.json 的 files 必须包含 cordis.patch.yml。profile 里的 dsh-mcpp 是 file: 依赖,pnpm 会按 files 打成一个真实目录副本;漏掉这个文件后,下一次 profile 更新重装插件就会得到「声明了 bundle patch 但文件不存在」的副本,dsh 的启动前组合校验会直接拒绝整个 profile:
dsh-mcpp: declared patch ./cordis.patch.yml is missing — the profile will fail to boot
test/package.test.mjs 会守住这条(files 三件套 lib / static / cordis.patch.yml 缺一即红)。改包结构后跑一遍全量测试再交给 profile 更新流程。
安装
见 INSTALL.md。一句话:构建 → 把 lib/(含 client.js)+ static/ 拷进目标 profile 的 node_modules/dsh-mcpp,profile 的 package.json 加依赖与 dsh.profile.bundles,cordis.patch.yml 加一行按 id 的 config,重启该 dsh 端。
与旧服务的已知偏差
| 场景 | 旧服务 | 本插件 |
|---|---|---|
| 非法 JSON body | 500 | 400 + JSON-RPC -32700 |
| 参数变换非法值 | 500 | JSON-RPC -32602 |
| 未知路径 / 错误方法 | FastAPI {"detail": ...} 404/405 |
同左(保持一致) |
| 上游 HTTP 错误 | 透传状态码到日志、JSON-RPC -32000 |
同左 |
其余行为按 test/fixtures/golden/(旧服务真实响应)与 test/integration.test.mjs 对齐。
No comments yet. Be the first to write one.