Status: v1 (draft, pre-Ads-port)
Scope: Cross-server contract for error envelopes and pre-flight normalization telemetry across amazon_sp_mcp and amazon_ads_mcp. Extensible to other Openbridge MCP servers.
Authority: This document is the source of truth for envelope shape. Each server provides its own implementation. Conformance is enforced via the JSON Schema in schemas/error-envelope.schema.json and the test suite in tests/conformance/.
Agents calling multiple Openbridge MCP servers should be able to write a single error handler that works against all of them. To make that possible, every server emits errors in the same envelope shape with a shared error_kind taxonomy. Implementations differ; the wire format does not.
- Shapes shared, code per-server. No shared Python package. Each server implements the contract in its own module using its own dependencies. Coordinated dependency releases would fight natural drift between SP-API and Ads API.
- Additive, not replacing. New optional fields (
_meta.*) layer on top without breaking consumers that ignore unknown keys. Required fields change only with a_envelope_versionbump and a one-releaselegacy_error_kindmigration window. - Schema-driven where possible, hint-driven as fallback. Pre-flight argument normalization corrects canonical-key mismatches before they reach upstream. Hints fire on errors that pre-flight cannot prevent (enum values, server-side rules).
- Telemetry surfaces server-only knowledge. Rate-limit headers, schema rewrites, and upstream warnings are visible to the server but invisible to the agent unless the server emits them. The contract specifies which surfaces and how.
Every emitted envelope includes _envelope_version (integer). Server capabilities expose the same value as contract_version. Bumping the version requires:
- A
legacy_error_kindmigration window of one release for breaking taxonomy changes - A
CHANGELOG.mdentry in each affected server with cutover date - Updated conformance fixtures in this repo
| Version | Status | Adds |
|---|---|---|
| 1 | Current | Envelope core + _meta.normalized + error_kind v1 taxonomy + hint categories |
| 1.1 (planned) | Phase 3 | rate_limit + retry_after_seconds on rate_limited errors; _meta.rate_limit on success |
| 1.2 (planned) | Phase 4 | _meta.warnings[] on success path; SP auth_error split |
Every error returned by a conforming server is a JSON object with these required fields:
| Field | Type | Description |
|---|---|---|
error_kind |
string | One of the enum values defined below. Determines which downstream handler runs. |
tool |
string | The MCP tool name that failed. "unknown_tool" if unknowable. |
summary |
string | One-line human-readable summary. Stable enough to log but not stable enough to string-match. |
details |
array of objects | Structured per-issue records. Each entry has path (string, dotted), issue (string), received_type (string). |
hints |
array of strings | Actionable next steps. Bounded length (server may truncate). |
examples |
array | Optional examples of valid input. May be empty. |
error_code |
string | Stable machine-readable code drawn from the standardized vocabulary in the Standardized error_code vocabulary section. Same condition produces the same code on both servers. |
retryable |
boolean | Whether retrying with the same input could succeed. |
_envelope_version |
integer | Currently 1. Required. |
Optional during migration windows:
| Field | Type | Description |
|---|---|---|
legacy_error_kind |
string | Prior error_kind value during a one-release migration. Dropped in the release after the version bump. |
_meta carries telemetry that is additive and may be absent. Consumers must tolerate _meta being missing entirely.
Array of pre-flight normalization events. Emitted using attempted_normalization semantics — the array is populated when the middleware attempted to normalize, including cases where no mutation occurred (i.e., unknown_field_passed_through).
{
"_meta": {
"normalized": [
{"kind": "renamed", "from": "MarketplaceIds", "to": "marketplaceIds", "reason": "schema_canonical_key"}
]
}
}kind values (closed enum, v1):
| Value | Meaning |
|---|---|
renamed |
Non-canonical key matched a unique schema field; rewritten in place. from/to required. |
dropped_alias |
Both canonical and alias provided; alias dropped to satisfy strict schema. from and canonical required. |
coerced |
Type adjusted to match schema (initially: scalar → single-item array). field, from_type, to_type required. |
unknown_field_passed_through |
Field not in schema; passed to upstream as-is. Emitted only when MCP_STRICT_UNKNOWN_FIELDS=false. field required. |
unknown_field_rejected |
Field not in schema; will be rejected downstream by SchemaValidationMiddleware. Emitted when MCP_STRICT_UNKNOWN_FIELDS=true (default). Round 12 follow-up to keep the event label aligned with actual outcome. field required. |
Reserved (not emitted in v1, may be emitted in future versions): unknown_field_dropped.
_meta.normalized is emitted on both successful responses (when at least one event occurred) and error envelopes (when normalization happened before the failing call). On clean calls with no normalization, the field is absent.
Telemetry emission is gated by MCP_SCHEMA_KEY_NORMALIZATION_META (currently default off; see Phase 5 in implementation plan).
Object describing upstream rate-limit headroom at the time of the response. Emitted only when at least one of the three values is parseable from upstream headers. Absent otherwise — servers must not emit synthetic or null values.
{
"_meta": {
"rate_limit": {
"limit_per_second": 0.0167,
"remaining": 0,
"reset_at": "2026-04-25T17:42:33Z"
}
}
}| Field | Type | Notes |
|---|---|---|
limit_per_second |
number or string | Numeric when parseable; raw string when upstream returns a non-numeric value. |
remaining |
number or string | Same. |
reset_at |
string | Raw upstream value. ISO-8601 preferred; servers pass through whatever upstream returns. |
Phase 3 status: shipped on both servers, both paths.
- SP emits on errors via
error_envelope.py:_build_envelopeand on success viaserver/meta_injection_middleware.py:MetaInjectionMiddleware. Header parsing inutils/http_client.py:extract_response_meta(x-amzn-ratelimit-*). - Ads emits on errors via
middleware/error_envelope.py:_merge_http_metaand on success viamiddleware/meta_injection_middleware.py:MetaInjectionMiddleware. Header parsing inutils/http/rate_limit_headers.py:extract_rate_limit_meta(X-RateLimit-*).
Both implementations honor the same emission contract: only fields parseable from upstream headers are emitted; absent headers result in absent keys, not synthetic None values.
Openbridge MCP status: this repository has adopted v1 envelope shape and _envelope_version, but does not currently emit _meta.rate_limit or _meta.retry_after_seconds on all upstream paths. Treat those _meta fields as optional/partial for openbridge-mcp until explicitly listed as shipped in this file.
- Numeric identifiers in tool signatures are
intand validated as strict integers. - Do not pass numeric IDs as strings (for example
"123"). Use123. - Common fields this applies to: subscription IDs, job IDs, history IDs, remote identity IDs, product IDs, and remote identity type IDs.
Number of seconds to wait before retrying. Emitted on rate_limited errors and on other 4xx/5xx errors that include a Retry-After header.
{
"_meta": {
"retry_after_seconds": 1.2
}
}Phase 3 status: shipped on both servers, both paths, under _meta.retry_after_seconds. Field placement is canonical: always under _meta, never at root envelope level.
Array of warnings about the response itself. Each entry has the same shape as a degraded-but-successful condition: {kind, summary, details, hints}.
{
"_meta": {
"warnings": [
{"kind": "upstream_warning", "summary": "199 - 'cached response'", "details": [], "hints": []}
]
}
}Currently shipped: SP emits _meta.warnings on error envelopes when upstream Warning headers are present (see SP utils/http_client.py:97-118). Phase 4 adds emission on success responses for degraded-but-successful conditions and defines per-server kind vocabularies.
Per-server kind vocabularies are documented in this document's appendices. Both servers emit upstream_warning automatically for any RFC 7234 Warning response header. Domain-specific kinds (e.g. cached_or_stale_data, profile_scope_warning) are reserved values that servers may emit when they detect the corresponding condition; agents must tolerate the reserved values appearing on responses.
Closed enum. Servers must not emit values outside this list.
| Value | Server | Meaning |
|---|---|---|
mcp_input_validation |
both | Server-side validator (Pydantic / FastMCP / JSON Schema) rejected before any upstream call. |
tool_not_found |
both | Caller invoked a tool name that isn't registered. Pairs with error_code: TOOL_NOT_FOUND. Round 12 additive entry; previously routed under mcp_input_validation. |
sp_api_http |
SP | Upstream SP-API returned an HTTP 4xx/5xx (excluding 429 → rate_limited from Phase 3). |
ads_api_http |
Ads | Upstream Ads API returned an HTTP 4xx/5xx (excluding 429 → rate_limited from Phase 3). Available after Phase 2. |
sp_api_client |
SP | Client-side error (e.g., body coercion, unhandled non-HTTP exception inside the SP client). |
ads_api_client |
Ads (reserved) | Client-side equivalent for Ads. Reserved; emit only when needed. |
auth_error |
both | Identity/credential problem. Available on Ads from Phase 2; on SP from Phase 4. Until then: SP returns these as sp_api_http or sp_api_client. |
rate_limited |
both | Upstream returned 429 OR pre-flight rate limiter triggered. Available from Phase 3. Until then: SP returns 429s as sp_api_http; Ads as ads_api_http. |
sandbox_runtime |
both (Code Mode only) | The Code Mode sandbox hit a known runtime limitation (blocked stdlib, sandbox-specific behavior). |
internal_error |
both | Server itself broke. Unhandled exception that wasn't classified above. |
When a release reclassifies an error from one bucket to another, the envelope includes legacy_error_kind carrying the prior value. Cutover:
- Release N — adds new
error_kindvalue, emitslegacy_error_kindpopulated with the prior value. - Release N+1 — drops
legacy_error_kind. Consumers must have migrated.
CHANGELOGs in each server document the cutover date.
How an envelope reaches the calling code depends on where the caller sits. Sandboxed Code Mode callers — the primary client surface — see envelopes as plain return values. Direct MCP-transport callers see them as ToolError raises (a wire-protocol constraint, not a server choice). The server guarantees the envelope shape on both surfaces so a single error_kind branching block works regardless of arrival path.
Inside Code Mode (CODE_MODE=true, the default), call_tool is the function FastMCP injects into the sandboxed namespace. Openbridge wraps that shim with _EnvelopeUnwrappingCodeMode (src/server/code_mode.py), which converts any ToolError whose message is a v1 envelope into a returned dict. The documented recovery pattern works as written:
err = await call_tool("get_jobs", {"subscription_id": 118666, "bogus_param": "x"})
if isinstance(err, dict) and err.get("_envelope_version") == 1:
if err["error_kind"] == "rate_limited":
await asyncio.sleep(err.get("_meta", {}).get("retry_after_seconds", 1))
elif err["error_kind"] == "mcp_input_validation":
# fix the args and retry
...This works for every envelope-shaped failure the sandbox can hit:
- Tool-detected errors (not-found by ID, upstream non-2xx, sanitized internal errors) — built by tool bodies and returned directly.
- Pydantic input-validation errors — built by
ErrorEnvelopeMiddlewareand unwrapped at the sandbox boundary. KeyError/ generic exceptions inside tool bodies — same.
Two cases the wrapper deliberately does NOT swallow:
- Non-envelope
ToolError(aToolErrorwhose message isn't valid v1 envelope JSON) propagates asToolError. Sandbox code cantry/except ToolErrorto recover from these. - Non-
ToolErrorexceptions (RuntimeError,asyncio.CancelledError, etc.) propagate unchanged so real Python errors and asyncio cancellation semantics still work.
Clients that talk to the MCP server directly over the wire (raw JSON-RPC, FastMCP Client, mcp-cli, etc.) — without going through Code Mode — see envelopes as ToolError raises. This is a wire-protocol constraint: ToolResult has no isError flag, so a tool cannot return a structured payload that the MCP protocol renders as an error. The server packs the envelope into ToolError.message as JSON and raises:
import json
from fastmcp.exceptions import ToolError
try:
result = await client.call_tool("not_a_real_tool", {})
except ToolError as exc:
try:
env = json.loads(str(exc))
except json.JSONDecodeError:
# Plain ToolError, no envelope. Treat as opaque error.
raise
if env.get("_envelope_version") != 1:
raise
# Now branch on error_kind exactly like a sandbox return value.
if env["error_kind"] == "tool_not_found":
...
elif env["error_kind"] == "mcp_input_validation":
...Regardless of arrival path, the server guarantees:
- The envelope is valid v1 (passes
schemas/error-envelope.schema.json). - The same
error_kindtaxonomy is used — recovery code can branch onerror_kindwithout knowing which surface produced the envelope. - Inside the sandbox, the recovery pattern is a single
if isinstance(err, dict) and err.get("error_kind"): ...— no try/except needed for envelope-shaped errors. - On the direct transport, a single defensive
except ToolError+json.loadsis the maximal recovery cost.
async def call_with_envelope(client, tool, args):
"""Return (result, envelope) — exactly one is non-None."""
try:
result = await client.call_tool(tool, args)
except ToolError as exc:
try:
env = json.loads(str(exc))
except json.JSONDecodeError:
raise
if env.get("_envelope_version") == 1:
return None, env
raise
if isinstance(result, dict) and result.get("_envelope_version") == 1:
return None, result
return result, NoneHint categories are shared (so cross-server agent code can pattern-match the kind of help). Hint matchers (rename tables, similarity thresholds, fuzzy logic) are per-server.
| Category | Description | Example |
|---|---|---|
case_mismatch |
Field name has wrong case for the schema. | "Use marketplaceIds instead of MarketplaceIds for this v2 endpoint." |
did_you_mean |
Field or value has a close-but-not-exact match. | "Did you mean marketplaceIds? Got marketplaceId." |
enum_suggest |
Value is not in the closed enum; suggest valid values. | "CONFIRMED is not valid for orderStatuses. Valid: [...]." |
missing_required |
Required field is absent. | "Required: marketplaceIds. See schema." |
In v1, hints are emitted as plain strings in the hints array. Future versions may add a structured hints form with explicit category tagging.
Before FastMCP validation, the server may rewrite client-provided arguments to match the canonical schema. This is the pre-flight equivalent of the post-flight case_mismatch hint.
- Unique schema match → rewrite the key to the canonical form.
- Ambiguous match (key matches more than one schema field via case-insensitive comparison) → leave unchanged.
- No match → leave unchanged. Pass to upstream as-is. Emit
unknown_field_passed_throughevent. - Canonical key already present alongside the alias → keep canonical, drop alias. Emit
dropped_aliasevent. - Schema field is array-typed but client provided scalar → wrap as single-item array. Emit
coercedevent.
Both servers honor MCP_STRICT_UNKNOWN_FIELDS (default True). When
on, the SchemaValidationMiddleware injects
additionalProperties: false into tool schemas that don't declare
their own intent. Result: typo'd or unknown top-level fields surface
as mcp_input_validation envelopes with
error_code: SCHEMA_ADDITIONAL_PROPERTIES instead of being silently
passed through to the upstream API.
The middleware respects existing intent:
- Schema declares
additionalProperties: false→ no change (already strict). - Schema declares
additionalProperties: true→ no override (author opted in). - Schema declares
additionalProperties: <sub-schema>→ no override (extras validated against the sub-schema). - Schema is silent → middleware injects
additionalProperties: false.
Set MCP_STRICT_UNKNOWN_FIELDS=false as an escape hatch when an
upstream spec ships fields ahead of the packaged OpenAPI and strict
rejection would block valid calls.
| Env var | Default | Effect |
|---|---|---|
MCP_SCHEMA_KEY_NORMALIZATION_ENABLED |
true |
Master switch. Set to false to bypass normalization entirely (escape hatch for upstream API changes that lag the OpenAPI spec). |
MCP_SCHEMA_KEY_NORMALIZATION_META |
false (v1) |
Whether to emit _meta.normalized events. Default flips to true after parametric soak (see implementation plan Phase 5). |
Both servers honor these env var names with identical semantics. No aliases.
Each server exposes its supported contract version and error_kind enum in its MCP capabilities response so agents can discover them at startup:
{
"openbridge_envelope": {
"contract_version": 1,
"error_kinds": ["mcp_input_validation", "sp_api_http", "sp_api_client", "sandbox_runtime", "internal_error"]
}
}A server's error_kinds list is a subset of the master taxonomy in this document. Agents must tolerate values they do not recognize.
A server conforms to v1 when:
- Every error response validates against
schemas/error-envelope.schema.json. - Every emitted
error_kindvalue appears in the v1 taxonomy table. _envelope_version: 1is present on every error envelope._meta.normalizedevents use only the four v1kindvalues.- Server capabilities include
openbridge_envelope.contract_version: 1and a validerror_kindssubset. - The fixtures in
tests/conformance/fixtures/for that server's specific error classes match the JSON Schema.
The conformance suite in tests/conformance/ is run by both server repos against their tagged releases. A release that fails conformance does not ship.
Phase 3 and Phase 5 require both servers to release on the same calendar day with cross-referenced CHANGELOG.md entries. The conformance suite in this repo is the merge gate — a release does not ship until both servers pass against the same suite version.
- Module:
amazon_sp_mcp/src/amazon_sp_mcp/server/error_envelope.py error_kindvalues used:mcp_input_validation,sp_api_http,sp_api_client,sandbox_runtime. After Phase 3:rate_limited. After Phase 4:auth_error.- Header parsing for
_meta.rate_limit:x-amzn-ratelimit-limit,x-amzn-ratelimit-remaining,x-amzn-ratelimit-reset(seeutils/http_client.py:69). _meta.warningskinds:upstream_warning(auto-emitted from RFC 7234Warningheaders; shipped). Reserved values for domain-specific conditions (emitted when SP detects them):cached_or_stale_data,partial_results,marketplace_not_enabled_for_identity,deprecated_parameter_accepted.- Hint matchers: Curated
_PASCAL_CASE_RENAMEStable forcase_mismatch(seeerror_envelope.py:378). Region alias suggestions fordid_you_mean(see_REGION_SUGGESTION_ALIASES). - BEHAVIOR.md:
amazon_sp_mcp/BEHAVIOR.md.
- Module:
amazon_ads_mcp/src/amazon_ads_mcp/middleware/error_envelope.py error_kindvalues used:mcp_input_validation,ads_api_http,auth_error,internal_error. After Phase 3:rate_limited.- Header parsing for
_meta.rate_limit:X-RateLimit-*family (Ads-specific). _meta.warningskinds:upstream_warning(auto-emitted from RFC 7234Warningheaders; shipped). Reserved values for domain-specific conditions (emitted when Ads detects them):cached_or_stale_data,partial_results,profile_scope_warning,deprecated_parameter_accepted.- Hint matchers: Per-server (see Ads
BEHAVIOR.mdpost-Phase 2). - BEHAVIOR.md:
amazon_ads_mcp/BEHAVIOR.md(created in Phase 2).
error_code is a stable machine-readable identifier for the failure
condition. Same condition → same code, regardless of which server emitted
the envelope. Agents can branch on error_code for recovery logic
without parsing the human-readable summary.
| Code | When emitted | error_kind |
|---|---|---|
INPUT_VALIDATION_FAILED |
Pydantic / FastMCP / typed-validation rejected input pre-flight | mcp_input_validation |
TOOL_NOT_FOUND |
Caller invoked a tool name that isn't registered | tool_not_found (Round 12; was mcp_input_validation in Round 11) |
AUTHENTICATION_ERROR |
Generic auth failure (token, credential, OAuth) | auth_error |
INTERNAL_ERROR |
Unhandled server-side exception | internal_error |
TOOL_EXECUTION_FAILED |
Server-side error before reaching upstream API | *_api_client (per-server) |
BODY_COERCION_INVALID_JSON |
JSON-like string in body could not be parsed | *_api_client (per-server) |
RATE_LIMITED |
Upstream returned 429 OR pre-flight rate limiter triggered | rate_limited |
CODE_MODE_SANDBOX_LIMITATION |
Code Mode sandbox hit a known runtime limitation | sandbox_runtime |
When the pre-flight SchemaValidationMiddleware rejects a tool call
because the args fail the tool's published JSON Schema, the envelope
carries one of these canonical codes. Both servers MUST emit the same
code for the same shape; the canonical mapping lives at
schemas/jsonschema_error_codes.json
and is enforced by a parity test in
amazon_ads_mcp/tests/unit/test_schema_validation_middleware.py::TestCanonicalMappingAlignment::test_sp_and_ads_maps_byte_identical.
| Code | jsonschema validator | Notes |
|---|---|---|
SCHEMA_TYPE_MISMATCH |
type |
details.expected_type, details.received_type |
SCHEMA_REQUIRED |
required |
details.field is the missing field name |
SCHEMA_MAX_ITEMS |
maxItems |
details.limit, details.actual |
SCHEMA_MIN_ITEMS |
minItems |
details.limit, details.actual |
SCHEMA_MAX_LENGTH / SCHEMA_MIN_LENGTH |
maxLength / minLength |
details.limit |
SCHEMA_MAXIMUM / SCHEMA_MINIMUM |
maximum / minimum |
details.limit |
SCHEMA_ENUM_MISMATCH |
enum |
details.allowed[] |
SCHEMA_PATTERN_MISMATCH |
pattern |
— |
SCHEMA_FORMAT_INVALID |
format |
— |
SCHEMA_ADDITIONAL_PROPERTIES |
additionalProperties |
details.extra (offending key) |
SCHEMA_ONE_OF_FAILED |
oneOf |
— |
SCHEMA_ANY_OF_FAILED |
anyOf |
— |
SCHEMA_ALL_OF_FAILED |
allOf |
— |
SCHEMA_UNIQUE_ITEMS |
uniqueItems |
— |
SCHEMA_CONST_MISMATCH |
const |
— |
SCHEMA_MULTIPLE_OF |
multipleOf |
— |
SCHEMA_VALIDATION_FAILED |
(any unrecognized validator) | Fallback; original validator name in _meta |
Domain validators (Round 12) — closed-enum checks against runtime
caches that share the SCHEMA_* envelope shape but don't correspond
to a jsonschema validator. They still carry
error_kind: mcp_input_validation and the same details.field /
details.hints surface.
| Code | Server | Tool | Closed-enum source |
|---|---|---|---|
SCHEMA_IDENTITY_NOT_FOUND |
SP | set_active_identity |
auth_manager.list_identities() cached results. Empty/erroring cache falls through to the upstream call (best-effort gate). |
details.field is rendered as a JSON-pointer-style path resolved from
jsonschema.ValidationError.absolute_path — e.g. a bad type two levels
deep surfaces as filters/0/marketplaceId, not just filters. Top-level
fields use the bare name.
Upstream HTTP errors keep a server-prefixed numeric code so the boundary is explicit:
| Code pattern | Server | When emitted |
|---|---|---|
SP_API_HTTP_<NNN> |
SP | Upstream SP-API returned HTTP N |
ADS_API_HTTP_<NNN> |
Ads | Upstream Amazon Ads API returned HTTP N |
OPENBRIDGE_HTTP_<NNN> |
SP | Internal Openbridge service (identity, etc.) returned HTTP N |
For 401/403 the error_kind is auth_error but the error_code keeps
the per-server *_API_HTTP_4NN form so agents can still map to the
specific failure.
| Code | When emitted | Status |
|---|---|---|
OAUTH_ERROR |
OAuth-flow specific failure (state mismatch, code exchange) | Reserved; auth_error envelopes today carry AUTHENTICATION_ERROR |
TOKEN_ERROR |
Refresh/access token failure | Reserved |
PERMISSION_DENIED |
Server-side permission rejection (distinct from auth credential failure) | Reserved |
These are deliberate design decisions called out in client conformance reports. All are working as designed; documented here so future reports can map "this is intentional" without re-litigating.
fastmcp.exceptions.NotFoundError (raised when a caller invokes an
unknown tool name) classifies as:
error_kind: tool_not_found(Round 12; wasmcp_input_validationin Round 11. Theerror_codeand the rest of the envelope are unchanged.)error_code: TOOL_NOT_FOUND
Reason: the call never reached the upstream API, so neither
*_api_client nor *_api_http would be accurate. Tool-name typos are
purely an MCP-side mistake — the dedicated tool_not_found kind lets
agents branch on a single value rather than parsing
error_code: TOOL_NOT_FOUND out of the broader
mcp_input_validation bucket. Clients should branch on error_kind
or error_code, search the catalog, and retry with the corrected name.
Typed validators (e.g. set_active_identity, set_region) raise
SPValidationError / AdsValidationError with error_kind: mcp_input_validation (not *_api_client). Reason: the call never
reaches upstream; the failure is purely schema-shaped. Carries
error_code: INPUT_VALIDATION_FAILED for legacy-typed validation;
SCHEMA_* for the new pre-flight schema validator.
The wire payload (raw JSON-RPC error.message or result.content[0].text)
is a clean v1 envelope JSON. The "ToolError: " prefix is added by the
fastmcp.client SDK's ToolError.__str__ rendering on the client
side. Phase 0b (Round 11) and Round 12 wire captures both confirmed
the prefix is not in the server's response, on either SP or Ads.
Both servers are wire-symmetric. The asymmetry the conformance report flagged is in the SDK rendering layer, not the server. Recommended client patterns:
# CORRECT — read the wire payload directly via the SDK's structured
# access path. No prefix stripping required; works on both servers.
envelope = json.loads(exc.error_data.content[0].text)
# WORKS — strip the SDK prefix first if you only have str(exc).
envelope = json.loads(str(exc).removeprefix("ToolError: "))
# WRONG — fails on both SP and Ads because the SDK prepends "ToolError: "
# before the JSON in __str__.
envelope = json.loads(str(exc))This is documented design, not a server bug. Round 12 ships no code change here; the Round 11 inner-envelope extractor (which strips the prefix when the server-side translator surfaces an inner envelope from the Code Mode sandbox bridge) continues to apply on the server side.
When the schema-driven normalization layer can't map an incoming key
to a single canonical field (no schema match, or ambiguous match), it
records unknown_field_passed_through in _meta.normalized[] and
forwards the field to the tool function unchanged. The tool's own
schema then decides accept/reject (after Round 11, the new
SchemaValidationMiddleware typically rejects with
SCHEMA_ADDITIONAL_PROPERTIES if the schema sets
additionalProperties: false).
Reason: no silent drops; the event in _meta.normalized[] is the
auditable artifact. Clients that want strict reject-on-unknown should
ensure their tool schemas use additionalProperties: false.
Upstream HTTP 401 and 403 classify as:
error_kind: auth_errorerror_code: SP_API_HTTP_4NN/ADS_API_HTTP_4NN(preserves boundary distinction)
Reason: 401/403 always means "fix credentials, not retry the same
call". A generic *_api_http envelope would lose the
agent-actionable hint surface ("Re-authorize the active identity if expired."). Clients should branch on error_kind for credential
recovery flows and on error_code for server-specific telemetry.
MetaInjectionMiddleware only attaches _meta to dict responses.
When a tool returns None, a list, or a primitive, the response is
forwarded untouched without _meta. Reason: protocol stability — the
middleware doesn't reshape return types. Clients that need rate-limit
telemetry on every call should call a dict-returning tool or wrap the
result themselves.
This document defines v1 of the contract. Adoption by each server is staged and tracked here.
| Server | _envelope_version field |
contract_version capability |
Conformance status |
|---|---|---|---|
amazon_sp_mcp |
Emitted (Round 5+) | Pending capability surface | v1-compliant; Round 11 adds TOOL_NOT_FOUND re-classification and pre-flight SCHEMA_* codes via SchemaValidationMiddleware. |
amazon_ads_mcp |
Emitted (Round 5+) | Pending capability surface | v1-compliant; Round 11 mirror complete — same TOOL_NOT_FOUND + SCHEMA_* codes via parallel middleware. Cross-server canonical-mapping parity asserted by test_sp_and_ads_maps_byte_identical. |
The captured fixtures in tests/conformance/fixtures/ represent the shape each server will emit once it adopts v1, not the current literal output. The conformance suite validates this target shape against the JSON Schema. Each server's adoption PR is a small, focused change (add _envelope_version: 1 to its envelope builder; expose contract_version: 1 in its capabilities response) and lands separately under each server team's authorization.
schemas/error-envelope.schema.json— JSON Schema for machine validation.tests/conformance/— fixture-based conformance test suite.amazon_sp_mcp/BEHAVIOR.md— SP-specific behavior contract.amazon_ads_mcp/BEHAVIOR.md— Ads-specific behavior contract (post-Phase 2).