fastmcp 2.6.1__py3-none-any.whl → 2.7.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 +10 -2
- fastmcp/cli/run.py +32 -1
- fastmcp/client/transports.py +21 -13
- fastmcp/contrib/bulk_tool_caller/example.py +1 -1
- fastmcp/contrib/mcp_mixin/mcp_mixin.py +10 -3
- fastmcp/prompts/prompt.py +68 -30
- fastmcp/prompts/prompt_manager.py +13 -6
- fastmcp/resources/__init__.py +1 -2
- fastmcp/resources/resource.py +92 -6
- fastmcp/resources/resource_manager.py +17 -6
- fastmcp/resources/template.py +90 -56
- fastmcp/resources/types.py +0 -44
- fastmcp/server/context.py +1 -1
- fastmcp/server/dependencies.py +1 -0
- fastmcp/server/http.py +2 -1
- fastmcp/server/openapi.py +17 -32
- fastmcp/server/proxy.py +5 -8
- fastmcp/server/server.py +280 -95
- fastmcp/settings.py +1 -1
- fastmcp/tools/__init__.py +2 -2
- fastmcp/tools/tool.py +62 -22
- fastmcp/tools/tool_manager.py +9 -3
- fastmcp/utilities/mcp_config.py +17 -7
- fastmcp/utilities/openapi.py +56 -32
- fastmcp/utilities/types.py +7 -1
- {fastmcp-2.6.1.dist-info → fastmcp-2.7.1.dist-info}/METADATA +10 -9
- {fastmcp-2.6.1.dist-info → fastmcp-2.7.1.dist-info}/RECORD +30 -31
- fastmcp/utilities/decorators.py +0 -101
- {fastmcp-2.6.1.dist-info → fastmcp-2.7.1.dist-info}/WHEEL +0 -0
- {fastmcp-2.6.1.dist-info → fastmcp-2.7.1.dist-info}/entry_points.txt +0 -0
- {fastmcp-2.6.1.dist-info → fastmcp-2.7.1.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
"""Resource manager functionality."""
|
|
2
2
|
|
|
3
3
|
import inspect
|
|
4
|
+
import warnings
|
|
4
5
|
from collections.abc import Callable
|
|
5
6
|
from typing import Any
|
|
6
7
|
|
|
7
8
|
from pydantic import AnyUrl
|
|
8
9
|
|
|
9
10
|
from fastmcp.exceptions import NotFoundError, ResourceError
|
|
10
|
-
from fastmcp.resources import FunctionResource
|
|
11
11
|
from fastmcp.resources.resource import Resource
|
|
12
12
|
from fastmcp.resources.template import (
|
|
13
13
|
ResourceTemplate,
|
|
@@ -121,13 +121,19 @@ class ResourceManager:
|
|
|
121
121
|
The added resource. If a resource with the same URI already exists,
|
|
122
122
|
returns the existing resource.
|
|
123
123
|
"""
|
|
124
|
-
|
|
124
|
+
# deprecated in 2.7.0
|
|
125
|
+
warnings.warn(
|
|
126
|
+
"add_resource_from_fn is deprecated. Use Resource.from_function() and call add_resource() instead.",
|
|
127
|
+
DeprecationWarning,
|
|
128
|
+
stacklevel=2,
|
|
129
|
+
)
|
|
130
|
+
resource = Resource.from_function(
|
|
125
131
|
fn=fn,
|
|
126
|
-
uri=
|
|
132
|
+
uri=uri,
|
|
127
133
|
name=name,
|
|
128
134
|
description=description,
|
|
129
|
-
mime_type=mime_type
|
|
130
|
-
tags=tags
|
|
135
|
+
mime_type=mime_type,
|
|
136
|
+
tags=tags,
|
|
131
137
|
)
|
|
132
138
|
return self.add_resource(resource)
|
|
133
139
|
|
|
@@ -172,7 +178,12 @@ class ResourceManager:
|
|
|
172
178
|
tags: set[str] | None = None,
|
|
173
179
|
) -> ResourceTemplate:
|
|
174
180
|
"""Create a template from a function."""
|
|
175
|
-
|
|
181
|
+
# deprecated in 2.7.0
|
|
182
|
+
warnings.warn(
|
|
183
|
+
"add_template_from_fn is deprecated. Use ResourceTemplate.from_function() and call add_template() instead.",
|
|
184
|
+
DeprecationWarning,
|
|
185
|
+
stacklevel=2,
|
|
186
|
+
)
|
|
176
187
|
template = ResourceTemplate.from_function(
|
|
177
188
|
fn,
|
|
178
189
|
uri_template=uri_template,
|
fastmcp/resources/template.py
CHANGED
|
@@ -10,18 +10,17 @@ from urllib.parse import unquote
|
|
|
10
10
|
|
|
11
11
|
from mcp.types import ResourceTemplate as MCPResourceTemplate
|
|
12
12
|
from pydantic import (
|
|
13
|
-
AnyUrl,
|
|
14
|
-
BaseModel,
|
|
15
13
|
BeforeValidator,
|
|
16
14
|
Field,
|
|
17
15
|
field_validator,
|
|
18
16
|
validate_call,
|
|
19
17
|
)
|
|
20
18
|
|
|
21
|
-
from fastmcp.resources.types import
|
|
19
|
+
from fastmcp.resources.types import Resource
|
|
22
20
|
from fastmcp.server.dependencies import get_context
|
|
23
21
|
from fastmcp.utilities.json_schema import compress_schema
|
|
24
22
|
from fastmcp.utilities.types import (
|
|
23
|
+
FastMCPBaseModel,
|
|
25
24
|
_convert_set_defaults,
|
|
26
25
|
find_kwarg_by_type,
|
|
27
26
|
get_cached_typeadapter,
|
|
@@ -52,12 +51,7 @@ def match_uri_template(uri: str, uri_template: str) -> dict[str, str] | None:
|
|
|
52
51
|
return None
|
|
53
52
|
|
|
54
53
|
|
|
55
|
-
class
|
|
56
|
-
key: str
|
|
57
|
-
value: int
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
class ResourceTemplate(BaseModel):
|
|
54
|
+
class ResourceTemplate(FastMCPBaseModel):
|
|
61
55
|
"""A template for dynamically creating resources."""
|
|
62
56
|
|
|
63
57
|
uri_template: str = Field(
|
|
@@ -71,11 +65,28 @@ class ResourceTemplate(BaseModel):
|
|
|
71
65
|
mime_type: str = Field(
|
|
72
66
|
default="text/plain", description="MIME type of the resource content"
|
|
73
67
|
)
|
|
74
|
-
fn: Callable[..., Any]
|
|
75
68
|
parameters: dict[str, Any] = Field(
|
|
76
69
|
description="JSON schema for function parameters"
|
|
77
70
|
)
|
|
78
71
|
|
|
72
|
+
@staticmethod
|
|
73
|
+
def from_function(
|
|
74
|
+
fn: Callable[..., Any],
|
|
75
|
+
uri_template: str,
|
|
76
|
+
name: str | None = None,
|
|
77
|
+
description: str | None = None,
|
|
78
|
+
mime_type: str | None = None,
|
|
79
|
+
tags: set[str] | None = None,
|
|
80
|
+
) -> FunctionResourceTemplate:
|
|
81
|
+
return FunctionResourceTemplate.from_function(
|
|
82
|
+
fn=fn,
|
|
83
|
+
uri_template=uri_template,
|
|
84
|
+
name=name,
|
|
85
|
+
description=description,
|
|
86
|
+
mime_type=mime_type,
|
|
87
|
+
tags=tags,
|
|
88
|
+
)
|
|
89
|
+
|
|
79
90
|
@field_validator("mime_type", mode="before")
|
|
80
91
|
@classmethod
|
|
81
92
|
def set_default_mime_type(cls, mime_type: str | None) -> str:
|
|
@@ -84,6 +95,70 @@ class ResourceTemplate(BaseModel):
|
|
|
84
95
|
return mime_type
|
|
85
96
|
return "text/plain"
|
|
86
97
|
|
|
98
|
+
def matches(self, uri: str) -> dict[str, Any] | None:
|
|
99
|
+
"""Check if URI matches template and extract parameters."""
|
|
100
|
+
return match_uri_template(uri, self.uri_template)
|
|
101
|
+
|
|
102
|
+
async def read(self, arguments: dict[str, Any]) -> str | bytes:
|
|
103
|
+
"""Read the resource content."""
|
|
104
|
+
raise NotImplementedError(
|
|
105
|
+
"Subclasses must implement read() or override create_resource()"
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
async def create_resource(self, uri: str, params: dict[str, Any]) -> Resource:
|
|
109
|
+
"""Create a resource from the template with the given parameters."""
|
|
110
|
+
|
|
111
|
+
async def resource_read_fn() -> str | bytes:
|
|
112
|
+
# Call function and check if result is a coroutine
|
|
113
|
+
result = await self.read(arguments=params)
|
|
114
|
+
return result
|
|
115
|
+
|
|
116
|
+
return Resource.from_function(
|
|
117
|
+
fn=resource_read_fn,
|
|
118
|
+
uri=uri,
|
|
119
|
+
name=self.name,
|
|
120
|
+
description=self.description,
|
|
121
|
+
mime_type=self.mime_type,
|
|
122
|
+
tags=self.tags,
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
def __eq__(self, other: object) -> bool:
|
|
126
|
+
if type(self) is not type(other):
|
|
127
|
+
return False
|
|
128
|
+
assert isinstance(other, type(self))
|
|
129
|
+
return self.model_dump() == other.model_dump()
|
|
130
|
+
|
|
131
|
+
def to_mcp_template(self, **overrides: Any) -> MCPResourceTemplate:
|
|
132
|
+
"""Convert the resource template to an MCPResourceTemplate."""
|
|
133
|
+
kwargs = {
|
|
134
|
+
"uriTemplate": self.uri_template,
|
|
135
|
+
"name": self.name,
|
|
136
|
+
"description": self.description,
|
|
137
|
+
"mimeType": self.mime_type,
|
|
138
|
+
}
|
|
139
|
+
return MCPResourceTemplate(**kwargs | overrides)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
class FunctionResourceTemplate(ResourceTemplate):
|
|
143
|
+
"""A template for dynamically creating resources."""
|
|
144
|
+
|
|
145
|
+
fn: Callable[..., Any]
|
|
146
|
+
|
|
147
|
+
async def read(self, arguments: dict[str, Any]) -> str | bytes:
|
|
148
|
+
"""Read the resource content."""
|
|
149
|
+
from fastmcp.server.context import Context
|
|
150
|
+
|
|
151
|
+
# Add context to parameters if needed
|
|
152
|
+
kwargs = arguments.copy()
|
|
153
|
+
context_kwarg = find_kwarg_by_type(self.fn, kwarg_type=Context)
|
|
154
|
+
if context_kwarg and context_kwarg not in kwargs:
|
|
155
|
+
kwargs[context_kwarg] = get_context()
|
|
156
|
+
|
|
157
|
+
result = self.fn(**kwargs)
|
|
158
|
+
if inspect.iscoroutine(result):
|
|
159
|
+
result = await result
|
|
160
|
+
return result
|
|
161
|
+
|
|
87
162
|
@classmethod
|
|
88
163
|
def from_function(
|
|
89
164
|
cls,
|
|
@@ -93,7 +168,7 @@ class ResourceTemplate(BaseModel):
|
|
|
93
168
|
description: str | None = None,
|
|
94
169
|
mime_type: str | None = None,
|
|
95
170
|
tags: set[str] | None = None,
|
|
96
|
-
) ->
|
|
171
|
+
) -> FunctionResourceTemplate:
|
|
97
172
|
"""Create a template from a function."""
|
|
98
173
|
from fastmcp.server.context import Context
|
|
99
174
|
|
|
@@ -150,8 +225,12 @@ class ResourceTemplate(BaseModel):
|
|
|
150
225
|
|
|
151
226
|
description = description or fn.__doc__
|
|
152
227
|
|
|
228
|
+
# if the fn is a callable class, we need to get the __call__ method from here out
|
|
153
229
|
if not inspect.isroutine(fn):
|
|
154
230
|
fn = fn.__call__
|
|
231
|
+
# if the fn is a staticmethod, we need to work with the underlying function
|
|
232
|
+
if isinstance(fn, staticmethod):
|
|
233
|
+
fn = fn.__func__
|
|
155
234
|
|
|
156
235
|
type_adapter = get_cached_typeadapter(fn)
|
|
157
236
|
parameters = type_adapter.json_schema()
|
|
@@ -172,48 +251,3 @@ class ResourceTemplate(BaseModel):
|
|
|
172
251
|
parameters=parameters,
|
|
173
252
|
tags=tags or set(),
|
|
174
253
|
)
|
|
175
|
-
|
|
176
|
-
def matches(self, uri: str) -> dict[str, Any] | None:
|
|
177
|
-
"""Check if URI matches template and extract parameters."""
|
|
178
|
-
return match_uri_template(uri, self.uri_template)
|
|
179
|
-
|
|
180
|
-
async def create_resource(self, uri: str, params: dict[str, Any]) -> Resource:
|
|
181
|
-
"""Create a resource from the template with the given parameters."""
|
|
182
|
-
from fastmcp.server.context import Context
|
|
183
|
-
|
|
184
|
-
# Add context to parameters if needed
|
|
185
|
-
kwargs = params.copy()
|
|
186
|
-
context_kwarg = find_kwarg_by_type(self.fn, kwarg_type=Context)
|
|
187
|
-
if context_kwarg and context_kwarg not in kwargs:
|
|
188
|
-
kwargs[context_kwarg] = get_context()
|
|
189
|
-
|
|
190
|
-
async def resource_read_fn() -> str | bytes:
|
|
191
|
-
# Call function and check if result is a coroutine
|
|
192
|
-
result = self.fn(**kwargs)
|
|
193
|
-
if inspect.iscoroutine(result):
|
|
194
|
-
result = await result
|
|
195
|
-
return result
|
|
196
|
-
|
|
197
|
-
return FunctionResource(
|
|
198
|
-
uri=AnyUrl(uri), # Explicitly convert to AnyUrl
|
|
199
|
-
name=self.name,
|
|
200
|
-
description=self.description,
|
|
201
|
-
mime_type=self.mime_type,
|
|
202
|
-
fn=resource_read_fn,
|
|
203
|
-
tags=self.tags,
|
|
204
|
-
)
|
|
205
|
-
|
|
206
|
-
def __eq__(self, other: object) -> bool:
|
|
207
|
-
if not isinstance(other, ResourceTemplate):
|
|
208
|
-
return False
|
|
209
|
-
return self.model_dump() == other.model_dump()
|
|
210
|
-
|
|
211
|
-
def to_mcp_template(self, **overrides: Any) -> MCPResourceTemplate:
|
|
212
|
-
"""Convert the resource template to an MCPResourceTemplate."""
|
|
213
|
-
kwargs = {
|
|
214
|
-
"uriTemplate": self.uri_template,
|
|
215
|
-
"name": self.name,
|
|
216
|
-
"description": self.description,
|
|
217
|
-
"mimeType": self.mime_type,
|
|
218
|
-
}
|
|
219
|
-
return MCPResourceTemplate(**kwargs | overrides)
|
fastmcp/resources/types.py
CHANGED
|
@@ -2,24 +2,18 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
-
import inspect
|
|
6
5
|
import json
|
|
7
|
-
from collections.abc import Callable
|
|
8
6
|
from pathlib import Path
|
|
9
|
-
from typing import Any
|
|
10
7
|
|
|
11
8
|
import anyio
|
|
12
9
|
import anyio.to_thread
|
|
13
10
|
import httpx
|
|
14
11
|
import pydantic.json
|
|
15
|
-
import pydantic_core
|
|
16
12
|
from pydantic import Field, ValidationInfo
|
|
17
13
|
|
|
18
14
|
from fastmcp.exceptions import ResourceError
|
|
19
15
|
from fastmcp.resources.resource import Resource
|
|
20
|
-
from fastmcp.server.dependencies import get_context
|
|
21
16
|
from fastmcp.utilities.logging import get_logger
|
|
22
|
-
from fastmcp.utilities.types import find_kwarg_by_type
|
|
23
17
|
|
|
24
18
|
logger = get_logger(__name__)
|
|
25
19
|
|
|
@@ -44,44 +38,6 @@ class BinaryResource(Resource):
|
|
|
44
38
|
return self.data
|
|
45
39
|
|
|
46
40
|
|
|
47
|
-
class FunctionResource(Resource):
|
|
48
|
-
"""A resource that defers data loading by wrapping a function.
|
|
49
|
-
|
|
50
|
-
The function is only called when the resource is read, allowing for lazy loading
|
|
51
|
-
of potentially expensive data. This is particularly useful when listing resources,
|
|
52
|
-
as the function won't be called until the resource is actually accessed.
|
|
53
|
-
|
|
54
|
-
The function can return:
|
|
55
|
-
- str for text content (default)
|
|
56
|
-
- bytes for binary content
|
|
57
|
-
- other types will be converted to JSON
|
|
58
|
-
"""
|
|
59
|
-
|
|
60
|
-
fn: Callable[[], Any]
|
|
61
|
-
|
|
62
|
-
async def read(self) -> str | bytes:
|
|
63
|
-
"""Read the resource by calling the wrapped function."""
|
|
64
|
-
from fastmcp.server.context import Context
|
|
65
|
-
|
|
66
|
-
kwargs = {}
|
|
67
|
-
context_kwarg = find_kwarg_by_type(self.fn, kwarg_type=Context)
|
|
68
|
-
if context_kwarg is not None:
|
|
69
|
-
kwargs[context_kwarg] = get_context()
|
|
70
|
-
|
|
71
|
-
result = self.fn(**kwargs)
|
|
72
|
-
if inspect.iscoroutinefunction(self.fn):
|
|
73
|
-
result = await result
|
|
74
|
-
|
|
75
|
-
if isinstance(result, Resource):
|
|
76
|
-
return await result.read()
|
|
77
|
-
elif isinstance(result, bytes):
|
|
78
|
-
return result
|
|
79
|
-
elif isinstance(result, str):
|
|
80
|
-
return result
|
|
81
|
-
else:
|
|
82
|
-
return pydantic_core.to_json(result, fallback=str, indent=2).decode()
|
|
83
|
-
|
|
84
|
-
|
|
85
41
|
class FileResource(Resource):
|
|
86
42
|
"""A resource that reads from a file.
|
|
87
43
|
|
fastmcp/server/context.py
CHANGED
|
@@ -49,7 +49,7 @@ class Context:
|
|
|
49
49
|
To use context in a tool function, add a parameter with the Context type annotation:
|
|
50
50
|
|
|
51
51
|
```python
|
|
52
|
-
@server.tool
|
|
52
|
+
@server.tool
|
|
53
53
|
def my_tool(x: int, ctx: Context) -> str:
|
|
54
54
|
# Log messages to the client
|
|
55
55
|
ctx.info(f"Processing {x}")
|
fastmcp/server/dependencies.py
CHANGED
fastmcp/server/http.py
CHANGED
|
@@ -13,6 +13,7 @@ from mcp.server.auth.middleware.bearer_auth import (
|
|
|
13
13
|
from mcp.server.auth.routes import create_auth_routes
|
|
14
14
|
from mcp.server.lowlevel.server import LifespanResultT
|
|
15
15
|
from mcp.server.sse import SseServerTransport
|
|
16
|
+
from mcp.server.streamable_http import EventStore
|
|
16
17
|
from mcp.server.streamable_http_manager import StreamableHTTPSessionManager
|
|
17
18
|
from starlette.applications import Starlette
|
|
18
19
|
from starlette.middleware import Middleware
|
|
@@ -241,7 +242,7 @@ def create_sse_app(
|
|
|
241
242
|
def create_streamable_http_app(
|
|
242
243
|
server: FastMCP[LifespanResultT],
|
|
243
244
|
streamable_http_path: str,
|
|
244
|
-
event_store: None = None,
|
|
245
|
+
event_store: EventStore | None = None,
|
|
245
246
|
auth: OAuthProvider | None = None,
|
|
246
247
|
json_response: bool = False,
|
|
247
248
|
stateless_http: bool = False,
|
fastmcp/server/openapi.py
CHANGED
|
@@ -233,7 +233,6 @@ class OpenAPITool(Tool):
|
|
|
233
233
|
name=name,
|
|
234
234
|
description=description,
|
|
235
235
|
parameters=parameters,
|
|
236
|
-
fn=self._execute_request, # We'll use an instance method instead of a global function
|
|
237
236
|
tags=tags,
|
|
238
237
|
annotations=annotations,
|
|
239
238
|
exclude_args=exclude_args,
|
|
@@ -247,9 +246,10 @@ class OpenAPITool(Tool):
|
|
|
247
246
|
"""Custom representation to prevent recursion errors when printing."""
|
|
248
247
|
return f"OpenAPITool(name={self.name!r}, method={self._route.method}, path={self._route.path})"
|
|
249
248
|
|
|
250
|
-
async def
|
|
249
|
+
async def run(
|
|
250
|
+
self, arguments: dict[str, Any]
|
|
251
|
+
) -> list[TextContent | ImageContent | EmbeddedResource]:
|
|
251
252
|
"""Execute the HTTP request based on the route configuration."""
|
|
252
|
-
context = kwargs.get("context")
|
|
253
253
|
|
|
254
254
|
# Prepare URL
|
|
255
255
|
path = self._route.path
|
|
@@ -258,11 +258,11 @@ class OpenAPITool(Tool):
|
|
|
258
258
|
# Path parameters should never be None as they're typically required
|
|
259
259
|
# but we'll handle that case anyway
|
|
260
260
|
path_params = {
|
|
261
|
-
p.name:
|
|
261
|
+
p.name: arguments.get(p.name)
|
|
262
262
|
for p in self._route.parameters
|
|
263
263
|
if p.location == "path"
|
|
264
|
-
and p.name in
|
|
265
|
-
and
|
|
264
|
+
and p.name in arguments
|
|
265
|
+
and arguments.get(p.name) is not None
|
|
266
266
|
}
|
|
267
267
|
|
|
268
268
|
# Ensure all path parameters are provided
|
|
@@ -340,11 +340,11 @@ class OpenAPITool(Tool):
|
|
|
340
340
|
for p in self._route.parameters:
|
|
341
341
|
if (
|
|
342
342
|
p.location == "query"
|
|
343
|
-
and p.name in
|
|
344
|
-
and
|
|
345
|
-
and
|
|
343
|
+
and p.name in arguments
|
|
344
|
+
and arguments.get(p.name) is not None
|
|
345
|
+
and arguments.get(p.name) != ""
|
|
346
346
|
):
|
|
347
|
-
param_value =
|
|
347
|
+
param_value = arguments.get(p.name)
|
|
348
348
|
|
|
349
349
|
# Format array query parameters as comma-separated strings
|
|
350
350
|
# following OpenAPI form style (default for query parameters)
|
|
@@ -399,10 +399,10 @@ class OpenAPITool(Tool):
|
|
|
399
399
|
for p in self._route.parameters:
|
|
400
400
|
if (
|
|
401
401
|
p.location == "header"
|
|
402
|
-
and p.name in
|
|
403
|
-
and
|
|
402
|
+
and p.name in arguments
|
|
403
|
+
and arguments[p.name] is not None
|
|
404
404
|
):
|
|
405
|
-
openapi_headers[p.name.lower()] = str(
|
|
405
|
+
openapi_headers[p.name.lower()] = str(arguments[p.name])
|
|
406
406
|
headers.update(openapi_headers)
|
|
407
407
|
|
|
408
408
|
# Add headers from the current MCP client HTTP request (these take precedence)
|
|
@@ -420,21 +420,13 @@ class OpenAPITool(Tool):
|
|
|
420
420
|
}
|
|
421
421
|
body_params = {
|
|
422
422
|
k: v
|
|
423
|
-
for k, v in
|
|
423
|
+
for k, v in arguments.items()
|
|
424
424
|
if k not in path_query_header_params and k != "context"
|
|
425
425
|
}
|
|
426
426
|
|
|
427
427
|
if body_params:
|
|
428
428
|
json_data = body_params
|
|
429
429
|
|
|
430
|
-
# Log the request details if a context is available
|
|
431
|
-
if context:
|
|
432
|
-
try:
|
|
433
|
-
await context.info(f"Making {self._route.method} request to {path}")
|
|
434
|
-
except (ValueError, AttributeError):
|
|
435
|
-
# Silently continue if context logging is not available
|
|
436
|
-
pass
|
|
437
|
-
|
|
438
430
|
# Execute the request
|
|
439
431
|
try:
|
|
440
432
|
response = await self._client.request(
|
|
@@ -451,10 +443,11 @@ class OpenAPITool(Tool):
|
|
|
451
443
|
|
|
452
444
|
# Try to parse as JSON first
|
|
453
445
|
try:
|
|
454
|
-
|
|
446
|
+
result = response.json()
|
|
455
447
|
except (json.JSONDecodeError, ValueError):
|
|
456
448
|
# Return text content if not JSON
|
|
457
|
-
|
|
449
|
+
result = response.text
|
|
450
|
+
return _convert_to_content(result)
|
|
458
451
|
|
|
459
452
|
except httpx.HTTPStatusError as e:
|
|
460
453
|
# Handle HTTP errors (4xx, 5xx)
|
|
@@ -474,13 +467,6 @@ class OpenAPITool(Tool):
|
|
|
474
467
|
# Handle request errors (connection, timeout, etc.)
|
|
475
468
|
raise ValueError(f"Request error: {str(e)}")
|
|
476
469
|
|
|
477
|
-
async def run(
|
|
478
|
-
self, arguments: dict[str, Any]
|
|
479
|
-
) -> list[TextContent | ImageContent | EmbeddedResource]:
|
|
480
|
-
"""Run the tool with arguments and optional context."""
|
|
481
|
-
response = await self._execute_request(**arguments)
|
|
482
|
-
return _convert_to_content(response)
|
|
483
|
-
|
|
484
470
|
|
|
485
471
|
class OpenAPIResource(Resource):
|
|
486
472
|
"""Resource implementation for OpenAPI endpoints."""
|
|
@@ -619,7 +605,6 @@ class OpenAPIResourceTemplate(ResourceTemplate):
|
|
|
619
605
|
uri_template=uri_template,
|
|
620
606
|
name=name,
|
|
621
607
|
description=description,
|
|
622
|
-
fn=lambda **kwargs: None,
|
|
623
608
|
parameters=parameters,
|
|
624
609
|
tags=tags,
|
|
625
610
|
)
|
fastmcp/server/proxy.py
CHANGED
|
@@ -32,10 +32,6 @@ if TYPE_CHECKING:
|
|
|
32
32
|
logger = get_logger(__name__)
|
|
33
33
|
|
|
34
34
|
|
|
35
|
-
def _proxy_passthrough():
|
|
36
|
-
pass
|
|
37
|
-
|
|
38
|
-
|
|
39
35
|
class ProxyTool(Tool):
|
|
40
36
|
def __init__(self, client: Client, **kwargs):
|
|
41
37
|
super().__init__(**kwargs)
|
|
@@ -48,7 +44,6 @@ class ProxyTool(Tool):
|
|
|
48
44
|
name=tool.name,
|
|
49
45
|
description=tool.description,
|
|
50
46
|
parameters=tool.inputSchema,
|
|
51
|
-
fn=_proxy_passthrough,
|
|
52
47
|
)
|
|
53
48
|
|
|
54
49
|
async def run(
|
|
@@ -69,6 +64,9 @@ class ProxyTool(Tool):
|
|
|
69
64
|
|
|
70
65
|
|
|
71
66
|
class ProxyResource(Resource):
|
|
67
|
+
_client: Client
|
|
68
|
+
_value: str | bytes | None = None
|
|
69
|
+
|
|
72
70
|
def __init__(self, client: Client, *, _value: str | bytes | None = None, **kwargs):
|
|
73
71
|
super().__init__(**kwargs)
|
|
74
72
|
self._client = client
|
|
@@ -114,7 +112,6 @@ class ProxyTemplate(ResourceTemplate):
|
|
|
114
112
|
uri_template=template.uriTemplate,
|
|
115
113
|
name=template.name,
|
|
116
114
|
description=template.description,
|
|
117
|
-
fn=_proxy_passthrough,
|
|
118
115
|
parameters={},
|
|
119
116
|
)
|
|
120
117
|
|
|
@@ -146,12 +143,13 @@ class ProxyTemplate(ResourceTemplate):
|
|
|
146
143
|
name=self.name,
|
|
147
144
|
description=self.description,
|
|
148
145
|
mime_type=result[0].mimeType,
|
|
149
|
-
contents=result,
|
|
150
146
|
_value=value,
|
|
151
147
|
)
|
|
152
148
|
|
|
153
149
|
|
|
154
150
|
class ProxyPrompt(Prompt):
|
|
151
|
+
_client: Client
|
|
152
|
+
|
|
155
153
|
def __init__(self, client: Client, **kwargs):
|
|
156
154
|
super().__init__(**kwargs)
|
|
157
155
|
self._client = client
|
|
@@ -163,7 +161,6 @@ class ProxyPrompt(Prompt):
|
|
|
163
161
|
name=prompt.name,
|
|
164
162
|
description=prompt.description,
|
|
165
163
|
arguments=[a.model_dump() for a in prompt.arguments or []],
|
|
166
|
-
fn=_proxy_passthrough,
|
|
167
164
|
)
|
|
168
165
|
|
|
169
166
|
async def render(self, arguments: dict[str, Any]) -> list[PromptMessage]:
|