fastmcp 2.6.1__py3-none-any.whl → 2.7.1__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 +10 -2
- fastmcp/cli/run.py +32 -1
- fastmcp/client/transports.py +21 -13
- fastmcp/contrib/bulk_tool_caller/example.py +1 -1
- fastmcp/contrib/mcp_mixin/mcp_mixin.py +10 -3
- fastmcp/prompts/prompt.py +68 -30
- fastmcp/prompts/prompt_manager.py +13 -6
- fastmcp/resources/__init__.py +1 -2
- fastmcp/resources/resource.py +92 -6
- fastmcp/resources/resource_manager.py +17 -6
- fastmcp/resources/template.py +90 -56
- fastmcp/resources/types.py +0 -44
- fastmcp/server/context.py +1 -1
- fastmcp/server/dependencies.py +1 -0
- fastmcp/server/http.py +2 -1
- fastmcp/server/openapi.py +17 -32
- fastmcp/server/proxy.py +5 -8
- fastmcp/server/server.py +280 -95
- fastmcp/settings.py +1 -1
- fastmcp/tools/__init__.py +2 -2
- fastmcp/tools/tool.py +62 -22
- fastmcp/tools/tool_manager.py +9 -3
- fastmcp/utilities/mcp_config.py +17 -7
- fastmcp/utilities/openapi.py +56 -32
- fastmcp/utilities/types.py +7 -1
- {fastmcp-2.6.1.dist-info → fastmcp-2.7.1.dist-info}/METADATA +10 -9
- {fastmcp-2.6.1.dist-info → fastmcp-2.7.1.dist-info}/RECORD +30 -31
- fastmcp/utilities/decorators.py +0 -101
- {fastmcp-2.6.1.dist-info → fastmcp-2.7.1.dist-info}/WHEEL +0 -0
- {fastmcp-2.6.1.dist-info → fastmcp-2.7.1.dist-info}/entry_points.txt +0 -0
- {fastmcp-2.6.1.dist-info → fastmcp-2.7.1.dist-info}/licenses/LICENSE +0 -0
fastmcp/cli/cli.py
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"""
|
|
1
|
+
"""FastMCP CLI tools."""
|
|
2
2
|
|
|
3
3
|
import importlib.metadata
|
|
4
4
|
import importlib.util
|
|
@@ -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/transports.py
CHANGED
|
@@ -27,15 +27,12 @@ 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
|
|
37
33
|
from typing_extensions import Unpack
|
|
38
34
|
|
|
35
|
+
from fastmcp.client.auth.bearer import BearerAuth
|
|
39
36
|
from fastmcp.client.auth.oauth import OAuth
|
|
40
37
|
from fastmcp.server.dependencies import get_http_headers
|
|
41
38
|
from fastmcp.server.server import FastMCP
|
|
@@ -141,6 +138,13 @@ class WSTransport(ClientTransport):
|
|
|
141
138
|
async def connect_session(
|
|
142
139
|
self, **session_kwargs: Unpack[SessionKwargs]
|
|
143
140
|
) -> AsyncIterator[ClientSession]:
|
|
141
|
+
try:
|
|
142
|
+
from mcp.client.websocket import websocket_client
|
|
143
|
+
except ImportError:
|
|
144
|
+
raise ImportError(
|
|
145
|
+
"The websocket transport is not available. Please install fastmcp[websockets] or install the websockets package manually."
|
|
146
|
+
)
|
|
147
|
+
|
|
144
148
|
async with websocket_client(self.url) as transport:
|
|
145
149
|
read_stream, write_stream = transport
|
|
146
150
|
async with ClientSession(
|
|
@@ -149,7 +153,7 @@ class WSTransport(ClientTransport):
|
|
|
149
153
|
yield session
|
|
150
154
|
|
|
151
155
|
def __repr__(self) -> str:
|
|
152
|
-
return f"<
|
|
156
|
+
return f"<WebSocketTransport(url='{self.url}')>"
|
|
153
157
|
|
|
154
158
|
|
|
155
159
|
class SSETransport(ClientTransport):
|
|
@@ -180,14 +184,15 @@ class SSETransport(ClientTransport):
|
|
|
180
184
|
if auth == "oauth":
|
|
181
185
|
auth = OAuth(self.url)
|
|
182
186
|
elif isinstance(auth, str):
|
|
183
|
-
|
|
184
|
-
auth = None
|
|
187
|
+
auth = BearerAuth(auth)
|
|
185
188
|
self.auth = auth
|
|
186
189
|
|
|
187
190
|
@contextlib.asynccontextmanager
|
|
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
|
|
@@ -216,7 +221,7 @@ class SSETransport(ClientTransport):
|
|
|
216
221
|
yield session
|
|
217
222
|
|
|
218
223
|
def __repr__(self) -> str:
|
|
219
|
-
return f"<
|
|
224
|
+
return f"<SSETransport(url='{self.url}')>"
|
|
220
225
|
|
|
221
226
|
|
|
222
227
|
class StreamableHttpTransport(ClientTransport):
|
|
@@ -247,14 +252,15 @@ class StreamableHttpTransport(ClientTransport):
|
|
|
247
252
|
if auth == "oauth":
|
|
248
253
|
auth = OAuth(self.url)
|
|
249
254
|
elif isinstance(auth, str):
|
|
250
|
-
|
|
251
|
-
auth = None
|
|
255
|
+
auth = BearerAuth(auth)
|
|
252
256
|
self.auth = auth
|
|
253
257
|
|
|
254
258
|
@contextlib.asynccontextmanager
|
|
255
259
|
async def connect_session(
|
|
256
260
|
self, **session_kwargs: Unpack[SessionKwargs]
|
|
257
261
|
) -> AsyncIterator[ClientSession]:
|
|
262
|
+
from mcp.client.streamable_http import streamablehttp_client
|
|
263
|
+
|
|
258
264
|
client_kwargs: dict[str, Any] = {}
|
|
259
265
|
|
|
260
266
|
# load headers from an active HTTP request, if available. This will only be true
|
|
@@ -284,7 +290,7 @@ class StreamableHttpTransport(ClientTransport):
|
|
|
284
290
|
yield session
|
|
285
291
|
|
|
286
292
|
def __repr__(self) -> str:
|
|
287
|
-
return f"<
|
|
293
|
+
return f"<StreamableHttpTransport(url='{self.url}')>"
|
|
288
294
|
|
|
289
295
|
|
|
290
296
|
class StdioTransport(ClientTransport):
|
|
@@ -350,6 +356,8 @@ class StdioTransport(ClientTransport):
|
|
|
350
356
|
return
|
|
351
357
|
|
|
352
358
|
async def _connect_task():
|
|
359
|
+
from mcp.client.stdio import stdio_client
|
|
360
|
+
|
|
353
361
|
async with contextlib.AsyncExitStack() as stack:
|
|
354
362
|
try:
|
|
355
363
|
server_params = StdioServerParameters(
|
|
@@ -674,7 +682,7 @@ class FastMCPTransport(ClientTransport):
|
|
|
674
682
|
yield session
|
|
675
683
|
|
|
676
684
|
def __repr__(self) -> str:
|
|
677
|
-
return f"<
|
|
685
|
+
return f"<FastMCPTransport(server='{self.server.name}')>"
|
|
678
686
|
|
|
679
687
|
|
|
680
688
|
class MCPConfigTransport(ClientTransport):
|
|
@@ -760,7 +768,7 @@ class MCPConfigTransport(ClientTransport):
|
|
|
760
768
|
yield session
|
|
761
769
|
|
|
762
770
|
def __repr__(self) -> str:
|
|
763
|
-
return f"<
|
|
771
|
+
return f"<MCPConfigTransport(config='{self.config}')>"
|
|
764
772
|
|
|
765
773
|
|
|
766
774
|
@overload
|
|
@@ -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,31 +54,86 @@ 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")
|
|
59
61
|
description: str | None = Field(
|
|
60
|
-
None, description="Description of what the argument does"
|
|
62
|
+
default=None, description="Description of what the argument does"
|
|
61
63
|
)
|
|
62
64
|
required: bool = Field(
|
|
63
65
|
default=False, description="Whether the argument is required"
|
|
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")
|
|
71
73
|
description: str | None = Field(
|
|
72
|
-
None, description="Description of what the prompt does"
|
|
74
|
+
default=None, description="Description of what the prompt does"
|
|
73
75
|
)
|
|
74
76
|
tags: Annotated[set[str], BeforeValidator(_convert_set_defaults)] = Field(
|
|
75
77
|
default_factory=set, description="Tags for the prompt"
|
|
76
78
|
)
|
|
77
79
|
arguments: list[PromptArgument] | None = Field(
|
|
78
|
-
None, description="Arguments that can be passed to the prompt"
|
|
80
|
+
default=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/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)
|
|
@@ -31,9 +38,9 @@ class Resource(BaseModel, abc.ABC):
|
|
|
31
38
|
uri: Annotated[AnyUrl, UrlConstraints(host_required=False)] = Field(
|
|
32
39
|
default=..., description="URI of the resource"
|
|
33
40
|
)
|
|
34
|
-
name: str | None = Field(description="Name of the resource"
|
|
41
|
+
name: str | None = Field(default=None, description="Name of the resource")
|
|
35
42
|
description: str | None = Field(
|
|
36
|
-
description="Description of the resource"
|
|
43
|
+
default=None, description="Description of the resource"
|
|
37
44
|
)
|
|
38
45
|
tags: Annotated[set[str], BeforeValidator(_convert_set_defaults)] = Field(
|
|
39
46
|
default_factory=set, description="Tags for the resource"
|
|
@@ -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()
|