Repository navigation
fix(mcp): keep loaded tools when their server goes away, and never stop silently - #60
Merged
Merged
Conversation
…op silently Deleting an MCP server cascade-deleted its catalog row, which is where the disclosure middleware rebuilt loaded tools from each turn. The tools vanished from the model's list, the model called one from memory with no schema, and the unparseable call ended the turn with no error shown. - Disclosure saves each loaded tool's definition in the thread (`toolDefinitions`, first definition wins) and builds the loaded tools from it, so the tool list never shrinks. A call to a tool whose server was removed or turned off returns `server_unavailable` instead of falling through to the tool node. Threads from before keep the catalog fallback. - The stream also takes tool results from the run's state, so a result a middleware returned itself (server_unavailable, auth_required, unknown_tool) arrives live and is no longer reported as pending approval. - invalidToolCallMiddleware re-asks the model when a tool call's arguments don't parse, with a correction never saved to the thread (invalid_arguments, or unknown_tool for a tool not offered). After two retries the reply ends with a visible notice, and the stream shows the notice live. Invalid calls are read from both `invalid_tool_calls` and v1 `invalid_tool_call` content blocks. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Deleting an MCP server mid-thread made the agent stop with no error. The cause was a chain of three gaps, and this fixes each one.
activeTools) and rebuilt each definition from the server's catalog row on every turn. Deleting a server cascade-deletes that row, so its loaded tools silently left the model's tool list. That also contradicted the package's own rule that loaded tools are never removed, which exists to protect the prompt cache.invalid_tool_call, which the agent never runs. With no valid calls, the turn just ended, and the UI showed nothing.messageStream.tsonly saw results from the tool-execution stream. A result the middleware answered directly showed up only after a reload, and the call was reported aspendingToolCallIds, which is the approval signal.Changes
Disclosure: loaded tools outlive their server (
packages/mcp-disclosure)toolDefinitions. The first definition saved for a name is kept, so what the model is shown never changes.wrapModelCallbuilds loaded tools from those saved definitions. Threads saved before this change hold names only, so they fall back to the catalog, andload_toolsback-fills their definitions.server_unavailable, naming the server and pointing to the Connections page. Before, it fell through to the agent's tool node, which has no MCP tools.!catalogBlockearly return dropped them.unknown_tool.Stream: results the middleware returned itself (
src/services/messageStream.ts)run.valuessnapshots and from the final state, limited to calls made in this run.Malformed tool calls are retried (
src/lib/agent/invalidToolCalls.ts)invalidToolCallMiddlewaresits just inside disclosure, so it sees the full tool list. When a response contains a call whose arguments don't parse, it re-asks the model, up to 2 times, with a correction that is never saved to the thread:invalid_argumentsfor a real toolunknown_toolwhen the model calls a tool it isn't offeredinvalid_tool_callsand from v1invalid_tool_callcontent blocks. The live test showed that the v1 output format leaves the field empty.Test plan
tsc --noEmit,pnpm typecheck:eval,pnpm test(488 passing, 16 new), and format checkserver_unavailablelive with no false pending, and the agent told the user to re-enable Notionnotesserver deleted): malformed calls showed a notice instead of a silent stop, and a later run answered thatnotesisn't availableunknown_toolcorrection live. The model didn't produce a malformed call on the run where it would have applied, so this path is covered by unit tests only.pnpm build, skipped locally because the dev server was using the same.nextfolderNotes
🤖 Generated with Claude Code