fastmcp 2.11.2__py3-none-any.whl → 2.12.0rc1__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.0rc1.dist-info}/METADATA +3 -2
- fastmcp-2.12.0rc1.dist-info/RECORD +129 -0
- fastmcp-2.11.2.dist-info/RECORD +0 -108
- {fastmcp-2.11.2.dist-info → fastmcp-2.12.0rc1.dist-info}/WHEEL +0 -0
- {fastmcp-2.11.2.dist-info → fastmcp-2.12.0rc1.dist-info}/entry_points.txt +0 -0
- {fastmcp-2.11.2.dist-info → fastmcp-2.12.0rc1.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
"""Azure (Microsoft Entra) OAuth provider for FastMCP.
|
|
2
|
+
|
|
3
|
+
This provider implements Azure/Microsoft Entra ID OAuth authentication
|
|
4
|
+
using the OAuth Proxy pattern for non-DCR OAuth flows.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import httpx
|
|
10
|
+
from pydantic import SecretStr, field_validator
|
|
11
|
+
from pydantic_settings import BaseSettings, SettingsConfigDict
|
|
12
|
+
|
|
13
|
+
from fastmcp.server.auth import AccessToken, TokenVerifier
|
|
14
|
+
from fastmcp.server.auth.oauth_proxy import OAuthProxy
|
|
15
|
+
from fastmcp.server.auth.registry import register_provider
|
|
16
|
+
from fastmcp.utilities.auth import parse_scopes
|
|
17
|
+
from fastmcp.utilities.logging import get_logger
|
|
18
|
+
from fastmcp.utilities.types import NotSet, NotSetT
|
|
19
|
+
|
|
20
|
+
logger = get_logger(__name__)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class AzureProviderSettings(BaseSettings):
|
|
24
|
+
"""Settings for Azure OAuth provider."""
|
|
25
|
+
|
|
26
|
+
model_config = SettingsConfigDict(
|
|
27
|
+
env_prefix="FASTMCP_SERVER_AUTH_AZURE_",
|
|
28
|
+
env_file=".env",
|
|
29
|
+
extra="ignore",
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
client_id: str | None = None
|
|
33
|
+
client_secret: SecretStr | None = None
|
|
34
|
+
tenant_id: str | None = None
|
|
35
|
+
base_url: str | None = None
|
|
36
|
+
redirect_path: str | None = None
|
|
37
|
+
required_scopes: list[str] | None = None
|
|
38
|
+
timeout_seconds: int | None = None
|
|
39
|
+
resource_server_url: str | None = None
|
|
40
|
+
allowed_client_redirect_uris: list[str] | None = None
|
|
41
|
+
|
|
42
|
+
@field_validator("required_scopes", mode="before")
|
|
43
|
+
@classmethod
|
|
44
|
+
def _parse_scopes(cls, v):
|
|
45
|
+
return parse_scopes(v)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class AzureTokenVerifier(TokenVerifier):
|
|
49
|
+
"""Token verifier for Azure OAuth tokens.
|
|
50
|
+
|
|
51
|
+
Azure tokens are JWTs, but we verify them by calling the Microsoft Graph API
|
|
52
|
+
to get user information and validate the token.
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
def __init__(
|
|
56
|
+
self,
|
|
57
|
+
*,
|
|
58
|
+
required_scopes: list[str] | None = None,
|
|
59
|
+
timeout_seconds: int = 10,
|
|
60
|
+
):
|
|
61
|
+
"""Initialize the Azure token verifier.
|
|
62
|
+
|
|
63
|
+
Args:
|
|
64
|
+
required_scopes: Required OAuth scopes
|
|
65
|
+
timeout_seconds: HTTP request timeout
|
|
66
|
+
"""
|
|
67
|
+
super().__init__(required_scopes=required_scopes)
|
|
68
|
+
self.timeout_seconds = timeout_seconds
|
|
69
|
+
|
|
70
|
+
async def verify_token(self, token: str) -> AccessToken | None:
|
|
71
|
+
"""Verify Azure OAuth token by calling Microsoft Graph API."""
|
|
72
|
+
try:
|
|
73
|
+
async with httpx.AsyncClient(timeout=self.timeout_seconds) as client:
|
|
74
|
+
# Use Microsoft Graph API to validate token and get user info
|
|
75
|
+
response = await client.get(
|
|
76
|
+
"https://graph.microsoft.com/v1.0/me",
|
|
77
|
+
headers={
|
|
78
|
+
"Authorization": f"Bearer {token}",
|
|
79
|
+
"User-Agent": "FastMCP-Azure-OAuth",
|
|
80
|
+
},
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
if response.status_code != 200:
|
|
84
|
+
logger.debug(
|
|
85
|
+
"Azure token verification failed: %d - %s",
|
|
86
|
+
response.status_code,
|
|
87
|
+
response.text[:200],
|
|
88
|
+
)
|
|
89
|
+
return None
|
|
90
|
+
|
|
91
|
+
user_data = response.json()
|
|
92
|
+
|
|
93
|
+
# Create AccessToken with Azure user info
|
|
94
|
+
return AccessToken(
|
|
95
|
+
token=token,
|
|
96
|
+
client_id=str(user_data.get("id", "unknown")),
|
|
97
|
+
scopes=self.required_scopes or [],
|
|
98
|
+
expires_at=None,
|
|
99
|
+
claims={
|
|
100
|
+
"sub": user_data.get("id"),
|
|
101
|
+
"email": user_data.get("mail")
|
|
102
|
+
or user_data.get("userPrincipalName"),
|
|
103
|
+
"name": user_data.get("displayName"),
|
|
104
|
+
"given_name": user_data.get("givenName"),
|
|
105
|
+
"family_name": user_data.get("surname"),
|
|
106
|
+
"job_title": user_data.get("jobTitle"),
|
|
107
|
+
"office_location": user_data.get("officeLocation"),
|
|
108
|
+
},
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
except httpx.RequestError as e:
|
|
112
|
+
logger.debug("Failed to verify Azure token: %s", e)
|
|
113
|
+
return None
|
|
114
|
+
except Exception as e:
|
|
115
|
+
logger.debug("Azure token verification error: %s", e)
|
|
116
|
+
return None
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
@register_provider("AZURE")
|
|
120
|
+
class AzureProvider(OAuthProxy):
|
|
121
|
+
"""Azure (Microsoft Entra) OAuth provider for FastMCP.
|
|
122
|
+
|
|
123
|
+
This provider implements Azure/Microsoft Entra ID authentication using the
|
|
124
|
+
OAuth Proxy pattern. It supports both organizational accounts and personal
|
|
125
|
+
Microsoft accounts depending on the tenant configuration.
|
|
126
|
+
|
|
127
|
+
Features:
|
|
128
|
+
- Transparent OAuth proxy to Azure/Microsoft identity platform
|
|
129
|
+
- Automatic token validation via Microsoft Graph API
|
|
130
|
+
- User information extraction
|
|
131
|
+
- Support for different tenant configurations (common, organizations, consumers)
|
|
132
|
+
|
|
133
|
+
Setup Requirements:
|
|
134
|
+
1. Register an application in Azure Portal (portal.azure.com)
|
|
135
|
+
2. Configure redirect URI as: http://localhost:8000/auth/callback
|
|
136
|
+
3. Note your Application (client) ID and create a client secret
|
|
137
|
+
4. Optionally note your Directory (tenant) ID for single-tenant apps
|
|
138
|
+
|
|
139
|
+
Example:
|
|
140
|
+
```python
|
|
141
|
+
from fastmcp import FastMCP
|
|
142
|
+
from fastmcp.server.auth.providers.azure import AzureProvider
|
|
143
|
+
|
|
144
|
+
auth = AzureProvider(
|
|
145
|
+
client_id="your-client-id",
|
|
146
|
+
client_secret="your-client-secret",
|
|
147
|
+
tenant_id="your-tenant-id", # Required: your Azure tenant ID from Azure Portal
|
|
148
|
+
base_url="http://localhost:8000"
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
mcp = FastMCP("My App", auth=auth)
|
|
152
|
+
```
|
|
153
|
+
"""
|
|
154
|
+
|
|
155
|
+
def __init__(
|
|
156
|
+
self,
|
|
157
|
+
*,
|
|
158
|
+
client_id: str | NotSetT = NotSet,
|
|
159
|
+
client_secret: str | NotSetT = NotSet,
|
|
160
|
+
tenant_id: str | NotSetT = NotSet,
|
|
161
|
+
base_url: str | NotSetT = NotSet,
|
|
162
|
+
redirect_path: str | NotSetT = NotSet,
|
|
163
|
+
required_scopes: list[str] | None | NotSetT = NotSet,
|
|
164
|
+
timeout_seconds: int | NotSetT = NotSet,
|
|
165
|
+
resource_server_url: str | NotSetT = NotSet,
|
|
166
|
+
allowed_client_redirect_uris: list[str] | NotSetT = NotSet,
|
|
167
|
+
):
|
|
168
|
+
"""Initialize Azure OAuth provider.
|
|
169
|
+
|
|
170
|
+
Args:
|
|
171
|
+
client_id: Azure application (client) ID
|
|
172
|
+
client_secret: Azure client secret
|
|
173
|
+
tenant_id: Azure tenant ID (your specific tenant ID, "organizations", or "consumers")
|
|
174
|
+
base_url: Public URL of your FastMCP server (for OAuth callbacks)
|
|
175
|
+
redirect_path: Redirect path configured in Azure (defaults to "/auth/callback")
|
|
176
|
+
required_scopes: Required scopes (defaults to ["User.Read", "email", "openid", "profile"])
|
|
177
|
+
timeout_seconds: HTTP request timeout for Azure API calls
|
|
178
|
+
resource_server_url: Path of the FastMCP server (defaults to base_url). If your MCP endpoint is at
|
|
179
|
+
a different path like {base_url}/mcp, specify it here for RFC 8707 compliance.
|
|
180
|
+
allowed_client_redirect_uris: List of allowed redirect URI patterns for MCP clients.
|
|
181
|
+
If None (default), all URIs are allowed. If empty list, no URIs are allowed.
|
|
182
|
+
"""
|
|
183
|
+
settings = AzureProviderSettings.model_validate(
|
|
184
|
+
{
|
|
185
|
+
k: v
|
|
186
|
+
for k, v in {
|
|
187
|
+
"client_id": client_id,
|
|
188
|
+
"client_secret": client_secret,
|
|
189
|
+
"tenant_id": tenant_id,
|
|
190
|
+
"base_url": base_url,
|
|
191
|
+
"redirect_path": redirect_path,
|
|
192
|
+
"required_scopes": required_scopes,
|
|
193
|
+
"timeout_seconds": timeout_seconds,
|
|
194
|
+
"resource_server_url": resource_server_url,
|
|
195
|
+
"allowed_client_redirect_uris": allowed_client_redirect_uris,
|
|
196
|
+
}.items()
|
|
197
|
+
if v is not NotSet
|
|
198
|
+
}
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
# Validate required settings
|
|
202
|
+
if not settings.client_id:
|
|
203
|
+
raise ValueError(
|
|
204
|
+
"client_id is required - set via parameter or FASTMCP_SERVER_AUTH_AZURE_CLIENT_ID"
|
|
205
|
+
)
|
|
206
|
+
if not settings.client_secret:
|
|
207
|
+
raise ValueError(
|
|
208
|
+
"client_secret is required - set via parameter or FASTMCP_SERVER_AUTH_AZURE_CLIENT_SECRET"
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
# Validate tenant_id is provided
|
|
212
|
+
if not settings.tenant_id:
|
|
213
|
+
raise ValueError(
|
|
214
|
+
"tenant_id is required - set via parameter or FASTMCP_SERVER_AUTH_AZURE_TENANT_ID. "
|
|
215
|
+
"Use your Azure tenant ID (found in Azure Portal), 'organizations', or 'consumers'"
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
# Apply defaults
|
|
219
|
+
tenant_id_final = settings.tenant_id
|
|
220
|
+
base_url_final = settings.base_url or "http://localhost:8000"
|
|
221
|
+
redirect_path_final = settings.redirect_path or "/auth/callback"
|
|
222
|
+
timeout_seconds_final = settings.timeout_seconds or 10
|
|
223
|
+
# Default scopes for Azure - User.Read gives us access to user info via Graph API
|
|
224
|
+
scopes_final = settings.required_scopes or [
|
|
225
|
+
"User.Read",
|
|
226
|
+
"email",
|
|
227
|
+
"openid",
|
|
228
|
+
"profile",
|
|
229
|
+
]
|
|
230
|
+
resource_server_url_final = settings.resource_server_url or base_url_final
|
|
231
|
+
allowed_client_redirect_uris_final = settings.allowed_client_redirect_uris
|
|
232
|
+
|
|
233
|
+
# Extract secret string from SecretStr
|
|
234
|
+
client_secret_str = (
|
|
235
|
+
settings.client_secret.get_secret_value() if settings.client_secret else ""
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
# Create Azure token verifier
|
|
239
|
+
token_verifier = AzureTokenVerifier(
|
|
240
|
+
required_scopes=scopes_final,
|
|
241
|
+
timeout_seconds=timeout_seconds_final,
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
# Build Azure OAuth endpoints with tenant
|
|
245
|
+
authorization_endpoint = (
|
|
246
|
+
f"https://login.microsoftonline.com/{tenant_id_final}/oauth2/v2.0/authorize"
|
|
247
|
+
)
|
|
248
|
+
token_endpoint = (
|
|
249
|
+
f"https://login.microsoftonline.com/{tenant_id_final}/oauth2/v2.0/token"
|
|
250
|
+
)
|
|
251
|
+
|
|
252
|
+
# Initialize OAuth proxy with Azure endpoints
|
|
253
|
+
super().__init__(
|
|
254
|
+
upstream_authorization_endpoint=authorization_endpoint,
|
|
255
|
+
upstream_token_endpoint=token_endpoint,
|
|
256
|
+
upstream_client_id=settings.client_id,
|
|
257
|
+
upstream_client_secret=client_secret_str,
|
|
258
|
+
token_verifier=token_verifier,
|
|
259
|
+
base_url=base_url_final,
|
|
260
|
+
redirect_path=redirect_path_final,
|
|
261
|
+
issuer_url=base_url_final,
|
|
262
|
+
allowed_client_redirect_uris=allowed_client_redirect_uris_final,
|
|
263
|
+
resource_server_url=resource_server_url_final,
|
|
264
|
+
)
|
|
265
|
+
|
|
266
|
+
logger.info(
|
|
267
|
+
"Initialized Azure OAuth provider for client %s with tenant %s",
|
|
268
|
+
settings.client_id,
|
|
269
|
+
tenant_id_final,
|
|
270
|
+
)
|
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
"""GitHub OAuth provider for FastMCP.
|
|
2
|
+
|
|
3
|
+
This module provides a complete GitHub OAuth integration that's ready to use
|
|
4
|
+
with just a client ID and client secret. It handles all the complexity of
|
|
5
|
+
GitHub's OAuth flow, token validation, and user management.
|
|
6
|
+
|
|
7
|
+
Example:
|
|
8
|
+
```python
|
|
9
|
+
from fastmcp import FastMCP
|
|
10
|
+
from fastmcp.server.auth.providers.github import GitHubProvider
|
|
11
|
+
|
|
12
|
+
# Simple GitHub OAuth protection
|
|
13
|
+
auth = GitHubProvider(
|
|
14
|
+
client_id="your-github-client-id",
|
|
15
|
+
client_secret="your-github-client-secret"
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
mcp = FastMCP("My Protected Server", auth=auth)
|
|
19
|
+
```
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
import httpx
|
|
25
|
+
from pydantic import AnyHttpUrl, SecretStr, field_validator
|
|
26
|
+
from pydantic_settings import BaseSettings, SettingsConfigDict
|
|
27
|
+
|
|
28
|
+
from fastmcp.server.auth import TokenVerifier
|
|
29
|
+
from fastmcp.server.auth.auth import AccessToken
|
|
30
|
+
from fastmcp.server.auth.oauth_proxy import OAuthProxy
|
|
31
|
+
from fastmcp.server.auth.registry import register_provider
|
|
32
|
+
from fastmcp.utilities.auth import parse_scopes
|
|
33
|
+
from fastmcp.utilities.logging import get_logger
|
|
34
|
+
from fastmcp.utilities.types import NotSet, NotSetT
|
|
35
|
+
|
|
36
|
+
logger = get_logger(__name__)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class GitHubProviderSettings(BaseSettings):
|
|
40
|
+
"""Settings for GitHub OAuth provider."""
|
|
41
|
+
|
|
42
|
+
model_config = SettingsConfigDict(
|
|
43
|
+
env_prefix="FASTMCP_SERVER_AUTH_GITHUB_",
|
|
44
|
+
env_file=".env",
|
|
45
|
+
extra="ignore",
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
client_id: str | None = None
|
|
49
|
+
client_secret: SecretStr | None = None
|
|
50
|
+
base_url: AnyHttpUrl | str | None = None
|
|
51
|
+
redirect_path: str | None = None
|
|
52
|
+
required_scopes: list[str] | None = None
|
|
53
|
+
timeout_seconds: int | None = None
|
|
54
|
+
resource_server_url: AnyHttpUrl | str | None = None
|
|
55
|
+
allowed_client_redirect_uris: list[str] | None = None
|
|
56
|
+
|
|
57
|
+
@field_validator("required_scopes", mode="before")
|
|
58
|
+
@classmethod
|
|
59
|
+
def _parse_scopes(cls, v):
|
|
60
|
+
return parse_scopes(v)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class GitHubTokenVerifier(TokenVerifier):
|
|
64
|
+
"""Token verifier for GitHub OAuth tokens.
|
|
65
|
+
|
|
66
|
+
GitHub OAuth tokens are opaque (not JWTs), so we verify them
|
|
67
|
+
by calling GitHub's API to check if they're valid and get user info.
|
|
68
|
+
"""
|
|
69
|
+
|
|
70
|
+
def __init__(
|
|
71
|
+
self,
|
|
72
|
+
*,
|
|
73
|
+
required_scopes: list[str] | None = None,
|
|
74
|
+
timeout_seconds: int = 10,
|
|
75
|
+
):
|
|
76
|
+
"""Initialize the GitHub token verifier.
|
|
77
|
+
|
|
78
|
+
Args:
|
|
79
|
+
required_scopes: Required OAuth scopes (e.g., ['user:email'])
|
|
80
|
+
timeout_seconds: HTTP request timeout
|
|
81
|
+
"""
|
|
82
|
+
super().__init__(required_scopes=required_scopes)
|
|
83
|
+
self.timeout_seconds = timeout_seconds
|
|
84
|
+
|
|
85
|
+
async def verify_token(self, token: str) -> AccessToken | None:
|
|
86
|
+
"""Verify GitHub OAuth token by calling GitHub API."""
|
|
87
|
+
try:
|
|
88
|
+
async with httpx.AsyncClient(timeout=self.timeout_seconds) as client:
|
|
89
|
+
# Get token info from GitHub API
|
|
90
|
+
response = await client.get(
|
|
91
|
+
"https://api.github.com/user",
|
|
92
|
+
headers={
|
|
93
|
+
"Authorization": f"Bearer {token}",
|
|
94
|
+
"Accept": "application/vnd.github.v3+json",
|
|
95
|
+
"User-Agent": "FastMCP-GitHub-OAuth",
|
|
96
|
+
},
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
if response.status_code != 200:
|
|
100
|
+
logger.debug(
|
|
101
|
+
"GitHub token verification failed: %d - %s",
|
|
102
|
+
response.status_code,
|
|
103
|
+
response.text[:200],
|
|
104
|
+
)
|
|
105
|
+
return None
|
|
106
|
+
|
|
107
|
+
user_data = response.json()
|
|
108
|
+
|
|
109
|
+
# Get token scopes from GitHub API
|
|
110
|
+
# GitHub includes scopes in the X-OAuth-Scopes header
|
|
111
|
+
scopes_response = await client.get(
|
|
112
|
+
"https://api.github.com/user/repos", # Any authenticated endpoint
|
|
113
|
+
headers={
|
|
114
|
+
"Authorization": f"Bearer {token}",
|
|
115
|
+
"Accept": "application/vnd.github.v3+json",
|
|
116
|
+
"User-Agent": "FastMCP-GitHub-OAuth",
|
|
117
|
+
},
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
# Extract scopes from X-OAuth-Scopes header if available
|
|
121
|
+
oauth_scopes_header = scopes_response.headers.get("x-oauth-scopes", "")
|
|
122
|
+
token_scopes = [
|
|
123
|
+
scope.strip()
|
|
124
|
+
for scope in oauth_scopes_header.split(",")
|
|
125
|
+
if scope.strip()
|
|
126
|
+
]
|
|
127
|
+
|
|
128
|
+
# If no scopes in header, assume basic scopes based on successful user API call
|
|
129
|
+
if not token_scopes:
|
|
130
|
+
token_scopes = ["user"] # Basic scope if we can access user info
|
|
131
|
+
|
|
132
|
+
# Check required scopes
|
|
133
|
+
if self.required_scopes:
|
|
134
|
+
token_scopes_set = set(token_scopes)
|
|
135
|
+
required_scopes_set = set(self.required_scopes)
|
|
136
|
+
if not required_scopes_set.issubset(token_scopes_set):
|
|
137
|
+
logger.debug(
|
|
138
|
+
"GitHub token missing required scopes. Has %d, needs %d",
|
|
139
|
+
len(token_scopes_set),
|
|
140
|
+
len(required_scopes_set),
|
|
141
|
+
)
|
|
142
|
+
return None
|
|
143
|
+
|
|
144
|
+
# Create AccessToken with GitHub user info
|
|
145
|
+
return AccessToken(
|
|
146
|
+
token=token,
|
|
147
|
+
client_id=str(user_data.get("id", "unknown")), # Use GitHub user ID
|
|
148
|
+
scopes=token_scopes,
|
|
149
|
+
expires_at=None, # GitHub tokens don't typically expire
|
|
150
|
+
claims={
|
|
151
|
+
"sub": str(user_data["id"]),
|
|
152
|
+
"login": user_data.get("login"),
|
|
153
|
+
"name": user_data.get("name"),
|
|
154
|
+
"email": user_data.get("email"),
|
|
155
|
+
"avatar_url": user_data.get("avatar_url"),
|
|
156
|
+
"github_user_data": user_data,
|
|
157
|
+
},
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
except httpx.RequestError as e:
|
|
161
|
+
logger.debug("Failed to verify GitHub token: %s", e)
|
|
162
|
+
return None
|
|
163
|
+
except Exception as e:
|
|
164
|
+
logger.debug("GitHub token verification error: %s", e)
|
|
165
|
+
return None
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
@register_provider("GitHub")
|
|
169
|
+
class GitHubProvider(OAuthProxy):
|
|
170
|
+
"""Complete GitHub OAuth provider for FastMCP.
|
|
171
|
+
|
|
172
|
+
This provider makes it trivial to add GitHub OAuth protection to any
|
|
173
|
+
FastMCP server. Just provide your GitHub OAuth app credentials and
|
|
174
|
+
a base URL, and you're ready to go.
|
|
175
|
+
|
|
176
|
+
Features:
|
|
177
|
+
- Transparent OAuth proxy to GitHub
|
|
178
|
+
- Automatic token validation via GitHub API
|
|
179
|
+
- User information extraction
|
|
180
|
+
- Minimal configuration required
|
|
181
|
+
|
|
182
|
+
Example:
|
|
183
|
+
```python
|
|
184
|
+
from fastmcp import FastMCP
|
|
185
|
+
from fastmcp.server.auth.providers.github import GitHubProvider
|
|
186
|
+
|
|
187
|
+
auth = GitHubProvider(
|
|
188
|
+
client_id="Ov23li...",
|
|
189
|
+
client_secret="abc123...",
|
|
190
|
+
base_url="https://my-server.com" # Optional, defaults to http://localhost:8000
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
mcp = FastMCP("My App", auth=auth)
|
|
194
|
+
```
|
|
195
|
+
"""
|
|
196
|
+
|
|
197
|
+
def __init__(
|
|
198
|
+
self,
|
|
199
|
+
*,
|
|
200
|
+
client_id: str | NotSetT = NotSet,
|
|
201
|
+
client_secret: str | NotSetT = NotSet,
|
|
202
|
+
base_url: AnyHttpUrl | str | NotSetT = NotSet,
|
|
203
|
+
redirect_path: str | NotSetT = NotSet,
|
|
204
|
+
required_scopes: list[str] | NotSetT = NotSet,
|
|
205
|
+
timeout_seconds: int | NotSetT = NotSet,
|
|
206
|
+
resource_server_url: AnyHttpUrl | str | NotSetT = NotSet,
|
|
207
|
+
allowed_client_redirect_uris: list[str] | NotSetT = NotSet,
|
|
208
|
+
):
|
|
209
|
+
"""Initialize GitHub OAuth provider.
|
|
210
|
+
|
|
211
|
+
Args:
|
|
212
|
+
client_id: GitHub OAuth app client ID (e.g., "Ov23li...")
|
|
213
|
+
client_secret: GitHub OAuth app client secret
|
|
214
|
+
base_url: Public URL of your FastMCP server (for OAuth callbacks)
|
|
215
|
+
redirect_path: Redirect path configured in GitHub OAuth app (defaults to "/auth/callback")
|
|
216
|
+
required_scopes: Required GitHub scopes (defaults to ["user"])
|
|
217
|
+
timeout_seconds: HTTP request timeout for GitHub API calls
|
|
218
|
+
resource_server_url: Path of the FastMCP server (defaults to base_url). If your MCP endpoint is at
|
|
219
|
+
a different path like {base_url}/mcp, specify it here for RFC 8707 compliance.
|
|
220
|
+
allowed_client_redirect_uris: List of allowed redirect URI patterns for MCP clients.
|
|
221
|
+
If None (default), all URIs are allowed. If empty list, no URIs are allowed.
|
|
222
|
+
"""
|
|
223
|
+
settings = GitHubProviderSettings.model_validate(
|
|
224
|
+
{
|
|
225
|
+
k: v
|
|
226
|
+
for k, v in {
|
|
227
|
+
"client_id": client_id,
|
|
228
|
+
"client_secret": client_secret,
|
|
229
|
+
"base_url": base_url,
|
|
230
|
+
"redirect_path": redirect_path,
|
|
231
|
+
"required_scopes": required_scopes,
|
|
232
|
+
"timeout_seconds": timeout_seconds,
|
|
233
|
+
"resource_server_url": resource_server_url,
|
|
234
|
+
"allowed_client_redirect_uris": allowed_client_redirect_uris,
|
|
235
|
+
}.items()
|
|
236
|
+
if v is not NotSet
|
|
237
|
+
}
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
# Validate required settings
|
|
241
|
+
if not settings.client_id:
|
|
242
|
+
raise ValueError(
|
|
243
|
+
"client_id is required - set via parameter or FASTMCP_SERVER_AUTH_GITHUB_CLIENT_ID"
|
|
244
|
+
)
|
|
245
|
+
if not settings.client_secret:
|
|
246
|
+
raise ValueError(
|
|
247
|
+
"client_secret is required - set via parameter or FASTMCP_SERVER_AUTH_GITHUB_CLIENT_SECRET"
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
# Apply defaults
|
|
251
|
+
base_url_final = settings.base_url or "http://localhost:8000"
|
|
252
|
+
redirect_path_final = settings.redirect_path or "/auth/callback"
|
|
253
|
+
timeout_seconds_final = settings.timeout_seconds or 10
|
|
254
|
+
required_scopes_final = settings.required_scopes or ["user"]
|
|
255
|
+
resource_server_url_final = settings.resource_server_url or base_url_final
|
|
256
|
+
allowed_client_redirect_uris_final = settings.allowed_client_redirect_uris
|
|
257
|
+
|
|
258
|
+
# Create GitHub token verifier
|
|
259
|
+
token_verifier = GitHubTokenVerifier(
|
|
260
|
+
required_scopes=required_scopes_final,
|
|
261
|
+
timeout_seconds=timeout_seconds_final,
|
|
262
|
+
)
|
|
263
|
+
|
|
264
|
+
# Extract secret string from SecretStr
|
|
265
|
+
client_secret_str = (
|
|
266
|
+
settings.client_secret.get_secret_value() if settings.client_secret else ""
|
|
267
|
+
)
|
|
268
|
+
|
|
269
|
+
# Initialize OAuth proxy with GitHub endpoints
|
|
270
|
+
super().__init__(
|
|
271
|
+
upstream_authorization_endpoint="https://github.com/login/oauth/authorize",
|
|
272
|
+
upstream_token_endpoint="https://github.com/login/oauth/access_token",
|
|
273
|
+
upstream_client_id=settings.client_id,
|
|
274
|
+
upstream_client_secret=client_secret_str,
|
|
275
|
+
token_verifier=token_verifier,
|
|
276
|
+
base_url=base_url_final,
|
|
277
|
+
redirect_path=redirect_path_final,
|
|
278
|
+
issuer_url=base_url_final, # We act as the issuer for client registration
|
|
279
|
+
allowed_client_redirect_uris=allowed_client_redirect_uris_final,
|
|
280
|
+
resource_server_url=resource_server_url_final,
|
|
281
|
+
)
|
|
282
|
+
|
|
283
|
+
logger.info(
|
|
284
|
+
"Initialized GitHub OAuth provider for client %s with scopes: %s",
|
|
285
|
+
settings.client_id,
|
|
286
|
+
required_scopes_final,
|
|
287
|
+
)
|