Skip to content

fix(aws-documentation-mcp-server): raise ToolError so read-path failures reach the model - #4748

Merged
alexisareyn merged 6 commits into
awslabs:mainfrom
alexisareyn:fix/surface-read-path-errors
Oct 9, 2026
Merged

alexisareyn merged 6 commits into
awslabs:mainfrom
alexisareyn:fix/surface-read-path-errors

Conversation

@alexisareyn

@alexisareyn alexisareyn commented Oct 8, 2026 •

Copy link
Copy Markdown
Contributor

Summary

MCP SDK 2.1.0 (python-sdk#3314, merged 2026-08-24) forwards only a ToolError's message to the client. Every other exception is re-raised as UnexpectedToolError with its text discarded, so the client sees a bare Error executing tool <name>. That was deliberate upstream, to keep crash details off the wire.

This server's read path raises a plain ValueError for failures it anticipates, so those messages are discarded too, even though they are written for the caller. The result is that a moved guide page, a 4xx, an unreadable index shell, and a rejected URL are all indistinguishable to the model, and the Requested <A>; served <B>. note the server carefully builds never arrives.

Reported in #4705.

Changes

Raise ToolError for anticipated failures

Add DocumentationToolError(ToolError, ValueError):

  • ToolError so the SDK forwards the message.
  • ValueError so existing callers and tests keep working unchanged. No existing test needed editing.

UnreadablePageError now subclasses it, since it escapes to tool callers for the same reason.

Converted the 23 deliberate raise sites across server_aws.py, server_utils.py and util.py: URL and argument validation, transport failure, HTTP status >= 400, non-HTML responses, unreadable pages, and no-matching-section. The partition check in server.py is startup configuration, not a tool failure, so it is untouched.

Unexpected exceptions are deliberately not converted. The blanket except Exception in read_sections_impl still re-raises unchanged, so a genuine crash stays generic. Converting those as well would undo what python-sdk#3314 set out to do.

Raise the mcp floor to 2.1.0

SDK 2.0.0 appends the text of any exception, so it forwards the message even for a plain ValueError. Any environment resolving 2.0.0 hides this entire class of bug, which is why this reached a release uncaught — the lock resolved 2.0.0, so CI would not have caught a regression here either.

pyproject.toml now declares mcp[cli]>=2.1.0,<3.0.0 and the lock resolves 2.3.0, so the suite runs against the behaviour customers actually get. The new tests still assert the exception type rather than the forwarded string, so they stay meaningful on any 2.x.

Name a next step on recoverable failures

A 4xx, and a redirect that landed on a page with nothing to read, now append Use search_documentation. — in the same spirit as the existing missing-subsections message that points at read_documentation. The redirect case is the one behind #4705: the message already said Requested <A>; served <B> without saying what to do about it.

Deliberately not added where a search would mislead. A page that returned content at the URL asked for but could not be parsed has not moved, and a transport failure says nothing about whether the page exists. Page grows a substituted property so the two cases can be told apart, which also simplifies message().

The suggestion asserts nothing about why the fetch failed. An earlier revision read "The page may have moved or may no longer exist", but a 4xx does not tell us which of those is true, or whether it is instead a typo'd URL, a region-gated page, or a transient edge error — so the message is just the action.

User Experience

Against SDK 2.3.0, through the real MCPServer.call_tool path:

anticipated -> ToolError: Error executing tool anticipated: URL must end with .html
crash       -> UnexpectedToolError: Error executing tool crash

The message arrives for the anticipated failure; the TypeError's text does not.

Before and after on the same assertion:

# before: ValueError: URL must end with .html          (pytest.raises(ToolError) fails)
# after:  1 passed

449 passed, ruff clean, pyright clean. 428 of those are pre-existing and unmodified.

Relationship to #4726

#4726 proposed the same DocumentationToolError(ToolError, ValueError) approach and that idea is the right one, so credit to @Christian-Sidak for it. This PR differs in four ways:

  1. It imports from mcp.server.mcpserver.exceptions. fix(aws-documentation-mcp-server): surface read-path errors to MCP clients #4726 uses mcp.server.fastmcp.exceptions, which does not exist on any 2.x SDK (the module was renamed in 2.0.0), so the server fails to import on exactly the versions the fix targets.
  2. It is based on current main. fix(aws-documentation-mcp-server): surface read-path errors to MCP clients #4726 branches from before fix(aws-documentation-mcp-server)!: correct read-path output and make failures raise #4650.
  3. It converts the three read_documentation_impl raises, which is the redirect case aws-documentation-mcp-server: read-path failure messages masked as "Error executing tool" since MCP SDK 2.x #4705 actually reports. fix(aws-documentation-mcp-server): surface read-path errors to MCP clients #4726 does not reach them, because they did not exist in its base.
  4. It leaves the blanket except Exception handler alone rather than converting it.

Fixes #4705

Checklist

  • I have reviewed the contributing guidelines
  • I have performed a self-review of this change
  • Changes have been tested
  • Changes are documented

Is this a breaking change? N

RFC issue number: N/A

By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of the project license.

…res reach the model

MCP SDK 2.1.0 forwards only a ToolError's message to the client and replaces the text of
every other exception with a bare "Error executing tool <name>", keeping crash details off
the wire (modelcontextprotocol/python-sdk#3314).

Every read-path failure this server raises deliberately already builds a message for the
caller, often the "Requested <A>; served <B>." substitution note, and then raised a plain
ValueError, so that message was discarded and the model saw no reason and no next step. A
moved guide page that redirects to an index shell, a 4xx response, a page with nothing to
convert, and a rejected URL were all indistinguishable.

Anticipated failures now raise DocumentationToolError, which subclasses ToolError so the
SDK forwards the message and ValueError so existing callers and tests keep working.
UnreadablePageError subclasses it for the same reason, since it escapes to tool callers.
The 23 deliberate raise sites across server_aws.py, server_utils.py and util.py are
converted; the partition check in server.py is startup configuration and is untouched.

Unexpected exceptions are deliberately NOT converted. The blanket handler in
read_sections_impl still re-raises unchanged so a crash stays generic, which is what
python-sdk#3314 set out to protect.

Verified against SDK 2.3.0: DocumentationToolError arrives as "Error executing tool t: URL
must end with .html" while a TypeError arrives as UnexpectedToolError with no detail. SDK
2.0.0 appends the text of any exception, so an environment that resolves 2.0.0 hides this
entire class of bug, which is why it was not caught before release.

Fixes awslabs#4705
@codecov

codecov Bot commented Oct 8, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 93.47%. Comparing base (70f56c3) to head (43211e7).
⚠️ Report is 5 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #4748      +/-   ##
==========================================
+ Coverage   93.45%   93.47%   +0.01%     
==========================================
  Files        1064     1064              
  Lines       91712    91791      +79     
  Branches    14887    14899      +12     
==========================================
+ Hits        85709    85799      +90     
+ Misses       3601     3595       -6     
+ Partials     2402     2397       -5     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

alexisareyn and others added 3 commits October 8, 2026 12:21
…ame a next step on recoverable failures

Both from review feedback on this PR.

Raise the floor. 2.1.0 is the release that stopped forwarding non-ToolError messages, so any
environment resolving 2.0.0 forwards them anyway and hides this entire class of bug. The lock
resolved 2.0.0, which is why the regression reached a release and why CI would not have caught a
future one. The lock now resolves 2.3.0 and the suite runs against it.

Name a next step. A 4xx, and a redirect that landed on a page with nothing to read, now add
"The page may have moved or may no longer exist. Use search_documentation to find the current
page.", in the same spirit as the existing missing-subsections message that points at
read_documentation. The redirect case is the one behind awslabs#4705: the message already said
"Requested <A>; served <B>" without saying what to do about it.

Deliberately not added where a search would mislead. A page that is unreadable at the URL asked
for has not moved, and a transport failure says nothing about whether the page exists. Page
grows a `substituted` property so the two cases can be told apart, which also simplifies
`message()`.

One existing test asserted the 404 message by exact string equality. Its intent, per its class
name, is that no substitution note is prepended without a redirect, so it now asserts that
directly and no longer breaks on an intended wording change.
…ng the page moved

The suggestion read "The page may have moved or may no longer exist. Use search_documentation to
find the current page." A 4xx does not tell us which of those is true, or whether it is instead a
typo'd URL, a region-gated page, or a transient edge error. The message is now just
"Use search_documentation." - the action we can stand behind, with no diagnosis attached.

The constant is renamed `_USE_SEARCH` accordingly; `_FIND_THE_PAGE` named an outcome the server
cannot promise. Its comment is dropped rather than reworded, since it carried the same assertion.

No test changes: the four tests in TestRecoverableFailuresNameANextStep assert on the
`search_documentation` substring, never the sentence, so they pin the behaviour and not the wording.
@alexisareyn
alexisareyn marked this pull request as ready for review October 9, 2026 18:05
@alexisareyn
alexisareyn enabled auto-merge October 9, 2026 18:05
alexisareyn added 2 commits October 9, 2026 15:26
`Page.message()` joined its parts with a bare space, and only the substitution note ended with a
period, so a reason and the next step ran together: "... could not be read: Page failed to be
simplified from HTML Use search_documentation."

Parts now pass through `_sentence()`, which appends a period unless the part already ends in one.
Fixed strings that bring their own punctuation are left alone, so nothing is doubled, and the
exception text that supplies most reasons no longer has to remember to terminate itself.

Two tests pin both halves of that: the run-on case Michael reported, and the already-a-sentence
case that must not gain a second period.
…t in a helper

Replaces the `_sentence()` helper from bd86e6d. The run-on it fixed only needed the two message
parts that precede a next step to end with a period, which is a property of those strings rather
than something the join should compute.

`message()` goes back to joining verbatim. The status-code reason gets its period inline at its
three call sites. The other reason ends with the exception text, so the period belongs at the
`UnreadablePageError` raise sites; two of the five already had one.

The two unit tests on the helper are replaced by one on the message it was there to produce, in
the class that covers the next-step behaviour.
@alexisareyn
alexisareyn added this pull request to the merge queue Oct 9, 2026
Merged via the queue into awslabs:main with commit 2520adc Oct 9, 2026
137 checks passed
@alexisareyn
alexisareyn deleted the fix/surface-read-path-errors branch October 9, 2026 20:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

aws-documentation-mcp-server: read-path failure messages masked as "Error executing tool" since MCP SDK 2.x

3 participants