Skip to content

fix(mcp): keep anticipated tool failures readable under mcp 2.1 - #452

Merged
jimisola merged 2 commits into
mainfrom
fix/mcp-anticipated-errors-reach-the-model
Aug 29, 2026
Merged

fix(mcp): keep anticipated tool failures readable under mcp 2.1#452
jimisola merged 2 commits into
mainfrom
fix/mcp-anticipated-errors-reach-the-model

Conversation

@jimisola

Copy link
Copy Markdown
Member

main is red. Its HEAD is the merge commit of #450 (mcp 2.0.0 → 2.1.0), whose build failed — one of the three PRs that landed without waiting for CI in reqstool/.github#111.

Cause

mcp 2.1 narrowed which exceptions reach the client. From the SDK's own docstring:

Any other exception bar MCPError (a protocol error) is treated as a crash: the model sees only Error executing tool <name>, and the server logs the traceback at ERROR.

Only a ToolError keeps its message. The server raised ValueError/RuntimeError for failures it fully anticipated — "Requirement X not found", "sources changed but reloading them failed" — so every one of them was masked. An agent asking for a mistyped REQ_TYPO got Error executing tool get_requirement and no way to see the typo.

This fix raises ToolError at the eight anticipated-failure sites. As a side effect they also drop from ERROR-with-traceback to INFO in the server log, which is where an anticipated failure belongs.

Nothing else changes: a snapshot that cannot be reloaded is still an error and never a fallback to superseded data (bug #437), and startup failures stay RuntimeError because they abort the server rather than answer a tool call.

The regression was six times wider than the test that caught it

Reverting only server.py and keeping the tests fails 8 tests, spanning get_requirement, get_svc, get_mvr, get_requirement_status and get_status. main caught it with one.

The other seven passed on main because they asserted result.is_error and stopped there — true, but blind to whether the model can tell why the call failed. They now assert the message survives too, so a future SDK bump cannot re-mask every error message behind a still-green suite.

Verification

  • 987 passed, 2 skipped (main is 986 passed, 1 failed); black --check and flake8 src tests clean.
  • Fix reverted, tests kept → 8 failures; fix restored → green. That is what shows the tests actually bind the behaviour rather than describing it.
  • CLI output unchanged — the change touches only MCP tool error paths — spot-checked with status against test_basic/baseline/ms-101.
  • mcp 2.1.1 carries the identical branch in tools/base.py, so this also unblocks fix(pypi): update dependency mcp to v2.1.1 #451, which is currently sitting open and red for the same reason.

Note

#451 being open-and-red is the reqstool/.github#111 fix working: autoMergeRequest is now None, renovate-approve[bot] still supplies the approving review, and Renovate is holding the merge because build is failing. Its three predecessors merged in 4–28 seconds with red builds.

Refs reqstool/.github#111

The mcp 2.1 SDK narrowed which exceptions reach the client: only a
ToolError keeps its message, and everything else is treated as a crash
and reported as a bare "Error executing tool <name>". The server raised
ValueError/RuntimeError for failures it fully anticipated, so all of
them were masked -- an agent asking for a mistyped REQ_TYPO got
"Error executing tool get_requirement" and no way to see the typo.

Raise ToolError at the eight anticipated-failure sites instead, which
also drops them from ERROR-with-traceback to INFO in the server log.

Reverting only the source change fails eight tests, but main caught
this with one: the others asserted is_error without asserting that the
reason survives. They now assert the message too, so a future SDK bump
cannot re-mask them silently.

Refs reqstool/.github#111

Signed-off-by: Jimisola Laursen <jimisola@jimisola.com>
get_urn_details and enrich_document raise ToolError like the rest, but
neither had any error-path test, so the mcp 2.1 masking would have gone
unnoticed there even with the rest of the suite strengthened.

Both fail against the pre-fix server, so they bind the behaviour rather
than describe it. refresh's reload-failure path is left uncovered --
provoking it needs a project that builds once and then cannot.

Refs reqstool/.github#111

Signed-off-by: Jimisola Laursen <jimisola@jimisola.com>
@jimisola
jimisola merged commit 2c4739b into main Aug 29, 2026
8 checks passed
@jimisola
jimisola deleted the fix/mcp-anticipated-errors-reach-the-model branch August 29, 2026 20:34
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