fastmcp 2.10.5__py3-none-any.whl → 2.11.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 +7 -2
- fastmcp/cli/cli.py +128 -33
- fastmcp/cli/install/__init__.py +2 -2
- fastmcp/cli/install/claude_code.py +42 -1
- fastmcp/cli/install/claude_desktop.py +42 -1
- fastmcp/cli/install/cursor.py +42 -1
- fastmcp/cli/install/{mcp_config.py → mcp_json.py} +51 -7
- fastmcp/cli/run.py +127 -1
- fastmcp/client/__init__.py +2 -0
- fastmcp/client/auth/oauth.py +68 -99
- fastmcp/client/oauth_callback.py +18 -0
- fastmcp/client/transports.py +69 -15
- fastmcp/contrib/component_manager/example.py +2 -2
- fastmcp/experimental/server/openapi/README.md +266 -0
- fastmcp/experimental/server/openapi/__init__.py +38 -0
- fastmcp/experimental/server/openapi/components.py +348 -0
- fastmcp/experimental/server/openapi/routing.py +132 -0
- fastmcp/experimental/server/openapi/server.py +466 -0
- fastmcp/experimental/utilities/openapi/README.md +239 -0
- fastmcp/experimental/utilities/openapi/__init__.py +68 -0
- fastmcp/experimental/utilities/openapi/director.py +208 -0
- fastmcp/experimental/utilities/openapi/formatters.py +355 -0
- fastmcp/experimental/utilities/openapi/json_schema_converter.py +340 -0
- fastmcp/experimental/utilities/openapi/models.py +85 -0
- fastmcp/experimental/utilities/openapi/parser.py +618 -0
- fastmcp/experimental/utilities/openapi/schemas.py +538 -0
- fastmcp/mcp_config.py +125 -88
- fastmcp/prompts/prompt.py +11 -1
- fastmcp/prompts/prompt_manager.py +1 -1
- fastmcp/resources/resource.py +21 -1
- fastmcp/resources/resource_manager.py +2 -2
- fastmcp/resources/template.py +20 -1
- fastmcp/server/auth/__init__.py +17 -2
- fastmcp/server/auth/auth.py +144 -7
- fastmcp/server/auth/providers/bearer.py +25 -473
- fastmcp/server/auth/providers/in_memory.py +4 -2
- fastmcp/server/auth/providers/jwt.py +538 -0
- fastmcp/server/auth/providers/workos.py +170 -0
- fastmcp/server/auth/registry.py +52 -0
- fastmcp/server/context.py +110 -26
- fastmcp/server/dependencies.py +9 -2
- fastmcp/server/http.py +62 -30
- fastmcp/server/middleware/middleware.py +3 -23
- fastmcp/server/openapi.py +26 -13
- fastmcp/server/proxy.py +89 -8
- fastmcp/server/server.py +170 -62
- fastmcp/settings.py +83 -18
- fastmcp/tools/tool.py +41 -6
- fastmcp/tools/tool_manager.py +39 -3
- fastmcp/tools/tool_transform.py +122 -6
- fastmcp/utilities/components.py +35 -2
- fastmcp/utilities/json_schema.py +136 -98
- fastmcp/utilities/json_schema_type.py +1 -3
- fastmcp/utilities/mcp_config.py +28 -0
- fastmcp/utilities/openapi.py +306 -30
- fastmcp/utilities/tests.py +54 -6
- fastmcp/utilities/types.py +89 -11
- {fastmcp-2.10.5.dist-info → fastmcp-2.11.0.dist-info}/METADATA +4 -3
- fastmcp-2.11.0.dist-info/RECORD +108 -0
- fastmcp/server/auth/providers/bearer_env.py +0 -63
- fastmcp/utilities/cache.py +0 -26
- fastmcp-2.10.5.dist-info/RECORD +0 -93
- {fastmcp-2.10.5.dist-info → fastmcp-2.11.0.dist-info}/WHEEL +0 -0
- {fastmcp-2.10.5.dist-info → fastmcp-2.11.0.dist-info}/entry_points.txt +0 -0
- {fastmcp-2.10.5.dist-info → fastmcp-2.11.0.dist-info}/licenses/LICENSE +0 -0
fastmcp/mcp_config.py
CHANGED
|
@@ -7,33 +7,47 @@ The configuration format supports both stdio and remote (HTTP/SSE) transports, w
|
|
|
7
7
|
field definitions for server metadata, authentication, and execution parameters.
|
|
8
8
|
|
|
9
9
|
Example configuration:
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
10
|
+
```json
|
|
11
|
+
{
|
|
12
|
+
"mcpServers": {
|
|
13
|
+
"my-server": {
|
|
14
|
+
"command": "npx",
|
|
15
|
+
"args": ["-y", "@my/mcp-server"],
|
|
16
|
+
"env": {"API_KEY": "secret"},
|
|
17
|
+
"timeout": 30000,
|
|
18
|
+
"description": "My MCP server"
|
|
19
19
|
}
|
|
20
20
|
}
|
|
21
|
+
}
|
|
22
|
+
```
|
|
21
23
|
"""
|
|
22
24
|
|
|
23
25
|
from __future__ import annotations
|
|
24
26
|
|
|
25
27
|
import datetime
|
|
26
|
-
import json
|
|
27
28
|
import re
|
|
28
29
|
from pathlib import Path
|
|
29
30
|
from typing import TYPE_CHECKING, Annotated, Any, Literal
|
|
30
31
|
from urllib.parse import urlparse
|
|
31
32
|
|
|
32
33
|
import httpx
|
|
33
|
-
from pydantic import
|
|
34
|
+
from pydantic import (
|
|
35
|
+
AnyUrl,
|
|
36
|
+
BaseModel,
|
|
37
|
+
ConfigDict,
|
|
38
|
+
Field,
|
|
39
|
+
ValidationInfo,
|
|
40
|
+
model_validator,
|
|
41
|
+
)
|
|
42
|
+
from typing_extensions import Self, override
|
|
43
|
+
|
|
44
|
+
from fastmcp.tools.tool_transform import ToolTransformConfig
|
|
45
|
+
from fastmcp.utilities.types import FastMCPBaseModel
|
|
34
46
|
|
|
35
47
|
if TYPE_CHECKING:
|
|
36
48
|
from fastmcp.client.transports import (
|
|
49
|
+
ClientTransport,
|
|
50
|
+
FastMCPTransport,
|
|
37
51
|
SSETransport,
|
|
38
52
|
StdioTransport,
|
|
39
53
|
StreamableHttpTransport,
|
|
@@ -60,6 +74,39 @@ def infer_transport_type_from_url(
|
|
|
60
74
|
return "http"
|
|
61
75
|
|
|
62
76
|
|
|
77
|
+
class _TransformingMCPServerMixin(FastMCPBaseModel):
|
|
78
|
+
"""A mixin that enables wrapping an MCP Server with tool transforms."""
|
|
79
|
+
|
|
80
|
+
tools: dict[str, ToolTransformConfig] = Field(...)
|
|
81
|
+
"""The multi-tool transform to apply to the tools."""
|
|
82
|
+
|
|
83
|
+
include_tags: set[str] | None = Field(
|
|
84
|
+
default=None,
|
|
85
|
+
description="The tags to include in the proxy.",
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
exclude_tags: set[str] | None = Field(
|
|
89
|
+
default=None,
|
|
90
|
+
description="The tags to exclude in the proxy.",
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
def to_transport(self) -> FastMCPTransport:
|
|
94
|
+
"""Get the transport for the server."""
|
|
95
|
+
from fastmcp.client.transports import FastMCPTransport
|
|
96
|
+
from fastmcp.server.server import FastMCP
|
|
97
|
+
|
|
98
|
+
transport: ClientTransport = super().to_transport() # pyright: ignore[reportUnknownMemberType, reportAttributeAccessIssue, reportUnknownVariableType]
|
|
99
|
+
|
|
100
|
+
wrapped_mcp_server = FastMCP.as_proxy(
|
|
101
|
+
transport,
|
|
102
|
+
tool_transformations=self.tools,
|
|
103
|
+
include_tags=self.include_tags,
|
|
104
|
+
exclude_tags=self.exclude_tags,
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
return FastMCPTransport(wrapped_mcp_server)
|
|
108
|
+
|
|
109
|
+
|
|
63
110
|
class StdioMCPServer(BaseModel):
|
|
64
111
|
"""MCP server configuration for stdio transport.
|
|
65
112
|
|
|
@@ -101,6 +148,10 @@ class StdioMCPServer(BaseModel):
|
|
|
101
148
|
)
|
|
102
149
|
|
|
103
150
|
|
|
151
|
+
class TransformingStdioMCPServer(_TransformingMCPServerMixin, StdioMCPServer):
|
|
152
|
+
"""A Stdio server with tool transforms."""
|
|
153
|
+
|
|
154
|
+
|
|
104
155
|
class RemoteMCPServer(BaseModel):
|
|
105
156
|
"""MCP server configuration for HTTP/SSE transport.
|
|
106
157
|
|
|
@@ -162,120 +213,106 @@ class RemoteMCPServer(BaseModel):
|
|
|
162
213
|
)
|
|
163
214
|
|
|
164
215
|
|
|
216
|
+
class TransformingRemoteMCPServer(_TransformingMCPServerMixin, RemoteMCPServer):
|
|
217
|
+
"""A Remote server with tool transforms."""
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
TransformingMCPServerTypes = TransformingStdioMCPServer | TransformingRemoteMCPServer
|
|
221
|
+
|
|
222
|
+
CanonicalMCPServerTypes = StdioMCPServer | RemoteMCPServer
|
|
223
|
+
|
|
224
|
+
MCPServerTypes = TransformingMCPServerTypes | CanonicalMCPServerTypes
|
|
225
|
+
|
|
226
|
+
|
|
165
227
|
class MCPConfig(BaseModel):
|
|
166
|
-
"""
|
|
228
|
+
"""A configuration object for MCP Servers that conforms to the canonical MCP configuration format
|
|
229
|
+
while adding additional fields for enabling FastMCP-specific features like tool transformations
|
|
230
|
+
and filtering by tags.
|
|
167
231
|
|
|
168
|
-
|
|
169
|
-
The format is designed to be client-agnostic and extensible for future use cases.
|
|
232
|
+
For an MCPConfig that is strictly canonical, see the `CanonicalMCPConfig` class.
|
|
170
233
|
"""
|
|
171
234
|
|
|
172
|
-
mcpServers: dict[str,
|
|
235
|
+
mcpServers: dict[str, MCPServerTypes]
|
|
173
236
|
|
|
174
237
|
model_config = ConfigDict(extra="allow") # Preserve unknown top-level fields
|
|
175
238
|
|
|
239
|
+
@model_validator(mode="before")
|
|
240
|
+
def validate_mcp_servers(self, info: ValidationInfo) -> dict[str, Any]:
|
|
241
|
+
"""Validate the MCP servers."""
|
|
242
|
+
if not isinstance(self, dict):
|
|
243
|
+
raise ValueError("MCPConfig format requires a dictionary of servers.")
|
|
244
|
+
|
|
245
|
+
if "mcpServers" not in self:
|
|
246
|
+
self = {"mcpServers": self}
|
|
247
|
+
|
|
248
|
+
return self
|
|
249
|
+
|
|
250
|
+
def add_server(self, name: str, server: MCPServerTypes) -> None:
|
|
251
|
+
"""Add or update a server in the configuration."""
|
|
252
|
+
self.mcpServers[name] = server
|
|
253
|
+
|
|
176
254
|
@classmethod
|
|
177
|
-
def from_dict(cls, config: dict[str, Any]) ->
|
|
255
|
+
def from_dict(cls, config: dict[str, Any]) -> Self:
|
|
178
256
|
"""Parse MCP configuration from dictionary format."""
|
|
179
|
-
|
|
180
|
-
if "mcpServers" not in config and any(
|
|
181
|
-
isinstance(v, dict) and ("command" in v or "url" in v)
|
|
182
|
-
for v in config.values()
|
|
183
|
-
):
|
|
184
|
-
# This looks like a bare mcpServers object
|
|
185
|
-
servers_dict = config
|
|
186
|
-
else:
|
|
187
|
-
# Standard format with mcpServers wrapper
|
|
188
|
-
servers_dict = config.get("mcpServers", {})
|
|
189
|
-
|
|
190
|
-
# Parse each server configuration
|
|
191
|
-
parsed_servers = {}
|
|
192
|
-
for name, server_config in servers_dict.items():
|
|
193
|
-
if not isinstance(server_config, dict):
|
|
194
|
-
continue
|
|
195
|
-
|
|
196
|
-
# Determine if this is stdio or remote based on fields
|
|
197
|
-
if "command" in server_config:
|
|
198
|
-
parsed_servers[name] = StdioMCPServer.model_validate(server_config)
|
|
199
|
-
elif "url" in server_config:
|
|
200
|
-
parsed_servers[name] = RemoteMCPServer.model_validate(server_config)
|
|
201
|
-
else:
|
|
202
|
-
# Skip invalid server configs but preserve them as raw dicts
|
|
203
|
-
# This allows for forward compatibility with unknown server types
|
|
204
|
-
continue
|
|
205
|
-
|
|
206
|
-
# Create config with any extra top-level fields preserved
|
|
207
|
-
config_data = {k: v for k, v in config.items() if k != "mcpServers"}
|
|
208
|
-
config_data["mcpServers"] = parsed_servers
|
|
209
|
-
|
|
210
|
-
return cls.model_validate(config_data)
|
|
257
|
+
return cls.model_validate(config)
|
|
211
258
|
|
|
212
259
|
def to_dict(self) -> dict[str, Any]:
|
|
213
260
|
"""Convert MCPConfig to dictionary format, preserving all fields."""
|
|
214
|
-
|
|
215
|
-
result = self.model_dump(exclude={"mcpServers"}, exclude_none=True)
|
|
216
|
-
|
|
217
|
-
# Add mcpServers with all fields preserved
|
|
218
|
-
result["mcpServers"] = {
|
|
219
|
-
name: server.model_dump(exclude_none=True)
|
|
220
|
-
for name, server in self.mcpServers.items()
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
return result
|
|
261
|
+
return self.model_dump(exclude_none=True)
|
|
224
262
|
|
|
225
263
|
def write_to_file(self, file_path: Path) -> None:
|
|
226
264
|
"""Write configuration to JSON file."""
|
|
227
265
|
file_path.parent.mkdir(parents=True, exist_ok=True)
|
|
228
|
-
|
|
229
|
-
json.dump(self.to_dict(), f, indent=2)
|
|
266
|
+
file_path.write_text(self.model_dump_json(indent=2))
|
|
230
267
|
|
|
231
268
|
@classmethod
|
|
232
|
-
def from_file(cls, file_path: Path) ->
|
|
269
|
+
def from_file(cls, file_path: Path) -> Self:
|
|
233
270
|
"""Load configuration from JSON file."""
|
|
234
|
-
if
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
271
|
+
if file_path.exists():
|
|
272
|
+
if content := file_path.read_text().strip():
|
|
273
|
+
return cls.model_validate_json(content)
|
|
274
|
+
|
|
275
|
+
raise ValueError(f"No MCP servers defined in the config: {file_path}")
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
class CanonicalMCPConfig(MCPConfig):
|
|
279
|
+
"""Canonical MCP configuration format.
|
|
280
|
+
|
|
281
|
+
This defines the standard configuration format for Model Context Protocol servers.
|
|
282
|
+
The format is designed to be client-agnostic and extensible for future use cases.
|
|
283
|
+
"""
|
|
284
|
+
|
|
285
|
+
mcpServers: dict[str, CanonicalMCPServerTypes]
|
|
286
|
+
|
|
287
|
+
@override
|
|
288
|
+
def add_server(self, name: str, server: CanonicalMCPServerTypes) -> None:
|
|
244
289
|
"""Add or update a server in the configuration."""
|
|
245
290
|
self.mcpServers[name] = server
|
|
246
291
|
|
|
247
|
-
def remove_server(self, name: str) -> None:
|
|
248
|
-
"""Remove a server from the configuration."""
|
|
249
|
-
if name in self.mcpServers:
|
|
250
|
-
del self.mcpServers[name]
|
|
251
|
-
|
|
252
292
|
|
|
253
293
|
def update_config_file(
|
|
254
294
|
file_path: Path,
|
|
255
295
|
server_name: str,
|
|
256
|
-
server_config:
|
|
296
|
+
server_config: CanonicalMCPServerTypes,
|
|
257
297
|
) -> None:
|
|
258
|
-
"""Update MCP configuration file
|
|
298
|
+
"""Update an MCP configuration file from a server object, preserving existing fields.
|
|
299
|
+
|
|
300
|
+
This is used for updating the mcpServer configurations of third-party tools so we do not
|
|
301
|
+
worry about transforming server objects here."""
|
|
259
302
|
config = MCPConfig.from_file(file_path)
|
|
260
303
|
|
|
261
304
|
# If updating an existing server, merge with existing configuration
|
|
262
305
|
# to preserve any unknown fields
|
|
263
|
-
if
|
|
264
|
-
existing_server = config.mcpServers[server_name]
|
|
306
|
+
if existing_server := config.mcpServers.get(server_name):
|
|
265
307
|
# Get the raw dict representation of both servers
|
|
266
308
|
existing_dict = existing_server.model_dump()
|
|
309
|
+
|
|
267
310
|
new_dict = server_config.model_dump(exclude_none=True)
|
|
268
311
|
|
|
269
312
|
# Merge, with new values taking precedence
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
# Create new server instance with merged data
|
|
273
|
-
if "command" in merged_dict:
|
|
274
|
-
merged_server = StdioMCPServer.model_validate(merged_dict)
|
|
275
|
-
else:
|
|
276
|
-
merged_server = RemoteMCPServer.model_validate(merged_dict)
|
|
313
|
+
merged_config = server_config.model_validate({**existing_dict, **new_dict})
|
|
277
314
|
|
|
278
|
-
config.add_server(server_name,
|
|
315
|
+
config.add_server(server_name, merged_config)
|
|
279
316
|
else:
|
|
280
317
|
config.add_server(server_name, server_config)
|
|
281
318
|
|
fastmcp/prompts/prompt.py
CHANGED
|
@@ -85,7 +85,12 @@ class Prompt(FastMCPComponent, ABC):
|
|
|
85
85
|
except RuntimeError:
|
|
86
86
|
pass # No context available
|
|
87
87
|
|
|
88
|
-
def to_mcp_prompt(
|
|
88
|
+
def to_mcp_prompt(
|
|
89
|
+
self,
|
|
90
|
+
*,
|
|
91
|
+
include_fastmcp_meta: bool | None = None,
|
|
92
|
+
**overrides: Any,
|
|
93
|
+
) -> MCPPrompt:
|
|
89
94
|
"""Convert the prompt to an MCP prompt."""
|
|
90
95
|
arguments = [
|
|
91
96
|
MCPPromptArgument(
|
|
@@ -100,6 +105,7 @@ class Prompt(FastMCPComponent, ABC):
|
|
|
100
105
|
"description": self.description,
|
|
101
106
|
"arguments": arguments,
|
|
102
107
|
"title": self.title,
|
|
108
|
+
"_meta": self.get_meta(include_fastmcp_meta=include_fastmcp_meta),
|
|
103
109
|
}
|
|
104
110
|
return MCPPrompt(**kwargs | overrides)
|
|
105
111
|
|
|
@@ -111,6 +117,7 @@ class Prompt(FastMCPComponent, ABC):
|
|
|
111
117
|
description: str | None = None,
|
|
112
118
|
tags: set[str] | None = None,
|
|
113
119
|
enabled: bool | None = None,
|
|
120
|
+
meta: dict[str, Any] | None = None,
|
|
114
121
|
) -> FunctionPrompt:
|
|
115
122
|
"""Create a Prompt from a function.
|
|
116
123
|
|
|
@@ -127,6 +134,7 @@ class Prompt(FastMCPComponent, ABC):
|
|
|
127
134
|
description=description,
|
|
128
135
|
tags=tags,
|
|
129
136
|
enabled=enabled,
|
|
137
|
+
meta=meta,
|
|
130
138
|
)
|
|
131
139
|
|
|
132
140
|
@abstractmethod
|
|
@@ -152,6 +160,7 @@ class FunctionPrompt(Prompt):
|
|
|
152
160
|
description: str | None = None,
|
|
153
161
|
tags: set[str] | None = None,
|
|
154
162
|
enabled: bool | None = None,
|
|
163
|
+
meta: dict[str, Any] | None = None,
|
|
155
164
|
) -> FunctionPrompt:
|
|
156
165
|
"""Create a Prompt from a function.
|
|
157
166
|
|
|
@@ -246,6 +255,7 @@ class FunctionPrompt(Prompt):
|
|
|
246
255
|
tags=tags or set(),
|
|
247
256
|
enabled=enabled if enabled is not None else True,
|
|
248
257
|
fn=fn,
|
|
258
|
+
meta=meta,
|
|
249
259
|
)
|
|
250
260
|
|
|
251
261
|
def _convert_string_arguments(self, kwargs: dict[str, Any]) -> dict[str, Any]:
|
|
@@ -78,7 +78,7 @@ class PromptManager:
|
|
|
78
78
|
except Exception as e:
|
|
79
79
|
# Skip failed mounts silently, matches existing behavior
|
|
80
80
|
logger.warning(
|
|
81
|
-
f"Failed to get prompts from mounted
|
|
81
|
+
f"Failed to get prompts from server: {mounted.server.name!r}, mounted at: {mounted.prefix!r}: {e}"
|
|
82
82
|
)
|
|
83
83
|
continue
|
|
84
84
|
|
fastmcp/resources/resource.py
CHANGED
|
@@ -8,6 +8,7 @@ from collections.abc import Callable
|
|
|
8
8
|
from typing import TYPE_CHECKING, Annotated, Any
|
|
9
9
|
|
|
10
10
|
import pydantic_core
|
|
11
|
+
from mcp.types import Annotations
|
|
11
12
|
from mcp.types import Resource as MCPResource
|
|
12
13
|
from pydantic import (
|
|
13
14
|
AnyUrl,
|
|
@@ -43,6 +44,10 @@ class Resource(FastMCPComponent, abc.ABC):
|
|
|
43
44
|
description="MIME type of the resource content",
|
|
44
45
|
pattern=r"^[a-zA-Z0-9]+/[a-zA-Z0-9\-+.]+$",
|
|
45
46
|
)
|
|
47
|
+
annotations: Annotated[
|
|
48
|
+
Annotations | None,
|
|
49
|
+
Field(description="Optional annotations about the resource's behavior"),
|
|
50
|
+
] = None
|
|
46
51
|
|
|
47
52
|
def enable(self) -> None:
|
|
48
53
|
super().enable()
|
|
@@ -70,6 +75,8 @@ class Resource(FastMCPComponent, abc.ABC):
|
|
|
70
75
|
mime_type: str | None = None,
|
|
71
76
|
tags: set[str] | None = None,
|
|
72
77
|
enabled: bool | None = None,
|
|
78
|
+
annotations: Annotations | None = None,
|
|
79
|
+
meta: dict[str, Any] | None = None,
|
|
73
80
|
) -> FunctionResource:
|
|
74
81
|
return FunctionResource.from_function(
|
|
75
82
|
fn=fn,
|
|
@@ -80,6 +87,8 @@ class Resource(FastMCPComponent, abc.ABC):
|
|
|
80
87
|
mime_type=mime_type,
|
|
81
88
|
tags=tags,
|
|
82
89
|
enabled=enabled,
|
|
90
|
+
annotations=annotations,
|
|
91
|
+
meta=meta,
|
|
83
92
|
)
|
|
84
93
|
|
|
85
94
|
@field_validator("mime_type", mode="before")
|
|
@@ -106,7 +115,12 @@ class Resource(FastMCPComponent, abc.ABC):
|
|
|
106
115
|
"""Read the resource content."""
|
|
107
116
|
pass
|
|
108
117
|
|
|
109
|
-
def to_mcp_resource(
|
|
118
|
+
def to_mcp_resource(
|
|
119
|
+
self,
|
|
120
|
+
*,
|
|
121
|
+
include_fastmcp_meta: bool | None = None,
|
|
122
|
+
**overrides: Any,
|
|
123
|
+
) -> MCPResource:
|
|
110
124
|
"""Convert the resource to an MCPResource."""
|
|
111
125
|
kwargs = {
|
|
112
126
|
"uri": self.uri,
|
|
@@ -114,6 +128,8 @@ class Resource(FastMCPComponent, abc.ABC):
|
|
|
114
128
|
"description": self.description,
|
|
115
129
|
"mimeType": self.mime_type,
|
|
116
130
|
"title": self.title,
|
|
131
|
+
"annotations": self.annotations,
|
|
132
|
+
"_meta": self.get_meta(include_fastmcp_meta=include_fastmcp_meta),
|
|
117
133
|
}
|
|
118
134
|
return MCPResource(**kwargs | overrides)
|
|
119
135
|
|
|
@@ -157,6 +173,8 @@ class FunctionResource(Resource):
|
|
|
157
173
|
mime_type: str | None = None,
|
|
158
174
|
tags: set[str] | None = None,
|
|
159
175
|
enabled: bool | None = None,
|
|
176
|
+
annotations: Annotations | None = None,
|
|
177
|
+
meta: dict[str, Any] | None = None,
|
|
160
178
|
) -> FunctionResource:
|
|
161
179
|
"""Create a FunctionResource from a function."""
|
|
162
180
|
if isinstance(uri, str):
|
|
@@ -170,6 +188,8 @@ class FunctionResource(Resource):
|
|
|
170
188
|
mime_type=mime_type or "text/plain",
|
|
171
189
|
tags=tags or set(),
|
|
172
190
|
enabled=enabled if enabled is not None else True,
|
|
191
|
+
annotations=annotations,
|
|
192
|
+
meta=meta,
|
|
173
193
|
)
|
|
174
194
|
|
|
175
195
|
async def read(self) -> str | bytes:
|
|
@@ -109,7 +109,7 @@ class ResourceManager:
|
|
|
109
109
|
except Exception as e:
|
|
110
110
|
# Skip failed mounts silently, matches existing behavior
|
|
111
111
|
logger.warning(
|
|
112
|
-
f"Failed to get resources from mounted
|
|
112
|
+
f"Failed to get resources from server: {mounted.server.name!r}, mounted at: {mounted.prefix!r}: {e}"
|
|
113
113
|
)
|
|
114
114
|
continue
|
|
115
115
|
|
|
@@ -157,7 +157,7 @@ class ResourceManager:
|
|
|
157
157
|
except Exception as e:
|
|
158
158
|
# Skip failed mounts silently, matches existing behavior
|
|
159
159
|
logger.warning(
|
|
160
|
-
f"Failed to get templates from mounted
|
|
160
|
+
f"Failed to get templates from server: {mounted.server.name!r}, mounted at: {mounted.prefix!r}: {e}"
|
|
161
161
|
)
|
|
162
162
|
continue
|
|
163
163
|
|
fastmcp/resources/template.py
CHANGED
|
@@ -8,6 +8,7 @@ from collections.abc import Callable
|
|
|
8
8
|
from typing import Any
|
|
9
9
|
from urllib.parse import unquote
|
|
10
10
|
|
|
11
|
+
from mcp.types import Annotations
|
|
11
12
|
from mcp.types import ResourceTemplate as MCPResourceTemplate
|
|
12
13
|
from pydantic import (
|
|
13
14
|
Field,
|
|
@@ -61,6 +62,9 @@ class ResourceTemplate(FastMCPComponent):
|
|
|
61
62
|
parameters: dict[str, Any] = Field(
|
|
62
63
|
description="JSON schema for function parameters"
|
|
63
64
|
)
|
|
65
|
+
annotations: Annotations | None = Field(
|
|
66
|
+
default=None, description="Optional annotations about the resource's behavior"
|
|
67
|
+
)
|
|
64
68
|
|
|
65
69
|
def __repr__(self) -> str:
|
|
66
70
|
return f"{self.__class__.__name__}(uri_template={self.uri_template!r}, name={self.name!r}, description={self.description!r}, tags={self.tags})"
|
|
@@ -91,6 +95,8 @@ class ResourceTemplate(FastMCPComponent):
|
|
|
91
95
|
mime_type: str | None = None,
|
|
92
96
|
tags: set[str] | None = None,
|
|
93
97
|
enabled: bool | None = None,
|
|
98
|
+
annotations: Annotations | None = None,
|
|
99
|
+
meta: dict[str, Any] | None = None,
|
|
94
100
|
) -> FunctionResourceTemplate:
|
|
95
101
|
return FunctionResourceTemplate.from_function(
|
|
96
102
|
fn=fn,
|
|
@@ -101,6 +107,8 @@ class ResourceTemplate(FastMCPComponent):
|
|
|
101
107
|
mime_type=mime_type,
|
|
102
108
|
tags=tags,
|
|
103
109
|
enabled=enabled,
|
|
110
|
+
annotations=annotations,
|
|
111
|
+
meta=meta,
|
|
104
112
|
)
|
|
105
113
|
|
|
106
114
|
@field_validator("mime_type", mode="before")
|
|
@@ -139,7 +147,12 @@ class ResourceTemplate(FastMCPComponent):
|
|
|
139
147
|
enabled=self.enabled,
|
|
140
148
|
)
|
|
141
149
|
|
|
142
|
-
def to_mcp_template(
|
|
150
|
+
def to_mcp_template(
|
|
151
|
+
self,
|
|
152
|
+
*,
|
|
153
|
+
include_fastmcp_meta: bool | None = None,
|
|
154
|
+
**overrides: Any,
|
|
155
|
+
) -> MCPResourceTemplate:
|
|
143
156
|
"""Convert the resource template to an MCPResourceTemplate."""
|
|
144
157
|
kwargs = {
|
|
145
158
|
"uriTemplate": self.uri_template,
|
|
@@ -147,6 +160,8 @@ class ResourceTemplate(FastMCPComponent):
|
|
|
147
160
|
"description": self.description,
|
|
148
161
|
"mimeType": self.mime_type,
|
|
149
162
|
"title": self.title,
|
|
163
|
+
"annotations": self.annotations,
|
|
164
|
+
"_meta": self.get_meta(include_fastmcp_meta=include_fastmcp_meta),
|
|
150
165
|
}
|
|
151
166
|
return MCPResourceTemplate(**kwargs | overrides)
|
|
152
167
|
|
|
@@ -205,6 +220,8 @@ class FunctionResourceTemplate(ResourceTemplate):
|
|
|
205
220
|
mime_type: str | None = None,
|
|
206
221
|
tags: set[str] | None = None,
|
|
207
222
|
enabled: bool | None = None,
|
|
223
|
+
annotations: Annotations | None = None,
|
|
224
|
+
meta: dict[str, Any] | None = None,
|
|
208
225
|
) -> FunctionResourceTemplate:
|
|
209
226
|
"""Create a template from a function."""
|
|
210
227
|
from fastmcp.server.context import Context
|
|
@@ -289,4 +306,6 @@ class FunctionResourceTemplate(ResourceTemplate):
|
|
|
289
306
|
parameters=parameters,
|
|
290
307
|
tags=tags or set(),
|
|
291
308
|
enabled=enabled if enabled is not None else True,
|
|
309
|
+
annotations=annotations,
|
|
310
|
+
meta=meta,
|
|
292
311
|
)
|
fastmcp/server/auth/__init__.py
CHANGED
|
@@ -1,4 +1,19 @@
|
|
|
1
|
-
from .
|
|
1
|
+
from .auth import OAuthProvider, TokenVerifier
|
|
2
|
+
from .providers.jwt import JWTVerifier, StaticTokenVerifier
|
|
2
3
|
|
|
3
4
|
|
|
4
|
-
__all__ = [
|
|
5
|
+
__all__ = [
|
|
6
|
+
"OAuthProvider",
|
|
7
|
+
"TokenVerifier",
|
|
8
|
+
"JWTVerifier",
|
|
9
|
+
"StaticTokenVerifier",
|
|
10
|
+
]
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def __getattr__(name: str):
|
|
14
|
+
# Defer import because it raises a deprecation warning
|
|
15
|
+
if name == "BearerAuthProvider":
|
|
16
|
+
from .providers.bearer import BearerAuthProvider
|
|
17
|
+
|
|
18
|
+
return BearerAuthProvider
|
|
19
|
+
raise AttributeError(f"module '{__name__}' has no attribute '{name}'")
|