feat(keycardai-fastmcp): grant-as-dependency typed injection (deprecate get_state)#204
Conversation
…mcp.dependencies
AuthProvider.grant() now returns a GrantDependency, a fastmcp.dependencies
Dependency subclass. Declared as a typed parameter default
(access: AccessContext = auth_provider.grant(...)), FastMCP resolves it per
request: __aenter__ reads the caller token, performs the RFC 8693 exchanges,
and injects the populated AccessContext, hidden from the tool input schema.
Errors are recorded on the AccessContext, never raised, preserving the
existing error-capture contract.
GrantDependency.__call__ keeps the @auth_provider.grant(...) decorator
spelling working from the same object. The decorator now injects into a
declared AccessContext parameter (hidden from the wrapper signature) and
emits a decoration-time DeprecationWarning, once per tool, when no
AccessContext parameter is declared; it keeps dual-writing
set_state("keycardai") during the deprecation window.
Also:
- AccessContext.from_context(ctx) escape hatch for helpers called from
inside tools that only hold the FastMCP Context.
- Fix the Context parameter check: resolve string annotations via
get_type_hints and match Context inside unions (Context | None) and
Annotated forms; a declared AccessContext parameter removes the
ctx: Context requirement entirely.
- override_access_context(): public seam that forces grant resolution to a
caller-supplied AccessContext (used by the testing utilities).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… override seam mock_access_context previously patched module internals (keycardai.fastmcp.provider.AccessContext and get_access_token). It now builds a real AccessContext, preloaded with the requested tokens or error state, and installs it through the public override_access_context() seam, so grant resolution short-circuits on both the injected-parameter and decorator paths without any patching. The yielded object is now a real AccessContext (subclassed only to serve a default token for arbitrary resources), so assertions exercise the same error paths as production code. override_access_context is re-exported from keycardai.fastmcp.testing for tests that want to build the AccessContext themselves. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ameter grant form
The README quick start, package docstrings, and both examples now declare
grants as typed AccessContext parameter defaults instead of the deprecated
decorator + ctx.get_state("keycardai") pattern. Adds README sections on
error capture, the from_context escape hatch, and the mock_access_context
testing utility.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ndency extension point GrantDependency subclasses fastmcp.dependencies.Dependency. The fastmcp.dependencies module exists since 3.0.0, but it only exports the Dependency base class (via the uncalled-for DI engine) starting with fastmcp 3.1.0; on 3.0.x the module exports Depends only, so keycardai-fastmcp would fail to import. Raise the manifest floor in the package and both examples accordingly (verified against the published 3.0.0/3.0.2 vs 3.1.0 wheels on PyPI). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…stMCP server Adds a round trip through FastMCP's in-memory client verifying the injected AccessContext parameter is resolved per request and excluded from the tool input schema. Ignores flake8-bugbear B008 in tests and examples: auth_provider.grant(...) in a parameter default is the intended injection spelling, matching how FastAPI treats Depends(). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
📦 Release PreviewThis analysis shows the expected release impact: 📈 Expected Version Changes📋 Package Details[
{
"package_name": "keycardai-fastmcp",
"package_dir": "packages/fastmcp",
"has_changes": true,
"current_version": "0.4.0",
"next_version": "0.5.0",
"increment": "MINOR"
}
]📝 Changelog PreviewThis comment was automatically generated by the release preview workflow. |
… primary nouns GrantDependency stays exported for type annotations but is documented as the return type of AuthProvider.grant(), not a concept to learn. override_access_context moves out of the package root: it is a testing seam and lives in keycardai.fastmcp.testing only. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
📦 Release PreviewThis analysis shows the expected release impact: 📈 Expected Version Changes📋 Package Details[
{
"package_name": "keycardai-fastmcp",
"package_dir": "packages/fastmcp",
"has_changes": true,
"current_version": "0.4.0",
"next_version": "0.5.0",
"increment": "MINOR"
}
]📝 Changelog PreviewThis comment was automatically generated by the release preview workflow. |
All-or-nothing multi-resource grant behavior is now stated on GrantDependency and _build_access_context. The mock's bare-token form answers for any resource; the docstring and README steer strict tests to resource_tokens. README gains a migration note covering warning escalation (-W error) on the deprecated decorator path and a B008 per-file-ignore example for downstream linters. The string-annotation fallback in _annotation_matches carries a comment on its narrow false-positive window. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
📦 Release PreviewThis analysis shows the expected release impact: 📈 Expected Version Changes📋 Package Details[
{
"package_name": "keycardai-fastmcp",
"package_dir": "packages/fastmcp",
"has_changes": true,
"current_version": "0.4.0",
"next_version": "0.5.0",
"increment": "MINOR"
}
]📝 Changelog PreviewThis comment was automatically generated by the release preview workflow. |
cmars
left a comment
There was a problem hiding this comment.
LGTM with a question about type signatures. I think type checking might be turned off but could be a latent issue if/when it's turned on (or is it supposed to be on?)
| request_scopes: str | list[str] | dict[str, str | list[str]] | None = None, | ||
| ): | ||
| """Decorator for automatic delegated token exchange. | ||
| ) -> GrantDependency: |
There was a problem hiding this comment.
Should this cast to AccessContext? That seems to be how it is used in the examples.
| ) -> GrantDependency: | |
| ) -> AccessContext: |
There was a problem hiding this comment.
good catch. the bigger problem is that the current annotation also makes the param default form fail type checking (mypy flags the default as incompatible). annotating AccessContext fixes that but breaks the decorator form since AccessContext is not callable. changed it to Any in 96021d2, which is what FastAPI Depends() and fastmcp own DI do. the param annotation carries the type for the injected form, and we can narrow to AccessContext once the decorator path is gone.
re type checking: CI does not run a checker on this package and none of our packages ship py.typed, so consumer checkers see everything as untyped anyway. filed ECO-171 to fix both.
…check A GrantDependency return annotation makes the documented parameter default (access: AccessContext = auth_provider.grant(...)) fail type checking, and AccessContext breaks the still-supported decorator form. Any is the FastAPI Depends() convention: the parameter annotation carries the type for the injected form and the decorator form stays callable. Narrow to AccessContext when the decorator path is removed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
📦 Release PreviewThis analysis shows the expected release impact: 📈 Expected Version Changes📋 Package Details[
{
"package_name": "keycardai-fastmcp",
"package_dir": "packages/fastmcp",
"has_changes": true,
"current_version": "0.4.0",
"next_version": "0.5.0",
"increment": "MINOR"
}
]📝 Changelog PreviewThis comment was automatically generated by the release preview workflow. |
Implements ECO-133 (SDK Framework Support Expansion): moves FastMCP tools off the
ctx.get_state("keycardai")magic string onto typed dependency injection, using FastMCP 3's ownfastmcp.dependenciesextension point.What changed
Grant as a dependency.
auth_provider.grant(...)now returns aGrantDependency(afastmcp.dependencies.Dependencysubclass). Declared as a parameter default, FastMCP resolves it per request: it reads the caller token viaget_access_token(), performs the RFC 8693 exchanges, and injects the populatedAccessContext. Errors are recorded on the context, never raised, preserving the existing error-capture contract.The same object still works as a decorator (
@auth_provider.grant(...)), so both idioms share one entry point. The parameter is excluded from the generated tool input schema.Deprecation path. The decorator spelling without a declared
AccessContextparameter emits aDeprecationWarningat decoration time (once per tool) and keeps dual-writingset_state("keycardai", ...). Tools that declare the parameter get it injected with no warning._has_contextfix. The identity check onctx: Contextannotations broke underfrom __future__ import annotationsandctx: Context | None. Replaced withget_type_hints-based resolution that handles string annotations, unions, andAnnotated.Escape hatch.
AccessContext.from_context(ctx)for helpers called from inside tools; returns the stored context or an error-carryingAccessContext, never raises.Public testing seam. New
override_access_context(...)context manager inkeycardai.fastmcp.testing.mock_access_contextnow installs a real preloadedAccessContextthrough it instead of patching module internals, with the same signature and matching error behavior (un-granted resources raiseResourceAccessError, as the realAccessContextand the old mock both did).Public surface stays two nouns. The concepts a developer learns remain
grantandAccessContext(per FRAMEWORK-INTEGRATIONS.md principle 2).GrantDependencyis exported for type annotations only and documented as the return type ofgrant(), never constructed directly. The testing seam lives in.testingonly, not the package root.Compatibility note: fastmcp floor raised to
>=3.1.0The
Dependencybase class is exported fromfastmcp.dependenciesstarting 3.1.0; on 3.0.x onlyDependsexists and the import fails, so the floor raise is unavoidable for this feature (package and both examples, manifests not just the lock). Consumers pinned to fastmcp 3.0.x must bump fastmcp when taking this minor; everything else is backward compatible (the decorator form keeps working and dual-writesset_state).One second-order effect worth knowing (flagged in review): the
DeprecationWarningfires at decoration time, i.e. module import. Setups that escalate warnings to errors (-W error, pytestfilterwarnings = ["error"]) will fail at import for tools still on the bare decorator form. The README migration section documents this and shows the targetedfilterwarningsallow-rule.Review follow-ups applied
From an independent review pass: documented the all-or-nothing multi-resource grant contract (pre-existing behavior, now stated on
GrantDependencyand in the README), documented that the mock's bare-token form serves any resource and steered strict tests toresource_tokens, added a B008 per-file-ignore example for downstream linters, and commented the narrow false-positive window of the string-annotation fallback. Deliberately not changed: decoration-time warning stays (specced in ECO-133, once per tool, filterable), the mock's default-token behavior stays (pre-PR documented contract), and all-or-nothing stays (changing it is a cross-SDK contract decision, not a PR nit).Docs. README quick start, docstrings, and the
delegated_accessexample flipped to the injected-parameter form, plus new sections on error capture,from_context, and testing.Deprecation timeline
v0.5.0 supports both forms and warns on the untyped decorator path; the
set_statewrite is removed in v0.7.0 or 1.0.Testing
85 tests pass in
packages/fastmcp(24 new: injected happy path, end-to-end through a real FastMCP server via the in-memory client, decorator compatibility and once-per-tool warning, dual-write, exchange-failure capture,from_context, override seam, string and union ctx annotations). Bridge (packages/mcp-fastmcp, 5) andpackages/mcp(542) suites green.just checkclean.Known gap, left for a follow-up to stay in scope: the justfile's
testtargets do not includepackages/fastmcp, so CI does not run this package's suite today.🤖 Generated with Claude Code