Skip to content

Add a headless MCP host (stdio, no dnSpy window) - #26

Merged
KernelErr merged 5 commits into
mainfrom
feature/headless-stdio
Sep 23, 2026
Merged

KernelErr merged 5 commits into
mainfrom
feature/headless-stdio

Conversation

@KernelErr

@KernelErr KernelErr commented Sep 22, 2026 •

Copy link
Copy Markdown
Owner

Summary

新增 headless 宿主 dnSpy.Extension.MCP.Headless.exe:不开 dnSpy 窗口,通过 MCP stdio 传输提供同样的 32 个工具和 6 份资源。MCP 客户端(Claude Desktop、Claude Code、Chatbox、Cursor、codex 等)会按需启动它,用户不必一直开着 dnSpy、也不用手动启用服务器。对应 #24 评论里提的 headless 需求。

{
  "mcpServers": {
    "dnspy": {
      "command": "C:\\Tools\\dnSpy\\dnSpy.Extension.MCP.Headless.exe",
      "args": []
    }
  }
}

Changes

主要提交:

  1. c09b567 解耦:McpTools 不再直接依赖 IDocumentTreeView / IDocumentTabService / IDecompilerService,改为通过 IMcpHost(列出/打开文档、反编译器、在 UI 线程执行、重命名后的树/标签页刷新)。dnSpy 内由 DnSpyMcpHost 实现,行为不变。JSON-RPC 分派从 McpServer 抽到与传输无关的 McpDispatcher。顺带删除了已经没有调用方的 RenameClassOrEnumSymbolCore。
  2. e2591a5 headless 宿主(headless/):
    • 像 dnSpy.Console.exe 一样通过反射加载 dnSpy 的 C# 反编译器;目标文件读入内存(运行期间不锁定 DLL),并包装成与 dnSpy 相同的 DsDotNetDocument 类型,所以所有工具不用改动。
    • stdout 专用于协议,日志写 stderr。不需要任何启动参数:启动时为空,由 AI 调用 open_files 加载目标;可选开关 --dnspy、--quiet、--version(传入路径也可预加载,但不是必需的)。
    • deploy-headless.ps1 按 dnSpy.Console.exe 的方式部署到每个发布包:复用该包自己的运行时配置(net48 用 dnSpy.exe.config,net10 用 dnSpy.Console.runtimeconfig.json),net10 用 AppHostPatcher 修补 apphost,因此它和所在的包一样是框架依赖或自包含。
    • apphost 必须和包的 CPU 架构一致(x64 的 apphost 加载不了 win-x86 包里的 x86 hostfxr):release.yml 按 RID 分别构建,部署脚本发现不匹配会直接报错。
    • release.yml 把它部署进三个一体包;build.yml 也会构建它。
    • run-tests.ps1 -Headless:通过 stdio 对部署好的 headless 宿主跑完整套测试(只跳过 HTTP 专属的 [23] 和 [36]),并检查 stdin 关闭后以退出码 0 退出。
    • CLAUDE.md 记录了宿主的相关约定。
  3. README 精简重写(中英文各 489 → 325 行):开头对比 headless 与 dnSpy 内两种运行方式,快速开始两种都给命令;客户端配置集中到“连接客户端”一节——Claude Code(--scope user、.mcp.json 及其一次性批准、如何验证)、Claude Desktop(Edit Config → 配置 → 彻底重启,日志位置)、其他客户端和 codex;工具说明改成每个一行,curl 演示和架构细节精简为传输表格并指向 CLAUDE.md。顺带去掉了原来 Claude Desktop 示例里并不存在的 "command": "http"。CLAUDE.md 另外记下了“每次改动都要在两种模式下跑 E2E”。

插件单独安装(只放 DLL)的用户暂时用不了 headless:它需要按上面的方式放在 dnSpy.Console.exe 旁边并匹配运行时,一体包已经配好。

Testing

以下 E2E 数字是在合入 #25 之后的代码上跑的([36] 只测 HTTP 会话,headless 下跳过)。

场景 结果
dnSpy 内完整测试(HTTP),net10 / net48 245/245,245/245
headless 完整测试(stdio),net10 / net48 239/239,239/239
官方 @modelcontextprotocol/sdk stdio 客户端 net48、net10、自包含 win-x64、自包含 win-x86 全部通过
Claude Code 2.1.280:claude mcp add + claude mcp list net10 与 net48 的 headless 均为 ✔ Connected
codex-cli 0.142.5:README 中两种配置写法 均能正确解析

两个自包含包是用 dnSpy 的 build.ps1 在本地构建的;COREHOST_TRACE 确认 headless exe 用的是包内自带的 hostfxr(self-contained 模式),不是本机安装的 .NET。部署脚本拒绝架构不匹配的情况也验证过。

与 #25 的关系

#25 已先合并。本分支已合入 main:CLAUDE.md 的冲突已解决,测试步骤 [36] 测的是 Streamable HTTP 会话,在 -Headless 下跳过(与 [23] 相同)。

🤖 Generated with Claude Code

Rui Li and others added 5 commits September 23, 2026 02:13
Groundwork for a headless host that serves the same tools without the
dnSpy window. Behaviour inside dnSpy is unchanged.

- McpTools depended directly on IDocumentTreeView, IDocumentTabService
  and IDecompilerService. It now takes an IMcpHost with exactly what the
  tools use: GetDocuments / OpenDocument, the decompiler, running a
  mutating tool on the UI thread, and the rename hooks (ApplyRename
  re-sorts and redraws the tree node and rebuilds open tabs;
  RefreshTreeNode / RefreshDecompiledViews for the rest). DnSpyMcpHost
  implements it with the same dnSpy calls the tools made before, so the
  tree/tab code now lives in one file.
- JSON-RPC dispatch (initialize / tools / resources, error codes) moves
  from McpServer into McpDispatcher, which has no transport code; the
  HTTP server delegates to it and a stdio host can too.
- McpSettings.Logged exposes each log line, so a host whose stdout is the
  protocol channel can mirror the log to stderr.
- Removes RenameClassOrEnumSymbolCore and CreateRenameTypeResult: dead
  since rename_symbol_by_token routed types through RenameAnyTypeSymbol.
- InternalsVisibleTo for the upcoming dnSpy.Extension.MCP.Headless, and
  headless/ is excluded from the extension's compile glob.

E2E: net10 240 pass / 0 fail.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
dnSpy.Extension.MCP.Headless.exe serves the same 32 tools and 6
resources over the MCP stdio transport with no dnSpy window, so an MCP
client (Claude Desktop, Claude Code, Chatbox, Cursor, codex, ...) can
start it on demand instead of the user keeping dnSpy open with the
server enabled. Follow-up to the headless request on #24.

- headless/: Program finds the dnSpy installation it is deployed in,
  installs an assembly resolver (dnSpy's bin, bin\Extensions\...), then
  wires HeadlessMcpHost -> McpTools -> McpDispatcher -> StdioTransport.
  HeadlessMcpHost implements IMcpHost the way dnSpy's document service
  works with memory-mapped I/O off: files are read into memory (targets
  stay unlocked), wrapped as DsDotNetDocument, cached in the resolver,
  PDBs loaded; the C# decompiler comes from dnSpy.Decompiler.ILSpy.Core
  exactly as dnSpy.Console.exe gets it. stdout is kept for the protocol
  and Console.Out goes to stderr with the log. Positional arguments
  preload files/folders through open_files; --dnspy, --quiet, --version.
- deploy-headless.ps1 lays it out like dnSpy.Console.exe in each bundle:
  the exe next to dnSpy.Console.exe, the net10 DLL next to
  dnSpy.Console.dll (AppHostPatcher -d bin for build.ps1's layout), and
  the runtime configuration copied from the bundle (net48
  dnSpy.exe.config, net10 dnSpy.Console.runtimeconfig.json), so it is
  framework-dependent or self-contained exactly as the bundle is. The
  apphost must match the bundle's architecture (an x64 apphost can't
  load the win-x86 bundle's hostfxr): release.yml builds one per RID and
  the script refuses a mismatch.
- release.yml deploys it into all three zips; build.yml builds it.
- run-tests.ps1 -Headless runs the whole suite over stdio against the
  deployed host (skipping only the HTTP step [23]), plus a clean-exit
  check when stdin closes.
- README (EN + zh-CN): a Headless mode section with client configs. It
  replaces the Claude Desktop example, which used a nonexistent
  "command": "http". CLAUDE.md documents the host and its rules.

Verified: GUI E2E net10 240/240 and net48 240/240; headless E2E net10
239/239 and net48 239/239; the official @modelcontextprotocol/sdk stdio
client against the net48, net10 and self-contained win-x64 / win-x86
bundles, built locally with dnSpy's build.ps1 (COREHOST_TRACE confirms
each self-contained bundle runs on its own bundled hostfxr).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
CLAUDE.md: keep this branch's architecture bullets and slot #25's session
adoption note under McpServer.cs; the test-fixture line names [36] too.
run-tests.ps1: [36] tests Streamable HTTP sessions, so it is skipped
under -Headless like [23].

E2E on this tree: GUI net10 245/245, net48 245/245; headless net10
239/239, net48 239/239.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The README grew around the in-dnSpy server, with headless bolted on as
one section and client setup spread over four places. Rewrite both
languages (489 -> 325 lines each) around the two ways to run it:

- A headless vs. inside-dnSpy comparison up front, and a quick start
  that offers both.
- One "Connecting a client" section: Claude Code (user / project scope,
  .mcp.json and its one-time approval, how to check), Claude Desktop
  step by step (Edit Config, full restart, where the host's log lands),
  and other clients, including codex's `mcp add`. Checked against
  Claude Code 2.1.280 (`claude mcp add` + `claude mcp list` report the
  net10 and net48 hosts as Connected) and codex-cli 0.142.5 (both config
  forms parse).
- One line per tool instead of a paragraph; the curl walkthroughs,
  architecture notes and technical details (all in CLAUDE.md) cut down
  to a transport table and a pointer.
- Development says changes must pass the end-to-end suite in both
  modes, and CLAUDE.md records that as a convention.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@KernelErr
KernelErr merged commit 1893d84 into main Sep 23, 2026
3 checks passed
@KernelErr
KernelErr deleted the feature/headless-stdio branch September 23, 2026 17:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant