fastmcp 2.0.0__py3-none-any.whl → 2.1.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 +4 -2
- fastmcp/client/client.py +80 -35
- fastmcp/client/transports.py +22 -0
- fastmcp/exceptions.py +4 -0
- fastmcp/prompts/__init__.py +2 -2
- fastmcp/prompts/{base.py → prompt.py} +29 -19
- fastmcp/prompts/prompt_manager.py +29 -12
- fastmcp/resources/__init__.py +3 -3
- fastmcp/resources/{base.py → resource.py} +20 -1
- fastmcp/resources/resource_manager.py +145 -19
- fastmcp/resources/{templates.py → template.py} +23 -3
- fastmcp/resources/types.py +2 -2
- fastmcp/server/context.py +1 -1
- fastmcp/server/openapi.py +16 -4
- fastmcp/server/proxy.py +31 -27
- fastmcp/server/server.py +247 -97
- fastmcp/settings.py +11 -3
- fastmcp/tools/__init__.py +1 -1
- fastmcp/tools/{base.py → tool.py} +26 -4
- fastmcp/tools/tool_manager.py +22 -16
- fastmcp/utilities/decorators.py +101 -0
- fastmcp/utilities/func_metadata.py +4 -1
- fastmcp/utilities/openapi.py +671 -292
- fastmcp/utilities/types.py +12 -0
- {fastmcp-2.0.0.dist-info → fastmcp-2.1.1.dist-info}/METADATA +72 -52
- fastmcp-2.1.1.dist-info/RECORD +40 -0
- fastmcp-2.0.0.dist-info/RECORD +0 -39
- {fastmcp-2.0.0.dist-info → fastmcp-2.1.1.dist-info}/WHEEL +0 -0
- {fastmcp-2.0.0.dist-info → fastmcp-2.1.1.dist-info}/entry_points.txt +0 -0
- {fastmcp-2.0.0.dist-info → fastmcp-2.1.1.dist-info}/licenses/LICENSE +0 -0
fastmcp/cli/cli.py
CHANGED
|
@@ -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
|
|
|
@@ -323,6 +323,8 @@ def run(
|
|
|
323
323
|
# Import and get server object
|
|
324
324
|
server = _import_server(file, server_object)
|
|
325
325
|
|
|
326
|
+
logger.info(f'Found server "{server.name}" in {file}')
|
|
327
|
+
|
|
326
328
|
# Run the server
|
|
327
329
|
kwargs = {}
|
|
328
330
|
if transport:
|
fastmcp/client/client.py
CHANGED
|
@@ -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
|
fastmcp/client/transports.py
CHANGED
|
@@ -208,6 +208,28 @@ class PythonStdioTransport(StdioTransport):
|
|
|
208
208
|
self.script_path = script_path
|
|
209
209
|
|
|
210
210
|
|
|
211
|
+
class FastMCPStdioTransport(StdioTransport):
|
|
212
|
+
"""Transport for running FastMCP servers using the FastMCP CLI."""
|
|
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
|
+
):
|
|
221
|
+
script_path = Path(script_path).resolve()
|
|
222
|
+
if not script_path.is_file():
|
|
223
|
+
raise FileNotFoundError(f"Script not found: {script_path}")
|
|
224
|
+
if not str(script_path).endswith(".py"):
|
|
225
|
+
raise ValueError(f"Not a Python script: {script_path}")
|
|
226
|
+
|
|
227
|
+
super().__init__(
|
|
228
|
+
command="fastmcp", args=["run", str(script_path)], env=env, cwd=cwd
|
|
229
|
+
)
|
|
230
|
+
self.script_path = script_path
|
|
231
|
+
|
|
232
|
+
|
|
211
233
|
class NodeStdioTransport(StdioTransport):
|
|
212
234
|
"""Transport for running Node.js scripts."""
|
|
213
235
|
|
fastmcp/exceptions.py
CHANGED
fastmcp/prompts/__init__.py
CHANGED
|
@@ -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
|
|
|
@@ -24,27 +27,17 @@ class Message(BaseModel):
|
|
|
24
27
|
super().__init__(content=content, **kwargs)
|
|
25
28
|
|
|
26
29
|
|
|
27
|
-
|
|
30
|
+
def UserMessage(content: str | CONTENT_TYPES, **kwargs: Any) -> Message:
|
|
28
31
|
"""A message from the user."""
|
|
32
|
+
return Message(content=content, role="user", **kwargs)
|
|
29
33
|
|
|
30
|
-
role: Literal["user", "assistant"] = "user"
|
|
31
|
-
|
|
32
|
-
def __init__(self, content: str | CONTENT_TYPES, **kwargs: Any):
|
|
33
|
-
super().__init__(content=content, **kwargs)
|
|
34
34
|
|
|
35
|
-
|
|
36
|
-
class AssistantMessage(Message):
|
|
35
|
+
def AssistantMessage(content: str | CONTENT_TYPES, **kwargs: Any) -> Message:
|
|
37
36
|
"""A message from the assistant."""
|
|
37
|
+
return Message(content=content, role="assistant", **kwargs)
|
|
38
38
|
|
|
39
|
-
role: Literal["user", "assistant"] = "assistant"
|
|
40
39
|
|
|
41
|
-
|
|
42
|
-
super().__init__(content=content, **kwargs)
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
message_validator = TypeAdapter[UserMessage | AssistantMessage](
|
|
46
|
-
UserMessage | AssistantMessage
|
|
47
|
-
)
|
|
40
|
+
message_validator = TypeAdapter[Message](Message)
|
|
48
41
|
|
|
49
42
|
SyncPromptResult = (
|
|
50
43
|
str | Message | dict[str, Any] | Sequence[str | Message | dict[str, Any]]
|
|
@@ -71,10 +64,13 @@ class Prompt(BaseModel):
|
|
|
71
64
|
description: str | None = Field(
|
|
72
65
|
None, description="Description of what the prompt does"
|
|
73
66
|
)
|
|
67
|
+
tags: Annotated[set[str], BeforeValidator(_convert_set_defaults)] = Field(
|
|
68
|
+
default_factory=set, description="Tags for the prompt"
|
|
69
|
+
)
|
|
74
70
|
arguments: list[PromptArgument] | None = Field(
|
|
75
71
|
None, description="Arguments that can be passed to the prompt"
|
|
76
72
|
)
|
|
77
|
-
fn: Callable[..., PromptResult | Awaitable[PromptResult]]
|
|
73
|
+
fn: Callable[..., PromptResult | Awaitable[PromptResult]]
|
|
78
74
|
|
|
79
75
|
@classmethod
|
|
80
76
|
def from_function(
|
|
@@ -82,6 +78,7 @@ class Prompt(BaseModel):
|
|
|
82
78
|
fn: Callable[..., PromptResult | Awaitable[PromptResult]],
|
|
83
79
|
name: str | None = None,
|
|
84
80
|
description: str | None = None,
|
|
81
|
+
tags: set[str] | None = None,
|
|
85
82
|
) -> "Prompt":
|
|
86
83
|
"""Create a Prompt from a function.
|
|
87
84
|
|
|
@@ -120,6 +117,7 @@ class Prompt(BaseModel):
|
|
|
120
117
|
description=description or fn.__doc__ or "",
|
|
121
118
|
arguments=arguments,
|
|
122
119
|
fn=fn,
|
|
120
|
+
tags=tags or set(),
|
|
123
121
|
)
|
|
124
122
|
|
|
125
123
|
async def render(self, arguments: dict[str, Any] | None = None) -> list[Message]:
|
|
@@ -152,7 +150,7 @@ class Prompt(BaseModel):
|
|
|
152
150
|
messages.append(message_validator.validate_python(msg))
|
|
153
151
|
elif isinstance(msg, str):
|
|
154
152
|
content = TextContent(type="text", text=msg)
|
|
155
|
-
messages.append(
|
|
153
|
+
messages.append(Message(role="user", content=content))
|
|
156
154
|
else:
|
|
157
155
|
content = json.dumps(pydantic_core.to_jsonable_python(msg))
|
|
158
156
|
messages.append(Message(role="user", content=content))
|
|
@@ -164,3 +162,15 @@ class Prompt(BaseModel):
|
|
|
164
162
|
return messages
|
|
165
163
|
except Exception as e:
|
|
166
164
|
raise ValueError(f"Error rendering prompt {self.name}: {e}")
|
|
165
|
+
|
|
166
|
+
def copy(self, updates: dict[str, Any] | None = None) -> Self:
|
|
167
|
+
"""Copy the prompt with optional updates."""
|
|
168
|
+
data = self.model_dump()
|
|
169
|
+
if updates:
|
|
170
|
+
data.update(updates)
|
|
171
|
+
return type(self)(**data)
|
|
172
|
+
|
|
173
|
+
def __eq__(self, other: object) -> bool:
|
|
174
|
+
if not isinstance(other, Prompt):
|
|
175
|
+
return False
|
|
176
|
+
return self.model_dump() == other.model_dump()
|
|
@@ -1,8 +1,11 @@
|
|
|
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.
|
|
6
|
+
from fastmcp.exceptions import PromptError
|
|
7
|
+
from fastmcp.prompts.prompt import Message, Prompt, PromptResult
|
|
8
|
+
from fastmcp.settings import DuplicateBehavior
|
|
6
9
|
from fastmcp.utilities.logging import get_logger
|
|
7
10
|
|
|
8
11
|
logger = get_logger(__name__)
|
|
@@ -11,9 +14,9 @@ logger = get_logger(__name__)
|
|
|
11
14
|
class PromptManager:
|
|
12
15
|
"""Manages FastMCP prompts."""
|
|
13
16
|
|
|
14
|
-
def __init__(self,
|
|
17
|
+
def __init__(self, duplicate_behavior: DuplicateBehavior = DuplicateBehavior.WARN):
|
|
15
18
|
self._prompts: dict[str, Prompt] = {}
|
|
16
|
-
self.
|
|
19
|
+
self.duplicate_behavior = duplicate_behavior
|
|
17
20
|
|
|
18
21
|
def get_prompt(self, name: str) -> Prompt | None:
|
|
19
22
|
"""Get prompt by name."""
|
|
@@ -23,18 +26,32 @@ class PromptManager:
|
|
|
23
26
|
"""List all registered prompts."""
|
|
24
27
|
return list(self._prompts.values())
|
|
25
28
|
|
|
26
|
-
def
|
|
29
|
+
def add_prompt_from_fn(
|
|
27
30
|
self,
|
|
28
|
-
|
|
31
|
+
fn: Callable[..., PromptResult | Awaitable[PromptResult]],
|
|
32
|
+
name: str | None = None,
|
|
33
|
+
description: str | None = None,
|
|
34
|
+
tags: set[str] | None = None,
|
|
29
35
|
) -> Prompt:
|
|
36
|
+
"""Create a prompt from a function."""
|
|
37
|
+
prompt = Prompt.from_function(fn, name=name, description=description, tags=tags)
|
|
38
|
+
return self.add_prompt(prompt)
|
|
39
|
+
|
|
40
|
+
def add_prompt(self, prompt: Prompt) -> Prompt:
|
|
30
41
|
"""Add a prompt to the manager."""
|
|
31
42
|
|
|
32
43
|
# Check for duplicates
|
|
33
44
|
existing = self._prompts.get(prompt.name)
|
|
34
45
|
if existing:
|
|
35
|
-
if self.
|
|
46
|
+
if self.duplicate_behavior == DuplicateBehavior.WARN:
|
|
36
47
|
logger.warning(f"Prompt already exists: {prompt.name}")
|
|
37
|
-
|
|
48
|
+
self._prompts[prompt.name] = prompt
|
|
49
|
+
elif self.duplicate_behavior == DuplicateBehavior.REPLACE:
|
|
50
|
+
self._prompts[prompt.name] = prompt
|
|
51
|
+
elif self.duplicate_behavior == DuplicateBehavior.ERROR:
|
|
52
|
+
raise ValueError(f"Prompt already exists: {prompt.name}")
|
|
53
|
+
elif self.duplicate_behavior == DuplicateBehavior.IGNORE:
|
|
54
|
+
pass
|
|
38
55
|
|
|
39
56
|
self._prompts[prompt.name] = prompt
|
|
40
57
|
return prompt
|
|
@@ -45,7 +62,7 @@ class PromptManager:
|
|
|
45
62
|
"""Render a prompt by name with arguments."""
|
|
46
63
|
prompt = self.get_prompt(name)
|
|
47
64
|
if not prompt:
|
|
48
|
-
raise
|
|
65
|
+
raise PromptError(f"Unknown prompt: {name}")
|
|
49
66
|
|
|
50
67
|
return await prompt.render(arguments)
|
|
51
68
|
|
|
@@ -64,11 +81,11 @@ class PromptManager:
|
|
|
64
81
|
the imported prompt would be available as "weather/forecast_prompt"
|
|
65
82
|
"""
|
|
66
83
|
for name, prompt in manager._prompts.items():
|
|
67
|
-
# Create prefixed name
|
|
84
|
+
# Create prefixed name
|
|
68
85
|
prefixed_name = f"{prefix}{name}" if prefix else name
|
|
69
86
|
|
|
70
|
-
|
|
71
|
-
logger.debug(f"Importing prompt with name {name} as {prefixed_name}")
|
|
87
|
+
new_prompt = prompt.copy(updates=dict(name=prefixed_name))
|
|
72
88
|
|
|
73
89
|
# Store the prompt with the prefixed name
|
|
74
|
-
self.
|
|
90
|
+
self.add_prompt(new_prompt)
|
|
91
|
+
logger.debug(f'Imported prompt "{name}" as "{prefixed_name}"')
|
fastmcp/resources/__init__.py
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
from .
|
|
2
|
-
from .
|
|
3
|
-
from .templates import ResourceTemplate
|
|
1
|
+
from .resource import Resource
|
|
2
|
+
from .template import ResourceTemplate
|
|
4
3
|
from .types import (
|
|
5
4
|
BinaryResource,
|
|
6
5
|
DirectoryResource,
|
|
@@ -9,6 +8,7 @@ from .types import (
|
|
|
9
8
|
HttpResource,
|
|
10
9
|
TextResource,
|
|
11
10
|
)
|
|
11
|
+
from .resource_manager import ResourceManager
|
|
12
12
|
|
|
13
13
|
__all__ = [
|
|
14
14
|
"Resource",
|
|
@@ -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()
|