Repository navigation
feat(mcp): classify tool errors on spans and give agents actionable, masked error text - #1355
Conversation
The native Datadog backend recorded only the exception class and a fixed "tool resulted in an error" message, so failed tool calls could not be grouped by cause. Prod showed 109 describe_cloud_connection failures that were all upstream 500s, which was only visible by opening child spans. - Reuse fastmcp-extensions' error_attributes so Datadog spans carry the same error.category, error.fault, and upstream.status_code as the OTel backend. - Add error.problem_type: the Airbyte API problem slug from the response body. Only the type (or title, for the generic errors page) is read, and only a short lowercase slug is exported; detail and data are never read. - Build error.message from those bounded tags only. - Classify failed and timed-out sync jobs as upstream faults instead of internal, on both backends. - Wrap SDKError in get_source_definition / get_destination_definition so describe_cloud_connector raises AirbyteMissingResourceError instead of a raw SDKError.
The native Datadog backend showed only an exception class name on a failed `mcp.tools_call` span. Tag it with the error facts fastmcp-extensions already computes for the `otel` backends and the telemetry event: `airbyte.mcp.error.category`, `airbyte.mcp.error.fault`, `airbyte.mcp.error.cause_types`, and `airbyte.mcp.upstream.status_code`. The values come from the library's shared per-call facts, so the span and the event agree. No exception message is read. Pins fastmcp-extensions to the prerelease built from airbytehq/fastmcp-extensions#148. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Code Coverage OverviewLanguages: Python Python / code-coverage/pytest-no-credsThe overall line coverage in commit 1a7ea03 in the Show a line coverage summary of the most impacted files.
Python / code-coverage/pytestThe overall line coverage in commit 1a7ea03 in the Show a line coverage summary of the most impacted files.
Python / code-coverage/pytest-unitThe overall line coverage in commit 1a7ea03 in the Show a line coverage summary of the most covered files.
Updated |
…rary Merge #1354 (classify tool errors on native Datadog spans) into the branch that pins the fastmcp-extensions prerelease. The classification code is #1354's; the shared `ToolCallFacts` wiring from this branch is dropped in its favour. Adjusted for the pinned library: - `error.cause_types` is a tuple of class names, written as a JSON array under `airbyte.mcp.error.cause_types`. - `airbyte.mcp.upstream.status_code` is a span metric, so numeric queries work; the error message reads it from there. - An exception no rule recognises is `unclassified` / `unknown`, no longer `internal` / `server`. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…g backends The slug lookup moves to _error_handling and is passed to the library as the error_reason hook, so the OTel backends export it too. The Datadog tag is renamed from error.problem_type to error.reason to match. Pins the fastmcp-extensions prerelease that adds the hook. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ns like the OTel backend Only FastMCP's ToolError wrapper is unwrapped, so its ValidationError is classified as invalid_arguments instead of the chained pydantic error. A name FastMCP rejects reports outcome and category unknown_tool. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
There was a problem hiding this comment.
🟡 Changes recommended
The production dependency remains pinned to an unreleased build, and reason extraction can export titles outside its documented generic-error case.
2 open findings
What changed in this PR
Adds bounded MCP tool-error classification to Datadog and OTel telemetry while preventing sensitive error details from being exported.
Changes:
- Adds error categories, fault attribution, status codes, cause types, and reason slugs.
- Classifies sync failures and wraps definition lookup SDK errors.
- Expands telemetry and API error tests.
| File | Description |
|---|---|
airbyte/mcp/_datadog.py |
Adds Datadog error classification and messages. |
airbyte/mcp/_otel.py |
Configures classifier and reason hooks. |
airbyte/mcp/_error_handling.py |
Implements sync classification and reason extraction. |
airbyte/_util/api_util.py |
Exposes response bodies and wraps definition errors. |
tests/unit_tests/test_mcp_datadog.py |
Tests Datadog classification and redaction. |
tests/unit_tests/test_mcp_otel.py |
Tests OTel hook configuration. |
tests/unit_tests/test_mcp_tool_errors.py |
Tests classifiers and ID filtering. |
tests/unit_tests/test_cloud_api_util.py |
Tests definition error wrapping. |
pyproject.toml |
Pins the development dependency. |
uv.lock |
Locks the development dependency artifacts. |
🧠 Review effort: Balanced
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Aaron ("AJ") Steers (aaronsteers)
left a comment
There was a problem hiding this comment.
Approved. Looks good for my part.
Pending review and resolution of bot feedback (which I didn't review yet), I think this is good to merge+ship.
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This reverts commit 82d5db0.
…lues Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
…only for generic types Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
|
Codex (@codex) review |
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 1ab4f5bf93
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "Codex (@codex) review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "Codex (@codex) address that feedback".
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
|
Codex (@codex) review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 1738378511
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "Codex (@codex) review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "Codex (@codex) address that feedback".
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
d67c5c5
into
release-candidate/v1


TL;DR: When a tool call fails, its span in Datadog and OTel now says what kind of failure it was and where in the code it happened, without exporting exception text or IDs. Agents get a fixed message they can act on instead of a debug dump, and argument values echoed in that message are masked.
When a tool call fails, the prod
mcp.tools_callspan in Datadog only shows a class name and "tool resulted in an error". Agents getAPI error occurred: API error occurredplus the workspace ID, API root and request URL. This includes the changes from #1354 (Cam's draft), merged here.What a failed span gains
airbyte.mcp.prefix)error.categoryupstream_errorerror.faultupstreamupstream.status_code(numeric)500error.cause_types["SDKError"]error.reasonunexpected-problemerror.message: becomestool resulted in an error (upstream_error, HTTP 500, unexpected-problem).module:qualname:linenoline per frame:error.stack, which also feeds the error panel and LLM Obs. On OTel it'sairbyte.mcp.error.stack/exception.stacktrace, built by fastmcp-extensions#148..../ last. A run at the raise site is kept whole.Changes
Tracing
fastmcp-extensionsclassifier the OTel backends use. Returned error results are classified too. The exception is still never passed to ddtrace; the stack comes fromfastmcp_extensionserror_stack.error.reason: the Airbyte API problem slug from the error response body. It's reported on both backends through the library's newerror_reasonhook. (It waserror.problem_typein feat(mcp): classify tool errors on native Datadog spans #1354.)type, or fromtitleonly whentypeis the generic errors URL. A missing or non-stringtypegives no reason.invalid_arguments/caller,unknown_tool/caller).datadog-otlpbackend: the LLM Obs metadata exporter keeps category, fault, reason, cause types and status code.upstream_error/upstream_timeouton both backends.get_source_definitionandget_destination_definitionnow wrapSDKError, so a 404 no longer surfaces as a rawSDKError.Agent-facing error text
describe_cloud_error(parse_cloud_error(status, body))in the newairbyte/_util/cloud_errors.pymaps Cloud's problemtypeto a fixed message plus one recovery step.airbyte/_util/cloud_errors.yaml; types not in it fall back to a status-based message.CONTRIBUTING.mdand a pointer inAGENTS.md.detailanddata.messageare never relayed; on a 5xx,data.messagecan be raw SQL.data.resourceTypeis shown only if it's in a fixed list (source,connection, ...).AirbyteCloudApiError) use the same table, keeping their fixed 401/403/gateway advice, so the request URL and response body never reach the agent.AgentErrorTextMiddleware: sits outsideUserFacingErrorMiddlewareand telemetry, so classification is unchanged. It:input_value;<value of source_id>, naming only the top-level parameter, never a nested dict key.userin the guidance stay intact. Secrets shorter than 4 characters are also masked only as whole tokens._get_public_api_jsonand_make_config_api_requestno longer put the request body or the response object in error context, which could hold connector secrets. This also changesstr(error)for library users.Depends on
airbytehq/fastmcp-extensions#148, released in
fastmcp-extensions0.34.1. This PR pins the releasedfastmcp-extensions>=0.34.2,<1.0.0(0.34.2 adds only the airbytehq/fastmcp-extensions#149 docs move).Verified locally
Ran a local server on both the
datadogandotelbackends with real and stubbed API failures:Heads-up
tool resulted in an errorshould match it as a prefix."500".outcome=unknown_toolon the Datadog span (wasexception), matching the OTel backend.tool_unavailable/caller, witherror.messagetool resulted in an error (tool_unavailable). Any tool-call failure that escapes as a protocol error now gets the same summary.forbidden, categoryuser_error.upstream_errorand the event saysunclassified. The OTel backends agree.typeortitleis never exported.🤖 Generated with Claude Code
Link to Devin session: https://app.devin.ai/sessions/85c491e543a74ab89151fdf7a0ba5d20
Open in Devin Desktop: https://app.devin.ai/desktop/session/85c491e543a74ab89151fdf7a0ba5d20?variant=devin
Requested by: Caleb Evans (@calebevans-ab)