fastmcp 2.12.4__py3-none-any.whl → 2.13.0rc1__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/cli/cli.py +6 -6
- fastmcp/cli/install/claude_code.py +3 -3
- fastmcp/cli/install/claude_desktop.py +3 -3
- fastmcp/cli/install/cursor.py +7 -7
- fastmcp/cli/install/gemini_cli.py +3 -3
- fastmcp/cli/install/mcp_json.py +3 -3
- fastmcp/cli/run.py +13 -8
- fastmcp/client/auth/oauth.py +100 -208
- fastmcp/client/client.py +11 -11
- fastmcp/client/logging.py +18 -14
- fastmcp/client/oauth_callback.py +81 -171
- fastmcp/client/transports.py +76 -22
- fastmcp/contrib/component_manager/component_service.py +6 -6
- fastmcp/contrib/mcp_mixin/README.md +32 -1
- fastmcp/contrib/mcp_mixin/mcp_mixin.py +14 -2
- fastmcp/experimental/utilities/openapi/json_schema_converter.py +4 -0
- fastmcp/experimental/utilities/openapi/parser.py +23 -3
- fastmcp/prompts/prompt.py +13 -6
- fastmcp/prompts/prompt_manager.py +16 -101
- fastmcp/resources/resource.py +13 -6
- fastmcp/resources/resource_manager.py +5 -164
- fastmcp/resources/template.py +107 -17
- fastmcp/server/auth/auth.py +40 -32
- fastmcp/server/auth/jwt_issuer.py +289 -0
- fastmcp/server/auth/oauth_proxy.py +1238 -234
- fastmcp/server/auth/oidc_proxy.py +8 -6
- fastmcp/server/auth/providers/auth0.py +12 -6
- fastmcp/server/auth/providers/aws.py +13 -2
- fastmcp/server/auth/providers/azure.py +137 -124
- fastmcp/server/auth/providers/descope.py +4 -6
- fastmcp/server/auth/providers/github.py +13 -7
- fastmcp/server/auth/providers/google.py +13 -7
- fastmcp/server/auth/providers/introspection.py +281 -0
- fastmcp/server/auth/providers/jwt.py +8 -2
- fastmcp/server/auth/providers/scalekit.py +179 -0
- fastmcp/server/auth/providers/supabase.py +172 -0
- fastmcp/server/auth/providers/workos.py +16 -13
- fastmcp/server/context.py +89 -34
- fastmcp/server/http.py +53 -16
- fastmcp/server/low_level.py +121 -2
- fastmcp/server/middleware/caching.py +469 -0
- fastmcp/server/middleware/error_handling.py +6 -2
- fastmcp/server/middleware/logging.py +48 -37
- fastmcp/server/middleware/middleware.py +28 -15
- fastmcp/server/middleware/rate_limiting.py +3 -3
- fastmcp/server/proxy.py +6 -6
- fastmcp/server/server.py +638 -183
- fastmcp/settings.py +22 -9
- fastmcp/tools/tool.py +7 -3
- fastmcp/tools/tool_manager.py +22 -108
- fastmcp/tools/tool_transform.py +3 -3
- fastmcp/utilities/cli.py +2 -2
- fastmcp/utilities/components.py +5 -0
- fastmcp/utilities/inspect.py +77 -21
- fastmcp/utilities/logging.py +118 -8
- fastmcp/utilities/mcp_server_config/v1/environments/uv.py +6 -6
- fastmcp/utilities/mcp_server_config/v1/mcp_server_config.py +3 -3
- fastmcp/utilities/mcp_server_config/v1/schema.json +3 -0
- fastmcp/utilities/tests.py +87 -4
- fastmcp/utilities/types.py +1 -1
- fastmcp/utilities/ui.py +497 -0
- {fastmcp-2.12.4.dist-info → fastmcp-2.13.0rc1.dist-info}/METADATA +8 -4
- {fastmcp-2.12.4.dist-info → fastmcp-2.13.0rc1.dist-info}/RECORD +66 -62
- fastmcp/cli/claude.py +0 -135
- fastmcp/utilities/storage.py +0 -204
- {fastmcp-2.12.4.dist-info → fastmcp-2.13.0rc1.dist-info}/WHEEL +0 -0
- {fastmcp-2.12.4.dist-info → fastmcp-2.13.0rc1.dist-info}/entry_points.txt +0 -0
- {fastmcp-2.12.4.dist-info → fastmcp-2.13.0rc1.dist-info}/licenses/LICENSE +0 -0
|
@@ -11,12 +11,15 @@ Tools:
|
|
|
11
11
|
* [enable/disable](https://gofastmcp.com/servers/tools#disabling-tools)
|
|
12
12
|
* [annotations](https://gofastmcp.com/servers/tools#annotations-2)
|
|
13
13
|
* [excluded arguments](https://gofastmcp.com/servers/tools#excluding-arguments)
|
|
14
|
+
* [meta](https://gofastmcp.com/servers/tools#param-meta)
|
|
14
15
|
|
|
15
16
|
Prompts:
|
|
16
17
|
* [enable/disable](https://gofastmcp.com/servers/prompts#disabling-prompts)
|
|
18
|
+
* [meta](https://gofastmcp.com/servers/prompts#param-meta)
|
|
17
19
|
|
|
18
20
|
Resources:
|
|
19
21
|
* [enable/disable](https://gofastmcp.com/servers/resources#disabling-resources)
|
|
22
|
+
* [meta](https://gofastmcp.com/servers/resources#param-meta)
|
|
20
23
|
|
|
21
24
|
## Usage
|
|
22
25
|
|
|
@@ -78,7 +81,16 @@ class MyComponent(MCPMixin):
|
|
|
78
81
|
if delete_all:
|
|
79
82
|
return "99 records deleted. I bet you're not a tool :)"
|
|
80
83
|
return "Tool executed, but you might be a tool!"
|
|
81
|
-
|
|
84
|
+
|
|
85
|
+
# example tool w/ meta
|
|
86
|
+
@mcp_tool(
|
|
87
|
+
name="data_tool",
|
|
88
|
+
description="Fetches user data from database",
|
|
89
|
+
meta={"version": "2.0", "category": "database", "author": "dev-team"}
|
|
90
|
+
)
|
|
91
|
+
def data_tool_method(self, user_id: int):
|
|
92
|
+
return f"Fetching data for user {user_id}"
|
|
93
|
+
|
|
82
94
|
@mcp_resource(uri="component://data")
|
|
83
95
|
def resource_method(self):
|
|
84
96
|
return {"data": "some data"}
|
|
@@ -88,6 +100,15 @@ class MyComponent(MCPMixin):
|
|
|
88
100
|
def resource_method(self):
|
|
89
101
|
return {"data": "some data"}
|
|
90
102
|
|
|
103
|
+
# example resource w/meta and title
|
|
104
|
+
@mcp_resource(
|
|
105
|
+
uri="component://config",
|
|
106
|
+
title="Data resource Title,
|
|
107
|
+
meta={"internal": True, "cache_ttl": 3600, "priority": "high"}
|
|
108
|
+
)
|
|
109
|
+
def config_resource_method(self):
|
|
110
|
+
return {"config": "data"}
|
|
111
|
+
|
|
91
112
|
# prompt
|
|
92
113
|
@mcp_prompt(name="A prompt")
|
|
93
114
|
def prompt_method(self, name):
|
|
@@ -98,6 +119,16 @@ class MyComponent(MCPMixin):
|
|
|
98
119
|
def prompt_method(self, name):
|
|
99
120
|
return f"What's up {name}?"
|
|
100
121
|
|
|
122
|
+
# example prompt w/title and meta
|
|
123
|
+
@mcp_prompt(
|
|
124
|
+
name="analysis_prompt",
|
|
125
|
+
title="Data Analysis Prompt",
|
|
126
|
+
description="Analyzes data patterns",
|
|
127
|
+
meta={"complexity": "high", "domain": "analytics", "requires_context": True}
|
|
128
|
+
)
|
|
129
|
+
def analysis_prompt_method(self, dataset: str):
|
|
130
|
+
return f"Analyze the patterns in {dataset}"
|
|
131
|
+
|
|
101
132
|
mcp_server = FastMCP()
|
|
102
133
|
component = MyComponent()
|
|
103
134
|
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
from collections.abc import Callable
|
|
4
4
|
from typing import TYPE_CHECKING, Any
|
|
5
5
|
|
|
6
|
-
from mcp.types import ToolAnnotations
|
|
6
|
+
from mcp.types import Annotations, ToolAnnotations
|
|
7
7
|
|
|
8
8
|
from fastmcp.prompts.prompt import Prompt
|
|
9
9
|
from fastmcp.resources.resource import Resource
|
|
@@ -29,6 +29,7 @@ def mcp_tool(
|
|
|
29
29
|
annotations: ToolAnnotations | dict[str, Any] | None = None,
|
|
30
30
|
exclude_args: list[str] | None = None,
|
|
31
31
|
serializer: Callable[[Any], str] | None = None,
|
|
32
|
+
meta: dict[str, Any] | None = None,
|
|
32
33
|
enabled: bool | None = None,
|
|
33
34
|
) -> Callable[[Callable[..., Any]], Callable[..., Any]]:
|
|
34
35
|
"""Decorator to mark a method as an MCP tool for later registration."""
|
|
@@ -41,6 +42,7 @@ def mcp_tool(
|
|
|
41
42
|
"annotations": annotations,
|
|
42
43
|
"exclude_args": exclude_args,
|
|
43
44
|
"serializer": serializer,
|
|
45
|
+
"meta": meta,
|
|
44
46
|
"enabled": enabled,
|
|
45
47
|
}
|
|
46
48
|
call_args = {k: v for k, v in call_args.items() if v is not None}
|
|
@@ -54,9 +56,12 @@ def mcp_resource(
|
|
|
54
56
|
uri: str,
|
|
55
57
|
*,
|
|
56
58
|
name: str | None = None,
|
|
59
|
+
title: str | None = None,
|
|
57
60
|
description: str | None = None,
|
|
58
61
|
mime_type: str | None = None,
|
|
59
62
|
tags: set[str] | None = None,
|
|
63
|
+
annotations: Annotations | None = None,
|
|
64
|
+
meta: dict[str, Any] | None = None,
|
|
60
65
|
enabled: bool | None = None,
|
|
61
66
|
) -> Callable[[Callable[..., Any]], Callable[..., Any]]:
|
|
62
67
|
"""Decorator to mark a method as an MCP resource for later registration."""
|
|
@@ -65,9 +70,12 @@ def mcp_resource(
|
|
|
65
70
|
call_args = {
|
|
66
71
|
"uri": uri,
|
|
67
72
|
"name": name or get_fn_name(func),
|
|
73
|
+
"title": title,
|
|
68
74
|
"description": description,
|
|
69
75
|
"mime_type": mime_type,
|
|
70
76
|
"tags": tags,
|
|
77
|
+
"annotations": annotations,
|
|
78
|
+
"meta": meta,
|
|
71
79
|
"enabled": enabled,
|
|
72
80
|
}
|
|
73
81
|
call_args = {k: v for k, v in call_args.items() if v is not None}
|
|
@@ -81,8 +89,10 @@ def mcp_resource(
|
|
|
81
89
|
|
|
82
90
|
def mcp_prompt(
|
|
83
91
|
name: str | None = None,
|
|
92
|
+
title: str | None = None,
|
|
84
93
|
description: str | None = None,
|
|
85
94
|
tags: set[str] | None = None,
|
|
95
|
+
meta: dict[str, Any] | None = None,
|
|
86
96
|
enabled: bool | None = None,
|
|
87
97
|
) -> Callable[[Callable[..., Any]], Callable[..., Any]]:
|
|
88
98
|
"""Decorator to mark a method as an MCP prompt for later registration."""
|
|
@@ -90,8 +100,10 @@ def mcp_prompt(
|
|
|
90
100
|
def decorator(func: Callable[..., Any]) -> Callable[..., Any]:
|
|
91
101
|
call_args = {
|
|
92
102
|
"name": name or get_fn_name(func),
|
|
103
|
+
"title": title,
|
|
93
104
|
"description": description,
|
|
94
105
|
"tags": tags,
|
|
106
|
+
"meta": meta,
|
|
95
107
|
"enabled": enabled,
|
|
96
108
|
}
|
|
97
109
|
|
|
@@ -151,7 +163,6 @@ class MCPMixin:
|
|
|
151
163
|
tool = Tool.from_function(
|
|
152
164
|
fn=method,
|
|
153
165
|
name=registration_info.get("name"),
|
|
154
|
-
title=registration_info.get("title"),
|
|
155
166
|
description=registration_info.get("description"),
|
|
156
167
|
tags=registration_info.get("tags"),
|
|
157
168
|
annotations=registration_info.get("annotations"),
|
|
@@ -195,6 +206,7 @@ class MCPMixin:
|
|
|
195
206
|
fn=method,
|
|
196
207
|
uri=registration_info["uri"],
|
|
197
208
|
name=registration_info.get("name"),
|
|
209
|
+
title=registration_info.get("title"),
|
|
198
210
|
description=registration_info.get("description"),
|
|
199
211
|
mime_type=registration_info.get("mime_type"),
|
|
200
212
|
tags=registration_info.get("tags"),
|
|
@@ -176,6 +176,10 @@ def _convert_nullable_field(schema: dict[str, Any]) -> dict[str, Any]:
|
|
|
176
176
|
# Wrap allOf in anyOf with null option
|
|
177
177
|
result["anyOf"] = [{"allOf": result.pop("allOf")}, {"type": "null"}]
|
|
178
178
|
|
|
179
|
+
# Handle enum fields - add null to enum values if present
|
|
180
|
+
if "enum" in result and None not in result["enum"]:
|
|
181
|
+
result["enum"] = result["enum"] + [None]
|
|
182
|
+
|
|
179
183
|
return result
|
|
180
184
|
|
|
181
185
|
|
|
@@ -474,9 +474,22 @@ class OpenAPIParser(
|
|
|
474
474
|
and media_type_obj.media_type_schema
|
|
475
475
|
):
|
|
476
476
|
try:
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
477
|
+
# Track if this is a top-level $ref before resolution
|
|
478
|
+
top_level_schema_name = None
|
|
479
|
+
media_schema = media_type_obj.media_type_schema
|
|
480
|
+
if isinstance(media_schema, self.reference_cls):
|
|
481
|
+
ref_str = media_schema.ref
|
|
482
|
+
if isinstance(ref_str, str) and ref_str.startswith(
|
|
483
|
+
"#/components/schemas/"
|
|
484
|
+
):
|
|
485
|
+
top_level_schema_name = ref_str.split("/")[-1]
|
|
486
|
+
|
|
487
|
+
schema_dict = self._extract_schema_as_dict(media_schema)
|
|
488
|
+
# Add marker for top-level schema if it was a ref
|
|
489
|
+
if top_level_schema_name:
|
|
490
|
+
schema_dict["x-fastmcp-top-level-schema"] = (
|
|
491
|
+
top_level_schema_name
|
|
492
|
+
)
|
|
480
493
|
resp_info.content_schema[media_type_str] = schema_dict
|
|
481
494
|
except ValueError as e:
|
|
482
495
|
# Re-raise ValueError for external reference errors
|
|
@@ -625,6 +638,13 @@ class OpenAPIParser(
|
|
|
625
638
|
for response in responses.values():
|
|
626
639
|
if response.content_schema:
|
|
627
640
|
for content_schema in response.content_schema.values():
|
|
641
|
+
# Check if this schema was originally a top-level $ref
|
|
642
|
+
if "x-fastmcp-top-level-schema" in content_schema:
|
|
643
|
+
schema_name = content_schema["x-fastmcp-top-level-schema"]
|
|
644
|
+
if schema_name in all_schemas:
|
|
645
|
+
needed_schemas.add(schema_name)
|
|
646
|
+
|
|
647
|
+
# Extract all dependencies (transitive refs within the schema)
|
|
628
648
|
deps = self._extract_schema_dependencies(
|
|
629
649
|
content_schema, all_schemas
|
|
630
650
|
)
|
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(
|
|
@@ -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,6 +117,7 @@ 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,
|
|
@@ -134,18 +135,22 @@ class Prompt(FastMCPComponent, ABC):
|
|
|
134
135
|
name=name,
|
|
135
136
|
title=title,
|
|
136
137
|
description=description,
|
|
138
|
+
icons=icons,
|
|
137
139
|
tags=tags,
|
|
138
140
|
enabled=enabled,
|
|
139
141
|
meta=meta,
|
|
140
142
|
)
|
|
141
143
|
|
|
142
|
-
@abstractmethod
|
|
143
144
|
async def render(
|
|
144
145
|
self,
|
|
145
146
|
arguments: dict[str, Any] | None = None,
|
|
146
147
|
) -> list[PromptMessage]:
|
|
147
|
-
"""Render the prompt with arguments.
|
|
148
|
-
|
|
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()")
|
|
149
154
|
|
|
150
155
|
|
|
151
156
|
class FunctionPrompt(Prompt):
|
|
@@ -160,6 +165,7 @@ class FunctionPrompt(Prompt):
|
|
|
160
165
|
name: str | None = None,
|
|
161
166
|
title: str | None = None,
|
|
162
167
|
description: str | None = None,
|
|
168
|
+
icons: list[Icon] | None = None,
|
|
163
169
|
tags: set[str] | None = None,
|
|
164
170
|
enabled: bool | None = None,
|
|
165
171
|
meta: dict[str, Any] | None = None,
|
|
@@ -253,6 +259,7 @@ class FunctionPrompt(Prompt):
|
|
|
253
259
|
name=func_name,
|
|
254
260
|
title=title,
|
|
255
261
|
description=description,
|
|
262
|
+
icons=icons,
|
|
256
263
|
arguments=arguments,
|
|
257
264
|
tags=tags or set(),
|
|
258
265
|
enabled=enabled if enabled is not None else True,
|
|
@@ -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/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,
|
|
@@ -31,7 +30,7 @@ if TYPE_CHECKING:
|
|
|
31
30
|
pass
|
|
32
31
|
|
|
33
32
|
|
|
34
|
-
class Resource(FastMCPComponent
|
|
33
|
+
class Resource(FastMCPComponent):
|
|
35
34
|
"""Base class for all resources."""
|
|
36
35
|
|
|
37
36
|
model_config = ConfigDict(validate_default=True)
|
|
@@ -73,6 +72,7 @@ class Resource(FastMCPComponent, abc.ABC):
|
|
|
73
72
|
name: str | None = None,
|
|
74
73
|
title: str | None = None,
|
|
75
74
|
description: str | None = None,
|
|
75
|
+
icons: list[Icon] | None = None,
|
|
76
76
|
mime_type: str | None = None,
|
|
77
77
|
tags: set[str] | None = None,
|
|
78
78
|
enabled: bool | None = None,
|
|
@@ -85,6 +85,7 @@ class Resource(FastMCPComponent, abc.ABC):
|
|
|
85
85
|
name=name,
|
|
86
86
|
title=title,
|
|
87
87
|
description=description,
|
|
88
|
+
icons=icons,
|
|
88
89
|
mime_type=mime_type,
|
|
89
90
|
tags=tags,
|
|
90
91
|
enabled=enabled,
|
|
@@ -111,10 +112,13 @@ class Resource(FastMCPComponent, abc.ABC):
|
|
|
111
112
|
raise ValueError("Either name or uri must be provided")
|
|
112
113
|
return self
|
|
113
114
|
|
|
114
|
-
@abc.abstractmethod
|
|
115
115
|
async def read(self) -> str | bytes:
|
|
116
|
-
"""Read the resource content.
|
|
117
|
-
|
|
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()")
|
|
118
122
|
|
|
119
123
|
def to_mcp_resource(
|
|
120
124
|
self,
|
|
@@ -130,6 +134,7 @@ class Resource(FastMCPComponent, abc.ABC):
|
|
|
130
134
|
description=overrides.get("description", self.description),
|
|
131
135
|
mimeType=overrides.get("mimeType", self.mime_type),
|
|
132
136
|
title=overrides.get("title", self.title),
|
|
137
|
+
icons=overrides.get("icons", self.icons),
|
|
133
138
|
annotations=overrides.get("annotations", self.annotations),
|
|
134
139
|
_meta=overrides.get(
|
|
135
140
|
"_meta", self.get_meta(include_fastmcp_meta=include_fastmcp_meta)
|
|
@@ -173,6 +178,7 @@ class FunctionResource(Resource):
|
|
|
173
178
|
name: str | None = None,
|
|
174
179
|
title: str | None = None,
|
|
175
180
|
description: str | None = None,
|
|
181
|
+
icons: list[Icon] | None = None,
|
|
176
182
|
mime_type: str | None = None,
|
|
177
183
|
tags: set[str] | None = None,
|
|
178
184
|
enabled: bool | None = None,
|
|
@@ -188,6 +194,7 @@ class FunctionResource(Resource):
|
|
|
188
194
|
name=name or get_fn_name(fn),
|
|
189
195
|
title=title,
|
|
190
196
|
description=description or inspect.getdoc(fn),
|
|
197
|
+
icons=icons,
|
|
191
198
|
mime_type=mime_type or "text/plain",
|
|
192
199
|
tags=tags or set(),
|
|
193
200
|
enabled=enabled if enabled is not None else True,
|