fastmcp 2.12.1__py3-none-any.whl → 2.13.2__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 +2 -2
- fastmcp/cli/cli.py +56 -36
- fastmcp/cli/install/__init__.py +2 -0
- fastmcp/cli/install/claude_code.py +7 -16
- fastmcp/cli/install/claude_desktop.py +4 -12
- fastmcp/cli/install/cursor.py +20 -30
- fastmcp/cli/install/gemini_cli.py +241 -0
- fastmcp/cli/install/mcp_json.py +4 -12
- fastmcp/cli/run.py +15 -94
- fastmcp/client/__init__.py +9 -9
- fastmcp/client/auth/oauth.py +117 -206
- fastmcp/client/client.py +123 -47
- fastmcp/client/elicitation.py +6 -1
- fastmcp/client/logging.py +18 -14
- fastmcp/client/oauth_callback.py +85 -171
- fastmcp/client/sampling.py +1 -1
- fastmcp/client/transports.py +81 -26
- fastmcp/contrib/component_manager/__init__.py +1 -1
- fastmcp/contrib/component_manager/component_manager.py +2 -2
- fastmcp/contrib/component_manager/component_service.py +7 -7
- fastmcp/contrib/mcp_mixin/README.md +35 -4
- fastmcp/contrib/mcp_mixin/__init__.py +2 -2
- fastmcp/contrib/mcp_mixin/mcp_mixin.py +54 -7
- fastmcp/experimental/sampling/handlers/openai.py +2 -2
- fastmcp/experimental/server/openapi/__init__.py +5 -8
- fastmcp/experimental/server/openapi/components.py +11 -7
- fastmcp/experimental/server/openapi/routing.py +2 -2
- fastmcp/experimental/utilities/openapi/__init__.py +10 -15
- fastmcp/experimental/utilities/openapi/director.py +16 -10
- fastmcp/experimental/utilities/openapi/json_schema_converter.py +6 -2
- fastmcp/experimental/utilities/openapi/models.py +3 -3
- fastmcp/experimental/utilities/openapi/parser.py +37 -16
- fastmcp/experimental/utilities/openapi/schemas.py +33 -7
- fastmcp/mcp_config.py +3 -4
- fastmcp/prompts/__init__.py +1 -1
- fastmcp/prompts/prompt.py +32 -27
- fastmcp/prompts/prompt_manager.py +16 -101
- fastmcp/resources/__init__.py +5 -5
- fastmcp/resources/resource.py +28 -20
- fastmcp/resources/resource_manager.py +9 -168
- fastmcp/resources/template.py +119 -27
- fastmcp/resources/types.py +30 -24
- fastmcp/server/__init__.py +1 -1
- fastmcp/server/auth/__init__.py +9 -5
- fastmcp/server/auth/auth.py +80 -47
- fastmcp/server/auth/handlers/authorize.py +326 -0
- fastmcp/server/auth/jwt_issuer.py +236 -0
- fastmcp/server/auth/middleware.py +96 -0
- fastmcp/server/auth/oauth_proxy.py +1556 -265
- fastmcp/server/auth/oidc_proxy.py +412 -0
- fastmcp/server/auth/providers/auth0.py +193 -0
- fastmcp/server/auth/providers/aws.py +263 -0
- fastmcp/server/auth/providers/azure.py +314 -129
- fastmcp/server/auth/providers/bearer.py +1 -1
- fastmcp/server/auth/providers/debug.py +114 -0
- fastmcp/server/auth/providers/descope.py +229 -0
- fastmcp/server/auth/providers/discord.py +308 -0
- fastmcp/server/auth/providers/github.py +31 -6
- fastmcp/server/auth/providers/google.py +50 -7
- fastmcp/server/auth/providers/in_memory.py +27 -3
- fastmcp/server/auth/providers/introspection.py +281 -0
- fastmcp/server/auth/providers/jwt.py +48 -31
- fastmcp/server/auth/providers/oci.py +233 -0
- fastmcp/server/auth/providers/scalekit.py +238 -0
- fastmcp/server/auth/providers/supabase.py +188 -0
- fastmcp/server/auth/providers/workos.py +37 -15
- fastmcp/server/context.py +194 -67
- fastmcp/server/dependencies.py +56 -16
- fastmcp/server/elicitation.py +1 -1
- fastmcp/server/http.py +57 -18
- fastmcp/server/low_level.py +121 -2
- fastmcp/server/middleware/__init__.py +1 -1
- fastmcp/server/middleware/caching.py +476 -0
- fastmcp/server/middleware/error_handling.py +14 -10
- fastmcp/server/middleware/logging.py +158 -116
- fastmcp/server/middleware/middleware.py +30 -16
- fastmcp/server/middleware/rate_limiting.py +3 -3
- fastmcp/server/middleware/tool_injection.py +116 -0
- fastmcp/server/openapi.py +15 -7
- fastmcp/server/proxy.py +22 -11
- fastmcp/server/server.py +744 -254
- fastmcp/settings.py +65 -15
- fastmcp/tools/__init__.py +1 -1
- fastmcp/tools/tool.py +173 -108
- fastmcp/tools/tool_manager.py +30 -112
- fastmcp/tools/tool_transform.py +13 -11
- fastmcp/utilities/cli.py +67 -28
- fastmcp/utilities/components.py +7 -2
- fastmcp/utilities/inspect.py +79 -23
- fastmcp/utilities/json_schema.py +21 -4
- fastmcp/utilities/json_schema_type.py +4 -4
- fastmcp/utilities/logging.py +182 -10
- fastmcp/utilities/mcp_server_config/__init__.py +3 -3
- fastmcp/utilities/mcp_server_config/v1/environments/base.py +1 -2
- fastmcp/utilities/mcp_server_config/v1/environments/uv.py +10 -45
- fastmcp/utilities/mcp_server_config/v1/mcp_server_config.py +8 -7
- fastmcp/utilities/mcp_server_config/v1/schema.json +5 -1
- fastmcp/utilities/mcp_server_config/v1/sources/base.py +0 -1
- fastmcp/utilities/openapi.py +11 -11
- fastmcp/utilities/tests.py +93 -10
- fastmcp/utilities/types.py +87 -21
- fastmcp/utilities/ui.py +626 -0
- {fastmcp-2.12.1.dist-info → fastmcp-2.13.2.dist-info}/METADATA +141 -60
- fastmcp-2.13.2.dist-info/RECORD +144 -0
- {fastmcp-2.12.1.dist-info → fastmcp-2.13.2.dist-info}/WHEEL +1 -1
- fastmcp/cli/claude.py +0 -144
- fastmcp-2.12.1.dist-info/RECORD +0 -128
- {fastmcp-2.12.1.dist-info → fastmcp-2.13.2.dist-info}/entry_points.txt +0 -0
- {fastmcp-2.12.1.dist-info → fastmcp-2.13.2.dist-info}/licenses/LICENSE +0 -0
fastmcp/server/context.py
CHANGED
|
@@ -1,25 +1,29 @@
|
|
|
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
|
-
from collections.abc import Generator, Mapping
|
|
8
|
+
from collections.abc import Generator, Mapping, Sequence
|
|
9
9
|
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
|
|
18
20
|
from mcp.shared.context import RequestContext
|
|
19
21
|
from mcp.types import (
|
|
22
|
+
AudioContent,
|
|
20
23
|
ClientCapabilities,
|
|
21
|
-
ContentBlock,
|
|
22
24
|
CreateMessageResult,
|
|
25
|
+
GetPromptResult,
|
|
26
|
+
ImageContent,
|
|
23
27
|
IncludeContext,
|
|
24
28
|
ModelHint,
|
|
25
29
|
ModelPreferences,
|
|
@@ -29,6 +33,8 @@ from mcp.types import (
|
|
|
29
33
|
TextContent,
|
|
30
34
|
)
|
|
31
35
|
from mcp.types import CreateMessageRequestParams as SamplingParams
|
|
36
|
+
from mcp.types import Prompt as MCPPrompt
|
|
37
|
+
from mcp.types import Resource as MCPResource
|
|
32
38
|
from pydantic.networks import AnyUrl
|
|
33
39
|
from starlette.requests import Request
|
|
34
40
|
from typing_extensions import TypeVar
|
|
@@ -43,14 +49,21 @@ from fastmcp.server.elicitation import (
|
|
|
43
49
|
get_elicitation_schema,
|
|
44
50
|
)
|
|
45
51
|
from fastmcp.server.server import FastMCP
|
|
46
|
-
from fastmcp.utilities.logging import get_logger
|
|
52
|
+
from fastmcp.utilities.logging import _clamp_logger, get_logger
|
|
47
53
|
from fastmcp.utilities.types import get_cached_typeadapter
|
|
48
54
|
|
|
49
|
-
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
|
+
|
|
50
63
|
|
|
51
64
|
T = TypeVar("T", default=Any)
|
|
52
65
|
_current_context: ContextVar[Context | None] = ContextVar("context", default=None) # type: ignore[assignment]
|
|
53
|
-
_flush_lock =
|
|
66
|
+
_flush_lock = anyio.Lock()
|
|
54
67
|
|
|
55
68
|
|
|
56
69
|
@dataclass
|
|
@@ -65,6 +78,18 @@ class LogData:
|
|
|
65
78
|
extra: Mapping[str, Any] | None = None
|
|
66
79
|
|
|
67
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
|
+
|
|
68
93
|
@contextmanager
|
|
69
94
|
def set_context(context: Context) -> Generator[Context, None, None]:
|
|
70
95
|
token = _current_context.set(context)
|
|
@@ -85,18 +110,18 @@ class Context:
|
|
|
85
110
|
|
|
86
111
|
```python
|
|
87
112
|
@server.tool
|
|
88
|
-
def my_tool(x: int, ctx: Context) -> str:
|
|
113
|
+
async def my_tool(x: int, ctx: Context) -> str:
|
|
89
114
|
# Log messages to the client
|
|
90
|
-
ctx.info(f"Processing {x}")
|
|
91
|
-
ctx.debug("Debug info")
|
|
92
|
-
ctx.warning("Warning message")
|
|
93
|
-
ctx.error("Error message")
|
|
115
|
+
await ctx.info(f"Processing {x}")
|
|
116
|
+
await ctx.debug("Debug info")
|
|
117
|
+
await ctx.warning("Warning message")
|
|
118
|
+
await ctx.error("Error message")
|
|
94
119
|
|
|
95
120
|
# Report progress
|
|
96
|
-
ctx.report_progress(50, 100, "Processing")
|
|
121
|
+
await ctx.report_progress(50, 100, "Processing")
|
|
97
122
|
|
|
98
123
|
# Access resources
|
|
99
|
-
data = ctx.read_resource("resource://data")
|
|
124
|
+
data = await ctx.read_resource("resource://data")
|
|
100
125
|
|
|
101
126
|
# Get request info
|
|
102
127
|
request_id = ctx.request_id
|
|
@@ -156,15 +181,33 @@ class Context:
|
|
|
156
181
|
_current_context.reset(token)
|
|
157
182
|
|
|
158
183
|
@property
|
|
159
|
-
def request_context(self) -> RequestContext[ServerSession, Any, Request]:
|
|
184
|
+
def request_context(self) -> RequestContext[ServerSession, Any, Request] | None:
|
|
160
185
|
"""Access to the underlying request context.
|
|
161
186
|
|
|
162
|
-
|
|
187
|
+
Returns None when the MCP session has not been established yet.
|
|
188
|
+
Returns the full RequestContext once the MCP session is available.
|
|
189
|
+
|
|
190
|
+
For HTTP request access in middleware, use `get_http_request()` from fastmcp.server.dependencies,
|
|
191
|
+
which works whether or not the MCP session is available.
|
|
192
|
+
|
|
193
|
+
Example in middleware:
|
|
194
|
+
```python
|
|
195
|
+
async def on_request(self, context, call_next):
|
|
196
|
+
ctx = context.fastmcp_context
|
|
197
|
+
if ctx.request_context:
|
|
198
|
+
# MCP session available - can access session_id, request_id, etc.
|
|
199
|
+
session_id = ctx.session_id
|
|
200
|
+
else:
|
|
201
|
+
# MCP session not available yet - use HTTP helpers
|
|
202
|
+
from fastmcp.server.dependencies import get_http_request
|
|
203
|
+
request = get_http_request()
|
|
204
|
+
return await call_next(context)
|
|
205
|
+
```
|
|
163
206
|
"""
|
|
164
207
|
try:
|
|
165
208
|
return request_ctx.get()
|
|
166
209
|
except LookupError:
|
|
167
|
-
|
|
210
|
+
return None
|
|
168
211
|
|
|
169
212
|
async def report_progress(
|
|
170
213
|
self, progress: float, total: float | None = None, message: str | None = None
|
|
@@ -178,7 +221,7 @@ class Context:
|
|
|
178
221
|
|
|
179
222
|
progress_token = (
|
|
180
223
|
self.request_context.meta.progressToken
|
|
181
|
-
if self.request_context.meta
|
|
224
|
+
if self.request_context and self.request_context.meta
|
|
182
225
|
else None
|
|
183
226
|
)
|
|
184
227
|
|
|
@@ -193,6 +236,36 @@ class Context:
|
|
|
193
236
|
related_request_id=self.request_id,
|
|
194
237
|
)
|
|
195
238
|
|
|
239
|
+
async def list_resources(self) -> list[MCPResource]:
|
|
240
|
+
"""List all available resources from the server.
|
|
241
|
+
|
|
242
|
+
Returns:
|
|
243
|
+
List of Resource objects available on the server
|
|
244
|
+
"""
|
|
245
|
+
return await self.fastmcp._list_resources_mcp()
|
|
246
|
+
|
|
247
|
+
async def list_prompts(self) -> list[MCPPrompt]:
|
|
248
|
+
"""List all available prompts from the server.
|
|
249
|
+
|
|
250
|
+
Returns:
|
|
251
|
+
List of Prompt objects available on the server
|
|
252
|
+
"""
|
|
253
|
+
return await self.fastmcp._list_prompts_mcp()
|
|
254
|
+
|
|
255
|
+
async def get_prompt(
|
|
256
|
+
self, name: str, arguments: dict[str, Any] | None = None
|
|
257
|
+
) -> GetPromptResult:
|
|
258
|
+
"""Get a prompt by name with optional arguments.
|
|
259
|
+
|
|
260
|
+
Args:
|
|
261
|
+
name: The name of the prompt to get
|
|
262
|
+
arguments: Optional arguments to pass to the prompt
|
|
263
|
+
|
|
264
|
+
Returns:
|
|
265
|
+
The prompt result
|
|
266
|
+
"""
|
|
267
|
+
return await self.fastmcp._get_prompt_mcp(name, arguments)
|
|
268
|
+
|
|
196
269
|
async def read_resource(self, uri: str | AnyUrl) -> list[ReadResourceContents]:
|
|
197
270
|
"""Read a resource by URI.
|
|
198
271
|
|
|
@@ -202,9 +275,7 @@ class Context:
|
|
|
202
275
|
Returns:
|
|
203
276
|
The resource content as either text or bytes
|
|
204
277
|
"""
|
|
205
|
-
|
|
206
|
-
raise ValueError("Context is not available outside of a request")
|
|
207
|
-
return await self.fastmcp._mcp_read_resource(uri)
|
|
278
|
+
return await self.fastmcp._read_resource_mcp(uri)
|
|
208
279
|
|
|
209
280
|
async def log(
|
|
210
281
|
self,
|
|
@@ -215,6 +286,8 @@ class Context:
|
|
|
215
286
|
) -> None:
|
|
216
287
|
"""Send a log message to the client.
|
|
217
288
|
|
|
289
|
+
Messages sent to Clients are also logged to the `fastmcp.server.context.to_client` logger with a level of `DEBUG`.
|
|
290
|
+
|
|
218
291
|
Args:
|
|
219
292
|
message: Log message
|
|
220
293
|
level: Optional log level. One of "debug", "info", "notice", "warning", "error", "critical",
|
|
@@ -222,13 +295,13 @@ class Context:
|
|
|
222
295
|
logger_name: Optional logger name
|
|
223
296
|
extra: Optional mapping for additional arguments
|
|
224
297
|
"""
|
|
225
|
-
if level is None:
|
|
226
|
-
level = "info"
|
|
227
298
|
data = LogData(msg=message, extra=extra)
|
|
228
|
-
|
|
229
|
-
|
|
299
|
+
|
|
300
|
+
await _log_to_server_and_client(
|
|
230
301
|
data=data,
|
|
231
|
-
|
|
302
|
+
session=self.session,
|
|
303
|
+
level=level or "info",
|
|
304
|
+
logger_name=logger_name,
|
|
232
305
|
related_request_id=self.request_id,
|
|
233
306
|
)
|
|
234
307
|
|
|
@@ -237,13 +310,21 @@ class Context:
|
|
|
237
310
|
"""Get the client ID if available."""
|
|
238
311
|
return (
|
|
239
312
|
getattr(self.request_context.meta, "client_id", None)
|
|
240
|
-
if self.request_context.meta
|
|
313
|
+
if self.request_context and self.request_context.meta
|
|
241
314
|
else None
|
|
242
315
|
)
|
|
243
316
|
|
|
244
317
|
@property
|
|
245
318
|
def request_id(self) -> str:
|
|
246
|
-
"""Get the unique ID for this request.
|
|
319
|
+
"""Get the unique ID for this request.
|
|
320
|
+
|
|
321
|
+
Raises RuntimeError if MCP request context is not available.
|
|
322
|
+
"""
|
|
323
|
+
if self.request_context is None:
|
|
324
|
+
raise RuntimeError(
|
|
325
|
+
"request_id is not available because the MCP session has not been established yet. "
|
|
326
|
+
"Check `context.request_context` for None before accessing this attribute."
|
|
327
|
+
)
|
|
247
328
|
return str(self.request_context.request_id)
|
|
248
329
|
|
|
249
330
|
@property
|
|
@@ -258,6 +339,9 @@ class Context:
|
|
|
258
339
|
The session ID for StreamableHTTP transports, or a generated ID
|
|
259
340
|
for other transports.
|
|
260
341
|
|
|
342
|
+
Raises:
|
|
343
|
+
RuntimeError if MCP request context is not available.
|
|
344
|
+
|
|
261
345
|
Example:
|
|
262
346
|
```python
|
|
263
347
|
@server.tool
|
|
@@ -268,6 +352,11 @@ class Context:
|
|
|
268
352
|
```
|
|
269
353
|
"""
|
|
270
354
|
request_ctx = self.request_context
|
|
355
|
+
if request_ctx is None:
|
|
356
|
+
raise RuntimeError(
|
|
357
|
+
"session_id is not available because the MCP session has not been established yet. "
|
|
358
|
+
"Check `context.request_context` for None before accessing this attribute."
|
|
359
|
+
)
|
|
271
360
|
session = request_ctx.session
|
|
272
361
|
|
|
273
362
|
# Try to get the session ID from the session attributes
|
|
@@ -287,12 +376,20 @@ class Context:
|
|
|
287
376
|
session_id = str(uuid4())
|
|
288
377
|
|
|
289
378
|
# Save the session id to the session attributes
|
|
290
|
-
|
|
379
|
+
session._fastmcp_id = session_id
|
|
291
380
|
return session_id
|
|
292
381
|
|
|
293
382
|
@property
|
|
294
383
|
def session(self) -> ServerSession:
|
|
295
|
-
"""Access to the underlying session for advanced usage.
|
|
384
|
+
"""Access to the underlying session for advanced usage.
|
|
385
|
+
|
|
386
|
+
Raises RuntimeError if MCP request context is not available.
|
|
387
|
+
"""
|
|
388
|
+
if self.request_context is None:
|
|
389
|
+
raise RuntimeError(
|
|
390
|
+
"session is not available because the MCP session has not been established yet. "
|
|
391
|
+
"Check `context.request_context` for None before accessing this attribute."
|
|
392
|
+
)
|
|
296
393
|
return self.request_context.session
|
|
297
394
|
|
|
298
395
|
# Convenience methods for common log levels
|
|
@@ -302,9 +399,14 @@ class Context:
|
|
|
302
399
|
logger_name: str | None = None,
|
|
303
400
|
extra: Mapping[str, Any] | None = None,
|
|
304
401
|
) -> None:
|
|
305
|
-
"""Send a
|
|
402
|
+
"""Send a `DEBUG`-level message to the connected MCP Client.
|
|
403
|
+
|
|
404
|
+
Messages sent to Clients are also logged to the `fastmcp.server.context.to_client` logger with a level of `DEBUG`."""
|
|
306
405
|
await self.log(
|
|
307
|
-
level="debug",
|
|
406
|
+
level="debug",
|
|
407
|
+
message=message,
|
|
408
|
+
logger_name=logger_name,
|
|
409
|
+
extra=extra,
|
|
308
410
|
)
|
|
309
411
|
|
|
310
412
|
async def info(
|
|
@@ -313,9 +415,14 @@ class Context:
|
|
|
313
415
|
logger_name: str | None = None,
|
|
314
416
|
extra: Mapping[str, Any] | None = None,
|
|
315
417
|
) -> None:
|
|
316
|
-
"""Send
|
|
418
|
+
"""Send a `INFO`-level message to the connected MCP Client.
|
|
419
|
+
|
|
420
|
+
Messages sent to Clients are also logged to the `fastmcp.server.context.to_client` logger with a level of `DEBUG`."""
|
|
317
421
|
await self.log(
|
|
318
|
-
level="info",
|
|
422
|
+
level="info",
|
|
423
|
+
message=message,
|
|
424
|
+
logger_name=logger_name,
|
|
425
|
+
extra=extra,
|
|
319
426
|
)
|
|
320
427
|
|
|
321
428
|
async def warning(
|
|
@@ -324,9 +431,14 @@ class Context:
|
|
|
324
431
|
logger_name: str | None = None,
|
|
325
432
|
extra: Mapping[str, Any] | None = None,
|
|
326
433
|
) -> None:
|
|
327
|
-
"""Send a
|
|
434
|
+
"""Send a `WARNING`-level message to the connected MCP Client.
|
|
435
|
+
|
|
436
|
+
Messages sent to Clients are also logged to the `fastmcp.server.context.to_client` logger with a level of `DEBUG`."""
|
|
328
437
|
await self.log(
|
|
329
|
-
level="warning",
|
|
438
|
+
level="warning",
|
|
439
|
+
message=message,
|
|
440
|
+
logger_name=logger_name,
|
|
441
|
+
extra=extra,
|
|
330
442
|
)
|
|
331
443
|
|
|
332
444
|
async def error(
|
|
@@ -335,9 +447,14 @@ class Context:
|
|
|
335
447
|
logger_name: str | None = None,
|
|
336
448
|
extra: Mapping[str, Any] | None = None,
|
|
337
449
|
) -> None:
|
|
338
|
-
"""Send
|
|
450
|
+
"""Send a `ERROR`-level message to the connected MCP Client.
|
|
451
|
+
|
|
452
|
+
Messages sent to Clients are also logged to the `fastmcp.server.context.to_client` logger with a level of `DEBUG`."""
|
|
339
453
|
await self.log(
|
|
340
|
-
level="error",
|
|
454
|
+
level="error",
|
|
455
|
+
message=message,
|
|
456
|
+
logger_name=logger_name,
|
|
457
|
+
extra=extra,
|
|
341
458
|
)
|
|
342
459
|
|
|
343
460
|
async def list_roots(self) -> list[Root]:
|
|
@@ -359,13 +476,13 @@ class Context:
|
|
|
359
476
|
|
|
360
477
|
async def sample(
|
|
361
478
|
self,
|
|
362
|
-
messages: str |
|
|
479
|
+
messages: str | Sequence[str | SamplingMessage],
|
|
363
480
|
system_prompt: str | None = None,
|
|
364
481
|
include_context: IncludeContext | None = None,
|
|
365
482
|
temperature: float | None = None,
|
|
366
483
|
max_tokens: int | None = None,
|
|
367
484
|
model_preferences: ModelPreferences | str | list[str] | None = None,
|
|
368
|
-
) ->
|
|
485
|
+
) -> TextContent | ImageContent | AudioContent:
|
|
369
486
|
"""
|
|
370
487
|
Send a sampling request to the client and await the response.
|
|
371
488
|
|
|
@@ -383,7 +500,7 @@ class Context:
|
|
|
383
500
|
content=TextContent(text=messages, type="text"), role="user"
|
|
384
501
|
)
|
|
385
502
|
]
|
|
386
|
-
elif isinstance(messages,
|
|
503
|
+
elif isinstance(messages, Sequence):
|
|
387
504
|
sampling_messages = [
|
|
388
505
|
SamplingMessage(content=TextContent(text=m, type="text"), role="user")
|
|
389
506
|
if isinstance(m, str)
|
|
@@ -449,7 +566,7 @@ class Context:
|
|
|
449
566
|
AcceptedElicitation[dict[str, Any]] | DeclinedElicitation | CancelledElicitation
|
|
450
567
|
): ...
|
|
451
568
|
|
|
452
|
-
"""When response_type is None, the accepted
|
|
569
|
+
"""When response_type is None, the accepted elicitation will contain an
|
|
453
570
|
empty dict"""
|
|
454
571
|
|
|
455
572
|
@overload
|
|
@@ -459,7 +576,7 @@ class Context:
|
|
|
459
576
|
response_type: type[T],
|
|
460
577
|
) -> AcceptedElicitation[T] | DeclinedElicitation | CancelledElicitation: ...
|
|
461
578
|
|
|
462
|
-
"""When response_type is not None, the accepted
|
|
579
|
+
"""When response_type is not None, the accepted elicitation will contain the
|
|
463
580
|
response data"""
|
|
464
581
|
|
|
465
582
|
@overload
|
|
@@ -469,7 +586,7 @@ class Context:
|
|
|
469
586
|
response_type: list[str],
|
|
470
587
|
) -> AcceptedElicitation[str] | DeclinedElicitation | CancelledElicitation: ...
|
|
471
588
|
|
|
472
|
-
"""When response_type is a list of strings, the accepted
|
|
589
|
+
"""When response_type is a list of strings, the accepted elicitation will
|
|
473
590
|
contain the selected string response"""
|
|
474
591
|
|
|
475
592
|
async def elicit(
|
|
@@ -520,13 +637,11 @@ class Context:
|
|
|
520
637
|
choice_literal = Literal[tuple(response_type)] # type: ignore
|
|
521
638
|
response_type = ScalarElicitationType[choice_literal] # type: ignore
|
|
522
639
|
# if the user provided a primitive scalar, wrap it in an object schema
|
|
523
|
-
elif
|
|
524
|
-
response_type
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
# if the user provided an Enum type, wrap it in an object schema
|
|
529
|
-
elif isinstance(response_type, type) and issubclass(response_type, Enum):
|
|
640
|
+
elif (
|
|
641
|
+
response_type in {bool, int, float, str}
|
|
642
|
+
or get_origin(response_type) is Literal
|
|
643
|
+
or (isinstance(response_type, type) and issubclass(response_type, Enum))
|
|
644
|
+
):
|
|
530
645
|
response_type = ScalarElicitationType[response_type] # type: ignore
|
|
531
646
|
|
|
532
647
|
response_type = cast(type[T], response_type)
|
|
@@ -573,7 +688,7 @@ class Context:
|
|
|
573
688
|
warnings.warn(
|
|
574
689
|
"Context.get_http_request() is deprecated and will be removed in a future version. "
|
|
575
690
|
"Use get_http_request() from fastmcp.server.dependencies instead. "
|
|
576
|
-
"See https://gofastmcp.com/
|
|
691
|
+
"See https://gofastmcp.com/servers/context#http-requests for more details.",
|
|
577
692
|
DeprecationWarning,
|
|
578
693
|
stacklevel=2,
|
|
579
694
|
)
|
|
@@ -591,30 +706,14 @@ class Context:
|
|
|
591
706
|
def _queue_tool_list_changed(self) -> None:
|
|
592
707
|
"""Queue a tool list changed notification."""
|
|
593
708
|
self._notification_queue.add("notifications/tools/list_changed")
|
|
594
|
-
self._try_flush_notifications()
|
|
595
709
|
|
|
596
710
|
def _queue_resource_list_changed(self) -> None:
|
|
597
711
|
"""Queue a resource list changed notification."""
|
|
598
712
|
self._notification_queue.add("notifications/resources/list_changed")
|
|
599
|
-
self._try_flush_notifications()
|
|
600
713
|
|
|
601
714
|
def _queue_prompt_list_changed(self) -> None:
|
|
602
715
|
"""Queue a prompt list changed notification."""
|
|
603
716
|
self._notification_queue.add("notifications/prompts/list_changed")
|
|
604
|
-
self._try_flush_notifications()
|
|
605
|
-
|
|
606
|
-
def _try_flush_notifications(self) -> None:
|
|
607
|
-
"""Synchronous method that attempts to flush notifications if we're in an async context."""
|
|
608
|
-
try:
|
|
609
|
-
# Check if we're in an async context
|
|
610
|
-
loop = asyncio.get_running_loop()
|
|
611
|
-
if loop and not loop.is_running():
|
|
612
|
-
return
|
|
613
|
-
# Schedule flush as a task (fire-and-forget)
|
|
614
|
-
asyncio.create_task(self._flush_notifications())
|
|
615
|
-
except RuntimeError:
|
|
616
|
-
# No event loop - will flush later
|
|
617
|
-
pass
|
|
618
717
|
|
|
619
718
|
async def _flush_notifications(self) -> None:
|
|
620
719
|
"""Send all queued notifications."""
|
|
@@ -674,3 +773,31 @@ def _parse_model_preferences(
|
|
|
674
773
|
raise ValueError(
|
|
675
774
|
"model_preferences must be one of: ModelPreferences, str, list[str], or None."
|
|
676
775
|
)
|
|
776
|
+
|
|
777
|
+
|
|
778
|
+
async def _log_to_server_and_client(
|
|
779
|
+
data: LogData,
|
|
780
|
+
session: ServerSession,
|
|
781
|
+
level: LoggingLevel,
|
|
782
|
+
logger_name: str | None = None,
|
|
783
|
+
related_request_id: str | None = None,
|
|
784
|
+
) -> None:
|
|
785
|
+
"""Log a message to the server and client."""
|
|
786
|
+
|
|
787
|
+
msg_prefix = f"Sending {level.upper()} to client"
|
|
788
|
+
|
|
789
|
+
if logger_name:
|
|
790
|
+
msg_prefix += f" ({logger_name})"
|
|
791
|
+
|
|
792
|
+
to_client_logger.log(
|
|
793
|
+
level=_mcp_level_to_python_level[level],
|
|
794
|
+
msg=f"{msg_prefix}: {data.msg}",
|
|
795
|
+
extra=data.extra,
|
|
796
|
+
)
|
|
797
|
+
|
|
798
|
+
await session.send_log_message(
|
|
799
|
+
level=level,
|
|
800
|
+
data=data,
|
|
801
|
+
logger=logger_name,
|
|
802
|
+
related_request_id=related_request_id,
|
|
803
|
+
)
|
fastmcp/server/dependencies.py
CHANGED
|
@@ -1,23 +1,30 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
import contextlib
|
|
3
4
|
from typing import TYPE_CHECKING
|
|
4
5
|
|
|
5
6
|
from mcp.server.auth.middleware.auth_context import (
|
|
6
7
|
get_access_token as _sdk_get_access_token,
|
|
7
8
|
)
|
|
9
|
+
from mcp.server.auth.middleware.bearer_auth import AuthenticatedUser
|
|
10
|
+
from mcp.server.auth.provider import (
|
|
11
|
+
AccessToken as _SDKAccessToken,
|
|
12
|
+
)
|
|
13
|
+
from mcp.server.lowlevel.server import request_ctx
|
|
8
14
|
from starlette.requests import Request
|
|
9
15
|
|
|
10
16
|
from fastmcp.server.auth import AccessToken
|
|
17
|
+
from fastmcp.server.http import _current_http_request
|
|
11
18
|
|
|
12
19
|
if TYPE_CHECKING:
|
|
13
20
|
from fastmcp.server.context import Context
|
|
14
21
|
|
|
15
22
|
__all__ = [
|
|
23
|
+
"AccessToken",
|
|
24
|
+
"get_access_token",
|
|
16
25
|
"get_context",
|
|
17
|
-
"get_http_request",
|
|
18
26
|
"get_http_headers",
|
|
19
|
-
"
|
|
20
|
-
"AccessToken",
|
|
27
|
+
"get_http_request",
|
|
21
28
|
]
|
|
22
29
|
|
|
23
30
|
|
|
@@ -37,13 +44,15 @@ def get_context() -> Context:
|
|
|
37
44
|
|
|
38
45
|
|
|
39
46
|
def get_http_request() -> Request:
|
|
40
|
-
|
|
41
|
-
|
|
47
|
+
# Try MCP SDK's request_ctx first (set during normal MCP request handling)
|
|
42
48
|
request = None
|
|
43
|
-
|
|
49
|
+
with contextlib.suppress(LookupError):
|
|
44
50
|
request = request_ctx.get().request
|
|
45
|
-
|
|
46
|
-
|
|
51
|
+
|
|
52
|
+
# Fallback to FastMCP's HTTP context variable
|
|
53
|
+
# This is needed during `on_initialize` middleware where request_ctx isn't set yet
|
|
54
|
+
if request is None:
|
|
55
|
+
request = _current_http_request.get()
|
|
47
56
|
|
|
48
57
|
if request is None:
|
|
49
58
|
raise RuntimeError("No active HTTP request found.")
|
|
@@ -103,21 +112,52 @@ def get_access_token() -> AccessToken | None:
|
|
|
103
112
|
"""
|
|
104
113
|
Get the FastMCP access token from the current context.
|
|
105
114
|
|
|
115
|
+
This function first tries to get the token from the current HTTP request's scope,
|
|
116
|
+
which is more reliable for long-lived connections where the SDK's auth_context_var
|
|
117
|
+
may become stale after token refresh. Falls back to the SDK's context var if no
|
|
118
|
+
request is available.
|
|
119
|
+
|
|
106
120
|
Returns:
|
|
107
121
|
The access token if an authenticated user is available, None otherwise.
|
|
108
122
|
"""
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
123
|
+
access_token: _SDKAccessToken | None = None
|
|
124
|
+
|
|
125
|
+
# First, try to get from current HTTP request's scope (issue #1863)
|
|
126
|
+
# This is more reliable than auth_context_var for Streamable HTTP sessions
|
|
127
|
+
# where tokens may be refreshed between MCP messages
|
|
128
|
+
try:
|
|
129
|
+
request = get_http_request()
|
|
130
|
+
user = request.scope.get("user")
|
|
131
|
+
if isinstance(user, AuthenticatedUser):
|
|
132
|
+
access_token = user.access_token
|
|
133
|
+
except RuntimeError:
|
|
134
|
+
# No HTTP request available, fall back to context var
|
|
135
|
+
pass
|
|
136
|
+
|
|
137
|
+
# Fall back to SDK's context var if we didn't get a token from the request
|
|
138
|
+
if access_token is None:
|
|
139
|
+
access_token = _sdk_get_access_token()
|
|
140
|
+
|
|
141
|
+
if access_token is None or isinstance(access_token, AccessToken):
|
|
142
|
+
return access_token
|
|
113
143
|
|
|
114
|
-
# If the object is not a FastMCP AccessToken, convert it to one if the
|
|
115
|
-
#
|
|
144
|
+
# If the object is not a FastMCP AccessToken, convert it to one if the
|
|
145
|
+
# fields are compatible (e.g. `claims` is not present in the SDK's AccessToken).
|
|
146
|
+
# This is a workaround for the case where the SDK or auth provider returns a different type
|
|
116
147
|
# If it fails, it will raise a TypeError
|
|
117
148
|
try:
|
|
118
|
-
|
|
149
|
+
access_token_as_dict = access_token.model_dump()
|
|
150
|
+
return AccessToken(
|
|
151
|
+
token=access_token_as_dict["token"],
|
|
152
|
+
client_id=access_token_as_dict["client_id"],
|
|
153
|
+
scopes=access_token_as_dict["scopes"],
|
|
154
|
+
# Optional fields
|
|
155
|
+
expires_at=access_token_as_dict.get("expires_at"),
|
|
156
|
+
resource_owner=access_token_as_dict.get("resource_owner"),
|
|
157
|
+
claims=access_token_as_dict.get("claims"),
|
|
158
|
+
)
|
|
119
159
|
except Exception as e:
|
|
120
160
|
raise TypeError(
|
|
121
|
-
f"Expected fastmcp.server.auth.auth.AccessToken, got {type(
|
|
161
|
+
f"Expected fastmcp.server.auth.auth.AccessToken, got {type(access_token).__name__}. "
|
|
122
162
|
"Ensure the SDK is using the correct AccessToken type."
|
|
123
163
|
) from e
|