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
|
@@ -21,10 +21,10 @@ try:
|
|
|
21
21
|
ChatCompletionUserMessageParam,
|
|
22
22
|
)
|
|
23
23
|
from openai.types.shared.chat_model import ChatModel
|
|
24
|
-
except ImportError:
|
|
24
|
+
except ImportError as e:
|
|
25
25
|
raise ImportError(
|
|
26
26
|
"The `openai` package is not installed. Please install `fastmcp[openai]` or add `openai` to your dependencies manually."
|
|
27
|
-
)
|
|
27
|
+
) from e
|
|
28
28
|
|
|
29
29
|
from typing_extensions import override
|
|
30
30
|
|
|
@@ -164,7 +164,7 @@ class OpenAISamplingHandler(BaseLLMSamplingHandler):
|
|
|
164
164
|
) -> ChatModel:
|
|
165
165
|
for model_option in self._iter_models_from_preferences(model_preferences):
|
|
166
166
|
if model_option in get_args(ChatModel):
|
|
167
|
-
chosen_model: ChatModel = model_option #
|
|
167
|
+
chosen_model: ChatModel = model_option # type: ignore[assignment]
|
|
168
168
|
return chosen_model
|
|
169
169
|
|
|
170
170
|
return self.default_model
|
|
@@ -1,38 +1,37 @@
|
|
|
1
|
-
"""
|
|
1
|
+
"""Deprecated: Import from fastmcp.server.openapi instead."""
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
from .server import FastMCPOpenAPI
|
|
3
|
+
import warnings
|
|
5
4
|
|
|
6
|
-
|
|
7
|
-
|
|
5
|
+
from fastmcp.server.openapi import (
|
|
6
|
+
ComponentFn,
|
|
7
|
+
DEFAULT_ROUTE_MAPPINGS,
|
|
8
|
+
FastMCPOpenAPI,
|
|
8
9
|
MCPType,
|
|
10
|
+
OpenAPIResource,
|
|
11
|
+
OpenAPIResourceTemplate,
|
|
12
|
+
OpenAPITool,
|
|
9
13
|
RouteMap,
|
|
10
14
|
RouteMapFn,
|
|
11
|
-
ComponentFn,
|
|
12
|
-
DEFAULT_ROUTE_MAPPINGS,
|
|
13
15
|
_determine_route_type,
|
|
14
16
|
)
|
|
15
17
|
|
|
16
|
-
#
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
18
|
+
# Deprecated in 2.14 when OpenAPI support was promoted out of experimental
|
|
19
|
+
warnings.warn(
|
|
20
|
+
"Importing from fastmcp.experimental.server.openapi is deprecated. "
|
|
21
|
+
"Import from fastmcp.server.openapi instead.",
|
|
22
|
+
DeprecationWarning,
|
|
23
|
+
stacklevel=2,
|
|
21
24
|
)
|
|
22
25
|
|
|
23
|
-
# Export public symbols - maintaining backward compatibility
|
|
24
26
|
__all__ = [
|
|
25
|
-
|
|
27
|
+
"DEFAULT_ROUTE_MAPPINGS",
|
|
28
|
+
"ComponentFn",
|
|
26
29
|
"FastMCPOpenAPI",
|
|
27
|
-
# Routing
|
|
28
30
|
"MCPType",
|
|
31
|
+
"OpenAPIResource",
|
|
32
|
+
"OpenAPIResourceTemplate",
|
|
33
|
+
"OpenAPITool",
|
|
29
34
|
"RouteMap",
|
|
30
35
|
"RouteMapFn",
|
|
31
|
-
"ComponentFn",
|
|
32
|
-
"DEFAULT_ROUTE_MAPPINGS",
|
|
33
36
|
"_determine_route_type",
|
|
34
|
-
# Components
|
|
35
|
-
"OpenAPITool",
|
|
36
|
-
"OpenAPIResource",
|
|
37
|
-
"OpenAPIResourceTemplate",
|
|
38
37
|
]
|
|
@@ -1,68 +1,37 @@
|
|
|
1
|
-
"""
|
|
1
|
+
"""Deprecated: Import from fastmcp.utilities.openapi instead."""
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
import warnings
|
|
4
|
+
|
|
5
|
+
from fastmcp.utilities.openapi import (
|
|
5
6
|
HTTPRoute,
|
|
6
7
|
HttpMethod,
|
|
7
|
-
JsonSchema,
|
|
8
8
|
ParameterInfo,
|
|
9
9
|
ParameterLocation,
|
|
10
10
|
RequestBodyInfo,
|
|
11
11
|
ResponseInfo,
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
# Import from parser
|
|
15
|
-
from .parser import parse_openapi_to_http_routes
|
|
16
|
-
|
|
17
|
-
# Import from formatters
|
|
18
|
-
from .formatters import (
|
|
19
|
-
format_array_parameter,
|
|
20
|
-
format_deep_object_parameter,
|
|
21
|
-
format_description_with_responses,
|
|
22
|
-
format_json_for_description,
|
|
12
|
+
extract_output_schema_from_responses,
|
|
23
13
|
format_simple_description,
|
|
24
|
-
|
|
25
|
-
)
|
|
26
|
-
|
|
27
|
-
# Import from schemas
|
|
28
|
-
from .schemas import (
|
|
14
|
+
parse_openapi_to_http_routes,
|
|
29
15
|
_combine_schemas,
|
|
30
|
-
extract_output_schema_from_responses,
|
|
31
|
-
clean_schema_for_display,
|
|
32
|
-
_make_optional_parameter_nullable,
|
|
33
16
|
)
|
|
34
17
|
|
|
35
|
-
#
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
18
|
+
# Deprecated in 2.14 when OpenAPI support was promoted out of experimental
|
|
19
|
+
warnings.warn(
|
|
20
|
+
"Importing from fastmcp.experimental.utilities.openapi is deprecated. "
|
|
21
|
+
"Import from fastmcp.utilities.openapi instead.",
|
|
22
|
+
DeprecationWarning,
|
|
23
|
+
stacklevel=2,
|
|
39
24
|
)
|
|
40
25
|
|
|
41
|
-
# Export public symbols - maintaining backward compatibility
|
|
42
26
|
__all__ = [
|
|
43
|
-
# Models
|
|
44
27
|
"HTTPRoute",
|
|
28
|
+
"HttpMethod",
|
|
45
29
|
"ParameterInfo",
|
|
30
|
+
"ParameterLocation",
|
|
46
31
|
"RequestBodyInfo",
|
|
47
32
|
"ResponseInfo",
|
|
48
|
-
"HttpMethod",
|
|
49
|
-
"ParameterLocation",
|
|
50
|
-
"JsonSchema",
|
|
51
|
-
# Parser
|
|
52
|
-
"parse_openapi_to_http_routes",
|
|
53
|
-
# Formatters
|
|
54
|
-
"format_array_parameter",
|
|
55
|
-
"format_deep_object_parameter",
|
|
56
|
-
"format_description_with_responses",
|
|
57
|
-
"format_json_for_description",
|
|
58
|
-
"format_simple_description",
|
|
59
|
-
"generate_example_from_schema",
|
|
60
|
-
# Schemas
|
|
61
33
|
"_combine_schemas",
|
|
62
34
|
"extract_output_schema_from_responses",
|
|
63
|
-
"
|
|
64
|
-
"
|
|
65
|
-
# JSON Schema Converter
|
|
66
|
-
"convert_openapi_schema_to_json_schema",
|
|
67
|
-
"convert_schema_definitions",
|
|
35
|
+
"format_simple_description",
|
|
36
|
+
"parse_openapi_to_http_routes",
|
|
68
37
|
]
|
fastmcp/mcp_config.py
CHANGED
|
@@ -101,7 +101,7 @@ class _TransformingMCPServerMixin(FastMCPBaseModel):
|
|
|
101
101
|
ClientTransport, # pyright: ignore[reportUnusedImport]
|
|
102
102
|
)
|
|
103
103
|
|
|
104
|
-
transport: ClientTransport = super().to_transport() # pyright: ignore[reportUnknownMemberType, reportAttributeAccessIssue, reportUnknownVariableType]
|
|
104
|
+
transport: ClientTransport = super().to_transport() # pyright: ignore[reportUnknownMemberType, reportAttributeAccessIssue, reportUnknownVariableType] # ty: ignore[unresolved-attribute]
|
|
105
105
|
transport = cast(ClientTransport, transport)
|
|
106
106
|
|
|
107
107
|
client: Client[ClientTransport] = Client(transport=transport, name=client_name)
|
|
@@ -288,9 +288,8 @@ class MCPConfig(BaseModel):
|
|
|
288
288
|
@classmethod
|
|
289
289
|
def from_file(cls, file_path: Path) -> Self:
|
|
290
290
|
"""Load configuration from JSON file."""
|
|
291
|
-
if file_path.exists():
|
|
292
|
-
|
|
293
|
-
return cls.model_validate_json(content)
|
|
291
|
+
if file_path.exists() and (content := file_path.read_text().strip()):
|
|
292
|
+
return cls.model_validate_json(content)
|
|
294
293
|
|
|
295
294
|
raise ValueError(f"No MCP servers defined in the config: {file_path}")
|
|
296
295
|
|
fastmcp/prompts/__init__.py
CHANGED
fastmcp/prompts/prompt.py
CHANGED
|
@@ -4,24 +4,23 @@ from __future__ import annotations as _annotations
|
|
|
4
4
|
|
|
5
5
|
import inspect
|
|
6
6
|
import json
|
|
7
|
-
from abc import ABC, abstractmethod
|
|
8
7
|
from collections.abc import Awaitable, Callable, Sequence
|
|
9
|
-
from typing import Any
|
|
8
|
+
from typing import Annotated, Any
|
|
10
9
|
|
|
11
10
|
import pydantic_core
|
|
12
|
-
from mcp.types import ContentBlock, PromptMessage, Role, TextContent
|
|
11
|
+
from mcp.types import ContentBlock, Icon, PromptMessage, Role, TextContent
|
|
13
12
|
from mcp.types import Prompt as MCPPrompt
|
|
14
13
|
from mcp.types import PromptArgument as MCPPromptArgument
|
|
15
14
|
from pydantic import Field, TypeAdapter
|
|
16
15
|
|
|
17
16
|
from fastmcp.exceptions import PromptError
|
|
18
|
-
from fastmcp.server.dependencies import get_context
|
|
17
|
+
from fastmcp.server.dependencies import get_context, without_injected_parameters
|
|
18
|
+
from fastmcp.server.tasks.config import TaskConfig
|
|
19
19
|
from fastmcp.utilities.components import FastMCPComponent
|
|
20
20
|
from fastmcp.utilities.json_schema import compress_schema
|
|
21
21
|
from fastmcp.utilities.logging import get_logger
|
|
22
22
|
from fastmcp.utilities.types import (
|
|
23
23
|
FastMCPBaseModel,
|
|
24
|
-
find_kwarg_by_type,
|
|
25
24
|
get_cached_typeadapter,
|
|
26
25
|
)
|
|
27
26
|
|
|
@@ -62,7 +61,7 @@ class PromptArgument(FastMCPBaseModel):
|
|
|
62
61
|
)
|
|
63
62
|
|
|
64
63
|
|
|
65
|
-
class Prompt(FastMCPComponent
|
|
64
|
+
class Prompt(FastMCPComponent):
|
|
66
65
|
"""A prompt template that can be rendered with parameters."""
|
|
67
66
|
|
|
68
67
|
arguments: list[PromptArgument] | None = Field(
|
|
@@ -106,6 +105,7 @@ class Prompt(FastMCPComponent, ABC):
|
|
|
106
105
|
description=overrides.get("description", self.description),
|
|
107
106
|
arguments=arguments,
|
|
108
107
|
title=overrides.get("title", self.title),
|
|
108
|
+
icons=overrides.get("icons", self.icons),
|
|
109
109
|
_meta=overrides.get(
|
|
110
110
|
"_meta", self.get_meta(include_fastmcp_meta=include_fastmcp_meta)
|
|
111
111
|
),
|
|
@@ -117,9 +117,11 @@ class Prompt(FastMCPComponent, ABC):
|
|
|
117
117
|
name: str | None = None,
|
|
118
118
|
title: str | None = None,
|
|
119
119
|
description: str | None = None,
|
|
120
|
+
icons: list[Icon] | None = None,
|
|
120
121
|
tags: set[str] | None = None,
|
|
121
122
|
enabled: bool | None = None,
|
|
122
123
|
meta: dict[str, Any] | None = None,
|
|
124
|
+
task: bool | TaskConfig | None = None,
|
|
123
125
|
) -> FunctionPrompt:
|
|
124
126
|
"""Create a Prompt from a function.
|
|
125
127
|
|
|
@@ -134,24 +136,33 @@ class Prompt(FastMCPComponent, ABC):
|
|
|
134
136
|
name=name,
|
|
135
137
|
title=title,
|
|
136
138
|
description=description,
|
|
139
|
+
icons=icons,
|
|
137
140
|
tags=tags,
|
|
138
141
|
enabled=enabled,
|
|
139
142
|
meta=meta,
|
|
143
|
+
task=task,
|
|
140
144
|
)
|
|
141
145
|
|
|
142
|
-
@abstractmethod
|
|
143
146
|
async def render(
|
|
144
147
|
self,
|
|
145
148
|
arguments: dict[str, Any] | None = None,
|
|
146
149
|
) -> list[PromptMessage]:
|
|
147
|
-
"""Render the prompt with arguments.
|
|
148
|
-
|
|
150
|
+
"""Render the prompt with arguments.
|
|
151
|
+
|
|
152
|
+
This method is not implemented in the base Prompt class and must be
|
|
153
|
+
implemented by subclasses.
|
|
154
|
+
"""
|
|
155
|
+
raise NotImplementedError("Subclasses must implement render()")
|
|
149
156
|
|
|
150
157
|
|
|
151
158
|
class FunctionPrompt(Prompt):
|
|
152
159
|
"""A prompt that is a function."""
|
|
153
160
|
|
|
154
161
|
fn: Callable[..., PromptResult | Awaitable[PromptResult]]
|
|
162
|
+
task_config: Annotated[
|
|
163
|
+
TaskConfig,
|
|
164
|
+
Field(description="Background task execution configuration (SEP-1686)."),
|
|
165
|
+
] = Field(default_factory=lambda: TaskConfig(mode="forbidden"))
|
|
155
166
|
|
|
156
167
|
@classmethod
|
|
157
168
|
def from_function(
|
|
@@ -160,9 +171,11 @@ class FunctionPrompt(Prompt):
|
|
|
160
171
|
name: str | None = None,
|
|
161
172
|
title: str | None = None,
|
|
162
173
|
description: str | None = None,
|
|
174
|
+
icons: list[Icon] | None = None,
|
|
163
175
|
tags: set[str] | None = None,
|
|
164
176
|
enabled: bool | None = None,
|
|
165
177
|
meta: dict[str, Any] | None = None,
|
|
178
|
+
task: bool | TaskConfig | None = None,
|
|
166
179
|
) -> FunctionPrompt:
|
|
167
180
|
"""Create a Prompt from a function.
|
|
168
181
|
|
|
@@ -172,7 +185,6 @@ class FunctionPrompt(Prompt):
|
|
|
172
185
|
- A dict (converted to a message)
|
|
173
186
|
- A sequence of any of the above
|
|
174
187
|
"""
|
|
175
|
-
from fastmcp.server.context import Context
|
|
176
188
|
|
|
177
189
|
func_name = name or getattr(fn, "__name__", None) or fn.__class__.__name__
|
|
178
190
|
|
|
@@ -188,25 +200,27 @@ class FunctionPrompt(Prompt):
|
|
|
188
200
|
|
|
189
201
|
description = description or inspect.getdoc(fn)
|
|
190
202
|
|
|
203
|
+
# Normalize task to TaskConfig and validate
|
|
204
|
+
if task is None:
|
|
205
|
+
task_config = TaskConfig(mode="forbidden")
|
|
206
|
+
elif isinstance(task, bool):
|
|
207
|
+
task_config = TaskConfig.from_bool(task)
|
|
208
|
+
else:
|
|
209
|
+
task_config = task
|
|
210
|
+
task_config.validate_function(fn, func_name)
|
|
211
|
+
|
|
191
212
|
# if the fn is a callable class, we need to get the __call__ method from here out
|
|
192
213
|
if not inspect.isroutine(fn):
|
|
193
214
|
fn = fn.__call__
|
|
194
215
|
# if the fn is a staticmethod, we need to work with the underlying function
|
|
195
216
|
if isinstance(fn, staticmethod):
|
|
196
|
-
fn = fn.__func__
|
|
217
|
+
fn = fn.__func__ # type: ignore[assignment]
|
|
197
218
|
|
|
198
|
-
|
|
219
|
+
# Wrap fn to handle dependency resolution internally
|
|
220
|
+
wrapped_fn = without_injected_parameters(fn)
|
|
221
|
+
type_adapter = get_cached_typeadapter(wrapped_fn)
|
|
199
222
|
parameters = type_adapter.json_schema()
|
|
200
|
-
|
|
201
|
-
# Auto-detect context parameter if not provided
|
|
202
|
-
|
|
203
|
-
context_kwarg = find_kwarg_by_type(fn, kwarg_type=Context)
|
|
204
|
-
if context_kwarg:
|
|
205
|
-
prune_params = [context_kwarg]
|
|
206
|
-
else:
|
|
207
|
-
prune_params = None
|
|
208
|
-
|
|
209
|
-
parameters = compress_schema(parameters, prune_params=prune_params)
|
|
223
|
+
parameters = compress_schema(parameters, prune_titles=True)
|
|
210
224
|
|
|
211
225
|
# Convert parameters to PromptArguments
|
|
212
226
|
arguments: list[PromptArgument] = []
|
|
@@ -221,7 +235,6 @@ class FunctionPrompt(Prompt):
|
|
|
221
235
|
if (
|
|
222
236
|
sig_param.annotation != inspect.Parameter.empty
|
|
223
237
|
and sig_param.annotation is not str
|
|
224
|
-
and param_name != context_kwarg
|
|
225
238
|
):
|
|
226
239
|
# Get the JSON schema for this specific parameter type
|
|
227
240
|
try:
|
|
@@ -253,40 +266,32 @@ class FunctionPrompt(Prompt):
|
|
|
253
266
|
name=func_name,
|
|
254
267
|
title=title,
|
|
255
268
|
description=description,
|
|
269
|
+
icons=icons,
|
|
256
270
|
arguments=arguments,
|
|
257
271
|
tags=tags or set(),
|
|
258
272
|
enabled=enabled if enabled is not None else True,
|
|
259
|
-
fn=
|
|
273
|
+
fn=wrapped_fn,
|
|
260
274
|
meta=meta,
|
|
275
|
+
task_config=task_config,
|
|
261
276
|
)
|
|
262
277
|
|
|
263
278
|
def _convert_string_arguments(self, kwargs: dict[str, Any]) -> dict[str, Any]:
|
|
264
279
|
"""Convert string arguments to expected types based on function signature."""
|
|
265
|
-
from fastmcp.server.
|
|
280
|
+
from fastmcp.server.dependencies import without_injected_parameters
|
|
266
281
|
|
|
267
|
-
|
|
282
|
+
wrapper_fn = without_injected_parameters(self.fn)
|
|
283
|
+
sig = inspect.signature(wrapper_fn)
|
|
268
284
|
converted_kwargs = {}
|
|
269
285
|
|
|
270
|
-
# Find context parameter name if any
|
|
271
|
-
context_param_name = find_kwarg_by_type(self.fn, kwarg_type=Context)
|
|
272
|
-
|
|
273
286
|
for param_name, param_value in kwargs.items():
|
|
274
287
|
if param_name in sig.parameters:
|
|
275
288
|
param = sig.parameters[param_name]
|
|
276
289
|
|
|
277
|
-
# Skip Context parameters - they're handled separately
|
|
278
|
-
if param_name == context_param_name:
|
|
279
|
-
converted_kwargs[param_name] = param_value
|
|
280
|
-
continue
|
|
281
|
-
|
|
282
290
|
# If parameter has no annotation or annotation is str, pass as-is
|
|
283
291
|
if (
|
|
284
292
|
param.annotation == inspect.Parameter.empty
|
|
285
293
|
or param.annotation is str
|
|
286
|
-
):
|
|
287
|
-
converted_kwargs[param_name] = param_value
|
|
288
|
-
# If argument is not a string, pass as-is (already properly typed)
|
|
289
|
-
elif not isinstance(param_value, str):
|
|
294
|
+
) or not isinstance(param_value, str):
|
|
290
295
|
converted_kwargs[param_name] = param_value
|
|
291
296
|
else:
|
|
292
297
|
# Try to convert string argument using type adapter
|
|
@@ -307,7 +312,7 @@ class FunctionPrompt(Prompt):
|
|
|
307
312
|
raise PromptError(
|
|
308
313
|
f"Could not convert argument '{param_name}' with value '{param_value}' "
|
|
309
314
|
f"to expected type {param.annotation}. Error: {e}"
|
|
310
|
-
)
|
|
315
|
+
) from e
|
|
311
316
|
else:
|
|
312
317
|
# Parameter not in function signature, pass as-is
|
|
313
318
|
converted_kwargs[param_name] = param_value
|
|
@@ -319,8 +324,6 @@ class FunctionPrompt(Prompt):
|
|
|
319
324
|
arguments: dict[str, Any] | None = None,
|
|
320
325
|
) -> list[PromptMessage]:
|
|
321
326
|
"""Render the prompt with arguments."""
|
|
322
|
-
from fastmcp.server.context import Context
|
|
323
|
-
|
|
324
327
|
# Validate required arguments
|
|
325
328
|
if self.arguments:
|
|
326
329
|
required = {arg.name for arg in self.arguments if arg.required}
|
|
@@ -330,16 +333,14 @@ class FunctionPrompt(Prompt):
|
|
|
330
333
|
raise ValueError(f"Missing required arguments: {missing}")
|
|
331
334
|
|
|
332
335
|
try:
|
|
333
|
-
# Prepare arguments
|
|
336
|
+
# Prepare arguments
|
|
334
337
|
kwargs = arguments.copy() if arguments else {}
|
|
335
|
-
context_kwarg = find_kwarg_by_type(self.fn, kwarg_type=Context)
|
|
336
|
-
if context_kwarg and context_kwarg not in kwargs:
|
|
337
|
-
kwargs[context_kwarg] = get_context()
|
|
338
338
|
|
|
339
|
-
# Convert string arguments to expected types
|
|
339
|
+
# Convert string arguments to expected types BEFORE validation
|
|
340
340
|
kwargs = self._convert_string_arguments(kwargs)
|
|
341
341
|
|
|
342
|
-
#
|
|
342
|
+
# self.fn is wrapped by without_injected_parameters which handles
|
|
343
|
+
# dependency resolution internally
|
|
343
344
|
result = self.fn(**kwargs)
|
|
344
345
|
if inspect.isawaitable(result):
|
|
345
346
|
result = await result
|
|
@@ -369,10 +370,12 @@ class FunctionPrompt(Prompt):
|
|
|
369
370
|
content=TextContent(type="text", text=content),
|
|
370
371
|
)
|
|
371
372
|
)
|
|
372
|
-
except Exception:
|
|
373
|
-
raise PromptError(
|
|
373
|
+
except Exception as e:
|
|
374
|
+
raise PromptError(
|
|
375
|
+
"Could not convert prompt result to message."
|
|
376
|
+
) from e
|
|
374
377
|
|
|
375
378
|
return messages
|
|
376
|
-
except Exception:
|
|
379
|
+
except Exception as e:
|
|
377
380
|
logger.exception(f"Error rendering prompt {self.name}")
|
|
378
|
-
raise PromptError(f"Error rendering prompt {self.name}.")
|
|
381
|
+
raise PromptError(f"Error rendering prompt {self.name}.") from e
|
|
@@ -2,7 +2,7 @@ from __future__ import annotations as _annotations
|
|
|
2
2
|
|
|
3
3
|
import warnings
|
|
4
4
|
from collections.abc import Awaitable, Callable
|
|
5
|
-
from typing import
|
|
5
|
+
from typing import Any
|
|
6
6
|
|
|
7
7
|
from mcp import GetPromptResult
|
|
8
8
|
|
|
@@ -12,9 +12,6 @@ from fastmcp.prompts.prompt import FunctionPrompt, Prompt, PromptResult
|
|
|
12
12
|
from fastmcp.settings import DuplicateBehavior
|
|
13
13
|
from fastmcp.utilities.logging import get_logger
|
|
14
14
|
|
|
15
|
-
if TYPE_CHECKING:
|
|
16
|
-
from fastmcp.server.server import MountedServer
|
|
17
|
-
|
|
18
15
|
logger = get_logger(__name__)
|
|
19
16
|
|
|
20
17
|
|
|
@@ -27,7 +24,6 @@ class PromptManager:
|
|
|
27
24
|
mask_error_details: bool | None = None,
|
|
28
25
|
):
|
|
29
26
|
self._prompts: dict[str, Prompt] = {}
|
|
30
|
-
self._mounted_servers: list[MountedServer] = []
|
|
31
27
|
self.mask_error_details = mask_error_details or settings.mask_error_details
|
|
32
28
|
|
|
33
29
|
# Default to "warn" if None is provided
|
|
@@ -42,52 +38,6 @@ class PromptManager:
|
|
|
42
38
|
|
|
43
39
|
self.duplicate_behavior = duplicate_behavior
|
|
44
40
|
|
|
45
|
-
def mount(self, server: MountedServer) -> None:
|
|
46
|
-
"""Adds a mounted server as a source for prompts."""
|
|
47
|
-
self._mounted_servers.append(server)
|
|
48
|
-
|
|
49
|
-
async def _load_prompts(self, *, via_server: bool = False) -> dict[str, Prompt]:
|
|
50
|
-
"""
|
|
51
|
-
The single, consolidated recursive method for fetching prompts. The 'via_server'
|
|
52
|
-
parameter determines the communication path.
|
|
53
|
-
|
|
54
|
-
- via_server=False: Manager-to-manager path for complete, unfiltered inventory
|
|
55
|
-
- via_server=True: Server-to-server path for filtered MCP requests
|
|
56
|
-
"""
|
|
57
|
-
all_prompts: dict[str, Prompt] = {}
|
|
58
|
-
|
|
59
|
-
for mounted in self._mounted_servers:
|
|
60
|
-
try:
|
|
61
|
-
if via_server:
|
|
62
|
-
# Use the server-to-server filtered path
|
|
63
|
-
child_results = await mounted.server._list_prompts()
|
|
64
|
-
else:
|
|
65
|
-
# Use the manager-to-manager unfiltered path
|
|
66
|
-
child_results = await mounted.server._prompt_manager.list_prompts()
|
|
67
|
-
|
|
68
|
-
# The combination logic is the same for both paths
|
|
69
|
-
child_dict = {p.key: p for p in child_results}
|
|
70
|
-
if mounted.prefix:
|
|
71
|
-
for prompt in child_dict.values():
|
|
72
|
-
prefixed_prompt = prompt.model_copy(
|
|
73
|
-
key=f"{mounted.prefix}_{prompt.key}"
|
|
74
|
-
)
|
|
75
|
-
all_prompts[prefixed_prompt.key] = prefixed_prompt
|
|
76
|
-
else:
|
|
77
|
-
all_prompts.update(child_dict)
|
|
78
|
-
except Exception as e:
|
|
79
|
-
# Skip failed mounts silently, matches existing behavior
|
|
80
|
-
logger.warning(
|
|
81
|
-
f"Failed to get prompts from server: {mounted.server.name!r}, mounted at: {mounted.prefix!r}: {e}"
|
|
82
|
-
)
|
|
83
|
-
if settings.mounted_components_raise_on_load_error:
|
|
84
|
-
raise
|
|
85
|
-
continue
|
|
86
|
-
|
|
87
|
-
# Finally, add local prompts, which always take precedence
|
|
88
|
-
all_prompts.update(self._prompts)
|
|
89
|
-
return all_prompts
|
|
90
|
-
|
|
91
41
|
async def has_prompt(self, key: str) -> bool:
|
|
92
42
|
"""Check if a prompt exists."""
|
|
93
43
|
prompts = await self.get_prompts()
|
|
@@ -102,16 +52,9 @@ class PromptManager:
|
|
|
102
52
|
|
|
103
53
|
async def get_prompts(self) -> dict[str, Prompt]:
|
|
104
54
|
"""
|
|
105
|
-
Gets the complete, unfiltered inventory of
|
|
106
|
-
"""
|
|
107
|
-
return await self._load_prompts(via_server=False)
|
|
108
|
-
|
|
109
|
-
async def list_prompts(self) -> list[Prompt]:
|
|
110
|
-
"""
|
|
111
|
-
Lists all prompts, applying protocol filtering.
|
|
55
|
+
Gets the complete, unfiltered inventory of local prompts.
|
|
112
56
|
"""
|
|
113
|
-
|
|
114
|
-
return list(prompts_dict.values())
|
|
57
|
+
return dict(self._prompts)
|
|
115
58
|
|
|
116
59
|
def add_prompt_from_fn(
|
|
117
60
|
self,
|
|
@@ -160,44 +103,16 @@ class PromptManager:
|
|
|
160
103
|
Internal API for servers: Finds and renders a prompt, respecting the
|
|
161
104
|
filtered protocol path.
|
|
162
105
|
"""
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
)
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
except PromptError as e:
|
|
177
|
-
logger.exception(f"Error rendering prompt {name!r}")
|
|
178
|
-
raise e
|
|
179
|
-
|
|
180
|
-
# Handle other exceptions
|
|
181
|
-
except Exception as e:
|
|
182
|
-
logger.exception(f"Error rendering prompt {name!r}")
|
|
183
|
-
if self.mask_error_details:
|
|
184
|
-
# Mask internal details
|
|
185
|
-
raise PromptError(f"Error rendering prompt {name!r}") from e
|
|
186
|
-
else:
|
|
187
|
-
# Include original error details
|
|
188
|
-
raise PromptError(f"Error rendering prompt {name!r}: {e}") from e
|
|
189
|
-
|
|
190
|
-
# 2. Check mounted servers using the filtered protocol path.
|
|
191
|
-
for mounted in reversed(self._mounted_servers):
|
|
192
|
-
prompt_key = name
|
|
193
|
-
if mounted.prefix:
|
|
194
|
-
if name.startswith(f"{mounted.prefix}_"):
|
|
195
|
-
prompt_key = name.removeprefix(f"{mounted.prefix}_")
|
|
196
|
-
else:
|
|
197
|
-
continue
|
|
198
|
-
try:
|
|
199
|
-
return await mounted.server._get_prompt(prompt_key, arguments)
|
|
200
|
-
except NotFoundError:
|
|
201
|
-
continue
|
|
202
|
-
|
|
203
|
-
raise NotFoundError(f"Unknown prompt: {name}")
|
|
106
|
+
prompt = await self.get_prompt(name)
|
|
107
|
+
try:
|
|
108
|
+
messages = await prompt.render(arguments)
|
|
109
|
+
return GetPromptResult(description=prompt.description, messages=messages)
|
|
110
|
+
except PromptError as e:
|
|
111
|
+
logger.exception(f"Error rendering prompt {name!r}")
|
|
112
|
+
raise e
|
|
113
|
+
except Exception as e:
|
|
114
|
+
logger.exception(f"Error rendering prompt {name!r}")
|
|
115
|
+
if self.mask_error_details:
|
|
116
|
+
raise PromptError(f"Error rendering prompt {name!r}") from e
|
|
117
|
+
else:
|
|
118
|
+
raise PromptError(f"Error rendering prompt {name!r}: {e}") from e
|
fastmcp/resources/__init__.py
CHANGED
|
@@ -10,13 +10,13 @@ from .types import (
|
|
|
10
10
|
from .resource_manager import ResourceManager
|
|
11
11
|
|
|
12
12
|
__all__ = [
|
|
13
|
-
"Resource",
|
|
14
|
-
"TextResource",
|
|
15
13
|
"BinaryResource",
|
|
16
|
-
"
|
|
14
|
+
"DirectoryResource",
|
|
17
15
|
"FileResource",
|
|
16
|
+
"FunctionResource",
|
|
18
17
|
"HttpResource",
|
|
19
|
-
"
|
|
20
|
-
"ResourceTemplate",
|
|
18
|
+
"Resource",
|
|
21
19
|
"ResourceManager",
|
|
20
|
+
"ResourceTemplate",
|
|
21
|
+
"TextResource",
|
|
22
22
|
]
|