DSH HUB
HomePlugin StorePlugin PacksCommunityRankingsResourcesPublish Guide
Plugin source
Back to catalog

VioletEverGarden-zc /

VioletEverGarden-zc/dsh-rea-mcp-toggle

Verified

在 DeepSeek Harness Web UI 中一键开关 REA MCP 服务器(rea-mcp),并向模型暴露配套的开关工具 rea_mcp_toggle;选择按 profile 持久化,重启后依然生效。

★ 0 Stars0 Forks0 IssuesN/A Community rating0 Confirmed installs
View on GitHub
READMESource: main@a4aa003f

dsh-rea-mcp-toggle

一个 DeepSeek Harness(DSH)bundle 插件:在 Harness Web UI 里一键开关 REA MCP 服务器,并把同一个开关交给模型(工具 rea_mcp_toggle)。选择按 profile 保存,重启后依然生效。

适用:DSH 0.2.1-alpha.2(web profile)。零运行时依赖(只用 Node 内置模块 + dsh 宿主服务)。


1. 项目解决什么问题

REA(Reverse Engineer Anything)通过 MCP 以 stdio 方式接入 DSH。它的工具集非常大:开启时是 170 个 mcp__rea__* 工具,这些工具的 schema 会随每一次请求一起发给模型。

实测同一实例、同一时刻,Tool.listTools 的返回体大小:

REA 状态 工具数 tools payload
开启 251 722,353 字节
关闭 81 94,807 字节

差价 627 KB 全部是那 170 个工具的定义。它们每次都进请求,于是产生两种浪费:

  1. token 费用:不管这一轮到底用不用逆向工具,都在为这 627 KB 付费(长会话里反复累积);
  2. 进程与内存:REA 的 MCP 子进程常驻(单进程约 240 MB),只有关掉才会退出。

而 DSH 自带的「插件开关」(Plugin Manager 里把 rea-mcp 行禁用)对这一行无效——原因见第 6 节,这不是配置问题,是 patch 层的机制决定的。所以本项目提供了能真正生效、且能持久化的开关。

典型场景:平时关着,等真要分析二进制 / APK 时再打开;或者反过来,只在需要省 token 时临时关闭。


2. 主要功能

2.1 UI 开关(主入口)

会话标题栏(Session header)里的一个小按钮,与「费用明细」等控件并列:

  • 显示 REA 开 / REA 关,绿点表示当前状态;
  • 点一下切换:即时生效,不需要重启 Harness;
  • 切换失败时按钮直接变成 REA 失败(红框),鼠标悬停显示具体原因 —— 不会静默无效;
  • 鼠标悬停还会显示当前工具数与配置行 id。

2.2 模型工具 rea_mcp_toggle

action 作用
status 读当前开关状态、工具数、是否已持久化、状态文件路径
set 切换(需 enabled: true/false);幂等,重复同向不发写、不产生副作用
probe 诊断:Loader 行状态、REA 工具数、缝是否在位、服务可用性、Remote endpoint 解析情况
selftest 用 Web UI 调用的同一条 Remote 网关路径自检本插件(用来区分「UI 坏了」还是「Host 坏了」)

UI 按钮与这个工具共用同一套实现(同一 service 的同一个方法),不存在两份逻辑。

2.3 状态持久化

选择保存在插件自己的状态文件里:

%DSH_PROFILE_DIR%\.rea-mcp-toggle.state.json     例如 {"enabled": false, "savedAt": "..."}

写入是原子的(临时文件 + rename)。重启后由家目录层的一行 !!js 缝读取该文件,开机即按选择生效(关闭状态下开机根本不会拉起 REA)。

2.4 工具可见性真正变化

关闭时 170 个 mcp__rea__* 工具从模型可调用工具列表中消失(不只是「不执行」);开启时全部回来。

2.5 进程生命周期

  • 开启:拉起 REA 的 stdio 子进程(每次切换恰好 1 个);
  • 关闭:dispose 该行,回收子进程、注销工具;
  • 关闭后不会自己复活(曾有一个「关闭后子进程又起来」的缺陷,已修并可复现验证)。

3. 安装方法

3.1 前置条件

  • 已安装 DSH,且 REA 已作为 MCP server 注册在家目录层 %DSH_HOME%\cordis.patch.yml,形如:

    - insert:
        - id: rea-mcp
          name: '@deepseek-ai/dsh-mcp-client'
          config:
            serverName: rea
            transport: stdio
            command: '<HARNESS_ROOT>\node\bin\node.exe'
            args: ['<NPM_GLOBAL>\node_modules\rea-agents\scripts\rea.mjs', 'mcp']
            env: { REA_MCP_INPUT_SCHEMA_PROFILE: compact }
            toolCallTimeoutMs: 900000
            failOnStartupError: false        # 硬约束:REA 坏了也不能让 harness 起不来
    
  • 该行 id 必须是 rea-mcp(本插件按这个 id 定位;其它 id 会被明确拒绝,不会误伤)。

3.2 安装本 bundle

本插件以路径链接方式安装,因此包目录内需要有指向 DSH 安装的 node_modules 链接(Node 从包的真实目录解析 bare import;本插件运行时只 import node:fs,但编译/探针脚本需要):

cd plugins\dsh-rea-mcp-toggle
node scripts/link-deps.mjs
# 通常不需要参数:脚本会自己找 harness(见下),失败时才需要显式给根目录
# node scripts/link-deps.mjs "<HARNESS_ROOT>"

脚本如何找到 harness(按顺序,每个候选都会被 marker 校验,错的目录不会静默胜出):

顺序 来源 说明
1 命令行参数 node scripts/link-deps.mjs "<root>"
2 <repo>/.harness-root 一行写根目录;模板见 .harness-root.example;已被 git 忽略。标准位置都描述不了的布局用这个
3 DSH_HARNESS_ROOT 环境变量
4 平台标准安装位置 Windows:%LOCALAPPDATA%\Programs、%ProgramFiles%、%ProgramFiles(x86)%、%LOCALAPPDATA% 下的 dsh-desktop / DeepSeek Harness / deepseek-harness,并在其 resources/harness 与 harness 处探测;macOS:/Applications、~/Applications;Linux:/opt、/usr/local/lib、/usr/lib
5 正在运行的 Harness 由进程的 node.exe 路径反推安装根,因此装在非标准目录也能找到

判定「是 harness 根」的条件:同时存在 node_modules/@deepseek-ai/{cordis,dsh,dsh-tools} 与 node_modules/js-yaml。本仓库自身被显式排除(它的 node_modules 是链接,否则会自我链接)。

然后在会话里让模型执行(需要 danger-full-access 或当次审批):

plugin_manager → install_bundle → <本目录绝对路径>

安装后必须重启一次 dsh-desktop。 Host 半边的模块是进程启动时导入的;不重启时插件行会处于 failed to import 或停在旧版本。

3.3 安装家目录层的「开关缝」(必须,一次)

这是唯一需要手改家目录层的地方。在你已有的 rea-mcp 行里加一个行级 disabled(不要放进 config 里面):

      - id: rea-mcp
        name: '@deepseek-ai/dsh-mcp-client'
        config:
          # ...原有内容保持不动...
        # ── 开关缝:由 dsh-rea-mcp-toggle 插件读取 ──
        disabled: !!js >-
          Boolean(ctx.get('profileContext')?.dir) && (() => { try { return process.getBuiltinModule('node:fs').readFileSync(process.getBuiltinModule('node:path').join(ctx.get('profileContext').dir, '.rea-mcp-toggle.state.json'), 'utf8') && JSON.parse(process.getBuiltinModule('node:fs').readFileSync(process.getBuiltinModule('node:path').join(ctx.get('profileContext').dir, '.rea-mcp-toggle.state.json'), 'utf8')).enabled === false } catch { return false } })()

改前先备份:Copy-Item .dsh\cordis.patch.yml .dsh\cordis.patch.yml.bak-<时间戳>

这段表达式是安全降级的:状态文件不存在 / JSON 损坏 / profileContext 不可用(例如离线 dsh --dump-config)时一律返回 false=保持启用,不会让 harness 起不来。

3.4 安装后自检

node scripts/acceptance.mjs        # 校验缝与状态文件在同一口径上

再在会话里执行 rea_mcp_toggle {action:"selftest"} —— 它走的就是 Web UI 的 Remote 网关路径。


4. 使用方法

4.1 日常:点按钮

标题栏的 REA 开 / REA 关 按钮,点一下即可。不需要重启。

4.2 让模型帮你切

把 REA 关掉,我在省钱          → 模型调用 rea_mcp_toggle {action:"set", enabled:false}
REA 现在开着吗?               → 模型调用 rea_mcp_toggle {action:"status"}
UI 按钮点了没反应,帮我看看     → 模型调用 rea_mcp_toggle {action:"selftest"}

4.3 关闭 / 禁用的几种情况

  • 临时关:点按钮。状态文件写 enabled:false,当前进程立即生效(工具消失、子进程回收)。
  • 永久不再启用:把手改的那段 disabled: !!js 缝从家目录层删掉(保留其余内容),该行即恢复为无条件启用。
  • 彻底卸载:见第 8 节。

5. 输入输出示例

以下均为真实运行结果(本机实测,compact schema profile)。

5.1 查询状态

输入:

{ "action": "status" }

输出(REA 关闭时):

REA MCP (rea-mcp) is OFF: the mcp__rea__* tools are not callable; the choice is saved.
entry=rea-mcp module=@deepseek-ai/dsh-mcp-client
enabled=false persisted=false tools=0 seam=true
stateFile=<PROFILE_DIR>\.rea-mcp-toggle.state.json

输出(REA 开启时):

REA MCP (rea-mcp) is ON: 170 tool(s); the choice is saved.
entry=rea-mcp module=@deepseek-ai/dsh-mcp-client
enabled=true persisted=true tools=170 seam=false
stateFile=<PROFILE_DIR>\.rea-mcp-toggle.state.json

seam=false 是正常的:即时开启会清掉该行进程内的 disabled 键(probe 里显示 rawDisabled=null),开机时缝会重新求值(那时是 seam=true)。

5.2 关闭 REA

输入:

{ "action": "set", "enabled": false }

输出:

REA MCP (rea-mcp) is OFF: the mcp__rea__* tools are not callable; the choice is saved.
entry=rea-mcp module=@deepseek-ai/dsh-mcp-client
enabled=false persisted=false tools=0 seam=false

同时 %DSH_PROFILE_DIR%\.rea-mcp-toggle.state.json 变为:

{ "enabled": false, "savedAt": "2026-10-13T17:59:27.803Z" }

5.3 开启 REA

输入:

{ "action": "set", "enabled": true }

输出(子进程握手完成后):

REA MCP (rea-mcp) is ON: 170 tool(s); the choice is saved.
enabled=true persisted=true tools=170 seam=false

5.4 幂等:重复同向不发写

连续两次 { "action": "set", "enabled": false },第二次不会重写状态文件 (实测文件 LastWriteTime 与内容完全不变),也不会新增 patch 行。

5.5 诊断

输入:

{ "action": "probe" }

输出(节选):

seam=false reaTools=0 stateFile=...\.rea-mcp-toggle.state.json
stateFileExists=true stateFileValue=false services={"loader":true,"tools":true,"typert":true,"pluginManager":true,"typertGateway":true}
row id=rea-mcp-toggle composite=include:rea-mcp-toggle module=dsh-rea-mcp-toggle rawDisabled=false effectiveDisabled=false fiberState=2 parentTree=include
row id=rea-mcp composite=include:rea-mcp module=@deepseek-ai/dsh-mcp-client rawDisabled=true effectiveDisabled=true fiberState=4 parentTree=include

读数要点:effectiveDisabled=true 且 fiberState=4(disposed)=该行确实已停; reaTools=0 = 模型侧看不到 REA 工具。

5.6 UI 自检(判断是 UI 坏还是 Host 坏)

输入:

{ "action": "selftest" }

输出:

gateway reaMcp/getState ok -> {...}
gateway reaMcp/setEnabled ok -> {...}
restoredTo=false

selftest 会先记住你的选择,结束后原样恢复(诊断不改变开关)。

5.7 安装后的效果对照

观察点 关闭 开启
Tool.listTools 里 mcp__rea__* 0 个 170 个
listTools payload ≈ 94.8 KB ≈ 722.4 KB
该实例的 REA 子进程 0 个 1 个
开销最大的部分 — 170 个工具 schema 进每次请求

6. 为什么不能用 profile 层的 disabled(关键设计决定)

DSH 的 patch 分层是:bundle 层 → profile 层(%DSH_PROFILE_DIR%\cordis.patch.yml)→ 家目录层(%DSH_HOME%\cordis.patch.yml)→ --patch 覆盖,后者覆盖前者。

rea-mcp 行是家目录层用 insert 声明的,而家目录层最后应用,insert 推入的是新的行对象。后果(用 Harness 自身的 readProfilePatches / composeEntries 复现,见 scripts/probe-layer-order.mjs):

  • profile 层写 { id: rea-mcp, disabled: true } → 组合结果里 rea-mcp 从不带 disabled;
  • 运行时 Entry.disabled 恒为 false;
  • 连官方 plugin_manager 对该行也只能返回 application: "overridden"。

所以本插件的做法是:状态放自己的文件,家目录层只承担一个「缝」(!!js 表达式)。表达式在读取 disabled 时才求值,因此不会被家目录层自己的 insert 盖掉。


7. 项目结构、已知限制与开发笔记

7.1 结构

dsh-rea-mcp-toggle/
├─ package.json          # bundle 清单(dsh.bundle.patch / dsh.client)
├─ cordis.patch.yml      # 只插入本插件自己的行;不碰 rea-mcp 行
├─ index.js              # Host 半边:唯一实现(Remote reaMcp + 工具 rea_mcp_toggle)
├─ state.js              # 状态文件的唯一读写者(原子写)
├─ client.js             # 浏览器半边:标题栏开关按钮
├─ locale/{zh,en}.json   # 插件在插件列表里的显示名与描述
├─ icon.svg              # 图标
├─ .gitignore            # 排除 node_modules 链接、状态文件、机器本地的 .harness-root
├─ .harness-root.example # 「harness 装在哪」的模板(真实文件 .harness-root 由使用者创建)
└─ scripts/              # 安装辅助与验证脚本(不属于运行时,不随包发布)
   └─ lib/harness-paths.mjs   # 定位 harness / profile:多级兜底,不硬编码任何本机路径

运行时零依赖:Host 半边只 import Node 内置模块(node:fs、node:path、node:url)与自身文件;scripts/ 里的脚本才需要 DSH 安装提供的包。

7.2 已知限制

  1. 作用域是「实例」:开关作用于承载它的那个 Harness 进程。若机器上残留了旧的 Harness 守护进程(桌面应用重启时偶尔不退出),那些实例不受影响 —— 用任务管理器确认 node.exe 中带 dsh\lib\bin.js 的条目数量,多于 1 个即需清理。
  2. disabled 行必须放在行级,写成 config 的子字段会导致该行永不生效(--dump-config 可看出层级)。
  3. 上游遗留(不在本项目内):@deepseek-ai/dsh-mcp-client 的 dispose() 先取子进程句柄快照再 await;若拆卸时正好有连接在飞,那次连接会在快照之后写入新句柄,其子进程不会被关闭(每关一次可能残留一个孤儿 REA 进程)。该行本身确实是停的(fiber 已 dispose、工具为 0),残留进程只占内存、不向模型暴露工具,Harness 退出时回收。
  4. selftest 需要一次重启后才可用(Host 模块按进程加载)。

7.3 三个踩过的坑(写给后来者)

  • 工具 parameters 必须是「干净」的 JSON Schema:ctx.tools.register() 收的是 raw JSON Schema,会被原样发给 provider。若把作者侧的 required: true 标记写进属性里(defineTool() 那种写法),provider 会直接报 Invalid schema for function 'rea_mcp_toggle': true is not of type 'array',每一轮都在调用工具之前就失败。正确写法是可选属性不加标记、必填项放进对象级的 required: ["action"] 数组。 仓库里保留了门禁脚本 scripts/check-tool-schema.mjs:它从 index.js 抽出 parameters 字面量,用 Harness 自己的 assertSupportedJsonSchema + 8 个参数用例(接受/拒绝各半)校验,防止再次漂移。

  • 不要继承 TypertRemoteService。它的构造器用 bindTypertRemote(this, this.name) 绑定,而 Cordis 的 Service.name 是 symbol 而非服务键字符串,导致网关校验失败:Receiver must be an instance of class ...。正确做法是普通类 + 显式绑定:

    this.typertRemote = Object.freeze({ service: this, serviceKey: 'reaMcp', namespace: 'reaMcp' })
    
  • Remote 调用返回的是 { ok, value } 信封,客户端必须 if (!result || result.ok !== true) throw ... 后取 result.value;只判断 ok === false 会把信封当成结果,表现为「按钮点了没反应」。

  • 无参端点要用空对象调用:gateway.invoke() 的入参不能省略 args,要传 args: {}。省略时参数校验会抛 Cannot convert undefined or null to object(浏览器侧的 Remote 调用方始终发 {},所以只有 Host 自检脚本会踩到)。

7.4 验证脚本

先跑一次 node scripts/link-deps.mjs '<HARNESS_ROOT>':这些脚本用 Harness 自己的实现来验证(组合算法、YAML 方言、JSON Schema 检查器),依赖包通过本包自己的 node_modules 链接解析,不硬编码任何本机路径。之后在 profile 启动的 shell 里(有 DSH_PROFILE_DIR / DSH_HOME)可以不带参数直接跑:

node --check index.js; node --check client.js; node --check state.js   # 语法
node scripts/check-tool-schema.mjs   # 工具 schema 门禁(Harness 自己的检查器 + 8 个用例)
node scripts/probe-layer-order.mjs   # 分层覆盖机制:profile 层 disabled 为何无效
node scripts/probe-composition.mjs   # 组合结果中该行是否带 disabled
node scripts/probe-seam.mjs          # 缝表达式 × Harness 真实 YAML 方言与求值器
node scripts/probe-roundtrip.mjs     # 真实写/读 × 缝读取,含「损坏文件」降级分支
node scripts/acceptance.mjs          # 缝与状态文件口径一致性

不在 profile shell 里时,用参数显式给路径:

node scripts/acceptance.mjs '<PROFILE_DIR>' '<DSH_HOME>'

8. 卸载 / 回滚

  1. plugin_manager → remove_bundle → dsh-rea-mcp-toggle;
  2. 删除家目录层里的 disabled: !!js >- ... 缝(保留该行其余内容),REA 即恢复为无条件启用;
  3. 可选:删除 %DSH_PROFILE_DIR%\.rea-mcp-toggle.state.json。

9. 许可

MIT © 2026 VioletEverGarden-zc

LICENSE 使用 MIT 标准文本,版权人为 VioletEverGarden-zc;package.json 的 license 字段同样是 MIT,三者口径一致。

10. 作者与仓库

  • 作者:VioletEverGarden-zc — https://github.com/VioletEverGarden-zc
  • 仓库:https://github.com/VioletEverGarden-zc/dsh-rea-mcp-toggle
  • 上游项目:REA(Reverse Engineer Anything)— https://github.com/morluto/rea。本插件只提供「开关」,REA 本体及其 mcp__rea__* 工具集由上游项目提供与维护(MIT)。

仓库 URL 按包名 dsh-rea-mcp-toggle 填写。若你实际建仓时用了别的名字,请同步替换 package.json 里的 repository / homepage / bugs 三处与本节链接。

11. 致谢

感谢 REA(Reverse Engineer Anything)的作者与贡献者。本插件只是替它加了一只开关,逆向能力全部来自上游;没有 REA,这个项目没有存在的理由。

—/ 5

No ratings yet

Verified DSH bundle

Commit a4aa003f527b

Community comments

No comments yet. Be the first to write one.

DSH HUB

A community index for DSH plugins. Not an official GitHub or DeepSeek AI product.

CommunityResourcesAPIAbout