Skip to content

Commit 4a3e0a1

Browse files
committed
deploy: d991b1e
1 parent 27a8ca8 commit 4a3e0a1

3 files changed

Lines changed: 183 additions & 40 deletions

File tree

protocols/index.html

Lines changed: 33 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1425,28 +1425,41 @@ <h3>Inherited members</h3>
14251425
message_text = item[&#34;text&#34;]
14261426
break
14271427

1428-
# Handle error responses
1428+
# Handle error responses per transport-errors.mdx §Client Detection
1429+
# Order. Extract the adcp_error object from structuredContent first,
1430+
# then from text fallback — whichever is present.
14291431
if is_error:
1430-
# For error responses, structuredContent is optional
1431-
# Use the error message from content as the error
1432-
error_message = message_text or &#34;Tool execution failed&#34;
1433-
structured_error = getattr(result, &#34;structuredContent&#34;, None)
1434-
# Prefer structured error codes when present, then fall back to
1435-
# scanning the text content — many MCP servers (FastMCP default)
1436-
# return is_error=true with only a text body carrying the code.
1437-
_idempotency.raise_for_idempotency_error(
1438-
tool_name, structured_error, self.agent_config.id
1439-
)
1432+
adcp_error = extract_adcp_error(result)
1433+
# Raise typed idempotency exceptions before building a generic
1434+
# TaskResult(failed), so callers that catch them distinctly
1435+
# don&#39;t lose the signal.
1436+
if adcp_error and adcp_error.get(&#34;code&#34;) in (
1437+
&#34;IDEMPOTENCY_CONFLICT&#34;,
1438+
&#34;IDEMPOTENCY_EXPIRED&#34;,
1439+
):
1440+
from adcp.exceptions import classify_task_error
1441+
1442+
raise classify_task_error(
1443+
tool_name, [adcp_error], agent_id=self.agent_config.id
1444+
)
1445+
# FastMCP-style is_error with plain-text content: text-match
1446+
# fallback for the two idempotency codes.
14401447
_idempotency.raise_for_idempotency_text(
14411448
tool_name, message_text, self.agent_config.id
14421449
)
1450+
error_message = (
1451+
(adcp_error.get(&#34;message&#34;) if adcp_error else None)
1452+
or message_text
1453+
or &#34;Tool execution failed&#34;
1454+
)
14431455
if self.agent_config.debug and start_time:
14441456
duration_ms = (time.time() - start_time) * 1000
14451457
debug_info = DebugInfo(
14461458
request=debug_request,
14471459
response={
14481460
&#34;error&#34;: error_message,
14491461
&#34;is_error&#34;: True,
1462+
&#34;adcp_error&#34;: adcp_error,
14501463
},
14511464
duration_ms=duration_ms,
14521465
)
@@ -1458,18 +1471,19 @@ <h3>Inherited members</h3>
14581471
idempotency_key=idempotency_key,
14591472
)
14601473

1461-
# For successful responses, structuredContent is required
1462-
if not hasattr(result, &#34;structuredContent&#34;) or result.structuredContent is None:
1474+
# Success extraction per mcp-response-extraction.mdx §Extraction
1475+
# Algorithm: prefer structuredContent (MCP 2025-03-26+), fall back
1476+
# to JSON-parsing content[].text for older servers (including the
1477+
# AdCP reference training agent).
1478+
data_to_return = extract_adcp_success(result)
1479+
if data_to_return is None:
14631480
raise ValueError(
1464-
f&#34;MCP tool {tool_name} did not return structuredContent. &#34;
1465-
f&#34;This SDK requires MCP tools to provide structured responses &#34;
1466-
f&#34;for successful calls. &#34;
1481+
f&#34;MCP tool {tool_name} returned no structured AdCP data. &#34;
1482+
f&#34;Neither structuredContent nor content[].text yielded a &#34;
1483+
f&#34;parseable non-adcp_error JSON object. &#34;
14671484
f&#34;Got content: {result.content if hasattr(result, &#39;content&#39;) else &#39;none&#39;}&#34;
14681485
)
14691486

1470-
# Extract the structured data (required for success)
1471-
data_to_return = result.structuredContent
1472-
14731487
if self.agent_config.debug and start_time:
14741488
duration_ms = (time.time() - start_time) * 1000
14751489
debug_info = DebugInfo(

protocols/mcp.html

Lines changed: 148 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,115 @@ <h1 class="title">Module <code>adcp.protocols.mcp</code></h1>
4242
<section>
4343
</section>
4444
<section>
45+
<h2 class="section-title" id="header-functions">Functions</h2>
46+
<dl>
47+
<dt id="adcp.protocols.mcp.extract_adcp_error"><code class="name flex">
48+
<span>def <span class="ident">extract_adcp_error</span></span>(<span>result: Any) ‑> dict[str, typing.Any] | None</span>
49+
</code></dt>
50+
<dd>
51+
<details class="source">
52+
<summary>
53+
<span>Expand source code</span>
54+
</summary>
55+
<pre><code class="python">def extract_adcp_error(result: Any) -&gt; dict[str, Any] | None:
56+
&#34;&#34;&#34;Extract and validate an AdCP ``adcp_error`` object from an MCP result.
57+
58+
Implements AdCP spec §Client Detection Order (MCP paths 1 + 5) from
59+
docs/building/implementation/transport-errors.mdx. Only applies when
60+
``isError`` is truthy. Returns a validated error object or ``None``.
61+
&#34;&#34;&#34;
62+
if not getattr(result, &#34;isError&#34;, False):
63+
return None
64+
65+
sc = getattr(result, &#34;structuredContent&#34;, None)
66+
if isinstance(sc, dict):
67+
validated = _validate_adcp_error(sc.get(&#34;adcp_error&#34;))
68+
if validated is not None:
69+
return validated
70+
71+
for item in getattr(result, &#34;content&#34;, None) or []:
72+
text = _text_of(item)
73+
# Apply the same 1MB pre-parse cap as the success path to prevent a
74+
# malicious server returning ``isError=true`` plus a giant payload from
75+
# forcing a multi-MB json.loads into memory before the 4KB validation
76+
# would reject it.
77+
if text is None or len(text) &gt; _MAX_TEXT_SIZE_BYTES:
78+
continue
79+
try:
80+
parsed = json.loads(text)
81+
except (json.JSONDecodeError, ValueError):
82+
continue
83+
if isinstance(parsed, dict):
84+
validated = _validate_adcp_error(parsed.get(&#34;adcp_error&#34;))
85+
if validated is not None:
86+
return validated
87+
return None</code></pre>
88+
</details>
89+
<div class="desc"><p>Extract and validate an AdCP <code>adcp_error</code> object from an MCP result.</p>
90+
<p>Implements AdCP spec §Client Detection Order (MCP paths 1 + 5) from
91+
docs/building/implementation/transport-errors.mdx. Only applies when
92+
<code>isError</code> is truthy. Returns a validated error object or <code>None</code>.</p></div>
93+
</dd>
94+
<dt id="adcp.protocols.mcp.extract_adcp_success"><code class="name flex">
95+
<span>def <span class="ident">extract_adcp_success</span></span>(<span>result: Any) ‑> dict[str, typing.Any] | None</span>
96+
</code></dt>
97+
<dd>
98+
<details class="source">
99+
<summary>
100+
<span>Expand source code</span>
101+
</summary>
102+
<pre><code class="python">def extract_adcp_success(result: Any) -&gt; dict[str, Any] | None:
103+
&#34;&#34;&#34;Extract AdCP success response data from an MCP tool result.
104+
105+
Implements the normative algorithm from AdCP spec §MCP Response Extraction
106+
(docs/building/implementation/mcp-response-extraction.mdx):
107+
108+
1. If ``isError`` is truthy, return ``None`` — error extraction is a
109+
separate path.
110+
2. ``structuredContent`` — if present and a non-array object that is NOT
111+
an ``adcp_error``-only payload, return it.
112+
3. Text fallback — iterate ``content[]`` in order; for each ``type=&#39;text&#39;``
113+
item within the 1MB size limit, ``json.loads`` and return the result
114+
if it is a non-array object that is NOT ``adcp_error``-only.
115+
4. No structured data found — return ``None``.
116+
&#34;&#34;&#34;
117+
if getattr(result, &#34;isError&#34;, False):
118+
return None
119+
120+
sc = getattr(result, &#34;structuredContent&#34;, None)
121+
if isinstance(sc, dict) and not (len(sc) == 1 and &#34;adcp_error&#34; in sc):
122+
return sc
123+
124+
for item in getattr(result, &#34;content&#34;, None) or []:
125+
text = _text_of(item)
126+
if text is None or len(text) &gt; _MAX_TEXT_SIZE_BYTES:
127+
continue
128+
try:
129+
parsed = json.loads(text)
130+
except (json.JSONDecodeError, ValueError):
131+
continue
132+
if (
133+
isinstance(parsed, dict)
134+
and not (len(parsed) == 1 and &#34;adcp_error&#34; in parsed)
135+
):
136+
return parsed
137+
return None</code></pre>
138+
</details>
139+
<div class="desc"><p>Extract AdCP success response data from an MCP tool result.</p>
140+
<p>Implements the normative algorithm from AdCP spec §MCP Response Extraction
141+
(docs/building/implementation/mcp-response-extraction.mdx):</p>
142+
<ol>
143+
<li>If <code>isError</code> is truthy, return <code>None</code> — error extraction is a
144+
separate path.</li>
145+
<li><code>structuredContent</code> — if present and a non-array object that is NOT
146+
an <code>adcp_error</code>-only payload, return it.</li>
147+
<li>Text fallback — iterate <code>content[]</code> in order; for each <code>type='text'</code>
148+
item within the 1MB size limit, <code>json.loads</code> and return the result
149+
if it is a non-array object that is NOT <code>adcp_error</code>-only.</li>
150+
<li>No structured data found — return <code>None</code>.</li>
151+
</ol></div>
152+
</dd>
153+
</dl>
45154
</section>
46155
<section>
47156
<h2 class="section-title" id="header-classes">Classes</h2>
@@ -353,28 +462,41 @@ <h2 class="section-title" id="header-classes">Classes</h2>
353462
message_text = item[&#34;text&#34;]
354463
break
355464

356-
# Handle error responses
465+
# Handle error responses per transport-errors.mdx §Client Detection
466+
# Order. Extract the adcp_error object from structuredContent first,
467+
# then from text fallback — whichever is present.
357468
if is_error:
358-
# For error responses, structuredContent is optional
359-
# Use the error message from content as the error
360-
error_message = message_text or &#34;Tool execution failed&#34;
361-
structured_error = getattr(result, &#34;structuredContent&#34;, None)
362-
# Prefer structured error codes when present, then fall back to
363-
# scanning the text content — many MCP servers (FastMCP default)
364-
# return is_error=true with only a text body carrying the code.
365-
_idempotency.raise_for_idempotency_error(
366-
tool_name, structured_error, self.agent_config.id
367-
)
469+
adcp_error = extract_adcp_error(result)
470+
# Raise typed idempotency exceptions before building a generic
471+
# TaskResult(failed), so callers that catch them distinctly
472+
# don&#39;t lose the signal.
473+
if adcp_error and adcp_error.get(&#34;code&#34;) in (
474+
&#34;IDEMPOTENCY_CONFLICT&#34;,
475+
&#34;IDEMPOTENCY_EXPIRED&#34;,
476+
):
477+
from adcp.exceptions import classify_task_error
478+
479+
raise classify_task_error(
480+
tool_name, [adcp_error], agent_id=self.agent_config.id
481+
)
482+
# FastMCP-style is_error with plain-text content: text-match
483+
# fallback for the two idempotency codes.
368484
_idempotency.raise_for_idempotency_text(
369485
tool_name, message_text, self.agent_config.id
370486
)
487+
error_message = (
488+
(adcp_error.get(&#34;message&#34;) if adcp_error else None)
489+
or message_text
490+
or &#34;Tool execution failed&#34;
491+
)
371492
if self.agent_config.debug and start_time:
372493
duration_ms = (time.time() - start_time) * 1000
373494
debug_info = DebugInfo(
374495
request=debug_request,
375496
response={
376497
&#34;error&#34;: error_message,
377498
&#34;is_error&#34;: True,
499+
&#34;adcp_error&#34;: adcp_error,
378500
},
379501
duration_ms=duration_ms,
380502
)
@@ -386,18 +508,19 @@ <h2 class="section-title" id="header-classes">Classes</h2>
386508
idempotency_key=idempotency_key,
387509
)
388510

389-
# For successful responses, structuredContent is required
390-
if not hasattr(result, &#34;structuredContent&#34;) or result.structuredContent is None:
511+
# Success extraction per mcp-response-extraction.mdx §Extraction
512+
# Algorithm: prefer structuredContent (MCP 2025-03-26+), fall back
513+
# to JSON-parsing content[].text for older servers (including the
514+
# AdCP reference training agent).
515+
data_to_return = extract_adcp_success(result)
516+
if data_to_return is None:
391517
raise ValueError(
392-
f&#34;MCP tool {tool_name} did not return structuredContent. &#34;
393-
f&#34;This SDK requires MCP tools to provide structured responses &#34;
394-
f&#34;for successful calls. &#34;
518+
f&#34;MCP tool {tool_name} returned no structured AdCP data. &#34;
519+
f&#34;Neither structuredContent nor content[].text yielded a &#34;
520+
f&#34;parseable non-adcp_error JSON object. &#34;
395521
f&#34;Got content: {result.content if hasattr(result, &#39;content&#39;) else &#39;none&#39;}&#34;
396522
)
397523

398-
# Extract the structured data (required for success)
399-
data_to_return = result.structuredContent
400-
401524
if self.agent_config.debug and start_time:
402525
duration_ms = (time.time() - start_time) * 1000
403526
debug_info = DebugInfo(
@@ -960,6 +1083,12 @@ <h3>Inherited members</h3>
9601083
<li><code><a title="adcp.protocols" href="index.html">adcp.protocols</a></code></li>
9611084
</ul>
9621085
</li>
1086+
<li><h3><a href="#header-functions">Functions</a></h3>
1087+
<ul class="">
1088+
<li><code><a title="adcp.protocols.mcp.extract_adcp_error" href="#adcp.protocols.mcp.extract_adcp_error">extract_adcp_error</a></code></li>
1089+
<li><code><a title="adcp.protocols.mcp.extract_adcp_success" href="#adcp.protocols.mcp.extract_adcp_success">extract_adcp_success</a></code></li>
1090+
</ul>
1091+
</li>
9631092
<li><h3><a href="#header-classes">Classes</a></h3>
9641093
<ul>
9651094
<li>

signing/index.html

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1611,7 +1611,7 @@ <h3>Instance variables</h3>
16111611
</dd>
16121612
<dt id="adcp.signing.VerifyOptions"><code class="flex name class">
16131613
<span>class <span class="ident">VerifyOptions</span></span>
1614-
<span>(</span><span>*,<br>now: float,<br>capability: <a title="adcp.signing.VerifierCapability" href="#adcp.signing.VerifierCapability">VerifierCapability</a>,<br>operation: str,<br>jwks_resolver: <a title="adcp.signing.JwksResolver" href="#adcp.signing.JwksResolver">JwksResolver</a>,<br>replay_store: <a title="adcp.signing.ReplayStore" href="#adcp.signing.ReplayStore">ReplayStore</a> | None = None,<br>revocation_checker: <a title="adcp.signing.RevocationChecker" href="#adcp.signing.RevocationChecker">RevocationChecker</a> | None = None,<br>revocation_list: <a title="adcp.signing.RevocationList" href="#adcp.signing.RevocationList">RevocationList</a> | None = None,<br>max_skew_seconds: int = 60,<br>max_window_seconds: int = 300,<br>label: str = 'sig1',<br>expected_tag: str = 'adcp/request-signing/v1',<br>allowed_algs: frozenset[str] = frozenset({'ecdsa-p256-sha256', 'ed25519'}),<br>agent_url: str | None = None)</span>
1614+
<span>(</span><span>*,<br>now: float,<br>capability: <a title="adcp.signing.VerifierCapability" href="#adcp.signing.VerifierCapability">VerifierCapability</a>,<br>operation: str,<br>jwks_resolver: <a title="adcp.signing.JwksResolver" href="#adcp.signing.JwksResolver">JwksResolver</a>,<br>replay_store: <a title="adcp.signing.ReplayStore" href="#adcp.signing.ReplayStore">ReplayStore</a> | None = None,<br>revocation_checker: <a title="adcp.signing.RevocationChecker" href="#adcp.signing.RevocationChecker">RevocationChecker</a> | None = None,<br>revocation_list: <a title="adcp.signing.RevocationList" href="#adcp.signing.RevocationList">RevocationList</a> | None = None,<br>max_skew_seconds: int = 60,<br>max_window_seconds: int = 300,<br>label: str = 'sig1',<br>expected_tag: str = 'adcp/request-signing/v1',<br>allowed_algs: frozenset[str] = frozenset({'ed25519', 'ecdsa-p256-sha256'}),<br>agent_url: str | None = None)</span>
16151615
</code></dt>
16161616
<dd>
16171617
<details class="source">
@@ -1634,7 +1634,7 @@ <h3>Instance variables</h3>
16341634
allowed_algs: frozenset[str] = ALLOWED_ALGS
16351635
agent_url: str | None = None</code></pre>
16361636
</details>
1637-
<div class="desc"><p>VerifyOptions(*, now: 'float', capability: 'VerifierCapability', operation: 'str', jwks_resolver: 'JwksResolver', replay_store: 'ReplayStore | None' = None, revocation_checker: 'RevocationChecker | None' = None, revocation_list: 'RevocationList | None' = None, max_skew_seconds: 'int' = 60, max_window_seconds: 'int' = 300, label: 'str' = 'sig1', expected_tag: 'str' = 'adcp/request-signing/v1', allowed_algs: 'frozenset[str]' = frozenset({'ecdsa-p256-sha256', 'ed25519'}), agent_url: 'str | None' = None)</p></div>
1637+
<div class="desc"><p>VerifyOptions(*, now: 'float', capability: 'VerifierCapability', operation: 'str', jwks_resolver: 'JwksResolver', replay_store: 'ReplayStore | None' = None, revocation_checker: 'RevocationChecker | None' = None, revocation_list: 'RevocationList | None' = None, max_skew_seconds: 'int' = 60, max_window_seconds: 'int' = 300, label: 'str' = 'sig1', expected_tag: 'str' = 'adcp/request-signing/v1', allowed_algs: 'frozenset[str]' = frozenset({'ed25519', 'ecdsa-p256-sha256'}), agent_url: 'str | None' = None)</p></div>
16381638
<h3>Instance variables</h3>
16391639
<dl>
16401640
<dt id="adcp.signing.VerifyOptions.agent_url"><code class="name">var <span class="ident">agent_url</span> : str | None</code></dt>

0 commit comments

Comments
 (0)