fastmcp 2.11.3__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 (69) 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/oauth_callback.py +91 -91
  13. fastmcp/client/sampling.py +12 -4
  14. fastmcp/client/transports.py +139 -64
  15. fastmcp/experimental/sampling/__init__.py +0 -0
  16. fastmcp/experimental/sampling/handlers/__init__.py +3 -0
  17. fastmcp/experimental/sampling/handlers/base.py +21 -0
  18. fastmcp/experimental/sampling/handlers/openai.py +163 -0
  19. fastmcp/experimental/server/openapi/routing.py +0 -2
  20. fastmcp/experimental/server/openapi/server.py +0 -2
  21. fastmcp/experimental/utilities/openapi/parser.py +5 -1
  22. fastmcp/mcp_config.py +40 -20
  23. fastmcp/prompts/prompt_manager.py +2 -0
  24. fastmcp/resources/resource_manager.py +4 -0
  25. fastmcp/server/auth/__init__.py +2 -0
  26. fastmcp/server/auth/auth.py +2 -1
  27. fastmcp/server/auth/oauth_proxy.py +1047 -0
  28. fastmcp/server/auth/providers/azure.py +270 -0
  29. fastmcp/server/auth/providers/github.py +287 -0
  30. fastmcp/server/auth/providers/google.py +305 -0
  31. fastmcp/server/auth/providers/jwt.py +24 -12
  32. fastmcp/server/auth/providers/workos.py +256 -2
  33. fastmcp/server/auth/redirect_validation.py +65 -0
  34. fastmcp/server/context.py +91 -41
  35. fastmcp/server/elicitation.py +60 -1
  36. fastmcp/server/http.py +3 -3
  37. fastmcp/server/middleware/logging.py +66 -28
  38. fastmcp/server/proxy.py +2 -0
  39. fastmcp/server/sampling/handler.py +19 -0
  40. fastmcp/server/server.py +76 -15
  41. fastmcp/settings.py +16 -1
  42. fastmcp/tools/tool.py +22 -9
  43. fastmcp/tools/tool_manager.py +2 -0
  44. fastmcp/tools/tool_transform.py +39 -10
  45. fastmcp/utilities/auth.py +34 -0
  46. fastmcp/utilities/cli.py +148 -15
  47. fastmcp/utilities/components.py +2 -1
  48. fastmcp/utilities/inspect.py +166 -37
  49. fastmcp/utilities/json_schema_type.py +4 -2
  50. fastmcp/utilities/logging.py +4 -1
  51. fastmcp/utilities/mcp_config.py +47 -18
  52. fastmcp/utilities/mcp_server_config/__init__.py +25 -0
  53. fastmcp/utilities/mcp_server_config/v1/__init__.py +0 -0
  54. fastmcp/utilities/mcp_server_config/v1/environments/__init__.py +6 -0
  55. fastmcp/utilities/mcp_server_config/v1/environments/base.py +30 -0
  56. fastmcp/utilities/mcp_server_config/v1/environments/uv.py +306 -0
  57. fastmcp/utilities/mcp_server_config/v1/mcp_server_config.py +446 -0
  58. fastmcp/utilities/mcp_server_config/v1/schema.json +361 -0
  59. fastmcp/utilities/mcp_server_config/v1/sources/__init__.py +0 -0
  60. fastmcp/utilities/mcp_server_config/v1/sources/base.py +30 -0
  61. fastmcp/utilities/mcp_server_config/v1/sources/filesystem.py +216 -0
  62. fastmcp/utilities/tests.py +7 -2
  63. fastmcp/utilities/types.py +15 -2
  64. {fastmcp-2.11.3.dist-info → fastmcp-2.12.0.dist-info}/METADATA +2 -1
  65. fastmcp-2.12.0.dist-info/RECORD +129 -0
  66. fastmcp-2.11.3.dist-info/RECORD +0 -108
  67. {fastmcp-2.11.3.dist-info → fastmcp-2.12.0.dist-info}/WHEEL +0 -0
  68. {fastmcp-2.11.3.dist-info → fastmcp-2.12.0.dist-info}/entry_points.txt +0 -0
  69. {fastmcp-2.11.3.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,12 +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 pydantic import AnyHttpUrl, SecretStr
14
+ from pydantic import AnyHttpUrl, SecretStr, field_validator
15
15
  from pydantic_settings import BaseSettings, SettingsConfigDict
16
16
  from typing_extensions import TypedDict
17
17
 
18
18
  from fastmcp.server.auth import AccessToken, TokenVerifier
19
19
  from fastmcp.server.auth.registry import register_provider
20
+ from fastmcp.utilities.auth import parse_scopes
20
21
  from fastmcp.utilities.logging import get_logger
21
22
  from fastmcp.utilities.types import NotSet, NotSetT
22
23
 
@@ -155,21 +156,29 @@ class JWTVerifierSettings(BaseSettings):
155
156
  required_scopes: list[str] | None = None
156
157
  resource_server_url: AnyHttpUrl | str | None = None
157
158
 
159
+ @field_validator("required_scopes", mode="before")
160
+ @classmethod
161
+ def _parse_scopes(cls, v):
162
+ return parse_scopes(v)
163
+
158
164
 
159
165
  @register_provider("JWT")
160
166
  class JWTVerifier(TokenVerifier):
161
167
  """
162
- JWT token verifier using public key or JWKS.
168
+ JWT token verifier supporting both asymmetric (RSA/ECDSA) and symmetric (HMAC) algorithms.
163
169
 
164
- This verifier validates JWT tokens signed by an external issuer. It's ideal for
165
- scenarios where you have a centralized identity provider (like Auth0, Okta, or
166
- your own OAuth server) that issues JWTs, and your FastMCP server acts as a
167
- 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.
168
177
 
169
178
  Use this when:
170
- - You have JWT tokens issued by an external service
171
- - You want asymmetric key verification (public/private key pairs)
172
- - 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)
173
182
  - Your tokens contain standard OAuth scopes and claims
174
183
  """
175
184
 
@@ -188,11 +197,14 @@ class JWTVerifier(TokenVerifier):
188
197
  Initialize the JWT token verifier.
189
198
 
190
199
  Args:
191
- public_key: PEM-encoded public key for verification
192
- 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)
193
203
  issuer: Expected issuer claim
194
204
  audience: Expected audience claim(s)
195
- 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
196
208
  required_scopes: Required scopes for all tokens
197
209
  resource_server_url: Resource server URL for TokenVerifier protocol
198
210
  """
@@ -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):