feat(server): TypeVar-bound ADCPHandler for typed ToolContext subclasses (#223)#234
Merged
feat(server): TypeVar-bound ADCPHandler for typed ToolContext subclasses (#223)#234
Conversation
…ses (closes #223) Roadmap PR-Q + expert-review followup from #219. Multi-tenant agents routinely subclass ToolContext to carry typed tenant/adapter/testing fields the base doesn't name — before this PR those fields required casts at every handler method. Now ADCPHandler is Generic[TContext] bound to ToolContext; downstream writes ``class MyAgent(ADCPHandler[MyContext])`` and every handler method signature propagates the subclass type. API - New TContext = TypeVar("TContext", bound="ToolContext") exported from adcp.server. Docstring at the declaration site explains the pattern. - ADCPHandler now inherits Generic[TContext]. All 57 method signatures in base.py rewritten to take context: TContext | None. - Protocol handlers (BrandHandler, ComplianceHandler, ContentStandardsHandler, GovernanceHandler, SponsoredIntelligenceHandler, TmpHandler) propagate TContext via class X(ADCPHandler[TContext], Generic[TContext]) so downstream can write class MyBrand(BrandHandler[MyContext]). - Internal SDK annotations (mcp_tools.py, serve.py, a2a_server.py, builder.py) use ADCPHandler[Any] where the SDK doesn't care about the TContext — the decorator-builder path and the transport executors don't thread a specific subclass. Backward compat - class MyAgent(ADCPHandler) without a TypeVar argument still works at runtime. Existing subclasses keep working without edits. - No runtime behavior change. The TypeVar is purely type-system narrowing; handler dispatch paths are unchanged. Tests — tests/test_handler_typevar.py (9 new, 1544 total) - Unparameterised subclass still works (backward compat). - Parameterised ADCPHandler[MyContext] constructs cleanly. - Protocol handlers propagate the TypeVar (BrandHandler[MyContext]). - Handler methods receive the subclass at dispatch time — the runtime isinstance(context, MyContext) is true. - BrandHandler[MyContext] propagation tested end-to-end. - TContext.__bound__ is ToolContext. - A2A executor dispatches a typed handler without issue. - Method signature structure preserved (self, params, context positions). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
5 tasks
- test_unparameterised_protocol_handler_still_works now covers ContentStandardsHandler, GovernanceHandler, and SponsoredIntelligenceHandler by dynamically building concrete subclasses that stub every abstract handle_<tool>. Proves the TypeVar refactor didn't accidentally add a new abstract on the base. - test_typevar_is_bound_to_toolcontext now forces forward-ref resolution via typing.get_type_hints so a typo in the bound (e.g. "ToolContect") would fail the test. Previously the unresolved forward-ref string was accepted as proof enough. - test_handler_method_signatures_accept_subclass_positionally renamed to test_handler_method_signatures_preserve_parameter_order — reflects what it actually checks. - Documented ADCPHandler[Any] choice in mcp_tools.py and a2a_server.py module docstrings: these modules dispatch by tool name and never read typed context fields, so Any is correct and avoids cascading the TypeVar through plumbing. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Closes #223 (and roadmap PR-Q). Multi-tenant agents routinely subclass `ToolContext` to carry typed tenant/adapter/testing fields. Before this PR those fields needed a `cast` or `isinstance` at every handler method. Now:
```python
from dataclasses import dataclass
from adcp.server import ADCPHandler, ToolContext
@DataClass
class MyContext(ToolContext):
adapter: MyPlatformAdapter
class MyAgent(ADCPHandler[MyContext]):
async def get_products(self, params, context: MyContext | None = None):
if context is not None:
adapter = context.adapter # typed, no cast
```
Backward compatibility
`class MyAgent(ADCPHandler)` without a TypeVar argument still works at runtime — Python allows unparameterised Generic inheritance. Every existing adopter keeps working without edits.
Test plan
Follow-ups (not in this PR)
🤖 Generated with Claude Code