DeepSeek Harness MCP client: vanishing tools, no reconnect
When DeepSeek Harness (DSH) connects to MCP servers, almost all failures happen in "the second stage after connecting" — registration and refresh, not the connection itself. This batch of threads has four typical forms, and each has a very recognizable log criterion: an entire family of tools vanishes (during the registration generation switch the rollback is incomplete, and "contains" semantics swallow the error, so some server ends the round with 0 tools while the old generation is already destroyed, recoverable only by restarting the host); session invalid without reconnect (the reconnect logic hangs off the transport-close callback, and "session expiry" does not close the transport — it only goes through the error callback, so every call reports No valid session ID provided); new tools invisible (the auto-refresh switch is written as a literal, off, and list_changed notifications are wasted); a pagination cursor never obtainable (a call without a cursor is handed to the SDK's auto-aggregation path, which merges multiple pages and deletes nextCursor) (#9864, #9740, #9625, #9374).
DSH plugin entire families of tools vanish: incomplete registration rollback
The one-line conclusion: at the generation switch the old generation is destroyed before the new one is registered; once registration throws, the rollback covers only the new generation, and the config uses "contains" semantics — the error is swallowed and an empty set returned, so this server ends the round with zero tools while its old generation is already gone. The symptom is "the server process is alive, resources list, a bare handshake lists 14 tools, but there is not a single mcp__<server>__* in the agent tool surface", recovering only after a host restart (#9864).
DSH plugin three steps to locate it: judge the stage by searchable log lines
- Do the bare-handshake comparison first: connect to that server directly with an MCP client and list tools — being able to list them means the server side is fine and the problem is in the host-side registration stage (#9864).
- Tell the stage apart by log line. The thread gives two grep-able log kinds: one is "registration failed, no tools registered" (the generation-switch/registration stage), the other is "resync failed" (the reconnect stage). The two have different causes and different handling (#9864).
- Confirm the consequence of "contains" semantics: if the log shows the server registered 0 tools this round rather than "a few missing", this is the case (#9864).
DSH plugin handling
- Immediate recovery: restart the host (the only effective means) (#9864).
- Reduce the trigger: make the same server not provide same-named tools on two tool surfaces at once, avoiding a registration conflict (#9864).
- The plugin-side approach and its cost: you can patch the tool registration interface to downgrade a conflict to non-fatal — but that trades "roll back the whole generation" for "drop only the conflicting name", a consistency-for-availability trade-off (#9864).
DSH plugin session invalid without reconnect: the error only goes through onerror, the transport is not closed
The one-line conclusion: the reconnect logic hangs only off the transport close event, and "session invalid" does not close the transport — the SDK reports it only through the error callback, so reconnect is never triggered and the tool closure stays bound forever to the dead generation of the connection. The symptom is every call reporting Bad Request: No valid session ID provided or 404 Session not found, recovering only after restarting DSH (#9740).
DSH plugin criterion and one easily missed trigger
- Criterion: the error clearly mentions an invalid session (400 or 404), and the server process may have restarted; restarting DSH recovers it immediately (#9740).
- Do not stare only at "the server restarted": the thread later corrected the trigger — session expiry from long idleness triggers it too (the magnitude mentioned in the thread is 12 hours idle), so "I did not restart anything" does not rule this out (#9740).
DSH plugin three ways to handle it
- Immediate recovery: restart the host process (the bridge is shared at the process level, which is also why a single session cannot self-heal) (#9740).
- Community patch:
MaxHou-infinity/dsh-mcp-session-recoverysubscribes to the error callback, does a narrow match on "session lost"-style errors, then reuses the existing connection-offline flow to trigger a reconnect — the key is the narrow match, to avoid treating every error as a disconnect (#9740). - Mitigate the idle trigger: avoid letting a stateful server be completely idle for a long time (for example, keep light periodic interaction), or prefer servers with a stateless transport form (#9740).
DSH plugin new tools invisible: list_changed auto-refresh is hardwired off
The one-line conclusion: at construction the client hardcodes "auto-refresh of tool-list changes" to off (a literal, not a config item); although the machinery that "refreshes on a change notification" still exists, it never fires because the switch is off. The symptom is that a running session never sees tools added to a server at runtime, reporting unknown tool "mcp__<server>__<newTool>", while a new session sees it immediately (#9625).
DSH plugin four steps to handle it
- Confirm the criterion: for the same server, the current session cannot see the new tool while a new session can; that is this case (#9625).
- Distinguish the two switches: the thread specifically warns that "auto-refresh" and "the refresh callback after a notification" are two switches — the presence of the latter does not mean the former is on; do not conclude "the logic is there, it just did not fire" from seeing the callback (#9625).
- Workaround: restart DSH (the reconnect path forces a sync once) (#9625).
- Mind the plugin's boundary: a developer notification is emitted when the session tool surface changes, but an out-of-tree plugin cannot reach the generation of the connection inside the closure, so it cannot turn it on from outside (#9625).
DSH plugin pagination cursor: a call without a cursor is swallowed by auto-aggregation
The one-line conclusion: when the caller passes no cursor, the request is handed to the SDK's auto-aggregation path — it merges all pages and deletes the nextCursor field, so the contract's "one page plus a cursor" is never obtainable; when the page count exceeds the internal cap of 64 it errors outright. The symptom is list_mcp_resources / list_mcp_resource_templates never returning a cursor, and servers with many pages reporting exceeded listMaxPages (64); server pagination did not terminate (#9374).
DSH plugin four steps to handle it and one contract change to note
- Confirm the criterion: the result never has a
nextCursorfield; with many pages it reports the above error (#9374). - Temporary detour: reduce the amount of data in play (shrink the server-side per-page size or reduce the resource count), or use a client that talks to the server directly to fetch the resource list (#9374).
- The fix (given in the thread): request one page at a time and return the cursor verbatim; the author provides a branch that changes only the connection layer (#9374).
- Be aware of the contract change: after switching to "one page + cursor", the call's returned volume goes from "all pages" to "one page" — this is a behavior change, and downstream consumers that depend on it need to adjust accordingly (#9374).
DSH plugin troubleshooting notes
The MCP troubleshooting mantra is: do a bare handshake to confirm the server side first, then use the logs to judge "registration stage or reconnect stage", and only then suspect the server implementation. Eight points:
- The bare-handshake comparison rules out half the possibilities in one shot, at the lowest cost (#9864).
- "A whole family drops to zero" and "a few are missing" are two causes: the former is a rollback defect, the latter is usually conflict downgrading (#9864).
- Session invalidation does not close the transport, so a reconnect never happens (#9740).
- Idleness also invalidates a session, not only a server restart (#9740).
- "A new session sees it but the current one does not" = a refresh-switch problem, the easiest criterion here (#9625).
- Do not conflate the two switches: auto-refresh ≠ change callback (#9625).
- A missing pagination cursor is eaten by the middle layer, not withheld by the server (#9374).
- State the fix status honestly: all four are unfixed in the versions discussed (
0.2.0-rc.2~0.2.1-alpha.*); the session reconnect and the pagination cursor have community patches/branches to reference, but none are merged, so they remain user-side workarounds.
MCP problems like "tools that come and go" most need a clear view of "which servers are actually connected and what the tool surface looks like". Instead of typing commands everywhere, use a visual marketplace like DSH Plugin Hub: the installed list, install/uninstall history, and system logs sit in one panel, so you can see the environment clearly first.

Source: Discussion #9864, Discussion #9740, Discussion #9625, Discussion #9374.
FAQ
In DeepSeek Harness, this comes from a rollback defect in the tool registration stage. When switching generations the client destroys the old generation before registering the new one; once registration throws a conflict error, the rollback covers only the new generation, and the misconfiguration is 'contains' semantics — it swallows the error and returns an empty set, so that server ends this round with zero tools while the old generation is already destroyed. The criterion is the searchable log lines: 'registration failed and no tools registered' and 'resync failed' are two different stages.
In DeepSeek Harness, this is because the client reconnects only on transport close events, and 'session invalid' does not close the transport: the SDK reports it only through the error callback, so the reconnect logic is never triggered and the tool closure stays bound forever to the dead generation of the connection. Known triggers include not only server restarts but also session expiry from long idleness. The only immediately workable recovery is restarting the host; the long-term direction is to subscribe to the error callback and reconnect after a narrow match on the specific error.
In DeepSeek Harness, this is because the auto-refresh switch is written as a literal instead of a config item. At construction the client hardcodes the auto-refresh of tool-list changes to off; although the machinery that 'refreshes on a change notification' still exists, it never fires because the switch is off. The criterion is that the same server's new tool is invisible in the current session but immediately visible in a new session; the workaround is restarting the host, and the correct fix is to make that switch configurable or on by default.
In DeepSeek Harness, this is because a call without a cursor is handed to the SDK's auto-aggregation path: it merges all pages and deletes the cursor field, so the contract's 'one page plus a cursor' is never obtainable; when the page count exceeds the internal cap of 64 it errors. The fix given in the thread is to request one page at a time and return the cursor verbatim; until then, you can reduce the per-page size or use a client that talks to the server directly to fetch the resource list.
In DeepSeek Harness, do one bare-handshake comparison: connect to that server with an MCP client directly and list its tools. If the bare handshake sees the tools but DSH does not, the problem is on the host side (registration or refresh); if the bare handshake cannot list them either, the server itself does not expose them. This step saves a lot of guessing, especially for the 'entire family of tools vanishes' class, which can only be judged by stage from the logs.
Related Terms
- MCP (Model Context Protocol)
- MCP is an open protocol for wiring external tools and resources into a model. DeepSeek Harness connects to MCP servers through mcp-client and registers the tools a server exposes into the session tool surface. Connecting and registering are two stages, and most failures in this family occur in the latter.— https://github.com/deepseek-ai/deepseek-harness/discussions/9864
- list_changed notification
- list_changed is the MCP protocol's notification by which a server tells the client 'my tool list changed'. The client must explicitly turn on the auto-refresh switch to re-fetch on that basis; otherwise a running session will not see tools that were added or removed.— https://github.com/deepseek-ai/deepseek-harness/discussions/9625
- nextCursor
- nextCursor is the cursor in an MCP paginated response that points to the next page, and the client fetches page by page under the 'one page plus one cursor' contract. If a middle layer merges multiple pages and deletes that field, the caller can never get a cursor and cannot page further.— https://github.com/deepseek-ai/deepseek-harness/discussions/9374
- generation.onclose
- generation.onclose is the callback in DeepSeek Harness's MCP client for sensing that a connection's lifecycle has ended, and the reconnect logic hangs off it. Errors like session invalidation do not close the transport, so they never trigger it and no reconnect happens.— https://github.com/deepseek-ai/deepseek-harness/discussions/9740
Sources
- #9864 — mcp-client tool registration silently fails: the server is alive and resources list, but an entire family of tools vanishes (only a host restart recovers it)· deepseek-ai (GitHub Discussions)
- #9740 — mcp-client: stateful MCP server restart leaves every tool call failing until DSH restarts· deepseek-ai (GitHub Discussions)
- #9625 — A running session cannot see tools added to an MCP server at runtime (list_changed is hardwired off)· deepseek-ai (GitHub Discussions)
- #9374 — list_mcp_resources never returns nextCursor; over 64 pages it errors outright· deepseek-ai (GitHub Discussions)