fastmcp 2.12.5__py3-none-any.whl → 2.14.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/__init__.py +2 -23
- fastmcp/cli/__init__.py +0 -3
- fastmcp/cli/__main__.py +5 -0
- fastmcp/cli/cli.py +19 -33
- fastmcp/cli/install/claude_code.py +6 -6
- fastmcp/cli/install/claude_desktop.py +3 -3
- fastmcp/cli/install/cursor.py +18 -12
- fastmcp/cli/install/gemini_cli.py +3 -3
- fastmcp/cli/install/mcp_json.py +3 -3
- fastmcp/cli/install/shared.py +0 -15
- fastmcp/cli/run.py +13 -8
- fastmcp/cli/tasks.py +110 -0
- fastmcp/client/__init__.py +9 -9
- fastmcp/client/auth/oauth.py +123 -225
- fastmcp/client/client.py +697 -95
- fastmcp/client/elicitation.py +11 -5
- fastmcp/client/logging.py +18 -14
- fastmcp/client/messages.py +7 -5
- fastmcp/client/oauth_callback.py +85 -171
- fastmcp/client/roots.py +2 -1
- fastmcp/client/sampling.py +1 -1
- fastmcp/client/tasks.py +614 -0
- fastmcp/client/transports.py +117 -30
- fastmcp/contrib/component_manager/__init__.py +1 -1
- fastmcp/contrib/component_manager/component_manager.py +2 -2
- fastmcp/contrib/component_manager/component_service.py +10 -26
- fastmcp/contrib/mcp_mixin/README.md +32 -1
- fastmcp/contrib/mcp_mixin/__init__.py +2 -2
- fastmcp/contrib/mcp_mixin/mcp_mixin.py +14 -2
- fastmcp/dependencies.py +25 -0
- fastmcp/experimental/sampling/handlers/openai.py +3 -3
- fastmcp/experimental/server/openapi/__init__.py +20 -21
- fastmcp/experimental/utilities/openapi/__init__.py +16 -47
- fastmcp/mcp_config.py +3 -4
- fastmcp/prompts/__init__.py +1 -1
- fastmcp/prompts/prompt.py +54 -51
- fastmcp/prompts/prompt_manager.py +16 -101
- fastmcp/resources/__init__.py +5 -5
- fastmcp/resources/resource.py +43 -21
- fastmcp/resources/resource_manager.py +9 -168
- fastmcp/resources/template.py +161 -61
- fastmcp/resources/types.py +30 -24
- fastmcp/server/__init__.py +1 -1
- fastmcp/server/auth/__init__.py +9 -14
- fastmcp/server/auth/auth.py +197 -46
- 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 +1469 -298
- fastmcp/server/auth/oidc_proxy.py +91 -20
- fastmcp/server/auth/providers/auth0.py +40 -21
- fastmcp/server/auth/providers/aws.py +29 -3
- fastmcp/server/auth/providers/azure.py +312 -131
- fastmcp/server/auth/providers/debug.py +114 -0
- fastmcp/server/auth/providers/descope.py +86 -29
- fastmcp/server/auth/providers/discord.py +308 -0
- fastmcp/server/auth/providers/github.py +29 -8
- fastmcp/server/auth/providers/google.py +48 -9
- fastmcp/server/auth/providers/in_memory.py +29 -5
- 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 +35 -17
- fastmcp/server/context.py +236 -116
- fastmcp/server/dependencies.py +503 -18
- fastmcp/server/elicitation.py +286 -48
- fastmcp/server/event_store.py +177 -0
- fastmcp/server/http.py +71 -20
- fastmcp/server/low_level.py +165 -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 +50 -39
- fastmcp/server/middleware/middleware.py +29 -16
- fastmcp/server/middleware/rate_limiting.py +3 -3
- fastmcp/server/middleware/tool_injection.py +116 -0
- fastmcp/server/openapi/__init__.py +35 -0
- fastmcp/{experimental/server → server}/openapi/components.py +15 -10
- fastmcp/{experimental/server → server}/openapi/routing.py +3 -3
- fastmcp/{experimental/server → server}/openapi/server.py +6 -5
- fastmcp/server/proxy.py +72 -48
- fastmcp/server/server.py +1415 -733
- fastmcp/server/tasks/__init__.py +21 -0
- fastmcp/server/tasks/capabilities.py +22 -0
- fastmcp/server/tasks/config.py +89 -0
- fastmcp/server/tasks/converters.py +205 -0
- fastmcp/server/tasks/handlers.py +356 -0
- fastmcp/server/tasks/keys.py +93 -0
- fastmcp/server/tasks/protocol.py +355 -0
- fastmcp/server/tasks/subscriptions.py +205 -0
- fastmcp/settings.py +125 -113
- fastmcp/tools/__init__.py +1 -1
- fastmcp/tools/tool.py +138 -55
- fastmcp/tools/tool_manager.py +30 -112
- fastmcp/tools/tool_transform.py +12 -21
- fastmcp/utilities/cli.py +67 -28
- fastmcp/utilities/components.py +10 -5
- fastmcp/utilities/inspect.py +79 -23
- fastmcp/utilities/json_schema.py +4 -4
- fastmcp/utilities/json_schema_type.py +8 -8
- fastmcp/utilities/logging.py +118 -8
- fastmcp/utilities/mcp_config.py +1 -2
- 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 +6 -6
- fastmcp/utilities/mcp_server_config/v1/mcp_server_config.py +5 -5
- fastmcp/utilities/mcp_server_config/v1/schema.json +3 -0
- fastmcp/utilities/mcp_server_config/v1/sources/base.py +0 -1
- fastmcp/{experimental/utilities → utilities}/openapi/README.md +7 -35
- fastmcp/utilities/openapi/__init__.py +63 -0
- fastmcp/{experimental/utilities → utilities}/openapi/director.py +14 -15
- fastmcp/{experimental/utilities → utilities}/openapi/formatters.py +5 -5
- fastmcp/{experimental/utilities → utilities}/openapi/json_schema_converter.py +7 -3
- fastmcp/{experimental/utilities → utilities}/openapi/parser.py +37 -16
- fastmcp/utilities/tests.py +92 -5
- fastmcp/utilities/types.py +86 -16
- fastmcp/utilities/ui.py +626 -0
- {fastmcp-2.12.5.dist-info → fastmcp-2.14.0.dist-info}/METADATA +24 -15
- fastmcp-2.14.0.dist-info/RECORD +156 -0
- {fastmcp-2.12.5.dist-info → fastmcp-2.14.0.dist-info}/WHEEL +1 -1
- fastmcp/cli/claude.py +0 -135
- fastmcp/server/auth/providers/bearer.py +0 -25
- fastmcp/server/openapi.py +0 -1083
- fastmcp/utilities/openapi.py +0 -1568
- fastmcp/utilities/storage.py +0 -204
- fastmcp-2.12.5.dist-info/RECORD +0 -134
- fastmcp/{experimental/server → server}/openapi/README.md +0 -0
- fastmcp/{experimental/utilities → utilities}/openapi/models.py +3 -3
- fastmcp/{experimental/utilities → utilities}/openapi/schemas.py +2 -2
- {fastmcp-2.12.5.dist-info → fastmcp-2.14.0.dist-info}/entry_points.txt +0 -0
- {fastmcp-2.12.5.dist-info → fastmcp-2.14.0.dist-info}/licenses/LICENSE +0 -0
fastmcp/server/context.py
CHANGED
|
@@ -1,57 +1,64 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
import asyncio
|
|
4
3
|
import copy
|
|
5
4
|
import inspect
|
|
6
|
-
import
|
|
5
|
+
import logging
|
|
7
6
|
import weakref
|
|
8
7
|
from collections.abc import Generator, Mapping, Sequence
|
|
9
8
|
from contextlib import contextmanager
|
|
10
9
|
from contextvars import ContextVar, Token
|
|
11
10
|
from dataclasses import dataclass
|
|
12
|
-
from
|
|
13
|
-
from typing import Any,
|
|
11
|
+
from logging import Logger
|
|
12
|
+
from typing import Any, overload
|
|
14
13
|
|
|
14
|
+
import anyio
|
|
15
15
|
from mcp import LoggingLevel, ServerSession
|
|
16
16
|
from mcp.server.lowlevel.helper_types import ReadResourceContents
|
|
17
17
|
from mcp.server.lowlevel.server import request_ctx
|
|
18
18
|
from mcp.shared.context import RequestContext
|
|
19
19
|
from mcp.types import (
|
|
20
|
-
AudioContent,
|
|
21
20
|
ClientCapabilities,
|
|
22
21
|
CreateMessageResult,
|
|
23
|
-
|
|
22
|
+
GetPromptResult,
|
|
24
23
|
IncludeContext,
|
|
25
24
|
ModelHint,
|
|
26
25
|
ModelPreferences,
|
|
27
26
|
Root,
|
|
28
27
|
SamplingCapability,
|
|
29
28
|
SamplingMessage,
|
|
29
|
+
SamplingMessageContentBlock,
|
|
30
30
|
TextContent,
|
|
31
31
|
)
|
|
32
32
|
from mcp.types import CreateMessageRequestParams as SamplingParams
|
|
33
|
+
from mcp.types import Prompt as MCPPrompt
|
|
34
|
+
from mcp.types import Resource as MCPResource
|
|
33
35
|
from pydantic.networks import AnyUrl
|
|
34
36
|
from starlette.requests import Request
|
|
35
37
|
from typing_extensions import TypeVar
|
|
36
38
|
|
|
37
|
-
import fastmcp.server.dependencies
|
|
38
|
-
from fastmcp import settings
|
|
39
39
|
from fastmcp.server.elicitation import (
|
|
40
40
|
AcceptedElicitation,
|
|
41
41
|
CancelledElicitation,
|
|
42
42
|
DeclinedElicitation,
|
|
43
|
-
|
|
44
|
-
|
|
43
|
+
handle_elicit_accept,
|
|
44
|
+
parse_elicit_response_type,
|
|
45
45
|
)
|
|
46
46
|
from fastmcp.server.server import FastMCP
|
|
47
|
-
from fastmcp.utilities.logging import get_logger
|
|
48
|
-
|
|
47
|
+
from fastmcp.utilities.logging import _clamp_logger, get_logger
|
|
48
|
+
|
|
49
|
+
logger: Logger = get_logger(name=__name__)
|
|
50
|
+
to_client_logger: Logger = logger.getChild(suffix="to_client")
|
|
51
|
+
|
|
52
|
+
# Convert all levels of server -> client messages to debug level
|
|
53
|
+
# This clamp can be undone at runtime by calling `_unclamp_logger` or calling
|
|
54
|
+
# `_clamp_logger` with a different max level.
|
|
55
|
+
_clamp_logger(logger=to_client_logger, max_level="DEBUG")
|
|
49
56
|
|
|
50
|
-
logger = get_logger(__name__)
|
|
51
57
|
|
|
52
58
|
T = TypeVar("T", default=Any)
|
|
59
|
+
|
|
53
60
|
_current_context: ContextVar[Context | None] = ContextVar("context", default=None) # type: ignore[assignment]
|
|
54
|
-
_flush_lock =
|
|
61
|
+
_flush_lock = anyio.Lock()
|
|
55
62
|
|
|
56
63
|
|
|
57
64
|
@dataclass
|
|
@@ -66,6 +73,18 @@ class LogData:
|
|
|
66
73
|
extra: Mapping[str, Any] | None = None
|
|
67
74
|
|
|
68
75
|
|
|
76
|
+
_mcp_level_to_python_level = {
|
|
77
|
+
"debug": logging.DEBUG,
|
|
78
|
+
"info": logging.INFO,
|
|
79
|
+
"notice": logging.INFO,
|
|
80
|
+
"warning": logging.WARNING,
|
|
81
|
+
"error": logging.ERROR,
|
|
82
|
+
"critical": logging.CRITICAL,
|
|
83
|
+
"alert": logging.CRITICAL,
|
|
84
|
+
"emergency": logging.CRITICAL,
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
|
|
69
88
|
@contextmanager
|
|
70
89
|
def set_context(context: Context) -> Generator[Context, None, None]:
|
|
71
90
|
token = _current_context.set(context)
|
|
@@ -145,6 +164,12 @@ class Context:
|
|
|
145
164
|
# Always set this context and save the token
|
|
146
165
|
token = _current_context.set(self)
|
|
147
166
|
self._tokens.append(token)
|
|
167
|
+
|
|
168
|
+
# Set current server for dependency injection (use weakref to avoid reference cycles)
|
|
169
|
+
from fastmcp.server.dependencies import _current_server
|
|
170
|
+
|
|
171
|
+
self._server_token = _current_server.set(weakref.ref(self.fastmcp))
|
|
172
|
+
|
|
148
173
|
return self
|
|
149
174
|
|
|
150
175
|
async def __aexit__(self, exc_type, exc_val, exc_tb) -> None:
|
|
@@ -152,20 +177,46 @@ class Context:
|
|
|
152
177
|
# Flush any remaining notifications before exiting
|
|
153
178
|
await self._flush_notifications()
|
|
154
179
|
|
|
180
|
+
# Reset server token
|
|
181
|
+
if hasattr(self, "_server_token"):
|
|
182
|
+
from fastmcp.server.dependencies import _current_server
|
|
183
|
+
|
|
184
|
+
_current_server.reset(self._server_token)
|
|
185
|
+
delattr(self, "_server_token")
|
|
186
|
+
|
|
187
|
+
# Reset context token
|
|
155
188
|
if self._tokens:
|
|
156
189
|
token = self._tokens.pop()
|
|
157
190
|
_current_context.reset(token)
|
|
158
191
|
|
|
159
192
|
@property
|
|
160
|
-
def request_context(self) -> RequestContext[ServerSession, Any, Request]:
|
|
193
|
+
def request_context(self) -> RequestContext[ServerSession, Any, Request] | None:
|
|
161
194
|
"""Access to the underlying request context.
|
|
162
195
|
|
|
163
|
-
|
|
196
|
+
Returns None when the MCP session has not been established yet.
|
|
197
|
+
Returns the full RequestContext once the MCP session is available.
|
|
198
|
+
|
|
199
|
+
For HTTP request access in middleware, use `get_http_request()` from fastmcp.server.dependencies,
|
|
200
|
+
which works whether or not the MCP session is available.
|
|
201
|
+
|
|
202
|
+
Example in middleware:
|
|
203
|
+
```python
|
|
204
|
+
async def on_request(self, context, call_next):
|
|
205
|
+
ctx = context.fastmcp_context
|
|
206
|
+
if ctx.request_context:
|
|
207
|
+
# MCP session available - can access session_id, request_id, etc.
|
|
208
|
+
session_id = ctx.session_id
|
|
209
|
+
else:
|
|
210
|
+
# MCP session not available yet - use HTTP helpers
|
|
211
|
+
from fastmcp.server.dependencies import get_http_request
|
|
212
|
+
request = get_http_request()
|
|
213
|
+
return await call_next(context)
|
|
214
|
+
```
|
|
164
215
|
"""
|
|
165
216
|
try:
|
|
166
217
|
return request_ctx.get()
|
|
167
218
|
except LookupError:
|
|
168
|
-
|
|
219
|
+
return None
|
|
169
220
|
|
|
170
221
|
async def report_progress(
|
|
171
222
|
self, progress: float, total: float | None = None, message: str | None = None
|
|
@@ -179,7 +230,7 @@ class Context:
|
|
|
179
230
|
|
|
180
231
|
progress_token = (
|
|
181
232
|
self.request_context.meta.progressToken
|
|
182
|
-
if self.request_context.meta
|
|
233
|
+
if self.request_context and self.request_context.meta
|
|
183
234
|
else None
|
|
184
235
|
)
|
|
185
236
|
|
|
@@ -194,6 +245,36 @@ class Context:
|
|
|
194
245
|
related_request_id=self.request_id,
|
|
195
246
|
)
|
|
196
247
|
|
|
248
|
+
async def list_resources(self) -> list[MCPResource]:
|
|
249
|
+
"""List all available resources from the server.
|
|
250
|
+
|
|
251
|
+
Returns:
|
|
252
|
+
List of Resource objects available on the server
|
|
253
|
+
"""
|
|
254
|
+
return await self.fastmcp._list_resources_mcp()
|
|
255
|
+
|
|
256
|
+
async def list_prompts(self) -> list[MCPPrompt]:
|
|
257
|
+
"""List all available prompts from the server.
|
|
258
|
+
|
|
259
|
+
Returns:
|
|
260
|
+
List of Prompt objects available on the server
|
|
261
|
+
"""
|
|
262
|
+
return await self.fastmcp._list_prompts_mcp()
|
|
263
|
+
|
|
264
|
+
async def get_prompt(
|
|
265
|
+
self, name: str, arguments: dict[str, Any] | None = None
|
|
266
|
+
) -> GetPromptResult:
|
|
267
|
+
"""Get a prompt by name with optional arguments.
|
|
268
|
+
|
|
269
|
+
Args:
|
|
270
|
+
name: The name of the prompt to get
|
|
271
|
+
arguments: Optional arguments to pass to the prompt
|
|
272
|
+
|
|
273
|
+
Returns:
|
|
274
|
+
The prompt result
|
|
275
|
+
"""
|
|
276
|
+
return await self.fastmcp._get_prompt_mcp(name, arguments)
|
|
277
|
+
|
|
197
278
|
async def read_resource(self, uri: str | AnyUrl) -> list[ReadResourceContents]:
|
|
198
279
|
"""Read a resource by URI.
|
|
199
280
|
|
|
@@ -203,9 +284,8 @@ class Context:
|
|
|
203
284
|
Returns:
|
|
204
285
|
The resource content as either text or bytes
|
|
205
286
|
"""
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
return await self.fastmcp._mcp_read_resource(uri)
|
|
287
|
+
# Context calls don't have task metadata, so always returns list
|
|
288
|
+
return await self.fastmcp._read_resource_mcp(uri) # type: ignore[return-value]
|
|
209
289
|
|
|
210
290
|
async def log(
|
|
211
291
|
self,
|
|
@@ -216,6 +296,8 @@ class Context:
|
|
|
216
296
|
) -> None:
|
|
217
297
|
"""Send a log message to the client.
|
|
218
298
|
|
|
299
|
+
Messages sent to Clients are also logged to the `fastmcp.server.context.to_client` logger with a level of `DEBUG`.
|
|
300
|
+
|
|
219
301
|
Args:
|
|
220
302
|
message: Log message
|
|
221
303
|
level: Optional log level. One of "debug", "info", "notice", "warning", "error", "critical",
|
|
@@ -223,13 +305,13 @@ class Context:
|
|
|
223
305
|
logger_name: Optional logger name
|
|
224
306
|
extra: Optional mapping for additional arguments
|
|
225
307
|
"""
|
|
226
|
-
if level is None:
|
|
227
|
-
level = "info"
|
|
228
308
|
data = LogData(msg=message, extra=extra)
|
|
229
|
-
|
|
230
|
-
|
|
309
|
+
|
|
310
|
+
await _log_to_server_and_client(
|
|
231
311
|
data=data,
|
|
232
|
-
|
|
312
|
+
session=self.session,
|
|
313
|
+
level=level or "info",
|
|
314
|
+
logger_name=logger_name,
|
|
233
315
|
related_request_id=self.request_id,
|
|
234
316
|
)
|
|
235
317
|
|
|
@@ -238,13 +320,21 @@ class Context:
|
|
|
238
320
|
"""Get the client ID if available."""
|
|
239
321
|
return (
|
|
240
322
|
getattr(self.request_context.meta, "client_id", None)
|
|
241
|
-
if self.request_context.meta
|
|
323
|
+
if self.request_context and self.request_context.meta
|
|
242
324
|
else None
|
|
243
325
|
)
|
|
244
326
|
|
|
245
327
|
@property
|
|
246
328
|
def request_id(self) -> str:
|
|
247
|
-
"""Get the unique ID for this request.
|
|
329
|
+
"""Get the unique ID for this request.
|
|
330
|
+
|
|
331
|
+
Raises RuntimeError if MCP request context is not available.
|
|
332
|
+
"""
|
|
333
|
+
if self.request_context is None:
|
|
334
|
+
raise RuntimeError(
|
|
335
|
+
"request_id is not available because the MCP session has not been established yet. "
|
|
336
|
+
"Check `context.request_context` for None before accessing this attribute."
|
|
337
|
+
)
|
|
248
338
|
return str(self.request_context.request_id)
|
|
249
339
|
|
|
250
340
|
@property
|
|
@@ -259,6 +349,9 @@ class Context:
|
|
|
259
349
|
The session ID for StreamableHTTP transports, or a generated ID
|
|
260
350
|
for other transports.
|
|
261
351
|
|
|
352
|
+
Raises:
|
|
353
|
+
RuntimeError if MCP request context is not available.
|
|
354
|
+
|
|
262
355
|
Example:
|
|
263
356
|
```python
|
|
264
357
|
@server.tool
|
|
@@ -269,6 +362,11 @@ class Context:
|
|
|
269
362
|
```
|
|
270
363
|
"""
|
|
271
364
|
request_ctx = self.request_context
|
|
365
|
+
if request_ctx is None:
|
|
366
|
+
raise RuntimeError(
|
|
367
|
+
"session_id is not available because the MCP session has not been established yet. "
|
|
368
|
+
"Check `context.request_context` for None before accessing this attribute."
|
|
369
|
+
)
|
|
272
370
|
session = request_ctx.session
|
|
273
371
|
|
|
274
372
|
# Try to get the session ID from the session attributes
|
|
@@ -288,12 +386,20 @@ class Context:
|
|
|
288
386
|
session_id = str(uuid4())
|
|
289
387
|
|
|
290
388
|
# Save the session id to the session attributes
|
|
291
|
-
|
|
389
|
+
session._fastmcp_id = session_id # type: ignore[attr-defined]
|
|
292
390
|
return session_id
|
|
293
391
|
|
|
294
392
|
@property
|
|
295
393
|
def session(self) -> ServerSession:
|
|
296
|
-
"""Access to the underlying session for advanced usage.
|
|
394
|
+
"""Access to the underlying session for advanced usage.
|
|
395
|
+
|
|
396
|
+
Raises RuntimeError if MCP request context is not available.
|
|
397
|
+
"""
|
|
398
|
+
if self.request_context is None:
|
|
399
|
+
raise RuntimeError(
|
|
400
|
+
"session is not available because the MCP session has not been established yet. "
|
|
401
|
+
"Check `context.request_context` for None before accessing this attribute."
|
|
402
|
+
)
|
|
297
403
|
return self.request_context.session
|
|
298
404
|
|
|
299
405
|
# Convenience methods for common log levels
|
|
@@ -303,9 +409,14 @@ class Context:
|
|
|
303
409
|
logger_name: str | None = None,
|
|
304
410
|
extra: Mapping[str, Any] | None = None,
|
|
305
411
|
) -> None:
|
|
306
|
-
"""Send a
|
|
412
|
+
"""Send a `DEBUG`-level message to the connected MCP Client.
|
|
413
|
+
|
|
414
|
+
Messages sent to Clients are also logged to the `fastmcp.server.context.to_client` logger with a level of `DEBUG`."""
|
|
307
415
|
await self.log(
|
|
308
|
-
level="debug",
|
|
416
|
+
level="debug",
|
|
417
|
+
message=message,
|
|
418
|
+
logger_name=logger_name,
|
|
419
|
+
extra=extra,
|
|
309
420
|
)
|
|
310
421
|
|
|
311
422
|
async def info(
|
|
@@ -314,9 +425,14 @@ class Context:
|
|
|
314
425
|
logger_name: str | None = None,
|
|
315
426
|
extra: Mapping[str, Any] | None = None,
|
|
316
427
|
) -> None:
|
|
317
|
-
"""Send
|
|
428
|
+
"""Send a `INFO`-level message to the connected MCP Client.
|
|
429
|
+
|
|
430
|
+
Messages sent to Clients are also logged to the `fastmcp.server.context.to_client` logger with a level of `DEBUG`."""
|
|
318
431
|
await self.log(
|
|
319
|
-
level="info",
|
|
432
|
+
level="info",
|
|
433
|
+
message=message,
|
|
434
|
+
logger_name=logger_name,
|
|
435
|
+
extra=extra,
|
|
320
436
|
)
|
|
321
437
|
|
|
322
438
|
async def warning(
|
|
@@ -325,9 +441,14 @@ class Context:
|
|
|
325
441
|
logger_name: str | None = None,
|
|
326
442
|
extra: Mapping[str, Any] | None = None,
|
|
327
443
|
) -> None:
|
|
328
|
-
"""Send a
|
|
444
|
+
"""Send a `WARNING`-level message to the connected MCP Client.
|
|
445
|
+
|
|
446
|
+
Messages sent to Clients are also logged to the `fastmcp.server.context.to_client` logger with a level of `DEBUG`."""
|
|
329
447
|
await self.log(
|
|
330
|
-
level="warning",
|
|
448
|
+
level="warning",
|
|
449
|
+
message=message,
|
|
450
|
+
logger_name=logger_name,
|
|
451
|
+
extra=extra,
|
|
331
452
|
)
|
|
332
453
|
|
|
333
454
|
async def error(
|
|
@@ -336,9 +457,14 @@ class Context:
|
|
|
336
457
|
logger_name: str | None = None,
|
|
337
458
|
extra: Mapping[str, Any] | None = None,
|
|
338
459
|
) -> None:
|
|
339
|
-
"""Send
|
|
460
|
+
"""Send a `ERROR`-level message to the connected MCP Client.
|
|
461
|
+
|
|
462
|
+
Messages sent to Clients are also logged to the `fastmcp.server.context.to_client` logger with a level of `DEBUG`."""
|
|
340
463
|
await self.log(
|
|
341
|
-
level="error",
|
|
464
|
+
level="error",
|
|
465
|
+
message=message,
|
|
466
|
+
logger_name=logger_name,
|
|
467
|
+
extra=extra,
|
|
342
468
|
)
|
|
343
469
|
|
|
344
470
|
async def list_roots(self) -> list[Root]:
|
|
@@ -358,6 +484,45 @@ class Context:
|
|
|
358
484
|
"""Send a prompt list changed notification to the client."""
|
|
359
485
|
await self.session.send_prompt_list_changed()
|
|
360
486
|
|
|
487
|
+
async def close_sse_stream(self) -> None:
|
|
488
|
+
"""Close the current response stream to trigger client reconnection.
|
|
489
|
+
|
|
490
|
+
When using StreamableHTTP transport with an EventStore configured, this
|
|
491
|
+
method gracefully closes the HTTP connection for the current request.
|
|
492
|
+
The client will automatically reconnect (after `retry_interval` milliseconds)
|
|
493
|
+
and resume receiving events from where it left off via the EventStore.
|
|
494
|
+
|
|
495
|
+
This is useful for long-running operations to avoid load balancer timeouts.
|
|
496
|
+
Instead of holding a connection open for minutes, you can periodically close
|
|
497
|
+
and let the client reconnect.
|
|
498
|
+
|
|
499
|
+
Example:
|
|
500
|
+
```python
|
|
501
|
+
@mcp.tool
|
|
502
|
+
async def long_running_task(ctx: Context) -> str:
|
|
503
|
+
for i in range(100):
|
|
504
|
+
await ctx.report_progress(i, 100)
|
|
505
|
+
|
|
506
|
+
# Close connection every 30 iterations to avoid LB timeouts
|
|
507
|
+
if i % 30 == 0 and i > 0:
|
|
508
|
+
await ctx.close_sse_stream()
|
|
509
|
+
|
|
510
|
+
await do_work()
|
|
511
|
+
return "Done"
|
|
512
|
+
```
|
|
513
|
+
|
|
514
|
+
Note:
|
|
515
|
+
This is a no-op (with a debug log) if not using StreamableHTTP
|
|
516
|
+
transport with an EventStore configured.
|
|
517
|
+
"""
|
|
518
|
+
if not self.request_context or not self.request_context.close_sse_stream:
|
|
519
|
+
logger.debug(
|
|
520
|
+
"close_sse_stream() called but not applicable "
|
|
521
|
+
"(requires StreamableHTTP transport with event_store)"
|
|
522
|
+
)
|
|
523
|
+
return
|
|
524
|
+
await self.request_context.close_sse_stream()
|
|
525
|
+
|
|
361
526
|
async def sample(
|
|
362
527
|
self,
|
|
363
528
|
messages: str | Sequence[str | SamplingMessage],
|
|
@@ -366,7 +531,7 @@ class Context:
|
|
|
366
531
|
temperature: float | None = None,
|
|
367
532
|
max_tokens: int | None = None,
|
|
368
533
|
model_preferences: ModelPreferences | str | list[str] | None = None,
|
|
369
|
-
) ->
|
|
534
|
+
) -> SamplingMessageContentBlock | list[SamplingMessageContentBlock]:
|
|
370
535
|
"""
|
|
371
536
|
Send a sampling request to the client and await the response.
|
|
372
537
|
|
|
@@ -412,7 +577,7 @@ class Context:
|
|
|
412
577
|
maxTokens=max_tokens,
|
|
413
578
|
modelPreferences=_parse_model_preferences(model_preferences),
|
|
414
579
|
),
|
|
415
|
-
self.request_context,
|
|
580
|
+
self.request_context, # type: ignore[arg-type]
|
|
416
581
|
)
|
|
417
582
|
|
|
418
583
|
if inspect.isawaitable(create_message_result):
|
|
@@ -507,80 +672,23 @@ class Context:
|
|
|
507
672
|
type or dataclass or BaseModel. If it is a primitive type, an
|
|
508
673
|
object schema with a single "value" field will be generated.
|
|
509
674
|
"""
|
|
510
|
-
|
|
511
|
-
schema = {"type": "object", "properties": {}}
|
|
512
|
-
else:
|
|
513
|
-
# if the user provided a list of strings, treat it as a Literal
|
|
514
|
-
if isinstance(response_type, list):
|
|
515
|
-
if not all(isinstance(item, str) for item in response_type):
|
|
516
|
-
raise ValueError(
|
|
517
|
-
"List of options must be a list of strings. Received: "
|
|
518
|
-
f"{response_type}"
|
|
519
|
-
)
|
|
520
|
-
# Convert list of options to Literal type and wrap
|
|
521
|
-
choice_literal = Literal[tuple(response_type)] # type: ignore
|
|
522
|
-
response_type = ScalarElicitationType[choice_literal] # type: ignore
|
|
523
|
-
# if the user provided a primitive scalar, wrap it in an object schema
|
|
524
|
-
elif response_type in {bool, int, float, str}:
|
|
525
|
-
response_type = ScalarElicitationType[response_type] # type: ignore
|
|
526
|
-
# if the user provided a Literal type, wrap it in an object schema
|
|
527
|
-
elif get_origin(response_type) is Literal:
|
|
528
|
-
response_type = ScalarElicitationType[response_type] # type: ignore
|
|
529
|
-
# if the user provided an Enum type, wrap it in an object schema
|
|
530
|
-
elif isinstance(response_type, type) and issubclass(response_type, Enum):
|
|
531
|
-
response_type = ScalarElicitationType[response_type] # type: ignore
|
|
532
|
-
|
|
533
|
-
response_type = cast(type[T], response_type)
|
|
534
|
-
|
|
535
|
-
schema = get_elicitation_schema(response_type)
|
|
675
|
+
config = parse_elicit_response_type(response_type)
|
|
536
676
|
|
|
537
677
|
result = await self.session.elicit(
|
|
538
678
|
message=message,
|
|
539
|
-
requestedSchema=schema,
|
|
679
|
+
requestedSchema=config.schema,
|
|
540
680
|
related_request_id=self.request_id,
|
|
541
681
|
)
|
|
542
682
|
|
|
543
683
|
if result.action == "accept":
|
|
544
|
-
|
|
545
|
-
type_adapter = get_cached_typeadapter(response_type)
|
|
546
|
-
validated_data = cast(
|
|
547
|
-
T | ScalarElicitationType[T],
|
|
548
|
-
type_adapter.validate_python(result.content),
|
|
549
|
-
)
|
|
550
|
-
if isinstance(validated_data, ScalarElicitationType):
|
|
551
|
-
return AcceptedElicitation[T](data=validated_data.value)
|
|
552
|
-
else:
|
|
553
|
-
return AcceptedElicitation[T](data=cast(T, validated_data))
|
|
554
|
-
elif result.content:
|
|
555
|
-
raise ValueError(
|
|
556
|
-
"Elicitation expected an empty response, but received: "
|
|
557
|
-
f"{result.content}"
|
|
558
|
-
)
|
|
559
|
-
else:
|
|
560
|
-
return AcceptedElicitation[dict[str, Any]](data={})
|
|
684
|
+
return handle_elicit_accept(config, result.content)
|
|
561
685
|
elif result.action == "decline":
|
|
562
686
|
return DeclinedElicitation()
|
|
563
687
|
elif result.action == "cancel":
|
|
564
688
|
return CancelledElicitation()
|
|
565
689
|
else:
|
|
566
|
-
# This should never happen, but handle it just in case
|
|
567
690
|
raise ValueError(f"Unexpected elicitation action: {result.action}")
|
|
568
691
|
|
|
569
|
-
def get_http_request(self) -> Request:
|
|
570
|
-
"""Get the active starlette request."""
|
|
571
|
-
|
|
572
|
-
# Deprecated in 2.2.11
|
|
573
|
-
if settings.deprecation_warnings:
|
|
574
|
-
warnings.warn(
|
|
575
|
-
"Context.get_http_request() is deprecated and will be removed in a future version. "
|
|
576
|
-
"Use get_http_request() from fastmcp.server.dependencies instead. "
|
|
577
|
-
"See https://gofastmcp.com/servers/context#http-requests for more details.",
|
|
578
|
-
DeprecationWarning,
|
|
579
|
-
stacklevel=2,
|
|
580
|
-
)
|
|
581
|
-
|
|
582
|
-
return fastmcp.server.dependencies.get_http_request()
|
|
583
|
-
|
|
584
692
|
def set_state(self, key: str, value: Any) -> None:
|
|
585
693
|
"""Set a value in the context state."""
|
|
586
694
|
self._state[key] = value
|
|
@@ -592,30 +700,14 @@ class Context:
|
|
|
592
700
|
def _queue_tool_list_changed(self) -> None:
|
|
593
701
|
"""Queue a tool list changed notification."""
|
|
594
702
|
self._notification_queue.add("notifications/tools/list_changed")
|
|
595
|
-
self._try_flush_notifications()
|
|
596
703
|
|
|
597
704
|
def _queue_resource_list_changed(self) -> None:
|
|
598
705
|
"""Queue a resource list changed notification."""
|
|
599
706
|
self._notification_queue.add("notifications/resources/list_changed")
|
|
600
|
-
self._try_flush_notifications()
|
|
601
707
|
|
|
602
708
|
def _queue_prompt_list_changed(self) -> None:
|
|
603
709
|
"""Queue a prompt list changed notification."""
|
|
604
710
|
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
711
|
|
|
620
712
|
async def _flush_notifications(self) -> None:
|
|
621
713
|
"""Send all queued notifications."""
|
|
@@ -675,3 +767,31 @@ def _parse_model_preferences(
|
|
|
675
767
|
raise ValueError(
|
|
676
768
|
"model_preferences must be one of: ModelPreferences, str, list[str], or None."
|
|
677
769
|
)
|
|
770
|
+
|
|
771
|
+
|
|
772
|
+
async def _log_to_server_and_client(
|
|
773
|
+
data: LogData,
|
|
774
|
+
session: ServerSession,
|
|
775
|
+
level: LoggingLevel,
|
|
776
|
+
logger_name: str | None = None,
|
|
777
|
+
related_request_id: str | None = None,
|
|
778
|
+
) -> None:
|
|
779
|
+
"""Log a message to the server and client."""
|
|
780
|
+
|
|
781
|
+
msg_prefix = f"Sending {level.upper()} to client"
|
|
782
|
+
|
|
783
|
+
if logger_name:
|
|
784
|
+
msg_prefix += f" ({logger_name})"
|
|
785
|
+
|
|
786
|
+
to_client_logger.log(
|
|
787
|
+
level=_mcp_level_to_python_level[level],
|
|
788
|
+
msg=f"{msg_prefix}: {data.msg}",
|
|
789
|
+
extra=data.extra,
|
|
790
|
+
)
|
|
791
|
+
|
|
792
|
+
await session.send_log_message(
|
|
793
|
+
level=level,
|
|
794
|
+
data=data,
|
|
795
|
+
logger=logger_name,
|
|
796
|
+
related_request_id=related_request_id,
|
|
797
|
+
)
|