fastmcp 2.1.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 +2 -0
- fastmcp/client/transports.py +22 -0
- fastmcp/exceptions.py +4 -0
- fastmcp/prompts/__init__.py +2 -2
- fastmcp/prompts/prompt.py +6 -16
- fastmcp/prompts/prompt_manager.py +2 -1
- fastmcp/resources/__init__.py +1 -1
- fastmcp/resources/resource_manager.py +89 -5
- fastmcp/server/context.py +1 -1
- fastmcp/server/proxy.py +4 -4
- fastmcp/server/server.py +156 -73
- fastmcp/tools/tool.py +4 -1
- fastmcp/utilities/decorators.py +101 -0
- fastmcp/utilities/func_metadata.py +4 -1
- fastmcp/utilities/openapi.py +671 -292
- {fastmcp-2.1.0.dist-info → fastmcp-2.1.1.dist-info}/METADATA +72 -52
- {fastmcp-2.1.0.dist-info → fastmcp-2.1.1.dist-info}/RECORD +20 -19
- {fastmcp-2.1.0.dist-info → fastmcp-2.1.1.dist-info}/WHEEL +0 -0
- {fastmcp-2.1.0.dist-info → fastmcp-2.1.1.dist-info}/entry_points.txt +0 -0
- {fastmcp-2.1.0.dist-info → fastmcp-2.1.1.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
|
@@ -27,27 +27,17 @@ class Message(BaseModel):
|
|
|
27
27
|
super().__init__(content=content, **kwargs)
|
|
28
28
|
|
|
29
29
|
|
|
30
|
-
|
|
30
|
+
def UserMessage(content: str | CONTENT_TYPES, **kwargs: Any) -> Message:
|
|
31
31
|
"""A message from the user."""
|
|
32
|
+
return Message(content=content, role="user", **kwargs)
|
|
32
33
|
|
|
33
|
-
role: Literal["user", "assistant"] = "user"
|
|
34
34
|
|
|
35
|
-
|
|
36
|
-
super().__init__(content=content, **kwargs)
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
class AssistantMessage(Message):
|
|
35
|
+
def AssistantMessage(content: str | CONTENT_TYPES, **kwargs: Any) -> Message:
|
|
40
36
|
"""A message from the assistant."""
|
|
37
|
+
return Message(content=content, role="assistant", **kwargs)
|
|
41
38
|
|
|
42
|
-
role: Literal["user", "assistant"] = "assistant"
|
|
43
|
-
|
|
44
|
-
def __init__(self, content: str | CONTENT_TYPES, **kwargs: Any):
|
|
45
|
-
super().__init__(content=content, **kwargs)
|
|
46
39
|
|
|
47
|
-
|
|
48
|
-
message_validator = TypeAdapter[UserMessage | AssistantMessage](
|
|
49
|
-
UserMessage | AssistantMessage
|
|
50
|
-
)
|
|
40
|
+
message_validator = TypeAdapter[Message](Message)
|
|
51
41
|
|
|
52
42
|
SyncPromptResult = (
|
|
53
43
|
str | Message | dict[str, Any] | Sequence[str | Message | dict[str, Any]]
|
|
@@ -160,7 +150,7 @@ class Prompt(BaseModel):
|
|
|
160
150
|
messages.append(message_validator.validate_python(msg))
|
|
161
151
|
elif isinstance(msg, str):
|
|
162
152
|
content = TextContent(type="text", text=msg)
|
|
163
|
-
messages.append(
|
|
153
|
+
messages.append(Message(role="user", content=content))
|
|
164
154
|
else:
|
|
165
155
|
content = json.dumps(pydantic_core.to_jsonable_python(msg))
|
|
166
156
|
messages.append(Message(role="user", content=content))
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
from collections.abc import Awaitable, Callable
|
|
4
4
|
from typing import Any
|
|
5
5
|
|
|
6
|
+
from fastmcp.exceptions import PromptError
|
|
6
7
|
from fastmcp.prompts.prompt import Message, Prompt, PromptResult
|
|
7
8
|
from fastmcp.settings import DuplicateBehavior
|
|
8
9
|
from fastmcp.utilities.logging import get_logger
|
|
@@ -61,7 +62,7 @@ class PromptManager:
|
|
|
61
62
|
"""Render a prompt by name with arguments."""
|
|
62
63
|
prompt = self.get_prompt(name)
|
|
63
64
|
if not prompt:
|
|
64
|
-
raise
|
|
65
|
+
raise PromptError(f"Unknown prompt: {name}")
|
|
65
66
|
|
|
66
67
|
return await prompt.render(arguments)
|
|
67
68
|
|
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",
|
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
"""Resource manager functionality."""
|
|
2
2
|
|
|
3
|
+
import inspect
|
|
4
|
+
import re
|
|
3
5
|
from collections.abc import Callable
|
|
4
6
|
from typing import Any
|
|
5
7
|
|
|
6
8
|
from pydantic import AnyUrl
|
|
7
9
|
|
|
8
|
-
from fastmcp.
|
|
10
|
+
from fastmcp.exceptions import ResourceError
|
|
11
|
+
from fastmcp.resources import FunctionResource, Resource
|
|
9
12
|
from fastmcp.resources.template import ResourceTemplate
|
|
10
13
|
from fastmcp.settings import DuplicateBehavior
|
|
11
14
|
from fastmcp.utilities.logging import get_logger
|
|
@@ -21,16 +24,86 @@ class ResourceManager:
|
|
|
21
24
|
self._templates: dict[str, ResourceTemplate] = {}
|
|
22
25
|
self.duplicate_behavior = duplicate_behavior
|
|
23
26
|
|
|
24
|
-
def
|
|
25
|
-
|
|
27
|
+
def add_resource_or_template_from_fn(
|
|
28
|
+
self,
|
|
29
|
+
fn: Callable[..., Any],
|
|
30
|
+
uri: str,
|
|
31
|
+
name: str | None = None,
|
|
32
|
+
description: str | None = None,
|
|
33
|
+
mime_type: str | None = None,
|
|
34
|
+
tags: set[str] | None = None,
|
|
35
|
+
) -> Resource | ResourceTemplate:
|
|
36
|
+
"""Add a resource or template to the manager from a function.
|
|
26
37
|
|
|
27
38
|
Args:
|
|
28
|
-
|
|
39
|
+
fn: The function to register as a resource or template
|
|
40
|
+
uri: The URI for the resource or template
|
|
41
|
+
name: Optional name for the resource or template
|
|
42
|
+
description: Optional description of the resource or template
|
|
43
|
+
mime_type: Optional MIME type for the resource or template
|
|
44
|
+
tags: Optional set of tags for categorizing the resource or template
|
|
45
|
+
|
|
46
|
+
Returns:
|
|
47
|
+
The added resource or template. If a resource or template with the same URI already exists,
|
|
48
|
+
returns the existing resource or template.
|
|
49
|
+
"""
|
|
50
|
+
# Check if this should be a template
|
|
51
|
+
has_uri_params = "{" in uri and "}" in uri
|
|
52
|
+
has_func_params = bool(inspect.signature(fn).parameters)
|
|
53
|
+
|
|
54
|
+
if has_uri_params and has_func_params:
|
|
55
|
+
return self.add_template_from_fn(
|
|
56
|
+
fn, uri, name, description, mime_type, tags
|
|
57
|
+
)
|
|
58
|
+
elif not has_uri_params and not has_func_params:
|
|
59
|
+
return self.add_resource_from_fn(
|
|
60
|
+
fn, uri, name, description, mime_type, tags
|
|
61
|
+
)
|
|
62
|
+
else:
|
|
63
|
+
raise ValueError(
|
|
64
|
+
"Invalid resource or template definition due to a "
|
|
65
|
+
"mismatch between URI parameters and function parameters."
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
def add_resource_from_fn(
|
|
69
|
+
self,
|
|
70
|
+
fn: Callable[..., Any],
|
|
71
|
+
uri: str,
|
|
72
|
+
name: str | None = None,
|
|
73
|
+
description: str | None = None,
|
|
74
|
+
mime_type: str | None = None,
|
|
75
|
+
tags: set[str] | None = None,
|
|
76
|
+
) -> Resource:
|
|
77
|
+
"""Add a resource to the manager from a function.
|
|
78
|
+
|
|
79
|
+
Args:
|
|
80
|
+
fn: The function to register as a resource
|
|
81
|
+
uri: The URI for the resource
|
|
82
|
+
name: Optional name for the resource
|
|
83
|
+
description: Optional description of the resource
|
|
84
|
+
mime_type: Optional MIME type for the resource
|
|
85
|
+
tags: Optional set of tags for categorizing the resource
|
|
29
86
|
|
|
30
87
|
Returns:
|
|
31
88
|
The added resource. If a resource with the same URI already exists,
|
|
32
89
|
returns the existing resource.
|
|
33
90
|
"""
|
|
91
|
+
resource = FunctionResource(
|
|
92
|
+
uri=AnyUrl(uri),
|
|
93
|
+
name=name,
|
|
94
|
+
description=description,
|
|
95
|
+
mime_type=mime_type or "text/plain",
|
|
96
|
+
fn=fn,
|
|
97
|
+
tags=tags or set(),
|
|
98
|
+
)
|
|
99
|
+
return self.add_resource(resource)
|
|
100
|
+
|
|
101
|
+
def add_resource(self, resource: Resource) -> Resource:
|
|
102
|
+
"""Add a resource to the manager.
|
|
103
|
+
|
|
104
|
+
Args:
|
|
105
|
+
resource: A Resource instance to add
|
|
106
|
+
"""
|
|
34
107
|
logger.debug(
|
|
35
108
|
"Adding resource",
|
|
36
109
|
extra={
|
|
@@ -63,6 +136,17 @@ class ResourceManager:
|
|
|
63
136
|
tags: set[str] | None = None,
|
|
64
137
|
) -> ResourceTemplate:
|
|
65
138
|
"""Create a template from a function."""
|
|
139
|
+
|
|
140
|
+
# Validate that URI params match function params
|
|
141
|
+
uri_params = set(re.findall(r"{(\w+)}", uri_template))
|
|
142
|
+
func_params = set(inspect.signature(fn).parameters.keys())
|
|
143
|
+
|
|
144
|
+
if uri_params != func_params:
|
|
145
|
+
raise ValueError(
|
|
146
|
+
f"Mismatch between URI parameters {uri_params} "
|
|
147
|
+
f"and function parameters {func_params}"
|
|
148
|
+
)
|
|
149
|
+
|
|
66
150
|
template = ResourceTemplate.from_function(
|
|
67
151
|
fn,
|
|
68
152
|
uri_template=uri_template,
|
|
@@ -122,7 +206,7 @@ class ResourceManager:
|
|
|
122
206
|
except Exception as e:
|
|
123
207
|
raise ValueError(f"Error creating resource from template: {e}")
|
|
124
208
|
|
|
125
|
-
raise
|
|
209
|
+
raise ResourceError(f"Unknown resource: {uri}")
|
|
126
210
|
|
|
127
211
|
def list_resources(self) -> list[Resource]:
|
|
128
212
|
"""List all registered resources."""
|
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):
|
fastmcp/server/server.py
CHANGED
|
@@ -1,9 +1,7 @@
|
|
|
1
1
|
"""FastMCP - A more ergonomic interface for MCP servers."""
|
|
2
2
|
|
|
3
|
-
import inspect
|
|
4
3
|
import json
|
|
5
|
-
import
|
|
6
|
-
from collections.abc import AsyncIterator, Callable
|
|
4
|
+
from collections.abc import AsyncIterator, Awaitable, Callable
|
|
7
5
|
from contextlib import (
|
|
8
6
|
AbstractAsyncContextManager,
|
|
9
7
|
AsyncExitStack,
|
|
@@ -43,8 +41,12 @@ import fastmcp
|
|
|
43
41
|
import fastmcp.settings
|
|
44
42
|
from fastmcp.exceptions import ResourceError
|
|
45
43
|
from fastmcp.prompts import Prompt, PromptManager
|
|
46
|
-
from fastmcp.
|
|
44
|
+
from fastmcp.prompts.prompt import Message, PromptResult
|
|
45
|
+
from fastmcp.resources import Resource, ResourceManager
|
|
46
|
+
from fastmcp.resources.template import ResourceTemplate
|
|
47
47
|
from fastmcp.tools import ToolManager
|
|
48
|
+
from fastmcp.tools.tool import Tool
|
|
49
|
+
from fastmcp.utilities.decorators import DecoratedFunction
|
|
48
50
|
from fastmcp.utilities.logging import configure_logging, get_logger
|
|
49
51
|
from fastmcp.utilities.types import Image
|
|
50
52
|
|
|
@@ -166,22 +168,32 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
166
168
|
Args:
|
|
167
169
|
transport: Transport protocol to use ("stdio" or "sse")
|
|
168
170
|
"""
|
|
171
|
+
logger.info(f'Starting server "{self.name}"...')
|
|
169
172
|
anyio.run(self.run_async, transport)
|
|
170
173
|
|
|
171
174
|
def _setup_handlers(self) -> None:
|
|
172
175
|
"""Set up core MCP protocol handlers."""
|
|
173
|
-
self._mcp_server.list_tools()(self.
|
|
176
|
+
self._mcp_server.list_tools()(self._mcp_list_tools)
|
|
174
177
|
self._mcp_server.call_tool()(self.call_tool)
|
|
175
|
-
self._mcp_server.list_resources()(self.
|
|
176
|
-
self._mcp_server.read_resource()(self.
|
|
177
|
-
self._mcp_server.list_prompts()(self.
|
|
178
|
-
self._mcp_server.get_prompt()(self.
|
|
179
|
-
self._mcp_server.list_resource_templates()(self.
|
|
178
|
+
self._mcp_server.list_resources()(self._mcp_list_resources)
|
|
179
|
+
self._mcp_server.read_resource()(self._mcp_read_resource)
|
|
180
|
+
self._mcp_server.list_prompts()(self._mcp_list_prompts)
|
|
181
|
+
self._mcp_server.get_prompt()(self._mcp_get_prompt)
|
|
182
|
+
self._mcp_server.list_resource_templates()(self._mcp_list_resource_templates)
|
|
180
183
|
|
|
181
|
-
|
|
182
|
-
|
|
184
|
+
def list_tools(self) -> list[Tool]:
|
|
185
|
+
return self._tool_manager.list_tools()
|
|
186
|
+
|
|
187
|
+
async def _mcp_list_tools(self) -> list[MCPTool]:
|
|
188
|
+
"""
|
|
189
|
+
List all available tools, in the format expected by the low-level MCP
|
|
190
|
+
server.
|
|
191
|
+
|
|
192
|
+
See `list_tools` for a more ergonomic way to list tools.
|
|
193
|
+
"""
|
|
194
|
+
|
|
195
|
+
tools = self.list_tools()
|
|
183
196
|
|
|
184
|
-
tools = self._tool_manager.list_tools()
|
|
185
197
|
return [
|
|
186
198
|
MCPTool(
|
|
187
199
|
name=info.name,
|
|
@@ -214,10 +226,18 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
214
226
|
converted_result = _convert_to_content(result)
|
|
215
227
|
return converted_result
|
|
216
228
|
|
|
217
|
-
|
|
218
|
-
|
|
229
|
+
def list_resources(self) -> list[Resource]:
|
|
230
|
+
return self._resource_manager.list_resources()
|
|
231
|
+
|
|
232
|
+
async def _mcp_list_resources(self) -> list[MCPResource]:
|
|
233
|
+
"""
|
|
234
|
+
List all available resources, in the format expected by the low-level MCP
|
|
235
|
+
server.
|
|
236
|
+
|
|
237
|
+
See `list_resources` for a more ergonomic way to list resources.
|
|
238
|
+
"""
|
|
219
239
|
|
|
220
|
-
resources = self.
|
|
240
|
+
resources = self.list_resources()
|
|
221
241
|
return [
|
|
222
242
|
MCPResource(
|
|
223
243
|
uri=resource.uri,
|
|
@@ -228,8 +248,18 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
228
248
|
for resource in resources
|
|
229
249
|
]
|
|
230
250
|
|
|
231
|
-
|
|
232
|
-
|
|
251
|
+
def list_resource_templates(self) -> list[ResourceTemplate]:
|
|
252
|
+
return self._resource_manager.list_templates()
|
|
253
|
+
|
|
254
|
+
async def _mcp_list_resource_templates(self) -> list[MCPResourceTemplate]:
|
|
255
|
+
"""
|
|
256
|
+
List all available resource templates, in the format expected by the low-level
|
|
257
|
+
MCP server.
|
|
258
|
+
|
|
259
|
+
See `list_resource_templates` for a more ergonomic way to list resource
|
|
260
|
+
templates.
|
|
261
|
+
"""
|
|
262
|
+
templates = self.list_resource_templates()
|
|
233
263
|
return [
|
|
234
264
|
MCPResourceTemplate(
|
|
235
265
|
uriTemplate=template.uri_template,
|
|
@@ -239,15 +269,27 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
239
269
|
for template in templates
|
|
240
270
|
]
|
|
241
271
|
|
|
242
|
-
async def read_resource(self, uri: AnyUrl | str) ->
|
|
272
|
+
async def read_resource(self, uri: AnyUrl | str) -> str | bytes:
|
|
243
273
|
"""Read a resource by URI."""
|
|
274
|
+
resource = await self._resource_manager.get_resource(uri)
|
|
275
|
+
if not resource:
|
|
276
|
+
raise ResourceError(f"Unknown resource: {uri}")
|
|
277
|
+
return await resource.read()
|
|
278
|
+
|
|
279
|
+
async def _mcp_read_resource(self, uri: AnyUrl | str) -> list[ReadResourceContents]:
|
|
280
|
+
"""
|
|
281
|
+
Read a resource by URI, in the format expected by the low-level MCP
|
|
282
|
+
server.
|
|
283
|
+
|
|
284
|
+
See `read_resource` for a more ergonomic way to read resources.
|
|
285
|
+
"""
|
|
244
286
|
|
|
245
287
|
resource = await self._resource_manager.get_resource(uri)
|
|
246
288
|
if not resource:
|
|
247
289
|
raise ResourceError(f"Unknown resource: {uri}")
|
|
248
290
|
|
|
249
291
|
try:
|
|
250
|
-
content = await
|
|
292
|
+
content = await self.read_resource(uri)
|
|
251
293
|
return [ReadResourceContents(content=content, mime_type=resource.mime_type)]
|
|
252
294
|
except Exception as e:
|
|
253
295
|
logger.error(f"Error reading resource {uri}: {e}")
|
|
@@ -307,6 +349,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
307
349
|
await context.report_progress(50, 100)
|
|
308
350
|
return str(x)
|
|
309
351
|
"""
|
|
352
|
+
|
|
310
353
|
# Check if user passed function directly instead of calling decorator
|
|
311
354
|
if callable(name):
|
|
312
355
|
raise TypeError(
|
|
@@ -316,7 +359,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
316
359
|
|
|
317
360
|
def decorator(fn: AnyFunction) -> AnyFunction:
|
|
318
361
|
self.add_tool(fn, name=name, description=description, tags=tags)
|
|
319
|
-
return fn
|
|
362
|
+
return DecoratedFunction(fn)
|
|
320
363
|
|
|
321
364
|
return decorator
|
|
322
365
|
|
|
@@ -326,8 +369,40 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
326
369
|
Args:
|
|
327
370
|
resource: A Resource instance to add
|
|
328
371
|
"""
|
|
372
|
+
|
|
329
373
|
self._resource_manager.add_resource(resource)
|
|
330
374
|
|
|
375
|
+
def add_resource_fn(
|
|
376
|
+
self,
|
|
377
|
+
fn: AnyFunction,
|
|
378
|
+
uri: str,
|
|
379
|
+
name: str | None = None,
|
|
380
|
+
description: str | None = None,
|
|
381
|
+
mime_type: str | None = None,
|
|
382
|
+
tags: set[str] | None = None,
|
|
383
|
+
) -> None:
|
|
384
|
+
"""Add a resource or template to the server from a function.
|
|
385
|
+
|
|
386
|
+
If the URI contains parameters (e.g. "resource://{param}") or the function
|
|
387
|
+
has parameters, it will be registered as a template resource.
|
|
388
|
+
|
|
389
|
+
Args:
|
|
390
|
+
fn: The function to register as a resource
|
|
391
|
+
uri: The URI for the resource
|
|
392
|
+
name: Optional name for the resource
|
|
393
|
+
description: Optional description of the resource
|
|
394
|
+
mime_type: Optional MIME type for the resource
|
|
395
|
+
tags: Optional set of tags for categorizing the resource
|
|
396
|
+
"""
|
|
397
|
+
self._resource_manager.add_resource_or_template_from_fn(
|
|
398
|
+
fn=fn,
|
|
399
|
+
uri=uri,
|
|
400
|
+
name=name,
|
|
401
|
+
description=description,
|
|
402
|
+
mime_type=mime_type,
|
|
403
|
+
tags=tags,
|
|
404
|
+
)
|
|
405
|
+
|
|
331
406
|
def resource(
|
|
332
407
|
self,
|
|
333
408
|
uri: str,
|
|
@@ -382,52 +457,36 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
382
457
|
)
|
|
383
458
|
|
|
384
459
|
def decorator(fn: AnyFunction) -> AnyFunction:
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
if uri_params != func_params:
|
|
395
|
-
raise ValueError(
|
|
396
|
-
f"Mismatch between URI parameters {uri_params} "
|
|
397
|
-
f"and function parameters {func_params}"
|
|
398
|
-
)
|
|
399
|
-
|
|
400
|
-
# Register as template
|
|
401
|
-
self._resource_manager.add_template_from_fn(
|
|
402
|
-
fn=fn,
|
|
403
|
-
uri_template=uri,
|
|
404
|
-
name=name,
|
|
405
|
-
description=description,
|
|
406
|
-
mime_type=mime_type or "text/plain",
|
|
407
|
-
tags=tags,
|
|
408
|
-
)
|
|
409
|
-
else:
|
|
410
|
-
# Register as regular resource
|
|
411
|
-
resource = FunctionResource(
|
|
412
|
-
uri=AnyUrl(uri),
|
|
413
|
-
name=name,
|
|
414
|
-
description=description,
|
|
415
|
-
mime_type=mime_type or "text/plain",
|
|
416
|
-
fn=fn,
|
|
417
|
-
tags=tags or set(), # Default to empty set if None
|
|
418
|
-
)
|
|
419
|
-
self.add_resource(resource)
|
|
420
|
-
return fn
|
|
460
|
+
self._resource_manager.add_resource_or_template_from_fn(
|
|
461
|
+
fn=fn,
|
|
462
|
+
uri=uri,
|
|
463
|
+
name=name,
|
|
464
|
+
description=description,
|
|
465
|
+
mime_type=mime_type,
|
|
466
|
+
tags=tags,
|
|
467
|
+
)
|
|
468
|
+
return DecoratedFunction(fn)
|
|
421
469
|
|
|
422
470
|
return decorator
|
|
423
471
|
|
|
424
|
-
def add_prompt(
|
|
472
|
+
def add_prompt(
|
|
473
|
+
self,
|
|
474
|
+
fn: Callable[..., PromptResult | Awaitable[PromptResult]],
|
|
475
|
+
name: str | None = None,
|
|
476
|
+
description: str | None = None,
|
|
477
|
+
tags: set[str] | None = None,
|
|
478
|
+
) -> None:
|
|
425
479
|
"""Add a prompt to the server.
|
|
426
480
|
|
|
427
481
|
Args:
|
|
428
482
|
prompt: A Prompt instance to add
|
|
429
483
|
"""
|
|
430
|
-
self._prompt_manager.
|
|
484
|
+
self._prompt_manager.add_prompt_from_fn(
|
|
485
|
+
fn=fn,
|
|
486
|
+
name=name,
|
|
487
|
+
description=description,
|
|
488
|
+
tags=tags,
|
|
489
|
+
)
|
|
431
490
|
|
|
432
491
|
def prompt(
|
|
433
492
|
self,
|
|
@@ -477,11 +536,8 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
477
536
|
)
|
|
478
537
|
|
|
479
538
|
def decorator(func: AnyFunction) -> AnyFunction:
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
)
|
|
483
|
-
self.add_prompt(prompt)
|
|
484
|
-
return func
|
|
539
|
+
self.add_prompt(func, name=name, description=description, tags=tags)
|
|
540
|
+
return DecoratedFunction(func)
|
|
485
541
|
|
|
486
542
|
return decorator
|
|
487
543
|
|
|
@@ -494,15 +550,20 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
494
550
|
self._mcp_server.create_initialization_options(),
|
|
495
551
|
)
|
|
496
552
|
|
|
497
|
-
async def run_sse_async(
|
|
553
|
+
async def run_sse_async(
|
|
554
|
+
self,
|
|
555
|
+
host: str | None = None,
|
|
556
|
+
port: int | None = None,
|
|
557
|
+
log_level: str | None = None,
|
|
558
|
+
) -> None:
|
|
498
559
|
"""Run the server using SSE transport."""
|
|
499
560
|
starlette_app = self.sse_app()
|
|
500
561
|
|
|
501
562
|
config = uvicorn.Config(
|
|
502
563
|
starlette_app,
|
|
503
|
-
host=self.settings.host,
|
|
504
|
-
port=self.settings.port,
|
|
505
|
-
log_level=self.settings.log_level.lower(),
|
|
564
|
+
host=host or self.settings.host,
|
|
565
|
+
port=port or self.settings.port,
|
|
566
|
+
log_level=log_level or self.settings.log_level.lower(),
|
|
506
567
|
)
|
|
507
568
|
server = uvicorn.Server(config)
|
|
508
569
|
await server.serve()
|
|
@@ -531,9 +592,20 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
531
592
|
],
|
|
532
593
|
)
|
|
533
594
|
|
|
534
|
-
|
|
535
|
-
"""
|
|
536
|
-
|
|
595
|
+
def list_prompts(self) -> list[Prompt]:
|
|
596
|
+
"""
|
|
597
|
+
List all available prompts.
|
|
598
|
+
"""
|
|
599
|
+
return self._prompt_manager.list_prompts()
|
|
600
|
+
|
|
601
|
+
async def _mcp_list_prompts(self) -> list[MCPPrompt]:
|
|
602
|
+
"""
|
|
603
|
+
List all available prompts, in the format expected by the low-level MCP
|
|
604
|
+
server.
|
|
605
|
+
|
|
606
|
+
See `list_prompts` for a more ergonomic way to list prompts.
|
|
607
|
+
"""
|
|
608
|
+
prompts = self.list_prompts()
|
|
537
609
|
return [
|
|
538
610
|
MCPPrompt(
|
|
539
611
|
name=prompt.name,
|
|
@@ -552,10 +624,21 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
552
624
|
|
|
553
625
|
async def get_prompt(
|
|
554
626
|
self, name: str, arguments: dict[str, Any] | None = None
|
|
555
|
-
) ->
|
|
627
|
+
) -> list[Message]:
|
|
556
628
|
"""Get a prompt by name with arguments."""
|
|
629
|
+
return await self._prompt_manager.render_prompt(name, arguments)
|
|
630
|
+
|
|
631
|
+
async def _mcp_get_prompt(
|
|
632
|
+
self, name: str, arguments: dict[str, Any] | None = None
|
|
633
|
+
) -> GetPromptResult:
|
|
634
|
+
"""
|
|
635
|
+
Get a prompt by name with arguments, in the format expected by the low-level
|
|
636
|
+
MCP server.
|
|
637
|
+
|
|
638
|
+
See `get_prompt` for a more ergonomic way to get prompts.
|
|
639
|
+
"""
|
|
557
640
|
try:
|
|
558
|
-
messages = await self.
|
|
641
|
+
messages = await self.get_prompt(name, arguments)
|
|
559
642
|
|
|
560
643
|
return GetPromptResult(messages=pydantic_core.to_jsonable_python(messages))
|
|
561
644
|
except Exception as e:
|
fastmcp/tools/tool.py
CHANGED
|
@@ -58,7 +58,10 @@ class Tool(BaseModel):
|
|
|
58
58
|
is_async = inspect.iscoroutinefunction(fn)
|
|
59
59
|
|
|
60
60
|
if context_kwarg is None:
|
|
61
|
-
|
|
61
|
+
if isinstance(fn, classmethod):
|
|
62
|
+
sig = inspect.signature(fn.__func__)
|
|
63
|
+
else:
|
|
64
|
+
sig = inspect.signature(fn)
|
|
62
65
|
for param_name, param in sig.parameters.items():
|
|
63
66
|
if param.annotation is Context:
|
|
64
67
|
context_kwarg = param_name
|