fastmcp 2.14.4__py3-none-any.whl → 3.0.0b1__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/_vendor/__init__.py +1 -0
- fastmcp/_vendor/docket_di/README.md +7 -0
- fastmcp/_vendor/docket_di/__init__.py +163 -0
- fastmcp/cli/cli.py +112 -28
- fastmcp/cli/install/claude_code.py +1 -5
- fastmcp/cli/install/claude_desktop.py +1 -5
- fastmcp/cli/install/cursor.py +1 -5
- fastmcp/cli/install/gemini_cli.py +1 -5
- fastmcp/cli/install/mcp_json.py +1 -6
- fastmcp/cli/run.py +146 -5
- fastmcp/client/__init__.py +7 -9
- fastmcp/client/auth/oauth.py +18 -17
- fastmcp/client/client.py +100 -870
- fastmcp/client/elicitation.py +1 -1
- fastmcp/client/mixins/__init__.py +13 -0
- fastmcp/client/mixins/prompts.py +295 -0
- fastmcp/client/mixins/resources.py +325 -0
- fastmcp/client/mixins/task_management.py +157 -0
- fastmcp/client/mixins/tools.py +397 -0
- fastmcp/client/sampling/handlers/anthropic.py +2 -2
- fastmcp/client/sampling/handlers/openai.py +1 -1
- fastmcp/client/tasks.py +3 -3
- fastmcp/client/telemetry.py +47 -0
- fastmcp/client/transports/__init__.py +38 -0
- fastmcp/client/transports/base.py +82 -0
- fastmcp/client/transports/config.py +170 -0
- fastmcp/client/transports/http.py +145 -0
- fastmcp/client/transports/inference.py +154 -0
- fastmcp/client/transports/memory.py +90 -0
- fastmcp/client/transports/sse.py +89 -0
- fastmcp/client/transports/stdio.py +543 -0
- fastmcp/contrib/component_manager/README.md +4 -10
- fastmcp/contrib/component_manager/__init__.py +1 -2
- fastmcp/contrib/component_manager/component_manager.py +95 -160
- fastmcp/contrib/component_manager/example.py +1 -1
- fastmcp/contrib/mcp_mixin/example.py +4 -4
- fastmcp/contrib/mcp_mixin/mcp_mixin.py +11 -4
- fastmcp/decorators.py +41 -0
- fastmcp/dependencies.py +12 -1
- fastmcp/exceptions.py +4 -0
- fastmcp/experimental/server/openapi/__init__.py +18 -15
- fastmcp/mcp_config.py +13 -4
- fastmcp/prompts/__init__.py +6 -3
- fastmcp/prompts/function_prompt.py +465 -0
- fastmcp/prompts/prompt.py +321 -271
- fastmcp/resources/__init__.py +5 -3
- fastmcp/resources/function_resource.py +335 -0
- fastmcp/resources/resource.py +325 -115
- fastmcp/resources/template.py +215 -43
- fastmcp/resources/types.py +27 -12
- fastmcp/server/__init__.py +2 -2
- fastmcp/server/auth/__init__.py +14 -0
- fastmcp/server/auth/auth.py +30 -10
- fastmcp/server/auth/authorization.py +190 -0
- fastmcp/server/auth/oauth_proxy/__init__.py +14 -0
- fastmcp/server/auth/oauth_proxy/consent.py +361 -0
- fastmcp/server/auth/oauth_proxy/models.py +178 -0
- fastmcp/server/auth/{oauth_proxy.py → oauth_proxy/proxy.py} +24 -778
- fastmcp/server/auth/oauth_proxy/ui.py +277 -0
- fastmcp/server/auth/oidc_proxy.py +2 -2
- fastmcp/server/auth/providers/auth0.py +24 -94
- fastmcp/server/auth/providers/aws.py +26 -95
- fastmcp/server/auth/providers/azure.py +41 -129
- fastmcp/server/auth/providers/descope.py +18 -49
- fastmcp/server/auth/providers/discord.py +25 -86
- fastmcp/server/auth/providers/github.py +23 -87
- fastmcp/server/auth/providers/google.py +24 -87
- fastmcp/server/auth/providers/introspection.py +60 -79
- fastmcp/server/auth/providers/jwt.py +30 -67
- fastmcp/server/auth/providers/oci.py +47 -110
- fastmcp/server/auth/providers/scalekit.py +23 -61
- fastmcp/server/auth/providers/supabase.py +18 -47
- fastmcp/server/auth/providers/workos.py +34 -127
- fastmcp/server/context.py +372 -419
- fastmcp/server/dependencies.py +541 -251
- fastmcp/server/elicitation.py +20 -18
- fastmcp/server/event_store.py +3 -3
- fastmcp/server/http.py +16 -6
- fastmcp/server/lifespan.py +198 -0
- fastmcp/server/low_level.py +92 -2
- fastmcp/server/middleware/__init__.py +5 -1
- fastmcp/server/middleware/authorization.py +312 -0
- fastmcp/server/middleware/caching.py +101 -54
- fastmcp/server/middleware/middleware.py +6 -9
- fastmcp/server/middleware/ping.py +70 -0
- fastmcp/server/middleware/tool_injection.py +2 -2
- fastmcp/server/mixins/__init__.py +7 -0
- fastmcp/server/mixins/lifespan.py +217 -0
- fastmcp/server/mixins/mcp_operations.py +392 -0
- fastmcp/server/mixins/transport.py +342 -0
- fastmcp/server/openapi/__init__.py +41 -21
- fastmcp/server/openapi/components.py +16 -339
- fastmcp/server/openapi/routing.py +34 -118
- fastmcp/server/openapi/server.py +67 -392
- fastmcp/server/providers/__init__.py +71 -0
- fastmcp/server/providers/aggregate.py +261 -0
- fastmcp/server/providers/base.py +578 -0
- fastmcp/server/providers/fastmcp_provider.py +674 -0
- fastmcp/server/providers/filesystem.py +226 -0
- fastmcp/server/providers/filesystem_discovery.py +327 -0
- fastmcp/server/providers/local_provider/__init__.py +11 -0
- fastmcp/server/providers/local_provider/decorators/__init__.py +15 -0
- fastmcp/server/providers/local_provider/decorators/prompts.py +256 -0
- fastmcp/server/providers/local_provider/decorators/resources.py +240 -0
- fastmcp/server/providers/local_provider/decorators/tools.py +315 -0
- fastmcp/server/providers/local_provider/local_provider.py +465 -0
- fastmcp/server/providers/openapi/__init__.py +39 -0
- fastmcp/server/providers/openapi/components.py +332 -0
- fastmcp/server/providers/openapi/provider.py +405 -0
- fastmcp/server/providers/openapi/routing.py +109 -0
- fastmcp/server/providers/proxy.py +867 -0
- fastmcp/server/providers/skills/__init__.py +59 -0
- fastmcp/server/providers/skills/_common.py +101 -0
- fastmcp/server/providers/skills/claude_provider.py +44 -0
- fastmcp/server/providers/skills/directory_provider.py +153 -0
- fastmcp/server/providers/skills/skill_provider.py +432 -0
- fastmcp/server/providers/skills/vendor_providers.py +142 -0
- fastmcp/server/providers/wrapped_provider.py +140 -0
- fastmcp/server/proxy.py +34 -700
- fastmcp/server/sampling/run.py +341 -2
- fastmcp/server/sampling/sampling_tool.py +4 -3
- fastmcp/server/server.py +1214 -2171
- fastmcp/server/tasks/__init__.py +2 -1
- fastmcp/server/tasks/capabilities.py +13 -1
- fastmcp/server/tasks/config.py +66 -3
- fastmcp/server/tasks/handlers.py +65 -273
- fastmcp/server/tasks/keys.py +4 -6
- fastmcp/server/tasks/requests.py +474 -0
- fastmcp/server/tasks/routing.py +76 -0
- fastmcp/server/tasks/subscriptions.py +20 -11
- fastmcp/server/telemetry.py +131 -0
- fastmcp/server/transforms/__init__.py +244 -0
- fastmcp/server/transforms/namespace.py +193 -0
- fastmcp/server/transforms/prompts_as_tools.py +175 -0
- fastmcp/server/transforms/resources_as_tools.py +190 -0
- fastmcp/server/transforms/tool_transform.py +96 -0
- fastmcp/server/transforms/version_filter.py +124 -0
- fastmcp/server/transforms/visibility.py +526 -0
- fastmcp/settings.py +34 -96
- fastmcp/telemetry.py +122 -0
- fastmcp/tools/__init__.py +10 -3
- fastmcp/tools/function_parsing.py +201 -0
- fastmcp/tools/function_tool.py +467 -0
- fastmcp/tools/tool.py +215 -362
- fastmcp/tools/tool_transform.py +38 -21
- fastmcp/utilities/async_utils.py +69 -0
- fastmcp/utilities/components.py +152 -91
- fastmcp/utilities/inspect.py +8 -20
- fastmcp/utilities/json_schema.py +12 -5
- fastmcp/utilities/json_schema_type.py +17 -15
- fastmcp/utilities/lifespan.py +56 -0
- fastmcp/utilities/logging.py +12 -4
- fastmcp/utilities/mcp_server_config/v1/mcp_server_config.py +3 -3
- fastmcp/utilities/openapi/parser.py +3 -3
- fastmcp/utilities/pagination.py +80 -0
- fastmcp/utilities/skills.py +253 -0
- fastmcp/utilities/tests.py +0 -16
- fastmcp/utilities/timeout.py +47 -0
- fastmcp/utilities/types.py +1 -1
- fastmcp/utilities/versions.py +285 -0
- {fastmcp-2.14.4.dist-info → fastmcp-3.0.0b1.dist-info}/METADATA +8 -5
- fastmcp-3.0.0b1.dist-info/RECORD +228 -0
- fastmcp/client/transports.py +0 -1170
- fastmcp/contrib/component_manager/component_service.py +0 -209
- fastmcp/prompts/prompt_manager.py +0 -117
- fastmcp/resources/resource_manager.py +0 -338
- fastmcp/server/tasks/converters.py +0 -206
- fastmcp/server/tasks/protocol.py +0 -359
- fastmcp/tools/tool_manager.py +0 -170
- fastmcp/utilities/mcp_config.py +0 -56
- fastmcp-2.14.4.dist-info/RECORD +0 -161
- /fastmcp/server/{openapi → providers/openapi}/README.md +0 -0
- {fastmcp-2.14.4.dist-info → fastmcp-3.0.0b1.dist-info}/WHEEL +0 -0
- {fastmcp-2.14.4.dist-info → fastmcp-3.0.0b1.dist-info}/entry_points.txt +0 -0
- {fastmcp-2.14.4.dist-info → fastmcp-3.0.0b1.dist-info}/licenses/LICENSE +0 -0
fastmcp/server/context.py
CHANGED
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
import copy
|
|
4
|
-
import json
|
|
5
3
|
import logging
|
|
6
4
|
import weakref
|
|
7
5
|
from collections.abc import Callable, Generator, Mapping, Sequence
|
|
@@ -9,35 +7,25 @@ from contextlib import contextmanager
|
|
|
9
7
|
from contextvars import ContextVar, Token
|
|
10
8
|
from dataclasses import dataclass
|
|
11
9
|
from logging import Logger
|
|
12
|
-
from typing import Any, Literal,
|
|
10
|
+
from typing import Any, Literal, overload
|
|
13
11
|
|
|
14
|
-
import
|
|
12
|
+
import mcp.types
|
|
15
13
|
from mcp import LoggingLevel, ServerSession
|
|
16
|
-
from mcp.server.lowlevel.helper_types import ReadResourceContents
|
|
17
14
|
from mcp.server.lowlevel.server import request_ctx
|
|
18
15
|
from mcp.shared.context import RequestContext
|
|
19
16
|
from mcp.types import (
|
|
20
|
-
CreateMessageResult,
|
|
21
|
-
CreateMessageResultWithTools,
|
|
22
17
|
GetPromptResult,
|
|
23
18
|
ModelPreferences,
|
|
24
19
|
Root,
|
|
25
20
|
SamplingMessage,
|
|
26
|
-
SamplingMessageContentBlock,
|
|
27
|
-
TextContent,
|
|
28
|
-
ToolChoice,
|
|
29
|
-
ToolResultContent,
|
|
30
|
-
ToolUseContent,
|
|
31
21
|
)
|
|
32
22
|
from mcp.types import Prompt as SDKPrompt
|
|
33
23
|
from mcp.types import Resource as SDKResource
|
|
34
|
-
from mcp.types import Tool as SDKTool
|
|
35
|
-
from pydantic import ValidationError
|
|
36
24
|
from pydantic.networks import AnyUrl
|
|
37
25
|
from starlette.requests import Request
|
|
38
26
|
from typing_extensions import TypeVar
|
|
39
27
|
|
|
40
|
-
from fastmcp import
|
|
28
|
+
from fastmcp.resources.resource import ResourceResult
|
|
41
29
|
from fastmcp.server.elicitation import (
|
|
42
30
|
AcceptedElicitation,
|
|
43
31
|
CancelledElicitation,
|
|
@@ -47,17 +35,30 @@ from fastmcp.server.elicitation import (
|
|
|
47
35
|
)
|
|
48
36
|
from fastmcp.server.sampling import SampleStep, SamplingResult, SamplingTool
|
|
49
37
|
from fastmcp.server.sampling.run import (
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
determine_handler_mode,
|
|
38
|
+
sample_impl,
|
|
39
|
+
sample_step_impl,
|
|
53
40
|
)
|
|
54
|
-
from fastmcp.server.
|
|
55
|
-
|
|
41
|
+
from fastmcp.server.server import FastMCP, StateValue
|
|
42
|
+
from fastmcp.server.transforms.visibility import (
|
|
43
|
+
Visibility,
|
|
44
|
+
)
|
|
45
|
+
from fastmcp.server.transforms.visibility import (
|
|
46
|
+
disable_components as _disable_components,
|
|
47
|
+
)
|
|
48
|
+
from fastmcp.server.transforms.visibility import (
|
|
49
|
+
enable_components as _enable_components,
|
|
50
|
+
)
|
|
51
|
+
from fastmcp.server.transforms.visibility import (
|
|
52
|
+
get_session_transforms as _get_session_transforms,
|
|
53
|
+
)
|
|
54
|
+
from fastmcp.server.transforms.visibility import (
|
|
55
|
+
get_visibility_rules as _get_visibility_rules,
|
|
56
|
+
)
|
|
57
|
+
from fastmcp.server.transforms.visibility import (
|
|
58
|
+
reset_visibility as _reset_visibility,
|
|
56
59
|
)
|
|
57
|
-
from fastmcp.server.server import FastMCP
|
|
58
|
-
from fastmcp.utilities.json_schema import compress_schema
|
|
59
60
|
from fastmcp.utilities.logging import _clamp_logger, get_logger
|
|
60
|
-
from fastmcp.utilities.
|
|
61
|
+
from fastmcp.utilities.versions import VersionSpec
|
|
61
62
|
|
|
62
63
|
logger: Logger = get_logger(name=__name__)
|
|
63
64
|
to_client_logger: Logger = logger.getChild(suffix="to_client")
|
|
@@ -71,13 +72,27 @@ _clamp_logger(logger=to_client_logger, max_level="DEBUG")
|
|
|
71
72
|
T = TypeVar("T", default=Any)
|
|
72
73
|
ResultT = TypeVar("ResultT", default=str)
|
|
73
74
|
|
|
74
|
-
#
|
|
75
|
-
|
|
75
|
+
# Import ToolChoiceOption from sampling module (after other imports)
|
|
76
|
+
from fastmcp.server.sampling.run import ToolChoiceOption # noqa: E402
|
|
77
|
+
|
|
78
|
+
_current_context: ContextVar[Context | None] = ContextVar("context", default=None)
|
|
79
|
+
|
|
80
|
+
TransportType = Literal["stdio", "sse", "streamable-http"]
|
|
81
|
+
_current_transport: ContextVar[TransportType | None] = ContextVar(
|
|
82
|
+
"transport", default=None
|
|
83
|
+
)
|
|
84
|
+
|
|
76
85
|
|
|
77
|
-
|
|
86
|
+
def set_transport(
|
|
87
|
+
transport: TransportType,
|
|
88
|
+
) -> Token[TransportType | None]:
|
|
89
|
+
"""Set the current transport type. Returns token for reset."""
|
|
90
|
+
return _current_transport.set(transport)
|
|
78
91
|
|
|
79
92
|
|
|
80
|
-
|
|
93
|
+
def reset_transport(token: Token[TransportType | None]) -> None:
|
|
94
|
+
"""Reset transport to previous value."""
|
|
95
|
+
_current_transport.reset(token)
|
|
81
96
|
|
|
82
97
|
|
|
83
98
|
@dataclass
|
|
@@ -141,29 +156,35 @@ class Context:
|
|
|
141
156
|
request_id = ctx.request_id
|
|
142
157
|
client_id = ctx.client_id
|
|
143
158
|
|
|
144
|
-
# Manage state across the
|
|
145
|
-
ctx.set_state("key", "value")
|
|
146
|
-
value = ctx.get_state("key")
|
|
159
|
+
# Manage state across the session (persists across requests)
|
|
160
|
+
await ctx.set_state("key", "value")
|
|
161
|
+
value = await ctx.get_state("key")
|
|
147
162
|
|
|
148
163
|
return str(x)
|
|
149
164
|
```
|
|
150
165
|
|
|
151
166
|
State Management:
|
|
152
|
-
Context
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
167
|
+
Context provides session-scoped state that persists across requests within
|
|
168
|
+
the same MCP session. State is automatically keyed by session, ensuring
|
|
169
|
+
isolation between different clients.
|
|
170
|
+
|
|
171
|
+
State set during `on_initialize` middleware will persist to subsequent tool
|
|
172
|
+
calls when using the same session object (STDIO, SSE, single-server HTTP).
|
|
173
|
+
For distributed/serverless HTTP deployments where different machines handle
|
|
174
|
+
the init and tool calls, state is isolated by the mcp-session-id header.
|
|
156
175
|
|
|
157
176
|
The context parameter name can be anything as long as it's annotated with Context.
|
|
158
177
|
The context is optional - tools that don't need it can omit the parameter.
|
|
159
178
|
|
|
160
179
|
"""
|
|
161
180
|
|
|
162
|
-
|
|
181
|
+
# Default TTL for session state: 1 day in seconds
|
|
182
|
+
_STATE_TTL_SECONDS: int = 86400
|
|
183
|
+
|
|
184
|
+
def __init__(self, fastmcp: FastMCP, session: ServerSession | None = None):
|
|
163
185
|
self._fastmcp: weakref.ref[FastMCP] = weakref.ref(fastmcp)
|
|
186
|
+
self._session: ServerSession | None = session # For state ops during init
|
|
164
187
|
self._tokens: list[Token] = []
|
|
165
|
-
self._notification_queue: set[str] = set() # Dedupe notifications
|
|
166
|
-
self._state: dict[str, Any] = {}
|
|
167
188
|
|
|
168
189
|
@property
|
|
169
190
|
def fastmcp(self) -> FastMCP:
|
|
@@ -175,11 +196,6 @@ class Context:
|
|
|
175
196
|
|
|
176
197
|
async def __aenter__(self) -> Context:
|
|
177
198
|
"""Enter the context manager and set this context as the current context."""
|
|
178
|
-
parent_context = _current_context.get(None)
|
|
179
|
-
if parent_context is not None:
|
|
180
|
-
# Inherit state from parent context
|
|
181
|
-
self._state = copy.deepcopy(parent_context._state)
|
|
182
|
-
|
|
183
199
|
# Always set this context and save the token
|
|
184
200
|
token = _current_context.set(self)
|
|
185
201
|
self._tokens.append(token)
|
|
@@ -194,8 +210,8 @@ class Context:
|
|
|
194
210
|
self._server_token = _current_server.set(weakref.ref(self.fastmcp))
|
|
195
211
|
|
|
196
212
|
# Set docket/worker from server instance for this request's context.
|
|
197
|
-
# This ensures ContextVars work even in environments (
|
|
198
|
-
# lifespan ContextVars don't propagate to request handlers.
|
|
213
|
+
# This ensures ContextVars work even in ASGI environments (Lambda, FastAPI mount)
|
|
214
|
+
# where lifespan ContextVars don't propagate to request handlers.
|
|
199
215
|
server = self.fastmcp
|
|
200
216
|
if server._docket is not None:
|
|
201
217
|
self._docket_token = _current_docket.set(server._docket)
|
|
@@ -207,9 +223,6 @@ class Context:
|
|
|
207
223
|
|
|
208
224
|
async def __aexit__(self, exc_type, exc_val, exc_tb) -> None:
|
|
209
225
|
"""Exit the context manager and reset the most recent token."""
|
|
210
|
-
# Flush any remaining notifications before exiting
|
|
211
|
-
await self._flush_notifications()
|
|
212
|
-
|
|
213
226
|
# Reset server/docket/worker tokens
|
|
214
227
|
from fastmcp.server.dependencies import (
|
|
215
228
|
_current_docket,
|
|
@@ -261,6 +274,29 @@ class Context:
|
|
|
261
274
|
except LookupError:
|
|
262
275
|
return None
|
|
263
276
|
|
|
277
|
+
@property
|
|
278
|
+
def lifespan_context(self) -> dict[str, Any]:
|
|
279
|
+
"""Access the server's lifespan context.
|
|
280
|
+
|
|
281
|
+
Returns the context dict yielded by the server's lifespan function.
|
|
282
|
+
Returns an empty dict if no lifespan was configured or if the MCP
|
|
283
|
+
session is not yet established.
|
|
284
|
+
|
|
285
|
+
Example:
|
|
286
|
+
```python
|
|
287
|
+
@server.tool
|
|
288
|
+
def my_tool(ctx: Context) -> str:
|
|
289
|
+
db = ctx.lifespan_context.get("db")
|
|
290
|
+
if db:
|
|
291
|
+
return db.query("SELECT 1")
|
|
292
|
+
return "No database connection"
|
|
293
|
+
```
|
|
294
|
+
"""
|
|
295
|
+
rc = self.request_context
|
|
296
|
+
if rc is None:
|
|
297
|
+
return {}
|
|
298
|
+
return rc.lifespan_context
|
|
299
|
+
|
|
264
300
|
async def report_progress(
|
|
265
301
|
self, progress: float, total: float | None = None, message: str | None = None
|
|
266
302
|
) -> None:
|
|
@@ -288,13 +324,48 @@ class Context:
|
|
|
288
324
|
related_request_id=self.request_id,
|
|
289
325
|
)
|
|
290
326
|
|
|
327
|
+
async def _paginate_list(
|
|
328
|
+
self,
|
|
329
|
+
request_factory: Callable[[str | None], Any],
|
|
330
|
+
call_method: Callable[[Any], Any],
|
|
331
|
+
extract_items: Callable[[Any], list[Any]],
|
|
332
|
+
) -> list[Any]:
|
|
333
|
+
"""Generic pagination helper for list operations.
|
|
334
|
+
|
|
335
|
+
Args:
|
|
336
|
+
request_factory: Function that creates a request from a cursor
|
|
337
|
+
call_method: Async method to call with the request
|
|
338
|
+
extract_items: Function to extract items from the result
|
|
339
|
+
|
|
340
|
+
Returns:
|
|
341
|
+
List of all items across all pages
|
|
342
|
+
"""
|
|
343
|
+
all_items: list[Any] = []
|
|
344
|
+
cursor: str | None = None
|
|
345
|
+
while True:
|
|
346
|
+
request = request_factory(cursor)
|
|
347
|
+
result = await call_method(request)
|
|
348
|
+
all_items.extend(extract_items(result))
|
|
349
|
+
if result.nextCursor is None:
|
|
350
|
+
break
|
|
351
|
+
cursor = result.nextCursor
|
|
352
|
+
return all_items
|
|
353
|
+
|
|
291
354
|
async def list_resources(self) -> list[SDKResource]:
|
|
292
355
|
"""List all available resources from the server.
|
|
293
356
|
|
|
294
357
|
Returns:
|
|
295
358
|
List of Resource objects available on the server
|
|
296
359
|
"""
|
|
297
|
-
return await self.
|
|
360
|
+
return await self._paginate_list(
|
|
361
|
+
request_factory=lambda cursor: mcp.types.ListResourcesRequest(
|
|
362
|
+
params=mcp.types.PaginatedRequestParams(cursor=cursor)
|
|
363
|
+
if cursor
|
|
364
|
+
else None
|
|
365
|
+
),
|
|
366
|
+
call_method=self.fastmcp._list_resources_mcp,
|
|
367
|
+
extract_items=lambda result: result.resources,
|
|
368
|
+
)
|
|
298
369
|
|
|
299
370
|
async def list_prompts(self) -> list[SDKPrompt]:
|
|
300
371
|
"""List all available prompts from the server.
|
|
@@ -302,7 +373,15 @@ class Context:
|
|
|
302
373
|
Returns:
|
|
303
374
|
List of Prompt objects available on the server
|
|
304
375
|
"""
|
|
305
|
-
return await self.
|
|
376
|
+
return await self._paginate_list(
|
|
377
|
+
request_factory=lambda cursor: mcp.types.ListPromptsRequest(
|
|
378
|
+
params=mcp.types.PaginatedRequestParams(cursor=cursor)
|
|
379
|
+
if cursor
|
|
380
|
+
else None
|
|
381
|
+
),
|
|
382
|
+
call_method=self.fastmcp._list_prompts_mcp,
|
|
383
|
+
extract_items=lambda result: result.prompts,
|
|
384
|
+
)
|
|
306
385
|
|
|
307
386
|
async def get_prompt(
|
|
308
387
|
self, name: str, arguments: dict[str, Any] | None = None
|
|
@@ -316,19 +395,28 @@ class Context:
|
|
|
316
395
|
Returns:
|
|
317
396
|
The prompt result
|
|
318
397
|
"""
|
|
319
|
-
|
|
398
|
+
result = await self.fastmcp.render_prompt(name, arguments)
|
|
399
|
+
if isinstance(result, mcp.types.CreateTaskResult):
|
|
400
|
+
raise RuntimeError(
|
|
401
|
+
"Unexpected CreateTaskResult: Context calls should not have task metadata"
|
|
402
|
+
)
|
|
403
|
+
return result.to_mcp_prompt_result()
|
|
320
404
|
|
|
321
|
-
async def read_resource(self, uri: str | AnyUrl) ->
|
|
405
|
+
async def read_resource(self, uri: str | AnyUrl) -> ResourceResult:
|
|
322
406
|
"""Read a resource by URI.
|
|
323
407
|
|
|
324
408
|
Args:
|
|
325
409
|
uri: Resource URI to read
|
|
326
410
|
|
|
327
411
|
Returns:
|
|
328
|
-
|
|
412
|
+
ResourceResult with contents
|
|
329
413
|
"""
|
|
330
|
-
|
|
331
|
-
|
|
414
|
+
result = await self.fastmcp.read_resource(str(uri))
|
|
415
|
+
if isinstance(result, mcp.types.CreateTaskResult):
|
|
416
|
+
raise RuntimeError(
|
|
417
|
+
"Unexpected CreateTaskResult: Context calls should not have task metadata"
|
|
418
|
+
)
|
|
419
|
+
return result
|
|
332
420
|
|
|
333
421
|
async def log(
|
|
334
422
|
self,
|
|
@@ -358,6 +446,15 @@ class Context:
|
|
|
358
446
|
related_request_id=self.request_id,
|
|
359
447
|
)
|
|
360
448
|
|
|
449
|
+
@property
|
|
450
|
+
def transport(self) -> TransportType | None:
|
|
451
|
+
"""Get the current transport type.
|
|
452
|
+
|
|
453
|
+
Returns the transport type used to run this server: "stdio", "sse",
|
|
454
|
+
or "streamable-http". Returns None if called outside of a server context.
|
|
455
|
+
"""
|
|
456
|
+
return _current_transport.get()
|
|
457
|
+
|
|
361
458
|
@property
|
|
362
459
|
def client_id(self) -> str | None:
|
|
363
460
|
"""Get the client ID if available."""
|
|
@@ -393,7 +490,7 @@ class Context:
|
|
|
393
490
|
for other transports.
|
|
394
491
|
|
|
395
492
|
Raises:
|
|
396
|
-
RuntimeError if
|
|
493
|
+
RuntimeError if no session is available.
|
|
397
494
|
|
|
398
495
|
Example:
|
|
399
496
|
```python
|
|
@@ -404,32 +501,37 @@ class Context:
|
|
|
404
501
|
return f"Data stored for session {session_id}"
|
|
405
502
|
```
|
|
406
503
|
"""
|
|
504
|
+
from uuid import uuid4
|
|
505
|
+
|
|
506
|
+
# Get session from request context or _session (for on_initialize)
|
|
407
507
|
request_ctx = self.request_context
|
|
408
|
-
if request_ctx is None:
|
|
508
|
+
if request_ctx is not None:
|
|
509
|
+
session = request_ctx.session
|
|
510
|
+
elif self._session is not None:
|
|
511
|
+
session = self._session
|
|
512
|
+
else:
|
|
409
513
|
raise RuntimeError(
|
|
410
|
-
"session_id is not available because
|
|
411
|
-
"
|
|
514
|
+
"session_id is not available because no session exists. "
|
|
515
|
+
"This typically means you're outside a request context."
|
|
412
516
|
)
|
|
413
|
-
session = request_ctx.session
|
|
414
517
|
|
|
415
|
-
#
|
|
416
|
-
session_id = getattr(session, "
|
|
518
|
+
# Check for cached session ID
|
|
519
|
+
session_id = getattr(session, "_fastmcp_state_prefix", None)
|
|
417
520
|
if session_id is not None:
|
|
418
521
|
return session_id
|
|
419
522
|
|
|
420
|
-
#
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
523
|
+
# For HTTP, try to get from header
|
|
524
|
+
if request_ctx is not None:
|
|
525
|
+
request = request_ctx.request
|
|
526
|
+
if request:
|
|
527
|
+
session_id = request.headers.get("mcp-session-id")
|
|
424
528
|
|
|
425
|
-
#
|
|
529
|
+
# For STDIO/SSE/in-memory, generate a UUID
|
|
426
530
|
if session_id is None:
|
|
427
|
-
from uuid import uuid4
|
|
428
|
-
|
|
429
531
|
session_id = str(uuid4())
|
|
430
532
|
|
|
431
|
-
#
|
|
432
|
-
session.
|
|
533
|
+
# Cache on session for consistency
|
|
534
|
+
session._fastmcp_state_prefix = session_id # type: ignore[attr-defined]
|
|
433
535
|
return session_id
|
|
434
536
|
|
|
435
537
|
@property
|
|
@@ -515,17 +617,15 @@ class Context:
|
|
|
515
617
|
result = await self.session.list_roots()
|
|
516
618
|
return result.roots
|
|
517
619
|
|
|
518
|
-
async def
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
async def send_resource_list_changed(self) -> None:
|
|
523
|
-
"""Send a resource list changed notification to the client."""
|
|
524
|
-
await self.session.send_resource_list_changed()
|
|
620
|
+
async def send_notification(
|
|
621
|
+
self, notification: mcp.types.ServerNotificationType
|
|
622
|
+
) -> None:
|
|
623
|
+
"""Send a notification to the client immediately.
|
|
525
624
|
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
625
|
+
Args:
|
|
626
|
+
notification: An MCP notification instance (e.g., ToolListChangedNotification())
|
|
627
|
+
"""
|
|
628
|
+
await self.session.send_notification(mcp.types.ServerNotification(notification))
|
|
529
629
|
|
|
530
630
|
async def close_sse_stream(self) -> None:
|
|
531
631
|
"""Close the current response stream to trigger client reconnection.
|
|
@@ -623,101 +723,19 @@ class Context:
|
|
|
623
723
|
# Continue with tool results
|
|
624
724
|
messages = step.history
|
|
625
725
|
"""
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
726
|
+
return await sample_step_impl(
|
|
727
|
+
self,
|
|
728
|
+
messages=messages,
|
|
729
|
+
system_prompt=system_prompt,
|
|
730
|
+
temperature=temperature,
|
|
731
|
+
max_tokens=max_tokens,
|
|
732
|
+
model_preferences=model_preferences,
|
|
733
|
+
tools=tools,
|
|
734
|
+
tool_choice=tool_choice,
|
|
735
|
+
auto_execute_tools=execute_tools,
|
|
736
|
+
mask_error_details=mask_error_details,
|
|
636
737
|
)
|
|
637
738
|
|
|
638
|
-
# Determine whether to use fallback handler or client
|
|
639
|
-
use_fallback = determine_handler_mode(self, bool(sampling_tools))
|
|
640
|
-
|
|
641
|
-
# Build tool choice
|
|
642
|
-
effective_tool_choice: ToolChoice | None = None
|
|
643
|
-
if tool_choice is not None:
|
|
644
|
-
if tool_choice not in ("auto", "required", "none"):
|
|
645
|
-
raise ValueError(
|
|
646
|
-
f"Invalid tool_choice: {tool_choice!r}. "
|
|
647
|
-
"Must be 'auto', 'required', or 'none'."
|
|
648
|
-
)
|
|
649
|
-
effective_tool_choice = ToolChoice(
|
|
650
|
-
mode=cast(Literal["auto", "required", "none"], tool_choice)
|
|
651
|
-
)
|
|
652
|
-
|
|
653
|
-
# Effective max_tokens
|
|
654
|
-
effective_max_tokens = max_tokens if max_tokens is not None else 512
|
|
655
|
-
|
|
656
|
-
# Make the LLM call
|
|
657
|
-
if use_fallback:
|
|
658
|
-
response = await call_sampling_handler(
|
|
659
|
-
self,
|
|
660
|
-
current_messages,
|
|
661
|
-
system_prompt=system_prompt,
|
|
662
|
-
temperature=temperature,
|
|
663
|
-
max_tokens=effective_max_tokens,
|
|
664
|
-
model_preferences=model_preferences,
|
|
665
|
-
sdk_tools=sdk_tools,
|
|
666
|
-
tool_choice=effective_tool_choice,
|
|
667
|
-
)
|
|
668
|
-
else:
|
|
669
|
-
response = await self.session.create_message(
|
|
670
|
-
messages=current_messages,
|
|
671
|
-
system_prompt=system_prompt,
|
|
672
|
-
temperature=temperature,
|
|
673
|
-
max_tokens=effective_max_tokens,
|
|
674
|
-
model_preferences=_parse_model_preferences(model_preferences),
|
|
675
|
-
tools=sdk_tools,
|
|
676
|
-
tool_choice=effective_tool_choice,
|
|
677
|
-
related_request_id=self.request_id,
|
|
678
|
-
)
|
|
679
|
-
|
|
680
|
-
# Check if this is a tool use response
|
|
681
|
-
is_tool_use_response = (
|
|
682
|
-
isinstance(response, CreateMessageResultWithTools)
|
|
683
|
-
and response.stopReason == "toolUse"
|
|
684
|
-
)
|
|
685
|
-
|
|
686
|
-
# Always include the assistant response in history
|
|
687
|
-
current_messages.append(
|
|
688
|
-
SamplingMessage(role="assistant", content=response.content)
|
|
689
|
-
)
|
|
690
|
-
|
|
691
|
-
# If not a tool use, return immediately
|
|
692
|
-
if not is_tool_use_response:
|
|
693
|
-
return SampleStep(response=response, history=current_messages)
|
|
694
|
-
|
|
695
|
-
# If not executing tools, return with assistant message but no tool results
|
|
696
|
-
if not execute_tools:
|
|
697
|
-
return SampleStep(response=response, history=current_messages)
|
|
698
|
-
|
|
699
|
-
# Execute tools and add results to history
|
|
700
|
-
step_tool_calls = _extract_tool_calls(response)
|
|
701
|
-
if step_tool_calls:
|
|
702
|
-
effective_mask = (
|
|
703
|
-
mask_error_details
|
|
704
|
-
if mask_error_details is not None
|
|
705
|
-
else settings.mask_error_details
|
|
706
|
-
)
|
|
707
|
-
tool_results = await run_sampling_tools(
|
|
708
|
-
step_tool_calls, tool_map, mask_error_details=effective_mask
|
|
709
|
-
)
|
|
710
|
-
|
|
711
|
-
if tool_results:
|
|
712
|
-
current_messages.append(
|
|
713
|
-
SamplingMessage(
|
|
714
|
-
role="user",
|
|
715
|
-
content=tool_results, # type: ignore[arg-type]
|
|
716
|
-
)
|
|
717
|
-
)
|
|
718
|
-
|
|
719
|
-
return SampleStep(response=response, history=current_messages)
|
|
720
|
-
|
|
721
739
|
@overload
|
|
722
740
|
async def sample(
|
|
723
741
|
self,
|
|
@@ -796,113 +814,17 @@ class Context:
|
|
|
796
814
|
- .result: The typed result (str for text, parsed object for structured)
|
|
797
815
|
- .history: All messages exchanged during sampling
|
|
798
816
|
"""
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
sampling_tools.append(final_response_tool)
|
|
811
|
-
|
|
812
|
-
# Always require tool calls when result_type is set - the LLM must
|
|
813
|
-
# eventually call final_response (text responses are not accepted)
|
|
814
|
-
tool_choice = "required"
|
|
815
|
-
|
|
816
|
-
# Convert messages for the loop
|
|
817
|
-
current_messages: str | Sequence[str | SamplingMessage] = messages
|
|
818
|
-
|
|
819
|
-
for _iteration in range(max_iterations):
|
|
820
|
-
step = await self.sample_step(
|
|
821
|
-
messages=current_messages,
|
|
822
|
-
system_prompt=system_prompt,
|
|
823
|
-
temperature=temperature,
|
|
824
|
-
max_tokens=max_tokens,
|
|
825
|
-
model_preferences=model_preferences,
|
|
826
|
-
tools=sampling_tools,
|
|
827
|
-
tool_choice=tool_choice,
|
|
828
|
-
mask_error_details=mask_error_details,
|
|
829
|
-
)
|
|
830
|
-
|
|
831
|
-
# Check for final_response tool call for structured output
|
|
832
|
-
if result_type is not None and result_type is not str and step.is_tool_use:
|
|
833
|
-
for tool_call in step.tool_calls:
|
|
834
|
-
if tool_call.name == "final_response":
|
|
835
|
-
# Validate and return the structured result
|
|
836
|
-
type_adapter = get_cached_typeadapter(result_type)
|
|
837
|
-
|
|
838
|
-
# Unwrap if we wrapped primitives (non-object schemas)
|
|
839
|
-
input_data = tool_call.input
|
|
840
|
-
original_schema = compress_schema(
|
|
841
|
-
type_adapter.json_schema(), prune_titles=True
|
|
842
|
-
)
|
|
843
|
-
if (
|
|
844
|
-
original_schema.get("type") != "object"
|
|
845
|
-
and isinstance(input_data, dict)
|
|
846
|
-
and "value" in input_data
|
|
847
|
-
):
|
|
848
|
-
input_data = input_data["value"]
|
|
849
|
-
|
|
850
|
-
try:
|
|
851
|
-
validated_result = type_adapter.validate_python(input_data)
|
|
852
|
-
text = json.dumps(
|
|
853
|
-
type_adapter.dump_python(validated_result, mode="json")
|
|
854
|
-
)
|
|
855
|
-
return SamplingResult(
|
|
856
|
-
text=text,
|
|
857
|
-
result=validated_result,
|
|
858
|
-
history=step.history,
|
|
859
|
-
)
|
|
860
|
-
except ValidationError as e:
|
|
861
|
-
# Validation failed - add error as tool result
|
|
862
|
-
step.history.append(
|
|
863
|
-
SamplingMessage(
|
|
864
|
-
role="user",
|
|
865
|
-
content=[
|
|
866
|
-
ToolResultContent(
|
|
867
|
-
type="tool_result",
|
|
868
|
-
toolUseId=tool_call.id,
|
|
869
|
-
content=[
|
|
870
|
-
TextContent(
|
|
871
|
-
type="text",
|
|
872
|
-
text=(
|
|
873
|
-
f"Validation error: {e}. "
|
|
874
|
-
"Please try again with valid data."
|
|
875
|
-
),
|
|
876
|
-
)
|
|
877
|
-
],
|
|
878
|
-
isError=True,
|
|
879
|
-
)
|
|
880
|
-
], # type: ignore[arg-type]
|
|
881
|
-
)
|
|
882
|
-
)
|
|
883
|
-
|
|
884
|
-
# If not a tool use response, we're done
|
|
885
|
-
if not step.is_tool_use:
|
|
886
|
-
# For structured output, the LLM must use the final_response tool
|
|
887
|
-
if result_type is not None and result_type is not str:
|
|
888
|
-
raise RuntimeError(
|
|
889
|
-
f"Expected structured output of type {result_type.__name__}, "
|
|
890
|
-
"but the LLM returned a text response instead of calling "
|
|
891
|
-
"the final_response tool."
|
|
892
|
-
)
|
|
893
|
-
return SamplingResult(
|
|
894
|
-
text=step.text,
|
|
895
|
-
result=cast(ResultT, step.text if step.text else ""),
|
|
896
|
-
history=step.history,
|
|
897
|
-
)
|
|
898
|
-
|
|
899
|
-
# Continue with the updated history
|
|
900
|
-
current_messages = step.history
|
|
901
|
-
|
|
902
|
-
# After first iteration, reset tool_choice to auto
|
|
903
|
-
tool_choice = None
|
|
904
|
-
|
|
905
|
-
raise RuntimeError(f"Sampling exceeded maximum iterations ({max_iterations})")
|
|
817
|
+
return await sample_impl(
|
|
818
|
+
self,
|
|
819
|
+
messages=messages,
|
|
820
|
+
system_prompt=system_prompt,
|
|
821
|
+
temperature=temperature,
|
|
822
|
+
max_tokens=max_tokens,
|
|
823
|
+
model_preferences=model_preferences,
|
|
824
|
+
tools=tools,
|
|
825
|
+
result_type=result_type,
|
|
826
|
+
mask_error_details=mask_error_details,
|
|
827
|
+
)
|
|
906
828
|
|
|
907
829
|
@overload
|
|
908
830
|
async def elicit(
|
|
@@ -936,10 +858,50 @@ class Context:
|
|
|
936
858
|
"""When response_type is a list of strings, the accepted elicitation will
|
|
937
859
|
contain the selected string response"""
|
|
938
860
|
|
|
861
|
+
@overload
|
|
939
862
|
async def elicit(
|
|
940
863
|
self,
|
|
941
864
|
message: str,
|
|
942
|
-
response_type:
|
|
865
|
+
response_type: dict[str, dict[str, str]],
|
|
866
|
+
) -> AcceptedElicitation[str] | DeclinedElicitation | CancelledElicitation: ...
|
|
867
|
+
|
|
868
|
+
"""When response_type is a dict mapping keys to title dicts, the accepted
|
|
869
|
+
elicitation will contain the selected key"""
|
|
870
|
+
|
|
871
|
+
@overload
|
|
872
|
+
async def elicit(
|
|
873
|
+
self,
|
|
874
|
+
message: str,
|
|
875
|
+
response_type: list[list[str]],
|
|
876
|
+
) -> (
|
|
877
|
+
AcceptedElicitation[list[str]] | DeclinedElicitation | CancelledElicitation
|
|
878
|
+
): ...
|
|
879
|
+
|
|
880
|
+
"""When response_type is a list containing a list of strings (multi-select),
|
|
881
|
+
the accepted elicitation will contain a list of selected strings"""
|
|
882
|
+
|
|
883
|
+
@overload
|
|
884
|
+
async def elicit(
|
|
885
|
+
self,
|
|
886
|
+
message: str,
|
|
887
|
+
response_type: list[dict[str, dict[str, str]]],
|
|
888
|
+
) -> (
|
|
889
|
+
AcceptedElicitation[list[str]] | DeclinedElicitation | CancelledElicitation
|
|
890
|
+
): ...
|
|
891
|
+
|
|
892
|
+
"""When response_type is a list containing a dict mapping keys to title dicts
|
|
893
|
+
(multi-select with titles), the accepted elicitation will contain a list of
|
|
894
|
+
selected keys"""
|
|
895
|
+
|
|
896
|
+
async def elicit(
|
|
897
|
+
self,
|
|
898
|
+
message: str,
|
|
899
|
+
response_type: type[T]
|
|
900
|
+
| list[str]
|
|
901
|
+
| dict[str, dict[str, str]]
|
|
902
|
+
| list[list[str]]
|
|
903
|
+
| list[dict[str, dict[str, str]]]
|
|
904
|
+
| None = None,
|
|
943
905
|
) -> (
|
|
944
906
|
AcceptedElicitation[T]
|
|
945
907
|
| AcceptedElicitation[dict[str, Any]]
|
|
@@ -988,43 +950,135 @@ class Context:
|
|
|
988
950
|
else:
|
|
989
951
|
raise ValueError(f"Unexpected elicitation action: {result.action}")
|
|
990
952
|
|
|
991
|
-
def
|
|
992
|
-
"""
|
|
993
|
-
self.
|
|
994
|
-
|
|
995
|
-
def
|
|
996
|
-
"""
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
953
|
+
def _make_state_key(self, key: str) -> str:
|
|
954
|
+
"""Create session-prefixed key for state storage."""
|
|
955
|
+
return f"{self.session_id}:{key}"
|
|
956
|
+
|
|
957
|
+
async def set_state(self, key: str, value: Any) -> None:
|
|
958
|
+
"""Set a value in the session-scoped state store.
|
|
959
|
+
|
|
960
|
+
Values persist across requests within the same MCP session.
|
|
961
|
+
The key is automatically prefixed with the session identifier.
|
|
962
|
+
State expires after 1 day to prevent unbounded memory growth.
|
|
963
|
+
"""
|
|
964
|
+
prefixed_key = self._make_state_key(key)
|
|
965
|
+
await self.fastmcp._state_store.put(
|
|
966
|
+
key=prefixed_key,
|
|
967
|
+
value=StateValue(value=value),
|
|
968
|
+
ttl=self._STATE_TTL_SECONDS,
|
|
969
|
+
)
|
|
970
|
+
|
|
971
|
+
async def get_state(self, key: str) -> Any:
|
|
972
|
+
"""Get a value from the session-scoped state store.
|
|
973
|
+
|
|
974
|
+
Returns None if the key is not found.
|
|
975
|
+
"""
|
|
976
|
+
prefixed_key = self._make_state_key(key)
|
|
977
|
+
result = await self.fastmcp._state_store.get(key=prefixed_key)
|
|
978
|
+
return result.value if result is not None else None
|
|
979
|
+
|
|
980
|
+
async def delete_state(self, key: str) -> None:
|
|
981
|
+
"""Delete a value from the session-scoped state store."""
|
|
982
|
+
prefixed_key = self._make_state_key(key)
|
|
983
|
+
await self.fastmcp._state_store.delete(key=prefixed_key)
|
|
984
|
+
|
|
985
|
+
# -------------------------------------------------------------------------
|
|
986
|
+
# Session visibility control
|
|
987
|
+
# -------------------------------------------------------------------------
|
|
988
|
+
|
|
989
|
+
async def _get_visibility_rules(self) -> list[dict[str, Any]]:
|
|
990
|
+
"""Load visibility rule dicts from session state."""
|
|
991
|
+
return await _get_visibility_rules(self)
|
|
992
|
+
|
|
993
|
+
async def _get_session_transforms(self) -> list[Visibility]:
|
|
994
|
+
"""Get session-specific Visibility transforms from state store."""
|
|
995
|
+
return await _get_session_transforms(self)
|
|
996
|
+
|
|
997
|
+
async def enable_components(
|
|
998
|
+
self,
|
|
999
|
+
*,
|
|
1000
|
+
names: set[str] | None = None,
|
|
1001
|
+
keys: set[str] | None = None,
|
|
1002
|
+
version: VersionSpec | None = None,
|
|
1003
|
+
tags: set[str] | None = None,
|
|
1004
|
+
components: set[Literal["tool", "resource", "template", "prompt"]]
|
|
1005
|
+
| None = None,
|
|
1006
|
+
match_all: bool = False,
|
|
1007
|
+
) -> None:
|
|
1008
|
+
"""Enable components matching criteria for this session only.
|
|
1009
|
+
|
|
1010
|
+
Session rules override global transforms. Rules accumulate - each call
|
|
1011
|
+
adds a new rule to the session. Later marks override earlier ones
|
|
1012
|
+
(Visibility transform semantics).
|
|
1013
|
+
|
|
1014
|
+
Sends notifications to this session only: ToolListChangedNotification,
|
|
1015
|
+
ResourceListChangedNotification, and PromptListChangedNotification.
|
|
1016
|
+
|
|
1017
|
+
Args:
|
|
1018
|
+
names: Component names or URIs to match.
|
|
1019
|
+
keys: Component keys to match (e.g., {"tool:my_tool@v1"}).
|
|
1020
|
+
version: Component version spec to match.
|
|
1021
|
+
tags: Tags to match (component must have at least one).
|
|
1022
|
+
components: Component types to match (e.g., {"tool", "prompt"}).
|
|
1023
|
+
match_all: If True, matches all components regardless of other criteria.
|
|
1024
|
+
"""
|
|
1025
|
+
await _enable_components(
|
|
1026
|
+
self,
|
|
1027
|
+
names=names,
|
|
1028
|
+
keys=keys,
|
|
1029
|
+
version=version,
|
|
1030
|
+
tags=tags,
|
|
1031
|
+
components=components,
|
|
1032
|
+
match_all=match_all,
|
|
1033
|
+
)
|
|
1034
|
+
|
|
1035
|
+
async def disable_components(
|
|
1036
|
+
self,
|
|
1037
|
+
*,
|
|
1038
|
+
names: set[str] | None = None,
|
|
1039
|
+
keys: set[str] | None = None,
|
|
1040
|
+
version: VersionSpec | None = None,
|
|
1041
|
+
tags: set[str] | None = None,
|
|
1042
|
+
components: set[Literal["tool", "resource", "template", "prompt"]]
|
|
1043
|
+
| None = None,
|
|
1044
|
+
match_all: bool = False,
|
|
1045
|
+
) -> None:
|
|
1046
|
+
"""Disable components matching criteria for this session only.
|
|
1047
|
+
|
|
1048
|
+
Session rules override global transforms. Rules accumulate - each call
|
|
1049
|
+
adds a new rule to the session. Later marks override earlier ones
|
|
1050
|
+
(Visibility transform semantics).
|
|
1051
|
+
|
|
1052
|
+
Sends notifications to this session only: ToolListChangedNotification,
|
|
1053
|
+
ResourceListChangedNotification, and PromptListChangedNotification.
|
|
1054
|
+
|
|
1055
|
+
Args:
|
|
1056
|
+
names: Component names or URIs to match.
|
|
1057
|
+
keys: Component keys to match (e.g., {"tool:my_tool@v1"}).
|
|
1058
|
+
version: Component version spec to match.
|
|
1059
|
+
tags: Tags to match (component must have at least one).
|
|
1060
|
+
components: Component types to match (e.g., {"tool", "prompt"}).
|
|
1061
|
+
match_all: If True, matches all components regardless of other criteria.
|
|
1062
|
+
"""
|
|
1063
|
+
await _disable_components(
|
|
1064
|
+
self,
|
|
1065
|
+
names=names,
|
|
1066
|
+
keys=keys,
|
|
1067
|
+
version=version,
|
|
1068
|
+
tags=tags,
|
|
1069
|
+
components=components,
|
|
1070
|
+
match_all=match_all,
|
|
1071
|
+
)
|
|
1072
|
+
|
|
1073
|
+
async def reset_visibility(self) -> None:
|
|
1074
|
+
"""Clear all session visibility rules.
|
|
1075
|
+
|
|
1076
|
+
Use this to reset session visibility back to global defaults.
|
|
1077
|
+
|
|
1078
|
+
Sends notifications to this session only: ToolListChangedNotification,
|
|
1079
|
+
ResourceListChangedNotification, and PromptListChangedNotification.
|
|
1080
|
+
"""
|
|
1081
|
+
await _reset_visibility(self)
|
|
1028
1082
|
|
|
1029
1083
|
|
|
1030
1084
|
async def _log_to_server_and_client(
|
|
@@ -1053,104 +1107,3 @@ async def _log_to_server_and_client(
|
|
|
1053
1107
|
logger=logger_name,
|
|
1054
1108
|
related_request_id=related_request_id,
|
|
1055
1109
|
)
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
def _create_final_response_tool(result_type: type) -> SamplingTool:
|
|
1059
|
-
"""Create a synthetic 'final_response' tool for structured output.
|
|
1060
|
-
|
|
1061
|
-
This tool is used to capture structured responses from the LLM.
|
|
1062
|
-
The tool's schema is derived from the result_type.
|
|
1063
|
-
"""
|
|
1064
|
-
type_adapter = get_cached_typeadapter(result_type)
|
|
1065
|
-
schema = type_adapter.json_schema()
|
|
1066
|
-
schema = compress_schema(schema, prune_titles=True)
|
|
1067
|
-
|
|
1068
|
-
# Tool parameters must be object-shaped. Wrap primitives in {"value": <schema>}
|
|
1069
|
-
if schema.get("type") != "object":
|
|
1070
|
-
schema = {
|
|
1071
|
-
"type": "object",
|
|
1072
|
-
"properties": {"value": schema},
|
|
1073
|
-
"required": ["value"],
|
|
1074
|
-
}
|
|
1075
|
-
|
|
1076
|
-
# The fn just returns the input as-is (validation happens in the loop)
|
|
1077
|
-
def final_response(**kwargs: Any) -> dict[str, Any]:
|
|
1078
|
-
return kwargs
|
|
1079
|
-
|
|
1080
|
-
return SamplingTool(
|
|
1081
|
-
name="final_response",
|
|
1082
|
-
description=(
|
|
1083
|
-
"Call this tool to provide your final response. "
|
|
1084
|
-
"Use this when you have completed the task and are ready to return the result."
|
|
1085
|
-
),
|
|
1086
|
-
parameters=schema,
|
|
1087
|
-
fn=final_response,
|
|
1088
|
-
)
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
def _extract_text_from_content(
|
|
1092
|
-
content: SamplingMessageContentBlock | list[SamplingMessageContentBlock],
|
|
1093
|
-
) -> str | None:
|
|
1094
|
-
"""Extract text from content block(s).
|
|
1095
|
-
|
|
1096
|
-
Returns the text if content is a TextContent or list containing TextContent,
|
|
1097
|
-
otherwise returns None.
|
|
1098
|
-
"""
|
|
1099
|
-
if isinstance(content, list):
|
|
1100
|
-
for block in content:
|
|
1101
|
-
if isinstance(block, TextContent):
|
|
1102
|
-
return block.text
|
|
1103
|
-
return None
|
|
1104
|
-
elif isinstance(content, TextContent):
|
|
1105
|
-
return content.text
|
|
1106
|
-
return None
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
def _prepare_messages(
|
|
1110
|
-
messages: str | Sequence[str | SamplingMessage],
|
|
1111
|
-
) -> list[SamplingMessage]:
|
|
1112
|
-
"""Convert various message formats to a list of SamplingMessage objects."""
|
|
1113
|
-
if isinstance(messages, str):
|
|
1114
|
-
return [
|
|
1115
|
-
SamplingMessage(
|
|
1116
|
-
content=TextContent(text=messages, type="text"), role="user"
|
|
1117
|
-
)
|
|
1118
|
-
]
|
|
1119
|
-
else:
|
|
1120
|
-
return [
|
|
1121
|
-
SamplingMessage(content=TextContent(text=m, type="text"), role="user")
|
|
1122
|
-
if isinstance(m, str)
|
|
1123
|
-
else m
|
|
1124
|
-
for m in messages
|
|
1125
|
-
]
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
def _prepare_tools(
|
|
1129
|
-
tools: Sequence[SamplingTool | Callable[..., Any]] | None,
|
|
1130
|
-
) -> list[SamplingTool] | None:
|
|
1131
|
-
"""Convert tools to SamplingTool objects."""
|
|
1132
|
-
if tools is None:
|
|
1133
|
-
return None
|
|
1134
|
-
|
|
1135
|
-
sampling_tools: list[SamplingTool] = []
|
|
1136
|
-
for t in tools:
|
|
1137
|
-
if isinstance(t, SamplingTool):
|
|
1138
|
-
sampling_tools.append(t)
|
|
1139
|
-
elif callable(t):
|
|
1140
|
-
sampling_tools.append(SamplingTool.from_function(t))
|
|
1141
|
-
else:
|
|
1142
|
-
raise TypeError(f"Expected SamplingTool or callable, got {type(t)}")
|
|
1143
|
-
|
|
1144
|
-
return sampling_tools if sampling_tools else None
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
def _extract_tool_calls(
|
|
1148
|
-
response: CreateMessageResult | CreateMessageResultWithTools,
|
|
1149
|
-
) -> list[ToolUseContent]:
|
|
1150
|
-
"""Extract tool calls from a response."""
|
|
1151
|
-
content = response.content
|
|
1152
|
-
if isinstance(content, list):
|
|
1153
|
-
return [c for c in content if isinstance(c, ToolUseContent)]
|
|
1154
|
-
elif isinstance(content, ToolUseContent):
|
|
1155
|
-
return [content]
|
|
1156
|
-
return []
|