Skip to content

_resolve_params_pydantic_model resolves base-class annotation, breaking subclass-extended request validation #596

Description

@bokelley

Summary

When a seller's DecisioningPlatform subclass overrides a tool method with a more-specific request annotation, the SDK's _resolve_params_pydantic_model resolves the base class's annotation, not the subclass's. The seller's extended schema (e.g. with extra="forbid" for strict-extra rejection) is silently bypassed at the wire boundary.

Repro

Salesagent has its own GetProductsRequest extending the library's, with extra="forbid" in dev mode to catch unknown fields:

# src/core/schemas/product.py
from adcp.types import GetProductsWholesaleRequest as LibraryGetProductsRequest

class GetProductsRequest(LibraryGetProductsRequest):
    model_config = ConfigDict(extra=get_pydantic_extra_mode())  # "forbid" in dev
    buying_mode: str | None = Field(None, ...)  # widened
    ...

The subclass platform method:

# core/platforms/mock.py
class MockSellerPlatform(DecisioningPlatform):
    async def get_products(
        self,
        req: GetProductsRequest,  # ← salesagent's strict-extra subclass
        ctx: RequestContext[Any],
    ) -> dict[str, Any]:
        return await _delegate_get_products(req, ctx)

Annotations resolve correctly via typing.get_type_hints:

hints: {'req': <class 'src.core.schemas.product.GetProductsRequest'>, ...}

But when the SDK builds the tool caller via create_tool_caller(handler, "get_products", ...), _resolve_params_pydantic_model(method) returns the library's type:

[DEBUG SDK] get_products: params_model=<class 'adcp.types.generated_poc.media_buy.get_products_request.GetProductsRequest'>

Library's GetProductsRequest has extra="allow", so calls with unknown fields slip through validation untouched. The seller's intended strict-extra contract is silently bypassed.

Impact

Any seller who extends a library request type to add stricter validation (forbid extra fields, narrow union types, custom validators) finds those rules silently bypassed at the MCP wire boundary. Tests that rely on the wire layer rejecting unknown fields (e.g. salesagent's test_unknown_field_rejected) fail with no obvious cause — the validation simply doesn't run.

Suspected cause

_resolve_params_pydantic_model likely walks the platform's base class methods (because that's how it gets the catalog of advertised tools and their canonical signatures) instead of the subclass's actual override. The base-class signature uses the library type; the subclass override uses the seller's extended type.

Suggested fix

When iterating advertised tools to build callers, look up getattr(handler, method_name) (which is the subclass-bound method) and use that method's annotations for the params model resolution — not the base class's signature. The base class's role is to advertise; the subclass's role is to type-narrow.

Reproduction repo: https://github.com/bokelley/salesagent (hits this on 4.5.0; salesagent test tests/integration/test_mcp_unknown_field_handling.py::TestMcpDevMode::test_unknown_field_rejected).

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions