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.
Files changed (77) hide show
  1. fastmcp/__init__.py +5 -4
  2. fastmcp/cli/claude.py +22 -18
  3. fastmcp/cli/cli.py +472 -136
  4. fastmcp/cli/install/claude_code.py +37 -40
  5. fastmcp/cli/install/claude_desktop.py +37 -42
  6. fastmcp/cli/install/cursor.py +148 -38
  7. fastmcp/cli/install/mcp_json.py +38 -43
  8. fastmcp/cli/install/shared.py +64 -7
  9. fastmcp/cli/run.py +122 -215
  10. fastmcp/client/auth/oauth.py +69 -13
  11. fastmcp/client/client.py +46 -9
  12. fastmcp/client/logging.py +25 -1
  13. fastmcp/client/oauth_callback.py +91 -91
  14. fastmcp/client/sampling.py +12 -4
  15. fastmcp/client/transports.py +143 -67
  16. fastmcp/experimental/sampling/__init__.py +0 -0
  17. fastmcp/experimental/sampling/handlers/__init__.py +3 -0
  18. fastmcp/experimental/sampling/handlers/base.py +21 -0
  19. fastmcp/experimental/sampling/handlers/openai.py +163 -0
  20. fastmcp/experimental/server/openapi/routing.py +1 -3
  21. fastmcp/experimental/server/openapi/server.py +10 -25
  22. fastmcp/experimental/utilities/openapi/__init__.py +2 -2
  23. fastmcp/experimental/utilities/openapi/formatters.py +34 -0
  24. fastmcp/experimental/utilities/openapi/models.py +5 -2
  25. fastmcp/experimental/utilities/openapi/parser.py +252 -70
  26. fastmcp/experimental/utilities/openapi/schemas.py +135 -106
  27. fastmcp/mcp_config.py +40 -20
  28. fastmcp/prompts/prompt_manager.py +4 -2
  29. fastmcp/resources/resource_manager.py +16 -6
  30. fastmcp/server/auth/__init__.py +11 -1
  31. fastmcp/server/auth/auth.py +19 -2
  32. fastmcp/server/auth/oauth_proxy.py +1047 -0
  33. fastmcp/server/auth/providers/azure.py +270 -0
  34. fastmcp/server/auth/providers/github.py +287 -0
  35. fastmcp/server/auth/providers/google.py +305 -0
  36. fastmcp/server/auth/providers/jwt.py +27 -16
  37. fastmcp/server/auth/providers/workos.py +256 -2
  38. fastmcp/server/auth/redirect_validation.py +65 -0
  39. fastmcp/server/auth/registry.py +1 -1
  40. fastmcp/server/context.py +91 -41
  41. fastmcp/server/dependencies.py +32 -2
  42. fastmcp/server/elicitation.py +60 -1
  43. fastmcp/server/http.py +44 -37
  44. fastmcp/server/middleware/logging.py +66 -28
  45. fastmcp/server/proxy.py +2 -0
  46. fastmcp/server/sampling/handler.py +19 -0
  47. fastmcp/server/server.py +85 -20
  48. fastmcp/settings.py +18 -3
  49. fastmcp/tools/tool.py +23 -10
  50. fastmcp/tools/tool_manager.py +5 -1
  51. fastmcp/tools/tool_transform.py +75 -32
  52. fastmcp/utilities/auth.py +34 -0
  53. fastmcp/utilities/cli.py +148 -15
  54. fastmcp/utilities/components.py +21 -5
  55. fastmcp/utilities/inspect.py +166 -37
  56. fastmcp/utilities/json_schema_type.py +4 -2
  57. fastmcp/utilities/logging.py +4 -1
  58. fastmcp/utilities/mcp_config.py +47 -18
  59. fastmcp/utilities/mcp_server_config/__init__.py +25 -0
  60. fastmcp/utilities/mcp_server_config/v1/__init__.py +0 -0
  61. fastmcp/utilities/mcp_server_config/v1/environments/__init__.py +6 -0
  62. fastmcp/utilities/mcp_server_config/v1/environments/base.py +30 -0
  63. fastmcp/utilities/mcp_server_config/v1/environments/uv.py +306 -0
  64. fastmcp/utilities/mcp_server_config/v1/mcp_server_config.py +446 -0
  65. fastmcp/utilities/mcp_server_config/v1/schema.json +361 -0
  66. fastmcp/utilities/mcp_server_config/v1/sources/__init__.py +0 -0
  67. fastmcp/utilities/mcp_server_config/v1/sources/base.py +30 -0
  68. fastmcp/utilities/mcp_server_config/v1/sources/filesystem.py +216 -0
  69. fastmcp/utilities/openapi.py +4 -4
  70. fastmcp/utilities/tests.py +7 -2
  71. fastmcp/utilities/types.py +15 -2
  72. {fastmcp-2.11.2.dist-info → fastmcp-2.12.0.dist-info}/METADATA +3 -2
  73. fastmcp-2.12.0.dist-info/RECORD +129 -0
  74. fastmcp-2.11.2.dist-info/RECORD +0 -108
  75. {fastmcp-2.11.2.dist-info → fastmcp-2.12.0.dist-info}/WHEEL +0 -0
  76. {fastmcp-2.11.2.dist-info → fastmcp-2.12.0.dist-info}/entry_points.txt +0 -0
  77. {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 mcp.server.auth.provider import AccessToken
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 using public key or JWKS.
168
+ JWT token verifier supporting both asymmetric (RSA/ECDSA) and symmetric (HMAC) algorithms.
166
169
 
167
- This verifier validates JWT tokens signed by an external issuer. It's ideal for
168
- scenarios where you have a centralized identity provider (like Auth0, Okta, or
169
- your own OAuth server) that issues JWTs, and your FastMCP server acts as a
170
- resource server validating those tokens.
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 want asymmetric key verification (public/private key pairs)
175
- - You need JWKS support for automatic key rotation
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 for verification
195
- jwks_uri: URI to fetch JSON Web Key Set
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 (default: RS256)
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):