feat(server): MCP error responses populate structuredContent.adcp_error (closes #509)#525
Merged
Merged
Conversation
…or (closes #509) The framework now projects AdcpError raised from a platform method onto a CallToolResult with isError=True AND structuredContent.adcp_error populated on the same envelope — matching transport-errors.mdx §MCP Binding and the storyboard runner's /adcp_error/code JSON-pointer assertion shape. The previous text-only projection (`MEDIA_BUY_NOT_FOUND[id]: ...`) is preserved as the content[] text fallback for clients that don't read structuredContent (LLM tool-use surfaces, log viewers). Implementation: bypass FastMCP's _make_error_result (which strips structuredContent on isError=True envelopes) by returning a pre-built CallToolResult directly from the registered tool function. A FuncMetadata subclass skips success-path output_model validation when result.isError is True, so the error envelope reaches the lowlevel handler's CallToolResult short-circuit (server.py:540) without re-validation. A2A parity: adcp.server.a2a_server already projects AdcpError as a DataPart-keyed adcp_error envelope on a failed task event (_send_adcp_error). It does not yet handle decisioning-layer AdcpError (only adcp.exceptions.ADCPError) or surface field/details/retry_after — follow-up issue. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
7 tasks
bokelley
added a commit
that referenced
this pull request
May 4, 2026
…ls/retry_after, catch decisioning AdcpError (closes #530) (#536) Mirrors PR #525's MCP-side fix on the A2A surface. Two parity gaps closed: 1. ``ADCPAgentExecutor.execute()`` now catches both ``adcp.exceptions.ADCPError`` AND ``adcp.decisioning.types.AdcpError``. The two are disjoint hierarchies; pre-fix decisioning errors fell into the generic ``except Exception`` and rendered as plain "Skill execution failed" text — losing the structured shape entirely for adopters using ``DecisioningPlatform``. 2. ``_send_adcp_error`` projects the full envelope: ``code``, ``message``, ``recovery``, ``field``, ``suggestion``, ``retry_after``, ``details``. Pre-fix only ``code`` / ``message`` / ``recovery`` / ``suggestion`` reached the wire; ``field`` / ``details`` / ``retry_after`` were dropped even when the raised error supplied them. Field extraction is shared with the MCP path via ``adcp.server.translate._extract_structured_fields`` (the helper #525 factored out) so both transports project off the same source-of-truth shape — no duplication, no drift. Tests parametrized over the same code set as #525's MCP test (``MEDIA_BUY_NOT_FOUND``, ``PACKAGE_NOT_FOUND``, ``TERMS_REJECTED``, ``BUDGET_TOO_LOW``) plus optional-field gating coverage and an ``ADCPTaskError`` regression case. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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.
Summary
CallToolResult(isError=True, structuredContent={"adcp_error": {code, message, recovery, field?, suggestion?, retry_after?, details?}})— matching transport-errors.mdx §MCP Binding.content[]is preserved (MEDIA_BUY_NOT_FOUND[id]: …) for clients that don't readstructuredContent./adcp_error/codeJSON-pointer assertions now resolve to the actual code (MEDIA_BUY_NOT_FOUND,PACKAGE_NOT_FOUND,TERMS_REJECTED, …) instead of the previousmcp_errorfallback.Closes #509. Surfacing context: PR #508 (multi_platform_seller storyboard fix-pack).
Approach
Went with option (b) — bypass FastMCP's error-result construction, not patch.
translate.pyexposes a newbuild_mcp_error_result(exc)helper that constructs aCallToolResultwithisError=TrueANDstructuredContent.adcp_errorpopulated.serve.py:_register_toolcatchesADCPError/ decisioningAdcpErrorinside the tool function and returns the pre-builtCallToolResultinstead of raisingToolError.FuncMetadatasubclass (_AdcpFuncMetadata) skips success-path output validation whenresult.isErrorisTrue, so the error envelope reaches the lowlevel handler'sCallToolResultshort-circuit (mcp/server/lowlevel/server.py:540) without being re-validated against the success schema.The previous limitation comment at
translate.py:246-250(about_make_error_resultstrippingstructuredContent) is now resolved for the standardserve()path. The text-projection_to_mcp/translate_errorcodepath remains for proxy / custom-transport adopters who need aToolErrorto raise.A2A parity
adcp.server.a2a_server._send_adcp_erroralready projects anadcp_errorenvelope as aDataParton a failed task event, so the A2A surface is structurally correct today. Two gaps that justify a separate follow-up issue (out of scope here):adcp.exceptions.ADCPErroronly — decisioning-layerAdcpErrorpropagates to the genericexcept Exceptionpath.code/message/recovery/suggestionbut notfield,details, orretry_after.Test plan
build_mcp_error_result(shape, optional field gating, all three exception types: ADCPError, decisioning AdcpError, Error model)mcp.call_toolexercising FastMCP → lowlevel handler with the structured envelope intactMEDIA_BUY_NOT_FOUND,PACKAGE_NOT_FOUND,TERMS_REJECTED,BUDGET_TOO_LOWisError=False)tests/test_translate.pysuite stays green (43 tests)ruff check+mypy src/adcp/server/clean/adcp_error/coderesolves toMEDIA_BUY_NOT_FOUNDfor anAdcpError("MEDIA_BUY_NOT_FOUND", ...)raise🤖 Generated with Claude Code