fastmcp 2.0.0__tar.gz → 2.1.0__tar.gz
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-2.1.0/.cursor/rules/core-mcp-objects.mdc +13 -0
- {fastmcp-2.0.0 → fastmcp-2.1.0}/PKG-INFO +1 -1
- {fastmcp-2.0.0 → fastmcp-2.1.0}/examples/mount_example.py +3 -2
- {fastmcp-2.0.0 → fastmcp-2.1.0}/src/fastmcp/cli/cli.py +2 -2
- {fastmcp-2.0.0 → fastmcp-2.1.0}/src/fastmcp/client/client.py +80 -35
- {fastmcp-2.0.0 → fastmcp-2.1.0}/src/fastmcp/prompts/__init__.py +1 -1
- fastmcp-2.0.0/src/fastmcp/prompts/base.py → fastmcp-2.1.0/src/fastmcp/prompts/prompt.py +23 -3
- {fastmcp-2.0.0 → fastmcp-2.1.0}/src/fastmcp/prompts/prompt_manager.py +27 -11
- {fastmcp-2.0.0 → fastmcp-2.1.0}/src/fastmcp/resources/__init__.py +2 -2
- fastmcp-2.0.0/src/fastmcp/resources/base.py → fastmcp-2.1.0/src/fastmcp/resources/resource.py +20 -1
- {fastmcp-2.0.0 → fastmcp-2.1.0}/src/fastmcp/resources/resource_manager.py +57 -15
- fastmcp-2.0.0/src/fastmcp/resources/templates.py → fastmcp-2.1.0/src/fastmcp/resources/template.py +23 -3
- {fastmcp-2.0.0 → fastmcp-2.1.0}/src/fastmcp/resources/types.py +2 -2
- {fastmcp-2.0.0 → fastmcp-2.1.0}/src/fastmcp/server/openapi.py +16 -4
- {fastmcp-2.0.0 → fastmcp-2.1.0}/src/fastmcp/server/proxy.py +27 -23
- {fastmcp-2.0.0 → fastmcp-2.1.0}/src/fastmcp/server/server.py +97 -30
- {fastmcp-2.0.0 → fastmcp-2.1.0}/src/fastmcp/settings.py +11 -3
- {fastmcp-2.0.0 → fastmcp-2.1.0}/src/fastmcp/tools/__init__.py +1 -1
- fastmcp-2.0.0/src/fastmcp/tools/base.py → fastmcp-2.1.0/src/fastmcp/tools/tool.py +22 -3
- {fastmcp-2.0.0 → fastmcp-2.1.0}/src/fastmcp/tools/tool_manager.py +22 -16
- {fastmcp-2.0.0 → fastmcp-2.1.0}/src/fastmcp/utilities/types.py +12 -0
- fastmcp-2.0.0/tests/client/test_fastmcp_transport.py → fastmcp-2.1.0/tests/client/test_client.py +11 -11
- {fastmcp-2.0.0 → fastmcp-2.1.0}/tests/client/test_roots.py +2 -2
- {fastmcp-2.0.0 → fastmcp-2.1.0}/tests/client/test_sampling.py +3 -3
- {fastmcp-2.0.0 → fastmcp-2.1.0}/tests/prompts/test_base.py +1 -1
- {fastmcp-2.0.0 → fastmcp-2.1.0}/tests/prompts/test_prompt_manager.py +141 -81
- {fastmcp-2.0.0 → fastmcp-2.1.0}/tests/resources/test_resource_manager.py +196 -2
- {fastmcp-2.0.0 → fastmcp-2.1.0}/tests/server/test_mount.py +59 -11
- {fastmcp-2.0.0 → fastmcp-2.1.0}/tests/server/test_openapi.py +99 -4
- {fastmcp-2.0.0 → fastmcp-2.1.0}/tests/server/test_server.py +216 -208
- {fastmcp-2.0.0 → fastmcp-2.1.0}/tests/tools/test_tool_manager.py +158 -42
- {fastmcp-2.0.0 → fastmcp-2.1.0}/tests/utilities/openapi/test_openapi.py +57 -0
- {fastmcp-2.0.0 → fastmcp-2.1.0}/tests/utilities/openapi/test_openapi_fastapi.py +88 -0
- {fastmcp-2.0.0 → fastmcp-2.1.0}/.github/release.yml +0 -0
- {fastmcp-2.0.0 → fastmcp-2.1.0}/.github/workflows/publish.yml +0 -0
- {fastmcp-2.0.0 → fastmcp-2.1.0}/.github/workflows/run-static.yml +0 -0
- {fastmcp-2.0.0 → fastmcp-2.1.0}/.github/workflows/run-tests.yml +0 -0
- {fastmcp-2.0.0 → fastmcp-2.1.0}/.gitignore +0 -0
- {fastmcp-2.0.0 → fastmcp-2.1.0}/.pre-commit-config.yaml +0 -0
- {fastmcp-2.0.0 → fastmcp-2.1.0}/LICENSE +0 -0
- {fastmcp-2.0.0 → fastmcp-2.1.0}/README.md +0 -0
- {fastmcp-2.0.0 → fastmcp-2.1.0}/Windows_Notes.md +0 -0
- {fastmcp-2.0.0 → fastmcp-2.1.0}/docs/assets/demo-inspector.png +0 -0
- {fastmcp-2.0.0 → fastmcp-2.1.0}/examples/complex_inputs.py +0 -0
- {fastmcp-2.0.0 → fastmcp-2.1.0}/examples/desktop.py +0 -0
- {fastmcp-2.0.0 → fastmcp-2.1.0}/examples/echo.py +0 -0
- {fastmcp-2.0.0 → fastmcp-2.1.0}/examples/memory.py +0 -0
- {fastmcp-2.0.0 → fastmcp-2.1.0}/examples/readme-quickstart.py +0 -0
- {fastmcp-2.0.0 → fastmcp-2.1.0}/examples/sampling.py +0 -0
- {fastmcp-2.0.0 → fastmcp-2.1.0}/examples/screenshot.py +0 -0
- {fastmcp-2.0.0 → fastmcp-2.1.0}/examples/simple_echo.py +0 -0
- {fastmcp-2.0.0 → fastmcp-2.1.0}/examples/text_me.py +0 -0
- {fastmcp-2.0.0 → fastmcp-2.1.0}/pyproject.toml +0 -0
- {fastmcp-2.0.0 → fastmcp-2.1.0}/src/fastmcp/__init__.py +0 -0
- {fastmcp-2.0.0 → fastmcp-2.1.0}/src/fastmcp/cli/__init__.py +0 -0
- {fastmcp-2.0.0 → fastmcp-2.1.0}/src/fastmcp/cli/claude.py +0 -0
- {fastmcp-2.0.0 → fastmcp-2.1.0}/src/fastmcp/client/__init__.py +0 -0
- {fastmcp-2.0.0 → fastmcp-2.1.0}/src/fastmcp/client/base.py +0 -0
- {fastmcp-2.0.0 → fastmcp-2.1.0}/src/fastmcp/client/roots.py +0 -0
- {fastmcp-2.0.0 → fastmcp-2.1.0}/src/fastmcp/client/sampling.py +0 -0
- {fastmcp-2.0.0 → fastmcp-2.1.0}/src/fastmcp/client/transports.py +0 -0
- {fastmcp-2.0.0 → fastmcp-2.1.0}/src/fastmcp/exceptions.py +0 -0
- {fastmcp-2.0.0 → fastmcp-2.1.0}/src/fastmcp/py.typed +0 -0
- {fastmcp-2.0.0 → fastmcp-2.1.0}/src/fastmcp/server/__init__.py +0 -0
- {fastmcp-2.0.0 → fastmcp-2.1.0}/src/fastmcp/server/context.py +0 -0
- {fastmcp-2.0.0 → fastmcp-2.1.0}/src/fastmcp/utilities/__init__.py +0 -0
- {fastmcp-2.0.0 → fastmcp-2.1.0}/src/fastmcp/utilities/func_metadata.py +0 -0
- {fastmcp-2.0.0 → fastmcp-2.1.0}/src/fastmcp/utilities/logging.py +0 -0
- {fastmcp-2.0.0 → fastmcp-2.1.0}/src/fastmcp/utilities/openapi.py +0 -0
- {fastmcp-2.0.0 → fastmcp-2.1.0}/tests/__init__.py +0 -0
- {fastmcp-2.0.0 → fastmcp-2.1.0}/tests/client/__init__.py +0 -0
- {fastmcp-2.0.0 → fastmcp-2.1.0}/tests/prompts/__init__.py +0 -0
- {fastmcp-2.0.0 → fastmcp-2.1.0}/tests/resources/__init__.py +0 -0
- {fastmcp-2.0.0 → fastmcp-2.1.0}/tests/resources/test_file_resources.py +0 -0
- {fastmcp-2.0.0 → fastmcp-2.1.0}/tests/resources/test_function_resources.py +0 -0
- {fastmcp-2.0.0 → fastmcp-2.1.0}/tests/resources/test_resource_template.py +0 -0
- {fastmcp-2.0.0 → fastmcp-2.1.0}/tests/resources/test_resources.py +0 -0
- {fastmcp-2.0.0 → fastmcp-2.1.0}/tests/server/__init__.py +0 -0
- {fastmcp-2.0.0 → fastmcp-2.1.0}/tests/server/test_file_server.py +0 -0
- {fastmcp-2.0.0 → fastmcp-2.1.0}/tests/server/test_lifespan.py +0 -0
- {fastmcp-2.0.0 → fastmcp-2.1.0}/tests/server/test_proxy.py +0 -0
- {fastmcp-2.0.0 → fastmcp-2.1.0}/tests/server/test_run_server.py +0 -0
- {fastmcp-2.0.0 → fastmcp-2.1.0}/tests/server/test_servers/fastmcp_server.py +0 -0
- {fastmcp-2.0.0 → fastmcp-2.1.0}/tests/server/test_servers/sse.py +0 -0
- {fastmcp-2.0.0 → fastmcp-2.1.0}/tests/server/test_servers/stdio.py +0 -0
- {fastmcp-2.0.0 → fastmcp-2.1.0}/tests/tools/__init__.py +0 -0
- {fastmcp-2.0.0 → fastmcp-2.1.0}/tests/utilities/__init__.py +0 -0
- {fastmcp-2.0.0 → fastmcp-2.1.0}/tests/utilities/openapi/__init__.py +0 -0
- {fastmcp-2.0.0 → fastmcp-2.1.0}/tests/utilities/openapi/conftest.py +0 -0
- {fastmcp-2.0.0 → fastmcp-2.1.0}/tests/utilities/openapi/test_openapi_advanced.py +0 -0
- {fastmcp-2.0.0 → fastmcp-2.1.0}/tests/utilities/test_func_metadata.py +0 -0
- {fastmcp-2.0.0 → fastmcp-2.1.0}/uv.lock +0 -0
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
---
|
|
2
|
+
description:
|
|
3
|
+
globs:
|
|
4
|
+
alwaysApply: true
|
|
5
|
+
---
|
|
6
|
+
There are four major MCP object types:
|
|
7
|
+
|
|
8
|
+
- Tools (src/tools/)
|
|
9
|
+
- Resources (src/resources/)
|
|
10
|
+
- Resource Templates (src/resources/)
|
|
11
|
+
- Prompts (src/prompts)
|
|
12
|
+
|
|
13
|
+
While these have slightly different semantics and implementations, in general changes that affect interactions with any one (like adding tags, importing, etc.) will need to be adopted, applied, and tested on all others. Be sure to look at not only the object definition but also the related `Manager` (e.g. `ToolManager`, `ResourceManager`, and `PromptManager`). Also note that while resources and resource templates are different objects, they both are handled by the `ResourceManager`.
|
|
@@ -64,10 +64,11 @@ def check_app_status() -> dict[str, str]:
|
|
|
64
64
|
|
|
65
65
|
# Mount sub-applications
|
|
66
66
|
app.mount("weather", weather_app)
|
|
67
|
+
|
|
67
68
|
app.mount("news", news_app)
|
|
68
69
|
|
|
69
70
|
|
|
70
|
-
async def
|
|
71
|
+
async def get_server_details():
|
|
71
72
|
"""Print information about mounted resources."""
|
|
72
73
|
# Print available tools
|
|
73
74
|
tools = app._tool_manager.list_tools()
|
|
@@ -105,7 +106,7 @@ async def start_server():
|
|
|
105
106
|
|
|
106
107
|
if __name__ == "__main__":
|
|
107
108
|
# First run our async function to display info
|
|
108
|
-
asyncio.run(
|
|
109
|
+
asyncio.run(get_server_details())
|
|
109
110
|
|
|
110
111
|
# Then start the server (uncomment to run the server)
|
|
111
112
|
# app.run()
|
|
@@ -65,7 +65,7 @@ def _build_uv_command(
|
|
|
65
65
|
"""Build the uv run command that runs a MCP server through mcp run."""
|
|
66
66
|
cmd = ["uv"]
|
|
67
67
|
|
|
68
|
-
cmd.extend(["run", "--with", "
|
|
68
|
+
cmd.extend(["run", "--with", "fastmcp"])
|
|
69
69
|
|
|
70
70
|
if with_editable:
|
|
71
71
|
cmd.extend(["--with-editable", str(with_editable)])
|
|
@@ -76,7 +76,7 @@ def _build_uv_command(
|
|
|
76
76
|
cmd.extend(["--with", pkg])
|
|
77
77
|
|
|
78
78
|
# Add mcp run command
|
|
79
|
-
cmd.extend(["
|
|
79
|
+
cmd.extend(["fastmcp", "run", file_spec])
|
|
80
80
|
return cmd
|
|
81
81
|
|
|
82
82
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import datetime
|
|
2
2
|
from contextlib import AbstractAsyncContextManager
|
|
3
3
|
from pathlib import Path
|
|
4
|
-
from typing import Any
|
|
4
|
+
from typing import Any, Literal, cast, overload
|
|
5
5
|
|
|
6
6
|
import mcp.types
|
|
7
7
|
from mcp import ClientSession
|
|
@@ -24,6 +24,10 @@ from .transports import ClientTransport, SessionKwargs, infer_transport
|
|
|
24
24
|
__all__ = ["Client", "RootsHandler", "RootsList"]
|
|
25
25
|
|
|
26
26
|
|
|
27
|
+
class ClientError(ValueError):
|
|
28
|
+
"""Base class for errors raised by the client."""
|
|
29
|
+
|
|
30
|
+
|
|
27
31
|
class Client:
|
|
28
32
|
"""
|
|
29
33
|
MCP client that delegates connection management to a Transport instance.
|
|
@@ -122,60 +126,101 @@ class Client:
|
|
|
122
126
|
"""Send a logging/setLevel request."""
|
|
123
127
|
await self.session.set_logging_level(level)
|
|
124
128
|
|
|
125
|
-
async def
|
|
129
|
+
async def send_roots_list_changed(self) -> None:
|
|
130
|
+
"""Send a roots/list_changed notification."""
|
|
131
|
+
await self.session.send_roots_list_changed()
|
|
132
|
+
|
|
133
|
+
async def list_resources(self) -> list[mcp.types.Resource]:
|
|
126
134
|
"""Send a resources/list request."""
|
|
127
|
-
|
|
135
|
+
result = await self.session.list_resources()
|
|
136
|
+
return result.resources
|
|
128
137
|
|
|
129
|
-
async def list_resource_templates(self) -> mcp.types.
|
|
138
|
+
async def list_resource_templates(self) -> list[mcp.types.ResourceTemplate]:
|
|
130
139
|
"""Send a resources/listResourceTemplates request."""
|
|
131
|
-
|
|
140
|
+
result = await self.session.list_resource_templates()
|
|
141
|
+
return result.resourceTemplates
|
|
132
142
|
|
|
133
|
-
async def read_resource(
|
|
143
|
+
async def read_resource(
|
|
144
|
+
self, uri: AnyUrl | str
|
|
145
|
+
) -> list[mcp.types.TextResourceContents | mcp.types.BlobResourceContents]:
|
|
134
146
|
"""Send a resources/read request."""
|
|
135
147
|
if isinstance(uri, str):
|
|
136
148
|
uri = AnyUrl(uri) # Ensure AnyUrl
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
149
|
+
result = await self.session.read_resource(uri)
|
|
150
|
+
return result.contents
|
|
151
|
+
|
|
152
|
+
# async def subscribe_resource(self, uri: AnyUrl | str) -> None:
|
|
153
|
+
# """Send a resources/subscribe request."""
|
|
154
|
+
# if isinstance(uri, str):
|
|
155
|
+
# uri = AnyUrl(uri)
|
|
156
|
+
# await self.session.subscribe_resource(uri)
|
|
157
|
+
|
|
158
|
+
# async def unsubscribe_resource(self, uri: AnyUrl | str) -> None:
|
|
159
|
+
# """Send a resources/unsubscribe request."""
|
|
160
|
+
# if isinstance(uri, str):
|
|
161
|
+
# uri = AnyUrl(uri)
|
|
162
|
+
# await self.session.unsubscribe_resource(uri)
|
|
163
|
+
|
|
164
|
+
async def list_prompts(self) -> list[mcp.types.Prompt]:
|
|
152
165
|
"""Send a prompts/list request."""
|
|
153
|
-
|
|
166
|
+
result = await self.session.list_prompts()
|
|
167
|
+
return result.prompts
|
|
154
168
|
|
|
155
169
|
async def get_prompt(
|
|
156
170
|
self, name: str, arguments: dict[str, str] | None = None
|
|
157
171
|
) -> mcp.types.GetPromptResult:
|
|
158
172
|
"""Send a prompts/get request."""
|
|
159
|
-
|
|
173
|
+
result = await self.session.get_prompt(name, arguments)
|
|
174
|
+
return result
|
|
160
175
|
|
|
161
176
|
async def complete(
|
|
162
177
|
self,
|
|
163
178
|
ref: mcp.types.ResourceReference | mcp.types.PromptReference,
|
|
164
179
|
argument: dict[str, str],
|
|
165
|
-
) -> mcp.types.
|
|
166
|
-
"""Send a completion
|
|
167
|
-
|
|
180
|
+
) -> mcp.types.Completion:
|
|
181
|
+
"""Send a completion request."""
|
|
182
|
+
result = await self.session.complete(ref, argument)
|
|
183
|
+
return result.completion
|
|
168
184
|
|
|
169
|
-
async def list_tools(self) -> mcp.types.
|
|
185
|
+
async def list_tools(self) -> list[mcp.types.Tool]:
|
|
170
186
|
"""Send a tools/list request."""
|
|
171
|
-
|
|
187
|
+
result = await self.session.list_tools()
|
|
188
|
+
return result.tools
|
|
172
189
|
|
|
190
|
+
@overload
|
|
173
191
|
async def call_tool(
|
|
174
|
-
self,
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
192
|
+
self,
|
|
193
|
+
name: str,
|
|
194
|
+
arguments: dict[str, Any] | None = None,
|
|
195
|
+
_return_raw_result: Literal[False] = False,
|
|
196
|
+
) -> list[
|
|
197
|
+
mcp.types.TextContent | mcp.types.ImageContent | mcp.types.EmbeddedResource
|
|
198
|
+
]: ...
|
|
199
|
+
|
|
200
|
+
@overload
|
|
201
|
+
async def call_tool(
|
|
202
|
+
self,
|
|
203
|
+
name: str,
|
|
204
|
+
arguments: dict[str, Any] | None = None,
|
|
205
|
+
_return_raw_result: Literal[True] = True,
|
|
206
|
+
) -> mcp.types.CallToolResult: ...
|
|
178
207
|
|
|
179
|
-
async def
|
|
180
|
-
|
|
181
|
-
|
|
208
|
+
async def call_tool(
|
|
209
|
+
self,
|
|
210
|
+
name: str,
|
|
211
|
+
arguments: dict[str, Any] | None = None,
|
|
212
|
+
_return_raw_result: bool = False,
|
|
213
|
+
) -> (
|
|
214
|
+
list[
|
|
215
|
+
mcp.types.TextContent | mcp.types.ImageContent | mcp.types.EmbeddedResource
|
|
216
|
+
]
|
|
217
|
+
| mcp.types.CallToolResult
|
|
218
|
+
):
|
|
219
|
+
"""Send a tools/call request."""
|
|
220
|
+
result = await self.session.call_tool(name, arguments)
|
|
221
|
+
if _return_raw_result:
|
|
222
|
+
return result
|
|
223
|
+
elif result.isError:
|
|
224
|
+
msg = cast(mcp.types.TextContent, result.content[0]).text
|
|
225
|
+
raise ClientError(msg)
|
|
226
|
+
return result.content
|
|
@@ -3,11 +3,14 @@
|
|
|
3
3
|
import inspect
|
|
4
4
|
import json
|
|
5
5
|
from collections.abc import Awaitable, Callable, Sequence
|
|
6
|
-
from typing import Any, Literal
|
|
6
|
+
from typing import Annotated, Any, Literal
|
|
7
7
|
|
|
8
8
|
import pydantic_core
|
|
9
9
|
from mcp.types import EmbeddedResource, ImageContent, TextContent
|
|
10
|
-
from pydantic import BaseModel, Field, TypeAdapter, validate_call
|
|
10
|
+
from pydantic import BaseModel, BeforeValidator, Field, TypeAdapter, validate_call
|
|
11
|
+
from typing_extensions import Self
|
|
12
|
+
|
|
13
|
+
from fastmcp.utilities.types import _convert_set_defaults
|
|
11
14
|
|
|
12
15
|
CONTENT_TYPES = TextContent | ImageContent | EmbeddedResource
|
|
13
16
|
|
|
@@ -71,10 +74,13 @@ class Prompt(BaseModel):
|
|
|
71
74
|
description: str | None = Field(
|
|
72
75
|
None, description="Description of what the prompt does"
|
|
73
76
|
)
|
|
77
|
+
tags: Annotated[set[str], BeforeValidator(_convert_set_defaults)] = Field(
|
|
78
|
+
default_factory=set, description="Tags for the prompt"
|
|
79
|
+
)
|
|
74
80
|
arguments: list[PromptArgument] | None = Field(
|
|
75
81
|
None, description="Arguments that can be passed to the prompt"
|
|
76
82
|
)
|
|
77
|
-
fn: Callable[..., PromptResult | Awaitable[PromptResult]]
|
|
83
|
+
fn: Callable[..., PromptResult | Awaitable[PromptResult]]
|
|
78
84
|
|
|
79
85
|
@classmethod
|
|
80
86
|
def from_function(
|
|
@@ -82,6 +88,7 @@ class Prompt(BaseModel):
|
|
|
82
88
|
fn: Callable[..., PromptResult | Awaitable[PromptResult]],
|
|
83
89
|
name: str | None = None,
|
|
84
90
|
description: str | None = None,
|
|
91
|
+
tags: set[str] | None = None,
|
|
85
92
|
) -> "Prompt":
|
|
86
93
|
"""Create a Prompt from a function.
|
|
87
94
|
|
|
@@ -120,6 +127,7 @@ class Prompt(BaseModel):
|
|
|
120
127
|
description=description or fn.__doc__ or "",
|
|
121
128
|
arguments=arguments,
|
|
122
129
|
fn=fn,
|
|
130
|
+
tags=tags or set(),
|
|
123
131
|
)
|
|
124
132
|
|
|
125
133
|
async def render(self, arguments: dict[str, Any] | None = None) -> list[Message]:
|
|
@@ -164,3 +172,15 @@ class Prompt(BaseModel):
|
|
|
164
172
|
return messages
|
|
165
173
|
except Exception as e:
|
|
166
174
|
raise ValueError(f"Error rendering prompt {self.name}: {e}")
|
|
175
|
+
|
|
176
|
+
def copy(self, updates: dict[str, Any] | None = None) -> Self:
|
|
177
|
+
"""Copy the prompt with optional updates."""
|
|
178
|
+
data = self.model_dump()
|
|
179
|
+
if updates:
|
|
180
|
+
data.update(updates)
|
|
181
|
+
return type(self)(**data)
|
|
182
|
+
|
|
183
|
+
def __eq__(self, other: object) -> bool:
|
|
184
|
+
if not isinstance(other, Prompt):
|
|
185
|
+
return False
|
|
186
|
+
return self.model_dump() == other.model_dump()
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
"""Prompt management functionality."""
|
|
2
2
|
|
|
3
|
+
from collections.abc import Awaitable, Callable
|
|
3
4
|
from typing import Any
|
|
4
5
|
|
|
5
|
-
from fastmcp.prompts.
|
|
6
|
+
from fastmcp.prompts.prompt import Message, Prompt, PromptResult
|
|
7
|
+
from fastmcp.settings import DuplicateBehavior
|
|
6
8
|
from fastmcp.utilities.logging import get_logger
|
|
7
9
|
|
|
8
10
|
logger = get_logger(__name__)
|
|
@@ -11,9 +13,9 @@ logger = get_logger(__name__)
|
|
|
11
13
|
class PromptManager:
|
|
12
14
|
"""Manages FastMCP prompts."""
|
|
13
15
|
|
|
14
|
-
def __init__(self,
|
|
16
|
+
def __init__(self, duplicate_behavior: DuplicateBehavior = DuplicateBehavior.WARN):
|
|
15
17
|
self._prompts: dict[str, Prompt] = {}
|
|
16
|
-
self.
|
|
18
|
+
self.duplicate_behavior = duplicate_behavior
|
|
17
19
|
|
|
18
20
|
def get_prompt(self, name: str) -> Prompt | None:
|
|
19
21
|
"""Get prompt by name."""
|
|
@@ -23,18 +25,32 @@ class PromptManager:
|
|
|
23
25
|
"""List all registered prompts."""
|
|
24
26
|
return list(self._prompts.values())
|
|
25
27
|
|
|
26
|
-
def
|
|
28
|
+
def add_prompt_from_fn(
|
|
27
29
|
self,
|
|
28
|
-
|
|
30
|
+
fn: Callable[..., PromptResult | Awaitable[PromptResult]],
|
|
31
|
+
name: str | None = None,
|
|
32
|
+
description: str | None = None,
|
|
33
|
+
tags: set[str] | None = None,
|
|
29
34
|
) -> Prompt:
|
|
35
|
+
"""Create a prompt from a function."""
|
|
36
|
+
prompt = Prompt.from_function(fn, name=name, description=description, tags=tags)
|
|
37
|
+
return self.add_prompt(prompt)
|
|
38
|
+
|
|
39
|
+
def add_prompt(self, prompt: Prompt) -> Prompt:
|
|
30
40
|
"""Add a prompt to the manager."""
|
|
31
41
|
|
|
32
42
|
# Check for duplicates
|
|
33
43
|
existing = self._prompts.get(prompt.name)
|
|
34
44
|
if existing:
|
|
35
|
-
if self.
|
|
45
|
+
if self.duplicate_behavior == DuplicateBehavior.WARN:
|
|
36
46
|
logger.warning(f"Prompt already exists: {prompt.name}")
|
|
37
|
-
|
|
47
|
+
self._prompts[prompt.name] = prompt
|
|
48
|
+
elif self.duplicate_behavior == DuplicateBehavior.REPLACE:
|
|
49
|
+
self._prompts[prompt.name] = prompt
|
|
50
|
+
elif self.duplicate_behavior == DuplicateBehavior.ERROR:
|
|
51
|
+
raise ValueError(f"Prompt already exists: {prompt.name}")
|
|
52
|
+
elif self.duplicate_behavior == DuplicateBehavior.IGNORE:
|
|
53
|
+
pass
|
|
38
54
|
|
|
39
55
|
self._prompts[prompt.name] = prompt
|
|
40
56
|
return prompt
|
|
@@ -64,11 +80,11 @@ class PromptManager:
|
|
|
64
80
|
the imported prompt would be available as "weather/forecast_prompt"
|
|
65
81
|
"""
|
|
66
82
|
for name, prompt in manager._prompts.items():
|
|
67
|
-
# Create prefixed name
|
|
83
|
+
# Create prefixed name
|
|
68
84
|
prefixed_name = f"{prefix}{name}" if prefix else name
|
|
69
85
|
|
|
70
|
-
|
|
71
|
-
logger.debug(f"Importing prompt with name {name} as {prefixed_name}")
|
|
86
|
+
new_prompt = prompt.copy(updates=dict(name=prefixed_name))
|
|
72
87
|
|
|
73
88
|
# Store the prompt with the prefixed name
|
|
74
|
-
self.
|
|
89
|
+
self.add_prompt(new_prompt)
|
|
90
|
+
logger.debug(f'Imported prompt "{name}" as "{prefixed_name}"')
|
fastmcp-2.0.0/src/fastmcp/resources/base.py → fastmcp-2.1.0/src/fastmcp/resources/resource.py
RENAMED
|
@@ -1,17 +1,21 @@
|
|
|
1
1
|
"""Base classes and interfaces for FastMCP resources."""
|
|
2
2
|
|
|
3
3
|
import abc
|
|
4
|
-
from typing import Annotated
|
|
4
|
+
from typing import Annotated, Any
|
|
5
5
|
|
|
6
6
|
from pydantic import (
|
|
7
7
|
AnyUrl,
|
|
8
8
|
BaseModel,
|
|
9
|
+
BeforeValidator,
|
|
9
10
|
ConfigDict,
|
|
10
11
|
Field,
|
|
11
12
|
UrlConstraints,
|
|
12
13
|
ValidationInfo,
|
|
13
14
|
field_validator,
|
|
14
15
|
)
|
|
16
|
+
from typing_extensions import Self
|
|
17
|
+
|
|
18
|
+
from fastmcp.utilities.types import _convert_set_defaults
|
|
15
19
|
|
|
16
20
|
|
|
17
21
|
class Resource(BaseModel, abc.ABC):
|
|
@@ -26,6 +30,9 @@ class Resource(BaseModel, abc.ABC):
|
|
|
26
30
|
description: str | None = Field(
|
|
27
31
|
description="Description of the resource", default=None
|
|
28
32
|
)
|
|
33
|
+
tags: Annotated[set[str], BeforeValidator(_convert_set_defaults)] = Field(
|
|
34
|
+
default_factory=set, description="Tags for the resource"
|
|
35
|
+
)
|
|
29
36
|
mime_type: str = Field(
|
|
30
37
|
default="text/plain",
|
|
31
38
|
description="MIME type of the resource content",
|
|
@@ -46,3 +53,15 @@ class Resource(BaseModel, abc.ABC):
|
|
|
46
53
|
async def read(self) -> str | bytes:
|
|
47
54
|
"""Read the resource content."""
|
|
48
55
|
pass
|
|
56
|
+
|
|
57
|
+
def copy(self, updates: dict[str, Any] | None = None) -> Self:
|
|
58
|
+
"""Copy the resource with optional updates."""
|
|
59
|
+
data = self.model_dump()
|
|
60
|
+
if updates:
|
|
61
|
+
data.update(updates)
|
|
62
|
+
return type(self)(**data)
|
|
63
|
+
|
|
64
|
+
def __eq__(self, other: object) -> bool:
|
|
65
|
+
if not isinstance(other, Resource):
|
|
66
|
+
return False
|
|
67
|
+
return self.model_dump() == other.model_dump()
|
|
@@ -5,8 +5,9 @@ from typing import Any
|
|
|
5
5
|
|
|
6
6
|
from pydantic import AnyUrl
|
|
7
7
|
|
|
8
|
-
from fastmcp.resources.
|
|
9
|
-
from fastmcp.resources.
|
|
8
|
+
from fastmcp.resources.resource import Resource
|
|
9
|
+
from fastmcp.resources.template import ResourceTemplate
|
|
10
|
+
from fastmcp.settings import DuplicateBehavior
|
|
10
11
|
from fastmcp.utilities.logging import get_logger
|
|
11
12
|
|
|
12
13
|
logger = get_logger(__name__)
|
|
@@ -15,10 +16,10 @@ logger = get_logger(__name__)
|
|
|
15
16
|
class ResourceManager:
|
|
16
17
|
"""Manages FastMCP resources."""
|
|
17
18
|
|
|
18
|
-
def __init__(self,
|
|
19
|
+
def __init__(self, duplicate_behavior: DuplicateBehavior = DuplicateBehavior.WARN):
|
|
19
20
|
self._resources: dict[str, Resource] = {}
|
|
20
21
|
self._templates: dict[str, ResourceTemplate] = {}
|
|
21
|
-
self.
|
|
22
|
+
self.duplicate_behavior = duplicate_behavior
|
|
22
23
|
|
|
23
24
|
def add_resource(self, resource: Resource) -> Resource:
|
|
24
25
|
"""Add a resource to the manager.
|
|
@@ -40,28 +41,67 @@ class ResourceManager:
|
|
|
40
41
|
)
|
|
41
42
|
existing = self._resources.get(str(resource.uri))
|
|
42
43
|
if existing:
|
|
43
|
-
if self.
|
|
44
|
+
if self.duplicate_behavior == DuplicateBehavior.WARN:
|
|
44
45
|
logger.warning(f"Resource already exists: {resource.uri}")
|
|
45
|
-
|
|
46
|
+
self._resources[str(resource.uri)] = resource
|
|
47
|
+
elif self.duplicate_behavior == DuplicateBehavior.REPLACE:
|
|
48
|
+
self._resources[str(resource.uri)] = resource
|
|
49
|
+
elif self.duplicate_behavior == DuplicateBehavior.ERROR:
|
|
50
|
+
raise ValueError(f"Resource already exists: {resource.uri}")
|
|
51
|
+
elif self.duplicate_behavior == DuplicateBehavior.IGNORE:
|
|
52
|
+
pass
|
|
46
53
|
self._resources[str(resource.uri)] = resource
|
|
47
54
|
return resource
|
|
48
55
|
|
|
49
|
-
def
|
|
56
|
+
def add_template_from_fn(
|
|
50
57
|
self,
|
|
51
58
|
fn: Callable[..., Any],
|
|
52
59
|
uri_template: str,
|
|
53
60
|
name: str | None = None,
|
|
54
61
|
description: str | None = None,
|
|
55
62
|
mime_type: str | None = None,
|
|
63
|
+
tags: set[str] | None = None,
|
|
56
64
|
) -> ResourceTemplate:
|
|
57
|
-
"""
|
|
65
|
+
"""Create a template from a function."""
|
|
58
66
|
template = ResourceTemplate.from_function(
|
|
59
67
|
fn,
|
|
60
68
|
uri_template=uri_template,
|
|
61
69
|
name=name,
|
|
62
70
|
description=description,
|
|
63
71
|
mime_type=mime_type,
|
|
72
|
+
tags=tags,
|
|
64
73
|
)
|
|
74
|
+
return self.add_template(template)
|
|
75
|
+
|
|
76
|
+
def add_template(self, template: ResourceTemplate) -> ResourceTemplate:
|
|
77
|
+
"""Add a template to the manager.
|
|
78
|
+
|
|
79
|
+
Args:
|
|
80
|
+
template: A ResourceTemplate instance to add
|
|
81
|
+
|
|
82
|
+
Returns:
|
|
83
|
+
The added template. If a template with the same URI already exists,
|
|
84
|
+
returns the existing template.
|
|
85
|
+
"""
|
|
86
|
+
logger.debug(
|
|
87
|
+
"Adding resource",
|
|
88
|
+
extra={
|
|
89
|
+
"uri": template.uri_template,
|
|
90
|
+
"type": type(template).__name__,
|
|
91
|
+
"resource_name": template.name,
|
|
92
|
+
},
|
|
93
|
+
)
|
|
94
|
+
existing = self._templates.get(str(template.uri_template))
|
|
95
|
+
if existing:
|
|
96
|
+
if self.duplicate_behavior == DuplicateBehavior.WARN:
|
|
97
|
+
logger.warning(f"Resource already exists: {template.uri_template}")
|
|
98
|
+
self._templates[str(template.uri_template)] = template
|
|
99
|
+
elif self.duplicate_behavior == DuplicateBehavior.REPLACE:
|
|
100
|
+
self._templates[str(template.uri_template)] = template
|
|
101
|
+
elif self.duplicate_behavior == DuplicateBehavior.ERROR:
|
|
102
|
+
raise ValueError(f"Resource already exists: {template.uri_template}")
|
|
103
|
+
elif self.duplicate_behavior == DuplicateBehavior.IGNORE:
|
|
104
|
+
pass
|
|
65
105
|
self._templates[template.uri_template] = template
|
|
66
106
|
return template
|
|
67
107
|
|
|
@@ -114,11 +154,11 @@ class ResourceManager:
|
|
|
114
154
|
# Create prefixed URI and copy the resource with the new URI
|
|
115
155
|
prefixed_uri = f"{prefix}{uri}" if prefix else uri
|
|
116
156
|
|
|
117
|
-
|
|
118
|
-
logger.debug(f"Importing resource with URI {uri} as {prefixed_uri}")
|
|
157
|
+
new_resource = resource.copy(updates=dict(uri=prefixed_uri))
|
|
119
158
|
|
|
120
159
|
# Store directly in resources dictionary
|
|
121
|
-
self.
|
|
160
|
+
self.add_resource(new_resource)
|
|
161
|
+
logger.debug(f'Imported resource "{uri}" as "{prefixed_uri}"')
|
|
122
162
|
|
|
123
163
|
def import_templates(
|
|
124
164
|
self, manager: "ResourceManager", prefix: str | None = None
|
|
@@ -142,10 +182,12 @@ class ResourceManager:
|
|
|
142
182
|
f"{prefix}{uri_template}" if prefix else uri_template
|
|
143
183
|
)
|
|
144
184
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
f"Importing resource template with URI {uri_template} as {prefixed_uri_template}"
|
|
185
|
+
new_template = template.copy(
|
|
186
|
+
updates=dict(uri_template=prefixed_uri_template)
|
|
148
187
|
)
|
|
149
188
|
|
|
150
189
|
# Store directly in templates dictionary
|
|
151
|
-
self.
|
|
190
|
+
self.add_template(new_template)
|
|
191
|
+
logger.debug(
|
|
192
|
+
f'Imported template "{uri_template}" as "{prefixed_uri_template}"'
|
|
193
|
+
)
|
fastmcp-2.0.0/src/fastmcp/resources/templates.py → fastmcp-2.1.0/src/fastmcp/resources/template.py
RENAMED
|
@@ -5,11 +5,13 @@ from __future__ import annotations
|
|
|
5
5
|
import inspect
|
|
6
6
|
import re
|
|
7
7
|
from collections.abc import Callable
|
|
8
|
-
from typing import Any
|
|
8
|
+
from typing import Annotated, Any
|
|
9
9
|
|
|
10
|
-
from pydantic import BaseModel, Field, TypeAdapter, validate_call
|
|
10
|
+
from pydantic import BaseModel, BeforeValidator, Field, TypeAdapter, validate_call
|
|
11
|
+
from typing_extensions import Self
|
|
11
12
|
|
|
12
13
|
from fastmcp.resources.types import FunctionResource, Resource
|
|
14
|
+
from fastmcp.utilities.types import _convert_set_defaults
|
|
13
15
|
|
|
14
16
|
|
|
15
17
|
class ResourceTemplate(BaseModel):
|
|
@@ -20,10 +22,13 @@ class ResourceTemplate(BaseModel):
|
|
|
20
22
|
)
|
|
21
23
|
name: str = Field(description="Name of the resource")
|
|
22
24
|
description: str | None = Field(description="Description of what the resource does")
|
|
25
|
+
tags: Annotated[set[str], BeforeValidator(_convert_set_defaults)] = Field(
|
|
26
|
+
default_factory=set, description="Tags for the resource"
|
|
27
|
+
)
|
|
23
28
|
mime_type: str = Field(
|
|
24
29
|
default="text/plain", description="MIME type of the resource content"
|
|
25
30
|
)
|
|
26
|
-
fn: Callable[..., Any]
|
|
31
|
+
fn: Callable[..., Any]
|
|
27
32
|
parameters: dict[str, Any] = Field(
|
|
28
33
|
description="JSON schema for function parameters"
|
|
29
34
|
)
|
|
@@ -36,6 +41,7 @@ class ResourceTemplate(BaseModel):
|
|
|
36
41
|
name: str | None = None,
|
|
37
42
|
description: str | None = None,
|
|
38
43
|
mime_type: str | None = None,
|
|
44
|
+
tags: set[str] | None = None,
|
|
39
45
|
) -> ResourceTemplate:
|
|
40
46
|
"""Create a template from a function."""
|
|
41
47
|
func_name = name or fn.__name__
|
|
@@ -55,6 +61,7 @@ class ResourceTemplate(BaseModel):
|
|
|
55
61
|
mime_type=mime_type or "text/plain",
|
|
56
62
|
fn=fn,
|
|
57
63
|
parameters=parameters,
|
|
64
|
+
tags=tags or set(),
|
|
58
65
|
)
|
|
59
66
|
|
|
60
67
|
def matches(self, uri: str) -> dict[str, Any] | None:
|
|
@@ -80,6 +87,19 @@ class ResourceTemplate(BaseModel):
|
|
|
80
87
|
description=self.description,
|
|
81
88
|
mime_type=self.mime_type,
|
|
82
89
|
fn=lambda: result, # Capture result in closure
|
|
90
|
+
tags=self.tags,
|
|
83
91
|
)
|
|
84
92
|
except Exception as e:
|
|
85
93
|
raise ValueError(f"Error creating resource from template: {e}")
|
|
94
|
+
|
|
95
|
+
def copy(self, updates: dict[str, Any] | None = None) -> Self:
|
|
96
|
+
"""Copy the resource template with optional updates."""
|
|
97
|
+
data = self.model_dump()
|
|
98
|
+
if updates:
|
|
99
|
+
data.update(updates)
|
|
100
|
+
return type(self)(**data)
|
|
101
|
+
|
|
102
|
+
def __eq__(self, other: object) -> bool:
|
|
103
|
+
if not isinstance(other, ResourceTemplate):
|
|
104
|
+
return False
|
|
105
|
+
return self.model_dump() == other.model_dump()
|
|
@@ -13,7 +13,7 @@ import pydantic.json
|
|
|
13
13
|
import pydantic_core
|
|
14
14
|
from pydantic import Field, ValidationInfo
|
|
15
15
|
|
|
16
|
-
from fastmcp.resources.
|
|
16
|
+
from fastmcp.resources.resource import Resource
|
|
17
17
|
|
|
18
18
|
|
|
19
19
|
class TextResource(Resource):
|
|
@@ -49,7 +49,7 @@ class FunctionResource(Resource):
|
|
|
49
49
|
- other types will be converted to JSON
|
|
50
50
|
"""
|
|
51
51
|
|
|
52
|
-
fn: Callable[[], Any]
|
|
52
|
+
fn: Callable[[], Any]
|
|
53
53
|
|
|
54
54
|
async def read(self) -> str | bytes:
|
|
55
55
|
"""Read the resource by calling the wrapped function."""
|