Skip to content

feat(server): MCP error responses populate structuredContent.adcp_error (closes #509)#525

Merged
bokelley merged 1 commit into
mainfrom
bokelley/feat-mcp-error-structured-content
May 4, 2026
Merged

feat(server): MCP error responses populate structuredContent.adcp_error (closes #509)#525
bokelley merged 1 commit into
mainfrom
bokelley/feat-mcp-error-structured-content

Conversation

@bokelley
Copy link
Copy Markdown
Contributor

@bokelley bokelley commented May 4, 2026

Summary

  • AdcpError raised from a platform method now reaches the MCP wire as CallToolResult(isError=True, structuredContent={"adcp_error": {code, message, recovery, field?, suggestion?, retry_after?, details?}}) — matching transport-errors.mdx §MCP Binding.
  • The text fallback in content[] is preserved (MEDIA_BUY_NOT_FOUND[id]: …) for clients that don't read structuredContent.
  • The storyboard runner's /adcp_error/code JSON-pointer assertions now resolve to the actual code (MEDIA_BUY_NOT_FOUND, PACKAGE_NOT_FOUND, TERMS_REJECTED, …) instead of the previous mcp_error fallback.

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.py exposes a new build_mcp_error_result(exc) helper that constructs a CallToolResult with isError=True AND structuredContent.adcp_error populated.
  • serve.py:_register_tool catches ADCPError / decisioning AdcpError inside the tool function and returns the pre-built CallToolResult instead of raising ToolError.
  • A small FuncMetadata subclass (_AdcpFuncMetadata) skips success-path output validation when result.isError is True, so the error envelope reaches the lowlevel handler's CallToolResult short-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_result stripping structuredContent) is now resolved for the standard serve() path. The text-projection _to_mcp / translate_error codepath remains for proxy / custom-transport adopters who need a ToolError to raise.

A2A parity

adcp.server.a2a_server._send_adcp_error already projects an adcp_error envelope as a DataPart on 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):

  • It catches adcp.exceptions.ADCPError only — decisioning-layer AdcpError propagates to the generic except Exception path.
  • It surfaces code/message/recovery/suggestion but not field, details, or retry_after.

Test plan

  • Unit tests on build_mcp_error_result (shape, optional field gating, all three exception types: ADCPError, decisioning AdcpError, Error model)
  • End-to-end test through mcp.call_tool exercising FastMCP → lowlevel handler with the structured envelope intact
  • Parametrized round-trip for MEDIA_BUY_NOT_FOUND, PACKAGE_NOT_FOUND, TERMS_REJECTED, BUDGET_TOO_LOW
  • Success-path regression (output schema validation still enforced for isError=False)
  • Existing tests/test_translate.py suite stays green (43 tests)
  • Full repo regression: 3784 passed
  • ruff check + mypy src/adcp/server/ clean
  • Manual JSON-pointer assertion sim: /adcp_error/code resolves to MEDIA_BUY_NOT_FOUND for an AdcpError("MEDIA_BUY_NOT_FOUND", ...) raise

🤖 Generated with Claude Code

…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>
@bokelley bokelley merged commit b50a87a into main May 4, 2026
15 checks passed
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>
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.

feat(server): MCP error responses must populate structuredContent.adcp_error.code (not just text)

1 participant