Skip to content

fix(mcp): keep deferred tools callable by name - #571

Merged
zzet merged 1 commit into
mainfrom
fix/mcp-call-time-tool-filter
Aug 12, 2026
Merged

fix(mcp): keep deferred tools callable by name#571
zzet merged 1 commit into
mainfrom
fix/mcp-call-time-tool-filter

Conversation

@zzet

@zzet zzet commented Aug 12, 2026

Copy link
Copy Markdown
Owner

The report

From community chat, on v0.63.0:

gortex call export_context --arg task="all command cli options flags"
Error: tool 'export_context' not found: tool not found

while gortex tools list shows export_context as a registered, readonly tool.

Scope

Not CLI-specific and not one tool. Reproduced at HEAD: 141 of 178 tools are uncallable. Live (core-preset) tools work, every deferred one fails:

get_repo_outline  => {...}            # live
graph_stats       => {...}            # live
export_context    => tool not found   # deferred
audit_health      => tool not found
distill_session   => tool not found
find_clones       => tool not found

gortex context, the dedicated verb for the same handler, is equally dead. Agent sessions are affected too: any tool tools_search promotes stays uncallable for the rest of the session.

Root cause

toolSurfaceFilter is registered via server.WithToolFilter to shape tools/list visibility. mcp-go v0.55.1 started re-running every registered filter at call time (server.passesToolFilters) against the single requested tool, and reports a filtered tool as tool '<name>' not found: tool not found — the reported string. v0.54.1 has no such call.

The v0.54.1 → v0.55.1 bump landed in bd5b44b and first shipped in v0.57.0, so every release since has carried this.

Promote-on-demand was never broken: the dispatcher promoted the tool into the live server and the call-time filter dropped it afterwards. tool_profile still reports status: deferred, enabled: true, which the call then contradicts.

internal/hooks/daemon_tool.go already dials with Tools:"full" and its comment describes this exact symptom (the Stop briefing's contracts section silently rendering nothing) — a workaround for the symptom, with the cause unfound until now.

Why CI stayed green: daemon_mcp_promote_test.go only covers the frame-name peek, and lazy_tools_e2e_test.go dispatches on a session-less context.Background(), where the filter is permissive. Neither exercises a session with a forwarded preset, which is what every real client sends.

The fix

The daemon dispatcher marks a tools/call whose name already passed IsToolEnabledForSession (WithAuthorizedToolCall), and toolSurfaceFilter returns early when it sees that marker alongside a single-tool slice matching it.

No surface is widened. The marker is only set for names the session's effective policy permits, and checkToolGate remains the authoritative per-call gate — hide-mode, facade-v1, planning, workflow and host exclusions are unchanged. The streamable-HTTP dispatcher delegates to the same mcpDispatcher.Dispatch, so it is covered.

Tests

internal/mcp/promote_call_gate_test.go:

  • a by-name call under core/defer reaches the handler (fails before this change with the reported error)
  • a hide-mode session still refuses a non-preset tool, with its structured tool_blocked_by_mode error rather than a misleading "not found"
  • tools/list stays narrow while the marker is set, so the carve-out cannot leak a tool into the visible surface

Verification

  • go test -race ./internal/mcp/ ./cmd/gortex/ clean
  • golangci-lint run ./internal/mcp/... ./cmd/gortex/... — 0 issues
  • end-to-end against an isolated daemon built from this branch: gortex call export_context and gortex call find_clones both return results

Note for release

Users on v0.57.0 and later have had roughly two thirds of the tool surface unreachable from the CLI and from any session that forwarded a preset. Worth a patch release rather than waiting for the next minor.

mcp-go v0.55.1 began re-running registered tool filters at call time
(server.passesToolFilters) with the single requested tool, answering
"tool '<name>' not found: tool not found" when a filter drops it.

toolSurfaceFilter shapes tools/list visibility, not call authorization —
so every tool outside a session's preset became uncallable: 141 of 178
tools under the `core` default. `gortex call <tool>` failed for any
non-core name, and an agent session could no longer call a tool that
tools_search had just promoted. Promote-on-demand still ran; the promoted
tool was then filtered out at dispatch.

The dispatcher now marks a tools/call whose name already passed
IsToolEnabledForSession, and the surface filter lets that single-tool
probe through. This does not widen any surface: the marker is only set
for names the session's effective policy permits, and checkToolGate
remains the authoritative per-call gate — hide-mode, facade, planning,
workflow and host exclusions are unchanged.

Tests cover the by-name call under core/defer, hide-mode still refusing a
non-preset tool with its structured error, and tools/list staying narrow
while the marker is set.
@zzet
zzet merged commit 375643c into main Aug 12, 2026
17 of 18 checks passed
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