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
|
@@ -1,12 +1,16 @@
|
|
|
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.
|
|
9
|
-
from fastmcp.resources
|
|
10
|
+
from fastmcp.exceptions import ResourceError
|
|
11
|
+
from fastmcp.resources import FunctionResource, Resource
|
|
12
|
+
from fastmcp.resources.template import ResourceTemplate
|
|
13
|
+
from fastmcp.settings import DuplicateBehavior
|
|
10
14
|
from fastmcp.utilities.logging import get_logger
|
|
11
15
|
|
|
12
16
|
logger = get_logger(__name__)
|
|
@@ -15,21 +19,91 @@ logger = get_logger(__name__)
|
|
|
15
19
|
class ResourceManager:
|
|
16
20
|
"""Manages FastMCP resources."""
|
|
17
21
|
|
|
18
|
-
def __init__(self,
|
|
22
|
+
def __init__(self, duplicate_behavior: DuplicateBehavior = DuplicateBehavior.WARN):
|
|
19
23
|
self._resources: dict[str, Resource] = {}
|
|
20
24
|
self._templates: dict[str, ResourceTemplate] = {}
|
|
21
|
-
self.
|
|
25
|
+
self.duplicate_behavior = duplicate_behavior
|
|
22
26
|
|
|
23
|
-
def
|
|
24
|
-
|
|
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.
|
|
25
37
|
|
|
26
38
|
Args:
|
|
27
|
-
|
|
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
|
|
28
86
|
|
|
29
87
|
Returns:
|
|
30
88
|
The added resource. If a resource with the same URI already exists,
|
|
31
89
|
returns the existing resource.
|
|
32
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
|
+
"""
|
|
33
107
|
logger.debug(
|
|
34
108
|
"Adding resource",
|
|
35
109
|
extra={
|
|
@@ -40,28 +114,78 @@ class ResourceManager:
|
|
|
40
114
|
)
|
|
41
115
|
existing = self._resources.get(str(resource.uri))
|
|
42
116
|
if existing:
|
|
43
|
-
if self.
|
|
117
|
+
if self.duplicate_behavior == DuplicateBehavior.WARN:
|
|
44
118
|
logger.warning(f"Resource already exists: {resource.uri}")
|
|
45
|
-
|
|
119
|
+
self._resources[str(resource.uri)] = resource
|
|
120
|
+
elif self.duplicate_behavior == DuplicateBehavior.REPLACE:
|
|
121
|
+
self._resources[str(resource.uri)] = resource
|
|
122
|
+
elif self.duplicate_behavior == DuplicateBehavior.ERROR:
|
|
123
|
+
raise ValueError(f"Resource already exists: {resource.uri}")
|
|
124
|
+
elif self.duplicate_behavior == DuplicateBehavior.IGNORE:
|
|
125
|
+
pass
|
|
46
126
|
self._resources[str(resource.uri)] = resource
|
|
47
127
|
return resource
|
|
48
128
|
|
|
49
|
-
def
|
|
129
|
+
def add_template_from_fn(
|
|
50
130
|
self,
|
|
51
131
|
fn: Callable[..., Any],
|
|
52
132
|
uri_template: str,
|
|
53
133
|
name: str | None = None,
|
|
54
134
|
description: str | None = None,
|
|
55
135
|
mime_type: str | None = None,
|
|
136
|
+
tags: set[str] | None = None,
|
|
56
137
|
) -> ResourceTemplate:
|
|
57
|
-
"""
|
|
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
|
+
|
|
58
150
|
template = ResourceTemplate.from_function(
|
|
59
151
|
fn,
|
|
60
152
|
uri_template=uri_template,
|
|
61
153
|
name=name,
|
|
62
154
|
description=description,
|
|
63
155
|
mime_type=mime_type,
|
|
156
|
+
tags=tags,
|
|
157
|
+
)
|
|
158
|
+
return self.add_template(template)
|
|
159
|
+
|
|
160
|
+
def add_template(self, template: ResourceTemplate) -> ResourceTemplate:
|
|
161
|
+
"""Add a template to the manager.
|
|
162
|
+
|
|
163
|
+
Args:
|
|
164
|
+
template: A ResourceTemplate instance to add
|
|
165
|
+
|
|
166
|
+
Returns:
|
|
167
|
+
The added template. If a template with the same URI already exists,
|
|
168
|
+
returns the existing template.
|
|
169
|
+
"""
|
|
170
|
+
logger.debug(
|
|
171
|
+
"Adding resource",
|
|
172
|
+
extra={
|
|
173
|
+
"uri": template.uri_template,
|
|
174
|
+
"type": type(template).__name__,
|
|
175
|
+
"resource_name": template.name,
|
|
176
|
+
},
|
|
64
177
|
)
|
|
178
|
+
existing = self._templates.get(str(template.uri_template))
|
|
179
|
+
if existing:
|
|
180
|
+
if self.duplicate_behavior == DuplicateBehavior.WARN:
|
|
181
|
+
logger.warning(f"Resource already exists: {template.uri_template}")
|
|
182
|
+
self._templates[str(template.uri_template)] = template
|
|
183
|
+
elif self.duplicate_behavior == DuplicateBehavior.REPLACE:
|
|
184
|
+
self._templates[str(template.uri_template)] = template
|
|
185
|
+
elif self.duplicate_behavior == DuplicateBehavior.ERROR:
|
|
186
|
+
raise ValueError(f"Resource already exists: {template.uri_template}")
|
|
187
|
+
elif self.duplicate_behavior == DuplicateBehavior.IGNORE:
|
|
188
|
+
pass
|
|
65
189
|
self._templates[template.uri_template] = template
|
|
66
190
|
return template
|
|
67
191
|
|
|
@@ -82,7 +206,7 @@ class ResourceManager:
|
|
|
82
206
|
except Exception as e:
|
|
83
207
|
raise ValueError(f"Error creating resource from template: {e}")
|
|
84
208
|
|
|
85
|
-
raise
|
|
209
|
+
raise ResourceError(f"Unknown resource: {uri}")
|
|
86
210
|
|
|
87
211
|
def list_resources(self) -> list[Resource]:
|
|
88
212
|
"""List all registered resources."""
|
|
@@ -114,11 +238,11 @@ class ResourceManager:
|
|
|
114
238
|
# Create prefixed URI and copy the resource with the new URI
|
|
115
239
|
prefixed_uri = f"{prefix}{uri}" if prefix else uri
|
|
116
240
|
|
|
117
|
-
|
|
118
|
-
logger.debug(f"Importing resource with URI {uri} as {prefixed_uri}")
|
|
241
|
+
new_resource = resource.copy(updates=dict(uri=prefixed_uri))
|
|
119
242
|
|
|
120
243
|
# Store directly in resources dictionary
|
|
121
|
-
self.
|
|
244
|
+
self.add_resource(new_resource)
|
|
245
|
+
logger.debug(f'Imported resource "{uri}" as "{prefixed_uri}"')
|
|
122
246
|
|
|
123
247
|
def import_templates(
|
|
124
248
|
self, manager: "ResourceManager", prefix: str | None = None
|
|
@@ -142,10 +266,12 @@ class ResourceManager:
|
|
|
142
266
|
f"{prefix}{uri_template}" if prefix else uri_template
|
|
143
267
|
)
|
|
144
268
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
f"Importing resource template with URI {uri_template} as {prefixed_uri_template}"
|
|
269
|
+
new_template = template.copy(
|
|
270
|
+
updates=dict(uri_template=prefixed_uri_template)
|
|
148
271
|
)
|
|
149
272
|
|
|
150
273
|
# Store directly in templates dictionary
|
|
151
|
-
self.
|
|
274
|
+
self.add_template(new_template)
|
|
275
|
+
logger.debug(
|
|
276
|
+
f'Imported template "{uri_template}" as "{prefixed_uri_template}"'
|
|
277
|
+
)
|
|
@@ -5,11 +5,13 @@ from __future__ import annotations
|
|
|
5
5
|
import inspect
|
|
6
6
|
import re
|
|
7
7
|
from collections.abc import Callable
|
|
8
|
-
from typing import Any
|
|
8
|
+
from typing import Annotated, Any
|
|
9
9
|
|
|
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
|
|
11
12
|
|
|
12
13
|
from fastmcp.resources.types import FunctionResource, Resource
|
|
14
|
+
from fastmcp.utilities.types import _convert_set_defaults
|
|
13
15
|
|
|
14
16
|
|
|
15
17
|
class ResourceTemplate(BaseModel):
|
|
@@ -20,10 +22,13 @@ class ResourceTemplate(BaseModel):
|
|
|
20
22
|
)
|
|
21
23
|
name: str = Field(description="Name of the resource")
|
|
22
24
|
description: str | None = Field(description="Description of what the resource does")
|
|
25
|
+
tags: Annotated[set[str], BeforeValidator(_convert_set_defaults)] = Field(
|
|
26
|
+
default_factory=set, description="Tags for the resource"
|
|
27
|
+
)
|
|
23
28
|
mime_type: str = Field(
|
|
24
29
|
default="text/plain", description="MIME type of the resource content"
|
|
25
30
|
)
|
|
26
|
-
fn: Callable[..., Any]
|
|
31
|
+
fn: Callable[..., Any]
|
|
27
32
|
parameters: dict[str, Any] = Field(
|
|
28
33
|
description="JSON schema for function parameters"
|
|
29
34
|
)
|
|
@@ -36,6 +41,7 @@ class ResourceTemplate(BaseModel):
|
|
|
36
41
|
name: str | None = None,
|
|
37
42
|
description: str | None = None,
|
|
38
43
|
mime_type: str | None = None,
|
|
44
|
+
tags: set[str] | None = None,
|
|
39
45
|
) -> ResourceTemplate:
|
|
40
46
|
"""Create a template from a function."""
|
|
41
47
|
func_name = name or fn.__name__
|
|
@@ -55,6 +61,7 @@ class ResourceTemplate(BaseModel):
|
|
|
55
61
|
mime_type=mime_type or "text/plain",
|
|
56
62
|
fn=fn,
|
|
57
63
|
parameters=parameters,
|
|
64
|
+
tags=tags or set(),
|
|
58
65
|
)
|
|
59
66
|
|
|
60
67
|
def matches(self, uri: str) -> dict[str, Any] | None:
|
|
@@ -80,6 +87,19 @@ class ResourceTemplate(BaseModel):
|
|
|
80
87
|
description=self.description,
|
|
81
88
|
mime_type=self.mime_type,
|
|
82
89
|
fn=lambda: result, # Capture result in closure
|
|
90
|
+
tags=self.tags,
|
|
83
91
|
)
|
|
84
92
|
except Exception as e:
|
|
85
93
|
raise ValueError(f"Error creating resource from template: {e}")
|
|
94
|
+
|
|
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
|
+
def __eq__(self, other: object) -> bool:
|
|
103
|
+
if not isinstance(other, ResourceTemplate):
|
|
104
|
+
return False
|
|
105
|
+
return self.model_dump() == other.model_dump()
|
fastmcp/resources/types.py
CHANGED
|
@@ -13,7 +13,7 @@ import pydantic.json
|
|
|
13
13
|
import pydantic_core
|
|
14
14
|
from pydantic import Field, ValidationInfo
|
|
15
15
|
|
|
16
|
-
from fastmcp.resources.
|
|
16
|
+
from fastmcp.resources.resource import Resource
|
|
17
17
|
|
|
18
18
|
|
|
19
19
|
class TextResource(Resource):
|
|
@@ -49,7 +49,7 @@ class FunctionResource(Resource):
|
|
|
49
49
|
- other types will be converted to JSON
|
|
50
50
|
"""
|
|
51
51
|
|
|
52
|
-
fn: Callable[[], Any]
|
|
52
|
+
fn: Callable[[], Any]
|
|
53
53
|
|
|
54
54
|
async def read(self) -> str | bytes:
|
|
55
55
|
"""Read the resource by calling the wrapped function."""
|
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/openapi.py
CHANGED
|
@@ -12,7 +12,7 @@ from pydantic.networks import AnyUrl
|
|
|
12
12
|
|
|
13
13
|
from fastmcp.resources import Resource, ResourceTemplate
|
|
14
14
|
from fastmcp.server.server import FastMCP
|
|
15
|
-
from fastmcp.tools.
|
|
15
|
+
from fastmcp.tools.tool import Tool
|
|
16
16
|
from fastmcp.utilities import openapi
|
|
17
17
|
from fastmcp.utilities.func_metadata import func_metadata
|
|
18
18
|
from fastmcp.utilities.logging import get_logger
|
|
@@ -115,6 +115,7 @@ class OpenAPITool(Tool):
|
|
|
115
115
|
parameters: dict[str, Any],
|
|
116
116
|
fn_metadata: Any,
|
|
117
117
|
is_async: bool = True,
|
|
118
|
+
tags: set[str] = set(),
|
|
118
119
|
):
|
|
119
120
|
super().__init__(
|
|
120
121
|
name=name,
|
|
@@ -124,6 +125,7 @@ class OpenAPITool(Tool):
|
|
|
124
125
|
fn_metadata=fn_metadata,
|
|
125
126
|
is_async=is_async,
|
|
126
127
|
context_kwarg="context", # Default context keyword argument
|
|
128
|
+
tags=tags,
|
|
127
129
|
)
|
|
128
130
|
self._client = client
|
|
129
131
|
self._route = route
|
|
@@ -242,12 +244,14 @@ class OpenAPIResource(Resource):
|
|
|
242
244
|
name: str,
|
|
243
245
|
description: str,
|
|
244
246
|
mime_type: str = "application/json",
|
|
247
|
+
tags: set[str] = set(),
|
|
245
248
|
):
|
|
246
249
|
super().__init__(
|
|
247
250
|
uri=AnyUrl(uri), # Convert string to AnyUrl
|
|
248
251
|
name=name,
|
|
249
252
|
description=description,
|
|
250
253
|
mime_type=mime_type,
|
|
254
|
+
tags=tags,
|
|
251
255
|
)
|
|
252
256
|
self._client = client
|
|
253
257
|
self._route = route
|
|
@@ -332,6 +336,7 @@ class OpenAPIResourceTemplate(ResourceTemplate):
|
|
|
332
336
|
name: str,
|
|
333
337
|
description: str,
|
|
334
338
|
parameters: dict[str, Any],
|
|
339
|
+
tags: set[str] = set(),
|
|
335
340
|
):
|
|
336
341
|
super().__init__(
|
|
337
342
|
uri_template=uri_template,
|
|
@@ -339,6 +344,7 @@ class OpenAPIResourceTemplate(ResourceTemplate):
|
|
|
339
344
|
description=description,
|
|
340
345
|
fn=self._create_resource_fn,
|
|
341
346
|
parameters=parameters,
|
|
347
|
+
tags=tags,
|
|
342
348
|
)
|
|
343
349
|
self._client = client
|
|
344
350
|
self._route = route
|
|
@@ -405,6 +411,7 @@ class OpenAPIResourceTemplate(ResourceTemplate):
|
|
|
405
411
|
description=self.description
|
|
406
412
|
or f"Resource for {self._route.path}", # Provide default if None
|
|
407
413
|
mime_type="application/json", # Default, will be updated when read
|
|
414
|
+
tags=set(self._route.tags or []),
|
|
408
415
|
)
|
|
409
416
|
|
|
410
417
|
|
|
@@ -525,10 +532,13 @@ class FastMCPOpenAPI(FastMCP):
|
|
|
525
532
|
parameters=combined_schema,
|
|
526
533
|
fn_metadata=func_metadata(_openapi_passthrough),
|
|
527
534
|
is_async=True,
|
|
535
|
+
tags=set(route.tags or []),
|
|
528
536
|
)
|
|
529
537
|
# Register the tool by directly assigning to the tools dictionary
|
|
530
538
|
self._tool_manager._tools[tool_name] = tool
|
|
531
|
-
logger.debug(
|
|
539
|
+
logger.debug(
|
|
540
|
+
f"Registered TOOL: {tool_name} ({route.method} {route.path}) with tags: {route.tags}"
|
|
541
|
+
)
|
|
532
542
|
|
|
533
543
|
def _create_openapi_resource(self, route: openapi.HTTPRoute, operation_id: str):
|
|
534
544
|
"""Creates and registers an OpenAPIResource with enhanced description."""
|
|
@@ -550,11 +560,12 @@ class FastMCPOpenAPI(FastMCP):
|
|
|
550
560
|
uri=resource_uri,
|
|
551
561
|
name=resource_name,
|
|
552
562
|
description=enhanced_description,
|
|
563
|
+
tags=set(route.tags or []),
|
|
553
564
|
)
|
|
554
565
|
# Register the resource by directly assigning to the resources dictionary
|
|
555
566
|
self._resource_manager._resources[str(resource.uri)] = resource
|
|
556
567
|
logger.debug(
|
|
557
|
-
f"Registered RESOURCE: {resource_uri} ({route.method} {route.path})"
|
|
568
|
+
f"Registered RESOURCE: {resource_uri} ({route.method} {route.path}) with tags: {route.tags}"
|
|
558
569
|
)
|
|
559
570
|
|
|
560
571
|
def _create_openapi_template(self, route: openapi.HTTPRoute, operation_id: str):
|
|
@@ -594,11 +605,12 @@ class FastMCPOpenAPI(FastMCP):
|
|
|
594
605
|
name=template_name,
|
|
595
606
|
description=enhanced_description,
|
|
596
607
|
parameters=template_params_schema,
|
|
608
|
+
tags=set(route.tags or []),
|
|
597
609
|
)
|
|
598
610
|
# Register the template by directly assigning to the templates dictionary
|
|
599
611
|
self._resource_manager._templates[uri_template_str] = template
|
|
600
612
|
logger.debug(
|
|
601
|
-
f"Registered TEMPLATE: {uri_template_str} ({route.method} {route.path})"
|
|
613
|
+
f"Registered TEMPLATE: {uri_template_str} ({route.method} {route.path}) with tags: {route.tags}"
|
|
602
614
|
)
|
|
603
615
|
|
|
604
616
|
async def call_tool(self, name: str, arguments: dict[str, Any]) -> Any:
|
fastmcp/server/proxy.py
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
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
|
|
12
|
-
from fastmcp.tools.
|
|
12
|
+
from fastmcp.tools.tool import Tool
|
|
13
13
|
from fastmcp.utilities.func_metadata import func_metadata
|
|
14
14
|
from fastmcp.utilities.logging import get_logger
|
|
15
15
|
|
|
@@ -40,11 +40,15 @@ class ProxyTool(Tool):
|
|
|
40
40
|
async def run(
|
|
41
41
|
self, arguments: dict[str, Any], context: Context | None = None
|
|
42
42
|
) -> Any:
|
|
43
|
+
# the client context manager will swallow any exceptions inside a TaskGroup
|
|
44
|
+
# so we return the raw result and raise an exception ourselves
|
|
43
45
|
async with self._client:
|
|
44
|
-
result = await self._client.call_tool(
|
|
46
|
+
result = await self._client.call_tool(
|
|
47
|
+
self.name, arguments, _return_raw_result=True
|
|
48
|
+
)
|
|
45
49
|
if result.isError:
|
|
46
50
|
raise ValueError(cast(mcp.types.TextContent, result.content[0]).text)
|
|
47
|
-
return result.content
|
|
51
|
+
return result.content
|
|
48
52
|
|
|
49
53
|
|
|
50
54
|
class ProxyResource(Resource):
|
|
@@ -73,12 +77,12 @@ class ProxyResource(Resource):
|
|
|
73
77
|
|
|
74
78
|
async with self._client:
|
|
75
79
|
result = await self._client.read_resource(self.uri)
|
|
76
|
-
if isinstance(result
|
|
77
|
-
return result
|
|
78
|
-
elif isinstance(result
|
|
79
|
-
return result
|
|
80
|
+
if isinstance(result[0], TextResourceContents):
|
|
81
|
+
return result[0].text
|
|
82
|
+
elif isinstance(result[0], BlobResourceContents):
|
|
83
|
+
return result[0].blob
|
|
80
84
|
else:
|
|
81
|
-
raise ValueError(f"Unsupported content type: {type(result
|
|
85
|
+
raise ValueError(f"Unsupported content type: {type(result[0])}")
|
|
82
86
|
|
|
83
87
|
|
|
84
88
|
class ProxyTemplate(ResourceTemplate):
|
|
@@ -103,20 +107,20 @@ class ProxyTemplate(ResourceTemplate):
|
|
|
103
107
|
async with self._client:
|
|
104
108
|
result = await self._client.read_resource(uri)
|
|
105
109
|
|
|
106
|
-
if isinstance(result
|
|
107
|
-
value = result
|
|
108
|
-
elif isinstance(result
|
|
109
|
-
value = result
|
|
110
|
+
if isinstance(result[0], TextResourceContents):
|
|
111
|
+
value = result[0].text
|
|
112
|
+
elif isinstance(result[0], BlobResourceContents):
|
|
113
|
+
value = result[0].blob
|
|
110
114
|
else:
|
|
111
|
-
raise ValueError(f"Unsupported content type: {type(result
|
|
115
|
+
raise ValueError(f"Unsupported content type: {type(result[0])}")
|
|
112
116
|
|
|
113
117
|
return ProxyResource(
|
|
114
118
|
client=self._client,
|
|
115
119
|
uri=uri,
|
|
116
120
|
name=self.name,
|
|
117
121
|
description=self.description,
|
|
118
|
-
mime_type=result
|
|
119
|
-
contents=result
|
|
122
|
+
mime_type=result[0].mimeType,
|
|
123
|
+
contents=result,
|
|
120
124
|
_value=value,
|
|
121
125
|
)
|
|
122
126
|
|
|
@@ -138,10 +142,10 @@ class ProxyPrompt(Prompt):
|
|
|
138
142
|
fn=_proxy_passthrough,
|
|
139
143
|
)
|
|
140
144
|
|
|
141
|
-
async def render(self, arguments: dict[str, Any]) -> list[
|
|
145
|
+
async def render(self, arguments: dict[str, Any]) -> list[Message]:
|
|
142
146
|
async with self._client:
|
|
143
147
|
result = await self._client.get_prompt(self.name, arguments)
|
|
144
|
-
return result.messages
|
|
148
|
+
return [Message(role=m.role, content=m.content) for m in result.messages]
|
|
145
149
|
|
|
146
150
|
|
|
147
151
|
class FastMCPProxy(FastMCP):
|
|
@@ -177,15 +181,15 @@ class FastMCPProxy(FastMCP):
|
|
|
177
181
|
|
|
178
182
|
async with client:
|
|
179
183
|
# Register proxies for client tools
|
|
180
|
-
|
|
181
|
-
for tool in
|
|
184
|
+
tools = await client.list_tools()
|
|
185
|
+
for tool in tools:
|
|
182
186
|
tool_proxy = await ProxyTool.from_client(client, tool)
|
|
183
187
|
server._tool_manager._tools[tool_proxy.name] = tool_proxy
|
|
184
188
|
logger.debug(f"Created proxy for tool: {tool_proxy.name}")
|
|
185
189
|
|
|
186
190
|
# Register proxies for client resources
|
|
187
|
-
|
|
188
|
-
for resource in
|
|
191
|
+
resources = await client.list_resources()
|
|
192
|
+
for resource in resources:
|
|
189
193
|
resource_proxy = await ProxyResource.from_client(client, resource)
|
|
190
194
|
server._resource_manager._resources[str(resource_proxy.uri)] = (
|
|
191
195
|
resource_proxy
|
|
@@ -193,8 +197,8 @@ class FastMCPProxy(FastMCP):
|
|
|
193
197
|
logger.debug(f"Created proxy for resource: {resource_proxy.uri}")
|
|
194
198
|
|
|
195
199
|
# Register proxies for client resource templates
|
|
196
|
-
|
|
197
|
-
for template in
|
|
200
|
+
templates = await client.list_resource_templates()
|
|
201
|
+
for template in templates:
|
|
198
202
|
template_proxy = await ProxyTemplate.from_client(client, template)
|
|
199
203
|
server._resource_manager._templates[template_proxy.uri_template] = (
|
|
200
204
|
template_proxy
|
|
@@ -204,8 +208,8 @@ class FastMCPProxy(FastMCP):
|
|
|
204
208
|
)
|
|
205
209
|
|
|
206
210
|
# Register proxies for client prompts
|
|
207
|
-
|
|
208
|
-
for prompt in
|
|
211
|
+
prompts = await client.list_prompts()
|
|
212
|
+
for prompt in prompts:
|
|
209
213
|
prompt_proxy = await ProxyPrompt.from_client(client, prompt)
|
|
210
214
|
server._prompt_manager._prompts[prompt_proxy.name] = prompt_proxy
|
|
211
215
|
logger.debug(f"Created proxy for prompt: {prompt_proxy.name}")
|