Skip to content

sdk(server): typed handler signatures — params: GetProductsRequest instead of params.get(...) #214

@bokelley

Description

@bokelley

Proposal

ADCPHandler methods currently take untyped params: dict[str, Any] and callers lean on params.get(...) everywhere. Every round of DX validation (rounds 4-7, see #205) flagged this as the biggest structural boilerplate complaint:

  • Agents re-learn field names from the schema for every handler.
  • No IDE autocomplete on request fields.
  • No Pydantic validation at the handler boundary — schema validation happens in the transport layer, which is too late for business-logic code.
  • Typos in params.get("brand") vs params.get("brnad") land at runtime as silent None.

Target API

class MySeller(ADCPHandler):
    async def get_products(
        self,
        params: GetProductsRequest,
        context: ToolContext | None = None,
    ) -> GetProductsResponse:
        # params.brief, params.promoted_offering, etc. — typed, validated
        ...

Handler registration would inspect the annotation and use the right <ToolName>Request Pydantic model to deserialize params before calling the handler. Handlers that still declare params: dict continue to work (back-compat).

Scope (estimate — multi-day, not 30-min)

  1. Base class dispatch layerADCPHandler._dispatch(tool_name, raw_params, context) inspects the override's signature annotation. If it's a Pydantic model, deserialize; else pass dict (back-compat).
  2. Response validation — optional: if the handler's return annotation is a Pydantic model, validate the return on the way out. Gives typed throughout.
  3. Examples — rewrite examples/seller_agent.py to use typed signatures. Reference implementation across seller/generative/retail.
  4. Skills — update all 5 build-*-agent skills to teach the typed pattern as the primary path, with a one-paragraph note on the legacy dict path for migrators.
  5. MCP tool registration_generate_pydantic_schemas() already maps tool name → request model. Dispatch layer should read from the same source of truth, so there's one mapping.
  6. Tests — exhaustive coverage of: typed handler with typed model, typed handler with dict fallback, typed handler raising Pydantic ValidationError, sibling handlers with mixed typed/untyped signatures.

Open questions

  • Return-type enforcement policy: strict (reject return shape that doesn't match the declared model) or lax (best-effort coerce)? The current response builders (products_response(...), media_buy_response(...)) return dicts; users may have already adopted the pattern of returning dicts. A strict return-type policy would break them.
  • Error surface when Pydantic validation fails at the handler boundary: should we return a structured AdCP error (INVALID_REQUEST with field=...) from the dispatch layer, or let the exception propagate? First option is friendlier; second is more transparent.
  • Generic request type: some handlers (e.g., comply_test_controller) take a union type. Current generator falls back to a stub for unions — does the typed-dispatch layer need to handle unions specially, or can it just require the author to model_validate the union themselves inside the handler?

Priority

4.1+ feature, not 4.0-gating. The current dict-based API works and is well-tested; this is DX polish worth landing in a dedicated PR with its own review cycle rather than rushing into a release cut.

Motivating agent feedback (verbatim from round 7)

"Every handler is params, context=None with manual params.get(...). The SDK has generated request models — typed handler signatures (params: SyncCreativesRequest) would give autocomplete + validation for free."
— round-7 creative skill validator (.context/dx-runs/creative/report.md)

"Handler takes params: dict[str, Any] when every other part of the SDK is strict Pydantic. Feels like the one place we stopped short."
— paraphrased from round-7 seller skill boilerplate complaint

Related: #205

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions