Skip to content

Surface MCP tool-discovery failures instead of running tool-less and silent - #434

Merged
TonsOfFun merged 2 commits into
mainfrom
fix/mcp-discovery-errors
Sep 11, 2026
Merged

Surface MCP tool-discovery failures instead of running tool-less and silent#434
TonsOfFun merged 2 commits into
mainfrom
fix/mcp-discovery-errors

Conversation

@TonsOfFun

@TonsOfFun TonsOfFun commented Sep 11, 2026

Copy link
Copy Markdown
Contributor

Closes #425.

MCPToolDispatcher#tool_definitions rescued a failed tools/list to [] and logged a warning, so a server that 401s and a server that legitimately serves no tools were indistinguishable. The agent then ran tool-less, the model fabricated an answer, and the run reported a plausible low score.

#425 records that costing several hours to diagnose, because the report offered "improve the instructions" recommendations for what was a transport failure. The only evidence was two logger.warn lines.

What changed

Contributing nothing still keeps the run alive — that part was deliberate and is unchanged. What was missing is the reason:

  • discovery_errors — why each declared server contributed no tools, naming the server, its URL and the underlying error:

    Cannot load tools from MCP server 'records' (https://host.example/mcp/records): 401 Unauthorized. Check its URL, transport and credentials.

  • all_servers_failed? — true when every declared HTTP server failed discovery. That is the case where a tool-backed scenario cannot produce a meaningful score, so a caller can fail loudly instead of grading an invented answer.

Errors reset per tool_definitions call, so a transient failure does not leak into a later run. An agent declaring no servers is not "failed" — its tool-less execution is the configured behaviour.

Relationship to #421

This takes the approach from the unmerged #421 (MCPToolBinding's @discovery_errors), narrowed to the dispatcher on current main. #421 is CONFLICTING/DIRTY and 54 commits behind, and #420 already solved execution a different way — so rebasing it wholesale was not worth it, while this idea was.

Tests

5 new tests, all verified to fail without the change (NoMethodError: undefined method 'discovery_errors').

  • actionagent/test: 345 runs, 0 failures (was 340)
  • test/evals: 115 runs, 0 failures
  • RuboCop clean

Follow-up

Surfacing these in the evaluation report is what turns "improve the instructions" back into "records returned 401". The scoring-side counterpart — that a fabricated answer raises no fault at all, so the judge is never asked for a tool — is #433.

🤖 Generated with Claude Code

https://claude.ai/code/session_01He34kWksjpqPPDpCCqJq2C


Update — second commit

093f8b4b makes something actually read the recorded errors, and fixes a related understatement:

The run now says when discovery failed. A run whose declared MCP servers could not be reached scored an agent with no tools to call — the model answered from its own weights, so the scores are real but meaningless. error_message now carries that, naming each server and its error, and distinguishes every server failing from some. The column is already rendered by the evaluations API (:362), so it reaches the UI without new plumbing.

The diagnosis roster was understated. tool_roster listed only AgentToolbox definitions, so an agent whose tools come from MCP servers was diagnosed against a roster those tools were missing from — Diagnosis would report "none of the available tools covers this task" while naming an incomplete list. It now includes the dispatcher's discovered tools, matching what AgentExecutionService#tool_schemas offers the provider.

Tests: actionagent/test 347 runs, 0 failures (main: 340). All new tests verified to fail without their implementation. RuboCop clean.

A failed tools/list was rescued to [] and logged as a warning, so a server
that 401s and a server that legitimately serves no tools were
indistinguishable. The agent then ran tool-less, the model fabricated an
answer, and the run reported a plausible low score — #425 records that
costing hours to diagnose, because the report recommended prompt changes
for what was a transport failure.

Contributing nothing still keeps the run alive; that part was deliberate
and is unchanged. What was missing is the reason, so callers can tell the
two cases apart:

- discovery_errors returns why each declared server contributed no tools,
  naming the server, its url and the underlying error.
- all_servers_failed? reports the case where nothing could be discovered
  at all, which is when a tool-backed scenario cannot produce a meaningful
  score and a caller should fail loudly rather than grade an invented
  answer.

Errors reset per tool_definitions call, so a transient failure does not
leak into a later run.

This takes the approach from the unmerged #421 (MCPToolBinding's
@discovery_errors), narrowed to the dispatcher on current main rather than
rebasing that branch across 54 commits of divergence.

Surfacing these in the evaluation report is the follow-up, and is what
turns "improve the instructions" back into "sparkle-diagnostic returned
401". Filed as the scoring-side counterpart in #433.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01He34kWksjpqPPDpCCqJq2C
@TonsOfFun
TonsOfFun force-pushed the fix/mcp-discovery-errors branch from bcdd6b0 to 321c314 Compare September 11, 2026 02:30
…real tool roster

Builds on the dispatcher change: recording why a server failed is only useful
if something reads it.

Two fixes to the scenario evaluation runner:

**The run now says when discovery failed.** A run whose declared MCP servers
could not be reached scored an agent with no tools to call — the model
answered from its own weights, so the scores are real but meaningless.
error_message now carries that, naming each server and its error, and
distinguishes "every server failed" from "some failed". The column is already
rendered by the evaluations API (:362), so it reaches the UI without new
plumbing.

**The diagnosis roster was understated.** tool_roster listed only
AgentToolbox definitions, so an agent whose tools come from MCP servers was
diagnosed against a roster those tools were missing from — Diagnosis would
report "none of the available tools covers this task" while naming an
incomplete list. It now includes the dispatcher's discovered tools, matching
what AgentExecutionService#tool_schemas actually offers the provider.

347 runs, 0 failures. Both new tests verified to fail without the change.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01He34kWksjpqPPDpCCqJq2C
@TonsOfFun TonsOfFun changed the title Record why an MCP server's tools/list failed, instead of a silent empty list Surface MCP tool-discovery failures instead of running tool-less and silent Sep 11, 2026
@TonsOfFun
TonsOfFun merged commit 40ce5d4 into main Sep 11, 2026
8 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.

MCPToolDispatcher swallows tools/list failures, so agents run tool-less and fabricate results

1 participant