authplane-fastmcp 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,79 @@
1
+ # Byte-compiled / optimized / DLL files
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+
6
+ # C extensions
7
+ *.so
8
+
9
+ # Distribution / packaging
10
+ .venv/
11
+ .Python
12
+ build/
13
+ develop-eggs/
14
+ dist/
15
+ downloads/
16
+ eggs/
17
+ .eggs/
18
+ lib/
19
+ lib64/
20
+ parts/
21
+ sdist/
22
+ var/
23
+ wheels/
24
+ share/python-wheels/
25
+ *.egg-info/
26
+ .installed.cfg
27
+ *.egg
28
+ MANIFEST
29
+
30
+ # PyInstaller
31
+ *.manifest
32
+ *.spec
33
+
34
+ # Unit test / coverage reports
35
+ htmlcov/
36
+ .tox/
37
+ .nox/
38
+ .coverage
39
+ .coverage.*
40
+ .cache
41
+ nosetests.xml
42
+ coverage.xml
43
+ *.cover
44
+ *.py,cover
45
+ .hypothesis/
46
+ .pytest_cache/
47
+ cover/
48
+
49
+ # Virtual environments
50
+ venv/
51
+ env/
52
+ ENV/
53
+ env.bak/
54
+ venv.bak/
55
+
56
+ # IDEs
57
+ .vscode/
58
+ .idea/
59
+ *.swp
60
+ *.swo
61
+ *~
62
+ .DS_Store
63
+
64
+ # mypy
65
+ .mypy_cache/
66
+ .dmypy.json
67
+ dmypy.json
68
+
69
+ # Pyre type checker
70
+ .pyre/
71
+
72
+ # pytype static type analyzer
73
+ .pytype/
74
+
75
+ # Cython debug symbols
76
+ cython_debug/
77
+
78
+ # Python version file
79
+ .python-version
@@ -0,0 +1,74 @@
1
+ Metadata-Version: 2.4
2
+ Name: authplane-fastmcp
3
+ Version: 0.1.0
4
+ Summary: Authplane JWT validation adapter for FastMCP
5
+ Project-URL: Homepage, https://github.com/AuthPlane/python-sdk/tree/main/authplane-fastmcp
6
+ Project-URL: Issues, https://github.com/AuthPlane/python-sdk/issues
7
+ Author: Authplane Team
8
+ License-Expression: Apache-2.0
9
+ Keywords: adapter,authplane,fastmcp,jwt,oauth
10
+ Requires-Python: >=3.11
11
+ Requires-Dist: authplane-sdk==0.1.0
12
+ Requires-Dist: fastmcp>=2.0
13
+ Requires-Dist: pydantic>=2.0
14
+ Provides-Extra: dev
15
+ Requires-Dist: coverage>=7; extra == 'dev'
16
+ Requires-Dist: cryptography>=42; extra == 'dev'
17
+ Requires-Dist: httpx>=0.27; extra == 'dev'
18
+ Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
19
+ Requires-Dist: pytest>=8.0; extra == 'dev'
20
+ Requires-Dist: respx>=0.21; extra == 'dev'
21
+ Requires-Dist: ruff>=0.8; extra == 'dev'
22
+ Description-Content-Type: text/markdown
23
+
24
+ # authplane-fastmcp
25
+
26
+ [![PyPI](https://img.shields.io/pypi/v/authplane-fastmcp?style=flat-square&label=authplane-fastmcp)](https://pypi.org/project/authplane-fastmcp/)
27
+ [![License](https://img.shields.io/badge/License-Apache_2.0-blue?style=flat-square)](https://opensource.org/licenses/Apache-2.0)
28
+
29
+ Authplane JWT validation for servers built on [FastMCP](https://github.com/PrefectHQ/fastmcp).
30
+
31
+ ## Install
32
+
33
+ ```bash
34
+ pip install authplane-fastmcp
35
+ ```
36
+
37
+ ## Quickstart
38
+
39
+ ```python
40
+ import asyncio
41
+ from authplane_fastmcp import authplane_auth
42
+ from fastmcp import FastMCP
43
+ from fastmcp.server.auth import AccessToken, require_scopes
44
+ from fastmcp.dependencies import CurrentAccessToken
45
+
46
+
47
+ async def main():
48
+ mcp = FastMCP(
49
+ "My MCP Server",
50
+ **await authplane_auth(
51
+ issuer="https://auth.company.com",
52
+ base_url="https://mcp.company.com",
53
+ scopes=["tools/query", "tools/write"],
54
+ ),
55
+ )
56
+
57
+ @mcp.tool(auth=require_scopes("tools/query"))
58
+ async def query_database(
59
+ query: str, token: AccessToken = CurrentAccessToken()
60
+ ) -> str:
61
+ user_id = token.claims.get("sub")
62
+ return f"Query: {query}, User: {user_id}"
63
+
64
+ await mcp.run_async(transport="http", host="0.0.0.0", port=8080)
65
+
66
+
67
+ asyncio.run(main())
68
+ ```
69
+
70
+ `authplane_auth()` holds background JWKS and metadata refresh tasks; call `aclose()` on the returned `client` during server shutdown.
71
+
72
+ ## Documentation
73
+
74
+ PRM behavior, dev mode, revocation checking, manual setup, scope enforcement semantics, claim access, the full `authplane_auth` / `AuthplaneTokenVerifier` API, and error handling: **[User Guide](docs/user-guide.md)**.
@@ -0,0 +1,51 @@
1
+ # authplane-fastmcp
2
+
3
+ [![PyPI](https://img.shields.io/pypi/v/authplane-fastmcp?style=flat-square&label=authplane-fastmcp)](https://pypi.org/project/authplane-fastmcp/)
4
+ [![License](https://img.shields.io/badge/License-Apache_2.0-blue?style=flat-square)](https://opensource.org/licenses/Apache-2.0)
5
+
6
+ Authplane JWT validation for servers built on [FastMCP](https://github.com/PrefectHQ/fastmcp).
7
+
8
+ ## Install
9
+
10
+ ```bash
11
+ pip install authplane-fastmcp
12
+ ```
13
+
14
+ ## Quickstart
15
+
16
+ ```python
17
+ import asyncio
18
+ from authplane_fastmcp import authplane_auth
19
+ from fastmcp import FastMCP
20
+ from fastmcp.server.auth import AccessToken, require_scopes
21
+ from fastmcp.dependencies import CurrentAccessToken
22
+
23
+
24
+ async def main():
25
+ mcp = FastMCP(
26
+ "My MCP Server",
27
+ **await authplane_auth(
28
+ issuer="https://auth.company.com",
29
+ base_url="https://mcp.company.com",
30
+ scopes=["tools/query", "tools/write"],
31
+ ),
32
+ )
33
+
34
+ @mcp.tool(auth=require_scopes("tools/query"))
35
+ async def query_database(
36
+ query: str, token: AccessToken = CurrentAccessToken()
37
+ ) -> str:
38
+ user_id = token.claims.get("sub")
39
+ return f"Query: {query}, User: {user_id}"
40
+
41
+ await mcp.run_async(transport="http", host="0.0.0.0", port=8080)
42
+
43
+
44
+ asyncio.run(main())
45
+ ```
46
+
47
+ `authplane_auth()` holds background JWKS and metadata refresh tasks; call `aclose()` on the returned `client` during server shutdown.
48
+
49
+ ## Documentation
50
+
51
+ PRM behavior, dev mode, revocation checking, manual setup, scope enforcement semantics, claim access, the full `authplane_auth` / `AuthplaneTokenVerifier` API, and error handling: **[User Guide](docs/user-guide.md)**.
@@ -0,0 +1,23 @@
1
+ """Authplane Python SDK - FastMCP adapter.
2
+
3
+ This package provides a thin adapter between the Authplane Python SDK and FastMCP,
4
+ enabling FastMCP servers to validate Authplane-issued JWT access tokens.
5
+
6
+ Core SDK types (``ASCredentials``, ``FetchSettings``, ``IntrospectionRevocation``,
7
+ DPoP types, errors, etc.) are imported from ``authplane`` directly. This package
8
+ exports only the adapter-owned glue.
9
+ """
10
+
11
+ __version__ = "0.1.0"
12
+
13
+ from .auth import AuthplaneAuthResult, authplane_auth
14
+ from .url_elicitation import to_url_elicitation_required_error
15
+ from .verifier import AuthplaneTokenVerifier
16
+
17
+ __all__ = [
18
+ "AuthplaneAuthResult",
19
+ "AuthplaneTokenVerifier",
20
+ "__version__",
21
+ "authplane_auth",
22
+ "to_url_elicitation_required_error",
23
+ ]
@@ -0,0 +1,283 @@
1
+ """Convenience factory for enabling Authplane auth on FastMCP servers.
2
+
3
+ Provides ``authplane_auth()``, an async factory function that creates and
4
+ configures all the components needed to add Authplane JWT validation to a
5
+ FastMCP server in a single call.
6
+ """
7
+
8
+ from collections.abc import Iterator
9
+ from typing import Any
10
+
11
+ from authplane import (
12
+ ASCredentials,
13
+ AuthplaneClient,
14
+ DPoPProvider,
15
+ FetchSettings,
16
+ InboundDPoPOptions,
17
+ IntrospectionRevocation,
18
+ RevocationChecker,
19
+ )
20
+ from authplane.oauth import TokenExchangeOptions, TokenResponse
21
+ from fastmcp.server.auth import RemoteAuthProvider
22
+ from pydantic import AnyHttpUrl
23
+
24
+ from .url_elicitation import to_url_elicitation_required_error
25
+ from .verifier import AuthplaneTokenVerifier
26
+
27
+
28
+ def _wrap_client_for_elicitation(client: AuthplaneClient) -> AuthplaneClient:
29
+ """Translate ``client.exchange`` consent errors into MCP ``-32042``.
30
+
31
+ Wrapping ``exchange()`` (the only method that realistically surfaces
32
+ ``ConsentRequiredError`` from the AS) means user tool code can call
33
+ ``result.client.exchange(...)`` without any try/except: a consent error
34
+ becomes a ``UrlElicitationRequiredError`` transparently, and FastMCP
35
+ forwards it as a ``-32042`` JSON-RPC error.
36
+ """
37
+ original_exchange = client.exchange
38
+
39
+ async def exchange(options: TokenExchangeOptions) -> TokenResponse:
40
+ try:
41
+ return await original_exchange(options)
42
+ except Exception as error:
43
+ mapped = to_url_elicitation_required_error(error)
44
+ if mapped is not None:
45
+ raise mapped from error
46
+ raise
47
+
48
+ client.exchange = exchange
49
+ return client
50
+
51
+
52
+ class AuthplaneAuthResult:
53
+ """Return value of ``authplane_auth()``.
54
+
55
+ Supports ``**`` unpacking into ``FastMCP()`` — only the ``auth`` key
56
+ is included in the mapping view so FastMCP receives exactly what it
57
+ expects. ``token_verifier`` and ``client`` are exposed as plain
58
+ attributes for advanced use cases such as RFC 8693 token exchange::
59
+
60
+ result = await authplane_auth(issuer=..., base_url=..., ...)
61
+ mcp = FastMCP("My Server", **result)
62
+
63
+ # Inside a tool handler — exchange a user token for a downstream token:
64
+ downstream = await result.client.exchange(TokenExchangeOptions(
65
+ subject_token=user_token,
66
+ ))
67
+
68
+ Call ``await result.aclose()`` on server shutdown to release background
69
+ tasks and HTTP connections held by the underlying ``AuthplaneClient``.
70
+ """
71
+
72
+ def __init__(
73
+ self,
74
+ auth: RemoteAuthProvider,
75
+ token_verifier: AuthplaneTokenVerifier,
76
+ client: AuthplaneClient,
77
+ ) -> None:
78
+ self.auth = auth
79
+ self.token_verifier = token_verifier
80
+ self.client = client
81
+
82
+ async def aclose(self) -> None:
83
+ """Release resources held by the underlying ``AuthplaneClient``.
84
+
85
+ Cancels the background JWKS refresh task and closes the HTTP
86
+ connection pool. Safe to call multiple times.
87
+ """
88
+ await self.client.aclose()
89
+
90
+ # Mapping protocol — yields only "auth" so FastMCP(**result) works cleanly.
91
+ def keys(self) -> list[str]:
92
+ return ["auth"]
93
+
94
+ def __getitem__(self, key: str) -> Any:
95
+ if key == "auth":
96
+ return self.auth
97
+ raise KeyError(key)
98
+
99
+ def __iter__(self) -> Iterator[str]:
100
+ return iter(self.keys())
101
+
102
+
103
+ async def authplane_auth(
104
+ issuer: str,
105
+ base_url: str,
106
+ scopes: list[str] | None = None,
107
+ *,
108
+ as_credentials: ASCredentials | None = None,
109
+ dpop: DPoPProvider | None = None,
110
+ allowed_algorithms: list[str] | None = None,
111
+ jwks_refresh_seconds: int | None = None,
112
+ metadata_refresh_seconds: int | None = None,
113
+ cache_ttl_buffer_seconds: float | None = None,
114
+ default_ttl_seconds: float | None = None,
115
+ circuit_breaker_threshold: int | None = None,
116
+ circuit_breaker_cooldown_seconds: float | None = None,
117
+ clock_skew_seconds: int | None = None,
118
+ dev_mode: bool | None = None,
119
+ fetch_settings: FetchSettings | None = None,
120
+ inbound_dpop: InboundDPoPOptions | None = None,
121
+ mcp_path: str = "/mcp",
122
+ revocation_checker: IntrospectionRevocation | RevocationChecker | None = None,
123
+ ) -> AuthplaneAuthResult:
124
+ """Build the kwargs to enable Authplane auth on a FastMCP server.
125
+
126
+ This async factory performs RFC 8414 metadata discovery, fetches the
127
+ JWKS, and wires up all components (client, resource, token verifier,
128
+ auth provider) in a single awaitable call.
129
+
130
+ Usage::
131
+
132
+ mcp = FastMCP("My Server", **await authplane_auth(
133
+ issuer="https://auth.company.com",
134
+ base_url="https://mcp.company.com",
135
+ scopes=["tools/query", "tools/write"],
136
+ ))
137
+
138
+ After setup:
139
+
140
+ - Use ``@mcp.tool(auth=require_scopes(...))`` for scope enforcement.
141
+ - Use ``CurrentAccessToken()`` or ``get_access_token()`` to access
142
+ token claims inside tool handlers (both are FastMCP built-ins).
143
+
144
+ Args:
145
+ issuer: Authplane authorization server URL
146
+ (e.g., ``"https://auth.company.com"``).
147
+ base_url: Root URL of the FastMCP server
148
+ (e.g., ``"https://mcp.company.com"``). Same value you would
149
+ pass to FastMCP for deployment.
150
+ scopes: All scopes this server supports. Used in AuthSettings
151
+ and the PRM document. Defaults to an empty list.
152
+ dpop: Optional DPoP provider used for outbound calls from the
153
+ underlying SDK client to the authorization server.
154
+ allowed_algorithms: Algorithms allowed for signature verification.
155
+ Defaults to SDK defaults (``["RS256", "ES256"]``).
156
+ jwks_refresh_seconds: JWKS cache TTL in seconds (default ``300``).
157
+ cache_ttl_buffer_seconds: Buffer subtracted from token TTLs
158
+ before cache expiry (default ``30.0``).
159
+ default_ttl_seconds: Fallback token cache TTL used when token
160
+ responses do not include expiry metadata (default ``3600.0``).
161
+ circuit_breaker_threshold: Number of transient failures before
162
+ opening the AS circuit breaker (default ``5``).
163
+ circuit_breaker_cooldown_seconds: Cooldown before allowing a
164
+ half-open probe request after the circuit opens
165
+ (default ``30.0``).
166
+ clock_skew_seconds: Leeway in seconds for exp/nbf/iat validation
167
+ (default ``30``).
168
+ dev_mode: Enable development mode. Relaxes SSRF checks to allow
169
+ HTTP, localhost, and private networks. Can also be set via
170
+ ``AUTHPLANE_DEV_MODE=true`` environment variable.
171
+ metadata_refresh_seconds: AS metadata cache TTL in seconds
172
+ (default ``3600``).
173
+ fetch_settings: Full ``FetchSettings`` object applied to both
174
+ metadata and JWKS fetches. When provided, overrides
175
+ ``dev_mode`` for those fetches.
176
+ inbound_dpop: Per-resource inbound DPoP policy
177
+ (:class:`InboundDPoPOptions`). Bundles ``required``,
178
+ ``signing_algs``, ``max_proof_age_seconds``,
179
+ ``clock_skew_seconds`` and ``replay_store``; see RFC 9728 §2 +
180
+ RFC 9449 §7.1 for the field semantics.
181
+ mcp_path: Mount path of the MCP endpoint (default ``"/mcp"``).
182
+ The JWT audience (resource) is derived as
183
+ ``base_url + mcp_path``. Only set this if you changed
184
+ FastMCP's default HTTP mount path.
185
+ as_credentials: Client credentials for authenticating to the AS.
186
+ Shared by introspection (RFC 7662) and token exchange (RFC 8693).
187
+ Required when using ``IntrospectionRevocation`` for authenticated
188
+ introspection, or when calling ``client.exchange()``.
189
+ revocation_checker: Controls token revocation checking after
190
+ signature validation passes.
191
+
192
+ - ``None`` (default): disables revocation checking (offline
193
+ validation only).
194
+ - ``IntrospectionRevocation()``: calls the AS
195
+ ``introspection_endpoint`` (RFC 7662) discovered from AS
196
+ metadata. Raises ``TokenRevokedError`` if ``active=false``.
197
+ Pass ``as_credentials`` for authenticated introspection.
198
+ Fails open if the endpoint is unavailable.
199
+ - async callable: custom checker called with
200
+ ``(VerifiedClaims, raw_token)``; return ``True`` to reject
201
+ the token (raises ``TokenRevokedError``).
202
+
203
+ Returns:
204
+ ``AuthplaneAuthResult`` with ``auth`` (``RemoteAuthProvider``),
205
+ ``token_verifier`` (``AuthplaneTokenVerifier``), and ``client``
206
+ (``AuthplaneClient``) attributes. Supports ``**`` unpacking into
207
+ ``FastMCP()`` — only ``auth`` is included in the mapping view.
208
+ Access ``client`` directly for RFC 8693 token exchange via
209
+ ``result.client.exchange()``.
210
+
211
+ Raises:
212
+ ValueError: If configuration is invalid (bad algorithms, etc.).
213
+ JWKSFetchError: If metadata discovery or JWKS fetching fails.
214
+ """
215
+ resolved_scopes = scopes or []
216
+
217
+ # Derive the canonical resource URL (= JWT audience) from base_url + mcp_path.
218
+ # This must match exactly what RemoteAuthProvider advertises in the PRM, which
219
+ # FastMCP computes as base_url + mcp_path via _get_resource_url().
220
+ resource = base_url.rstrip("/") + "/" + mcp_path.lstrip("/")
221
+
222
+ # Prepare client-level kwargs, filtering out None to use SDK defaults
223
+ client_kwargs_raw: dict[str, Any] = {
224
+ "dpop": dpop,
225
+ "dev_mode": dev_mode,
226
+ "fetch_settings": fetch_settings,
227
+ "jwks_refresh_seconds": jwks_refresh_seconds,
228
+ "metadata_refresh_seconds": metadata_refresh_seconds,
229
+ "cache_ttl_buffer_seconds": cache_ttl_buffer_seconds,
230
+ "default_ttl_seconds": default_ttl_seconds,
231
+ "circuit_breaker_threshold": circuit_breaker_threshold,
232
+ "circuit_breaker_cooldown_seconds": circuit_breaker_cooldown_seconds,
233
+ }
234
+ client_kwargs: dict[str, Any] = {k: v for k, v in client_kwargs_raw.items() if v is not None}
235
+
236
+ # Prepare resource-level kwargs, filtering out None to use SDK defaults
237
+ verifier_kwargs_raw: dict[str, Any] = {
238
+ "allowed_algorithms": allowed_algorithms,
239
+ "clock_skew_seconds": clock_skew_seconds,
240
+ "inbound_dpop": inbound_dpop,
241
+ }
242
+ verifier_kwargs: dict[str, Any] = {
243
+ k: v for k, v in verifier_kwargs_raw.items() if v is not None
244
+ }
245
+
246
+ # Create the AuthplaneClient (handles metadata discovery, JWKS fetching, caching)
247
+ client = await AuthplaneClient.create(
248
+ issuer=issuer,
249
+ auth=as_credentials,
250
+ **client_kwargs,
251
+ )
252
+
253
+ # Translate ConsentRequiredError → MCP UrlElicitationRequiredError at the
254
+ # client boundary, before user tool code sees it. Tool authors don't need
255
+ # to wrap handlers or import elicitation primitives — the MCP wire-format
256
+ # mapping is owned by the adapter that constructs the client.
257
+ client = _wrap_client_for_elicitation(client)
258
+
259
+ # Create the resource from the client
260
+ verifier = client.resource(
261
+ resource=resource,
262
+ scopes=resolved_scopes,
263
+ revocation_checker=revocation_checker,
264
+ **verifier_kwargs,
265
+ )
266
+
267
+ # Wrap in AuthplaneTokenVerifier
268
+ # Note: FastMCP 3.0.0 uses token_verifier.base_url for PRM generation if provided
269
+ token_verifier = AuthplaneTokenVerifier(verifier, base_url=base_url)
270
+
271
+ # Wrap in RemoteAuthProvider to get PRM routes
272
+ auth_provider = RemoteAuthProvider(
273
+ token_verifier=token_verifier,
274
+ authorization_servers=[AnyHttpUrl(issuer)],
275
+ base_url=AnyHttpUrl(base_url),
276
+ scopes_supported=resolved_scopes,
277
+ )
278
+
279
+ return AuthplaneAuthResult(
280
+ auth=auth_provider,
281
+ token_verifier=token_verifier,
282
+ client=client,
283
+ )
File without changes
@@ -0,0 +1,42 @@
1
+ """URL elicitation primitive for the FastMCP adapter.
2
+
3
+ The factory function :func:`authplane_fastmcp.authplane_auth` wraps the
4
+ ``AuthplaneClient`` it returns so that ``client.exchange()`` consent errors
5
+ auto-translate into MCP ``-32042`` (URL elicitation required) before user
6
+ tool code sees them. This module exposes the underlying conversion as a
7
+ small primitive for unusual flows where users build a consent error outside
8
+ the wrapped client and want to raise the MCP-shaped error themselves.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ from uuid import uuid4
14
+
15
+ from authplane.errors import ConsentRequiredError
16
+ from mcp.shared.exceptions import UrlElicitationRequiredError
17
+ from mcp.types import ElicitRequestURLParams
18
+
19
+
20
+ def to_url_elicitation_required_error(
21
+ error: BaseException,
22
+ ) -> UrlElicitationRequiredError | None:
23
+ """Map a :class:`ConsentRequiredError` with a ``consent_url`` to MCP -32042.
24
+
25
+ Returns ``None`` for any other input — non-consent errors and consent
26
+ errors without a ``consent_url`` are not translatable. Callers then
27
+ re-raise the original exception unchanged.
28
+ """
29
+ if not isinstance(error, ConsentRequiredError) or not error.consent_url:
30
+ return None
31
+
32
+ return UrlElicitationRequiredError(
33
+ elicitations=[
34
+ ElicitRequestURLParams(
35
+ mode="url",
36
+ url=error.consent_url,
37
+ elicitationId=str(uuid4()),
38
+ message=error.describe(),
39
+ )
40
+ ],
41
+ message=str(error),
42
+ )
@@ -0,0 +1,95 @@
1
+ """AuthplaneTokenVerifier - FastMCP TokenVerifier backed by AuthplaneResource.
2
+
3
+ Bridges the Authplane core SDK with FastMCP's ``TokenVerifier`` interface,
4
+ delegating all JWT validation to ``AuthplaneResource`` and mapping results
5
+ to FastMCP's ``AccessToken`` with the full JWT payload in ``claims``.
6
+ """
7
+
8
+ from typing import Any, cast
9
+
10
+ from authplane import AuthplaneError, AuthplaneResource
11
+ from fastmcp.server.auth import AccessToken, TokenVerifier
12
+
13
+
14
+ class AuthplaneTokenVerifier(TokenVerifier):
15
+ """FastMCP TokenVerifier backed by AuthplaneResource.
16
+
17
+ Validates JWTs once per request via the core Authplane SDK and returns
18
+ a FastMCP ``AccessToken`` with ``claims`` populated from the full JWT
19
+ payload (``VerifiedClaims.raw``). Token claims are then available in
20
+ tool handlers via FastMCP's native ``CurrentAccessToken()`` dependency
21
+ or ``get_access_token()`` function.
22
+
23
+ All security-critical logic (signature verification, claim validation,
24
+ JWKS caching, SSRF protection) is handled by the core SDK. This class
25
+ is a thin adapter that maps between the two interfaces.
26
+
27
+ Scope enforcement is FastMCP's responsibility via
28
+ ``@mcp.tool(auth=require_scopes(...))``.
29
+ """
30
+
31
+ def __init__(
32
+ self,
33
+ verifier: AuthplaneResource,
34
+ base_url: str | None = None,
35
+ required_scopes: list[str] | None = None,
36
+ ) -> None:
37
+ """Initialize the token verifier.
38
+
39
+ Args:
40
+ verifier: A fully initialized ``AuthplaneResource`` instance,
41
+ typically created via ``AuthplaneClient.create()`` and
42
+ ``client.resource()``.
43
+ base_url: The base URL of this server. Passed to the parent
44
+ ``TokenVerifier`` for PRM generation.
45
+ required_scopes: Scopes required for all requests. Passed to
46
+ the parent ``TokenVerifier``.
47
+ """
48
+ super().__init__(base_url=base_url, required_scopes=required_scopes)
49
+ self._verifier = verifier
50
+
51
+ @property
52
+ def verifier(self) -> AuthplaneResource:
53
+ """The underlying ``AuthplaneResource`` instance."""
54
+ return self._verifier
55
+
56
+ @property
57
+ def scopes_supported(self) -> list[str]:
58
+ """Return scopes supported by this verifier.
59
+
60
+ Returns the scopes configured in the ``AuthplaneResource``, which
61
+ are used by FastMCP for PRM metadata generation at
62
+ ``/.well-known/oauth-protected-resource``.
63
+ """
64
+ return list(self._verifier.scopes)
65
+
66
+ async def verify_token(self, token: str) -> AccessToken | None:
67
+ """Validate a JWT and return a FastMCP AccessToken.
68
+
69
+ Called by FastMCP once per authenticated request. Delegates to
70
+ ``AuthplaneResource.verify()`` for all validation, then maps the
71
+ resulting ``VerifiedClaims`` to a FastMCP ``AccessToken`` with the
72
+ full JWT payload in ``claims``.
73
+
74
+ Args:
75
+ token: The raw JWT string (FastMCP strips ``'Bearer '``
76
+ before calling this method).
77
+
78
+ Returns:
79
+ ``AccessToken`` on successful validation with ``token``,
80
+ ``client_id``, ``scopes``, ``expires_at``, and ``claims``
81
+ (full JWT payload dict) fields populated. Returns ``None``
82
+ on any validation failure (FastMCP responds with 401).
83
+ """
84
+ try:
85
+ claims = await self._verifier.verify(token)
86
+ except AuthplaneError:
87
+ return None
88
+
89
+ return AccessToken(
90
+ token=token,
91
+ client_id=claims.client_id,
92
+ scopes=list(claims.scopes),
93
+ expires_at=claims.expires_at,
94
+ claims=cast("dict[str, Any]", claims.raw), # full JWT payload
95
+ )