fastmcp 2.6.0__py3-none-any.whl → 2.7.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/cli/cli.py +9 -1
- fastmcp/cli/run.py +32 -1
- fastmcp/client/auth/oauth.py +0 -3
- fastmcp/client/transports.py +13 -4
- fastmcp/contrib/bulk_tool_caller/example.py +1 -1
- fastmcp/contrib/mcp_mixin/mcp_mixin.py +10 -3
- fastmcp/prompts/prompt.py +65 -27
- fastmcp/prompts/prompt_manager.py +13 -6
- fastmcp/py.typed +0 -0
- fastmcp/resources/__init__.py +1 -2
- fastmcp/resources/resource.py +90 -4
- fastmcp/resources/resource_manager.py +17 -6
- fastmcp/resources/template.py +90 -56
- fastmcp/resources/types.py +0 -44
- fastmcp/server/auth/providers/in_memory.py +1 -6
- fastmcp/server/context.py +1 -1
- fastmcp/server/openapi.py +17 -32
- fastmcp/server/proxy.py +5 -8
- fastmcp/server/server.py +274 -100
- fastmcp/tools/__init__.py +2 -2
- fastmcp/tools/tool.py +59 -19
- fastmcp/tools/tool_manager.py +9 -3
- fastmcp/utilities/mcp_config.py +6 -4
- fastmcp/utilities/openapi.py +56 -32
- fastmcp/utilities/types.py +7 -1
- {fastmcp-2.6.0.dist-info → fastmcp-2.7.0.dist-info}/METADATA +29 -17
- {fastmcp-2.6.0.dist-info → fastmcp-2.7.0.dist-info}/RECORD +30 -30
- fastmcp/utilities/decorators.py +0 -101
- {fastmcp-2.6.0.dist-info → fastmcp-2.7.0.dist-info}/WHEEL +0 -0
- {fastmcp-2.6.0.dist-info → fastmcp-2.7.0.dist-info}/entry_points.txt +0 -0
- {fastmcp-2.6.0.dist-info → fastmcp-2.7.0.dist-info}/licenses/LICENSE +0 -0
fastmcp/cli/cli.py
CHANGED
|
@@ -219,8 +219,9 @@ def dev(
|
|
|
219
219
|
sys.exit(1)
|
|
220
220
|
|
|
221
221
|
|
|
222
|
-
@app.command()
|
|
222
|
+
@app.command(context_settings={"allow_extra_args": True})
|
|
223
223
|
def run(
|
|
224
|
+
ctx: typer.Context,
|
|
224
225
|
server_spec: str = typer.Argument(
|
|
225
226
|
...,
|
|
226
227
|
help="Python file, object specification (file:obj), or URL",
|
|
@@ -266,7 +267,12 @@ def run(
|
|
|
266
267
|
|
|
267
268
|
Note: This command runs the server directly. You are responsible for ensuring
|
|
268
269
|
all dependencies are available.
|
|
270
|
+
|
|
271
|
+
Server arguments can be passed after -- :
|
|
272
|
+
fastmcp run server.py -- --config config.json --debug
|
|
269
273
|
"""
|
|
274
|
+
server_args = ctx.args # extra args after --
|
|
275
|
+
|
|
270
276
|
logger.debug(
|
|
271
277
|
"Running server or client",
|
|
272
278
|
extra={
|
|
@@ -275,6 +281,7 @@ def run(
|
|
|
275
281
|
"host": host,
|
|
276
282
|
"port": port,
|
|
277
283
|
"log_level": log_level,
|
|
284
|
+
"server_args": server_args,
|
|
278
285
|
},
|
|
279
286
|
)
|
|
280
287
|
|
|
@@ -285,6 +292,7 @@ def run(
|
|
|
285
292
|
host=host,
|
|
286
293
|
port=port,
|
|
287
294
|
log_level=log_level,
|
|
295
|
+
server_args=server_args,
|
|
288
296
|
)
|
|
289
297
|
except Exception as e:
|
|
290
298
|
logger.error(
|
fastmcp/cli/run.py
CHANGED
|
@@ -71,6 +71,9 @@ def import_server(file: Path, server_object: str | None = None) -> Any:
|
|
|
71
71
|
logger.error("Could not load module", extra={"file": str(file)})
|
|
72
72
|
sys.exit(1)
|
|
73
73
|
|
|
74
|
+
assert spec is not None
|
|
75
|
+
assert spec.loader is not None
|
|
76
|
+
|
|
74
77
|
module = importlib.util.module_from_spec(spec)
|
|
75
78
|
spec.loader.exec_module(module)
|
|
76
79
|
|
|
@@ -89,6 +92,8 @@ def import_server(file: Path, server_object: str | None = None) -> Any:
|
|
|
89
92
|
)
|
|
90
93
|
sys.exit(1)
|
|
91
94
|
|
|
95
|
+
assert server_object is not None
|
|
96
|
+
|
|
92
97
|
# Handle module:object syntax
|
|
93
98
|
if ":" in server_object:
|
|
94
99
|
module_name, object_name = server_object.split(":", 1)
|
|
@@ -135,12 +140,37 @@ def create_client_server(url: str) -> Any:
|
|
|
135
140
|
sys.exit(1)
|
|
136
141
|
|
|
137
142
|
|
|
143
|
+
def import_server_with_args(
|
|
144
|
+
file: Path, server_object: str | None = None, server_args: list[str] | None = None
|
|
145
|
+
) -> Any:
|
|
146
|
+
"""Import a server with optional command line arguments.
|
|
147
|
+
|
|
148
|
+
Args:
|
|
149
|
+
file: Path to the server file
|
|
150
|
+
server_object: Optional server object name
|
|
151
|
+
server_args: Optional command line arguments to inject
|
|
152
|
+
|
|
153
|
+
Returns:
|
|
154
|
+
The imported server object
|
|
155
|
+
"""
|
|
156
|
+
if server_args:
|
|
157
|
+
original_argv = sys.argv[:]
|
|
158
|
+
try:
|
|
159
|
+
sys.argv = [str(file)] + server_args
|
|
160
|
+
return import_server(file, server_object)
|
|
161
|
+
finally:
|
|
162
|
+
sys.argv = original_argv
|
|
163
|
+
else:
|
|
164
|
+
return import_server(file, server_object)
|
|
165
|
+
|
|
166
|
+
|
|
138
167
|
def run_command(
|
|
139
168
|
server_spec: str,
|
|
140
169
|
transport: str | None = None,
|
|
141
170
|
host: str | None = None,
|
|
142
171
|
port: int | None = None,
|
|
143
172
|
log_level: str | None = None,
|
|
173
|
+
server_args: list[str] | None = None,
|
|
144
174
|
) -> None:
|
|
145
175
|
"""Run a MCP server or connect to a remote one.
|
|
146
176
|
|
|
@@ -150,6 +180,7 @@ def run_command(
|
|
|
150
180
|
host: Host to bind to when using http transport
|
|
151
181
|
port: Port to bind to when using http transport
|
|
152
182
|
log_level: Log level
|
|
183
|
+
server_args: Additional arguments to pass to the server
|
|
153
184
|
"""
|
|
154
185
|
if is_url(server_spec):
|
|
155
186
|
# Handle URL case
|
|
@@ -158,7 +189,7 @@ def run_command(
|
|
|
158
189
|
else:
|
|
159
190
|
# Handle file case
|
|
160
191
|
file, server_object = parse_file_path(server_spec)
|
|
161
|
-
server =
|
|
192
|
+
server = import_server_with_args(file, server_object, server_args)
|
|
162
193
|
logger.debug(f'Found server "{server.name}" in {file}')
|
|
163
194
|
|
|
164
195
|
# Run the server
|
fastmcp/client/auth/oauth.py
CHANGED
|
@@ -68,9 +68,6 @@ class ServerOAuthMetadata(_MCPServerOAuthMetadata):
|
|
|
68
68
|
class OAuthClientProvider(_MCPOAuthClientProvider):
|
|
69
69
|
"""
|
|
70
70
|
OAuth client provider with more flexible OAuth metadata discovery.
|
|
71
|
-
|
|
72
|
-
This subclass handles real-world OAuth servers that may not conform
|
|
73
|
-
strictly to the MCP OAuth specification but are still valid OAuth 2.0 servers.
|
|
74
71
|
"""
|
|
75
72
|
|
|
76
73
|
async def _discover_oauth_metadata(
|
fastmcp/client/transports.py
CHANGED
|
@@ -27,10 +27,6 @@ from mcp.client.session import (
|
|
|
27
27
|
MessageHandlerFnT,
|
|
28
28
|
SamplingFnT,
|
|
29
29
|
)
|
|
30
|
-
from mcp.client.sse import sse_client
|
|
31
|
-
from mcp.client.stdio import stdio_client
|
|
32
|
-
from mcp.client.streamable_http import streamablehttp_client
|
|
33
|
-
from mcp.client.websocket import websocket_client
|
|
34
30
|
from mcp.server.fastmcp import FastMCP as FastMCP1Server
|
|
35
31
|
from mcp.shared.memory import create_connected_server_and_client_session
|
|
36
32
|
from pydantic import AnyUrl
|
|
@@ -141,6 +137,13 @@ class WSTransport(ClientTransport):
|
|
|
141
137
|
async def connect_session(
|
|
142
138
|
self, **session_kwargs: Unpack[SessionKwargs]
|
|
143
139
|
) -> AsyncIterator[ClientSession]:
|
|
140
|
+
try:
|
|
141
|
+
from mcp.client.websocket import websocket_client
|
|
142
|
+
except ImportError:
|
|
143
|
+
raise ImportError(
|
|
144
|
+
"The websocket transport is not available. Please install fastmcp[websockets] or install the websockets package manually."
|
|
145
|
+
)
|
|
146
|
+
|
|
144
147
|
async with websocket_client(self.url) as transport:
|
|
145
148
|
read_stream, write_stream = transport
|
|
146
149
|
async with ClientSession(
|
|
@@ -188,6 +191,8 @@ class SSETransport(ClientTransport):
|
|
|
188
191
|
async def connect_session(
|
|
189
192
|
self, **session_kwargs: Unpack[SessionKwargs]
|
|
190
193
|
) -> AsyncIterator[ClientSession]:
|
|
194
|
+
from mcp.client.sse import sse_client
|
|
195
|
+
|
|
191
196
|
client_kwargs: dict[str, Any] = {}
|
|
192
197
|
|
|
193
198
|
# load headers from an active HTTP request, if available. This will only be true
|
|
@@ -255,6 +260,8 @@ class StreamableHttpTransport(ClientTransport):
|
|
|
255
260
|
async def connect_session(
|
|
256
261
|
self, **session_kwargs: Unpack[SessionKwargs]
|
|
257
262
|
) -> AsyncIterator[ClientSession]:
|
|
263
|
+
from mcp.client.streamable_http import streamablehttp_client
|
|
264
|
+
|
|
258
265
|
client_kwargs: dict[str, Any] = {}
|
|
259
266
|
|
|
260
267
|
# load headers from an active HTTP request, if available. This will only be true
|
|
@@ -350,6 +357,8 @@ class StdioTransport(ClientTransport):
|
|
|
350
357
|
return
|
|
351
358
|
|
|
352
359
|
async def _connect_task():
|
|
360
|
+
from mcp.client.stdio import stdio_client
|
|
361
|
+
|
|
353
362
|
async with contextlib.AsyncExitStack() as stack:
|
|
354
363
|
try:
|
|
355
364
|
server_params = StdioServerParameters(
|
|
@@ -3,6 +3,10 @@
|
|
|
3
3
|
from collections.abc import Callable
|
|
4
4
|
from typing import TYPE_CHECKING, Any
|
|
5
5
|
|
|
6
|
+
from fastmcp.prompts.prompt import Prompt
|
|
7
|
+
from fastmcp.resources.resource import Resource
|
|
8
|
+
from fastmcp.tools.tool import Tool
|
|
9
|
+
|
|
6
10
|
if TYPE_CHECKING:
|
|
7
11
|
from fastmcp.server import FastMCP
|
|
8
12
|
|
|
@@ -128,7 +132,8 @@ class MCPMixin:
|
|
|
128
132
|
registration_info["name"] = (
|
|
129
133
|
f"{prefix}{separator}{registration_info['name']}"
|
|
130
134
|
)
|
|
131
|
-
|
|
135
|
+
tool = Tool.from_function(fn=method, **registration_info)
|
|
136
|
+
mcp_server.add_tool(tool)
|
|
132
137
|
|
|
133
138
|
def register_resources(
|
|
134
139
|
self,
|
|
@@ -156,7 +161,8 @@ class MCPMixin:
|
|
|
156
161
|
registration_info["uri"] = (
|
|
157
162
|
f"{prefix}{separator}{registration_info['uri']}"
|
|
158
163
|
)
|
|
159
|
-
|
|
164
|
+
resource = Resource.from_function(fn=method, **registration_info)
|
|
165
|
+
mcp_server.add_resource(resource)
|
|
160
166
|
|
|
161
167
|
def register_prompts(
|
|
162
168
|
self,
|
|
@@ -180,7 +186,8 @@ class MCPMixin:
|
|
|
180
186
|
registration_info["name"] = (
|
|
181
187
|
f"{prefix}{separator}{registration_info['name']}"
|
|
182
188
|
)
|
|
183
|
-
|
|
189
|
+
prompt = Prompt.from_function(fn=method, **registration_info)
|
|
190
|
+
mcp_server.add_prompt(prompt)
|
|
184
191
|
|
|
185
192
|
def register_all(
|
|
186
193
|
self,
|
fastmcp/prompts/prompt.py
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
from __future__ import annotations as _annotations
|
|
4
4
|
|
|
5
5
|
import inspect
|
|
6
|
+
from abc import ABC, abstractmethod
|
|
6
7
|
from collections.abc import Awaitable, Callable, Sequence
|
|
7
8
|
from typing import TYPE_CHECKING, Annotated, Any
|
|
8
9
|
|
|
@@ -10,13 +11,14 @@ import pydantic_core
|
|
|
10
11
|
from mcp.types import EmbeddedResource, ImageContent, PromptMessage, Role, TextContent
|
|
11
12
|
from mcp.types import Prompt as MCPPrompt
|
|
12
13
|
from mcp.types import PromptArgument as MCPPromptArgument
|
|
13
|
-
from pydantic import
|
|
14
|
+
from pydantic import BeforeValidator, Field, TypeAdapter, validate_call
|
|
14
15
|
|
|
15
16
|
from fastmcp.exceptions import PromptError
|
|
16
17
|
from fastmcp.server.dependencies import get_context
|
|
17
18
|
from fastmcp.utilities.json_schema import compress_schema
|
|
18
19
|
from fastmcp.utilities.logging import get_logger
|
|
19
20
|
from fastmcp.utilities.types import (
|
|
21
|
+
FastMCPBaseModel,
|
|
20
22
|
_convert_set_defaults,
|
|
21
23
|
find_kwarg_by_type,
|
|
22
24
|
get_cached_typeadapter,
|
|
@@ -52,7 +54,7 @@ SyncPromptResult = (
|
|
|
52
54
|
PromptResult = SyncPromptResult | Awaitable[SyncPromptResult]
|
|
53
55
|
|
|
54
56
|
|
|
55
|
-
class PromptArgument(
|
|
57
|
+
class PromptArgument(FastMCPBaseModel):
|
|
56
58
|
"""An argument that can be passed to a prompt."""
|
|
57
59
|
|
|
58
60
|
name: str = Field(description="Name of the argument")
|
|
@@ -64,7 +66,7 @@ class PromptArgument(BaseModel):
|
|
|
64
66
|
)
|
|
65
67
|
|
|
66
68
|
|
|
67
|
-
class Prompt(
|
|
69
|
+
class Prompt(FastMCPBaseModel, ABC):
|
|
68
70
|
"""A prompt template that can be rendered with parameters."""
|
|
69
71
|
|
|
70
72
|
name: str = Field(description="Name of the prompt")
|
|
@@ -77,6 +79,61 @@ class Prompt(BaseModel):
|
|
|
77
79
|
arguments: list[PromptArgument] | None = Field(
|
|
78
80
|
None, description="Arguments that can be passed to the prompt"
|
|
79
81
|
)
|
|
82
|
+
|
|
83
|
+
def __eq__(self, other: object) -> bool:
|
|
84
|
+
if type(self) is not type(other):
|
|
85
|
+
return False
|
|
86
|
+
assert isinstance(other, type(self))
|
|
87
|
+
return self.model_dump() == other.model_dump()
|
|
88
|
+
|
|
89
|
+
def to_mcp_prompt(self, **overrides: Any) -> MCPPrompt:
|
|
90
|
+
"""Convert the prompt to an MCP prompt."""
|
|
91
|
+
arguments = [
|
|
92
|
+
MCPPromptArgument(
|
|
93
|
+
name=arg.name,
|
|
94
|
+
description=arg.description,
|
|
95
|
+
required=arg.required,
|
|
96
|
+
)
|
|
97
|
+
for arg in self.arguments or []
|
|
98
|
+
]
|
|
99
|
+
kwargs = {
|
|
100
|
+
"name": self.name,
|
|
101
|
+
"description": self.description,
|
|
102
|
+
"arguments": arguments,
|
|
103
|
+
}
|
|
104
|
+
return MCPPrompt(**kwargs | overrides)
|
|
105
|
+
|
|
106
|
+
@staticmethod
|
|
107
|
+
def from_function(
|
|
108
|
+
fn: Callable[..., PromptResult | Awaitable[PromptResult]],
|
|
109
|
+
name: str | None = None,
|
|
110
|
+
description: str | None = None,
|
|
111
|
+
tags: set[str] | None = None,
|
|
112
|
+
) -> FunctionPrompt:
|
|
113
|
+
"""Create a Prompt from a function.
|
|
114
|
+
|
|
115
|
+
The function can return:
|
|
116
|
+
- A string (converted to a message)
|
|
117
|
+
- A Message object
|
|
118
|
+
- A dict (converted to a message)
|
|
119
|
+
- A sequence of any of the above
|
|
120
|
+
"""
|
|
121
|
+
return FunctionPrompt.from_function(
|
|
122
|
+
fn=fn, name=name, description=description, tags=tags
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
@abstractmethod
|
|
126
|
+
async def render(
|
|
127
|
+
self,
|
|
128
|
+
arguments: dict[str, Any] | None = None,
|
|
129
|
+
) -> list[PromptMessage]:
|
|
130
|
+
"""Render the prompt with arguments."""
|
|
131
|
+
raise NotImplementedError("Prompt.render() must be implemented by subclasses")
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
class FunctionPrompt(Prompt):
|
|
135
|
+
"""A prompt that is a function."""
|
|
136
|
+
|
|
80
137
|
fn: Callable[..., PromptResult | Awaitable[PromptResult]]
|
|
81
138
|
|
|
82
139
|
@classmethod
|
|
@@ -86,7 +143,7 @@ class Prompt(BaseModel):
|
|
|
86
143
|
name: str | None = None,
|
|
87
144
|
description: str | None = None,
|
|
88
145
|
tags: set[str] | None = None,
|
|
89
|
-
) ->
|
|
146
|
+
) -> FunctionPrompt:
|
|
90
147
|
"""Create a Prompt from a function.
|
|
91
148
|
|
|
92
149
|
The function can return:
|
|
@@ -114,6 +171,9 @@ class Prompt(BaseModel):
|
|
|
114
171
|
# if the fn is a callable class, we need to get the __call__ method from here out
|
|
115
172
|
if not inspect.isroutine(fn):
|
|
116
173
|
fn = fn.__call__
|
|
174
|
+
# if the fn is a staticmethod, we need to work with the underlying function
|
|
175
|
+
if isinstance(fn, staticmethod):
|
|
176
|
+
fn = fn.__func__
|
|
117
177
|
|
|
118
178
|
type_adapter = get_cached_typeadapter(fn)
|
|
119
179
|
parameters = type_adapter.json_schema()
|
|
@@ -147,8 +207,8 @@ class Prompt(BaseModel):
|
|
|
147
207
|
name=func_name,
|
|
148
208
|
description=description,
|
|
149
209
|
arguments=arguments,
|
|
150
|
-
fn=fn,
|
|
151
210
|
tags=tags or set(),
|
|
211
|
+
fn=fn,
|
|
152
212
|
)
|
|
153
213
|
|
|
154
214
|
async def render(
|
|
@@ -212,25 +272,3 @@ class Prompt(BaseModel):
|
|
|
212
272
|
except Exception as e:
|
|
213
273
|
logger.exception(f"Error rendering prompt {self.name}: {e}")
|
|
214
274
|
raise PromptError(f"Error rendering prompt {self.name}.")
|
|
215
|
-
|
|
216
|
-
def __eq__(self, other: object) -> bool:
|
|
217
|
-
if not isinstance(other, Prompt):
|
|
218
|
-
return False
|
|
219
|
-
return self.model_dump() == other.model_dump()
|
|
220
|
-
|
|
221
|
-
def to_mcp_prompt(self, **overrides: Any) -> MCPPrompt:
|
|
222
|
-
"""Convert the prompt to an MCP prompt."""
|
|
223
|
-
arguments = [
|
|
224
|
-
MCPPromptArgument(
|
|
225
|
-
name=arg.name,
|
|
226
|
-
description=arg.description,
|
|
227
|
-
required=arg.required,
|
|
228
|
-
)
|
|
229
|
-
for arg in self.arguments or []
|
|
230
|
-
]
|
|
231
|
-
kwargs = {
|
|
232
|
-
"name": self.name,
|
|
233
|
-
"description": self.description,
|
|
234
|
-
"arguments": arguments,
|
|
235
|
-
}
|
|
236
|
-
return MCPPrompt(**kwargs | overrides)
|
|
@@ -1,14 +1,13 @@
|
|
|
1
|
-
"""Prompt management functionality."""
|
|
2
|
-
|
|
3
1
|
from __future__ import annotations as _annotations
|
|
4
2
|
|
|
3
|
+
import warnings
|
|
5
4
|
from collections.abc import Awaitable, Callable
|
|
6
5
|
from typing import TYPE_CHECKING, Any
|
|
7
6
|
|
|
8
7
|
from mcp import GetPromptResult
|
|
9
8
|
|
|
10
9
|
from fastmcp.exceptions import NotFoundError, PromptError
|
|
11
|
-
from fastmcp.prompts.prompt import Prompt, PromptResult
|
|
10
|
+
from fastmcp.prompts.prompt import FunctionPrompt, Prompt, PromptResult
|
|
12
11
|
from fastmcp.settings import DuplicateBehavior
|
|
13
12
|
from fastmcp.utilities.logging import get_logger
|
|
14
13
|
|
|
@@ -55,10 +54,18 @@ class PromptManager:
|
|
|
55
54
|
name: str | None = None,
|
|
56
55
|
description: str | None = None,
|
|
57
56
|
tags: set[str] | None = None,
|
|
58
|
-
) ->
|
|
57
|
+
) -> FunctionPrompt:
|
|
59
58
|
"""Create a prompt from a function."""
|
|
60
|
-
|
|
61
|
-
|
|
59
|
+
# deprecated in 2.7.0
|
|
60
|
+
warnings.warn(
|
|
61
|
+
"PromptManager.add_prompt_from_fn() is deprecated. Use Prompt.from_function() and call add_prompt() instead.",
|
|
62
|
+
DeprecationWarning,
|
|
63
|
+
stacklevel=2,
|
|
64
|
+
)
|
|
65
|
+
prompt = FunctionPrompt.from_function(
|
|
66
|
+
fn, name=name, description=description, tags=tags
|
|
67
|
+
)
|
|
68
|
+
return self.add_prompt(prompt) # type: ignore
|
|
62
69
|
|
|
63
70
|
def add_prompt(self, prompt: Prompt, key: str | None = None) -> Prompt:
|
|
64
71
|
"""Add a prompt to the manager."""
|
fastmcp/py.typed
ADDED
|
File without changes
|
fastmcp/resources/__init__.py
CHANGED
fastmcp/resources/resource.py
CHANGED
|
@@ -3,12 +3,14 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import abc
|
|
6
|
+
import inspect
|
|
7
|
+
from collections.abc import Callable
|
|
6
8
|
from typing import TYPE_CHECKING, Annotated, Any
|
|
7
9
|
|
|
10
|
+
import pydantic_core
|
|
8
11
|
from mcp.types import Resource as MCPResource
|
|
9
12
|
from pydantic import (
|
|
10
13
|
AnyUrl,
|
|
11
|
-
BaseModel,
|
|
12
14
|
BeforeValidator,
|
|
13
15
|
ConfigDict,
|
|
14
16
|
Field,
|
|
@@ -17,13 +19,18 @@ from pydantic import (
|
|
|
17
19
|
field_validator,
|
|
18
20
|
)
|
|
19
21
|
|
|
20
|
-
from fastmcp.
|
|
22
|
+
from fastmcp.server.dependencies import get_context
|
|
23
|
+
from fastmcp.utilities.types import (
|
|
24
|
+
FastMCPBaseModel,
|
|
25
|
+
_convert_set_defaults,
|
|
26
|
+
find_kwarg_by_type,
|
|
27
|
+
)
|
|
21
28
|
|
|
22
29
|
if TYPE_CHECKING:
|
|
23
30
|
pass
|
|
24
31
|
|
|
25
32
|
|
|
26
|
-
class Resource(
|
|
33
|
+
class Resource(FastMCPBaseModel, abc.ABC):
|
|
27
34
|
"""Base class for all resources."""
|
|
28
35
|
|
|
29
36
|
model_config = ConfigDict(validate_default=True)
|
|
@@ -44,6 +51,24 @@ class Resource(BaseModel, abc.ABC):
|
|
|
44
51
|
pattern=r"^[a-zA-Z0-9]+/[a-zA-Z0-9\-+.]+$",
|
|
45
52
|
)
|
|
46
53
|
|
|
54
|
+
@staticmethod
|
|
55
|
+
def from_function(
|
|
56
|
+
fn: Callable[[], Any],
|
|
57
|
+
uri: str | AnyUrl,
|
|
58
|
+
name: str | None = None,
|
|
59
|
+
description: str | None = None,
|
|
60
|
+
mime_type: str | None = None,
|
|
61
|
+
tags: set[str] | None = None,
|
|
62
|
+
) -> FunctionResource:
|
|
63
|
+
return FunctionResource.from_function(
|
|
64
|
+
fn=fn,
|
|
65
|
+
uri=uri,
|
|
66
|
+
name=name,
|
|
67
|
+
description=description,
|
|
68
|
+
mime_type=mime_type,
|
|
69
|
+
tags=tags,
|
|
70
|
+
)
|
|
71
|
+
|
|
47
72
|
@field_validator("mime_type", mode="before")
|
|
48
73
|
@classmethod
|
|
49
74
|
def set_default_mime_type(cls, mime_type: str | None) -> str:
|
|
@@ -68,8 +93,9 @@ class Resource(BaseModel, abc.ABC):
|
|
|
68
93
|
pass
|
|
69
94
|
|
|
70
95
|
def __eq__(self, other: object) -> bool:
|
|
71
|
-
if not
|
|
96
|
+
if type(self) is not type(other):
|
|
72
97
|
return False
|
|
98
|
+
assert isinstance(other, type(self))
|
|
73
99
|
return self.model_dump() == other.model_dump()
|
|
74
100
|
|
|
75
101
|
def to_mcp_resource(self, **overrides: Any) -> MCPResource:
|
|
@@ -81,3 +107,63 @@ class Resource(BaseModel, abc.ABC):
|
|
|
81
107
|
"mimeType": self.mime_type,
|
|
82
108
|
}
|
|
83
109
|
return MCPResource(**kwargs | overrides)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
class FunctionResource(Resource):
|
|
113
|
+
"""A resource that defers data loading by wrapping a function.
|
|
114
|
+
|
|
115
|
+
The function is only called when the resource is read, allowing for lazy loading
|
|
116
|
+
of potentially expensive data. This is particularly useful when listing resources,
|
|
117
|
+
as the function won't be called until the resource is actually accessed.
|
|
118
|
+
|
|
119
|
+
The function can return:
|
|
120
|
+
- str for text content (default)
|
|
121
|
+
- bytes for binary content
|
|
122
|
+
- other types will be converted to JSON
|
|
123
|
+
"""
|
|
124
|
+
|
|
125
|
+
fn: Callable[[], Any]
|
|
126
|
+
|
|
127
|
+
@classmethod
|
|
128
|
+
def from_function(
|
|
129
|
+
cls,
|
|
130
|
+
fn: Callable[[], Any],
|
|
131
|
+
uri: str | AnyUrl,
|
|
132
|
+
name: str | None = None,
|
|
133
|
+
description: str | None = None,
|
|
134
|
+
mime_type: str | None = None,
|
|
135
|
+
tags: set[str] | None = None,
|
|
136
|
+
) -> FunctionResource:
|
|
137
|
+
"""Create a FunctionResource from a function."""
|
|
138
|
+
if isinstance(uri, str):
|
|
139
|
+
uri = AnyUrl(uri)
|
|
140
|
+
return cls(
|
|
141
|
+
fn=fn,
|
|
142
|
+
uri=uri,
|
|
143
|
+
name=name or fn.__name__,
|
|
144
|
+
description=description or fn.__doc__,
|
|
145
|
+
mime_type=mime_type or "text/plain",
|
|
146
|
+
tags=tags or set(),
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
async def read(self) -> str | bytes:
|
|
150
|
+
"""Read the resource by calling the wrapped function."""
|
|
151
|
+
from fastmcp.server.context import Context
|
|
152
|
+
|
|
153
|
+
kwargs = {}
|
|
154
|
+
context_kwarg = find_kwarg_by_type(self.fn, kwarg_type=Context)
|
|
155
|
+
if context_kwarg is not None:
|
|
156
|
+
kwargs[context_kwarg] = get_context()
|
|
157
|
+
|
|
158
|
+
result = self.fn(**kwargs)
|
|
159
|
+
if inspect.iscoroutinefunction(self.fn):
|
|
160
|
+
result = await result
|
|
161
|
+
|
|
162
|
+
if isinstance(result, Resource):
|
|
163
|
+
return await result.read()
|
|
164
|
+
elif isinstance(result, bytes):
|
|
165
|
+
return result
|
|
166
|
+
elif isinstance(result, str):
|
|
167
|
+
return result
|
|
168
|
+
else:
|
|
169
|
+
return pydantic_core.to_json(result, fallback=str, indent=2).decode()
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
"""Resource manager functionality."""
|
|
2
2
|
|
|
3
3
|
import inspect
|
|
4
|
+
import warnings
|
|
4
5
|
from collections.abc import Callable
|
|
5
6
|
from typing import Any
|
|
6
7
|
|
|
7
8
|
from pydantic import AnyUrl
|
|
8
9
|
|
|
9
10
|
from fastmcp.exceptions import NotFoundError, ResourceError
|
|
10
|
-
from fastmcp.resources import FunctionResource
|
|
11
11
|
from fastmcp.resources.resource import Resource
|
|
12
12
|
from fastmcp.resources.template import (
|
|
13
13
|
ResourceTemplate,
|
|
@@ -121,13 +121,19 @@ class ResourceManager:
|
|
|
121
121
|
The added resource. If a resource with the same URI already exists,
|
|
122
122
|
returns the existing resource.
|
|
123
123
|
"""
|
|
124
|
-
|
|
124
|
+
# deprecated in 2.7.0
|
|
125
|
+
warnings.warn(
|
|
126
|
+
"add_resource_from_fn is deprecated. Use Resource.from_function() and call add_resource() instead.",
|
|
127
|
+
DeprecationWarning,
|
|
128
|
+
stacklevel=2,
|
|
129
|
+
)
|
|
130
|
+
resource = Resource.from_function(
|
|
125
131
|
fn=fn,
|
|
126
|
-
uri=
|
|
132
|
+
uri=uri,
|
|
127
133
|
name=name,
|
|
128
134
|
description=description,
|
|
129
|
-
mime_type=mime_type
|
|
130
|
-
tags=tags
|
|
135
|
+
mime_type=mime_type,
|
|
136
|
+
tags=tags,
|
|
131
137
|
)
|
|
132
138
|
return self.add_resource(resource)
|
|
133
139
|
|
|
@@ -172,7 +178,12 @@ class ResourceManager:
|
|
|
172
178
|
tags: set[str] | None = None,
|
|
173
179
|
) -> ResourceTemplate:
|
|
174
180
|
"""Create a template from a function."""
|
|
175
|
-
|
|
181
|
+
# deprecated in 2.7.0
|
|
182
|
+
warnings.warn(
|
|
183
|
+
"add_template_from_fn is deprecated. Use ResourceTemplate.from_function() and call add_template() instead.",
|
|
184
|
+
DeprecationWarning,
|
|
185
|
+
stacklevel=2,
|
|
186
|
+
)
|
|
176
187
|
template = ResourceTemplate.from_function(
|
|
177
188
|
fn,
|
|
178
189
|
uri_template=uri_template,
|