Skip to content

Adopt unknown Streamable HTTP session ids - #25

Merged
KernelErr merged 1 commit into
mainfrom
fix/issue-24-unknown-session-id
Sep 23, 2026
Merged

KernelErr merged 1 commit into
mainfrom
fix/issue-24-unknown-session-id

Conversation

@KernelErr

Copy link
Copy Markdown
Owner

…restart

Fixes #24. Streamable HTTP sessions live only in dnSpy's memory, so after dnSpy is closed and reopened, a client that kept its Mcp-Session-Id got 404 "Unknown Mcp-Session-Id" on every call. The spec says the client must then re-initialize, but the official TypeScript SDK (which Chatbox is built on) just throws "Error POSTing to endpoint: Unknown Mcp-Session-Id" and keeps the stale id, so the server stayed unusable until the user reconnected by hand. Chatbox's connection test passed because it opens a fresh session each time.

A session carries no state here (each POST is answered inline; the session object is just the id), so POST and the standalone GET stream now adopt an id this process never issued instead of refusing it. Only an id the client explicitly ended with DELETE is still refused with 404, which keeps the spec's behaviour for real terminations.

Reproduced with @modelcontextprotocol/sdk 1.30.0 against a real dnSpy restart (connect, call a tool, restart dnSpy, call again with the same client): before, the second call failed with exactly the reported error and the GET stream's reconnect got 404; after, both succeed. run-tests.ps1 step [36] covers adoption over POST and GET plus the DELETE case. E2E: net10 245 pass / 0 fail, net48 245 pass / 0 fail.

…restart

Fixes #24. Streamable HTTP sessions live only in dnSpy's memory, so after
dnSpy is closed and reopened, a client that kept its Mcp-Session-Id got
404 "Unknown Mcp-Session-Id" on every call. The spec says the client must
then re-initialize, but the official TypeScript SDK (which Chatbox is
built on) just throws "Error POSTing to endpoint: Unknown Mcp-Session-Id"
and keeps the stale id, so the server stayed unusable until the user
reconnected by hand. Chatbox's connection test passed because it opens a
fresh session each time.

A session carries no state here (each POST is answered inline; the
session object is just the id), so POST and the standalone GET stream now
adopt an id this process never issued instead of refusing it. Only an id
the client explicitly ended with DELETE is still refused with 404, which
keeps the spec's behaviour for real terminations.

Reproduced with @modelcontextprotocol/sdk 1.30.0 against a real dnSpy
restart (connect, call a tool, restart dnSpy, call again with the same
client): before, the second call failed with exactly the reported error
and the GET stream's reconnect got 404; after, both succeed.
run-tests.ps1 step [36] covers adoption over POST and GET plus the
DELETE case. E2E: net10 245 pass / 0 fail, net48 245 pass / 0 fail.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@KernelErr KernelErr changed the title Adopt unknown Streamable HTTP session ids so clients survive a dnSpy … Adopt unknown Streamable HTTP session ids Sep 22, 2026
@KernelErr
KernelErr merged commit 2e94460 into main Sep 23, 2026
3 checks passed
@KernelErr
KernelErr deleted the fix/issue-24-unknown-session-id branch September 23, 2026 17:24
KernelErr pushed a commit that referenced this pull request Sep 23, 2026
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>
KernelErr added a commit that referenced this pull request Sep 23, 2026
* Decouple the tools from dnSpy's GUI services behind IMcpHost

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.

* Add a headless host that serves the tools over stdio, without dnSpy

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 steps [23] and [36]), 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).

* docs: headless needs no arguments; the AI loads targets with open_files

* docs: streamline the README around the two ways to run it

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.

* Merge main (#25): step [36] tests Streamable HTTP sessions, so it is
  skipped under -Headless like [23].

E2E on the merged code: GUI net10 245/245 and net48 245/245; headless
net10 239/239 and net48 239/239.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
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.

chatbox的ds提示会话失效

1 participant