fastmcp 2.11.2__py3-none-any.whl → 2.12.0__py3-none-any.whl
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.
- fastmcp/__init__.py +5 -4
- fastmcp/cli/claude.py +22 -18
- fastmcp/cli/cli.py +472 -136
- fastmcp/cli/install/claude_code.py +37 -40
- fastmcp/cli/install/claude_desktop.py +37 -42
- fastmcp/cli/install/cursor.py +148 -38
- fastmcp/cli/install/mcp_json.py +38 -43
- fastmcp/cli/install/shared.py +64 -7
- fastmcp/cli/run.py +122 -215
- fastmcp/client/auth/oauth.py +69 -13
- fastmcp/client/client.py +46 -9
- fastmcp/client/logging.py +25 -1
- fastmcp/client/oauth_callback.py +91 -91
- fastmcp/client/sampling.py +12 -4
- fastmcp/client/transports.py +143 -67
- fastmcp/experimental/sampling/__init__.py +0 -0
- fastmcp/experimental/sampling/handlers/__init__.py +3 -0
- fastmcp/experimental/sampling/handlers/base.py +21 -0
- fastmcp/experimental/sampling/handlers/openai.py +163 -0
- fastmcp/experimental/server/openapi/routing.py +1 -3
- fastmcp/experimental/server/openapi/server.py +10 -25
- fastmcp/experimental/utilities/openapi/__init__.py +2 -2
- fastmcp/experimental/utilities/openapi/formatters.py +34 -0
- fastmcp/experimental/utilities/openapi/models.py +5 -2
- fastmcp/experimental/utilities/openapi/parser.py +252 -70
- fastmcp/experimental/utilities/openapi/schemas.py +135 -106
- fastmcp/mcp_config.py +40 -20
- fastmcp/prompts/prompt_manager.py +4 -2
- fastmcp/resources/resource_manager.py +16 -6
- fastmcp/server/auth/__init__.py +11 -1
- fastmcp/server/auth/auth.py +19 -2
- fastmcp/server/auth/oauth_proxy.py +1047 -0
- fastmcp/server/auth/providers/azure.py +270 -0
- fastmcp/server/auth/providers/github.py +287 -0
- fastmcp/server/auth/providers/google.py +305 -0
- fastmcp/server/auth/providers/jwt.py +27 -16
- fastmcp/server/auth/providers/workos.py +256 -2
- fastmcp/server/auth/redirect_validation.py +65 -0
- fastmcp/server/auth/registry.py +1 -1
- fastmcp/server/context.py +91 -41
- fastmcp/server/dependencies.py +32 -2
- fastmcp/server/elicitation.py +60 -1
- fastmcp/server/http.py +44 -37
- fastmcp/server/middleware/logging.py +66 -28
- fastmcp/server/proxy.py +2 -0
- fastmcp/server/sampling/handler.py +19 -0
- fastmcp/server/server.py +85 -20
- fastmcp/settings.py +18 -3
- fastmcp/tools/tool.py +23 -10
- fastmcp/tools/tool_manager.py +5 -1
- fastmcp/tools/tool_transform.py +75 -32
- fastmcp/utilities/auth.py +34 -0
- fastmcp/utilities/cli.py +148 -15
- fastmcp/utilities/components.py +21 -5
- fastmcp/utilities/inspect.py +166 -37
- fastmcp/utilities/json_schema_type.py +4 -2
- fastmcp/utilities/logging.py +4 -1
- fastmcp/utilities/mcp_config.py +47 -18
- fastmcp/utilities/mcp_server_config/__init__.py +25 -0
- fastmcp/utilities/mcp_server_config/v1/__init__.py +0 -0
- fastmcp/utilities/mcp_server_config/v1/environments/__init__.py +6 -0
- fastmcp/utilities/mcp_server_config/v1/environments/base.py +30 -0
- fastmcp/utilities/mcp_server_config/v1/environments/uv.py +306 -0
- fastmcp/utilities/mcp_server_config/v1/mcp_server_config.py +446 -0
- fastmcp/utilities/mcp_server_config/v1/schema.json +361 -0
- fastmcp/utilities/mcp_server_config/v1/sources/__init__.py +0 -0
- fastmcp/utilities/mcp_server_config/v1/sources/base.py +30 -0
- fastmcp/utilities/mcp_server_config/v1/sources/filesystem.py +216 -0
- fastmcp/utilities/openapi.py +4 -4
- fastmcp/utilities/tests.py +7 -2
- fastmcp/utilities/types.py +15 -2
- {fastmcp-2.11.2.dist-info → fastmcp-2.12.0.dist-info}/METADATA +3 -2
- fastmcp-2.12.0.dist-info/RECORD +129 -0
- fastmcp-2.11.2.dist-info/RECORD +0 -108
- {fastmcp-2.11.2.dist-info → fastmcp-2.12.0.dist-info}/WHEEL +0 -0
- {fastmcp-2.11.2.dist-info → fastmcp-2.12.0.dist-info}/entry_points.txt +0 -0
- {fastmcp-2.11.2.dist-info → fastmcp-2.12.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
"""Google OAuth provider for FastMCP.
|
|
2
|
+
|
|
3
|
+
This module provides a complete Google OAuth integration that's ready to use
|
|
4
|
+
with just a client ID and client secret. It handles all the complexity of
|
|
5
|
+
Google's OAuth flow, token validation, and user management.
|
|
6
|
+
|
|
7
|
+
Example:
|
|
8
|
+
```python
|
|
9
|
+
from fastmcp import FastMCP
|
|
10
|
+
from fastmcp.server.auth.providers.google import GoogleProvider
|
|
11
|
+
|
|
12
|
+
# Simple Google OAuth protection
|
|
13
|
+
auth = GoogleProvider(
|
|
14
|
+
client_id="your-google-client-id.apps.googleusercontent.com",
|
|
15
|
+
client_secret="your-google-client-secret"
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
mcp = FastMCP("My Protected Server", auth=auth)
|
|
19
|
+
```
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
import time
|
|
25
|
+
|
|
26
|
+
import httpx
|
|
27
|
+
from pydantic import AnyHttpUrl, SecretStr, field_validator
|
|
28
|
+
from pydantic_settings import BaseSettings, SettingsConfigDict
|
|
29
|
+
|
|
30
|
+
from fastmcp.server.auth import TokenVerifier
|
|
31
|
+
from fastmcp.server.auth.auth import AccessToken
|
|
32
|
+
from fastmcp.server.auth.oauth_proxy import OAuthProxy
|
|
33
|
+
from fastmcp.server.auth.registry import register_provider
|
|
34
|
+
from fastmcp.utilities.auth import parse_scopes
|
|
35
|
+
from fastmcp.utilities.logging import get_logger
|
|
36
|
+
from fastmcp.utilities.types import NotSet, NotSetT
|
|
37
|
+
|
|
38
|
+
logger = get_logger(__name__)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class GoogleProviderSettings(BaseSettings):
|
|
42
|
+
"""Settings for Google OAuth provider."""
|
|
43
|
+
|
|
44
|
+
model_config = SettingsConfigDict(
|
|
45
|
+
env_prefix="FASTMCP_SERVER_AUTH_GOOGLE_",
|
|
46
|
+
env_file=".env",
|
|
47
|
+
extra="ignore",
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
client_id: str | None = None
|
|
51
|
+
client_secret: SecretStr | None = None
|
|
52
|
+
base_url: AnyHttpUrl | str | None = None
|
|
53
|
+
redirect_path: str | None = None
|
|
54
|
+
required_scopes: list[str] | None = None
|
|
55
|
+
timeout_seconds: int | None = None
|
|
56
|
+
resource_server_url: AnyHttpUrl | str | None = None
|
|
57
|
+
allowed_client_redirect_uris: list[str] | None = None
|
|
58
|
+
|
|
59
|
+
@field_validator("required_scopes", mode="before")
|
|
60
|
+
@classmethod
|
|
61
|
+
def _parse_scopes(cls, v):
|
|
62
|
+
return parse_scopes(v)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class GoogleTokenVerifier(TokenVerifier):
|
|
66
|
+
"""Token verifier for Google OAuth tokens.
|
|
67
|
+
|
|
68
|
+
Google OAuth tokens are opaque (not JWTs), so we verify them
|
|
69
|
+
by calling Google's tokeninfo API to check if they're valid and get user info.
|
|
70
|
+
"""
|
|
71
|
+
|
|
72
|
+
def __init__(
|
|
73
|
+
self,
|
|
74
|
+
*,
|
|
75
|
+
required_scopes: list[str] | None = None,
|
|
76
|
+
timeout_seconds: int = 10,
|
|
77
|
+
):
|
|
78
|
+
"""Initialize the Google token verifier.
|
|
79
|
+
|
|
80
|
+
Args:
|
|
81
|
+
required_scopes: Required OAuth scopes (e.g., ['openid', 'email'])
|
|
82
|
+
timeout_seconds: HTTP request timeout
|
|
83
|
+
"""
|
|
84
|
+
super().__init__(required_scopes=required_scopes)
|
|
85
|
+
self.timeout_seconds = timeout_seconds
|
|
86
|
+
|
|
87
|
+
async def verify_token(self, token: str) -> AccessToken | None:
|
|
88
|
+
"""Verify Google OAuth token by calling Google's tokeninfo API."""
|
|
89
|
+
try:
|
|
90
|
+
async with httpx.AsyncClient(timeout=self.timeout_seconds) as client:
|
|
91
|
+
# Use Google's tokeninfo endpoint to validate the token
|
|
92
|
+
response = await client.get(
|
|
93
|
+
"https://www.googleapis.com/oauth2/v1/tokeninfo",
|
|
94
|
+
params={"access_token": token},
|
|
95
|
+
headers={"User-Agent": "FastMCP-Google-OAuth"},
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
if response.status_code != 200:
|
|
99
|
+
logger.debug(
|
|
100
|
+
"Google token verification failed: %d",
|
|
101
|
+
response.status_code,
|
|
102
|
+
)
|
|
103
|
+
return None
|
|
104
|
+
|
|
105
|
+
token_info = response.json()
|
|
106
|
+
|
|
107
|
+
# Check if token is expired
|
|
108
|
+
expires_in = token_info.get("expires_in")
|
|
109
|
+
if expires_in and int(expires_in) <= 0:
|
|
110
|
+
logger.debug("Google token has expired")
|
|
111
|
+
return None
|
|
112
|
+
|
|
113
|
+
# Extract scopes from token info
|
|
114
|
+
scope_string = token_info.get("scope", "")
|
|
115
|
+
token_scopes = [
|
|
116
|
+
scope.strip() for scope in scope_string.split(" ") if scope.strip()
|
|
117
|
+
]
|
|
118
|
+
|
|
119
|
+
# Check required scopes
|
|
120
|
+
if self.required_scopes:
|
|
121
|
+
token_scopes_set = set(token_scopes)
|
|
122
|
+
required_scopes_set = set(self.required_scopes)
|
|
123
|
+
if not required_scopes_set.issubset(token_scopes_set):
|
|
124
|
+
logger.debug(
|
|
125
|
+
"Google token missing required scopes. Has %d, needs %d",
|
|
126
|
+
len(token_scopes_set),
|
|
127
|
+
len(required_scopes_set),
|
|
128
|
+
)
|
|
129
|
+
return None
|
|
130
|
+
|
|
131
|
+
# Get additional user info if we have the right scopes
|
|
132
|
+
user_data = {}
|
|
133
|
+
if "openid" in token_scopes or "profile" in token_scopes:
|
|
134
|
+
try:
|
|
135
|
+
userinfo_response = await client.get(
|
|
136
|
+
"https://www.googleapis.com/oauth2/v2/userinfo",
|
|
137
|
+
headers={
|
|
138
|
+
"Authorization": f"Bearer {token}",
|
|
139
|
+
"User-Agent": "FastMCP-Google-OAuth",
|
|
140
|
+
},
|
|
141
|
+
)
|
|
142
|
+
if userinfo_response.status_code == 200:
|
|
143
|
+
user_data = userinfo_response.json()
|
|
144
|
+
except Exception as e:
|
|
145
|
+
logger.debug("Failed to fetch Google user info: %s", e)
|
|
146
|
+
|
|
147
|
+
# Calculate expiration time
|
|
148
|
+
expires_at = None
|
|
149
|
+
if expires_in:
|
|
150
|
+
expires_at = int(time.time() + int(expires_in))
|
|
151
|
+
|
|
152
|
+
# Create AccessToken with Google user info
|
|
153
|
+
access_token = AccessToken(
|
|
154
|
+
token=token,
|
|
155
|
+
client_id=token_info.get(
|
|
156
|
+
"audience", "unknown"
|
|
157
|
+
), # Use audience as client_id
|
|
158
|
+
scopes=token_scopes,
|
|
159
|
+
expires_at=expires_at,
|
|
160
|
+
claims={
|
|
161
|
+
"sub": user_data.get("id")
|
|
162
|
+
or token_info.get("user_id", "unknown"),
|
|
163
|
+
"email": user_data.get("email"),
|
|
164
|
+
"name": user_data.get("name"),
|
|
165
|
+
"picture": user_data.get("picture"),
|
|
166
|
+
"given_name": user_data.get("given_name"),
|
|
167
|
+
"family_name": user_data.get("family_name"),
|
|
168
|
+
"locale": user_data.get("locale"),
|
|
169
|
+
"google_user_data": user_data,
|
|
170
|
+
"google_token_info": token_info,
|
|
171
|
+
},
|
|
172
|
+
)
|
|
173
|
+
logger.debug("Google token verified successfully")
|
|
174
|
+
return access_token
|
|
175
|
+
|
|
176
|
+
except httpx.RequestError as e:
|
|
177
|
+
logger.debug("Failed to verify Google token: %s", e)
|
|
178
|
+
return None
|
|
179
|
+
except Exception as e:
|
|
180
|
+
logger.debug("Google token verification error: %s", e)
|
|
181
|
+
return None
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
@register_provider("Google")
|
|
185
|
+
class GoogleProvider(OAuthProxy):
|
|
186
|
+
"""Complete Google OAuth provider for FastMCP.
|
|
187
|
+
|
|
188
|
+
This provider makes it trivial to add Google OAuth protection to any
|
|
189
|
+
FastMCP server. Just provide your Google OAuth app credentials and
|
|
190
|
+
a base URL, and you're ready to go.
|
|
191
|
+
|
|
192
|
+
Features:
|
|
193
|
+
- Transparent OAuth proxy to Google
|
|
194
|
+
- Automatic token validation via Google's tokeninfo API
|
|
195
|
+
- User information extraction from Google APIs
|
|
196
|
+
- Minimal configuration required
|
|
197
|
+
|
|
198
|
+
Example:
|
|
199
|
+
```python
|
|
200
|
+
from fastmcp import FastMCP
|
|
201
|
+
from fastmcp.server.auth.providers.google import GoogleProvider
|
|
202
|
+
|
|
203
|
+
auth = GoogleProvider(
|
|
204
|
+
client_id="123456789.apps.googleusercontent.com",
|
|
205
|
+
client_secret="GOCSPX-abc123...",
|
|
206
|
+
base_url="https://my-server.com" # Optional, defaults to http://localhost:8000
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
mcp = FastMCP("My App", auth=auth)
|
|
210
|
+
```
|
|
211
|
+
"""
|
|
212
|
+
|
|
213
|
+
def __init__(
|
|
214
|
+
self,
|
|
215
|
+
*,
|
|
216
|
+
client_id: str | NotSetT = NotSet,
|
|
217
|
+
client_secret: str | NotSetT = NotSet,
|
|
218
|
+
base_url: AnyHttpUrl | str | NotSetT = NotSet,
|
|
219
|
+
redirect_path: str | NotSetT = NotSet,
|
|
220
|
+
required_scopes: list[str] | NotSetT = NotSet,
|
|
221
|
+
timeout_seconds: int | NotSetT = NotSet,
|
|
222
|
+
resource_server_url: AnyHttpUrl | str | NotSetT = NotSet,
|
|
223
|
+
allowed_client_redirect_uris: list[str] | NotSetT = NotSet,
|
|
224
|
+
):
|
|
225
|
+
"""Initialize Google OAuth provider.
|
|
226
|
+
|
|
227
|
+
Args:
|
|
228
|
+
client_id: Google OAuth client ID (e.g., "123456789.apps.googleusercontent.com")
|
|
229
|
+
client_secret: Google OAuth client secret (e.g., "GOCSPX-abc123...")
|
|
230
|
+
base_url: Public URL of your FastMCP server (for OAuth callbacks)
|
|
231
|
+
redirect_path: Redirect path configured in Google OAuth app (defaults to "/auth/callback")
|
|
232
|
+
required_scopes: Required Google scopes (defaults to ["openid"]). Common scopes include:
|
|
233
|
+
- "openid" for OpenID Connect (default)
|
|
234
|
+
- "https://www.googleapis.com/auth/userinfo.email" for email access
|
|
235
|
+
- "https://www.googleapis.com/auth/userinfo.profile" for profile info
|
|
236
|
+
timeout_seconds: HTTP request timeout for Google API calls
|
|
237
|
+
allowed_client_redirect_uris: List of allowed redirect URI patterns for MCP clients.
|
|
238
|
+
If None (default), all URIs are allowed. If empty list, no URIs are allowed.
|
|
239
|
+
"""
|
|
240
|
+
settings = GoogleProviderSettings.model_validate(
|
|
241
|
+
{
|
|
242
|
+
k: v
|
|
243
|
+
for k, v in {
|
|
244
|
+
"client_id": client_id,
|
|
245
|
+
"client_secret": client_secret,
|
|
246
|
+
"base_url": base_url,
|
|
247
|
+
"redirect_path": redirect_path,
|
|
248
|
+
"required_scopes": required_scopes,
|
|
249
|
+
"timeout_seconds": timeout_seconds,
|
|
250
|
+
"resource_server_url": resource_server_url,
|
|
251
|
+
"allowed_client_redirect_uris": allowed_client_redirect_uris,
|
|
252
|
+
}.items()
|
|
253
|
+
if v is not NotSet
|
|
254
|
+
}
|
|
255
|
+
)
|
|
256
|
+
|
|
257
|
+
# Validate required settings
|
|
258
|
+
if not settings.client_id:
|
|
259
|
+
raise ValueError(
|
|
260
|
+
"client_id is required - set via parameter or FASTMCP_SERVER_AUTH_GOOGLE_CLIENT_ID"
|
|
261
|
+
)
|
|
262
|
+
if not settings.client_secret:
|
|
263
|
+
raise ValueError(
|
|
264
|
+
"client_secret is required - set via parameter or FASTMCP_SERVER_AUTH_GOOGLE_CLIENT_SECRET"
|
|
265
|
+
)
|
|
266
|
+
|
|
267
|
+
# Apply defaults
|
|
268
|
+
base_url_final = settings.base_url or "http://localhost:8000"
|
|
269
|
+
redirect_path_final = settings.redirect_path or "/auth/callback"
|
|
270
|
+
timeout_seconds_final = settings.timeout_seconds or 10
|
|
271
|
+
# Google requires at least one scope - openid is the minimal OIDC scope
|
|
272
|
+
required_scopes_final = settings.required_scopes or ["openid"]
|
|
273
|
+
resource_server_url_final = settings.resource_server_url or base_url_final
|
|
274
|
+
allowed_client_redirect_uris_final = settings.allowed_client_redirect_uris
|
|
275
|
+
|
|
276
|
+
# Create Google token verifier
|
|
277
|
+
token_verifier = GoogleTokenVerifier(
|
|
278
|
+
required_scopes=required_scopes_final,
|
|
279
|
+
timeout_seconds=timeout_seconds_final,
|
|
280
|
+
)
|
|
281
|
+
|
|
282
|
+
# Extract secret string from SecretStr
|
|
283
|
+
client_secret_str = (
|
|
284
|
+
settings.client_secret.get_secret_value() if settings.client_secret else ""
|
|
285
|
+
)
|
|
286
|
+
|
|
287
|
+
# Initialize OAuth proxy with Google endpoints
|
|
288
|
+
super().__init__(
|
|
289
|
+
upstream_authorization_endpoint="https://accounts.google.com/o/oauth2/v2/auth",
|
|
290
|
+
upstream_token_endpoint="https://oauth2.googleapis.com/token",
|
|
291
|
+
upstream_client_id=settings.client_id,
|
|
292
|
+
upstream_client_secret=client_secret_str,
|
|
293
|
+
token_verifier=token_verifier,
|
|
294
|
+
base_url=base_url_final,
|
|
295
|
+
redirect_path=redirect_path_final,
|
|
296
|
+
issuer_url=base_url_final, # We act as the issuer for client registration
|
|
297
|
+
allowed_client_redirect_uris=allowed_client_redirect_uris_final,
|
|
298
|
+
resource_server_url=resource_server_url_final,
|
|
299
|
+
)
|
|
300
|
+
|
|
301
|
+
logger.info(
|
|
302
|
+
"Initialized Google OAuth provider for client %s with scopes: %s",
|
|
303
|
+
settings.client_id,
|
|
304
|
+
required_scopes_final,
|
|
305
|
+
)
|
|
@@ -11,13 +11,13 @@ from authlib.jose import JsonWebKey, JsonWebToken
|
|
|
11
11
|
from authlib.jose.errors import JoseError
|
|
12
12
|
from cryptography.hazmat.primitives import serialization
|
|
13
13
|
from cryptography.hazmat.primitives.asymmetric import rsa
|
|
14
|
-
from
|
|
15
|
-
from pydantic import AnyHttpUrl, SecretStr
|
|
14
|
+
from pydantic import AnyHttpUrl, SecretStr, field_validator
|
|
16
15
|
from pydantic_settings import BaseSettings, SettingsConfigDict
|
|
17
16
|
from typing_extensions import TypedDict
|
|
18
17
|
|
|
19
|
-
from fastmcp.server.auth import TokenVerifier
|
|
18
|
+
from fastmcp.server.auth import AccessToken, TokenVerifier
|
|
20
19
|
from fastmcp.server.auth.registry import register_provider
|
|
20
|
+
from fastmcp.utilities.auth import parse_scopes
|
|
21
21
|
from fastmcp.utilities.logging import get_logger
|
|
22
22
|
from fastmcp.utilities.types import NotSet, NotSetT
|
|
23
23
|
|
|
@@ -108,8 +108,6 @@ class RSAKeyPair:
|
|
|
108
108
|
additional_claims: Any additional claims to include
|
|
109
109
|
kid: Key ID to include in header
|
|
110
110
|
"""
|
|
111
|
-
import time
|
|
112
|
-
|
|
113
111
|
# Create header
|
|
114
112
|
header = {"alg": "RS256"}
|
|
115
113
|
if kid:
|
|
@@ -158,21 +156,29 @@ class JWTVerifierSettings(BaseSettings):
|
|
|
158
156
|
required_scopes: list[str] | None = None
|
|
159
157
|
resource_server_url: AnyHttpUrl | str | None = None
|
|
160
158
|
|
|
159
|
+
@field_validator("required_scopes", mode="before")
|
|
160
|
+
@classmethod
|
|
161
|
+
def _parse_scopes(cls, v):
|
|
162
|
+
return parse_scopes(v)
|
|
163
|
+
|
|
161
164
|
|
|
162
165
|
@register_provider("JWT")
|
|
163
166
|
class JWTVerifier(TokenVerifier):
|
|
164
167
|
"""
|
|
165
|
-
JWT token verifier
|
|
168
|
+
JWT token verifier supporting both asymmetric (RSA/ECDSA) and symmetric (HMAC) algorithms.
|
|
166
169
|
|
|
167
|
-
This verifier validates JWT tokens
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
170
|
+
This verifier validates JWT tokens using various signing algorithms:
|
|
171
|
+
- **Asymmetric algorithms** (RS256/384/512, ES256/384/512, PS256/384/512):
|
|
172
|
+
Uses public/private key pairs. Ideal for external clients and services where
|
|
173
|
+
only the authorization server has the private key.
|
|
174
|
+
- **Symmetric algorithms** (HS256/384/512): Uses a shared secret for both
|
|
175
|
+
signing and verification. Perfect for internal microservices and trusted
|
|
176
|
+
environments where the secret can be securely shared.
|
|
171
177
|
|
|
172
178
|
Use this when:
|
|
173
|
-
- You have JWT tokens issued by an external service
|
|
174
|
-
- You
|
|
175
|
-
- You
|
|
179
|
+
- You have JWT tokens issued by an external service (asymmetric)
|
|
180
|
+
- You need JWKS support for automatic key rotation (asymmetric)
|
|
181
|
+
- You have internal microservices sharing a secret key (symmetric)
|
|
176
182
|
- Your tokens contain standard OAuth scopes and claims
|
|
177
183
|
"""
|
|
178
184
|
|
|
@@ -191,11 +197,14 @@ class JWTVerifier(TokenVerifier):
|
|
|
191
197
|
Initialize the JWT token verifier.
|
|
192
198
|
|
|
193
199
|
Args:
|
|
194
|
-
public_key: PEM-encoded public key
|
|
195
|
-
|
|
200
|
+
public_key: For asymmetric algorithms (RS256, ES256, etc.): PEM-encoded public key.
|
|
201
|
+
For symmetric algorithms (HS256, HS384, HS512): The shared secret string.
|
|
202
|
+
jwks_uri: URI to fetch JSON Web Key Set (only for asymmetric algorithms)
|
|
196
203
|
issuer: Expected issuer claim
|
|
197
204
|
audience: Expected audience claim(s)
|
|
198
|
-
algorithm: JWT signing algorithm
|
|
205
|
+
algorithm: JWT signing algorithm. Supported algorithms:
|
|
206
|
+
- Asymmetric: RS256/384/512, ES256/384/512, PS256/384/512 (default: RS256)
|
|
207
|
+
- Symmetric: HS256, HS384, HS512
|
|
199
208
|
required_scopes: Required scopes for all tokens
|
|
200
209
|
resource_server_url: Resource server URL for TokenVerifier protocol
|
|
201
210
|
"""
|
|
@@ -448,6 +457,7 @@ class JWTVerifier(TokenVerifier):
|
|
|
448
457
|
client_id=str(client_id),
|
|
449
458
|
scopes=scopes,
|
|
450
459
|
expires_at=int(exp) if exp else None,
|
|
460
|
+
claims=claims,
|
|
451
461
|
)
|
|
452
462
|
|
|
453
463
|
except JoseError:
|
|
@@ -535,4 +545,5 @@ class StaticTokenVerifier(TokenVerifier):
|
|
|
535
545
|
client_id=token_data["client_id"],
|
|
536
546
|
scopes=scopes,
|
|
537
547
|
expires_at=expires_at,
|
|
548
|
+
claims=token_data,
|
|
538
549
|
)
|
|
@@ -1,20 +1,269 @@
|
|
|
1
|
+
"""WorkOS authentication providers for FastMCP.
|
|
2
|
+
|
|
3
|
+
This module provides two WorkOS authentication strategies:
|
|
4
|
+
|
|
5
|
+
1. WorkOSProvider - OAuth proxy for WorkOS Connect applications (non-DCR)
|
|
6
|
+
2. AuthKitProvider - DCR-compliant provider for WorkOS AuthKit
|
|
7
|
+
|
|
8
|
+
Choose based on your WorkOS setup and authentication requirements.
|
|
9
|
+
"""
|
|
10
|
+
|
|
1
11
|
from __future__ import annotations
|
|
2
12
|
|
|
3
13
|
import httpx
|
|
4
|
-
from pydantic import AnyHttpUrl
|
|
14
|
+
from pydantic import AnyHttpUrl, SecretStr, field_validator
|
|
5
15
|
from pydantic_settings import BaseSettings, SettingsConfigDict
|
|
6
16
|
from starlette.responses import JSONResponse
|
|
7
17
|
from starlette.routing import Route
|
|
8
18
|
|
|
9
|
-
from fastmcp.server.auth import RemoteAuthProvider, TokenVerifier
|
|
19
|
+
from fastmcp.server.auth import AccessToken, RemoteAuthProvider, TokenVerifier
|
|
20
|
+
from fastmcp.server.auth.oauth_proxy import OAuthProxy
|
|
10
21
|
from fastmcp.server.auth.providers.jwt import JWTVerifier
|
|
11
22
|
from fastmcp.server.auth.registry import register_provider
|
|
23
|
+
from fastmcp.utilities.auth import parse_scopes
|
|
12
24
|
from fastmcp.utilities.logging import get_logger
|
|
13
25
|
from fastmcp.utilities.types import NotSet, NotSetT
|
|
14
26
|
|
|
15
27
|
logger = get_logger(__name__)
|
|
16
28
|
|
|
17
29
|
|
|
30
|
+
class WorkOSProviderSettings(BaseSettings):
|
|
31
|
+
"""Settings for WorkOS OAuth provider."""
|
|
32
|
+
|
|
33
|
+
model_config = SettingsConfigDict(
|
|
34
|
+
env_prefix="FASTMCP_SERVER_AUTH_WORKOS_",
|
|
35
|
+
env_file=".env",
|
|
36
|
+
extra="ignore",
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
client_id: str | None = None
|
|
40
|
+
client_secret: SecretStr | None = None
|
|
41
|
+
authkit_domain: str | None = None # e.g., "https://your-app.authkit.app"
|
|
42
|
+
base_url: AnyHttpUrl | str | None = None
|
|
43
|
+
redirect_path: str | None = None
|
|
44
|
+
required_scopes: list[str] | None = None
|
|
45
|
+
timeout_seconds: int | None = None
|
|
46
|
+
resource_server_url: AnyHttpUrl | str | None = None
|
|
47
|
+
allowed_client_redirect_uris: list[str] | None = None
|
|
48
|
+
|
|
49
|
+
@field_validator("required_scopes", mode="before")
|
|
50
|
+
@classmethod
|
|
51
|
+
def _parse_scopes(cls, v):
|
|
52
|
+
return parse_scopes(v)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class WorkOSTokenVerifier(TokenVerifier):
|
|
56
|
+
"""Token verifier for WorkOS OAuth tokens.
|
|
57
|
+
|
|
58
|
+
WorkOS AuthKit tokens are opaque, so we verify them by calling
|
|
59
|
+
the /oauth2/userinfo endpoint to check validity and get user info.
|
|
60
|
+
"""
|
|
61
|
+
|
|
62
|
+
def __init__(
|
|
63
|
+
self,
|
|
64
|
+
*,
|
|
65
|
+
authkit_domain: str,
|
|
66
|
+
required_scopes: list[str] | None = None,
|
|
67
|
+
timeout_seconds: int = 10,
|
|
68
|
+
):
|
|
69
|
+
"""Initialize the WorkOS token verifier.
|
|
70
|
+
|
|
71
|
+
Args:
|
|
72
|
+
authkit_domain: WorkOS AuthKit domain (e.g., "https://your-app.authkit.app")
|
|
73
|
+
required_scopes: Required OAuth scopes
|
|
74
|
+
timeout_seconds: HTTP request timeout
|
|
75
|
+
"""
|
|
76
|
+
super().__init__(required_scopes=required_scopes)
|
|
77
|
+
self.authkit_domain = authkit_domain.rstrip("/")
|
|
78
|
+
self.timeout_seconds = timeout_seconds
|
|
79
|
+
|
|
80
|
+
async def verify_token(self, token: str) -> AccessToken | None:
|
|
81
|
+
"""Verify WorkOS OAuth token by calling userinfo endpoint."""
|
|
82
|
+
try:
|
|
83
|
+
async with httpx.AsyncClient(timeout=self.timeout_seconds) as client:
|
|
84
|
+
# Use WorkOS AuthKit userinfo endpoint to validate token
|
|
85
|
+
response = await client.get(
|
|
86
|
+
f"{self.authkit_domain}/oauth2/userinfo",
|
|
87
|
+
headers={
|
|
88
|
+
"Authorization": f"Bearer {token}",
|
|
89
|
+
"User-Agent": "FastMCP-WorkOS-OAuth",
|
|
90
|
+
},
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
if response.status_code != 200:
|
|
94
|
+
logger.debug(
|
|
95
|
+
"WorkOS token verification failed: %d - %s",
|
|
96
|
+
response.status_code,
|
|
97
|
+
response.text[:200],
|
|
98
|
+
)
|
|
99
|
+
return None
|
|
100
|
+
|
|
101
|
+
user_data = response.json()
|
|
102
|
+
|
|
103
|
+
# Create AccessToken with WorkOS user info
|
|
104
|
+
return AccessToken(
|
|
105
|
+
token=token,
|
|
106
|
+
client_id=str(user_data.get("sub", "unknown")),
|
|
107
|
+
scopes=self.required_scopes or [],
|
|
108
|
+
expires_at=None, # Will be set from token introspection if needed
|
|
109
|
+
claims={
|
|
110
|
+
"sub": user_data.get("sub"),
|
|
111
|
+
"email": user_data.get("email"),
|
|
112
|
+
"email_verified": user_data.get("email_verified"),
|
|
113
|
+
"name": user_data.get("name"),
|
|
114
|
+
"given_name": user_data.get("given_name"),
|
|
115
|
+
"family_name": user_data.get("family_name"),
|
|
116
|
+
},
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
except httpx.RequestError as e:
|
|
120
|
+
logger.debug("Failed to verify WorkOS token: %s", e)
|
|
121
|
+
return None
|
|
122
|
+
except Exception as e:
|
|
123
|
+
logger.debug("WorkOS token verification error: %s", e)
|
|
124
|
+
return None
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
@register_provider("WORKOS")
|
|
128
|
+
class WorkOSProvider(OAuthProxy):
|
|
129
|
+
"""Complete WorkOS OAuth provider for FastMCP.
|
|
130
|
+
|
|
131
|
+
This provider implements WorkOS AuthKit OAuth using the OAuth Proxy pattern.
|
|
132
|
+
It provides OAuth2 authentication for users through WorkOS Connect applications.
|
|
133
|
+
|
|
134
|
+
Features:
|
|
135
|
+
- Transparent OAuth proxy to WorkOS AuthKit
|
|
136
|
+
- Automatic token validation via userinfo endpoint
|
|
137
|
+
- User information extraction from ID tokens
|
|
138
|
+
- Support for standard OAuth scopes (openid, profile, email)
|
|
139
|
+
|
|
140
|
+
Setup Requirements:
|
|
141
|
+
1. Create a WorkOS Connect application in your dashboard
|
|
142
|
+
2. Note your AuthKit domain (e.g., "https://your-app.authkit.app")
|
|
143
|
+
3. Configure redirect URI as: http://localhost:8000/auth/callback
|
|
144
|
+
4. Note your Client ID and Client Secret
|
|
145
|
+
|
|
146
|
+
Example:
|
|
147
|
+
```python
|
|
148
|
+
from fastmcp import FastMCP
|
|
149
|
+
from fastmcp.server.auth.providers.workos import WorkOSProvider
|
|
150
|
+
|
|
151
|
+
auth = WorkOSProvider(
|
|
152
|
+
client_id="client_123",
|
|
153
|
+
client_secret="sk_test_456",
|
|
154
|
+
authkit_domain="https://your-app.authkit.app",
|
|
155
|
+
base_url="http://localhost:8000"
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
mcp = FastMCP("My App", auth=auth)
|
|
159
|
+
```
|
|
160
|
+
"""
|
|
161
|
+
|
|
162
|
+
def __init__(
|
|
163
|
+
self,
|
|
164
|
+
*,
|
|
165
|
+
client_id: str | NotSetT = NotSet,
|
|
166
|
+
client_secret: str | NotSetT = NotSet,
|
|
167
|
+
authkit_domain: str | NotSetT = NotSet,
|
|
168
|
+
base_url: AnyHttpUrl | str | NotSetT = NotSet,
|
|
169
|
+
redirect_path: str | NotSetT = NotSet,
|
|
170
|
+
required_scopes: list[str] | None | NotSetT = NotSet,
|
|
171
|
+
timeout_seconds: int | NotSetT = NotSet,
|
|
172
|
+
resource_server_url: AnyHttpUrl | str | NotSetT = NotSet,
|
|
173
|
+
allowed_client_redirect_uris: list[str] | NotSetT = NotSet,
|
|
174
|
+
):
|
|
175
|
+
"""Initialize WorkOS OAuth provider.
|
|
176
|
+
|
|
177
|
+
Args:
|
|
178
|
+
client_id: WorkOS client ID
|
|
179
|
+
client_secret: WorkOS client secret
|
|
180
|
+
authkit_domain: Your WorkOS AuthKit domain (e.g., "https://your-app.authkit.app")
|
|
181
|
+
base_url: Public URL of your FastMCP server (for OAuth callbacks)
|
|
182
|
+
redirect_path: Redirect path configured in WorkOS (defaults to "/auth/callback")
|
|
183
|
+
required_scopes: Required OAuth scopes (no default)
|
|
184
|
+
timeout_seconds: HTTP request timeout for WorkOS API calls
|
|
185
|
+
resource_server_url: Path of the FastMCP server (defaults to base_url). If your MCP endpoint is at
|
|
186
|
+
a different path like {base_url}/mcp, specify it here for RFC 8707 compliance.
|
|
187
|
+
allowed_client_redirect_uris: List of allowed redirect URI patterns for MCP clients.
|
|
188
|
+
If None (default), all URIs are allowed. If empty list, no URIs are allowed.
|
|
189
|
+
"""
|
|
190
|
+
settings = WorkOSProviderSettings.model_validate(
|
|
191
|
+
{
|
|
192
|
+
k: v
|
|
193
|
+
for k, v in {
|
|
194
|
+
"client_id": client_id,
|
|
195
|
+
"client_secret": client_secret,
|
|
196
|
+
"authkit_domain": authkit_domain,
|
|
197
|
+
"base_url": base_url,
|
|
198
|
+
"redirect_path": redirect_path,
|
|
199
|
+
"required_scopes": required_scopes,
|
|
200
|
+
"timeout_seconds": timeout_seconds,
|
|
201
|
+
"resource_server_url": resource_server_url,
|
|
202
|
+
"allowed_client_redirect_uris": allowed_client_redirect_uris,
|
|
203
|
+
}.items()
|
|
204
|
+
if v is not NotSet
|
|
205
|
+
}
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
# Validate required settings
|
|
209
|
+
if not settings.client_id:
|
|
210
|
+
raise ValueError(
|
|
211
|
+
"client_id is required - set via parameter or FASTMCP_SERVER_AUTH_WORKOS_CLIENT_ID"
|
|
212
|
+
)
|
|
213
|
+
if not settings.client_secret:
|
|
214
|
+
raise ValueError(
|
|
215
|
+
"client_secret is required - set via parameter or FASTMCP_SERVER_AUTH_WORKOS_CLIENT_SECRET"
|
|
216
|
+
)
|
|
217
|
+
if not settings.authkit_domain:
|
|
218
|
+
raise ValueError(
|
|
219
|
+
"authkit_domain is required - set via parameter or FASTMCP_SERVER_AUTH_WORKOS_AUTHKIT_DOMAIN"
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
# Apply defaults and ensure authkit_domain is a full URL
|
|
223
|
+
authkit_domain_str = settings.authkit_domain
|
|
224
|
+
if not authkit_domain_str.startswith(("http://", "https://")):
|
|
225
|
+
authkit_domain_str = f"https://{authkit_domain_str}"
|
|
226
|
+
authkit_domain_final = authkit_domain_str.rstrip("/")
|
|
227
|
+
base_url_final = settings.base_url or "http://localhost:8000"
|
|
228
|
+
redirect_path_final = settings.redirect_path or "/auth/callback"
|
|
229
|
+
timeout_seconds_final = settings.timeout_seconds or 10
|
|
230
|
+
scopes_final = settings.required_scopes or []
|
|
231
|
+
resource_server_url_final = settings.resource_server_url or base_url_final
|
|
232
|
+
allowed_client_redirect_uris_final = settings.allowed_client_redirect_uris
|
|
233
|
+
|
|
234
|
+
# Extract secret string from SecretStr
|
|
235
|
+
client_secret_str = (
|
|
236
|
+
settings.client_secret.get_secret_value() if settings.client_secret else ""
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
# Create WorkOS token verifier
|
|
240
|
+
token_verifier = WorkOSTokenVerifier(
|
|
241
|
+
authkit_domain=authkit_domain_final,
|
|
242
|
+
required_scopes=scopes_final,
|
|
243
|
+
timeout_seconds=timeout_seconds_final,
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
# Initialize OAuth proxy with WorkOS AuthKit endpoints
|
|
247
|
+
super().__init__(
|
|
248
|
+
upstream_authorization_endpoint=f"{authkit_domain_final}/oauth2/authorize",
|
|
249
|
+
upstream_token_endpoint=f"{authkit_domain_final}/oauth2/token",
|
|
250
|
+
upstream_client_id=settings.client_id,
|
|
251
|
+
upstream_client_secret=client_secret_str,
|
|
252
|
+
token_verifier=token_verifier,
|
|
253
|
+
base_url=base_url_final,
|
|
254
|
+
redirect_path=redirect_path_final,
|
|
255
|
+
issuer_url=base_url_final,
|
|
256
|
+
allowed_client_redirect_uris=allowed_client_redirect_uris_final,
|
|
257
|
+
resource_server_url=resource_server_url_final,
|
|
258
|
+
)
|
|
259
|
+
|
|
260
|
+
logger.info(
|
|
261
|
+
"Initialized WorkOS OAuth provider for client %s with AuthKit domain %s",
|
|
262
|
+
settings.client_id,
|
|
263
|
+
authkit_domain_final,
|
|
264
|
+
)
|
|
265
|
+
|
|
266
|
+
|
|
18
267
|
class AuthKitProviderSettings(BaseSettings):
|
|
19
268
|
model_config = SettingsConfigDict(
|
|
20
269
|
env_prefix="FASTMCP_SERVER_AUTH_AUTHKITPROVIDER_",
|
|
@@ -26,6 +275,11 @@ class AuthKitProviderSettings(BaseSettings):
|
|
|
26
275
|
base_url: AnyHttpUrl
|
|
27
276
|
required_scopes: list[str] | None = None
|
|
28
277
|
|
|
278
|
+
@field_validator("required_scopes", mode="before")
|
|
279
|
+
@classmethod
|
|
280
|
+
def _parse_scopes(cls, v):
|
|
281
|
+
return parse_scopes(v)
|
|
282
|
+
|
|
29
283
|
|
|
30
284
|
@register_provider("AUTHKIT")
|
|
31
285
|
class AuthKitProvider(RemoteAuthProvider):
|