@@ -1173,6 +1173,49 @@ def _resolve(node: Any, seen: frozenset[str]) -> Any:
11731173 return result
11741174
11751175
1176+ def _model_to_json_schema (
1177+ model_type : Any , * , allow_root_union : bool = False
1178+ ) -> dict [str , Any ] | None :
1179+ """Generate a flat JSON Schema for a Pydantic model or union.
1180+
1181+ * Plain ``BaseModel`` subclasses use ``model_json_schema()``.
1182+ * Union / Optional types use ``TypeAdapter`` so discriminated unions
1183+ and aliases (``CreateMediaBuyResponse = ...Response1 | ...Response2``)
1184+ generate as ``anyOf``.
1185+ * ``$ref`` nodes are inlined (see :func:`_inline_refs`) so MCP
1186+ clients that don't resolve references see the full surface.
1187+
1188+ When ``allow_root_union`` is ``False`` (the default — used for input
1189+ schemas), schemas with a root-level ``anyOf`` / ``$ref`` return
1190+ ``None`` so the caller falls back to a hand-crafted shape.
1191+ Input schemas need ``type: "object"`` at the root so MCP clients can
1192+ render the form. Output schemas can validly be a discriminated
1193+ union, so ``allow_root_union=True`` keeps the ``anyOf``.
1194+
1195+ Returns ``None`` on any failure — callers fall back to skipping.
1196+ """
1197+ try :
1198+ from pydantic import TypeAdapter
1199+
1200+ if isinstance (model_type , type ) and hasattr (model_type , "model_json_schema" ):
1201+ schema = model_type .model_json_schema ()
1202+ else :
1203+ adapter = TypeAdapter (model_type )
1204+ schema = adapter .json_schema ()
1205+ except Exception :
1206+ return None
1207+
1208+ schema .pop ("title" , None )
1209+
1210+ if not allow_root_union and ("anyOf" in schema or "$ref" in schema ):
1211+ return None
1212+
1213+ try :
1214+ return _inline_refs (schema )
1215+ except Exception :
1216+ return None
1217+
1218+
11761219def _generate_pydantic_schemas () -> dict [str , dict [str , Any ]]:
11771220 """Generate JSON schemas from Pydantic request models.
11781221
@@ -1188,8 +1231,6 @@ def _generate_pydantic_schemas() -> dict[str, dict[str, Any]]:
11881231 against that regression by asserting every tool has an entry here.
11891232 """
11901233 try :
1191- from pydantic import TypeAdapter
1192-
11931234 from adcp .types import (
11941235 AcquireRightsRequest ,
11951236 ActivateSignalRequest ,
@@ -1331,49 +1372,212 @@ def _generate_pydantic_schemas() -> dict[str, dict[str, Any]]:
13311372
13321373 schemas : dict [str , dict [str , Any ]] = {}
13331374 for tool_name , request_type in _tool_to_request .items ():
1334- try :
1335- # Handle union types (e.g. PreviewCreativeRequest, ComplyTestControllerRequest)
1336- if isinstance (request_type , type ) and hasattr (request_type , "model_json_schema" ):
1337- schema = request_type .model_json_schema ()
1338- else :
1339- # Union types need TypeAdapter
1340- adapter = TypeAdapter (request_type )
1341- schema = adapter .json_schema ()
1375+ # Input schemas must be flat ``type: "object"`` — root-level
1376+ # ``anyOf`` / ``$ref`` schemas are skipped so the hand-crafted
1377+ # stub stays in place.
1378+ schema = _model_to_json_schema (request_type , allow_root_union = False )
1379+ if schema is None :
1380+ logger .debug (
1381+ "Pydantic input-schema generation skipped for %s, using hand-crafted schema" ,
1382+ tool_name ,
1383+ )
1384+ continue
1385+ schemas [tool_name ] = schema
13421386
1343- schema . pop ( "title" , None )
1387+ return schemas
13441388
1345- # Union types produce anyOf with $ref at root — these can't
1346- # be represented as flat MCP schemas. Keep hand-crafted.
1347- if "anyOf" in schema or "$ref" in schema :
1348- continue
13491389
1350- # Inline every $ref into its $defs body so MCP clients that
1351- # don't resolve JSON-Schema references (a surprisingly large
1352- # slice of the ecosystem) still see the full tool surface.
1353- # Spec-wise the schema is equivalent — just flat.
1354- schema = _inline_refs (schema )
1390+ def _generate_pydantic_output_schemas () -> dict [str , dict [str , Any ]]:
1391+ """Generate JSON schemas from Pydantic response models.
13551392
1356- schemas [tool_name ] = schema
1357- except Exception :
1393+ Mirror of :func:`_generate_pydantic_schemas` for the response side.
1394+ Each AdCP tool has a corresponding ``Response`` type — for plain
1395+ success responses this is a single ``BaseModel`` subclass; for tools
1396+ that distinguish success / error / pending / rejected on the wire
1397+ (``CreateMediaBuyResponse``, ``AcquireRightsResponse``, etc.) it's a
1398+ union alias.
1399+
1400+ Output schemas advertise the structured-content shape on
1401+ ``tools/list`` (matches the TS port) so MCP clients can validate
1402+ ``structuredContent`` without a separate spec lookup.
1403+
1404+ Unlike input schemas, root-level ``anyOf`` is allowed — discriminated
1405+ response unions are valid JSON Schema and clients that consume
1406+ ``outputSchema`` already handle them.
1407+ """
1408+ try :
1409+ from adcp .types import (
1410+ AcquireRightsResponse ,
1411+ ActivateSignalResponse ,
1412+ BuildCreativeResponse ,
1413+ CalibrateContentResponse ,
1414+ CheckGovernanceResponse ,
1415+ ComplyTestControllerResponse ,
1416+ ContextMatchResponse ,
1417+ CreateCollectionListResponse ,
1418+ CreateContentStandardsResponse ,
1419+ CreateMediaBuyResponse ,
1420+ CreatePropertyListResponse ,
1421+ DeleteCollectionListResponse ,
1422+ DeletePropertyListResponse ,
1423+ GetAccountFinancialsResponse ,
1424+ GetAdcpCapabilitiesResponse ,
1425+ GetBrandIdentityResponse ,
1426+ GetCollectionListResponse ,
1427+ GetContentStandardsResponse ,
1428+ GetCreativeDeliveryResponse ,
1429+ GetCreativeFeaturesResponse ,
1430+ GetMediaBuyArtifactsResponse ,
1431+ GetMediaBuyDeliveryResponse ,
1432+ GetMediaBuysResponse ,
1433+ GetPlanAuditLogsResponse ,
1434+ GetProductsResponse ,
1435+ GetPropertyListResponse ,
1436+ GetRightsResponse ,
1437+ GetSignalsResponse ,
1438+ IdentityMatchResponse ,
1439+ ListAccountsResponse ,
1440+ ListCollectionListsResponse ,
1441+ ListContentStandardsResponse ,
1442+ ListCreativeFormatsResponse ,
1443+ ListCreativesResponse ,
1444+ ListPropertyListsResponse ,
1445+ LogEventResponse ,
1446+ PreviewCreativeResponse ,
1447+ ProvidePerformanceFeedbackResponse ,
1448+ ReportPlanOutcomeResponse ,
1449+ ReportUsageResponse ,
1450+ SiGetOfferingResponse ,
1451+ SiInitiateSessionResponse ,
1452+ SiSendMessageResponse ,
1453+ SiTerminateSessionResponse ,
1454+ SyncAccountsResponse ,
1455+ SyncAudiencesResponse ,
1456+ SyncCatalogsResponse ,
1457+ SyncCreativesResponse ,
1458+ SyncEventSourcesResponse ,
1459+ SyncGovernanceResponse ,
1460+ SyncPlansResponse ,
1461+ UpdateCollectionListResponse ,
1462+ UpdateContentStandardsResponse ,
1463+ UpdateMediaBuyResponse ,
1464+ UpdatePropertyListResponse ,
1465+ UpdateRightsResponse ,
1466+ ValidateContentDeliveryResponse ,
1467+ )
1468+ except ImportError :
1469+ return {}
1470+
1471+ _tool_to_response : dict [str , Any ] = {
1472+ # Catalog
1473+ "get_products" : GetProductsResponse ,
1474+ "list_creative_formats" : ListCreativeFormatsResponse ,
1475+ # Creative
1476+ "sync_creatives" : SyncCreativesResponse ,
1477+ "list_creatives" : ListCreativesResponse ,
1478+ "build_creative" : BuildCreativeResponse ,
1479+ "preview_creative" : PreviewCreativeResponse ,
1480+ "get_creative_delivery" : GetCreativeDeliveryResponse ,
1481+ # Media Buy
1482+ "create_media_buy" : CreateMediaBuyResponse ,
1483+ "update_media_buy" : UpdateMediaBuyResponse ,
1484+ "get_media_buy_delivery" : GetMediaBuyDeliveryResponse ,
1485+ "get_media_buys" : GetMediaBuysResponse ,
1486+ # Signals
1487+ "get_signals" : GetSignalsResponse ,
1488+ "activate_signal" : ActivateSignalResponse ,
1489+ # Account
1490+ "list_accounts" : ListAccountsResponse ,
1491+ "sync_accounts" : SyncAccountsResponse ,
1492+ "get_account_financials" : GetAccountFinancialsResponse ,
1493+ "report_usage" : ReportUsageResponse ,
1494+ # Events & Catalogs
1495+ "log_event" : LogEventResponse ,
1496+ "sync_event_sources" : SyncEventSourcesResponse ,
1497+ "sync_audiences" : SyncAudiencesResponse ,
1498+ "sync_catalogs" : SyncCatalogsResponse ,
1499+ "sync_governance" : SyncGovernanceResponse ,
1500+ # Feedback
1501+ "provide_performance_feedback" : ProvidePerformanceFeedbackResponse ,
1502+ # Protocol Discovery
1503+ "get_adcp_capabilities" : GetAdcpCapabilitiesResponse ,
1504+ # Compliance
1505+ "comply_test_controller" : ComplyTestControllerResponse ,
1506+ # Content Standards
1507+ "create_content_standards" : CreateContentStandardsResponse ,
1508+ "get_content_standards" : GetContentStandardsResponse ,
1509+ "list_content_standards" : ListContentStandardsResponse ,
1510+ "update_content_standards" : UpdateContentStandardsResponse ,
1511+ "calibrate_content" : CalibrateContentResponse ,
1512+ "validate_content_delivery" : ValidateContentDeliveryResponse ,
1513+ "get_media_buy_artifacts" : GetMediaBuyArtifactsResponse ,
1514+ # Governance
1515+ "get_creative_features" : GetCreativeFeaturesResponse ,
1516+ "sync_plans" : SyncPlansResponse ,
1517+ "check_governance" : CheckGovernanceResponse ,
1518+ "report_plan_outcome" : ReportPlanOutcomeResponse ,
1519+ "get_plan_audit_logs" : GetPlanAuditLogsResponse ,
1520+ # Property Lists
1521+ "create_property_list" : CreatePropertyListResponse ,
1522+ "get_property_list" : GetPropertyListResponse ,
1523+ "list_property_lists" : ListPropertyListsResponse ,
1524+ "update_property_list" : UpdatePropertyListResponse ,
1525+ "delete_property_list" : DeletePropertyListResponse ,
1526+ # Collection Lists
1527+ "create_collection_list" : CreateCollectionListResponse ,
1528+ "get_collection_list" : GetCollectionListResponse ,
1529+ "list_collection_lists" : ListCollectionListsResponse ,
1530+ "update_collection_list" : UpdateCollectionListResponse ,
1531+ "delete_collection_list" : DeleteCollectionListResponse ,
1532+ # Sponsored Intelligence
1533+ "si_get_offering" : SiGetOfferingResponse ,
1534+ "si_initiate_session" : SiInitiateSessionResponse ,
1535+ "si_send_message" : SiSendMessageResponse ,
1536+ "si_terminate_session" : SiTerminateSessionResponse ,
1537+ # Brand
1538+ "get_brand_identity" : GetBrandIdentityResponse ,
1539+ "get_rights" : GetRightsResponse ,
1540+ "acquire_rights" : AcquireRightsResponse ,
1541+ "update_rights" : UpdateRightsResponse ,
1542+ # TMP
1543+ "context_match" : ContextMatchResponse ,
1544+ "identity_match" : IdentityMatchResponse ,
1545+ }
1546+
1547+ schemas : dict [str , dict [str , Any ]] = {}
1548+ for tool_name , response_type in _tool_to_response .items ():
1549+ schema = _model_to_json_schema (response_type , allow_root_union = True )
1550+ if schema is None :
13581551 logger .debug (
1359- "Pydantic schema generation failed for %s, using hand-crafted schema " ,
1552+ "Pydantic output- schema generation failed for %s" ,
13601553 tool_name ,
1361- exc_info = True ,
13621554 )
1555+ continue
1556+ schemas [tool_name ] = schema
13631557
13641558 return schemas
13651559
13661560
13671561# Generate schemas once at import time
13681562_PYDANTIC_SCHEMAS = _generate_pydantic_schemas ()
1563+ _PYDANTIC_OUTPUT_SCHEMAS = _generate_pydantic_output_schemas ()
13691564
13701565
13711566def _apply_pydantic_schemas () -> None :
1372- """Replace hand-crafted inputSchemas with Pydantic-generated ones."""
1567+ """Apply Pydantic-generated input + output schemas to tool definitions.
1568+
1569+ * ``inputSchema``: replaced when a Pydantic-generated schema is
1570+ available (handles drift between hand-crafted stubs and the spec).
1571+ * ``outputSchema``: added so ``tools/list`` advertises the structured
1572+ response shape — matches the TS port and lets MCP clients validate
1573+ ``structuredContent`` without a separate spec lookup.
1574+ """
13731575 for tool_def in ADCP_TOOL_DEFINITIONS :
13741576 name = tool_def ["name" ]
13751577 if name in _PYDANTIC_SCHEMAS :
13761578 tool_def ["inputSchema" ] = _PYDANTIC_SCHEMAS [name ]
1579+ if name in _PYDANTIC_OUTPUT_SCHEMAS :
1580+ tool_def ["outputSchema" ] = _PYDANTIC_OUTPUT_SCHEMAS [name ]
13771581
13781582
13791583_apply_pydantic_schemas ()
0 commit comments