fastmcp 2.1.0__py3-none-any.whl → 2.1.2__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 +2 -0
- fastmcp/client/transports.py +22 -0
- fastmcp/exceptions.py +4 -0
- fastmcp/prompts/__init__.py +2 -2
- fastmcp/prompts/prompt.py +6 -24
- fastmcp/prompts/prompt_manager.py +5 -2
- fastmcp/resources/__init__.py +1 -1
- fastmcp/resources/resource.py +1 -9
- fastmcp/resources/resource_manager.py +94 -9
- fastmcp/resources/template.py +0 -8
- fastmcp/server/context.py +1 -1
- fastmcp/server/proxy.py +4 -4
- fastmcp/server/server.py +156 -73
- fastmcp/tools/tool.py +4 -9
- fastmcp/tools/tool_manager.py +4 -1
- fastmcp/utilities/decorators.py +101 -0
- fastmcp/utilities/func_metadata.py +4 -1
- fastmcp/utilities/logging.py +14 -6
- fastmcp/utilities/openapi.py +650 -358
- {fastmcp-2.1.0.dist-info → fastmcp-2.1.2.dist-info}/METADATA +86 -54
- fastmcp-2.1.2.dist-info/RECORD +40 -0
- fastmcp-2.1.0.dist-info/RECORD +0 -39
- {fastmcp-2.1.0.dist-info → fastmcp-2.1.2.dist-info}/WHEEL +0 -0
- {fastmcp-2.1.0.dist-info → fastmcp-2.1.2.dist-info}/entry_points.txt +0 -0
- {fastmcp-2.1.0.dist-info → fastmcp-2.1.2.dist-info}/licenses/LICENSE +0 -0
fastmcp/cli/cli.py
CHANGED
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
fastmcp/prompts/prompt.py
CHANGED
|
@@ -8,7 +8,6 @@ from typing import Annotated, Any, Literal
|
|
|
8
8
|
import pydantic_core
|
|
9
9
|
from mcp.types import EmbeddedResource, ImageContent, TextContent
|
|
10
10
|
from pydantic import BaseModel, BeforeValidator, Field, TypeAdapter, validate_call
|
|
11
|
-
from typing_extensions import Self
|
|
12
11
|
|
|
13
12
|
from fastmcp.utilities.types import _convert_set_defaults
|
|
14
13
|
|
|
@@ -27,27 +26,17 @@ class Message(BaseModel):
|
|
|
27
26
|
super().__init__(content=content, **kwargs)
|
|
28
27
|
|
|
29
28
|
|
|
30
|
-
|
|
29
|
+
def UserMessage(content: str | CONTENT_TYPES, **kwargs: Any) -> Message:
|
|
31
30
|
"""A message from the user."""
|
|
31
|
+
return Message(content=content, role="user", **kwargs)
|
|
32
32
|
|
|
33
|
-
role: Literal["user", "assistant"] = "user"
|
|
34
33
|
|
|
35
|
-
|
|
36
|
-
super().__init__(content=content, **kwargs)
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
class AssistantMessage(Message):
|
|
34
|
+
def AssistantMessage(content: str | CONTENT_TYPES, **kwargs: Any) -> Message:
|
|
40
35
|
"""A message from the assistant."""
|
|
36
|
+
return Message(content=content, role="assistant", **kwargs)
|
|
41
37
|
|
|
42
|
-
role: Literal["user", "assistant"] = "assistant"
|
|
43
38
|
|
|
44
|
-
|
|
45
|
-
super().__init__(content=content, **kwargs)
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
message_validator = TypeAdapter[UserMessage | AssistantMessage](
|
|
49
|
-
UserMessage | AssistantMessage
|
|
50
|
-
)
|
|
39
|
+
message_validator = TypeAdapter[Message](Message)
|
|
51
40
|
|
|
52
41
|
SyncPromptResult = (
|
|
53
42
|
str | Message | dict[str, Any] | Sequence[str | Message | dict[str, Any]]
|
|
@@ -160,7 +149,7 @@ class Prompt(BaseModel):
|
|
|
160
149
|
messages.append(message_validator.validate_python(msg))
|
|
161
150
|
elif isinstance(msg, str):
|
|
162
151
|
content = TextContent(type="text", text=msg)
|
|
163
|
-
messages.append(
|
|
152
|
+
messages.append(Message(role="user", content=content))
|
|
164
153
|
else:
|
|
165
154
|
content = json.dumps(pydantic_core.to_jsonable_python(msg))
|
|
166
155
|
messages.append(Message(role="user", content=content))
|
|
@@ -173,13 +162,6 @@ class Prompt(BaseModel):
|
|
|
173
162
|
except Exception as e:
|
|
174
163
|
raise ValueError(f"Error rendering prompt {self.name}: {e}")
|
|
175
164
|
|
|
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
165
|
def __eq__(self, other: object) -> bool:
|
|
184
166
|
if not isinstance(other, Prompt):
|
|
185
167
|
return False
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
"""Prompt management functionality."""
|
|
2
2
|
|
|
3
|
+
import copy
|
|
3
4
|
from collections.abc import Awaitable, Callable
|
|
4
5
|
from typing import Any
|
|
5
6
|
|
|
7
|
+
from fastmcp.exceptions import PromptError
|
|
6
8
|
from fastmcp.prompts.prompt import Message, Prompt, PromptResult
|
|
7
9
|
from fastmcp.settings import DuplicateBehavior
|
|
8
10
|
from fastmcp.utilities.logging import get_logger
|
|
@@ -61,7 +63,7 @@ class PromptManager:
|
|
|
61
63
|
"""Render a prompt by name with arguments."""
|
|
62
64
|
prompt = self.get_prompt(name)
|
|
63
65
|
if not prompt:
|
|
64
|
-
raise
|
|
66
|
+
raise PromptError(f"Unknown prompt: {name}")
|
|
65
67
|
|
|
66
68
|
return await prompt.render(arguments)
|
|
67
69
|
|
|
@@ -83,7 +85,8 @@ class PromptManager:
|
|
|
83
85
|
# Create prefixed name
|
|
84
86
|
prefixed_name = f"{prefix}{name}" if prefix else name
|
|
85
87
|
|
|
86
|
-
new_prompt =
|
|
88
|
+
new_prompt = copy.copy(prompt)
|
|
89
|
+
new_prompt.name = prefixed_name
|
|
87
90
|
|
|
88
91
|
# Store the prompt with the prefixed name
|
|
89
92
|
self.add_prompt(new_prompt)
|
fastmcp/resources/__init__.py
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
from .resource import Resource
|
|
2
|
-
from .resource_manager import ResourceManager
|
|
3
2
|
from .template import ResourceTemplate
|
|
4
3
|
from .types import (
|
|
5
4
|
BinaryResource,
|
|
@@ -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",
|
fastmcp/resources/resource.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 Annotated
|
|
4
|
+
from typing import Annotated
|
|
5
5
|
|
|
6
6
|
from pydantic import (
|
|
7
7
|
AnyUrl,
|
|
@@ -13,7 +13,6 @@ from pydantic import (
|
|
|
13
13
|
ValidationInfo,
|
|
14
14
|
field_validator,
|
|
15
15
|
)
|
|
16
|
-
from typing_extensions import Self
|
|
17
16
|
|
|
18
17
|
from fastmcp.utilities.types import _convert_set_defaults
|
|
19
18
|
|
|
@@ -54,13 +53,6 @@ class Resource(BaseModel, abc.ABC):
|
|
|
54
53
|
"""Read the resource content."""
|
|
55
54
|
pass
|
|
56
55
|
|
|
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
56
|
def __eq__(self, other: object) -> bool:
|
|
65
57
|
if not isinstance(other, Resource):
|
|
66
58
|
return False
|
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
"""Resource manager functionality."""
|
|
2
2
|
|
|
3
|
+
import copy
|
|
4
|
+
import inspect
|
|
5
|
+
import re
|
|
3
6
|
from collections.abc import Callable
|
|
4
7
|
from typing import Any
|
|
5
8
|
|
|
6
9
|
from pydantic import AnyUrl
|
|
7
10
|
|
|
8
|
-
from fastmcp.
|
|
11
|
+
from fastmcp.exceptions import ResourceError
|
|
12
|
+
from fastmcp.resources import FunctionResource, Resource
|
|
9
13
|
from fastmcp.resources.template import ResourceTemplate
|
|
10
14
|
from fastmcp.settings import DuplicateBehavior
|
|
11
15
|
from fastmcp.utilities.logging import get_logger
|
|
@@ -21,16 +25,86 @@ class ResourceManager:
|
|
|
21
25
|
self._templates: dict[str, ResourceTemplate] = {}
|
|
22
26
|
self.duplicate_behavior = duplicate_behavior
|
|
23
27
|
|
|
24
|
-
def
|
|
25
|
-
|
|
28
|
+
def add_resource_or_template_from_fn(
|
|
29
|
+
self,
|
|
30
|
+
fn: Callable[..., Any],
|
|
31
|
+
uri: str,
|
|
32
|
+
name: str | None = None,
|
|
33
|
+
description: str | None = None,
|
|
34
|
+
mime_type: str | None = None,
|
|
35
|
+
tags: set[str] | None = None,
|
|
36
|
+
) -> Resource | ResourceTemplate:
|
|
37
|
+
"""Add a resource or template to the manager from a function.
|
|
26
38
|
|
|
27
39
|
Args:
|
|
28
|
-
|
|
40
|
+
fn: The function to register as a resource or template
|
|
41
|
+
uri: The URI for the resource or template
|
|
42
|
+
name: Optional name for the resource or template
|
|
43
|
+
description: Optional description of the resource or template
|
|
44
|
+
mime_type: Optional MIME type for the resource or template
|
|
45
|
+
tags: Optional set of tags for categorizing the resource or template
|
|
46
|
+
|
|
47
|
+
Returns:
|
|
48
|
+
The added resource or template. If a resource or template with the same URI already exists,
|
|
49
|
+
returns the existing resource or template.
|
|
50
|
+
"""
|
|
51
|
+
# Check if this should be a template
|
|
52
|
+
has_uri_params = "{" in uri and "}" in uri
|
|
53
|
+
has_func_params = bool(inspect.signature(fn).parameters)
|
|
54
|
+
|
|
55
|
+
if has_uri_params and has_func_params:
|
|
56
|
+
return self.add_template_from_fn(
|
|
57
|
+
fn, uri, name, description, mime_type, tags
|
|
58
|
+
)
|
|
59
|
+
elif not has_uri_params and not has_func_params:
|
|
60
|
+
return self.add_resource_from_fn(
|
|
61
|
+
fn, uri, name, description, mime_type, tags
|
|
62
|
+
)
|
|
63
|
+
else:
|
|
64
|
+
raise ValueError(
|
|
65
|
+
"Invalid resource or template definition due to a "
|
|
66
|
+
"mismatch between URI parameters and function parameters."
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
def add_resource_from_fn(
|
|
70
|
+
self,
|
|
71
|
+
fn: Callable[..., Any],
|
|
72
|
+
uri: str,
|
|
73
|
+
name: str | None = None,
|
|
74
|
+
description: str | None = None,
|
|
75
|
+
mime_type: str | None = None,
|
|
76
|
+
tags: set[str] | None = None,
|
|
77
|
+
) -> Resource:
|
|
78
|
+
"""Add a resource to the manager from a function.
|
|
79
|
+
|
|
80
|
+
Args:
|
|
81
|
+
fn: The function to register as a resource
|
|
82
|
+
uri: The URI for the resource
|
|
83
|
+
name: Optional name for the resource
|
|
84
|
+
description: Optional description of the resource
|
|
85
|
+
mime_type: Optional MIME type for the resource
|
|
86
|
+
tags: Optional set of tags for categorizing the resource
|
|
29
87
|
|
|
30
88
|
Returns:
|
|
31
89
|
The added resource. If a resource with the same URI already exists,
|
|
32
90
|
returns the existing resource.
|
|
33
91
|
"""
|
|
92
|
+
resource = FunctionResource(
|
|
93
|
+
uri=AnyUrl(uri),
|
|
94
|
+
name=name,
|
|
95
|
+
description=description,
|
|
96
|
+
mime_type=mime_type or "text/plain",
|
|
97
|
+
fn=fn,
|
|
98
|
+
tags=tags or set(),
|
|
99
|
+
)
|
|
100
|
+
return self.add_resource(resource)
|
|
101
|
+
|
|
102
|
+
def add_resource(self, resource: Resource) -> Resource:
|
|
103
|
+
"""Add a resource to the manager.
|
|
104
|
+
|
|
105
|
+
Args:
|
|
106
|
+
resource: A Resource instance to add
|
|
107
|
+
"""
|
|
34
108
|
logger.debug(
|
|
35
109
|
"Adding resource",
|
|
36
110
|
extra={
|
|
@@ -63,6 +137,17 @@ class ResourceManager:
|
|
|
63
137
|
tags: set[str] | None = None,
|
|
64
138
|
) -> ResourceTemplate:
|
|
65
139
|
"""Create a template from a function."""
|
|
140
|
+
|
|
141
|
+
# Validate that URI params match function params
|
|
142
|
+
uri_params = set(re.findall(r"{(\w+)}", uri_template))
|
|
143
|
+
func_params = set(inspect.signature(fn).parameters.keys())
|
|
144
|
+
|
|
145
|
+
if uri_params != func_params:
|
|
146
|
+
raise ValueError(
|
|
147
|
+
f"Mismatch between URI parameters {uri_params} "
|
|
148
|
+
f"and function parameters {func_params}"
|
|
149
|
+
)
|
|
150
|
+
|
|
66
151
|
template = ResourceTemplate.from_function(
|
|
67
152
|
fn,
|
|
68
153
|
uri_template=uri_template,
|
|
@@ -122,7 +207,7 @@ class ResourceManager:
|
|
|
122
207
|
except Exception as e:
|
|
123
208
|
raise ValueError(f"Error creating resource from template: {e}")
|
|
124
209
|
|
|
125
|
-
raise
|
|
210
|
+
raise ResourceError(f"Unknown resource: {uri}")
|
|
126
211
|
|
|
127
212
|
def list_resources(self) -> list[Resource]:
|
|
128
213
|
"""List all registered resources."""
|
|
@@ -154,7 +239,8 @@ class ResourceManager:
|
|
|
154
239
|
# Create prefixed URI and copy the resource with the new URI
|
|
155
240
|
prefixed_uri = f"{prefix}{uri}" if prefix else uri
|
|
156
241
|
|
|
157
|
-
new_resource =
|
|
242
|
+
new_resource = copy.copy(resource)
|
|
243
|
+
new_resource.uri = AnyUrl(prefixed_uri)
|
|
158
244
|
|
|
159
245
|
# Store directly in resources dictionary
|
|
160
246
|
self.add_resource(new_resource)
|
|
@@ -182,9 +268,8 @@ class ResourceManager:
|
|
|
182
268
|
f"{prefix}{uri_template}" if prefix else uri_template
|
|
183
269
|
)
|
|
184
270
|
|
|
185
|
-
new_template =
|
|
186
|
-
|
|
187
|
-
)
|
|
271
|
+
new_template = copy.copy(template)
|
|
272
|
+
new_template.uri_template = prefixed_uri_template
|
|
188
273
|
|
|
189
274
|
# Store directly in templates dictionary
|
|
190
275
|
self.add_template(new_template)
|
fastmcp/resources/template.py
CHANGED
|
@@ -8,7 +8,6 @@ from collections.abc import Callable
|
|
|
8
8
|
from typing import Annotated, Any
|
|
9
9
|
|
|
10
10
|
from pydantic import BaseModel, BeforeValidator, Field, TypeAdapter, validate_call
|
|
11
|
-
from typing_extensions import Self
|
|
12
11
|
|
|
13
12
|
from fastmcp.resources.types import FunctionResource, Resource
|
|
14
13
|
from fastmcp.utilities.types import _convert_set_defaults
|
|
@@ -92,13 +91,6 @@ class ResourceTemplate(BaseModel):
|
|
|
92
91
|
except Exception as e:
|
|
93
92
|
raise ValueError(f"Error creating resource from template: {e}")
|
|
94
93
|
|
|
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
94
|
def __eq__(self, other: object) -> bool:
|
|
103
95
|
if not isinstance(other, ResourceTemplate):
|
|
104
96
|
return False
|
fastmcp/server/context.py
CHANGED
|
@@ -118,7 +118,7 @@ class Context(BaseModel, Generic[ServerSessionT, LifespanContextT]):
|
|
|
118
118
|
assert self._fastmcp is not None, (
|
|
119
119
|
"Context is not available outside of a request"
|
|
120
120
|
)
|
|
121
|
-
return await self._fastmcp.
|
|
121
|
+
return await self._fastmcp._mcp_read_resource(uri)
|
|
122
122
|
|
|
123
123
|
async def log(
|
|
124
124
|
self,
|
fastmcp/server/proxy.py
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
from typing import Any, cast
|
|
2
2
|
|
|
3
3
|
import mcp.types
|
|
4
|
-
from mcp.types import BlobResourceContents,
|
|
4
|
+
from mcp.types import BlobResourceContents, TextResourceContents
|
|
5
5
|
|
|
6
6
|
import fastmcp
|
|
7
7
|
from fastmcp.client import Client
|
|
8
|
-
from fastmcp.prompts import Prompt
|
|
8
|
+
from fastmcp.prompts import Message, Prompt
|
|
9
9
|
from fastmcp.resources import Resource, ResourceTemplate
|
|
10
10
|
from fastmcp.server.context import Context
|
|
11
11
|
from fastmcp.server.server import FastMCP
|
|
@@ -142,10 +142,10 @@ class ProxyPrompt(Prompt):
|
|
|
142
142
|
fn=_proxy_passthrough,
|
|
143
143
|
)
|
|
144
144
|
|
|
145
|
-
async def render(self, arguments: dict[str, Any]) -> list[
|
|
145
|
+
async def render(self, arguments: dict[str, Any]) -> list[Message]:
|
|
146
146
|
async with self._client:
|
|
147
147
|
result = await self._client.get_prompt(self.name, arguments)
|
|
148
|
-
return result.messages
|
|
148
|
+
return [Message(role=m.role, content=m.content) for m in result.messages]
|
|
149
149
|
|
|
150
150
|
|
|
151
151
|
class FastMCPProxy(FastMCP):
|