fastmcp 0.1.0__py3-none-any.whl → 0.3.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 +8 -1
- fastmcp/cli/__init__.py +3 -1
- fastmcp/cli/claude.py +42 -19
- fastmcp/cli/cli.py +87 -35
- fastmcp/prompts/__init__.py +4 -0
- fastmcp/prompts/base.py +149 -0
- fastmcp/prompts/manager.py +50 -0
- fastmcp/prompts/prompt_manager.py +36 -0
- fastmcp/resources/__init__.py +23 -0
- fastmcp/resources/base.py +63 -0
- fastmcp/resources/resource_manager.py +94 -0
- fastmcp/resources/templates.py +80 -0
- fastmcp/resources/types.py +171 -0
- fastmcp/server.py +455 -74
- fastmcp/tools/__init__.py +4 -0
- fastmcp/tools/base.py +79 -0
- fastmcp/tools/tool_manager.py +55 -0
- fastmcp/utilities/types.py +53 -0
- fastmcp-0.3.0.dist-info/METADATA +385 -0
- fastmcp-0.3.0.dist-info/RECORD +26 -0
- {fastmcp-0.1.0.dist-info → fastmcp-0.3.0.dist-info}/WHEEL +1 -2
- fastmcp-0.3.0.dist-info/licenses/LICENSE +201 -0
- fastmcp/_version.py +0 -16
- fastmcp/cli.py +0 -6
- fastmcp/resources.py +0 -219
- fastmcp/tools.py +0 -101
- fastmcp-0.1.0.dist-info/METADATA +0 -121
- fastmcp-0.1.0.dist-info/RECORD +0 -17
- fastmcp-0.1.0.dist-info/top_level.txt +0 -1
- {fastmcp-0.1.0.dist-info → fastmcp-0.3.0.dist-info}/entry_points.txt +0 -0
fastmcp/__init__.py
CHANGED
|
@@ -1 +1,8 @@
|
|
|
1
|
-
|
|
1
|
+
"""FastMCP - A more ergonomic interface for MCP servers."""
|
|
2
|
+
|
|
3
|
+
from importlib.metadata import version
|
|
4
|
+
from .server import FastMCP, Context
|
|
5
|
+
from .utilities.types import Image
|
|
6
|
+
|
|
7
|
+
__version__ = version("fastmcp")
|
|
8
|
+
__all__ = ["FastMCP", "Context", "Image"]
|
fastmcp/cli/__init__.py
CHANGED
fastmcp/cli/claude.py
CHANGED
|
@@ -25,18 +25,21 @@ def get_claude_config_path() -> Path | None:
|
|
|
25
25
|
|
|
26
26
|
|
|
27
27
|
def update_claude_config(
|
|
28
|
-
|
|
29
|
-
server_name:
|
|
28
|
+
file_spec: str,
|
|
29
|
+
server_name: str,
|
|
30
30
|
*,
|
|
31
|
-
|
|
31
|
+
with_editable: Optional[Path] = None,
|
|
32
|
+
with_packages: Optional[list[str]] = None,
|
|
33
|
+
force: bool = False,
|
|
32
34
|
) -> bool:
|
|
33
35
|
"""Add the MCP server to Claude's configuration.
|
|
34
36
|
|
|
35
37
|
Args:
|
|
36
|
-
|
|
37
|
-
server_name:
|
|
38
|
-
|
|
39
|
-
|
|
38
|
+
file_spec: Path to the server file, optionally with :object suffix
|
|
39
|
+
server_name: Name for the server in Claude's config
|
|
40
|
+
with_editable: Optional directory to install in editable mode
|
|
41
|
+
with_packages: Optional list of additional packages to install
|
|
42
|
+
force: If True, replace existing server with same name
|
|
40
43
|
"""
|
|
41
44
|
config_dir = get_claude_config_path()
|
|
42
45
|
if not config_dir:
|
|
@@ -51,29 +54,49 @@ def update_claude_config(
|
|
|
51
54
|
if "mcpServers" not in config:
|
|
52
55
|
config["mcpServers"] = {}
|
|
53
56
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
57
|
+
if server_name in config["mcpServers"]:
|
|
58
|
+
if not force:
|
|
59
|
+
logger.warning(
|
|
60
|
+
f"Server '{server_name}' already exists in Claude config. "
|
|
61
|
+
"Use `--force` to replace.",
|
|
62
|
+
extra={"config_file": str(config_file)},
|
|
63
|
+
)
|
|
64
|
+
return False
|
|
65
|
+
logger.info(
|
|
66
|
+
f"Replacing existing server '{server_name}' in Claude config",
|
|
59
67
|
extra={"config_file": str(config_file)},
|
|
60
68
|
)
|
|
61
|
-
return False
|
|
62
69
|
|
|
63
70
|
# Build uv run command
|
|
64
|
-
args = []
|
|
65
|
-
if uv_directory:
|
|
66
|
-
args.extend(["--directory", str(uv_directory)])
|
|
67
|
-
args.extend(["run", str(file)])
|
|
71
|
+
args = ["run", "--with", "fastmcp"]
|
|
68
72
|
|
|
69
|
-
|
|
73
|
+
if with_editable:
|
|
74
|
+
args.extend(["--with-editable", str(with_editable)])
|
|
75
|
+
|
|
76
|
+
if with_packages:
|
|
77
|
+
for pkg in with_packages:
|
|
78
|
+
if pkg:
|
|
79
|
+
args.extend(["--with", pkg])
|
|
80
|
+
|
|
81
|
+
# Convert file path to absolute before adding to command
|
|
82
|
+
# Split off any :object suffix first
|
|
83
|
+
if ":" in file_spec:
|
|
84
|
+
file_path, server_object = file_spec.rsplit(":", 1)
|
|
85
|
+
file_spec = f"{Path(file_path).resolve()}:{server_object}"
|
|
86
|
+
else:
|
|
87
|
+
file_spec = str(Path(file_spec).resolve())
|
|
88
|
+
|
|
89
|
+
# Add fastmcp run command
|
|
90
|
+
args.extend(["fastmcp", "run", file_spec])
|
|
91
|
+
|
|
92
|
+
config["mcpServers"][server_name] = {
|
|
70
93
|
"command": "uv",
|
|
71
94
|
"args": args,
|
|
72
95
|
}
|
|
73
96
|
|
|
74
97
|
config_file.write_text(json.dumps(config, indent=2))
|
|
75
98
|
logger.info(
|
|
76
|
-
f"Added server '{
|
|
99
|
+
f"Added server '{server_name}' to Claude config",
|
|
77
100
|
extra={"config_file": str(config_file)},
|
|
78
101
|
)
|
|
79
102
|
return True
|
fastmcp/cli/cli.py
CHANGED
|
@@ -24,16 +24,25 @@ app = typer.Typer(
|
|
|
24
24
|
|
|
25
25
|
|
|
26
26
|
def _build_uv_command(
|
|
27
|
-
|
|
28
|
-
|
|
27
|
+
file_spec: str,
|
|
28
|
+
with_editable: Optional[Path] = None,
|
|
29
|
+
with_packages: Optional[list[str]] = None,
|
|
29
30
|
) -> list[str]:
|
|
30
|
-
"""Build the uv run command."""
|
|
31
|
+
"""Build the uv run command that runs a FastMCP server through fastmcp run."""
|
|
31
32
|
cmd = ["uv"]
|
|
32
33
|
|
|
33
|
-
|
|
34
|
-
cmd.extend(["--directory", str(uv_directory)])
|
|
34
|
+
cmd.extend(["run", "--with", "fastmcp"])
|
|
35
35
|
|
|
36
|
-
|
|
36
|
+
if with_editable:
|
|
37
|
+
cmd.extend(["--with-editable", str(with_editable)])
|
|
38
|
+
|
|
39
|
+
if with_packages:
|
|
40
|
+
for pkg in with_packages:
|
|
41
|
+
if pkg:
|
|
42
|
+
cmd.extend(["--with", pkg])
|
|
43
|
+
|
|
44
|
+
# Add fastmcp run command
|
|
45
|
+
cmd.extend(["fastmcp", "run", file_spec])
|
|
37
46
|
return cmd
|
|
38
47
|
|
|
39
48
|
|
|
@@ -81,7 +90,7 @@ def _import_server(file: Path, server_object: Optional[str] = None):
|
|
|
81
90
|
module = importlib.util.module_from_spec(spec)
|
|
82
91
|
spec.loader.exec_module(module)
|
|
83
92
|
|
|
84
|
-
# If no object specified, try
|
|
93
|
+
# If no object specified, try common server names
|
|
85
94
|
if not server_object:
|
|
86
95
|
# Look for the most common server object names
|
|
87
96
|
for name in ["mcp", "server", "app"]:
|
|
@@ -89,7 +98,9 @@ def _import_server(file: Path, server_object: Optional[str] = None):
|
|
|
89
98
|
return getattr(module, name)
|
|
90
99
|
|
|
91
100
|
logger.error(
|
|
92
|
-
f"No server object found in {file}. Please
|
|
101
|
+
f"No server object found in {file}. Please either:\n"
|
|
102
|
+
"1. Use a standard variable name (mcp, server, or app)\n"
|
|
103
|
+
"2. Specify the object name with file:object syntax",
|
|
93
104
|
extra={"file": str(file)},
|
|
94
105
|
)
|
|
95
106
|
sys.exit(1)
|
|
@@ -137,17 +148,24 @@ def dev(
|
|
|
137
148
|
...,
|
|
138
149
|
help="Python file to run, optionally with :object suffix",
|
|
139
150
|
),
|
|
140
|
-
|
|
151
|
+
with_editable: Annotated[
|
|
141
152
|
Optional[Path],
|
|
142
153
|
typer.Option(
|
|
143
|
-
"--
|
|
144
|
-
"-
|
|
145
|
-
help="Directory containing pyproject.toml
|
|
154
|
+
"--with-editable",
|
|
155
|
+
"-e",
|
|
156
|
+
help="Directory containing pyproject.toml to install in editable mode",
|
|
146
157
|
exists=True,
|
|
147
158
|
file_okay=False,
|
|
148
159
|
resolve_path=True,
|
|
149
160
|
),
|
|
150
161
|
] = None,
|
|
162
|
+
with_packages: Annotated[
|
|
163
|
+
list[str],
|
|
164
|
+
typer.Option(
|
|
165
|
+
"--with",
|
|
166
|
+
help="Additional packages to install",
|
|
167
|
+
),
|
|
168
|
+
] = [],
|
|
151
169
|
) -> None:
|
|
152
170
|
"""Run a FastMCP server with the MCP Inspector."""
|
|
153
171
|
file, server_object = _parse_file_path(file_spec)
|
|
@@ -157,12 +175,13 @@ def dev(
|
|
|
157
175
|
extra={
|
|
158
176
|
"file": str(file),
|
|
159
177
|
"server_object": server_object,
|
|
160
|
-
"
|
|
178
|
+
"with_editable": str(with_editable) if with_editable else None,
|
|
179
|
+
"with_packages": with_packages,
|
|
161
180
|
},
|
|
162
181
|
)
|
|
163
182
|
|
|
164
183
|
try:
|
|
165
|
-
uv_cmd = _build_uv_command(
|
|
184
|
+
uv_cmd = _build_uv_command(file_spec, with_editable, with_packages)
|
|
166
185
|
# Run the MCP Inspector command
|
|
167
186
|
process = subprocess.run(
|
|
168
187
|
["npx", "@modelcontextprotocol/inspector"] + uv_cmd,
|
|
@@ -201,19 +220,24 @@ def run(
|
|
|
201
220
|
help="Transport protocol to use (stdio or sse)",
|
|
202
221
|
),
|
|
203
222
|
] = None,
|
|
204
|
-
|
|
223
|
+
with_editable: Annotated[
|
|
205
224
|
Optional[Path],
|
|
206
225
|
typer.Option(
|
|
207
|
-
"--
|
|
208
|
-
"-
|
|
209
|
-
help="Directory containing pyproject.toml
|
|
226
|
+
"--with-editable",
|
|
227
|
+
"-e",
|
|
228
|
+
help="Directory containing pyproject.toml to install in editable mode",
|
|
210
229
|
exists=True,
|
|
211
230
|
file_okay=False,
|
|
212
231
|
resolve_path=True,
|
|
213
232
|
),
|
|
214
233
|
] = None,
|
|
215
234
|
) -> None:
|
|
216
|
-
"""Run a FastMCP server.
|
|
235
|
+
"""Run a FastMCP server.
|
|
236
|
+
|
|
237
|
+
The server can be specified in two ways:
|
|
238
|
+
1. Module approach: server.py - runs the module directly, expecting a server.run() call
|
|
239
|
+
2. Import approach: server.py:app - imports and runs the specified server object
|
|
240
|
+
"""
|
|
217
241
|
file, server_object = _parse_file_path(file_spec)
|
|
218
242
|
|
|
219
243
|
logger.debug(
|
|
@@ -222,13 +246,11 @@ def run(
|
|
|
222
246
|
"file": str(file),
|
|
223
247
|
"server_object": server_object,
|
|
224
248
|
"transport": transport,
|
|
225
|
-
"
|
|
249
|
+
"with_editable": str(with_editable) if with_editable else None,
|
|
226
250
|
},
|
|
227
251
|
)
|
|
228
252
|
|
|
229
253
|
try:
|
|
230
|
-
uv_cmd = _build_uv_command(file, uv_directory)
|
|
231
|
-
|
|
232
254
|
# Import and get server object
|
|
233
255
|
server = _import_server(file, server_object)
|
|
234
256
|
|
|
@@ -261,20 +283,35 @@ def install(
|
|
|
261
283
|
typer.Option(
|
|
262
284
|
"--name",
|
|
263
285
|
"-n",
|
|
264
|
-
help="Custom name for the server (defaults to file name)",
|
|
286
|
+
help="Custom name for the server (defaults to server's name attribute or file name)",
|
|
265
287
|
),
|
|
266
288
|
] = None,
|
|
267
|
-
|
|
289
|
+
with_editable: Annotated[
|
|
268
290
|
Optional[Path],
|
|
269
291
|
typer.Option(
|
|
270
|
-
"--
|
|
271
|
-
"-
|
|
272
|
-
help="Directory containing pyproject.toml
|
|
292
|
+
"--with-editable",
|
|
293
|
+
"-e",
|
|
294
|
+
help="Directory containing pyproject.toml to install in editable mode",
|
|
273
295
|
exists=True,
|
|
274
296
|
file_okay=False,
|
|
275
297
|
resolve_path=True,
|
|
276
298
|
),
|
|
277
299
|
] = None,
|
|
300
|
+
with_packages: Annotated[
|
|
301
|
+
list[str],
|
|
302
|
+
typer.Option(
|
|
303
|
+
"--with",
|
|
304
|
+
help="Additional packages to install",
|
|
305
|
+
),
|
|
306
|
+
] = [],
|
|
307
|
+
force: Annotated[
|
|
308
|
+
bool,
|
|
309
|
+
typer.Option(
|
|
310
|
+
"--force",
|
|
311
|
+
"-f",
|
|
312
|
+
help="Replace existing server if one exists with the same name",
|
|
313
|
+
),
|
|
314
|
+
] = False,
|
|
278
315
|
) -> None:
|
|
279
316
|
"""Install a FastMCP server in the Claude desktop app."""
|
|
280
317
|
file, server_object = _parse_file_path(file_spec)
|
|
@@ -285,7 +322,9 @@ def install(
|
|
|
285
322
|
"file": str(file),
|
|
286
323
|
"server_name": server_name,
|
|
287
324
|
"server_object": server_object,
|
|
288
|
-
"
|
|
325
|
+
"with_editable": str(with_editable) if with_editable else None,
|
|
326
|
+
"with_packages": with_packages,
|
|
327
|
+
"force": force,
|
|
289
328
|
},
|
|
290
329
|
)
|
|
291
330
|
|
|
@@ -293,14 +332,27 @@ def install(
|
|
|
293
332
|
logger.error("Claude app not found")
|
|
294
333
|
sys.exit(1)
|
|
295
334
|
|
|
296
|
-
|
|
297
|
-
|
|
335
|
+
# Try to import server to get its name, but fall back to file name if dependencies missing
|
|
336
|
+
name = server_name
|
|
337
|
+
if not name:
|
|
338
|
+
try:
|
|
339
|
+
server = _import_server(file, server_object)
|
|
340
|
+
name = server.name
|
|
341
|
+
except (ImportError, ModuleNotFoundError) as e:
|
|
342
|
+
logger.debug(
|
|
343
|
+
"Could not import server (likely missing dependencies), using file name",
|
|
344
|
+
extra={"error": str(e)},
|
|
345
|
+
)
|
|
346
|
+
name = file.stem
|
|
347
|
+
|
|
348
|
+
if claude.update_claude_config(
|
|
349
|
+
file_spec,
|
|
350
|
+
name,
|
|
351
|
+
with_editable=with_editable,
|
|
352
|
+
with_packages=with_packages,
|
|
353
|
+
force=force,
|
|
354
|
+
):
|
|
298
355
|
print(f"Successfully installed {name} in Claude app")
|
|
299
356
|
else:
|
|
300
|
-
name = server_name or file.stem
|
|
301
357
|
print(f"Failed to install {name} in Claude app")
|
|
302
358
|
sys.exit(1)
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
if __name__ == "__main__":
|
|
306
|
-
app()
|
fastmcp/prompts/base.py
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
"""Base classes for FastMCP prompts."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from typing import Any, Callable, Dict, Literal, Optional, Sequence, Union
|
|
5
|
+
import inspect
|
|
6
|
+
|
|
7
|
+
from pydantic import BaseModel, Field, TypeAdapter, field_validator, validate_call
|
|
8
|
+
from mcp.types import TextContent, ImageContent, EmbeddedResource
|
|
9
|
+
import pydantic_core
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class Message(BaseModel):
|
|
13
|
+
"""Base class for all prompt messages."""
|
|
14
|
+
|
|
15
|
+
role: Literal["user", "assistant"]
|
|
16
|
+
content: Union[TextContent, ImageContent, EmbeddedResource]
|
|
17
|
+
|
|
18
|
+
def __init__(self, content, **kwargs):
|
|
19
|
+
super().__init__(content=content, **kwargs)
|
|
20
|
+
|
|
21
|
+
@field_validator("content", mode="before")
|
|
22
|
+
def validate_content(cls, v):
|
|
23
|
+
if isinstance(v, str):
|
|
24
|
+
return TextContent(type="text", text=v)
|
|
25
|
+
return v
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class UserMessage(Message):
|
|
29
|
+
"""A message from the user."""
|
|
30
|
+
|
|
31
|
+
role: Literal["user"] = "user"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class AssistantMessage(Message):
|
|
35
|
+
"""A message from the assistant."""
|
|
36
|
+
|
|
37
|
+
role: Literal["assistant"] = "assistant"
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
message_validator = TypeAdapter(Union[UserMessage, AssistantMessage])
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class PromptArgument(BaseModel):
|
|
44
|
+
"""An argument that can be passed to a prompt."""
|
|
45
|
+
|
|
46
|
+
name: str = Field(description="Name of the argument")
|
|
47
|
+
description: str | None = Field(
|
|
48
|
+
None, description="Description of what the argument does"
|
|
49
|
+
)
|
|
50
|
+
required: bool = Field(
|
|
51
|
+
default=False, description="Whether the argument is required"
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class Prompt(BaseModel):
|
|
56
|
+
"""A prompt template that can be rendered with parameters."""
|
|
57
|
+
|
|
58
|
+
name: str = Field(description="Name of the prompt")
|
|
59
|
+
description: str | None = Field(
|
|
60
|
+
None, description="Description of what the prompt does"
|
|
61
|
+
)
|
|
62
|
+
arguments: list[PromptArgument] | None = Field(
|
|
63
|
+
None, description="Arguments that can be passed to the prompt"
|
|
64
|
+
)
|
|
65
|
+
fn: Callable = Field(exclude=True)
|
|
66
|
+
|
|
67
|
+
@classmethod
|
|
68
|
+
def from_function(
|
|
69
|
+
cls,
|
|
70
|
+
fn: Callable[..., Sequence[Message]],
|
|
71
|
+
name: Optional[str] = None,
|
|
72
|
+
description: Optional[str] = None,
|
|
73
|
+
) -> "Prompt":
|
|
74
|
+
"""Create a Prompt from a function."""
|
|
75
|
+
func_name = name or fn.__name__
|
|
76
|
+
|
|
77
|
+
if func_name == "<lambda>":
|
|
78
|
+
raise ValueError("You must provide a name for lambda functions")
|
|
79
|
+
|
|
80
|
+
# Get schema from TypeAdapter - will fail if function isn't properly typed
|
|
81
|
+
parameters = TypeAdapter(fn).json_schema()
|
|
82
|
+
|
|
83
|
+
# Convert parameters to PromptArguments
|
|
84
|
+
arguments = []
|
|
85
|
+
if "properties" in parameters:
|
|
86
|
+
for param_name, param in parameters["properties"].items():
|
|
87
|
+
required = param_name in parameters.get("required", [])
|
|
88
|
+
arguments.append(
|
|
89
|
+
PromptArgument(
|
|
90
|
+
name=param_name,
|
|
91
|
+
description=param.get("description"),
|
|
92
|
+
required=required,
|
|
93
|
+
)
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
# ensure the arguments are properly cast
|
|
97
|
+
fn = validate_call(fn)
|
|
98
|
+
|
|
99
|
+
return cls(
|
|
100
|
+
name=func_name,
|
|
101
|
+
description=description or fn.__doc__ or "",
|
|
102
|
+
arguments=arguments,
|
|
103
|
+
fn=fn,
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
async def render(self, arguments: Optional[Dict[str, Any]] = None) -> list[Message]:
|
|
107
|
+
"""Render the prompt with arguments."""
|
|
108
|
+
# Validate required arguments
|
|
109
|
+
if self.arguments:
|
|
110
|
+
required = {arg.name for arg in self.arguments if arg.required}
|
|
111
|
+
provided = set(arguments or {})
|
|
112
|
+
missing = required - provided
|
|
113
|
+
if missing:
|
|
114
|
+
raise ValueError(f"Missing required arguments: {missing}")
|
|
115
|
+
|
|
116
|
+
try:
|
|
117
|
+
# Call function and check if result is a coroutine
|
|
118
|
+
result = self.fn(**(arguments or {}))
|
|
119
|
+
if inspect.iscoroutine(result):
|
|
120
|
+
result = await result
|
|
121
|
+
|
|
122
|
+
# Validate messages
|
|
123
|
+
if not isinstance(result, (list, tuple)):
|
|
124
|
+
result = [result]
|
|
125
|
+
|
|
126
|
+
# Convert result to messages
|
|
127
|
+
messages = []
|
|
128
|
+
for msg in result:
|
|
129
|
+
try:
|
|
130
|
+
if isinstance(msg, Message):
|
|
131
|
+
messages.append(msg)
|
|
132
|
+
elif isinstance(msg, dict):
|
|
133
|
+
msg = message_validator.validate_python(msg)
|
|
134
|
+
messages.append(msg)
|
|
135
|
+
elif isinstance(msg, str):
|
|
136
|
+
messages.append(
|
|
137
|
+
UserMessage(content=TextContent(type="text", text=msg))
|
|
138
|
+
)
|
|
139
|
+
else:
|
|
140
|
+
msg = json.dumps(pydantic_core.to_jsonable_python(msg))
|
|
141
|
+
messages.append(Message(role="user", content=msg))
|
|
142
|
+
except Exception:
|
|
143
|
+
raise ValueError(
|
|
144
|
+
f"Could not convert prompt result to message: {msg}"
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
return messages
|
|
148
|
+
except Exception as e:
|
|
149
|
+
raise ValueError(f"Error rendering prompt {self.name}: {e}")
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"""Prompt management functionality."""
|
|
2
|
+
|
|
3
|
+
from typing import Any, Dict, Optional
|
|
4
|
+
|
|
5
|
+
from fastmcp.prompts.base import Message, Prompt
|
|
6
|
+
from fastmcp.utilities.logging import get_logger
|
|
7
|
+
|
|
8
|
+
logger = get_logger(__name__)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class PromptManager:
|
|
12
|
+
"""Manages FastMCP prompts."""
|
|
13
|
+
|
|
14
|
+
def __init__(self, warn_on_duplicate_prompts: bool = True):
|
|
15
|
+
self._prompts: Dict[str, Prompt] = {}
|
|
16
|
+
self.warn_on_duplicate_prompts = warn_on_duplicate_prompts
|
|
17
|
+
|
|
18
|
+
def get_prompt(self, name: str) -> Optional[Prompt]:
|
|
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:
|
|
30
|
+
"""Add a prompt to the manager."""
|
|
31
|
+
|
|
32
|
+
# Check for duplicates
|
|
33
|
+
existing = self._prompts.get(prompt.name)
|
|
34
|
+
if existing:
|
|
35
|
+
if self.warn_on_duplicate_prompts:
|
|
36
|
+
logger.warning(f"Prompt already exists: {prompt.name}")
|
|
37
|
+
return existing
|
|
38
|
+
|
|
39
|
+
self._prompts[prompt.name] = prompt
|
|
40
|
+
return prompt
|
|
41
|
+
|
|
42
|
+
async def render_prompt(
|
|
43
|
+
self, name: str, arguments: Optional[Dict[str, Any]] = 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}")
|
|
49
|
+
|
|
50
|
+
return await prompt.render(arguments)
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"""Prompt management functionality."""
|
|
2
|
+
|
|
3
|
+
from typing import Dict, Optional
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
from fastmcp.prompts.base import Prompt
|
|
7
|
+
from fastmcp.utilities.logging import get_logger
|
|
8
|
+
|
|
9
|
+
logger = get_logger(__name__)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class PromptManager:
|
|
13
|
+
"""Manages FastMCP prompts."""
|
|
14
|
+
|
|
15
|
+
def __init__(self, warn_on_duplicate_prompts: bool = True):
|
|
16
|
+
self._prompts: Dict[str, Prompt] = {}
|
|
17
|
+
self.warn_on_duplicate_prompts = warn_on_duplicate_prompts
|
|
18
|
+
|
|
19
|
+
def add_prompt(self, prompt: Prompt) -> Prompt:
|
|
20
|
+
"""Add a prompt to the manager."""
|
|
21
|
+
logger.debug(f"Adding prompt: {prompt.name}")
|
|
22
|
+
existing = self._prompts.get(prompt.name)
|
|
23
|
+
if existing:
|
|
24
|
+
if self.warn_on_duplicate_prompts:
|
|
25
|
+
logger.warning(f"Prompt already exists: {prompt.name}")
|
|
26
|
+
return existing
|
|
27
|
+
self._prompts[prompt.name] = prompt
|
|
28
|
+
return prompt
|
|
29
|
+
|
|
30
|
+
def get_prompt(self, name: str) -> Optional[Prompt]:
|
|
31
|
+
"""Get prompt by name."""
|
|
32
|
+
return self._prompts.get(name)
|
|
33
|
+
|
|
34
|
+
def list_prompts(self) -> list[Prompt]:
|
|
35
|
+
"""List all registered prompts."""
|
|
36
|
+
return list(self._prompts.values())
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
from .base import Resource
|
|
2
|
+
from .types import (
|
|
3
|
+
TextResource,
|
|
4
|
+
BinaryResource,
|
|
5
|
+
FunctionResource,
|
|
6
|
+
FileResource,
|
|
7
|
+
HttpResource,
|
|
8
|
+
DirectoryResource,
|
|
9
|
+
)
|
|
10
|
+
from .templates import ResourceTemplate
|
|
11
|
+
from .resource_manager import ResourceManager
|
|
12
|
+
|
|
13
|
+
__all__ = [
|
|
14
|
+
"Resource",
|
|
15
|
+
"TextResource",
|
|
16
|
+
"BinaryResource",
|
|
17
|
+
"FunctionResource",
|
|
18
|
+
"FileResource",
|
|
19
|
+
"HttpResource",
|
|
20
|
+
"DirectoryResource",
|
|
21
|
+
"ResourceTemplate",
|
|
22
|
+
"ResourceManager",
|
|
23
|
+
]
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"""Base classes and interfaces for FastMCP resources."""
|
|
2
|
+
|
|
3
|
+
import abc
|
|
4
|
+
from typing import Annotated, Union
|
|
5
|
+
|
|
6
|
+
from pydantic import (
|
|
7
|
+
AnyUrl,
|
|
8
|
+
BaseModel,
|
|
9
|
+
BeforeValidator,
|
|
10
|
+
ConfigDict,
|
|
11
|
+
Field,
|
|
12
|
+
FileUrl,
|
|
13
|
+
ValidationInfo,
|
|
14
|
+
field_validator,
|
|
15
|
+
)
|
|
16
|
+
from pydantic.networks import _BaseUrl # TODO: remove this once pydantic is updated
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def maybe_cast_str_to_any_url(x) -> AnyUrl:
|
|
20
|
+
if isinstance(x, FileUrl):
|
|
21
|
+
return x
|
|
22
|
+
elif isinstance(x, AnyUrl):
|
|
23
|
+
return x
|
|
24
|
+
elif isinstance(x, str):
|
|
25
|
+
if x.startswith("file://"):
|
|
26
|
+
return FileUrl(x)
|
|
27
|
+
return AnyUrl(x)
|
|
28
|
+
raise ValueError(f"Expected str or AnyUrl, got {type(x)}")
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
LaxAnyUrl = Annotated[_BaseUrl | str, BeforeValidator(maybe_cast_str_to_any_url)]
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class Resource(BaseModel, abc.ABC):
|
|
35
|
+
"""Base class for all resources."""
|
|
36
|
+
|
|
37
|
+
model_config = ConfigDict(validate_default=True)
|
|
38
|
+
|
|
39
|
+
uri: LaxAnyUrl = Field(default=..., description="URI of the resource")
|
|
40
|
+
name: str | None = Field(description="Name of the resource", default=None)
|
|
41
|
+
description: str | None = Field(
|
|
42
|
+
description="Description of the resource", default=None
|
|
43
|
+
)
|
|
44
|
+
mime_type: str = Field(
|
|
45
|
+
default="text/plain",
|
|
46
|
+
description="MIME type of the resource content",
|
|
47
|
+
pattern=r"^[a-zA-Z0-9]+/[a-zA-Z0-9\-+.]+$",
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
@field_validator("name", mode="before")
|
|
51
|
+
@classmethod
|
|
52
|
+
def set_default_name(cls, name: str | None, info: ValidationInfo) -> str:
|
|
53
|
+
"""Set default name from URI if not provided."""
|
|
54
|
+
if name:
|
|
55
|
+
return name
|
|
56
|
+
if uri := info.data.get("uri"):
|
|
57
|
+
return str(uri)
|
|
58
|
+
raise ValueError("Either name or uri must be provided")
|
|
59
|
+
|
|
60
|
+
@abc.abstractmethod
|
|
61
|
+
async def read(self) -> Union[str, bytes]:
|
|
62
|
+
"""Read the resource content."""
|
|
63
|
+
pass
|