fastmcp 2.12.1__py3-none-any.whl → 2.13.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- fastmcp/__init__.py +2 -2
- fastmcp/cli/cli.py +56 -36
- fastmcp/cli/install/__init__.py +2 -0
- fastmcp/cli/install/claude_code.py +7 -16
- fastmcp/cli/install/claude_desktop.py +4 -12
- fastmcp/cli/install/cursor.py +20 -30
- fastmcp/cli/install/gemini_cli.py +241 -0
- fastmcp/cli/install/mcp_json.py +4 -12
- fastmcp/cli/run.py +15 -94
- fastmcp/client/__init__.py +9 -9
- fastmcp/client/auth/oauth.py +117 -206
- fastmcp/client/client.py +123 -47
- fastmcp/client/elicitation.py +6 -1
- fastmcp/client/logging.py +18 -14
- fastmcp/client/oauth_callback.py +85 -171
- fastmcp/client/sampling.py +1 -1
- fastmcp/client/transports.py +81 -26
- fastmcp/contrib/component_manager/__init__.py +1 -1
- fastmcp/contrib/component_manager/component_manager.py +2 -2
- fastmcp/contrib/component_manager/component_service.py +7 -7
- fastmcp/contrib/mcp_mixin/README.md +35 -4
- fastmcp/contrib/mcp_mixin/__init__.py +2 -2
- fastmcp/contrib/mcp_mixin/mcp_mixin.py +54 -7
- fastmcp/experimental/sampling/handlers/openai.py +2 -2
- fastmcp/experimental/server/openapi/__init__.py +5 -8
- fastmcp/experimental/server/openapi/components.py +11 -7
- fastmcp/experimental/server/openapi/routing.py +2 -2
- fastmcp/experimental/utilities/openapi/__init__.py +10 -15
- fastmcp/experimental/utilities/openapi/director.py +16 -10
- fastmcp/experimental/utilities/openapi/json_schema_converter.py +6 -2
- fastmcp/experimental/utilities/openapi/models.py +3 -3
- fastmcp/experimental/utilities/openapi/parser.py +37 -16
- fastmcp/experimental/utilities/openapi/schemas.py +33 -7
- fastmcp/mcp_config.py +3 -4
- fastmcp/prompts/__init__.py +1 -1
- fastmcp/prompts/prompt.py +32 -27
- fastmcp/prompts/prompt_manager.py +16 -101
- fastmcp/resources/__init__.py +5 -5
- fastmcp/resources/resource.py +28 -20
- fastmcp/resources/resource_manager.py +9 -168
- fastmcp/resources/template.py +119 -27
- fastmcp/resources/types.py +30 -24
- fastmcp/server/__init__.py +1 -1
- fastmcp/server/auth/__init__.py +9 -5
- fastmcp/server/auth/auth.py +80 -47
- fastmcp/server/auth/handlers/authorize.py +326 -0
- fastmcp/server/auth/jwt_issuer.py +236 -0
- fastmcp/server/auth/middleware.py +96 -0
- fastmcp/server/auth/oauth_proxy.py +1556 -265
- fastmcp/server/auth/oidc_proxy.py +412 -0
- fastmcp/server/auth/providers/auth0.py +193 -0
- fastmcp/server/auth/providers/aws.py +263 -0
- fastmcp/server/auth/providers/azure.py +314 -129
- fastmcp/server/auth/providers/bearer.py +1 -1
- fastmcp/server/auth/providers/debug.py +114 -0
- fastmcp/server/auth/providers/descope.py +229 -0
- fastmcp/server/auth/providers/discord.py +308 -0
- fastmcp/server/auth/providers/github.py +31 -6
- fastmcp/server/auth/providers/google.py +50 -7
- fastmcp/server/auth/providers/in_memory.py +27 -3
- fastmcp/server/auth/providers/introspection.py +281 -0
- fastmcp/server/auth/providers/jwt.py +48 -31
- fastmcp/server/auth/providers/oci.py +233 -0
- fastmcp/server/auth/providers/scalekit.py +238 -0
- fastmcp/server/auth/providers/supabase.py +188 -0
- fastmcp/server/auth/providers/workos.py +37 -15
- fastmcp/server/context.py +194 -67
- fastmcp/server/dependencies.py +56 -16
- fastmcp/server/elicitation.py +1 -1
- fastmcp/server/http.py +57 -18
- fastmcp/server/low_level.py +121 -2
- fastmcp/server/middleware/__init__.py +1 -1
- fastmcp/server/middleware/caching.py +476 -0
- fastmcp/server/middleware/error_handling.py +14 -10
- fastmcp/server/middleware/logging.py +158 -116
- fastmcp/server/middleware/middleware.py +30 -16
- fastmcp/server/middleware/rate_limiting.py +3 -3
- fastmcp/server/middleware/tool_injection.py +116 -0
- fastmcp/server/openapi.py +15 -7
- fastmcp/server/proxy.py +22 -11
- fastmcp/server/server.py +744 -254
- fastmcp/settings.py +65 -15
- fastmcp/tools/__init__.py +1 -1
- fastmcp/tools/tool.py +173 -108
- fastmcp/tools/tool_manager.py +30 -112
- fastmcp/tools/tool_transform.py +13 -11
- fastmcp/utilities/cli.py +67 -28
- fastmcp/utilities/components.py +7 -2
- fastmcp/utilities/inspect.py +79 -23
- fastmcp/utilities/json_schema.py +21 -4
- fastmcp/utilities/json_schema_type.py +4 -4
- fastmcp/utilities/logging.py +182 -10
- fastmcp/utilities/mcp_server_config/__init__.py +3 -3
- fastmcp/utilities/mcp_server_config/v1/environments/base.py +1 -2
- fastmcp/utilities/mcp_server_config/v1/environments/uv.py +10 -45
- fastmcp/utilities/mcp_server_config/v1/mcp_server_config.py +8 -7
- fastmcp/utilities/mcp_server_config/v1/schema.json +5 -1
- fastmcp/utilities/mcp_server_config/v1/sources/base.py +0 -1
- fastmcp/utilities/openapi.py +11 -11
- fastmcp/utilities/tests.py +93 -10
- fastmcp/utilities/types.py +87 -21
- fastmcp/utilities/ui.py +626 -0
- {fastmcp-2.12.1.dist-info → fastmcp-2.13.2.dist-info}/METADATA +141 -60
- fastmcp-2.13.2.dist-info/RECORD +144 -0
- {fastmcp-2.12.1.dist-info → fastmcp-2.13.2.dist-info}/WHEEL +1 -1
- fastmcp/cli/claude.py +0 -144
- fastmcp-2.12.1.dist-info/RECORD +0 -128
- {fastmcp-2.12.1.dist-info → fastmcp-2.13.2.dist-info}/entry_points.txt +0 -0
- {fastmcp-2.12.1.dist-info → fastmcp-2.13.2.dist-info}/licenses/LICENSE +0 -0
|
@@ -79,6 +79,7 @@ def _replace_ref_with_defs(
|
|
|
79
79
|
|
|
80
80
|
Examples:
|
|
81
81
|
- {"type": "object", "properties": {"$ref": "#/components/schemas/..."}}
|
|
82
|
+
- {"type": "object", "additionalProperties": {"$ref": "#/components/schemas/..."}, "properties": {...}}
|
|
82
83
|
- {"$ref": "#/components/schemas/..."}
|
|
83
84
|
- {"items": {"$ref": "#/components/schemas/..."}}
|
|
84
85
|
- {"anyOf": [{"$ref": "#/components/schemas/..."}]}
|
|
@@ -117,6 +118,11 @@ def _replace_ref_with_defs(
|
|
|
117
118
|
for section in ["anyOf", "allOf", "oneOf"]:
|
|
118
119
|
for i, item in enumerate(schema.get(section, [])):
|
|
119
120
|
schema[section][i] = _replace_ref_with_defs(item)
|
|
121
|
+
if additionalProperties := schema.get("additionalProperties"):
|
|
122
|
+
if not isinstance(additionalProperties, bool):
|
|
123
|
+
schema["additionalProperties"] = _replace_ref_with_defs(
|
|
124
|
+
additionalProperties
|
|
125
|
+
)
|
|
120
126
|
if info.get("description", description) and not schema.get("description"):
|
|
121
127
|
schema["description"] = description
|
|
122
128
|
return schema
|
|
@@ -297,9 +303,11 @@ def _combine_schemas_and_map_params(
|
|
|
297
303
|
|
|
298
304
|
# Convert refs if needed
|
|
299
305
|
if convert_refs:
|
|
300
|
-
param_schema = _replace_ref_with_defs(param.schema_)
|
|
306
|
+
param_schema = _replace_ref_with_defs(param.schema_, param.description)
|
|
301
307
|
else:
|
|
302
|
-
param_schema = param.schema_
|
|
308
|
+
param_schema = param.schema_.copy()
|
|
309
|
+
if param.description and not param_schema.get("description"):
|
|
310
|
+
param_schema["description"] = param.description
|
|
303
311
|
original_desc = param_schema.get("description", "")
|
|
304
312
|
location_desc = f"({param.location.capitalize()} parameter)"
|
|
305
313
|
if original_desc:
|
|
@@ -324,9 +332,11 @@ def _combine_schemas_and_map_params(
|
|
|
324
332
|
|
|
325
333
|
# Convert refs if needed
|
|
326
334
|
if convert_refs:
|
|
327
|
-
param_schema = _replace_ref_with_defs(param.schema_)
|
|
335
|
+
param_schema = _replace_ref_with_defs(param.schema_, param.description)
|
|
328
336
|
else:
|
|
329
|
-
param_schema = param.schema_
|
|
337
|
+
param_schema = param.schema_.copy()
|
|
338
|
+
if param.description and not param_schema.get("description"):
|
|
339
|
+
param_schema["description"] = param.description
|
|
330
340
|
|
|
331
341
|
# Don't make optional parameters nullable - they can simply be omitted
|
|
332
342
|
# The OpenAPI specification doesn't require optional parameters to accept null values
|
|
@@ -344,7 +354,7 @@ def _combine_schemas_and_map_params(
|
|
|
344
354
|
if route.request_body.required:
|
|
345
355
|
required.append("body")
|
|
346
356
|
parameter_map["body"] = {"location": "body", "openapi_name": "body"}
|
|
347
|
-
|
|
357
|
+
elif body_props:
|
|
348
358
|
# Normal case: body has properties
|
|
349
359
|
for prop_name, prop_schema in body_props.items():
|
|
350
360
|
properties[prop_name] = prop_schema
|
|
@@ -357,6 +367,22 @@ def _combine_schemas_and_map_params(
|
|
|
357
367
|
|
|
358
368
|
if route.request_body.required:
|
|
359
369
|
required.extend(body_schema.get("required", []))
|
|
370
|
+
else:
|
|
371
|
+
# Handle direct array/primitive schemas (like list[str] parameters from FastAPI)
|
|
372
|
+
# Use the schema title as parameter name, fall back to generic name
|
|
373
|
+
param_name = body_schema.get("title", "body").lower()
|
|
374
|
+
|
|
375
|
+
# Clean the parameter name to be valid
|
|
376
|
+
import re
|
|
377
|
+
|
|
378
|
+
param_name = re.sub(r"[^a-zA-Z0-9_]", "_", param_name)
|
|
379
|
+
if not param_name or param_name[0].isdigit():
|
|
380
|
+
param_name = "body_data"
|
|
381
|
+
|
|
382
|
+
properties[param_name] = body_schema
|
|
383
|
+
if route.request_body.required:
|
|
384
|
+
required.append(param_name)
|
|
385
|
+
parameter_map[param_name] = {"location": "body", "openapi_name": param_name}
|
|
360
386
|
|
|
361
387
|
result = {
|
|
362
388
|
"type": "object",
|
|
@@ -559,9 +585,9 @@ def extract_output_schema_from_responses(
|
|
|
559
585
|
|
|
560
586
|
# Export public symbols
|
|
561
587
|
__all__ = [
|
|
562
|
-
"clean_schema_for_display",
|
|
563
588
|
"_combine_schemas",
|
|
564
589
|
"_combine_schemas_and_map_params",
|
|
565
|
-
"extract_output_schema_from_responses",
|
|
566
590
|
"_make_optional_parameter_nullable",
|
|
591
|
+
"clean_schema_for_display",
|
|
592
|
+
"extract_output_schema_from_responses",
|
|
567
593
|
]
|
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,12 +4,11 @@ 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
8
|
from typing import 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
|
|
@@ -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(
|
|
@@ -100,14 +99,17 @@ class Prompt(FastMCPComponent, ABC):
|
|
|
100
99
|
)
|
|
101
100
|
for arg in self.arguments or []
|
|
102
101
|
]
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
"
|
|
106
|
-
"
|
|
107
|
-
|
|
108
|
-
"
|
|
109
|
-
|
|
110
|
-
|
|
102
|
+
|
|
103
|
+
return MCPPrompt(
|
|
104
|
+
name=overrides.get("name", self.name),
|
|
105
|
+
description=overrides.get("description", self.description),
|
|
106
|
+
arguments=arguments,
|
|
107
|
+
title=overrides.get("title", self.title),
|
|
108
|
+
icons=overrides.get("icons", self.icons),
|
|
109
|
+
_meta=overrides.get(
|
|
110
|
+
"_meta", self.get_meta(include_fastmcp_meta=include_fastmcp_meta)
|
|
111
|
+
),
|
|
112
|
+
)
|
|
111
113
|
|
|
112
114
|
@staticmethod
|
|
113
115
|
def from_function(
|
|
@@ -115,6 +117,7 @@ class Prompt(FastMCPComponent, ABC):
|
|
|
115
117
|
name: str | None = None,
|
|
116
118
|
title: str | None = None,
|
|
117
119
|
description: str | None = None,
|
|
120
|
+
icons: list[Icon] | None = None,
|
|
118
121
|
tags: set[str] | None = None,
|
|
119
122
|
enabled: bool | None = None,
|
|
120
123
|
meta: dict[str, Any] | None = None,
|
|
@@ -132,18 +135,22 @@ class Prompt(FastMCPComponent, ABC):
|
|
|
132
135
|
name=name,
|
|
133
136
|
title=title,
|
|
134
137
|
description=description,
|
|
138
|
+
icons=icons,
|
|
135
139
|
tags=tags,
|
|
136
140
|
enabled=enabled,
|
|
137
141
|
meta=meta,
|
|
138
142
|
)
|
|
139
143
|
|
|
140
|
-
@abstractmethod
|
|
141
144
|
async def render(
|
|
142
145
|
self,
|
|
143
146
|
arguments: dict[str, Any] | None = None,
|
|
144
147
|
) -> list[PromptMessage]:
|
|
145
|
-
"""Render the prompt with arguments.
|
|
146
|
-
|
|
148
|
+
"""Render the prompt with arguments.
|
|
149
|
+
|
|
150
|
+
This method is not implemented in the base Prompt class and must be
|
|
151
|
+
implemented by subclasses.
|
|
152
|
+
"""
|
|
153
|
+
raise NotImplementedError("Subclasses must implement render()")
|
|
147
154
|
|
|
148
155
|
|
|
149
156
|
class FunctionPrompt(Prompt):
|
|
@@ -158,6 +165,7 @@ class FunctionPrompt(Prompt):
|
|
|
158
165
|
name: str | None = None,
|
|
159
166
|
title: str | None = None,
|
|
160
167
|
description: str | None = None,
|
|
168
|
+
icons: list[Icon] | None = None,
|
|
161
169
|
tags: set[str] | None = None,
|
|
162
170
|
enabled: bool | None = None,
|
|
163
171
|
meta: dict[str, Any] | None = None,
|
|
@@ -199,10 +207,7 @@ class FunctionPrompt(Prompt):
|
|
|
199
207
|
# Auto-detect context parameter if not provided
|
|
200
208
|
|
|
201
209
|
context_kwarg = find_kwarg_by_type(fn, kwarg_type=Context)
|
|
202
|
-
if context_kwarg
|
|
203
|
-
prune_params = [context_kwarg]
|
|
204
|
-
else:
|
|
205
|
-
prune_params = None
|
|
210
|
+
prune_params = [context_kwarg] if context_kwarg else None
|
|
206
211
|
|
|
207
212
|
parameters = compress_schema(parameters, prune_params=prune_params)
|
|
208
213
|
|
|
@@ -251,6 +256,7 @@ class FunctionPrompt(Prompt):
|
|
|
251
256
|
name=func_name,
|
|
252
257
|
title=title,
|
|
253
258
|
description=description,
|
|
259
|
+
icons=icons,
|
|
254
260
|
arguments=arguments,
|
|
255
261
|
tags=tags or set(),
|
|
256
262
|
enabled=enabled if enabled is not None else True,
|
|
@@ -281,10 +287,7 @@ class FunctionPrompt(Prompt):
|
|
|
281
287
|
if (
|
|
282
288
|
param.annotation == inspect.Parameter.empty
|
|
283
289
|
or param.annotation is str
|
|
284
|
-
):
|
|
285
|
-
converted_kwargs[param_name] = param_value
|
|
286
|
-
# If argument is not a string, pass as-is (already properly typed)
|
|
287
|
-
elif not isinstance(param_value, str):
|
|
290
|
+
) or not isinstance(param_value, str):
|
|
288
291
|
converted_kwargs[param_name] = param_value
|
|
289
292
|
else:
|
|
290
293
|
# Try to convert string argument using type adapter
|
|
@@ -305,7 +308,7 @@ class FunctionPrompt(Prompt):
|
|
|
305
308
|
raise PromptError(
|
|
306
309
|
f"Could not convert argument '{param_name}' with value '{param_value}' "
|
|
307
310
|
f"to expected type {param.annotation}. Error: {e}"
|
|
308
|
-
)
|
|
311
|
+
) from e
|
|
309
312
|
else:
|
|
310
313
|
# Parameter not in function signature, pass as-is
|
|
311
314
|
converted_kwargs[param_name] = param_value
|
|
@@ -367,10 +370,12 @@ class FunctionPrompt(Prompt):
|
|
|
367
370
|
content=TextContent(type="text", text=content),
|
|
368
371
|
)
|
|
369
372
|
)
|
|
370
|
-
except Exception:
|
|
371
|
-
raise PromptError(
|
|
373
|
+
except Exception as e:
|
|
374
|
+
raise PromptError(
|
|
375
|
+
"Could not convert prompt result to message."
|
|
376
|
+
) from e
|
|
372
377
|
|
|
373
378
|
return messages
|
|
374
|
-
except Exception:
|
|
379
|
+
except Exception as e:
|
|
375
380
|
logger.exception(f"Error rendering prompt {self.name}")
|
|
376
|
-
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
|
]
|
fastmcp/resources/resource.py
CHANGED
|
@@ -2,13 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
-
import abc
|
|
6
5
|
import inspect
|
|
7
6
|
from collections.abc import Callable
|
|
8
7
|
from typing import TYPE_CHECKING, Annotated, Any
|
|
9
8
|
|
|
10
9
|
import pydantic_core
|
|
11
|
-
from mcp.types import Annotations
|
|
10
|
+
from mcp.types import Annotations, Icon
|
|
12
11
|
from mcp.types import Resource as MCPResource
|
|
13
12
|
from pydantic import (
|
|
14
13
|
AnyUrl,
|
|
@@ -24,13 +23,14 @@ from fastmcp.server.dependencies import get_context
|
|
|
24
23
|
from fastmcp.utilities.components import FastMCPComponent
|
|
25
24
|
from fastmcp.utilities.types import (
|
|
26
25
|
find_kwarg_by_type,
|
|
26
|
+
get_fn_name,
|
|
27
27
|
)
|
|
28
28
|
|
|
29
29
|
if TYPE_CHECKING:
|
|
30
30
|
pass
|
|
31
31
|
|
|
32
32
|
|
|
33
|
-
class Resource(FastMCPComponent
|
|
33
|
+
class Resource(FastMCPComponent):
|
|
34
34
|
"""Base class for all resources."""
|
|
35
35
|
|
|
36
36
|
model_config = ConfigDict(validate_default=True)
|
|
@@ -72,6 +72,7 @@ class Resource(FastMCPComponent, abc.ABC):
|
|
|
72
72
|
name: str | None = None,
|
|
73
73
|
title: str | None = None,
|
|
74
74
|
description: str | None = None,
|
|
75
|
+
icons: list[Icon] | None = None,
|
|
75
76
|
mime_type: str | None = None,
|
|
76
77
|
tags: set[str] | None = None,
|
|
77
78
|
enabled: bool | None = None,
|
|
@@ -84,6 +85,7 @@ class Resource(FastMCPComponent, abc.ABC):
|
|
|
84
85
|
name=name,
|
|
85
86
|
title=title,
|
|
86
87
|
description=description,
|
|
88
|
+
icons=icons,
|
|
87
89
|
mime_type=mime_type,
|
|
88
90
|
tags=tags,
|
|
89
91
|
enabled=enabled,
|
|
@@ -110,10 +112,13 @@ class Resource(FastMCPComponent, abc.ABC):
|
|
|
110
112
|
raise ValueError("Either name or uri must be provided")
|
|
111
113
|
return self
|
|
112
114
|
|
|
113
|
-
@abc.abstractmethod
|
|
114
115
|
async def read(self) -> str | bytes:
|
|
115
|
-
"""Read the resource content.
|
|
116
|
-
|
|
116
|
+
"""Read the resource content.
|
|
117
|
+
|
|
118
|
+
This method is not implemented in the base Resource class and must be
|
|
119
|
+
implemented by subclasses.
|
|
120
|
+
"""
|
|
121
|
+
raise NotImplementedError("Subclasses must implement read()")
|
|
117
122
|
|
|
118
123
|
def to_mcp_resource(
|
|
119
124
|
self,
|
|
@@ -122,16 +127,19 @@ class Resource(FastMCPComponent, abc.ABC):
|
|
|
122
127
|
**overrides: Any,
|
|
123
128
|
) -> MCPResource:
|
|
124
129
|
"""Convert the resource to an MCPResource."""
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
"name"
|
|
128
|
-
"
|
|
129
|
-
"
|
|
130
|
-
"
|
|
131
|
-
"
|
|
132
|
-
"
|
|
133
|
-
|
|
134
|
-
|
|
130
|
+
|
|
131
|
+
return MCPResource(
|
|
132
|
+
name=overrides.get("name", self.name),
|
|
133
|
+
uri=overrides.get("uri", self.uri),
|
|
134
|
+
description=overrides.get("description", self.description),
|
|
135
|
+
mimeType=overrides.get("mimeType", self.mime_type),
|
|
136
|
+
title=overrides.get("title", self.title),
|
|
137
|
+
icons=overrides.get("icons", self.icons),
|
|
138
|
+
annotations=overrides.get("annotations", self.annotations),
|
|
139
|
+
_meta=overrides.get(
|
|
140
|
+
"_meta", self.get_meta(include_fastmcp_meta=include_fastmcp_meta)
|
|
141
|
+
),
|
|
142
|
+
)
|
|
135
143
|
|
|
136
144
|
def __repr__(self) -> str:
|
|
137
145
|
return f"{self.__class__.__name__}(uri={self.uri!r}, name={self.name!r}, description={self.description!r}, tags={self.tags})"
|
|
@@ -170,6 +178,7 @@ class FunctionResource(Resource):
|
|
|
170
178
|
name: str | None = None,
|
|
171
179
|
title: str | None = None,
|
|
172
180
|
description: str | None = None,
|
|
181
|
+
icons: list[Icon] | None = None,
|
|
173
182
|
mime_type: str | None = None,
|
|
174
183
|
tags: set[str] | None = None,
|
|
175
184
|
enabled: bool | None = None,
|
|
@@ -182,9 +191,10 @@ class FunctionResource(Resource):
|
|
|
182
191
|
return cls(
|
|
183
192
|
fn=fn,
|
|
184
193
|
uri=uri,
|
|
185
|
-
name=name or fn
|
|
194
|
+
name=name or get_fn_name(fn),
|
|
186
195
|
title=title,
|
|
187
196
|
description=description or inspect.getdoc(fn),
|
|
197
|
+
icons=icons,
|
|
188
198
|
mime_type=mime_type or "text/plain",
|
|
189
199
|
tags=tags or set(),
|
|
190
200
|
enabled=enabled if enabled is not None else True,
|
|
@@ -207,9 +217,7 @@ class FunctionResource(Resource):
|
|
|
207
217
|
|
|
208
218
|
if isinstance(result, Resource):
|
|
209
219
|
return await result.read()
|
|
210
|
-
elif isinstance(result, bytes):
|
|
211
|
-
return result
|
|
212
|
-
elif isinstance(result, str):
|
|
220
|
+
elif isinstance(result, bytes | str):
|
|
213
221
|
return result
|
|
214
222
|
else:
|
|
215
223
|
return pydantic_core.to_json(result, fallback=str).decode()
|