dsh-workspace-mcp
Verified@jinngoo/dsh-workspace-mcp · v0.1.1 · MIT
DSH Bundle:工作区级 MCP 配置。每个会话启动时读取该会话工作区根下的 .dsh/mcp.json,把声明的每个 MCP 服务器挂成 agent 作用域的 MCP client,工具以 mcp__<serverName>__<tool> 出现在该会话。无需为每个项目新建 profile。
Install
dsh plugin add @jinngoo/dsh-workspace-mcp Confirm the layer applied with dsh --profile default --dump-config — see the install guide.
Source
Tags
Readme
@jinngoo/dsh-workspace-mcp
一句话:在任意工作区里放一个 .dsh/mcp.json,该工作区的每个 DSH 会话自动获得其中声明的 MCP 服务器工具——不用改 profile、不用给每个项目建 profile。
工具命名沿用 DSH / Claude Code 的稳定契约:mcp__<serverName>__<rawTool>。
工作区 A/.dsh/mcp.json ─┐
├─ 会话 A(cwd=A)→ agent 作用域 MCP client → mcp__demo__ping
工作区 B/.dsh/mcp.json ─┘
└─ 会话 B(cwd=B)→ 另一个 agent 作用域 MCP client → 互不可见
1. 为什么需要它
DSH 的配置层只有:profile bundles → profile cordis.patch.yml → $DSH_HOME/cordis.patch.yml → --patch,没有"工作区级"这一层(见 docs/architecture.zh.md)。所以 MCP 配置过去只有两种写法:
- 写死在 profile 里:所有项目共享,与具体项目无关;
- 每个项目一个 profile:维护成本高。
本插件补上工作区层:配置跟着仓库走(可以提交进 git、可以 clone 后直接生效),而 profile 只装一次插件。
2. 它怎么工作
- 插件是 host 平面的一行,注册
agent/created监听器。 - 每个会话创建时,用会话原始工作目录(
agent.session.header.cwd,不可变)定位工作区根;只有 header 没有 cwd 时才退回workingDirectory.defaultDirectory(最后退回宿主进程目录)。不会用会话当前目录——会话里cd到别处不应该换掉 MCP 服务器。 - 读取
<工作区根>/.dsh/mcp.json,逐条校验成@deepseek-ai/dsh-mcp-client的 Config。 - 对每条配置执行
agent.ctx.plugin(McpClient, config)——和 ACP 桥接完全同款(packages/acp/acp/src/mcp.ts)。MCP client 与工具注册都落在 agent 作用域:只对该会话可见,会话结束自动回收。 agent/created的监听器是 serial 且会被等待的,所以首轮请求就能看到工具;同时整体挂载有超时(startupTimeoutMs),慢服务器只会推迟工具出现,不会卡死会话创建。
3. 安装
前提:DSH 0.2.x(
engines.dsh: ^0.2.0-rc.1)。
方式一(推荐,npm registry):
dsh plugin --profile <profile> add @jinngoo/dsh-workspace-mcp
或在 DSH 会话里用 plugin_manager:
action: install_bundle
target: @jinngoo/dsh-workspace-mcp@latest
方式二(GitHub 直装,lib/ 已入库,无需构建):
dsh plugin --profile <profile> add git+https://github.com/Jinngoo/dsh-workspace-mcp.git
方式三(本地开发,绝对目录):link: 安装的运行时依赖与 peer 都从 checkout 自己的 node_modules 解析,所以先装依赖再安装:
git clone [email protected]:Jinngoo/dsh-workspace-mcp.git
cd dsh-workspace-mcp && pnpm install
dsh plugin --profile <profile> add <克隆目录的绝对路径>
安装后插件行由本包的 cordis.patch.yml 自动插入,不需要手改 profile 的 cordis.patch.yml。工作区配置文件本身也不需要 profile 做任何事。
4. 工作区声明文件
默认路径 <工作区根>/.dsh/mcp.json(可用插件的 fileName 改)。文件不存在 = 该工作区没有 MCP 服务器,静默跳过。
4.1 Claude Code 风格:对象按服务器名索引
{
"mcpServers": {
"creator": {
"type": "stdio",
"command": "node",
"args": ["D:/tools/creator-mcp/server.js"],
"env": { "CREATOR_HOME": "D:/tools/creator-mcp" },
"cwd": "."
},
"docs": {
"type": "http",
"url": "http://127.0.0.1:3456/mcp",
"headers": { "Authorization": "Bearer <token>" }
}
}
}
4.2 ACP 风格:数组 + 显式 name
{
"mcpServers": [
{ "name": "demo", "type": "http", "url": "http://127.0.0.1:3456/mcp" }
]
}
4.3 字段规则
| 字段 | 说明 |
|---|---|
key / name |
serverName,必须匹配 ^[A-Za-z0-9_-]{1,32}$。不合法直接报错跳过,不做静默改名——工具名是会话历史与权限规则引用的稳定契约,悄悄改名比报错更危险。 |
type |
stdio 或 http(也接受 streamable-http)。省略时按 command / url 推断。SSE 不支持,会明确报错。 |
command / args / env / cwd |
stdio。cwd 相对工作区根解析,绝对路径原样;省略 cwd 即工作区根。env 合并到 mcp-client 清洗过的环境之上。 |
url / headers |
Streamable HTTP。URL 必须是绝对 http(s)://。 |
toolCallTimeoutMs |
可选,覆盖插件级默认值。 |
maxInstructionBytes / reconnect |
可选,原样转发给 mcp-client(由其 schema 校验)。 |
校验行为:
- 坏 JSON / 顶层不是对象 / 缺
mcpServers→ 一条明确错误日志,会话照常创建(没有 MCP 工具)。 - 单个服务器条目非法(名字、传输、URL、字段类型、stdio 被禁)→ 该条跳过并告警,其它服务器正常挂载。
- 文件不存在 → 静默(debug 级日志)。
- 同一会话内重名由 mcp-client 自己的命名空间预留兜底;不同会话/工作区可以复用同一个
serverName(agent 作用域隔离)。
5. 插件自身配置(profile patch 行)
安装后行长这样(cordis.patch.yml):
- id: workspace-mcp
name: '@jinngoo/dsh-workspace-mcp'
config:
fileName: '.dsh/mcp.json'
watch: false
startupTimeoutMs: 10000
allowStdio: false
stdioAllowlist: []
mountInSubagents: false
| 键 | 默认 | 含义 |
|---|---|---|
fileName |
.dsh/mcp.json |
声明文件,相对工作区根;绝对路径也接受。 |
watch |
false |
监视声明文件;变化后为仍存活的会话重新挂载(先卸旧、再挂新,串行、300ms 去抖)。实现用 fs.watchFile 轮询(1s),因此文件不存在、.dsh 稍后创建、编辑器原子替换都能覆盖。 |
startupTimeoutMs |
10000 |
等待「首次连接 + 工具发现」的上限。超时后会话继续,工具待服务器就绪后出现(日志有告警)。 |
allowStdio |
false |
是否允许 stdio 服务器。 |
stdioAllowlist |
[] |
可选护栏:非空时,command 必须等于某项,或位于某项之下(C:\tools 允许 C:\tools\node.exe,不允许 C:\toolsevil.exe)。 |
mountInSubagents |
false |
子 agent 是否也挂载。 |
toolCallTimeoutMs |
不设(mcp-client 默认 60s) | 每次工具调用的默认超时。 |
想改这些默认值,在 profile 的 cordis.patch.yml 里按 id: workspace-mcp 覆盖即可(patch 按 id 覆盖整行 config,记得写全你要保留的键)。
6. 安全边界(必读)
.dsh/mcp.json 是工作区文件,可能来自 clone 下来的仓库。本插件的默认姿态:
- stdio 默认禁止(
allowStdio: false)。stdio = 让工作区文件在本机执行任意命令,这是最实际的风险面。只对你信任的仓库/配置开启;开了之后还可以用stdioAllowlist收窄到固定可执行文件。 - HTTP 默认允许。HTTP 不能直接在本机执行命令,但仍是网络请求(SSRF)与提示注入的载体:不要盲目信任来源不明的
.dsh/mcp.json,尤其是headers里的凭据。插件不读取、不复用你环境里的令牌;headers只来自该文件。 - 插件不绕过任何权限层:MCP 工具仍走 ToolRuntime 的正常权限/审批流程。
- 建议:把
.dsh/mcp.json加入仓库前先 review;公开仓库里避免放本地路径与私有令牌。
7. 子 agent 行为
子 agent 的会话也继承父会话的 cwd。默认 不给子 agent 挂载(mountInSubagents: false),原因:
- stdio 服务器会为每个子 agent 再起一个进程,重型 MCP(浏览器、创作者工具)可能冲突或耗尽资源;
- HTTP 服务器也会为每个子 agent 新建连接与工具集;
- 子 agent 是内部临时 agent,通常不需要完整工具面。
如果委派出去的任务确实需要 MCP 工具,把 mountInSubagents 设为 true 即可(每个 agent 独立挂载,隔离正确但资源按 agent 数增长)。
8. 验收 / 自检
自动化测试(23 条):
pnpm install
pnpm test # 15 条解析单测 + 8 条 production AgentLoop + 真实 HTTP MCP fixture 集成测试
pnpm typecheck
pnpm build
集成测试覆盖:工作区挂载、无配置、坏 JSON、坏条目跳过、两个工作区同名 server 隔离、watch 热重载、子 agent 默认不挂载、mountInSubagents: true 挂载。
端到端(真实 headless 会话 + 运行中模型的完整证据见 docs/verification.md):
- 用本机 HTTP MCP fixture(只有
ping→pong)起一个 Streamable HTTP 端点; - 在测试工作区写
.dsh/mcp.json; - 新建会话 → 工具列表出现
mcp__demo__ping,调用返回pong; - 没有配置文件 / 坏 JSON / 坏条目 → 会话正常创建,日志可见明确错误;
- 两个工作区并行 → 工具互不串;
- 会话结束 → MCP 子进程退出;
- 重启 DSH → 行为不变(配置来自工作区)。
9. 实现说明与设计取舍
- 挂载点:必须是 agent 作用域(
agent.ctx),不是全局/root。agent 作用域的工具只对该 agent 可见,且随 agent 销毁自动回收;不同工作区可以复用同名serverName而不冲突。 - 根目录:
session.header.cwd≠workingDirectory.get(session)。前者是创建会话时的原始项目目录(不可变),后者会被会话内cd改掉。本插件只用前者。 - 首轮可见性:
agent/created监听器被 AgentLoop 串行 await,所以挂载完成(或超时放行)后才开始首轮。 - 失败隔离:任何异常都不会 reject
agent/created;最坏情况是该会话没有 MCP 工具,但会话本身可用。 - 热更新:
watch: true时只对仍存活的 agent 重挂;不使用fs.watch,因为文件可能稍后才创建、编辑器会用原子替换。 - 卸载:插件行卸载时关闭所有 watcher、卸载本插件挂载的 MCP client;agent 作用域本身也会在会话结束时回收各自 client。
- 不做的事:不做 per-tool 权限控制(交给 ToolRuntime)、不做 MCP 提示词模板、不做
sse传输、不做${VAR}环境变量展开(需要时请用插件级配置/环境变量自身机制)。
10. 目录结构
dsh-workspace-mcp/
├─ src/config.ts # 纯解析/校验:mcp.json → mcp-client Config(无 Cordis 依赖,可单测)
├─ src/index.ts # 插件入口:agent/created 挂载、超时、热更新、清理
├─ tests/config.spec.ts
├─ cordis.patch.yml # bundle patch:插入 workspace-mcp 行
├─ locale/{en,zh}.json
├─ icon.svg
└─ lib/ # tsc 构建产物(安装前必须 build)
11. 故障排查
| 现象 | 检查 |
|---|---|
会话里没有任何 mcp__* 工具 |
1) .dsh/mcp.json 是否在工作区根(session.header.cwd,不是当前目录);2) 日志 workspace-mcp 是否有 skipped/error;3) type: http 的 URL 是否可达;4) 是否用了 sse(不支持)。 |
| 日志说 stdio 被禁用 | profile patch 里把 allowStdio 设为 true(只对可信工作区)。 |
| 会话创建变慢 | 调小 startupTimeoutMs,或检查 MCP 服务器启动耗时;超时后会放行会话。 |
改了 .dsh/mcp.json 不生效 |
watch 默认 false:默认只在会话创建时读取;需要热更新就开 watch: true,或新建会话。 |
| 子 agent 没有 MCP 工具 | 默认 mountInSubagents: false;需要时开启。 |