fastmcp 2.12.5__py3-none-any.whl → 2.13.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/cli/cli.py +7 -6
- fastmcp/cli/install/claude_code.py +6 -6
- fastmcp/cli/install/claude_desktop.py +3 -3
- fastmcp/cli/install/cursor.py +7 -7
- fastmcp/cli/install/gemini_cli.py +3 -3
- fastmcp/cli/install/mcp_json.py +3 -3
- fastmcp/cli/run.py +13 -8
- fastmcp/client/auth/oauth.py +100 -208
- fastmcp/client/client.py +11 -11
- fastmcp/client/logging.py +18 -14
- fastmcp/client/oauth_callback.py +85 -171
- fastmcp/client/transports.py +77 -22
- fastmcp/contrib/component_manager/component_service.py +6 -6
- fastmcp/contrib/mcp_mixin/README.md +32 -1
- fastmcp/contrib/mcp_mixin/mcp_mixin.py +14 -2
- fastmcp/experimental/utilities/openapi/json_schema_converter.py +4 -0
- fastmcp/experimental/utilities/openapi/parser.py +23 -3
- fastmcp/prompts/prompt.py +13 -6
- fastmcp/prompts/prompt_manager.py +16 -101
- fastmcp/resources/resource.py +13 -6
- fastmcp/resources/resource_manager.py +5 -164
- fastmcp/resources/template.py +107 -17
- fastmcp/resources/types.py +30 -24
- fastmcp/server/auth/auth.py +40 -32
- fastmcp/server/auth/handlers/authorize.py +324 -0
- fastmcp/server/auth/jwt_issuer.py +236 -0
- fastmcp/server/auth/middleware.py +96 -0
- fastmcp/server/auth/oauth_proxy.py +1256 -242
- fastmcp/server/auth/oidc_proxy.py +23 -6
- fastmcp/server/auth/providers/auth0.py +40 -21
- fastmcp/server/auth/providers/aws.py +29 -3
- fastmcp/server/auth/providers/azure.py +178 -127
- fastmcp/server/auth/providers/descope.py +4 -6
- fastmcp/server/auth/providers/github.py +29 -8
- fastmcp/server/auth/providers/google.py +30 -9
- fastmcp/server/auth/providers/introspection.py +281 -0
- fastmcp/server/auth/providers/jwt.py +8 -2
- fastmcp/server/auth/providers/scalekit.py +179 -0
- fastmcp/server/auth/providers/supabase.py +172 -0
- fastmcp/server/auth/providers/workos.py +32 -14
- fastmcp/server/context.py +122 -36
- fastmcp/server/http.py +58 -18
- fastmcp/server/low_level.py +121 -2
- fastmcp/server/middleware/caching.py +469 -0
- fastmcp/server/middleware/error_handling.py +6 -2
- fastmcp/server/middleware/logging.py +48 -37
- fastmcp/server/middleware/middleware.py +28 -15
- fastmcp/server/middleware/rate_limiting.py +3 -3
- fastmcp/server/middleware/tool_injection.py +116 -0
- fastmcp/server/proxy.py +6 -6
- fastmcp/server/server.py +683 -207
- fastmcp/settings.py +24 -10
- fastmcp/tools/tool.py +7 -3
- fastmcp/tools/tool_manager.py +30 -112
- fastmcp/tools/tool_transform.py +3 -3
- fastmcp/utilities/cli.py +62 -22
- fastmcp/utilities/components.py +5 -0
- fastmcp/utilities/inspect.py +77 -21
- fastmcp/utilities/logging.py +118 -8
- fastmcp/utilities/mcp_server_config/v1/environments/uv.py +6 -6
- fastmcp/utilities/mcp_server_config/v1/mcp_server_config.py +3 -3
- fastmcp/utilities/mcp_server_config/v1/schema.json +3 -0
- fastmcp/utilities/tests.py +87 -4
- fastmcp/utilities/types.py +1 -1
- fastmcp/utilities/ui.py +617 -0
- {fastmcp-2.12.5.dist-info → fastmcp-2.13.0.dist-info}/METADATA +10 -6
- {fastmcp-2.12.5.dist-info → fastmcp-2.13.0.dist-info}/RECORD +70 -63
- fastmcp/cli/claude.py +0 -135
- fastmcp/utilities/storage.py +0 -204
- {fastmcp-2.12.5.dist-info → fastmcp-2.13.0.dist-info}/WHEEL +0 -0
- {fastmcp-2.12.5.dist-info → fastmcp-2.13.0.dist-info}/entry_points.txt +0 -0
- {fastmcp-2.12.5.dist-info → fastmcp-2.13.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
"""Supabase authentication provider for FastMCP.
|
|
2
|
+
|
|
3
|
+
This module provides SupabaseProvider - a complete authentication solution that integrates
|
|
4
|
+
with Supabase Auth's JWT verification, supporting Dynamic Client Registration (DCR)
|
|
5
|
+
for seamless MCP client authentication.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import httpx
|
|
11
|
+
from pydantic import AnyHttpUrl, field_validator
|
|
12
|
+
from pydantic_settings import BaseSettings, SettingsConfigDict
|
|
13
|
+
from starlette.responses import JSONResponse
|
|
14
|
+
from starlette.routing import Route
|
|
15
|
+
|
|
16
|
+
from fastmcp.server.auth import RemoteAuthProvider, TokenVerifier
|
|
17
|
+
from fastmcp.server.auth.providers.jwt import JWTVerifier
|
|
18
|
+
from fastmcp.settings import ENV_FILE
|
|
19
|
+
from fastmcp.utilities.auth import parse_scopes
|
|
20
|
+
from fastmcp.utilities.logging import get_logger
|
|
21
|
+
from fastmcp.utilities.types import NotSet, NotSetT
|
|
22
|
+
|
|
23
|
+
logger = get_logger(__name__)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class SupabaseProviderSettings(BaseSettings):
|
|
27
|
+
model_config = SettingsConfigDict(
|
|
28
|
+
env_prefix="FASTMCP_SERVER_AUTH_SUPABASE_",
|
|
29
|
+
env_file=ENV_FILE,
|
|
30
|
+
extra="ignore",
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
project_url: AnyHttpUrl
|
|
34
|
+
base_url: AnyHttpUrl
|
|
35
|
+
required_scopes: list[str] | None = None
|
|
36
|
+
|
|
37
|
+
@field_validator("required_scopes", mode="before")
|
|
38
|
+
@classmethod
|
|
39
|
+
def _parse_scopes(cls, v):
|
|
40
|
+
return parse_scopes(v)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class SupabaseProvider(RemoteAuthProvider):
|
|
44
|
+
"""Supabase metadata provider for DCR (Dynamic Client Registration).
|
|
45
|
+
|
|
46
|
+
This provider implements Supabase Auth integration using metadata forwarding.
|
|
47
|
+
This approach allows Supabase to handle the OAuth flow directly while FastMCP acts
|
|
48
|
+
as a resource server, verifying JWTs issued by Supabase Auth.
|
|
49
|
+
|
|
50
|
+
IMPORTANT SETUP REQUIREMENTS:
|
|
51
|
+
|
|
52
|
+
1. Supabase Project Setup:
|
|
53
|
+
- Create a Supabase project at https://supabase.com
|
|
54
|
+
- Note your project URL (e.g., "https://abc123.supabase.co")
|
|
55
|
+
- For projects created after May 1st, 2025, asymmetric RS256 keys are used by default
|
|
56
|
+
- For older projects, consider migrating to asymmetric keys for better security
|
|
57
|
+
|
|
58
|
+
2. JWT Verification:
|
|
59
|
+
- FastMCP verifies JWTs using the JWKS endpoint at {project_url}/auth/v1/.well-known/jwks.json
|
|
60
|
+
- JWTs are issued by {project_url}/auth/v1
|
|
61
|
+
- Tokens are cached for up to 10 minutes by Supabase's edge servers
|
|
62
|
+
|
|
63
|
+
For detailed setup instructions, see:
|
|
64
|
+
https://supabase.com/docs/guides/auth/jwts
|
|
65
|
+
|
|
66
|
+
Example:
|
|
67
|
+
```python
|
|
68
|
+
from fastmcp.server.auth.providers.supabase import SupabaseProvider
|
|
69
|
+
|
|
70
|
+
# Create Supabase metadata provider (JWT verifier created automatically)
|
|
71
|
+
supabase_auth = SupabaseProvider(
|
|
72
|
+
project_url="https://abc123.supabase.co",
|
|
73
|
+
base_url="https://your-fastmcp-server.com",
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
# Use with FastMCP
|
|
77
|
+
mcp = FastMCP("My App", auth=supabase_auth)
|
|
78
|
+
```
|
|
79
|
+
"""
|
|
80
|
+
|
|
81
|
+
def __init__(
|
|
82
|
+
self,
|
|
83
|
+
*,
|
|
84
|
+
project_url: AnyHttpUrl | str | NotSetT = NotSet,
|
|
85
|
+
base_url: AnyHttpUrl | str | NotSetT = NotSet,
|
|
86
|
+
required_scopes: list[str] | None | NotSetT = NotSet,
|
|
87
|
+
token_verifier: TokenVerifier | None = None,
|
|
88
|
+
):
|
|
89
|
+
"""Initialize Supabase metadata provider.
|
|
90
|
+
|
|
91
|
+
Args:
|
|
92
|
+
project_url: Your Supabase project URL (e.g., "https://abc123.supabase.co")
|
|
93
|
+
base_url: Public URL of this FastMCP server
|
|
94
|
+
required_scopes: Optional list of scopes to require for all requests
|
|
95
|
+
token_verifier: Optional token verifier. If None, creates JWT verifier for Supabase
|
|
96
|
+
"""
|
|
97
|
+
settings = SupabaseProviderSettings.model_validate(
|
|
98
|
+
{
|
|
99
|
+
k: v
|
|
100
|
+
for k, v in {
|
|
101
|
+
"project_url": project_url,
|
|
102
|
+
"base_url": base_url,
|
|
103
|
+
"required_scopes": required_scopes,
|
|
104
|
+
}.items()
|
|
105
|
+
if v is not NotSet
|
|
106
|
+
}
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
self.project_url = str(settings.project_url).rstrip("/")
|
|
110
|
+
self.base_url = str(settings.base_url).rstrip("/")
|
|
111
|
+
|
|
112
|
+
# Create default JWT verifier if none provided
|
|
113
|
+
if token_verifier is None:
|
|
114
|
+
token_verifier = JWTVerifier(
|
|
115
|
+
jwks_uri=f"{self.project_url}/auth/v1/.well-known/jwks.json",
|
|
116
|
+
issuer=f"{self.project_url}/auth/v1",
|
|
117
|
+
algorithm="ES256", # Supabase uses ES256 for asymmetric keys
|
|
118
|
+
required_scopes=settings.required_scopes,
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
# Initialize RemoteAuthProvider with Supabase as the authorization server
|
|
122
|
+
super().__init__(
|
|
123
|
+
token_verifier=token_verifier,
|
|
124
|
+
authorization_servers=[AnyHttpUrl(f"{self.project_url}/auth/v1")],
|
|
125
|
+
base_url=self.base_url,
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
def get_routes(
|
|
129
|
+
self,
|
|
130
|
+
mcp_path: str | None = None,
|
|
131
|
+
) -> list[Route]:
|
|
132
|
+
"""Get OAuth routes including Supabase authorization server metadata forwarding.
|
|
133
|
+
|
|
134
|
+
This returns the standard protected resource routes plus an authorization server
|
|
135
|
+
metadata endpoint that forwards Supabase's OAuth metadata to clients.
|
|
136
|
+
|
|
137
|
+
Args:
|
|
138
|
+
mcp_path: The path where the MCP endpoint is mounted (e.g., "/mcp")
|
|
139
|
+
This is used to advertise the resource URL in metadata.
|
|
140
|
+
"""
|
|
141
|
+
# Get the standard protected resource routes from RemoteAuthProvider
|
|
142
|
+
routes = super().get_routes(mcp_path)
|
|
143
|
+
|
|
144
|
+
async def oauth_authorization_server_metadata(request):
|
|
145
|
+
"""Forward Supabase OAuth authorization server metadata with FastMCP customizations."""
|
|
146
|
+
try:
|
|
147
|
+
async with httpx.AsyncClient() as client:
|
|
148
|
+
response = await client.get(
|
|
149
|
+
f"{self.project_url}/auth/v1/.well-known/oauth-authorization-server"
|
|
150
|
+
)
|
|
151
|
+
response.raise_for_status()
|
|
152
|
+
metadata = response.json()
|
|
153
|
+
return JSONResponse(metadata)
|
|
154
|
+
except Exception as e:
|
|
155
|
+
return JSONResponse(
|
|
156
|
+
{
|
|
157
|
+
"error": "server_error",
|
|
158
|
+
"error_description": f"Failed to fetch Supabase metadata: {e}",
|
|
159
|
+
},
|
|
160
|
+
status_code=500,
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
# Add Supabase authorization server metadata forwarding
|
|
164
|
+
routes.append(
|
|
165
|
+
Route(
|
|
166
|
+
"/.well-known/oauth-authorization-server",
|
|
167
|
+
endpoint=oauth_authorization_server_metadata,
|
|
168
|
+
methods=["GET"],
|
|
169
|
+
)
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
return routes
|
|
@@ -10,9 +10,8 @@ Choose based on your WorkOS setup and authentication requirements.
|
|
|
10
10
|
|
|
11
11
|
from __future__ import annotations
|
|
12
12
|
|
|
13
|
-
from typing import Any
|
|
14
|
-
|
|
15
13
|
import httpx
|
|
14
|
+
from key_value.aio.protocols import AsyncKeyValue
|
|
16
15
|
from pydantic import AnyHttpUrl, SecretStr, field_validator
|
|
17
16
|
from pydantic_settings import BaseSettings, SettingsConfigDict
|
|
18
17
|
from starlette.responses import JSONResponse
|
|
@@ -21,9 +20,9 @@ from starlette.routing import Route
|
|
|
21
20
|
from fastmcp.server.auth import AccessToken, RemoteAuthProvider, TokenVerifier
|
|
22
21
|
from fastmcp.server.auth.oauth_proxy import OAuthProxy
|
|
23
22
|
from fastmcp.server.auth.providers.jwt import JWTVerifier
|
|
23
|
+
from fastmcp.settings import ENV_FILE
|
|
24
24
|
from fastmcp.utilities.auth import parse_scopes
|
|
25
25
|
from fastmcp.utilities.logging import get_logger
|
|
26
|
-
from fastmcp.utilities.storage import KVStorage
|
|
27
26
|
from fastmcp.utilities.types import NotSet, NotSetT
|
|
28
27
|
|
|
29
28
|
logger = get_logger(__name__)
|
|
@@ -34,7 +33,7 @@ class WorkOSProviderSettings(BaseSettings):
|
|
|
34
33
|
|
|
35
34
|
model_config = SettingsConfigDict(
|
|
36
35
|
env_prefix="FASTMCP_SERVER_AUTH_WORKOS_",
|
|
37
|
-
env_file=
|
|
36
|
+
env_file=ENV_FILE,
|
|
38
37
|
extra="ignore",
|
|
39
38
|
)
|
|
40
39
|
|
|
@@ -42,10 +41,12 @@ class WorkOSProviderSettings(BaseSettings):
|
|
|
42
41
|
client_secret: SecretStr | None = None
|
|
43
42
|
authkit_domain: str | None = None # e.g., "https://your-app.authkit.app"
|
|
44
43
|
base_url: AnyHttpUrl | str | None = None
|
|
44
|
+
issuer_url: AnyHttpUrl | str | None = None
|
|
45
45
|
redirect_path: str | None = None
|
|
46
46
|
required_scopes: list[str] | None = None
|
|
47
47
|
timeout_seconds: int | None = None
|
|
48
48
|
allowed_client_redirect_uris: list[str] | None = None
|
|
49
|
+
jwt_signing_key: str | None = None
|
|
49
50
|
|
|
50
51
|
@field_validator("required_scopes", mode="before")
|
|
51
52
|
@classmethod
|
|
@@ -166,11 +167,14 @@ class WorkOSProvider(OAuthProxy):
|
|
|
166
167
|
client_secret: str | NotSetT = NotSet,
|
|
167
168
|
authkit_domain: str | NotSetT = NotSet,
|
|
168
169
|
base_url: AnyHttpUrl | str | NotSetT = NotSet,
|
|
170
|
+
issuer_url: AnyHttpUrl | str | NotSetT = NotSet,
|
|
169
171
|
redirect_path: str | NotSetT = NotSet,
|
|
170
172
|
required_scopes: list[str] | None | NotSetT = NotSet,
|
|
171
173
|
timeout_seconds: int | NotSetT = NotSet,
|
|
172
174
|
allowed_client_redirect_uris: list[str] | NotSetT = NotSet,
|
|
173
|
-
client_storage:
|
|
175
|
+
client_storage: AsyncKeyValue | None = None,
|
|
176
|
+
jwt_signing_key: str | bytes | NotSetT = NotSet,
|
|
177
|
+
require_authorization_consent: bool = True,
|
|
174
178
|
):
|
|
175
179
|
"""Initialize WorkOS OAuth provider.
|
|
176
180
|
|
|
@@ -178,14 +182,24 @@ class WorkOSProvider(OAuthProxy):
|
|
|
178
182
|
client_id: WorkOS client ID
|
|
179
183
|
client_secret: WorkOS client secret
|
|
180
184
|
authkit_domain: Your WorkOS AuthKit domain (e.g., "https://your-app.authkit.app")
|
|
181
|
-
base_url: Public URL
|
|
185
|
+
base_url: Public URL where OAuth endpoints will be accessible (includes any mount path)
|
|
186
|
+
issuer_url: Issuer URL for OAuth metadata (defaults to base_url). Use root-level URL
|
|
187
|
+
to avoid 404s during discovery when mounting under a path.
|
|
182
188
|
redirect_path: Redirect path configured in WorkOS (defaults to "/auth/callback")
|
|
183
189
|
required_scopes: Required OAuth scopes (no default)
|
|
184
190
|
timeout_seconds: HTTP request timeout for WorkOS API calls
|
|
185
191
|
allowed_client_redirect_uris: List of allowed redirect URI patterns for MCP clients.
|
|
186
192
|
If None (default), all URIs are allowed. If empty list, no URIs are allowed.
|
|
187
|
-
client_storage: Storage
|
|
188
|
-
|
|
193
|
+
client_storage: Storage backend for OAuth state (client registrations, encrypted tokens).
|
|
194
|
+
If None, a DiskStore will be created in the data directory (derived from `platformdirs`). The
|
|
195
|
+
disk store will be encrypted using a key derived from the JWT Signing Key.
|
|
196
|
+
jwt_signing_key: Secret for signing FastMCP JWT tokens (any string or bytes). If bytes are provided,
|
|
197
|
+
they will be used as is. If a string is provided, it will be derived into a 32-byte key. If not
|
|
198
|
+
provided, the upstream client secret will be used to derive a 32-byte key using PBKDF2.
|
|
199
|
+
require_authorization_consent: Whether to require user consent before authorizing clients (default True).
|
|
200
|
+
When True, users see a consent screen before being redirected to WorkOS.
|
|
201
|
+
When False, authorization proceeds directly without user confirmation.
|
|
202
|
+
SECURITY WARNING: Only disable for local development or testing environments.
|
|
189
203
|
"""
|
|
190
204
|
|
|
191
205
|
settings = WorkOSProviderSettings.model_validate(
|
|
@@ -196,10 +210,12 @@ class WorkOSProvider(OAuthProxy):
|
|
|
196
210
|
"client_secret": client_secret,
|
|
197
211
|
"authkit_domain": authkit_domain,
|
|
198
212
|
"base_url": base_url,
|
|
213
|
+
"issuer_url": issuer_url,
|
|
199
214
|
"redirect_path": redirect_path,
|
|
200
215
|
"required_scopes": required_scopes,
|
|
201
216
|
"timeout_seconds": timeout_seconds,
|
|
202
217
|
"allowed_client_redirect_uris": allowed_client_redirect_uris,
|
|
218
|
+
"jwt_signing_key": jwt_signing_key,
|
|
203
219
|
}.items()
|
|
204
220
|
if v is not NotSet
|
|
205
221
|
}
|
|
@@ -249,12 +265,15 @@ class WorkOSProvider(OAuthProxy):
|
|
|
249
265
|
token_verifier=token_verifier,
|
|
250
266
|
base_url=settings.base_url,
|
|
251
267
|
redirect_path=settings.redirect_path,
|
|
252
|
-
issuer_url=settings.
|
|
268
|
+
issuer_url=settings.issuer_url
|
|
269
|
+
or settings.base_url, # Default to base_url if not specified
|
|
253
270
|
allowed_client_redirect_uris=allowed_client_redirect_uris_final,
|
|
254
271
|
client_storage=client_storage,
|
|
272
|
+
jwt_signing_key=settings.jwt_signing_key,
|
|
273
|
+
require_authorization_consent=require_authorization_consent,
|
|
255
274
|
)
|
|
256
275
|
|
|
257
|
-
logger.
|
|
276
|
+
logger.debug(
|
|
258
277
|
"Initialized WorkOS OAuth provider for client %s with AuthKit domain %s",
|
|
259
278
|
settings.client_id,
|
|
260
279
|
authkit_domain_final,
|
|
@@ -264,7 +283,7 @@ class WorkOSProvider(OAuthProxy):
|
|
|
264
283
|
class AuthKitProviderSettings(BaseSettings):
|
|
265
284
|
model_config = SettingsConfigDict(
|
|
266
285
|
env_prefix="FASTMCP_SERVER_AUTH_AUTHKITPROVIDER_",
|
|
267
|
-
env_file=
|
|
286
|
+
env_file=ENV_FILE,
|
|
268
287
|
extra="ignore",
|
|
269
288
|
)
|
|
270
289
|
|
|
@@ -364,7 +383,6 @@ class AuthKitProvider(RemoteAuthProvider):
|
|
|
364
383
|
def get_routes(
|
|
365
384
|
self,
|
|
366
385
|
mcp_path: str | None = None,
|
|
367
|
-
mcp_endpoint: Any | None = None,
|
|
368
386
|
) -> list[Route]:
|
|
369
387
|
"""Get OAuth routes including AuthKit authorization server metadata forwarding.
|
|
370
388
|
|
|
@@ -373,10 +391,10 @@ class AuthKitProvider(RemoteAuthProvider):
|
|
|
373
391
|
|
|
374
392
|
Args:
|
|
375
393
|
mcp_path: The path where the MCP endpoint is mounted (e.g., "/mcp")
|
|
376
|
-
|
|
394
|
+
This is used to advertise the resource URL in metadata.
|
|
377
395
|
"""
|
|
378
396
|
# Get the standard protected resource routes from RemoteAuthProvider
|
|
379
|
-
routes = super().get_routes(mcp_path
|
|
397
|
+
routes = super().get_routes(mcp_path)
|
|
380
398
|
|
|
381
399
|
async def oauth_authorization_server_metadata(request):
|
|
382
400
|
"""Forward AuthKit OAuth authorization server metadata with FastMCP customizations."""
|
fastmcp/server/context.py
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
import asyncio
|
|
4
3
|
import copy
|
|
5
4
|
import inspect
|
|
5
|
+
import logging
|
|
6
6
|
import warnings
|
|
7
7
|
import weakref
|
|
8
8
|
from collections.abc import Generator, Mapping, Sequence
|
|
@@ -10,8 +10,10 @@ from contextlib import contextmanager
|
|
|
10
10
|
from contextvars import ContextVar, Token
|
|
11
11
|
from dataclasses import dataclass
|
|
12
12
|
from enum import Enum
|
|
13
|
+
from logging import Logger
|
|
13
14
|
from typing import Any, Literal, cast, get_origin, overload
|
|
14
15
|
|
|
16
|
+
import anyio
|
|
15
17
|
from mcp import LoggingLevel, ServerSession
|
|
16
18
|
from mcp.server.lowlevel.helper_types import ReadResourceContents
|
|
17
19
|
from mcp.server.lowlevel.server import request_ctx
|
|
@@ -20,6 +22,7 @@ from mcp.types import (
|
|
|
20
22
|
AudioContent,
|
|
21
23
|
ClientCapabilities,
|
|
22
24
|
CreateMessageResult,
|
|
25
|
+
GetPromptResult,
|
|
23
26
|
ImageContent,
|
|
24
27
|
IncludeContext,
|
|
25
28
|
ModelHint,
|
|
@@ -30,6 +33,8 @@ from mcp.types import (
|
|
|
30
33
|
TextContent,
|
|
31
34
|
)
|
|
32
35
|
from mcp.types import CreateMessageRequestParams as SamplingParams
|
|
36
|
+
from mcp.types import Prompt as MCPPrompt
|
|
37
|
+
from mcp.types import Resource as MCPResource
|
|
33
38
|
from pydantic.networks import AnyUrl
|
|
34
39
|
from starlette.requests import Request
|
|
35
40
|
from typing_extensions import TypeVar
|
|
@@ -44,14 +49,21 @@ from fastmcp.server.elicitation import (
|
|
|
44
49
|
get_elicitation_schema,
|
|
45
50
|
)
|
|
46
51
|
from fastmcp.server.server import FastMCP
|
|
47
|
-
from fastmcp.utilities.logging import get_logger
|
|
52
|
+
from fastmcp.utilities.logging import _clamp_logger, get_logger
|
|
48
53
|
from fastmcp.utilities.types import get_cached_typeadapter
|
|
49
54
|
|
|
50
|
-
logger = get_logger(__name__)
|
|
55
|
+
logger: Logger = get_logger(name=__name__)
|
|
56
|
+
to_client_logger: Logger = logger.getChild(suffix="to_client")
|
|
57
|
+
|
|
58
|
+
# Convert all levels of server -> client messages to debug level
|
|
59
|
+
# This clamp can be undone at runtime by calling `_unclamp_logger` or calling
|
|
60
|
+
# `_clamp_logger` with a different max level.
|
|
61
|
+
_clamp_logger(logger=to_client_logger, max_level="DEBUG")
|
|
62
|
+
|
|
51
63
|
|
|
52
64
|
T = TypeVar("T", default=Any)
|
|
53
65
|
_current_context: ContextVar[Context | None] = ContextVar("context", default=None) # type: ignore[assignment]
|
|
54
|
-
_flush_lock =
|
|
66
|
+
_flush_lock = anyio.Lock()
|
|
55
67
|
|
|
56
68
|
|
|
57
69
|
@dataclass
|
|
@@ -66,6 +78,18 @@ class LogData:
|
|
|
66
78
|
extra: Mapping[str, Any] | None = None
|
|
67
79
|
|
|
68
80
|
|
|
81
|
+
_mcp_level_to_python_level = {
|
|
82
|
+
"debug": logging.DEBUG,
|
|
83
|
+
"info": logging.INFO,
|
|
84
|
+
"notice": logging.INFO,
|
|
85
|
+
"warning": logging.WARNING,
|
|
86
|
+
"error": logging.ERROR,
|
|
87
|
+
"critical": logging.CRITICAL,
|
|
88
|
+
"alert": logging.CRITICAL,
|
|
89
|
+
"emergency": logging.CRITICAL,
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
|
|
69
93
|
@contextmanager
|
|
70
94
|
def set_context(context: Context) -> Generator[Context, None, None]:
|
|
71
95
|
token = _current_context.set(context)
|
|
@@ -194,6 +218,36 @@ class Context:
|
|
|
194
218
|
related_request_id=self.request_id,
|
|
195
219
|
)
|
|
196
220
|
|
|
221
|
+
async def list_resources(self) -> list[MCPResource]:
|
|
222
|
+
"""List all available resources from the server.
|
|
223
|
+
|
|
224
|
+
Returns:
|
|
225
|
+
List of Resource objects available on the server
|
|
226
|
+
"""
|
|
227
|
+
return await self.fastmcp._list_resources_mcp()
|
|
228
|
+
|
|
229
|
+
async def list_prompts(self) -> list[MCPPrompt]:
|
|
230
|
+
"""List all available prompts from the server.
|
|
231
|
+
|
|
232
|
+
Returns:
|
|
233
|
+
List of Prompt objects available on the server
|
|
234
|
+
"""
|
|
235
|
+
return await self.fastmcp._list_prompts_mcp()
|
|
236
|
+
|
|
237
|
+
async def get_prompt(
|
|
238
|
+
self, name: str, arguments: dict[str, Any] | None = None
|
|
239
|
+
) -> GetPromptResult:
|
|
240
|
+
"""Get a prompt by name with optional arguments.
|
|
241
|
+
|
|
242
|
+
Args:
|
|
243
|
+
name: The name of the prompt to get
|
|
244
|
+
arguments: Optional arguments to pass to the prompt
|
|
245
|
+
|
|
246
|
+
Returns:
|
|
247
|
+
The prompt result
|
|
248
|
+
"""
|
|
249
|
+
return await self.fastmcp._get_prompt_mcp(name, arguments)
|
|
250
|
+
|
|
197
251
|
async def read_resource(self, uri: str | AnyUrl) -> list[ReadResourceContents]:
|
|
198
252
|
"""Read a resource by URI.
|
|
199
253
|
|
|
@@ -203,9 +257,7 @@ class Context:
|
|
|
203
257
|
Returns:
|
|
204
258
|
The resource content as either text or bytes
|
|
205
259
|
"""
|
|
206
|
-
|
|
207
|
-
raise ValueError("Context is not available outside of a request")
|
|
208
|
-
return await self.fastmcp._mcp_read_resource(uri)
|
|
260
|
+
return await self.fastmcp._read_resource_mcp(uri)
|
|
209
261
|
|
|
210
262
|
async def log(
|
|
211
263
|
self,
|
|
@@ -216,6 +268,8 @@ class Context:
|
|
|
216
268
|
) -> None:
|
|
217
269
|
"""Send a log message to the client.
|
|
218
270
|
|
|
271
|
+
Messages sent to Clients are also logged to the `fastmcp.server.context.to_client` logger with a level of `DEBUG`.
|
|
272
|
+
|
|
219
273
|
Args:
|
|
220
274
|
message: Log message
|
|
221
275
|
level: Optional log level. One of "debug", "info", "notice", "warning", "error", "critical",
|
|
@@ -223,13 +277,13 @@ class Context:
|
|
|
223
277
|
logger_name: Optional logger name
|
|
224
278
|
extra: Optional mapping for additional arguments
|
|
225
279
|
"""
|
|
226
|
-
if level is None:
|
|
227
|
-
level = "info"
|
|
228
280
|
data = LogData(msg=message, extra=extra)
|
|
229
|
-
|
|
230
|
-
|
|
281
|
+
|
|
282
|
+
await _log_to_server_and_client(
|
|
231
283
|
data=data,
|
|
232
|
-
|
|
284
|
+
session=self.session,
|
|
285
|
+
level=level or "info",
|
|
286
|
+
logger_name=logger_name,
|
|
233
287
|
related_request_id=self.request_id,
|
|
234
288
|
)
|
|
235
289
|
|
|
@@ -303,9 +357,14 @@ class Context:
|
|
|
303
357
|
logger_name: str | None = None,
|
|
304
358
|
extra: Mapping[str, Any] | None = None,
|
|
305
359
|
) -> None:
|
|
306
|
-
"""Send a
|
|
360
|
+
"""Send a `DEBUG`-level message to the connected MCP Client.
|
|
361
|
+
|
|
362
|
+
Messages sent to Clients are also logged to the `fastmcp.server.context.to_client` logger with a level of `DEBUG`."""
|
|
307
363
|
await self.log(
|
|
308
|
-
level="debug",
|
|
364
|
+
level="debug",
|
|
365
|
+
message=message,
|
|
366
|
+
logger_name=logger_name,
|
|
367
|
+
extra=extra,
|
|
309
368
|
)
|
|
310
369
|
|
|
311
370
|
async def info(
|
|
@@ -314,9 +373,14 @@ class Context:
|
|
|
314
373
|
logger_name: str | None = None,
|
|
315
374
|
extra: Mapping[str, Any] | None = None,
|
|
316
375
|
) -> None:
|
|
317
|
-
"""Send
|
|
376
|
+
"""Send a `INFO`-level message to the connected MCP Client.
|
|
377
|
+
|
|
378
|
+
Messages sent to Clients are also logged to the `fastmcp.server.context.to_client` logger with a level of `DEBUG`."""
|
|
318
379
|
await self.log(
|
|
319
|
-
level="info",
|
|
380
|
+
level="info",
|
|
381
|
+
message=message,
|
|
382
|
+
logger_name=logger_name,
|
|
383
|
+
extra=extra,
|
|
320
384
|
)
|
|
321
385
|
|
|
322
386
|
async def warning(
|
|
@@ -325,9 +389,14 @@ class Context:
|
|
|
325
389
|
logger_name: str | None = None,
|
|
326
390
|
extra: Mapping[str, Any] | None = None,
|
|
327
391
|
) -> None:
|
|
328
|
-
"""Send a
|
|
392
|
+
"""Send a `WARNING`-level message to the connected MCP Client.
|
|
393
|
+
|
|
394
|
+
Messages sent to Clients are also logged to the `fastmcp.server.context.to_client` logger with a level of `DEBUG`."""
|
|
329
395
|
await self.log(
|
|
330
|
-
level="warning",
|
|
396
|
+
level="warning",
|
|
397
|
+
message=message,
|
|
398
|
+
logger_name=logger_name,
|
|
399
|
+
extra=extra,
|
|
331
400
|
)
|
|
332
401
|
|
|
333
402
|
async def error(
|
|
@@ -336,9 +405,14 @@ class Context:
|
|
|
336
405
|
logger_name: str | None = None,
|
|
337
406
|
extra: Mapping[str, Any] | None = None,
|
|
338
407
|
) -> None:
|
|
339
|
-
"""Send
|
|
408
|
+
"""Send a `ERROR`-level message to the connected MCP Client.
|
|
409
|
+
|
|
410
|
+
Messages sent to Clients are also logged to the `fastmcp.server.context.to_client` logger with a level of `DEBUG`."""
|
|
340
411
|
await self.log(
|
|
341
|
-
level="error",
|
|
412
|
+
level="error",
|
|
413
|
+
message=message,
|
|
414
|
+
logger_name=logger_name,
|
|
415
|
+
extra=extra,
|
|
342
416
|
)
|
|
343
417
|
|
|
344
418
|
async def list_roots(self) -> list[Root]:
|
|
@@ -592,30 +666,14 @@ class Context:
|
|
|
592
666
|
def _queue_tool_list_changed(self) -> None:
|
|
593
667
|
"""Queue a tool list changed notification."""
|
|
594
668
|
self._notification_queue.add("notifications/tools/list_changed")
|
|
595
|
-
self._try_flush_notifications()
|
|
596
669
|
|
|
597
670
|
def _queue_resource_list_changed(self) -> None:
|
|
598
671
|
"""Queue a resource list changed notification."""
|
|
599
672
|
self._notification_queue.add("notifications/resources/list_changed")
|
|
600
|
-
self._try_flush_notifications()
|
|
601
673
|
|
|
602
674
|
def _queue_prompt_list_changed(self) -> None:
|
|
603
675
|
"""Queue a prompt list changed notification."""
|
|
604
676
|
self._notification_queue.add("notifications/prompts/list_changed")
|
|
605
|
-
self._try_flush_notifications()
|
|
606
|
-
|
|
607
|
-
def _try_flush_notifications(self) -> None:
|
|
608
|
-
"""Synchronous method that attempts to flush notifications if we're in an async context."""
|
|
609
|
-
try:
|
|
610
|
-
# Check if we're in an async context
|
|
611
|
-
loop = asyncio.get_running_loop()
|
|
612
|
-
if loop and not loop.is_running():
|
|
613
|
-
return
|
|
614
|
-
# Schedule flush as a task (fire-and-forget)
|
|
615
|
-
asyncio.create_task(self._flush_notifications())
|
|
616
|
-
except RuntimeError:
|
|
617
|
-
# No event loop - will flush later
|
|
618
|
-
pass
|
|
619
677
|
|
|
620
678
|
async def _flush_notifications(self) -> None:
|
|
621
679
|
"""Send all queued notifications."""
|
|
@@ -675,3 +733,31 @@ def _parse_model_preferences(
|
|
|
675
733
|
raise ValueError(
|
|
676
734
|
"model_preferences must be one of: ModelPreferences, str, list[str], or None."
|
|
677
735
|
)
|
|
736
|
+
|
|
737
|
+
|
|
738
|
+
async def _log_to_server_and_client(
|
|
739
|
+
data: LogData,
|
|
740
|
+
session: ServerSession,
|
|
741
|
+
level: LoggingLevel,
|
|
742
|
+
logger_name: str | None = None,
|
|
743
|
+
related_request_id: str | None = None,
|
|
744
|
+
) -> None:
|
|
745
|
+
"""Log a message to the server and client."""
|
|
746
|
+
|
|
747
|
+
msg_prefix = f"Sending {level.upper()} to client"
|
|
748
|
+
|
|
749
|
+
if logger_name:
|
|
750
|
+
msg_prefix += f" ({logger_name})"
|
|
751
|
+
|
|
752
|
+
to_client_logger.log(
|
|
753
|
+
level=_mcp_level_to_python_level[level],
|
|
754
|
+
msg=f"{msg_prefix}: {data.msg}",
|
|
755
|
+
extra=data.extra,
|
|
756
|
+
)
|
|
757
|
+
|
|
758
|
+
await session.send_log_message(
|
|
759
|
+
level=level,
|
|
760
|
+
data=data,
|
|
761
|
+
logger=logger_name,
|
|
762
|
+
related_request_id=related_request_id,
|
|
763
|
+
)
|