fastmcp 2.3.3__py3-none-any.whl → 2.3.5__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 +30 -138
- fastmcp/cli/run.py +179 -0
- fastmcp/client/__init__.py +2 -0
- fastmcp/client/client.py +131 -24
- fastmcp/client/logging.py +8 -0
- fastmcp/client/progress.py +38 -0
- fastmcp/client/transports.py +80 -64
- fastmcp/exceptions.py +2 -0
- fastmcp/prompts/prompt.py +12 -6
- fastmcp/resources/resource_manager.py +22 -1
- fastmcp/resources/template.py +21 -17
- fastmcp/resources/types.py +25 -27
- fastmcp/server/context.py +6 -3
- fastmcp/server/http.py +47 -14
- fastmcp/server/openapi.py +14 -1
- fastmcp/server/proxy.py +4 -4
- fastmcp/server/server.py +159 -96
- fastmcp/settings.py +55 -29
- fastmcp/tools/tool.py +45 -45
- fastmcp/tools/tool_manager.py +27 -2
- fastmcp/utilities/exceptions.py +49 -0
- fastmcp/utilities/json_schema.py +78 -17
- fastmcp/utilities/logging.py +11 -6
- fastmcp/utilities/openapi.py +122 -7
- {fastmcp-2.3.3.dist-info → fastmcp-2.3.5.dist-info}/METADATA +3 -3
- {fastmcp-2.3.3.dist-info → fastmcp-2.3.5.dist-info}/RECORD +29 -27
- fastmcp/low_level/sse_server_transport.py +0 -104
- {fastmcp-2.3.3.dist-info → fastmcp-2.3.5.dist-info}/WHEEL +0 -0
- {fastmcp-2.3.3.dist-info → fastmcp-2.3.5.dist-info}/entry_points.txt +0 -0
- {fastmcp-2.3.3.dist-info → fastmcp-2.3.5.dist-info}/licenses/LICENSE +0 -0
|
@@ -6,7 +6,7 @@ from typing import Any
|
|
|
6
6
|
|
|
7
7
|
from pydantic import AnyUrl
|
|
8
8
|
|
|
9
|
-
from fastmcp.exceptions import NotFoundError
|
|
9
|
+
from fastmcp.exceptions import NotFoundError, ResourceError
|
|
10
10
|
from fastmcp.resources import FunctionResource
|
|
11
11
|
from fastmcp.resources.resource import Resource
|
|
12
12
|
from fastmcp.resources.template import (
|
|
@@ -244,11 +244,32 @@ class ResourceManager:
|
|
|
244
244
|
uri_str,
|
|
245
245
|
params=params,
|
|
246
246
|
)
|
|
247
|
+
except ResourceError as e:
|
|
248
|
+
logger.error(f"Error creating resource from template: {e}")
|
|
249
|
+
raise e
|
|
247
250
|
except Exception as e:
|
|
251
|
+
logger.error(f"Error creating resource from template: {e}")
|
|
248
252
|
raise ValueError(f"Error creating resource from template: {e}")
|
|
249
253
|
|
|
250
254
|
raise NotFoundError(f"Unknown resource: {uri_str}")
|
|
251
255
|
|
|
256
|
+
async def read_resource(self, uri: AnyUrl | str) -> str | bytes:
|
|
257
|
+
"""Read a resource contents."""
|
|
258
|
+
resource = await self.get_resource(uri)
|
|
259
|
+
|
|
260
|
+
try:
|
|
261
|
+
return await resource.read()
|
|
262
|
+
|
|
263
|
+
# raise ResourceErrors as-is
|
|
264
|
+
except ResourceError as e:
|
|
265
|
+
logger.error(f"Error reading resource {uri!r}: {e}")
|
|
266
|
+
raise e
|
|
267
|
+
|
|
268
|
+
# raise other exceptions as ResourceErrors without revealing internal details
|
|
269
|
+
except Exception as e:
|
|
270
|
+
logger.error(f"Error reading resource {uri!r}: {e}")
|
|
271
|
+
raise ResourceError(f"Error reading resource {uri!r}") from e
|
|
272
|
+
|
|
252
273
|
def get_resources(self) -> dict[str, Resource]:
|
|
253
274
|
"""Get all registered resources, keyed by URI."""
|
|
254
275
|
return self._resources
|
fastmcp/resources/template.py
CHANGED
|
@@ -21,6 +21,7 @@ from pydantic import (
|
|
|
21
21
|
|
|
22
22
|
from fastmcp.resources.types import FunctionResource, Resource
|
|
23
23
|
from fastmcp.server.dependencies import get_context
|
|
24
|
+
from fastmcp.utilities.json_schema import compress_schema
|
|
24
25
|
from fastmcp.utilities.types import (
|
|
25
26
|
_convert_set_defaults,
|
|
26
27
|
find_kwarg_by_type,
|
|
@@ -150,6 +151,10 @@ class ResourceTemplate(BaseModel):
|
|
|
150
151
|
# Get schema from TypeAdapter - will fail if function isn't properly typed
|
|
151
152
|
parameters = TypeAdapter(fn).json_schema()
|
|
152
153
|
|
|
154
|
+
# compress the schema
|
|
155
|
+
prune_params = [context_kwarg] if context_kwarg else None
|
|
156
|
+
parameters = compress_schema(parameters, prune_params=prune_params)
|
|
157
|
+
|
|
153
158
|
# ensure the arguments are properly cast
|
|
154
159
|
fn = validate_call(fn)
|
|
155
160
|
|
|
@@ -171,28 +176,27 @@ class ResourceTemplate(BaseModel):
|
|
|
171
176
|
"""Create a resource from the template with the given parameters."""
|
|
172
177
|
from fastmcp.server.context import Context
|
|
173
178
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
kwargs[context_kwarg] = get_context()
|
|
179
|
+
# Add context to parameters if needed
|
|
180
|
+
kwargs = params.copy()
|
|
181
|
+
context_kwarg = find_kwarg_by_type(self.fn, kwarg_type=Context)
|
|
182
|
+
if context_kwarg and context_kwarg not in kwargs:
|
|
183
|
+
kwargs[context_kwarg] = get_context()
|
|
180
184
|
|
|
185
|
+
async def resource_read_fn() -> str | bytes:
|
|
181
186
|
# Call function and check if result is a coroutine
|
|
182
187
|
result = self.fn(**kwargs)
|
|
183
188
|
if inspect.iscoroutine(result):
|
|
184
189
|
result = await result
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
raise ValueError(f"Error creating resource from template: {e}")
|
|
190
|
+
return result
|
|
191
|
+
|
|
192
|
+
return FunctionResource(
|
|
193
|
+
uri=AnyUrl(uri), # Explicitly convert to AnyUrl
|
|
194
|
+
name=self.name,
|
|
195
|
+
description=self.description,
|
|
196
|
+
mime_type=self.mime_type,
|
|
197
|
+
fn=resource_read_fn,
|
|
198
|
+
tags=self.tags,
|
|
199
|
+
)
|
|
196
200
|
|
|
197
201
|
def __eq__(self, other: object) -> bool:
|
|
198
202
|
if not isinstance(other, ResourceTemplate):
|
fastmcp/resources/types.py
CHANGED
|
@@ -6,7 +6,7 @@ import inspect
|
|
|
6
6
|
import json
|
|
7
7
|
from collections.abc import Callable
|
|
8
8
|
from pathlib import Path
|
|
9
|
-
from typing import
|
|
9
|
+
from typing import Any
|
|
10
10
|
|
|
11
11
|
import anyio
|
|
12
12
|
import anyio.to_thread
|
|
@@ -15,12 +15,13 @@ import pydantic.json
|
|
|
15
15
|
import pydantic_core
|
|
16
16
|
from pydantic import Field, ValidationInfo
|
|
17
17
|
|
|
18
|
+
from fastmcp.exceptions import ResourceError
|
|
18
19
|
from fastmcp.resources.resource import Resource
|
|
19
20
|
from fastmcp.server.dependencies import get_context
|
|
21
|
+
from fastmcp.utilities.logging import get_logger
|
|
20
22
|
from fastmcp.utilities.types import find_kwarg_by_type
|
|
21
23
|
|
|
22
|
-
|
|
23
|
-
pass
|
|
24
|
+
logger = get_logger(__name__)
|
|
24
25
|
|
|
25
26
|
|
|
26
27
|
class TextResource(Resource):
|
|
@@ -62,26 +63,23 @@ class FunctionResource(Resource):
|
|
|
62
63
|
"""Read the resource by calling the wrapped function."""
|
|
63
64
|
from fastmcp.server.context import Context
|
|
64
65
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
return pydantic_core.to_json(result, fallback=str, indent=2).decode()
|
|
83
|
-
except Exception as e:
|
|
84
|
-
raise ValueError(f"Error reading resource {self.uri}: {e}")
|
|
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()
|
|
85
83
|
|
|
86
84
|
|
|
87
85
|
class FileResource(Resource):
|
|
@@ -124,7 +122,7 @@ class FileResource(Resource):
|
|
|
124
122
|
return await anyio.to_thread.run_sync(self.path.read_bytes)
|
|
125
123
|
return await anyio.to_thread.run_sync(self.path.read_text)
|
|
126
124
|
except Exception as e:
|
|
127
|
-
raise
|
|
125
|
+
raise ResourceError(f"Error reading file {self.path}") from e
|
|
128
126
|
|
|
129
127
|
|
|
130
128
|
class HttpResource(Resource):
|
|
@@ -185,7 +183,7 @@ class DirectoryResource(Resource):
|
|
|
185
183
|
else list(self.path.rglob("*"))
|
|
186
184
|
)
|
|
187
185
|
except Exception as e:
|
|
188
|
-
raise
|
|
186
|
+
raise ResourceError(f"Error listing directory {self.path}: {e}")
|
|
189
187
|
|
|
190
188
|
async def read(self) -> str: # Always returns JSON string
|
|
191
189
|
"""Read the directory listing."""
|
|
@@ -193,5 +191,5 @@ class DirectoryResource(Resource):
|
|
|
193
191
|
files = await anyio.to_thread.run_sync(self.list_files)
|
|
194
192
|
file_list = [str(f.relative_to(self.path)) for f in files if f.is_file()]
|
|
195
193
|
return json.dumps({"files": file_list}, indent=2)
|
|
196
|
-
except Exception
|
|
197
|
-
raise
|
|
194
|
+
except Exception:
|
|
195
|
+
raise ResourceError(f"Error reading directory {self.path}")
|
fastmcp/server/context.py
CHANGED
|
@@ -56,7 +56,7 @@ class Context:
|
|
|
56
56
|
ctx.error("Error message")
|
|
57
57
|
|
|
58
58
|
# Report progress
|
|
59
|
-
ctx.report_progress(50, 100)
|
|
59
|
+
ctx.report_progress(50, 100, "Processing")
|
|
60
60
|
|
|
61
61
|
# Access resources
|
|
62
62
|
data = ctx.read_resource("resource://data")
|
|
@@ -96,7 +96,7 @@ class Context:
|
|
|
96
96
|
return self.fastmcp._mcp_server.request_context
|
|
97
97
|
|
|
98
98
|
async def report_progress(
|
|
99
|
-
self, progress: float, total: float | None = None
|
|
99
|
+
self, progress: float, total: float | None = None, message: str | None = None
|
|
100
100
|
) -> None:
|
|
101
101
|
"""Report progress for the current operation.
|
|
102
102
|
|
|
@@ -115,7 +115,10 @@ class Context:
|
|
|
115
115
|
return
|
|
116
116
|
|
|
117
117
|
await self.request_context.session.send_progress_notification(
|
|
118
|
-
progress_token=progress_token,
|
|
118
|
+
progress_token=progress_token,
|
|
119
|
+
progress=progress,
|
|
120
|
+
total=total,
|
|
121
|
+
message=message,
|
|
119
122
|
)
|
|
120
123
|
|
|
121
124
|
async def read_resource(self, uri: str | AnyUrl) -> list[ReadResourceContents]:
|
fastmcp/server/http.py
CHANGED
|
@@ -10,9 +10,16 @@ from mcp.server.auth.middleware.bearer_auth import (
|
|
|
10
10
|
BearerAuthBackend,
|
|
11
11
|
RequireAuthMiddleware,
|
|
12
12
|
)
|
|
13
|
-
from mcp.server.auth.provider import
|
|
13
|
+
from mcp.server.auth.provider import (
|
|
14
|
+
AccessTokenT,
|
|
15
|
+
AuthorizationCodeT,
|
|
16
|
+
OAuthAuthorizationServerProvider,
|
|
17
|
+
RefreshTokenT,
|
|
18
|
+
)
|
|
14
19
|
from mcp.server.auth.routes import create_auth_routes
|
|
15
20
|
from mcp.server.auth.settings import AuthSettings
|
|
21
|
+
from mcp.server.lowlevel.server import LifespanResultT
|
|
22
|
+
from mcp.server.sse import SseServerTransport
|
|
16
23
|
from mcp.server.streamable_http_manager import StreamableHTTPSessionManager
|
|
17
24
|
from starlette.applications import Starlette
|
|
18
25
|
from starlette.middleware import Middleware
|
|
@@ -20,9 +27,8 @@ from starlette.middleware.authentication import AuthenticationMiddleware
|
|
|
20
27
|
from starlette.requests import Request
|
|
21
28
|
from starlette.responses import Response
|
|
22
29
|
from starlette.routing import BaseRoute, Mount, Route
|
|
23
|
-
from starlette.types import Receive, Scope, Send
|
|
30
|
+
from starlette.types import Lifespan, Receive, Scope, Send
|
|
24
31
|
|
|
25
|
-
from fastmcp.low_level.sse_server_transport import SseServerTransport
|
|
26
32
|
from fastmcp.utilities.logging import get_logger
|
|
27
33
|
|
|
28
34
|
if TYPE_CHECKING:
|
|
@@ -30,12 +36,19 @@ if TYPE_CHECKING:
|
|
|
30
36
|
|
|
31
37
|
logger = get_logger(__name__)
|
|
32
38
|
|
|
39
|
+
|
|
33
40
|
_current_http_request: ContextVar[Request | None] = ContextVar(
|
|
34
41
|
"http_request",
|
|
35
42
|
default=None,
|
|
36
43
|
)
|
|
37
44
|
|
|
38
45
|
|
|
46
|
+
class StarletteWithLifespan(Starlette):
|
|
47
|
+
@property
|
|
48
|
+
def lifespan(self) -> Lifespan:
|
|
49
|
+
return self.router.lifespan_context
|
|
50
|
+
|
|
51
|
+
|
|
39
52
|
@contextmanager
|
|
40
53
|
def set_http_request(request: Request) -> Generator[Request, None, None]:
|
|
41
54
|
token = _current_http_request.set(request)
|
|
@@ -62,7 +75,10 @@ class RequestContextMiddleware:
|
|
|
62
75
|
|
|
63
76
|
|
|
64
77
|
def setup_auth_middleware_and_routes(
|
|
65
|
-
auth_server_provider: OAuthAuthorizationServerProvider
|
|
78
|
+
auth_server_provider: OAuthAuthorizationServerProvider[
|
|
79
|
+
AuthorizationCodeT, RefreshTokenT, AccessTokenT
|
|
80
|
+
]
|
|
81
|
+
| None,
|
|
66
82
|
auth_settings: AuthSettings | None,
|
|
67
83
|
) -> tuple[list[Middleware], list[BaseRoute], list[str]]:
|
|
68
84
|
"""Set up authentication middleware and routes if auth is enabled.
|
|
@@ -112,7 +128,7 @@ def create_base_app(
|
|
|
112
128
|
middleware: list[Middleware],
|
|
113
129
|
debug: bool = False,
|
|
114
130
|
lifespan: Callable | None = None,
|
|
115
|
-
) ->
|
|
131
|
+
) -> StarletteWithLifespan:
|
|
116
132
|
"""Create a base Starlette app with common middleware and routes.
|
|
117
133
|
|
|
118
134
|
Args:
|
|
@@ -127,7 +143,7 @@ def create_base_app(
|
|
|
127
143
|
# Always add RequestContextMiddleware as the outermost middleware
|
|
128
144
|
middleware.append(Middleware(RequestContextMiddleware))
|
|
129
145
|
|
|
130
|
-
return
|
|
146
|
+
return StarletteWithLifespan(
|
|
131
147
|
routes=routes,
|
|
132
148
|
middleware=middleware,
|
|
133
149
|
debug=debug,
|
|
@@ -136,15 +152,18 @@ def create_base_app(
|
|
|
136
152
|
|
|
137
153
|
|
|
138
154
|
def create_sse_app(
|
|
139
|
-
server: FastMCP,
|
|
155
|
+
server: FastMCP[LifespanResultT],
|
|
140
156
|
message_path: str,
|
|
141
157
|
sse_path: str,
|
|
142
|
-
auth_server_provider: OAuthAuthorizationServerProvider
|
|
158
|
+
auth_server_provider: OAuthAuthorizationServerProvider[
|
|
159
|
+
AuthorizationCodeT, RefreshTokenT, AccessTokenT
|
|
160
|
+
]
|
|
161
|
+
| None = None,
|
|
143
162
|
auth_settings: AuthSettings | None = None,
|
|
144
163
|
debug: bool = False,
|
|
145
164
|
routes: list[BaseRoute] | None = None,
|
|
146
165
|
middleware: list[Middleware] | None = None,
|
|
147
|
-
) ->
|
|
166
|
+
) -> StarletteWithLifespan:
|
|
148
167
|
"""Return an instance of the SSE server app.
|
|
149
168
|
|
|
150
169
|
Args:
|
|
@@ -228,25 +247,33 @@ def create_sse_app(
|
|
|
228
247
|
server_middleware.extend(middleware)
|
|
229
248
|
|
|
230
249
|
# Create and return the app
|
|
231
|
-
|
|
250
|
+
app = create_base_app(
|
|
232
251
|
routes=server_routes,
|
|
233
252
|
middleware=server_middleware,
|
|
234
253
|
debug=debug,
|
|
235
254
|
)
|
|
255
|
+
# Store the FastMCP server instance on the Starlette app state
|
|
256
|
+
app.state.fastmcp_server = server
|
|
257
|
+
app.state.path = sse_path
|
|
258
|
+
|
|
259
|
+
return app
|
|
236
260
|
|
|
237
261
|
|
|
238
262
|
def create_streamable_http_app(
|
|
239
|
-
server: FastMCP,
|
|
263
|
+
server: FastMCP[LifespanResultT],
|
|
240
264
|
streamable_http_path: str,
|
|
241
265
|
event_store: None = None,
|
|
242
|
-
auth_server_provider: OAuthAuthorizationServerProvider
|
|
266
|
+
auth_server_provider: OAuthAuthorizationServerProvider[
|
|
267
|
+
AuthorizationCodeT, RefreshTokenT, AccessTokenT
|
|
268
|
+
]
|
|
269
|
+
| None = None,
|
|
243
270
|
auth_settings: AuthSettings | None = None,
|
|
244
271
|
json_response: bool = False,
|
|
245
272
|
stateless_http: bool = False,
|
|
246
273
|
debug: bool = False,
|
|
247
274
|
routes: list[BaseRoute] | None = None,
|
|
248
275
|
middleware: list[Middleware] | None = None,
|
|
249
|
-
) ->
|
|
276
|
+
) -> StarletteWithLifespan:
|
|
250
277
|
"""Return an instance of the StreamableHTTP server app.
|
|
251
278
|
|
|
252
279
|
Args:
|
|
@@ -322,9 +349,15 @@ def create_streamable_http_app(
|
|
|
322
349
|
yield
|
|
323
350
|
|
|
324
351
|
# Create and return the app with lifespan
|
|
325
|
-
|
|
352
|
+
app = create_base_app(
|
|
326
353
|
routes=server_routes,
|
|
327
354
|
middleware=server_middleware,
|
|
328
355
|
debug=debug,
|
|
329
356
|
lifespan=lifespan,
|
|
330
357
|
)
|
|
358
|
+
# Store the FastMCP server instance on the Starlette app state
|
|
359
|
+
app.state.fastmcp_server = server
|
|
360
|
+
|
|
361
|
+
app.state.path = streamable_http_path
|
|
362
|
+
|
|
363
|
+
return app
|
fastmcp/server/openapi.py
CHANGED
|
@@ -14,6 +14,7 @@ import httpx
|
|
|
14
14
|
from mcp.types import EmbeddedResource, ImageContent, TextContent, ToolAnnotations
|
|
15
15
|
from pydantic.networks import AnyUrl
|
|
16
16
|
|
|
17
|
+
from fastmcp.exceptions import ToolError
|
|
17
18
|
from fastmcp.resources import Resource, ResourceTemplate
|
|
18
19
|
from fastmcp.server.server import FastMCP
|
|
19
20
|
from fastmcp.tools.tool import Tool, _convert_to_content
|
|
@@ -137,6 +138,10 @@ class OpenAPITool(Tool):
|
|
|
137
138
|
self._route = route
|
|
138
139
|
self._timeout = timeout
|
|
139
140
|
|
|
141
|
+
def __repr__(self) -> str:
|
|
142
|
+
"""Custom representation to prevent recursion errors when printing."""
|
|
143
|
+
return f"OpenAPITool(name={self.name!r}, method={self._route.method}, path={self._route.path})"
|
|
144
|
+
|
|
140
145
|
async def _execute_request(self, *args, **kwargs):
|
|
141
146
|
"""Execute the HTTP request based on the route configuration."""
|
|
142
147
|
context = kwargs.get("context")
|
|
@@ -163,7 +168,7 @@ class OpenAPITool(Tool):
|
|
|
163
168
|
}
|
|
164
169
|
missing_params = required_path_params - path_params.keys()
|
|
165
170
|
if missing_params:
|
|
166
|
-
raise
|
|
171
|
+
raise ToolError(f"Missing required path parameters: {missing_params}")
|
|
167
172
|
|
|
168
173
|
for param_name, param_value in path_params.items():
|
|
169
174
|
path = path.replace(f"{{{param_name}}}", str(param_value))
|
|
@@ -286,6 +291,10 @@ class OpenAPIResource(Resource):
|
|
|
286
291
|
self._route = route
|
|
287
292
|
self._timeout = timeout
|
|
288
293
|
|
|
294
|
+
def __repr__(self) -> str:
|
|
295
|
+
"""Custom representation to prevent recursion errors when printing."""
|
|
296
|
+
return f"OpenAPIResource(name={self.name!r}, uri={self.uri!r}, path={self._route.path})"
|
|
297
|
+
|
|
289
298
|
async def read(self) -> str | bytes:
|
|
290
299
|
"""Fetch the resource data by making an HTTP request."""
|
|
291
300
|
try:
|
|
@@ -396,6 +405,10 @@ class OpenAPIResourceTemplate(ResourceTemplate):
|
|
|
396
405
|
self._route = route
|
|
397
406
|
self._timeout = timeout
|
|
398
407
|
|
|
408
|
+
def __repr__(self) -> str:
|
|
409
|
+
"""Custom representation to prevent recursion errors when printing."""
|
|
410
|
+
return f"OpenAPIResourceTemplate(name={self.name!r}, uri_template={self.uri_template!r}, path={self._route.path})"
|
|
411
|
+
|
|
399
412
|
async def create_resource(
|
|
400
413
|
self,
|
|
401
414
|
uri: str,
|
fastmcp/server/proxy.py
CHANGED
|
@@ -18,7 +18,7 @@ from mcp.types import (
|
|
|
18
18
|
from pydantic.networks import AnyUrl
|
|
19
19
|
|
|
20
20
|
from fastmcp.client import Client
|
|
21
|
-
from fastmcp.exceptions import NotFoundError
|
|
21
|
+
from fastmcp.exceptions import NotFoundError, ResourceError, ToolError
|
|
22
22
|
from fastmcp.prompts import Prompt, PromptMessage
|
|
23
23
|
from fastmcp.resources import Resource, ResourceTemplate
|
|
24
24
|
from fastmcp.server.context import Context
|
|
@@ -64,7 +64,7 @@ class ProxyTool(Tool):
|
|
|
64
64
|
arguments=arguments,
|
|
65
65
|
)
|
|
66
66
|
if result.isError:
|
|
67
|
-
raise
|
|
67
|
+
raise ToolError(cast(mcp.types.TextContent, result.content[0]).text)
|
|
68
68
|
return result.content
|
|
69
69
|
|
|
70
70
|
|
|
@@ -97,7 +97,7 @@ class ProxyResource(Resource):
|
|
|
97
97
|
elif isinstance(result[0], BlobResourceContents):
|
|
98
98
|
return result[0].blob
|
|
99
99
|
else:
|
|
100
|
-
raise
|
|
100
|
+
raise ResourceError(f"Unsupported content type: {type(result[0])}")
|
|
101
101
|
|
|
102
102
|
|
|
103
103
|
class ProxyTemplate(ResourceTemplate):
|
|
@@ -138,7 +138,7 @@ class ProxyTemplate(ResourceTemplate):
|
|
|
138
138
|
elif isinstance(result[0], BlobResourceContents):
|
|
139
139
|
value = result[0].blob
|
|
140
140
|
else:
|
|
141
|
-
raise
|
|
141
|
+
raise ResourceError(f"Unsupported content type: {type(result[0])}")
|
|
142
142
|
|
|
143
143
|
return ProxyResource(
|
|
144
144
|
client=self._client,
|