Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 1 addition & 11 deletions packages/fastmcp/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,6 @@ dependencies = [
"httpx>=0.27.2",
"keycardai-oauth>=0.7.0",
"fastmcp>=3.1.0",
# Pinned to the 1.x line, both ends load-bearing. fastmcp 3.x pins
# mcp<2.0, and keycardai-mcp 1.x is the release line that agrees (2.x
# requires mcp>=2.0). 0.x releases before 0.27 declare an unbounded mcp
# floor and resolve onto mcp 2.0, which fails to import. Move both ends
# together with the fastmcp 4.x bump (ECO-198).
"keycardai-mcp>=1,<2",
]
keywords = ["fastmcp", "mcp", "model-context-protocol", "oauth", "token-exchange", "authentication", "keycard"]
classifiers = [
Expand Down Expand Up @@ -51,12 +45,8 @@ Documentation = "https://docs.keycardai.com"
Issues = "https://github.com/keycardai/python-sdk/issues"

[tool.uv.sources]
# Only keycardai-mcp comes from the index: it is on mcp>=2.0 and this package is
# on mcp<2.0, so the local copy is genuinely unusable here. Everything else is
# path-linked so a breaking change in a sibling still fails this package's CI.
# keycardai-oauth is path-linked so a breaking sibling change fails this package's CI.
keycardai-oauth = { path = "../oauth", editable = true }
# keycardai-starlette stays on the index: it is a transitive dependency via
# keycardai-mcp, and tool.uv.sources only redirects direct dependencies.

[build-system]
requires = ["hatchling", "uv-dynamic-versioning"]
Expand Down
37 changes: 19 additions & 18 deletions packages/fastmcp/src/keycardai/fastmcp/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,11 @@

Re-export Guide:
Local definitions (primary API): AuthProvider, AccessContext
From keycardai.mcp.server.auth: ApplicationCredential, ClientSecret, EKSWorkloadIdentity, WebIdentity
From keycardai.mcp.server.auth.client_factory: ClientFactory, DefaultClientFactory
From keycardai.oauth.server: ApplicationCredential, ClientSecret, EKSWorkloadIdentity, WebIdentity
From keycardai.oauth.server.client_factory: ClientFactory, DefaultClientFactory
From keycardai.oauth.http.auth: AuthStrategy, BasicAuth, MultiZoneBasicAuth, NoneAuth
From keycardai.mcp.server.exceptions: All exceptions
From keycardai.oauth.server.exceptions: All exceptions except MissingContextError
Locally defined: MissingContextError
For canonical imports, use the source packages directly.

Basic Usage:
Expand Down Expand Up @@ -86,14 +87,20 @@ async def sync_calendar_to_drive(
)
"""

from keycardai.mcp.server.auth import (
from keycardai.oauth.http.auth import (
AuthStrategy,
BasicAuth,
MultiZoneBasicAuth,
NoneAuth,
)
from keycardai.oauth.server import (
ApplicationCredential,
ClientSecret,
EKSWorkloadIdentity,
WebIdentity,
)
from keycardai.mcp.server.auth.client_factory import ClientFactory, DefaultClientFactory
from keycardai.mcp.server.exceptions import (
from keycardai.oauth.server.client_factory import ClientFactory, DefaultClientFactory
from keycardai.oauth.server.exceptions import (
# Specific exceptions
AuthProviderConfigurationError,
AuthProviderInternalError,
Expand All @@ -102,21 +109,15 @@ async def sync_calendar_to_drive(
EKSWorkloadIdentityConfigurationError,
EKSWorkloadIdentityRuntimeError,
JWKSValidationError,
# Base exception
MCPServerError,
MetadataDiscoveryError,
MissingContextError,
OAuthClientConfigurationError,
# Base exception
OAuthServerError as MCPServerError,
ResourceAccessError,
TokenExchangeError,
)
from keycardai.oauth.http.auth import (
AuthStrategy,
BasicAuth,
MultiZoneBasicAuth,
NoneAuth,
)

from .exceptions import MissingContextError
from .provider import (
AccessContext,
AuthProvider,
Expand All @@ -131,12 +132,12 @@ async def sync_calendar_to_drive(
# === Typing Support ===
# Return type of AuthProvider.grant(); exported for annotations, never constructed directly
"GrantDependency",
# === Application Credentials (re-exported from keycardai.mcp.server.auth) ===
# === Application Credentials (re-exported from keycardai.oauth.server) ===
"ApplicationCredential",
"ClientSecret",
"EKSWorkloadIdentity",
"WebIdentity",
# === Client Factory (Advanced - re-exported from keycardai.mcp.server.auth) ===
# === Client Factory (Advanced - re-exported from keycardai.oauth.server.client_factory) ===
# Use ClientFactory protocol for custom implementations; DefaultClientFactory for defaults
"ClientFactory",
"DefaultClientFactory",
Expand All @@ -145,7 +146,7 @@ async def sync_calendar_to_drive(
"BasicAuth",
"MultiZoneBasicAuth",
"NoneAuth",
# === Exceptions (re-exported from keycardai.mcp.server.exceptions) ===
# === Exceptions (re-exported from keycardai.oauth.server.exceptions) ===
# Base
"MCPServerError",
# Configuration
Expand Down
64 changes: 64 additions & 0 deletions packages/fastmcp/src/keycardai/fastmcp/exceptions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
"""Exceptions for the Keycard FastMCP integration.

Framework-free exceptions come from keycardai.oauth.server.exceptions.
``MissingContextError`` is defined here because its guidance references
FastMCP ``Context``.
"""

from __future__ import annotations

from keycardai.oauth.server.exceptions import OAuthServerError

__all__ = ["MissingContextError"]


class MissingContextError(OAuthServerError):
"""Raised when the grant decorator encounters a missing context error."""

def __init__(
self,
message: str | None = None,
*,
function_name: str | None = None,
parameters: list[str] | None = None,
runtime_context: bool = False,
):
if message is None:
func_info = f"'{function_name}'" if function_name else "function"

if runtime_context:
message = (
f"Context parameter not found in {func_info} arguments.\n\n"
"This error occurs when:\n"
"1. Context parameter is not properly annotated with type hint\n"
"2. Context is not passed when calling the function\n\n"
"Ensure your function signature looks like:\n"
" from fastmcp import Context\n\n"
f" async def {function_name or 'your_function'}(ctx: Context, ...): # <- Context must be type-hinted\n\n"
"FastMCP injects Context on tool calls; when calling the "
"function directly, pass a Context explicitly."
)
else:
message = (
f"Function {func_info} must have a Context parameter to use @grant decorator.\n\n"
"The @grant decorator requires access to Context to store access tokens.\n\n"
"Fix by adding Context parameter:\n"
" from fastmcp import Context\n\n"
" @auth_provider.grant('https://api.example.com')\n"
f" async def {function_name or 'your_function'}(ctx: Context, ...): # <- Add 'ctx: Context' parameter\n"
" access_context = await ctx.get_state('keycardai')\n"
" # ... rest of function"
)

details = {
"function_name": function_name or "unknown",
"current_parameters": parameters or [],
"runtime_context": runtime_context,
"solution": (
"Add 'ctx: Context' parameter to function signature"
if not runtime_context
else "Ensure Context parameter is properly type-hinted and passed"
),
}

super().__init__(message, details=details)
15 changes: 8 additions & 7 deletions packages/fastmcp/src/keycardai/fastmcp/provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,28 +27,29 @@
from fastmcp.server.auth import RemoteAuthProvider
from fastmcp.server.auth.providers.jwt import JWTVerifier
from fastmcp.server.dependencies import get_access_token, get_context
from keycardai.mcp.server.auth import (
from keycardai.oauth import AsyncClient, Client
from keycardai.oauth.http.auth import NoneAuth
from keycardai.oauth.server import (
ApplicationCredential,
ClientSecret,
EKSWorkloadIdentity,
WebIdentity,
)
from keycardai.mcp.server.auth.client_factory import (
from keycardai.oauth.server.client_factory import (
ClientFactory,
DefaultClientFactory,
)
from keycardai.mcp.server.exceptions import (
from keycardai.oauth.server.exceptions import (
AuthProviderConfigurationError,
AuthProviderInternalError,
AuthProviderRemoteError,
MissingContextError,
ResourceAccessError,
)
from keycardai.oauth import AsyncClient, Client
from keycardai.oauth.http.auth import NoneAuth
from keycardai.oauth.types.models import TokenExchangeRequest, TokenResponse
from keycardai.oauth.utils.jwt import extract_scopes, get_claims

from .exceptions import MissingContextError

__all__ = [
"INTROSPECT",
"AccessContext",
Expand Down Expand Up @@ -634,7 +635,7 @@ class AuthProvider:
)

# To configure access delegation, provide client credentials
from keycardai.mcp.server.auth import ClientSecret
from keycardai.fastmcp import ClientSecret

auth_provider = AuthProvider(
zone_id="abc1234",
Expand Down
2 changes: 1 addition & 1 deletion packages/fastmcp/tests/integration/test_grant_decorator.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,9 @@
from keycardai.fastmcp import (
AccessContext,
AuthProvider,
MissingContextError,
ResourceAccessError,
)
from keycardai.mcp.server.exceptions import MissingContextError
from keycardai.oauth.types.models import TokenExchangeRequest, TokenResponse


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,9 @@
AccessContext,
AuthProvider,
GrantDependency,
MissingContextError,
)
from keycardai.fastmcp.testing import override_access_context
from keycardai.mcp.server.exceptions import MissingContextError
from keycardai.oauth.types.models import TokenResponse


Expand Down
2 changes: 1 addition & 1 deletion packages/fastmcp/tests/test_access_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
import pytest

from keycardai.fastmcp.provider import AccessContext
from keycardai.mcp.server.exceptions import ResourceAccessError
from keycardai.oauth.server.exceptions import ResourceAccessError
from keycardai.oauth.types.models import TokenResponse


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@
import pytest

from keycardai.fastmcp.provider import AuthProvider, ClientFactory
from keycardai.mcp.server.auth import ClientSecret, EKSWorkloadIdentity, WebIdentity
from keycardai.mcp.server.exceptions import AuthProviderConfigurationError
from keycardai.oauth.server import ClientSecret, EKSWorkloadIdentity, WebIdentity
from keycardai.oauth.server.exceptions import AuthProviderConfigurationError


@pytest.fixture
Expand Down
Loading
Loading