MCP lifecycle hardening, dispatch-only tool sources, compose spec caching (0.10.1) - #9
Merged
Conversation
Dispatch-only catalogs: specs() empty, get() passes through, inner catalog events drained but not forwarded. Fills the gap between Filtered (hides dispatch too) and fully advertised sources — needed for compose children the model should invoke through scripts without seeing individually.
- disconnect_server: unregister tools and emit ServerDisconnected even when close() fails; error is advisory. Previously a close failure left stale, still-callable adapters in the catalog with no retry path (UnknownServer on reattempt). - unregister_server: new API to fully remove a server (config, options, auth) so detached servers stop accreting and cannot be resurrected by bulk connects. - connect_servers_settled: settle a chosen subset in parallel, idempotent per server — building block for lazy on-demand connection strategies. connect_all_settled now delegates to it and no longer re-handshakes live servers. - close replaced connections on overwrite instead of dropping them silently (leaked Streamable HTTP sessions). - map IO/transport/timeout call failures to ToolError::Unavailable so callers can trigger reconnects without string-matching. - McpServerHandle::tool_names(): list namespaced names without building adapters.
specs() re-rendered every child's output schema into the compose description on each call — once per model step — even for frozen catalogs. Cache the rendered spec and invalidate on drain_catalog_events, the same signal the loop uses to refresh the model-visible catalog, so the cache is never staler than the model's view. Document Unadvertised as the supported way to keep children dispatchable without enumerating them.
agent-client-protocol enables serde_json preserve_order for the whole workspace, so map serialization order depends on the build graph — snapshots recorded pre-ACP mismatched deterministically (permission_deny) or flaked by binary (after_tool_result). Sort object keys during normalisation and refresh recordings, which also picks up the failure_kind metadata and explicit None fields stale snapshots predated.
InvalidParams/InvalidRequest/ParseError mean the caller sent a bad request — map them to ToolError::InvalidInput so the model is told to fix its arguments instead of treating the tool as broken. Only applies after the error responder declines (PassThrough) or when none is installed; server-side and custom codes stay ExecutionFailed.
Adds HttpServerHandle::shutdown so tests can kill the server midway and pin that disconnect_server leaves clean state — connection untracked, tools unregistered, ServerDisconnected emitted, config retryable — regardless of the close outcome.
The read-before-write policy is enforced inside the tool body (it needs resource state), so its denial surfaces as Failed — FailedBeforeInvocation is reserved for checker denials that stop the tool from ever starting. The assertion predated that split.
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.
Hardens the MCP server lifecycle APIs around error paths and adds the primitives a lazy, on-demand connection strategy needs — driven by findings from an agent building exactly that on top of agentkit.
MCP lifecycle (
agentkit-mcp)disconnect_serveris now state-consistent on close failure: tools are always unregistered andServerDisconnectedalways emitted; the close error is returned as advisory. Previously a failed close left stale, still-callable adapters in the catalog with no retry path (reattempt returnedUnknownServer).unregister_server(new): fully removes a server — config, options, credentials — disconnecting first if live. Configs no longer accrete forever, and detached servers can't be resurrected by bulk connects.connect_servers_settled(ids)(new): settle-connect a chosen subset in parallel. Idempotent per server (already-connected handles are returned untouched, no re-handshake), unknown ids settle asUnknownServerfailures, duplicates are attempted once.connect_all_settlednow delegates to it.McpServerHandle::tool_names()(new): list namespaced tool names without building adapters or cloning schemas.Tool error vocabulary
McpToolAdaptercollapsed every failure intoToolError::ExecutionFailed(String). Now:Io/Transport/Timeout→ToolError::Unavailable— "the connection is unhealthy", so reconnect logic can match on the variant instead of string-sniffing.InvalidParams/InvalidRequest/ParseError→ToolError::InvalidInput— "fix your arguments", same feedback path as schema violations. Applies only when noMcpErrorRespondersynthesizes a result; responder behavior is untouched.ExecutionFailed.Tool sources (
agentkit-tools-core,agentkit-tool-compose)Unadvertised<S>/ToolSource::unadvertised()(new): dispatch-only sources —specs()empty,get()passes through. Fills the gap betweenFiltered(hides dispatch too) and fully advertised sources.specs()call — once per model step. It's now memoized and invalidated bydrain_catalog_events, the same signal the loop uses to refresh the model-visible catalog, so the cache can never be staler than the model's view. Children wrapped withunadvertised()stay callable from scripts without being enumerated in the description.Tests & snapshots
HttpServerHandle::shutdown()), unadvertised dispatch end-to-end through a Lua script, spec-cache invalidation, and the error-mapping table.agent-client-protocolenables serde_json'spreserve_orderworkspace-wide, so serialization order depended on the test binary's build graph (deterministicpermission_denyfailure, flakyafter_tool_result). Stale recordings refreshed.Version bumped to 0.10.1.
Also fixes the pre-existing
agentkit-tool-fsreplace_move_and_delete_workfailure (stale assertion: in-tool permission denials surface asFailed, notFailedBeforeInvocation).