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
|
@@ -0,0 +1,465 @@
|
|
|
1
|
+
"""Standalone @prompt decorator for FastMCP."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import inspect
|
|
6
|
+
import json
|
|
7
|
+
import warnings
|
|
8
|
+
from collections.abc import Callable
|
|
9
|
+
from dataclasses import dataclass, field
|
|
10
|
+
from typing import (
|
|
11
|
+
TYPE_CHECKING,
|
|
12
|
+
Any,
|
|
13
|
+
Literal,
|
|
14
|
+
Protocol,
|
|
15
|
+
TypeVar,
|
|
16
|
+
overload,
|
|
17
|
+
runtime_checkable,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
import pydantic_core
|
|
21
|
+
from mcp.types import Icon
|
|
22
|
+
|
|
23
|
+
import fastmcp
|
|
24
|
+
from fastmcp.decorators import resolve_task_config
|
|
25
|
+
from fastmcp.exceptions import PromptError
|
|
26
|
+
from fastmcp.prompts.prompt import Prompt, PromptArgument, PromptResult
|
|
27
|
+
from fastmcp.server.dependencies import (
|
|
28
|
+
transform_context_annotations,
|
|
29
|
+
without_injected_parameters,
|
|
30
|
+
)
|
|
31
|
+
from fastmcp.server.tasks.config import TaskConfig
|
|
32
|
+
from fastmcp.tools.tool import AuthCheckCallable
|
|
33
|
+
from fastmcp.utilities.async_utils import call_sync_fn_in_threadpool
|
|
34
|
+
from fastmcp.utilities.json_schema import compress_schema
|
|
35
|
+
from fastmcp.utilities.logging import get_logger
|
|
36
|
+
from fastmcp.utilities.types import get_cached_typeadapter
|
|
37
|
+
|
|
38
|
+
if TYPE_CHECKING:
|
|
39
|
+
from docket import Docket
|
|
40
|
+
from docket.execution import Execution
|
|
41
|
+
|
|
42
|
+
F = TypeVar("F", bound=Callable[..., Any])
|
|
43
|
+
|
|
44
|
+
logger = get_logger(__name__)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@runtime_checkable
|
|
48
|
+
class DecoratedPrompt(Protocol):
|
|
49
|
+
"""Protocol for functions decorated with @prompt."""
|
|
50
|
+
|
|
51
|
+
__fastmcp__: PromptMeta
|
|
52
|
+
|
|
53
|
+
def __call__(self, *args: Any, **kwargs: Any) -> Any: ...
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@dataclass(frozen=True, kw_only=True)
|
|
57
|
+
class PromptMeta:
|
|
58
|
+
"""Metadata attached to functions by the @prompt decorator."""
|
|
59
|
+
|
|
60
|
+
type: Literal["prompt"] = field(default="prompt", init=False)
|
|
61
|
+
name: str | None = None
|
|
62
|
+
version: str | int | None = None
|
|
63
|
+
title: str | None = None
|
|
64
|
+
description: str | None = None
|
|
65
|
+
icons: list[Icon] | None = None
|
|
66
|
+
tags: set[str] | None = None
|
|
67
|
+
meta: dict[str, Any] | None = None
|
|
68
|
+
task: bool | TaskConfig | None = None
|
|
69
|
+
auth: AuthCheckCallable | list[AuthCheckCallable] | None = None
|
|
70
|
+
enabled: bool = True
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class FunctionPrompt(Prompt):
|
|
74
|
+
"""A prompt that is a function."""
|
|
75
|
+
|
|
76
|
+
fn: Callable[..., Any]
|
|
77
|
+
|
|
78
|
+
@classmethod
|
|
79
|
+
def from_function(
|
|
80
|
+
cls,
|
|
81
|
+
fn: Callable[..., Any],
|
|
82
|
+
*,
|
|
83
|
+
metadata: PromptMeta | None = None,
|
|
84
|
+
# Keep individual params for backwards compat
|
|
85
|
+
name: str | None = None,
|
|
86
|
+
version: str | int | None = None,
|
|
87
|
+
title: str | None = None,
|
|
88
|
+
description: str | None = None,
|
|
89
|
+
icons: list[Icon] | None = None,
|
|
90
|
+
tags: set[str] | None = None,
|
|
91
|
+
meta: dict[str, Any] | None = None,
|
|
92
|
+
task: bool | TaskConfig | None = None,
|
|
93
|
+
auth: AuthCheckCallable | list[AuthCheckCallable] | None = None,
|
|
94
|
+
) -> FunctionPrompt:
|
|
95
|
+
"""Create a Prompt from a function.
|
|
96
|
+
|
|
97
|
+
Args:
|
|
98
|
+
fn: The function to wrap
|
|
99
|
+
metadata: PromptMeta object with all configuration. If provided,
|
|
100
|
+
individual parameters must not be passed.
|
|
101
|
+
name, title, etc.: Individual parameters for backwards compatibility.
|
|
102
|
+
Cannot be used together with metadata parameter.
|
|
103
|
+
|
|
104
|
+
The function can return:
|
|
105
|
+
- str: wrapped as single user Message
|
|
106
|
+
- list[Message | str]: converted to list[Message]
|
|
107
|
+
- PromptResult: used directly
|
|
108
|
+
"""
|
|
109
|
+
# Check mutual exclusion
|
|
110
|
+
individual_params_provided = any(
|
|
111
|
+
x is not None
|
|
112
|
+
for x in [name, version, title, description, icons, tags, meta, task, auth]
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
if metadata is not None and individual_params_provided:
|
|
116
|
+
raise TypeError(
|
|
117
|
+
"Cannot pass both 'metadata' and individual parameters to from_function(). "
|
|
118
|
+
"Use metadata alone or individual parameters alone."
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
# Build metadata from kwargs if not provided
|
|
122
|
+
if metadata is None:
|
|
123
|
+
metadata = PromptMeta(
|
|
124
|
+
name=name,
|
|
125
|
+
version=version,
|
|
126
|
+
title=title,
|
|
127
|
+
description=description,
|
|
128
|
+
icons=icons,
|
|
129
|
+
tags=tags,
|
|
130
|
+
meta=meta,
|
|
131
|
+
task=task,
|
|
132
|
+
auth=auth,
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
func_name = (
|
|
136
|
+
metadata.name or getattr(fn, "__name__", None) or fn.__class__.__name__
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
if func_name == "<lambda>":
|
|
140
|
+
raise ValueError("You must provide a name for lambda functions")
|
|
141
|
+
|
|
142
|
+
# Reject functions with *args or **kwargs
|
|
143
|
+
sig = inspect.signature(fn)
|
|
144
|
+
for param in sig.parameters.values():
|
|
145
|
+
if param.kind == inspect.Parameter.VAR_POSITIONAL:
|
|
146
|
+
raise ValueError("Functions with *args are not supported as prompts")
|
|
147
|
+
if param.kind == inspect.Parameter.VAR_KEYWORD:
|
|
148
|
+
raise ValueError("Functions with **kwargs are not supported as prompts")
|
|
149
|
+
|
|
150
|
+
description = metadata.description or inspect.getdoc(fn)
|
|
151
|
+
|
|
152
|
+
# Normalize task to TaskConfig and validate
|
|
153
|
+
task_value = metadata.task
|
|
154
|
+
if task_value is None:
|
|
155
|
+
task_config = TaskConfig(mode="forbidden")
|
|
156
|
+
elif isinstance(task_value, bool):
|
|
157
|
+
task_config = TaskConfig.from_bool(task_value)
|
|
158
|
+
else:
|
|
159
|
+
task_config = task_value
|
|
160
|
+
task_config.validate_function(fn, func_name)
|
|
161
|
+
|
|
162
|
+
# if the fn is a callable class, we need to get the __call__ method from here out
|
|
163
|
+
if not inspect.isroutine(fn):
|
|
164
|
+
fn = fn.__call__
|
|
165
|
+
# if the fn is a staticmethod, we need to work with the underlying function
|
|
166
|
+
if isinstance(fn, staticmethod):
|
|
167
|
+
fn = fn.__func__
|
|
168
|
+
|
|
169
|
+
# Transform Context type annotations to Depends() for unified DI
|
|
170
|
+
fn = transform_context_annotations(fn)
|
|
171
|
+
|
|
172
|
+
# Wrap fn to handle dependency resolution internally
|
|
173
|
+
wrapped_fn = without_injected_parameters(fn)
|
|
174
|
+
type_adapter = get_cached_typeadapter(wrapped_fn)
|
|
175
|
+
parameters = type_adapter.json_schema()
|
|
176
|
+
parameters = compress_schema(parameters, prune_titles=True)
|
|
177
|
+
|
|
178
|
+
# Convert parameters to PromptArguments
|
|
179
|
+
arguments: list[PromptArgument] = []
|
|
180
|
+
if "properties" in parameters:
|
|
181
|
+
for param_name, param in parameters["properties"].items():
|
|
182
|
+
arg_description = param.get("description")
|
|
183
|
+
|
|
184
|
+
# For non-string parameters, append JSON schema info to help users
|
|
185
|
+
# understand the expected format when passing as strings (MCP requirement)
|
|
186
|
+
if param_name in sig.parameters:
|
|
187
|
+
sig_param = sig.parameters[param_name]
|
|
188
|
+
if (
|
|
189
|
+
sig_param.annotation != inspect.Parameter.empty
|
|
190
|
+
and sig_param.annotation is not str
|
|
191
|
+
):
|
|
192
|
+
# Get the JSON schema for this specific parameter type
|
|
193
|
+
try:
|
|
194
|
+
param_adapter = get_cached_typeadapter(sig_param.annotation)
|
|
195
|
+
param_schema = param_adapter.json_schema()
|
|
196
|
+
|
|
197
|
+
# Create compact schema representation
|
|
198
|
+
schema_str = json.dumps(param_schema, separators=(",", ":"))
|
|
199
|
+
|
|
200
|
+
# Append schema info to description
|
|
201
|
+
schema_note = f"Provide as a JSON string matching the following schema: {schema_str}"
|
|
202
|
+
if arg_description:
|
|
203
|
+
arg_description = f"{arg_description}\n\n{schema_note}"
|
|
204
|
+
else:
|
|
205
|
+
arg_description = schema_note
|
|
206
|
+
except Exception as e:
|
|
207
|
+
# If schema generation fails, skip enhancement
|
|
208
|
+
logger.debug(
|
|
209
|
+
"Failed to generate schema for prompt argument %s: %s",
|
|
210
|
+
param_name,
|
|
211
|
+
e,
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
arguments.append(
|
|
215
|
+
PromptArgument(
|
|
216
|
+
name=param_name,
|
|
217
|
+
description=arg_description,
|
|
218
|
+
required=param_name in parameters.get("required", []),
|
|
219
|
+
)
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
return cls(
|
|
223
|
+
name=func_name,
|
|
224
|
+
version=str(metadata.version) if metadata.version is not None else None,
|
|
225
|
+
title=metadata.title,
|
|
226
|
+
description=description,
|
|
227
|
+
icons=metadata.icons,
|
|
228
|
+
arguments=arguments,
|
|
229
|
+
tags=metadata.tags or set(),
|
|
230
|
+
fn=wrapped_fn,
|
|
231
|
+
meta=metadata.meta,
|
|
232
|
+
task_config=task_config,
|
|
233
|
+
auth=metadata.auth,
|
|
234
|
+
)
|
|
235
|
+
|
|
236
|
+
def _convert_string_arguments(self, kwargs: dict[str, Any]) -> dict[str, Any]:
|
|
237
|
+
"""Convert string arguments to expected types based on function signature."""
|
|
238
|
+
from fastmcp.server.dependencies import without_injected_parameters
|
|
239
|
+
|
|
240
|
+
wrapper_fn = without_injected_parameters(self.fn)
|
|
241
|
+
sig = inspect.signature(wrapper_fn)
|
|
242
|
+
converted_kwargs = {}
|
|
243
|
+
|
|
244
|
+
for param_name, param_value in kwargs.items():
|
|
245
|
+
if param_name in sig.parameters:
|
|
246
|
+
param = sig.parameters[param_name]
|
|
247
|
+
|
|
248
|
+
# If parameter has no annotation or annotation is str, pass as-is
|
|
249
|
+
if (
|
|
250
|
+
param.annotation == inspect.Parameter.empty
|
|
251
|
+
or param.annotation is str
|
|
252
|
+
) or not isinstance(param_value, str):
|
|
253
|
+
converted_kwargs[param_name] = param_value
|
|
254
|
+
else:
|
|
255
|
+
# Try to convert string argument using type adapter
|
|
256
|
+
try:
|
|
257
|
+
adapter = get_cached_typeadapter(param.annotation)
|
|
258
|
+
# Try JSON parsing first for complex types
|
|
259
|
+
try:
|
|
260
|
+
converted_kwargs[param_name] = adapter.validate_json(
|
|
261
|
+
param_value
|
|
262
|
+
)
|
|
263
|
+
except (ValueError, TypeError, pydantic_core.ValidationError):
|
|
264
|
+
# Fallback to direct validation
|
|
265
|
+
converted_kwargs[param_name] = adapter.validate_python(
|
|
266
|
+
param_value
|
|
267
|
+
)
|
|
268
|
+
except (ValueError, TypeError, pydantic_core.ValidationError) as e:
|
|
269
|
+
# If conversion fails, provide informative error
|
|
270
|
+
raise PromptError(
|
|
271
|
+
f"Could not convert argument '{param_name}' with value '{param_value}' "
|
|
272
|
+
f"to expected type {param.annotation}. Error: {e}"
|
|
273
|
+
) from e
|
|
274
|
+
else:
|
|
275
|
+
# Parameter not in function signature, pass as-is
|
|
276
|
+
converted_kwargs[param_name] = param_value
|
|
277
|
+
|
|
278
|
+
return converted_kwargs
|
|
279
|
+
|
|
280
|
+
async def render(
|
|
281
|
+
self,
|
|
282
|
+
arguments: dict[str, Any] | None = None,
|
|
283
|
+
) -> PromptResult:
|
|
284
|
+
"""Render the prompt with arguments."""
|
|
285
|
+
# Validate required arguments
|
|
286
|
+
if self.arguments:
|
|
287
|
+
required = {arg.name for arg in self.arguments if arg.required}
|
|
288
|
+
provided = set(arguments or {})
|
|
289
|
+
missing = required - provided
|
|
290
|
+
if missing:
|
|
291
|
+
raise ValueError(f"Missing required arguments: {missing}")
|
|
292
|
+
|
|
293
|
+
try:
|
|
294
|
+
# Prepare arguments
|
|
295
|
+
kwargs = arguments.copy() if arguments else {}
|
|
296
|
+
|
|
297
|
+
# Convert string arguments to expected types BEFORE validation
|
|
298
|
+
kwargs = self._convert_string_arguments(kwargs)
|
|
299
|
+
|
|
300
|
+
# self.fn is wrapped by without_injected_parameters which handles
|
|
301
|
+
# dependency resolution internally
|
|
302
|
+
if inspect.iscoroutinefunction(self.fn):
|
|
303
|
+
result = await self.fn(**kwargs)
|
|
304
|
+
else:
|
|
305
|
+
# Run sync functions in threadpool to avoid blocking the event loop
|
|
306
|
+
result = await call_sync_fn_in_threadpool(self.fn, **kwargs)
|
|
307
|
+
# Handle sync wrappers that return awaitables (e.g., partial(async_fn))
|
|
308
|
+
if inspect.isawaitable(result):
|
|
309
|
+
result = await result
|
|
310
|
+
|
|
311
|
+
return self.convert_result(result)
|
|
312
|
+
except Exception as e:
|
|
313
|
+
logger.exception(f"Error rendering prompt {self.name}")
|
|
314
|
+
raise PromptError(f"Error rendering prompt {self.name}.") from e
|
|
315
|
+
|
|
316
|
+
def register_with_docket(self, docket: Docket) -> None:
|
|
317
|
+
"""Register this prompt with docket for background execution.
|
|
318
|
+
|
|
319
|
+
FunctionPrompt registers the underlying function, which has the user's
|
|
320
|
+
Depends parameters for docket to resolve.
|
|
321
|
+
"""
|
|
322
|
+
if not self.task_config.supports_tasks():
|
|
323
|
+
return
|
|
324
|
+
docket.register(self.fn, names=[self.key])
|
|
325
|
+
|
|
326
|
+
async def add_to_docket(
|
|
327
|
+
self,
|
|
328
|
+
docket: Docket,
|
|
329
|
+
arguments: dict[str, Any] | None,
|
|
330
|
+
*,
|
|
331
|
+
fn_key: str | None = None,
|
|
332
|
+
task_key: str | None = None,
|
|
333
|
+
**kwargs: Any,
|
|
334
|
+
) -> Execution:
|
|
335
|
+
"""Schedule this prompt for background execution via docket.
|
|
336
|
+
|
|
337
|
+
FunctionPrompt splats the arguments dict since .fn expects **kwargs.
|
|
338
|
+
|
|
339
|
+
Args:
|
|
340
|
+
docket: The Docket instance
|
|
341
|
+
arguments: Prompt arguments
|
|
342
|
+
fn_key: Function lookup key in Docket registry (defaults to self.key)
|
|
343
|
+
task_key: Redis storage key for the result
|
|
344
|
+
**kwargs: Additional kwargs passed to docket.add()
|
|
345
|
+
"""
|
|
346
|
+
lookup_key = fn_key or self.key
|
|
347
|
+
if task_key:
|
|
348
|
+
kwargs["key"] = task_key
|
|
349
|
+
return await docket.add(lookup_key, **kwargs)(**(arguments or {}))
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
@overload
|
|
353
|
+
def prompt(fn: F) -> F: ...
|
|
354
|
+
@overload
|
|
355
|
+
def prompt(
|
|
356
|
+
name_or_fn: str,
|
|
357
|
+
*,
|
|
358
|
+
version: str | int | None = None,
|
|
359
|
+
title: str | None = None,
|
|
360
|
+
description: str | None = None,
|
|
361
|
+
icons: list[Icon] | None = None,
|
|
362
|
+
tags: set[str] | None = None,
|
|
363
|
+
meta: dict[str, Any] | None = None,
|
|
364
|
+
task: bool | TaskConfig | None = None,
|
|
365
|
+
auth: AuthCheckCallable | list[AuthCheckCallable] | None = None,
|
|
366
|
+
) -> Callable[[F], F]: ...
|
|
367
|
+
@overload
|
|
368
|
+
def prompt(
|
|
369
|
+
name_or_fn: None = None,
|
|
370
|
+
*,
|
|
371
|
+
name: str | None = None,
|
|
372
|
+
version: str | int | None = None,
|
|
373
|
+
title: str | None = None,
|
|
374
|
+
description: str | None = None,
|
|
375
|
+
icons: list[Icon] | None = None,
|
|
376
|
+
tags: set[str] | None = None,
|
|
377
|
+
meta: dict[str, Any] | None = None,
|
|
378
|
+
task: bool | TaskConfig | None = None,
|
|
379
|
+
auth: AuthCheckCallable | list[AuthCheckCallable] | None = None,
|
|
380
|
+
) -> Callable[[F], F]: ...
|
|
381
|
+
|
|
382
|
+
|
|
383
|
+
def prompt(
|
|
384
|
+
name_or_fn: str | Callable[..., Any] | None = None,
|
|
385
|
+
*,
|
|
386
|
+
name: str | None = None,
|
|
387
|
+
version: str | int | None = None,
|
|
388
|
+
title: str | None = None,
|
|
389
|
+
description: str | None = None,
|
|
390
|
+
icons: list[Icon] | None = None,
|
|
391
|
+
tags: set[str] | None = None,
|
|
392
|
+
meta: dict[str, Any] | None = None,
|
|
393
|
+
task: bool | TaskConfig | None = None,
|
|
394
|
+
auth: AuthCheckCallable | list[AuthCheckCallable] | None = None,
|
|
395
|
+
) -> Any:
|
|
396
|
+
"""Standalone decorator to mark a function as an MCP prompt.
|
|
397
|
+
|
|
398
|
+
Returns the original function with metadata attached. Register with a server
|
|
399
|
+
using mcp.add_prompt().
|
|
400
|
+
"""
|
|
401
|
+
if isinstance(name_or_fn, classmethod):
|
|
402
|
+
raise TypeError(
|
|
403
|
+
"To decorate a classmethod, use @classmethod above @prompt. "
|
|
404
|
+
"See https://gofastmcp.com/servers/prompts#using-with-methods"
|
|
405
|
+
)
|
|
406
|
+
|
|
407
|
+
def create_prompt(
|
|
408
|
+
fn: Callable[..., Any], prompt_name: str | None
|
|
409
|
+
) -> FunctionPrompt:
|
|
410
|
+
# Create metadata first, then pass it
|
|
411
|
+
prompt_meta = PromptMeta(
|
|
412
|
+
name=prompt_name,
|
|
413
|
+
version=version,
|
|
414
|
+
title=title,
|
|
415
|
+
description=description,
|
|
416
|
+
icons=icons,
|
|
417
|
+
tags=tags,
|
|
418
|
+
meta=meta,
|
|
419
|
+
task=resolve_task_config(task),
|
|
420
|
+
auth=auth,
|
|
421
|
+
)
|
|
422
|
+
return FunctionPrompt.from_function(fn, metadata=prompt_meta)
|
|
423
|
+
|
|
424
|
+
def attach_metadata(fn: F, prompt_name: str | None) -> F:
|
|
425
|
+
metadata = PromptMeta(
|
|
426
|
+
name=prompt_name,
|
|
427
|
+
version=version,
|
|
428
|
+
title=title,
|
|
429
|
+
description=description,
|
|
430
|
+
icons=icons,
|
|
431
|
+
tags=tags,
|
|
432
|
+
meta=meta,
|
|
433
|
+
task=task,
|
|
434
|
+
auth=auth,
|
|
435
|
+
)
|
|
436
|
+
target = fn.__func__ if hasattr(fn, "__func__") else fn
|
|
437
|
+
target.__fastmcp__ = metadata
|
|
438
|
+
return fn
|
|
439
|
+
|
|
440
|
+
def decorator(fn: F, prompt_name: str | None) -> F:
|
|
441
|
+
if fastmcp.settings.decorator_mode == "object":
|
|
442
|
+
warnings.warn(
|
|
443
|
+
"decorator_mode='object' is deprecated and will be removed in a future version. "
|
|
444
|
+
"Decorators now return the original function with metadata attached.",
|
|
445
|
+
DeprecationWarning,
|
|
446
|
+
stacklevel=4,
|
|
447
|
+
)
|
|
448
|
+
return create_prompt(fn, prompt_name) # type: ignore[return-value]
|
|
449
|
+
return attach_metadata(fn, prompt_name)
|
|
450
|
+
|
|
451
|
+
if inspect.isroutine(name_or_fn):
|
|
452
|
+
return decorator(name_or_fn, name)
|
|
453
|
+
elif isinstance(name_or_fn, str):
|
|
454
|
+
if name is not None:
|
|
455
|
+
raise TypeError("Cannot specify name both as first argument and keyword")
|
|
456
|
+
prompt_name = name_or_fn
|
|
457
|
+
elif name_or_fn is None:
|
|
458
|
+
prompt_name = name
|
|
459
|
+
else:
|
|
460
|
+
raise TypeError(f"Invalid first argument: {type(name_or_fn)}")
|
|
461
|
+
|
|
462
|
+
def wrapper(fn: F) -> F:
|
|
463
|
+
return decorator(fn, prompt_name)
|
|
464
|
+
|
|
465
|
+
return wrapper
|