|
56 | 56 | resolve_session_and_client, |
57 | 57 | ) |
58 | 58 | from ._internal import MCPAnalyticsData |
| 59 | +from ._output_instructions import ( |
| 60 | + add_instructions_to_output_schema, |
| 61 | + mirror_instructions_into_structured_content, |
| 62 | +) |
59 | 63 | from .logger import log |
60 | 64 | from .tools import ( |
61 | 65 | GET_MORE_TOOLS_NAME as _GET_MORE_TOOLS_NAME, |
@@ -323,8 +327,12 @@ async def wrapped( |
323 | 327 | duration_ms = (time.monotonic() - start) * 1000 |
324 | 328 |
|
325 | 329 | delivered_conversation_id = conversation_id |
326 | | - if minted and conversation_id: |
327 | | - if not _append_prompt_back(result, conversation_id): |
| 330 | + if conversation_id: |
| 331 | + result, delivered = _deliver_conversation_id( |
| 332 | + data, result, name, conversation_id, minted |
| 333 | + ) |
| 334 | + # Only a minted handle can be lost this way — one the agent supplied, it has. |
| 335 | + if minted and not delivered: |
328 | 336 | delivered_conversation_id = None |
329 | 337 |
|
330 | 338 | await record_tool_call( |
@@ -365,6 +373,24 @@ def _append_prompt_back(result: Any, conversation_id: str) -> bool: |
365 | 373 | return False |
366 | 374 |
|
367 | 375 |
|
| 376 | +def _deliver_conversation_id( |
| 377 | + data: MCPAnalyticsData, result: Any, name: str, conversation_id: str, minted: bool |
| 378 | +) -> Tuple[Any, bool]: |
| 379 | + """Hand the conversation handle back over both channels a result has: |
| 380 | + mirrored into ``structuredContent`` on every response (for tools whose |
| 381 | + output schema we declared the key on), and as a ``content`` text block on |
| 382 | + the minting response only. Returns ``(result, delivered)`` — a minted handle |
| 383 | + the agent never received must not be stamped on the event.""" |
| 384 | + delivered = False |
| 385 | + if data.tool_output_instructions.get(name): |
| 386 | + result, delivered = mirror_instructions_into_structured_content( |
| 387 | + result, conversation_id |
| 388 | + ) |
| 389 | + if minted and _append_prompt_back(result, conversation_id): |
| 390 | + delivered = True |
| 391 | + return result, delivered |
| 392 | + |
| 393 | + |
368 | 394 | # --- low-level: tools/call ------------------------------------------------------ |
369 | 395 |
|
370 | 396 |
|
@@ -444,8 +470,12 @@ async def handler(ctx: Any, params: Any) -> Any: |
444 | 470 | duration_ms = (time.monotonic() - start) * 1000 |
445 | 471 |
|
446 | 472 | delivered_conversation_id = conversation_id |
447 | | - if minted and conversation_id: |
448 | | - if not _append_prompt_back(result, conversation_id): |
| 473 | + if conversation_id: |
| 474 | + result, delivered = _deliver_conversation_id( |
| 475 | + data, result, name, conversation_id, minted |
| 476 | + ) |
| 477 | + # Only a minted handle can be lost this way — one the agent supplied, it has. |
| 478 | + if minted and not delivered: |
449 | 479 | delivered_conversation_id = None |
450 | 480 |
|
451 | 481 | await record_tool_call( |
@@ -562,6 +592,13 @@ async def handler(ctx: Any, params: Any) -> Any: |
562 | 592 | tool.input_schema = schema |
563 | 593 | except Exception: # noqa: BLE001 - some schema attrs may be read-only |
564 | 594 | log(f"WARN: could not set input_schema on tool {tool.name}") |
| 595 | + # Declare the structuredContent channel and remember the answer: |
| 596 | + # clients that read structuredContent never see the content text |
| 597 | + # block, and only a declared key may be written back on a call. |
| 598 | + if data.options.enable_conversation_id: |
| 599 | + data.tool_output_instructions[tool.name] = ( |
| 600 | + add_instructions_to_output_schema(tool) |
| 601 | + ) |
565 | 602 |
|
566 | 603 | if data.options.report_missing: |
567 | 604 | missing_name = resolve_missing_capability_tool_name(data.options) |
|
0 commit comments