Repository navigation
feat(mcp): set $mcp_error_status from the raised exception's cause chain #1229
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
lucasheriques
wants to merge
3
commits into
lucas/mcp-fastmcp-tool-exception
from
lucas/mcp-error-status
+232
−7
Open
Changes from all commits
Commits
Show all changes
3 commits
Select commit
Hold shift + click to select a range
cbf24f3
feat(mcp): set $mcp_error_status from the raised exception's cause chain
lucasheriques 2d94ba4
fix(mcp): follow __cause__ whenever set, and cap the status walk at 5…
lucasheriques cc1d8d8
fix(mcp): type the status walk's seen set, and record ERROR_STATUS in…
lucasheriques File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| --- | ||
| pypi/posthog: minor | ||
| --- | ||
|
|
||
| `posthog.mcp`: a failed tool call now sets `$mcp_error_status` to the HTTP status behind it, read from the raised exception or its cause chain (`status_code`, `statusCode` or `status`, on the exception or its `response`, as `httpx.HTTPStatusError` and `requests.HTTPError` carry it). Failures from an upstream API now group by status in MCP Analytics instead of landing in "unknown". |
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
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
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
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
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
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
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
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,145 @@ | ||
| """``$mcp_error_status``: the HTTP status behind a failed call, read off the | ||
| raised exception's cause chain (posthog-python#1223, sdk-specs mcp-analytics | ||
| "Failure capture").""" | ||
|
|
||
| from http import HTTPStatus | ||
| from types import SimpleNamespace | ||
|
|
||
| import httpx | ||
| import pytest | ||
|
|
||
| from posthog.mcp import PostHogMCP | ||
| from posthog.mcp.constants import PostHogMCPAnalyticsProperty as P | ||
| from posthog.test.mcp._helpers import events_named, flush_background | ||
|
|
||
|
|
||
| class Failure(Exception): | ||
| def __init__(self, message="failed", **attributes): | ||
| super().__init__(message) | ||
| for name, value in attributes.items(): | ||
| setattr(self, name, value) | ||
|
|
||
|
|
||
| def _http_status_error(status): | ||
| request = httpx.Request("GET", "https://api.example.com/invoices/1") | ||
| response = httpx.Response(status, request=request) | ||
| return httpx.HTTPStatusError( | ||
| f"{status} from upstream", request=request, response=response | ||
| ) | ||
|
|
||
|
|
||
| def _raised_from(outer, inner): | ||
| try: | ||
| try: | ||
| raise inner | ||
| except Exception as error: | ||
| raise outer from error | ||
| except Exception as error: | ||
| return error | ||
|
|
||
|
|
||
| def _raised_during(outer, inner): | ||
| try: | ||
| try: | ||
| raise inner | ||
| except Exception: | ||
| raise outer | ||
| except Exception as error: | ||
| return error | ||
|
|
||
|
|
||
| def _raised_from_none(outer, inner): | ||
| try: | ||
| try: | ||
| raise inner | ||
| except Exception: | ||
| raise outer from None | ||
| except Exception as error: | ||
| return error | ||
|
|
||
|
|
||
| def _cause_without_suppressed_context(): | ||
| error = _raised_during(RuntimeError("lookup failed"), Failure(status=500)) | ||
| error.__cause__ = Failure(status=404) | ||
| error.__suppress_context__ = False | ||
| return error | ||
|
|
||
|
|
||
| def _chain_of(length, status): | ||
| error = Failure("innermost", status=status) | ||
| for _ in range(length - 1): | ||
| error = _raised_from(Failure("wrapper"), error) | ||
| return error | ||
|
|
||
|
|
||
| class UnreadableResponse(Exception): | ||
| @property | ||
| def response(self): | ||
| raise RuntimeError("response not loaded") | ||
|
|
||
|
|
||
| def _cycle(): | ||
| first, second = Failure("first"), Failure("second") | ||
| first.__cause__, second.__cause__ = second, first | ||
| return first | ||
|
|
||
|
|
||
| @pytest.mark.parametrize( | ||
| "error, status", | ||
| [ | ||
| (_http_status_error(503), 503), | ||
| (_raised_from(RuntimeError("lookup failed"), _http_status_error(503)), 503), | ||
| (_raised_during(RuntimeError("lookup failed"), _http_status_error(429)), 429), | ||
| ( | ||
| _raised_from_none(RuntimeError("lookup failed"), _http_status_error(503)), | ||
| None, | ||
| ), | ||
| (_raised_from(Failure(status_code=404), _http_status_error(503)), 404), | ||
| (Failure(status_code=502), 502), | ||
| (Failure(statusCode=502), 502), | ||
| (Failure(status=502), 502), | ||
| (Failure(status_code=None, status=502), 502), | ||
| (Failure(response=SimpleNamespace(status=502)), 502), | ||
| (Failure(status="failed", response=SimpleNamespace(status_code=401)), 401), | ||
| (Failure(status=HTTPStatus.BAD_GATEWAY), 502), | ||
| (Failure(status_code=42), None), | ||
| (Failure(status_code=600), None), | ||
| (Failure(status_code=True), None), | ||
| (Failure(status_code="503"), None), | ||
| (Failure(status_code=503.0), None), | ||
| (Failure("no status here"), None), | ||
| (_cause_without_suppressed_context(), 404), | ||
| (_chain_of(50, 503), 503), | ||
| (_chain_of(51, 503), None), | ||
| (_cycle(), None), | ||
| (UnreadableResponse("failed"), None), | ||
| ("upstream returned 503", None), | ||
| ], | ||
| ) | ||
| async def test_failed_call_carries_the_status_from_the_cause_chain(error, status): | ||
| client = PostHogMCP("phc_test") | ||
| captured = [] | ||
| client.capture = lambda event, **kw: captured.append({"event": event, **kw}) | ||
|
|
||
| client.capture_tool_call("fetch_invoice", is_error=True, error=error) | ||
| await flush_background() | ||
|
|
||
| [call] = events_named(captured, "$mcp_tool_call") | ||
| assert call["properties"].get(P.ERROR_STATUS) == status | ||
|
|
||
|
|
||
| async def test_explicit_status_wins_over_the_derived_one(): | ||
| client = PostHogMCP("phc_test") | ||
| captured = [] | ||
| client.capture = lambda event, **kw: captured.append({"event": event, **kw}) | ||
|
|
||
| client.capture_tool_call( | ||
| "fetch_invoice", | ||
| is_error=True, | ||
| error=_http_status_error(503), | ||
| properties={P.ERROR_STATUS: 418}, | ||
| ) | ||
| await flush_background() | ||
|
|
||
| [call] = events_named(captured, "$mcp_tool_call") | ||
| assert call["properties"][P.ERROR_STATUS] == 418 |
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
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
Oops, something went wrong.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
P2 · Update the public API snapshot for ERROR_STATUS
Adding
ERROR_STATUSchanges the exported public API, butreferences/public_api_snapshot.txtlacks its entry. With the locked Griffe version,make public_api_checkexits with this exact mismatch, failing the Public API snapshot CI job. AGENTS.md:11 requires snapshot updates for public API changes; regenerate and include the snapshot.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed in ddeb000.
references/public_api_snapshot.txtnow listsPostHogMCPAnalyticsProperty.ERROR_STATUS, andcheck_public_api.pyreports it up to date.