fastmcp 0.4.1__py3-none-any.whl → 2.0.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 +15 -4
- fastmcp/cli/__init__.py +0 -1
- fastmcp/cli/claude.py +13 -11
- fastmcp/cli/cli.py +61 -41
- fastmcp/client/__init__.py +25 -0
- fastmcp/client/base.py +1 -0
- fastmcp/client/client.py +181 -0
- fastmcp/client/roots.py +75 -0
- fastmcp/client/sampling.py +50 -0
- fastmcp/client/transports.py +411 -0
- fastmcp/prompts/__init__.py +1 -1
- fastmcp/prompts/base.py +27 -26
- fastmcp/prompts/prompt_manager.py +50 -12
- fastmcp/resources/__init__.py +5 -5
- fastmcp/resources/base.py +2 -2
- fastmcp/resources/resource_manager.py +66 -9
- fastmcp/resources/templates.py +15 -10
- fastmcp/resources/types.py +16 -11
- fastmcp/server/__init__.py +5 -0
- fastmcp/server/context.py +222 -0
- fastmcp/server/openapi.py +625 -0
- fastmcp/server/proxy.py +219 -0
- fastmcp/{server.py → server/server.py} +251 -265
- fastmcp/settings.py +73 -0
- fastmcp/tools/base.py +28 -18
- fastmcp/tools/tool_manager.py +45 -10
- fastmcp/utilities/func_metadata.py +33 -19
- fastmcp/utilities/openapi.py +797 -0
- fastmcp/utilities/types.py +3 -4
- fastmcp-2.0.0.dist-info/METADATA +770 -0
- fastmcp-2.0.0.dist-info/RECORD +39 -0
- {fastmcp-0.4.1.dist-info → fastmcp-2.0.0.dist-info}/WHEEL +1 -1
- fastmcp-2.0.0.dist-info/licenses/LICENSE +201 -0
- fastmcp/prompts/manager.py +0 -50
- fastmcp-0.4.1.dist-info/METADATA +0 -587
- fastmcp-0.4.1.dist-info/RECORD +0 -28
- fastmcp-0.4.1.dist-info/licenses/LICENSE +0 -21
- {fastmcp-0.4.1.dist-info → fastmcp-2.0.0.dist-info}/entry_points.txt +0 -0
|
@@ -0,0 +1,411 @@
|
|
|
1
|
+
import abc
|
|
2
|
+
import contextlib
|
|
3
|
+
import datetime
|
|
4
|
+
import os
|
|
5
|
+
from collections.abc import AsyncIterator
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import (
|
|
8
|
+
TypedDict,
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
from mcp import ClientSession, StdioServerParameters
|
|
12
|
+
from mcp.client.session import (
|
|
13
|
+
ListRootsFnT,
|
|
14
|
+
LoggingFnT,
|
|
15
|
+
MessageHandlerFnT,
|
|
16
|
+
SamplingFnT,
|
|
17
|
+
)
|
|
18
|
+
from mcp.client.sse import sse_client
|
|
19
|
+
from mcp.client.stdio import stdio_client
|
|
20
|
+
from mcp.client.websocket import websocket_client
|
|
21
|
+
from mcp.shared.memory import create_connected_server_and_client_session
|
|
22
|
+
from pydantic import AnyUrl
|
|
23
|
+
from typing_extensions import Unpack
|
|
24
|
+
|
|
25
|
+
from fastmcp.server import FastMCP as FastMCPServer
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class SessionKwargs(TypedDict, total=False):
|
|
29
|
+
"""Keyword arguments for the MCP ClientSession constructor."""
|
|
30
|
+
|
|
31
|
+
sampling_callback: SamplingFnT | None
|
|
32
|
+
list_roots_callback: ListRootsFnT | None
|
|
33
|
+
logging_callback: LoggingFnT | None
|
|
34
|
+
message_handler: MessageHandlerFnT | None
|
|
35
|
+
read_timeout_seconds: datetime.timedelta | None
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class ClientTransport(abc.ABC):
|
|
39
|
+
"""
|
|
40
|
+
Abstract base class for different MCP client transport mechanisms.
|
|
41
|
+
|
|
42
|
+
A Transport is responsible for establishing and managing connections
|
|
43
|
+
to an MCP server, and providing a ClientSession within an async context.
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
@abc.abstractmethod
|
|
47
|
+
@contextlib.asynccontextmanager
|
|
48
|
+
async def connect_session(
|
|
49
|
+
self, **session_kwargs: Unpack[SessionKwargs]
|
|
50
|
+
) -> AsyncIterator[ClientSession]:
|
|
51
|
+
"""
|
|
52
|
+
Establishes a connection and yields an active, initialized ClientSession.
|
|
53
|
+
|
|
54
|
+
The session is guaranteed to be valid only within the scope of the
|
|
55
|
+
async context manager. Connection setup and teardown are handled
|
|
56
|
+
within this context.
|
|
57
|
+
|
|
58
|
+
Args:
|
|
59
|
+
**session_kwargs: Keyword arguments to pass to the ClientSession
|
|
60
|
+
constructor (e.g., callbacks, timeouts).
|
|
61
|
+
|
|
62
|
+
Yields:
|
|
63
|
+
An initialized mcp.ClientSession instance.
|
|
64
|
+
"""
|
|
65
|
+
raise NotImplementedError
|
|
66
|
+
yield None # type: ignore
|
|
67
|
+
|
|
68
|
+
def __repr__(self) -> str:
|
|
69
|
+
# Basic representation for subclasses
|
|
70
|
+
return f"<{self.__class__.__name__}>"
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class WSTransport(ClientTransport):
|
|
74
|
+
"""Transport implementation that connects to an MCP server via WebSockets."""
|
|
75
|
+
|
|
76
|
+
def __init__(self, url: str | AnyUrl):
|
|
77
|
+
if isinstance(url, AnyUrl):
|
|
78
|
+
url = str(url)
|
|
79
|
+
if not isinstance(url, str) or not url.startswith("ws"):
|
|
80
|
+
raise ValueError("Invalid WebSocket URL provided.")
|
|
81
|
+
self.url = url
|
|
82
|
+
|
|
83
|
+
@contextlib.asynccontextmanager
|
|
84
|
+
async def connect_session(
|
|
85
|
+
self, **session_kwargs: Unpack[SessionKwargs]
|
|
86
|
+
) -> AsyncIterator[ClientSession]:
|
|
87
|
+
async with websocket_client(self.url) as transport:
|
|
88
|
+
read_stream, write_stream = transport
|
|
89
|
+
async with ClientSession(
|
|
90
|
+
read_stream, write_stream, **session_kwargs
|
|
91
|
+
) as session:
|
|
92
|
+
await session.initialize() # Initialize after session creation
|
|
93
|
+
yield session
|
|
94
|
+
|
|
95
|
+
def __repr__(self) -> str:
|
|
96
|
+
return f"<WebSocket(url='{self.url}')>"
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
class SSETransport(ClientTransport):
|
|
100
|
+
"""Transport implementation that connects to an MCP server via Server-Sent Events."""
|
|
101
|
+
|
|
102
|
+
def __init__(self, url: str | AnyUrl, headers: dict[str, str] | None = None):
|
|
103
|
+
if isinstance(url, AnyUrl):
|
|
104
|
+
url = str(url)
|
|
105
|
+
if not isinstance(url, str) or not url.startswith("http"):
|
|
106
|
+
raise ValueError("Invalid HTTP/S URL provided for SSE.")
|
|
107
|
+
self.url = url
|
|
108
|
+
self.headers = headers or {}
|
|
109
|
+
|
|
110
|
+
@contextlib.asynccontextmanager
|
|
111
|
+
async def connect_session(
|
|
112
|
+
self, **session_kwargs: Unpack[SessionKwargs]
|
|
113
|
+
) -> AsyncIterator[ClientSession]:
|
|
114
|
+
async with sse_client(self.url, headers=self.headers) as transport:
|
|
115
|
+
read_stream, write_stream = transport
|
|
116
|
+
async with ClientSession(
|
|
117
|
+
read_stream, write_stream, **session_kwargs
|
|
118
|
+
) as session:
|
|
119
|
+
await session.initialize()
|
|
120
|
+
yield session
|
|
121
|
+
|
|
122
|
+
def __repr__(self) -> str:
|
|
123
|
+
return f"<SSE(url='{self.url}')>"
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
class StdioTransport(ClientTransport):
|
|
127
|
+
"""
|
|
128
|
+
Base transport for connecting to an MCP server via subprocess with stdio.
|
|
129
|
+
|
|
130
|
+
This is a base class that can be subclassed for specific command-based
|
|
131
|
+
transports like Python, Node, Uvx, etc.
|
|
132
|
+
"""
|
|
133
|
+
|
|
134
|
+
def __init__(
|
|
135
|
+
self,
|
|
136
|
+
command: str,
|
|
137
|
+
args: list[str],
|
|
138
|
+
env: dict[str, str] | None = None,
|
|
139
|
+
cwd: str | None = None,
|
|
140
|
+
):
|
|
141
|
+
"""
|
|
142
|
+
Initialize a Stdio transport.
|
|
143
|
+
|
|
144
|
+
Args:
|
|
145
|
+
command: The command to run (e.g., "python", "node", "uvx")
|
|
146
|
+
args: The arguments to pass to the command
|
|
147
|
+
env: Environment variables to set for the subprocess
|
|
148
|
+
cwd: Current working directory for the subprocess
|
|
149
|
+
"""
|
|
150
|
+
self.command = command
|
|
151
|
+
self.args = args
|
|
152
|
+
self.env = env
|
|
153
|
+
self.cwd = cwd
|
|
154
|
+
|
|
155
|
+
@contextlib.asynccontextmanager
|
|
156
|
+
async def connect_session(
|
|
157
|
+
self, **session_kwargs: Unpack[SessionKwargs]
|
|
158
|
+
) -> AsyncIterator[ClientSession]:
|
|
159
|
+
server_params = StdioServerParameters(
|
|
160
|
+
command=self.command, args=self.args, env=self.env, cwd=self.cwd
|
|
161
|
+
)
|
|
162
|
+
async with stdio_client(server_params) as transport:
|
|
163
|
+
read_stream, write_stream = transport
|
|
164
|
+
async with ClientSession(
|
|
165
|
+
read_stream, write_stream, **session_kwargs
|
|
166
|
+
) as session:
|
|
167
|
+
await session.initialize()
|
|
168
|
+
yield session
|
|
169
|
+
|
|
170
|
+
def __repr__(self) -> str:
|
|
171
|
+
return (
|
|
172
|
+
f"<{self.__class__.__name__}(command='{self.command}', args={self.args})>"
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
class PythonStdioTransport(StdioTransport):
|
|
177
|
+
"""Transport for running Python scripts."""
|
|
178
|
+
|
|
179
|
+
def __init__(
|
|
180
|
+
self,
|
|
181
|
+
script_path: str | Path,
|
|
182
|
+
args: list[str] | None = None,
|
|
183
|
+
env: dict[str, str] | None = None,
|
|
184
|
+
cwd: str | None = None,
|
|
185
|
+
python_cmd: str = "python",
|
|
186
|
+
):
|
|
187
|
+
"""
|
|
188
|
+
Initialize a Python transport.
|
|
189
|
+
|
|
190
|
+
Args:
|
|
191
|
+
script_path: Path to the Python script to run
|
|
192
|
+
args: Additional arguments to pass to the script
|
|
193
|
+
env: Environment variables to set for the subprocess
|
|
194
|
+
cwd: Current working directory for the subprocess
|
|
195
|
+
python_cmd: Python command to use (default: "python")
|
|
196
|
+
"""
|
|
197
|
+
script_path = Path(script_path).resolve()
|
|
198
|
+
if not script_path.is_file():
|
|
199
|
+
raise FileNotFoundError(f"Script not found: {script_path}")
|
|
200
|
+
if not str(script_path).endswith(".py"):
|
|
201
|
+
raise ValueError(f"Not a Python script: {script_path}")
|
|
202
|
+
|
|
203
|
+
full_args = [str(script_path)]
|
|
204
|
+
if args:
|
|
205
|
+
full_args.extend(args)
|
|
206
|
+
|
|
207
|
+
super().__init__(command=python_cmd, args=full_args, env=env, cwd=cwd)
|
|
208
|
+
self.script_path = script_path
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
class NodeStdioTransport(StdioTransport):
|
|
212
|
+
"""Transport for running Node.js scripts."""
|
|
213
|
+
|
|
214
|
+
def __init__(
|
|
215
|
+
self,
|
|
216
|
+
script_path: str | Path,
|
|
217
|
+
args: list[str] | None = None,
|
|
218
|
+
env: dict[str, str] | None = None,
|
|
219
|
+
cwd: str | None = None,
|
|
220
|
+
node_cmd: str = "node",
|
|
221
|
+
):
|
|
222
|
+
"""
|
|
223
|
+
Initialize a Node transport.
|
|
224
|
+
|
|
225
|
+
Args:
|
|
226
|
+
script_path: Path to the Node.js script to run
|
|
227
|
+
args: Additional arguments to pass to the script
|
|
228
|
+
env: Environment variables to set for the subprocess
|
|
229
|
+
cwd: Current working directory for the subprocess
|
|
230
|
+
node_cmd: Node.js command to use (default: "node")
|
|
231
|
+
"""
|
|
232
|
+
script_path = Path(script_path).resolve()
|
|
233
|
+
if not script_path.is_file():
|
|
234
|
+
raise FileNotFoundError(f"Script not found: {script_path}")
|
|
235
|
+
if not str(script_path).endswith(".js"):
|
|
236
|
+
raise ValueError(f"Not a JavaScript script: {script_path}")
|
|
237
|
+
|
|
238
|
+
full_args = [str(script_path)]
|
|
239
|
+
if args:
|
|
240
|
+
full_args.extend(args)
|
|
241
|
+
|
|
242
|
+
super().__init__(command=node_cmd, args=full_args, env=env, cwd=cwd)
|
|
243
|
+
self.script_path = script_path
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
class UvxStdioTransport(StdioTransport):
|
|
247
|
+
"""Transport for running commands via the uvx tool."""
|
|
248
|
+
|
|
249
|
+
def __init__(
|
|
250
|
+
self,
|
|
251
|
+
tool_name: str,
|
|
252
|
+
tool_args: list[str] | None = None,
|
|
253
|
+
project_directory: str | None = None,
|
|
254
|
+
python_version: str | None = None,
|
|
255
|
+
with_packages: list[str] | None = None,
|
|
256
|
+
from_package: str | None = None,
|
|
257
|
+
env_vars: dict[str, str] | None = None,
|
|
258
|
+
):
|
|
259
|
+
"""
|
|
260
|
+
Initialize a Uvx transport.
|
|
261
|
+
|
|
262
|
+
Args:
|
|
263
|
+
tool_name: Name of the tool to run via uvx
|
|
264
|
+
tool_args: Arguments to pass to the tool
|
|
265
|
+
project_directory: Project directory (for package resolution)
|
|
266
|
+
python_version: Python version to use
|
|
267
|
+
with_packages: Additional packages to include
|
|
268
|
+
from_package: Package to install the tool from
|
|
269
|
+
env_vars: Additional environment variables
|
|
270
|
+
"""
|
|
271
|
+
# Basic validation
|
|
272
|
+
if project_directory and not Path(project_directory).exists():
|
|
273
|
+
raise NotADirectoryError(
|
|
274
|
+
f"Project directory not found: {project_directory}"
|
|
275
|
+
)
|
|
276
|
+
|
|
277
|
+
# Build uvx arguments
|
|
278
|
+
uvx_args = []
|
|
279
|
+
if python_version:
|
|
280
|
+
uvx_args.extend(["--python", python_version])
|
|
281
|
+
if from_package:
|
|
282
|
+
uvx_args.extend(["--from", from_package])
|
|
283
|
+
for pkg in with_packages or []:
|
|
284
|
+
uvx_args.extend(["--with", pkg])
|
|
285
|
+
|
|
286
|
+
# Add the tool name and tool args
|
|
287
|
+
uvx_args.append(tool_name)
|
|
288
|
+
if tool_args:
|
|
289
|
+
uvx_args.extend(tool_args)
|
|
290
|
+
|
|
291
|
+
# Get environment with any additional variables
|
|
292
|
+
env = None
|
|
293
|
+
if env_vars:
|
|
294
|
+
env = os.environ.copy()
|
|
295
|
+
env.update(env_vars)
|
|
296
|
+
|
|
297
|
+
super().__init__(command="uvx", args=uvx_args, env=env, cwd=project_directory)
|
|
298
|
+
self.tool_name = tool_name
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
class NpxStdioTransport(StdioTransport):
|
|
302
|
+
"""Transport for running commands via the npx tool."""
|
|
303
|
+
|
|
304
|
+
def __init__(
|
|
305
|
+
self,
|
|
306
|
+
package: str,
|
|
307
|
+
args: list[str] | None = None,
|
|
308
|
+
project_directory: str | None = None,
|
|
309
|
+
env_vars: dict[str, str] | None = None,
|
|
310
|
+
use_package_lock: bool = True,
|
|
311
|
+
):
|
|
312
|
+
"""
|
|
313
|
+
Initialize an Npx transport.
|
|
314
|
+
|
|
315
|
+
Args:
|
|
316
|
+
package: Name of the npm package to run
|
|
317
|
+
args: Arguments to pass to the package command
|
|
318
|
+
project_directory: Project directory with package.json
|
|
319
|
+
env_vars: Additional environment variables
|
|
320
|
+
use_package_lock: Whether to use package-lock.json (--prefer-offline)
|
|
321
|
+
"""
|
|
322
|
+
# Basic validation
|
|
323
|
+
if project_directory and not Path(project_directory).exists():
|
|
324
|
+
raise NotADirectoryError(
|
|
325
|
+
f"Project directory not found: {project_directory}"
|
|
326
|
+
)
|
|
327
|
+
|
|
328
|
+
# Build npx arguments
|
|
329
|
+
npx_args = []
|
|
330
|
+
if use_package_lock:
|
|
331
|
+
npx_args.append("--prefer-offline")
|
|
332
|
+
|
|
333
|
+
# Add the package name and args
|
|
334
|
+
npx_args.append(package)
|
|
335
|
+
if args:
|
|
336
|
+
npx_args.extend(args)
|
|
337
|
+
|
|
338
|
+
# Get environment with any additional variables
|
|
339
|
+
env = None
|
|
340
|
+
if env_vars:
|
|
341
|
+
env = os.environ.copy()
|
|
342
|
+
env.update(env_vars)
|
|
343
|
+
|
|
344
|
+
super().__init__(command="npx", args=npx_args, env=env, cwd=project_directory)
|
|
345
|
+
self.package = package
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
class FastMCPTransport(ClientTransport):
|
|
349
|
+
"""
|
|
350
|
+
Special transport for in-memory connections to an MCP server.
|
|
351
|
+
|
|
352
|
+
This is particularly useful for testing or when client and server
|
|
353
|
+
are in the same process.
|
|
354
|
+
"""
|
|
355
|
+
|
|
356
|
+
def __init__(self, mcp: FastMCPServer):
|
|
357
|
+
self._fastmcp = mcp # Can be FastMCP or MCPServer
|
|
358
|
+
|
|
359
|
+
@contextlib.asynccontextmanager
|
|
360
|
+
async def connect_session(
|
|
361
|
+
self, **session_kwargs: Unpack[SessionKwargs]
|
|
362
|
+
) -> AsyncIterator[ClientSession]:
|
|
363
|
+
# create_connected_server_and_client_session manages the session lifecycle itself
|
|
364
|
+
async with create_connected_server_and_client_session(
|
|
365
|
+
server=self._fastmcp._mcp_server,
|
|
366
|
+
**session_kwargs,
|
|
367
|
+
) as session:
|
|
368
|
+
yield session
|
|
369
|
+
|
|
370
|
+
def __repr__(self) -> str:
|
|
371
|
+
return f"<FastMCP(server='{self._fastmcp.name}')>"
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
def infer_transport(
|
|
375
|
+
transport: ClientTransport | FastMCPServer | AnyUrl | Path | str,
|
|
376
|
+
) -> ClientTransport:
|
|
377
|
+
"""
|
|
378
|
+
Infer the appropriate transport type from the given transport argument.
|
|
379
|
+
|
|
380
|
+
This function attempts to infer the correct transport type from the provided
|
|
381
|
+
argument, handling various input types and converting them to the appropriate
|
|
382
|
+
ClientTransport subclass.
|
|
383
|
+
"""
|
|
384
|
+
# the transport is already a ClientTransport
|
|
385
|
+
if isinstance(transport, ClientTransport):
|
|
386
|
+
return transport
|
|
387
|
+
|
|
388
|
+
# the transport is a FastMCP server
|
|
389
|
+
elif isinstance(transport, FastMCPServer):
|
|
390
|
+
return FastMCPTransport(mcp=transport)
|
|
391
|
+
|
|
392
|
+
# the transport is a path to a script
|
|
393
|
+
elif isinstance(transport, Path | str) and Path(transport).exists():
|
|
394
|
+
if str(transport).endswith(".py"):
|
|
395
|
+
return PythonStdioTransport(script_path=transport)
|
|
396
|
+
elif str(transport).endswith(".js"):
|
|
397
|
+
return NodeStdioTransport(script_path=transport)
|
|
398
|
+
else:
|
|
399
|
+
raise ValueError(f"Unsupported script type: {transport}")
|
|
400
|
+
|
|
401
|
+
# the transport is an http(s) URL
|
|
402
|
+
elif isinstance(transport, AnyUrl | str) and str(transport).startswith("http"):
|
|
403
|
+
return SSETransport(url=transport)
|
|
404
|
+
|
|
405
|
+
# the transport is a websocket URL
|
|
406
|
+
elif isinstance(transport, AnyUrl | str) and str(transport).startswith("ws"):
|
|
407
|
+
return WSTransport(url=transport)
|
|
408
|
+
|
|
409
|
+
# the transport is an unknown type
|
|
410
|
+
else:
|
|
411
|
+
raise ValueError(f"Could not infer a valid transport from: {transport}")
|
fastmcp/prompts/__init__.py
CHANGED
fastmcp/prompts/base.py
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
"""Base classes for FastMCP prompts."""
|
|
2
2
|
|
|
3
|
-
import json
|
|
4
|
-
from typing import Any, Callable, Dict, Literal, Optional, Sequence, Awaitable
|
|
5
3
|
import inspect
|
|
4
|
+
import json
|
|
5
|
+
from collections.abc import Awaitable, Callable, Sequence
|
|
6
|
+
from typing import Any, Literal
|
|
6
7
|
|
|
7
|
-
from pydantic import BaseModel, Field, TypeAdapter, validate_call
|
|
8
|
-
from mcp.types import TextContent, ImageContent, EmbeddedResource
|
|
9
8
|
import pydantic_core
|
|
9
|
+
from mcp.types import EmbeddedResource, ImageContent, TextContent
|
|
10
|
+
from pydantic import BaseModel, Field, TypeAdapter, validate_call
|
|
10
11
|
|
|
11
12
|
CONTENT_TYPES = TextContent | ImageContent | EmbeddedResource
|
|
12
13
|
|
|
@@ -17,7 +18,7 @@ class Message(BaseModel):
|
|
|
17
18
|
role: Literal["user", "assistant"]
|
|
18
19
|
content: CONTENT_TYPES
|
|
19
20
|
|
|
20
|
-
def __init__(self, content: str | CONTENT_TYPES, **kwargs):
|
|
21
|
+
def __init__(self, content: str | CONTENT_TYPES, **kwargs: Any):
|
|
21
22
|
if isinstance(content, str):
|
|
22
23
|
content = TextContent(type="text", text=content)
|
|
23
24
|
super().__init__(content=content, **kwargs)
|
|
@@ -26,22 +27,24 @@ class Message(BaseModel):
|
|
|
26
27
|
class UserMessage(Message):
|
|
27
28
|
"""A message from the user."""
|
|
28
29
|
|
|
29
|
-
role: Literal["user"] = "user"
|
|
30
|
+
role: Literal["user", "assistant"] = "user"
|
|
30
31
|
|
|
31
|
-
def __init__(self, content: str | CONTENT_TYPES, **kwargs):
|
|
32
|
+
def __init__(self, content: str | CONTENT_TYPES, **kwargs: Any):
|
|
32
33
|
super().__init__(content=content, **kwargs)
|
|
33
34
|
|
|
34
35
|
|
|
35
36
|
class AssistantMessage(Message):
|
|
36
37
|
"""A message from the assistant."""
|
|
37
38
|
|
|
38
|
-
role: Literal["assistant"] = "assistant"
|
|
39
|
+
role: Literal["user", "assistant"] = "assistant"
|
|
39
40
|
|
|
40
|
-
def __init__(self, content: str | CONTENT_TYPES, **kwargs):
|
|
41
|
+
def __init__(self, content: str | CONTENT_TYPES, **kwargs: Any):
|
|
41
42
|
super().__init__(content=content, **kwargs)
|
|
42
43
|
|
|
43
44
|
|
|
44
|
-
message_validator = TypeAdapter
|
|
45
|
+
message_validator = TypeAdapter[UserMessage | AssistantMessage](
|
|
46
|
+
UserMessage | AssistantMessage
|
|
47
|
+
)
|
|
45
48
|
|
|
46
49
|
SyncPromptResult = (
|
|
47
50
|
str | Message | dict[str, Any] | Sequence[str | Message | dict[str, Any]]
|
|
@@ -71,14 +74,14 @@ class Prompt(BaseModel):
|
|
|
71
74
|
arguments: list[PromptArgument] | None = Field(
|
|
72
75
|
None, description="Arguments that can be passed to the prompt"
|
|
73
76
|
)
|
|
74
|
-
fn: Callable = Field(exclude=True)
|
|
77
|
+
fn: Callable[..., PromptResult | Awaitable[PromptResult]] = Field(exclude=True)
|
|
75
78
|
|
|
76
79
|
@classmethod
|
|
77
80
|
def from_function(
|
|
78
81
|
cls,
|
|
79
|
-
fn: Callable[..., PromptResult],
|
|
80
|
-
name:
|
|
81
|
-
description:
|
|
82
|
+
fn: Callable[..., PromptResult | Awaitable[PromptResult]],
|
|
83
|
+
name: str | None = None,
|
|
84
|
+
description: str | None = None,
|
|
82
85
|
) -> "Prompt":
|
|
83
86
|
"""Create a Prompt from a function.
|
|
84
87
|
|
|
@@ -97,7 +100,7 @@ class Prompt(BaseModel):
|
|
|
97
100
|
parameters = TypeAdapter(fn).json_schema()
|
|
98
101
|
|
|
99
102
|
# Convert parameters to PromptArguments
|
|
100
|
-
arguments = []
|
|
103
|
+
arguments: list[PromptArgument] = []
|
|
101
104
|
if "properties" in parameters:
|
|
102
105
|
for param_name, param in parameters["properties"].items():
|
|
103
106
|
required = param_name in parameters.get("required", [])
|
|
@@ -119,7 +122,7 @@ class Prompt(BaseModel):
|
|
|
119
122
|
fn=fn,
|
|
120
123
|
)
|
|
121
124
|
|
|
122
|
-
async def render(self, arguments:
|
|
125
|
+
async def render(self, arguments: dict[str, Any] | None = None) -> list[Message]:
|
|
123
126
|
"""Render the prompt with arguments."""
|
|
124
127
|
# Validate required arguments
|
|
125
128
|
if self.arguments:
|
|
@@ -136,25 +139,23 @@ class Prompt(BaseModel):
|
|
|
136
139
|
result = await result
|
|
137
140
|
|
|
138
141
|
# Validate messages
|
|
139
|
-
if not isinstance(result,
|
|
142
|
+
if not isinstance(result, list | tuple):
|
|
140
143
|
result = [result]
|
|
141
144
|
|
|
142
145
|
# Convert result to messages
|
|
143
|
-
messages = []
|
|
144
|
-
for msg in result:
|
|
146
|
+
messages: list[Message] = []
|
|
147
|
+
for msg in result: # type: ignore[reportUnknownVariableType]
|
|
145
148
|
try:
|
|
146
149
|
if isinstance(msg, Message):
|
|
147
150
|
messages.append(msg)
|
|
148
151
|
elif isinstance(msg, dict):
|
|
149
|
-
|
|
150
|
-
messages.append(msg)
|
|
152
|
+
messages.append(message_validator.validate_python(msg))
|
|
151
153
|
elif isinstance(msg, str):
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
)
|
|
154
|
+
content = TextContent(type="text", text=msg)
|
|
155
|
+
messages.append(UserMessage(content=content))
|
|
155
156
|
else:
|
|
156
|
-
|
|
157
|
-
messages.append(Message(role="user", content=
|
|
157
|
+
content = json.dumps(pydantic_core.to_jsonable_python(msg))
|
|
158
|
+
messages.append(Message(role="user", content=content))
|
|
158
159
|
except Exception:
|
|
159
160
|
raise ValueError(
|
|
160
161
|
f"Could not convert prompt result to message: {msg}"
|
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
"""Prompt management functionality."""
|
|
2
2
|
|
|
3
|
-
from typing import
|
|
3
|
+
from typing import Any
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
from fastmcp.prompts.base import Prompt
|
|
5
|
+
from fastmcp.prompts.base import Message, Prompt
|
|
7
6
|
from fastmcp.utilities.logging import get_logger
|
|
8
7
|
|
|
9
8
|
logger = get_logger(__name__)
|
|
@@ -13,24 +12,63 @@ class PromptManager:
|
|
|
13
12
|
"""Manages FastMCP prompts."""
|
|
14
13
|
|
|
15
14
|
def __init__(self, warn_on_duplicate_prompts: bool = True):
|
|
16
|
-
self._prompts:
|
|
15
|
+
self._prompts: dict[str, Prompt] = {}
|
|
17
16
|
self.warn_on_duplicate_prompts = warn_on_duplicate_prompts
|
|
18
17
|
|
|
19
|
-
def
|
|
18
|
+
def get_prompt(self, name: str) -> Prompt | None:
|
|
19
|
+
"""Get prompt by name."""
|
|
20
|
+
return self._prompts.get(name)
|
|
21
|
+
|
|
22
|
+
def list_prompts(self) -> list[Prompt]:
|
|
23
|
+
"""List all registered prompts."""
|
|
24
|
+
return list(self._prompts.values())
|
|
25
|
+
|
|
26
|
+
def add_prompt(
|
|
27
|
+
self,
|
|
28
|
+
prompt: Prompt,
|
|
29
|
+
) -> Prompt:
|
|
20
30
|
"""Add a prompt to the manager."""
|
|
21
|
-
|
|
31
|
+
|
|
32
|
+
# Check for duplicates
|
|
22
33
|
existing = self._prompts.get(prompt.name)
|
|
23
34
|
if existing:
|
|
24
35
|
if self.warn_on_duplicate_prompts:
|
|
25
36
|
logger.warning(f"Prompt already exists: {prompt.name}")
|
|
26
37
|
return existing
|
|
38
|
+
|
|
27
39
|
self._prompts[prompt.name] = prompt
|
|
28
40
|
return prompt
|
|
29
41
|
|
|
30
|
-
def
|
|
31
|
-
|
|
32
|
-
|
|
42
|
+
async def render_prompt(
|
|
43
|
+
self, name: str, arguments: dict[str, Any] | None = None
|
|
44
|
+
) -> list[Message]:
|
|
45
|
+
"""Render a prompt by name with arguments."""
|
|
46
|
+
prompt = self.get_prompt(name)
|
|
47
|
+
if not prompt:
|
|
48
|
+
raise ValueError(f"Unknown prompt: {name}")
|
|
33
49
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
50
|
+
return await prompt.render(arguments)
|
|
51
|
+
|
|
52
|
+
def import_prompts(
|
|
53
|
+
self, manager: "PromptManager", prefix: str | None = None
|
|
54
|
+
) -> None:
|
|
55
|
+
"""
|
|
56
|
+
Import all prompts from another PromptManager with prefixed names.
|
|
57
|
+
|
|
58
|
+
Args:
|
|
59
|
+
manager: Another PromptManager instance to import prompts from
|
|
60
|
+
prefix: Prefix to add to prompt names. The resulting prompt name will
|
|
61
|
+
be in the format "{prefix}{original_name}" if prefix is provided,
|
|
62
|
+
otherwise the original name is used.
|
|
63
|
+
For example, with prefix "weather/" and prompt "forecast_prompt",
|
|
64
|
+
the imported prompt would be available as "weather/forecast_prompt"
|
|
65
|
+
"""
|
|
66
|
+
for name, prompt in manager._prompts.items():
|
|
67
|
+
# Create prefixed name - we keep the original name in the Prompt object
|
|
68
|
+
prefixed_name = f"{prefix}{name}" if prefix else name
|
|
69
|
+
|
|
70
|
+
# Log the import
|
|
71
|
+
logger.debug(f"Importing prompt with name {name} as {prefixed_name}")
|
|
72
|
+
|
|
73
|
+
# Store the prompt with the prefixed name
|
|
74
|
+
self._prompts[prefixed_name] = prompt
|
fastmcp/resources/__init__.py
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
from .base import Resource
|
|
2
|
+
from .resource_manager import ResourceManager
|
|
3
|
+
from .templates import ResourceTemplate
|
|
2
4
|
from .types import (
|
|
3
|
-
TextResource,
|
|
4
5
|
BinaryResource,
|
|
5
|
-
|
|
6
|
+
DirectoryResource,
|
|
6
7
|
FileResource,
|
|
8
|
+
FunctionResource,
|
|
7
9
|
HttpResource,
|
|
8
|
-
|
|
10
|
+
TextResource,
|
|
9
11
|
)
|
|
10
|
-
from .templates import ResourceTemplate
|
|
11
|
-
from .resource_manager import ResourceManager
|
|
12
12
|
|
|
13
13
|
__all__ = [
|
|
14
14
|
"Resource",
|
fastmcp/resources/base.py
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"""Base classes and interfaces for FastMCP resources."""
|
|
2
2
|
|
|
3
3
|
import abc
|
|
4
|
-
from typing import
|
|
4
|
+
from typing import Annotated
|
|
5
5
|
|
|
6
6
|
from pydantic import (
|
|
7
7
|
AnyUrl,
|
|
@@ -43,6 +43,6 @@ class Resource(BaseModel, abc.ABC):
|
|
|
43
43
|
raise ValueError("Either name or uri must be provided")
|
|
44
44
|
|
|
45
45
|
@abc.abstractmethod
|
|
46
|
-
async def read(self) ->
|
|
46
|
+
async def read(self) -> str | bytes:
|
|
47
47
|
"""Read the resource content."""
|
|
48
48
|
pass
|