fastmcp 1.0__py3-none-any.whl → 2.1.0__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/__init__.py +15 -4
- fastmcp/cli/__init__.py +0 -1
- fastmcp/cli/claude.py +13 -11
- fastmcp/cli/cli.py +59 -39
- fastmcp/client/__init__.py +25 -0
- fastmcp/client/base.py +1 -0
- fastmcp/client/client.py +226 -0
- fastmcp/client/roots.py +75 -0
- fastmcp/client/sampling.py +50 -0
- fastmcp/client/transports.py +411 -0
- fastmcp/prompts/__init__.py +2 -2
- fastmcp/prompts/{base.py → prompt.py} +47 -26
- fastmcp/prompts/prompt_manager.py +69 -15
- fastmcp/resources/__init__.py +6 -6
- fastmcp/resources/{base.py → resource.py} +21 -2
- fastmcp/resources/resource_manager.py +116 -17
- fastmcp/resources/{templates.py → template.py} +36 -11
- fastmcp/resources/types.py +18 -13
- fastmcp/server/__init__.py +5 -0
- fastmcp/server/context.py +222 -0
- fastmcp/server/openapi.py +637 -0
- fastmcp/server/proxy.py +223 -0
- fastmcp/{server.py → server/server.py} +323 -267
- fastmcp/settings.py +81 -0
- fastmcp/tools/__init__.py +1 -1
- fastmcp/tools/{base.py → tool.py} +47 -18
- fastmcp/tools/tool_manager.py +57 -16
- fastmcp/utilities/func_metadata.py +33 -19
- fastmcp/utilities/openapi.py +797 -0
- fastmcp/utilities/types.py +15 -4
- fastmcp-2.1.0.dist-info/METADATA +770 -0
- fastmcp-2.1.0.dist-info/RECORD +39 -0
- fastmcp-2.1.0.dist-info/licenses/LICENSE +201 -0
- fastmcp/prompts/manager.py +0 -50
- fastmcp-1.0.dist-info/METADATA +0 -604
- fastmcp-1.0.dist-info/RECORD +0 -28
- fastmcp-1.0.dist-info/licenses/LICENSE +0 -21
- {fastmcp-1.0.dist-info → fastmcp-2.1.0.dist-info}/WHEEL +0 -0
- {fastmcp-1.0.dist-info → fastmcp-2.1.0.dist-info}/entry_points.txt +0 -0
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
"""Prompt management functionality."""
|
|
2
2
|
|
|
3
|
-
from
|
|
3
|
+
from collections.abc import Awaitable, Callable
|
|
4
|
+
from typing import Any
|
|
4
5
|
|
|
5
|
-
|
|
6
|
-
from fastmcp.
|
|
6
|
+
from fastmcp.prompts.prompt import Message, Prompt, PromptResult
|
|
7
|
+
from fastmcp.settings import DuplicateBehavior
|
|
7
8
|
from fastmcp.utilities.logging import get_logger
|
|
8
9
|
|
|
9
10
|
logger = get_logger(__name__)
|
|
@@ -12,25 +13,78 @@ logger = get_logger(__name__)
|
|
|
12
13
|
class PromptManager:
|
|
13
14
|
"""Manages FastMCP prompts."""
|
|
14
15
|
|
|
15
|
-
def __init__(self,
|
|
16
|
-
self._prompts:
|
|
17
|
-
self.
|
|
16
|
+
def __init__(self, duplicate_behavior: DuplicateBehavior = DuplicateBehavior.WARN):
|
|
17
|
+
self._prompts: dict[str, Prompt] = {}
|
|
18
|
+
self.duplicate_behavior = duplicate_behavior
|
|
19
|
+
|
|
20
|
+
def get_prompt(self, name: str) -> Prompt | None:
|
|
21
|
+
"""Get prompt by name."""
|
|
22
|
+
return self._prompts.get(name)
|
|
23
|
+
|
|
24
|
+
def list_prompts(self) -> list[Prompt]:
|
|
25
|
+
"""List all registered prompts."""
|
|
26
|
+
return list(self._prompts.values())
|
|
27
|
+
|
|
28
|
+
def add_prompt_from_fn(
|
|
29
|
+
self,
|
|
30
|
+
fn: Callable[..., PromptResult | Awaitable[PromptResult]],
|
|
31
|
+
name: str | None = None,
|
|
32
|
+
description: str | None = None,
|
|
33
|
+
tags: set[str] | None = None,
|
|
34
|
+
) -> Prompt:
|
|
35
|
+
"""Create a prompt from a function."""
|
|
36
|
+
prompt = Prompt.from_function(fn, name=name, description=description, tags=tags)
|
|
37
|
+
return self.add_prompt(prompt)
|
|
18
38
|
|
|
19
39
|
def add_prompt(self, prompt: Prompt) -> Prompt:
|
|
20
40
|
"""Add a prompt to the manager."""
|
|
21
|
-
|
|
41
|
+
|
|
42
|
+
# Check for duplicates
|
|
22
43
|
existing = self._prompts.get(prompt.name)
|
|
23
44
|
if existing:
|
|
24
|
-
if self.
|
|
45
|
+
if self.duplicate_behavior == DuplicateBehavior.WARN:
|
|
25
46
|
logger.warning(f"Prompt already exists: {prompt.name}")
|
|
26
|
-
|
|
47
|
+
self._prompts[prompt.name] = prompt
|
|
48
|
+
elif self.duplicate_behavior == DuplicateBehavior.REPLACE:
|
|
49
|
+
self._prompts[prompt.name] = prompt
|
|
50
|
+
elif self.duplicate_behavior == DuplicateBehavior.ERROR:
|
|
51
|
+
raise ValueError(f"Prompt already exists: {prompt.name}")
|
|
52
|
+
elif self.duplicate_behavior == DuplicateBehavior.IGNORE:
|
|
53
|
+
pass
|
|
54
|
+
|
|
27
55
|
self._prompts[prompt.name] = prompt
|
|
28
56
|
return prompt
|
|
29
57
|
|
|
30
|
-
def
|
|
31
|
-
|
|
32
|
-
|
|
58
|
+
async def render_prompt(
|
|
59
|
+
self, name: str, arguments: dict[str, Any] | None = None
|
|
60
|
+
) -> list[Message]:
|
|
61
|
+
"""Render a prompt by name with arguments."""
|
|
62
|
+
prompt = self.get_prompt(name)
|
|
63
|
+
if not prompt:
|
|
64
|
+
raise ValueError(f"Unknown prompt: {name}")
|
|
33
65
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
66
|
+
return await prompt.render(arguments)
|
|
67
|
+
|
|
68
|
+
def import_prompts(
|
|
69
|
+
self, manager: "PromptManager", prefix: str | None = None
|
|
70
|
+
) -> None:
|
|
71
|
+
"""
|
|
72
|
+
Import all prompts from another PromptManager with prefixed names.
|
|
73
|
+
|
|
74
|
+
Args:
|
|
75
|
+
manager: Another PromptManager instance to import prompts from
|
|
76
|
+
prefix: Prefix to add to prompt names. The resulting prompt name will
|
|
77
|
+
be in the format "{prefix}{original_name}" if prefix is provided,
|
|
78
|
+
otherwise the original name is used.
|
|
79
|
+
For example, with prefix "weather/" and prompt "forecast_prompt",
|
|
80
|
+
the imported prompt would be available as "weather/forecast_prompt"
|
|
81
|
+
"""
|
|
82
|
+
for name, prompt in manager._prompts.items():
|
|
83
|
+
# Create prefixed name
|
|
84
|
+
prefixed_name = f"{prefix}{name}" if prefix else name
|
|
85
|
+
|
|
86
|
+
new_prompt = prompt.copy(updates=dict(name=prefixed_name))
|
|
87
|
+
|
|
88
|
+
# Store the prompt with the prefixed name
|
|
89
|
+
self.add_prompt(new_prompt)
|
|
90
|
+
logger.debug(f'Imported prompt "{name}" as "{prefixed_name}"')
|
fastmcp/resources/__init__.py
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
|
-
from .
|
|
1
|
+
from .resource import Resource
|
|
2
|
+
from .resource_manager import ResourceManager
|
|
3
|
+
from .template import ResourceTemplate
|
|
2
4
|
from .types import (
|
|
3
|
-
TextResource,
|
|
4
5
|
BinaryResource,
|
|
5
|
-
|
|
6
|
+
DirectoryResource,
|
|
6
7
|
FileResource,
|
|
8
|
+
FunctionResource,
|
|
7
9
|
HttpResource,
|
|
8
|
-
|
|
10
|
+
TextResource,
|
|
9
11
|
)
|
|
10
|
-
from .templates import ResourceTemplate
|
|
11
|
-
from .resource_manager import ResourceManager
|
|
12
12
|
|
|
13
13
|
__all__ = [
|
|
14
14
|
"Resource",
|
|
@@ -1,17 +1,21 @@
|
|
|
1
1
|
"""Base classes and interfaces for FastMCP resources."""
|
|
2
2
|
|
|
3
3
|
import abc
|
|
4
|
-
from typing import
|
|
4
|
+
from typing import Annotated, Any
|
|
5
5
|
|
|
6
6
|
from pydantic import (
|
|
7
7
|
AnyUrl,
|
|
8
8
|
BaseModel,
|
|
9
|
+
BeforeValidator,
|
|
9
10
|
ConfigDict,
|
|
10
11
|
Field,
|
|
11
12
|
UrlConstraints,
|
|
12
13
|
ValidationInfo,
|
|
13
14
|
field_validator,
|
|
14
15
|
)
|
|
16
|
+
from typing_extensions import Self
|
|
17
|
+
|
|
18
|
+
from fastmcp.utilities.types import _convert_set_defaults
|
|
15
19
|
|
|
16
20
|
|
|
17
21
|
class Resource(BaseModel, abc.ABC):
|
|
@@ -26,6 +30,9 @@ class Resource(BaseModel, abc.ABC):
|
|
|
26
30
|
description: str | None = Field(
|
|
27
31
|
description="Description of the resource", default=None
|
|
28
32
|
)
|
|
33
|
+
tags: Annotated[set[str], BeforeValidator(_convert_set_defaults)] = Field(
|
|
34
|
+
default_factory=set, description="Tags for the resource"
|
|
35
|
+
)
|
|
29
36
|
mime_type: str = Field(
|
|
30
37
|
default="text/plain",
|
|
31
38
|
description="MIME type of the resource content",
|
|
@@ -43,6 +50,18 @@ class Resource(BaseModel, abc.ABC):
|
|
|
43
50
|
raise ValueError("Either name or uri must be provided")
|
|
44
51
|
|
|
45
52
|
@abc.abstractmethod
|
|
46
|
-
async def read(self) ->
|
|
53
|
+
async def read(self) -> str | bytes:
|
|
47
54
|
"""Read the resource content."""
|
|
48
55
|
pass
|
|
56
|
+
|
|
57
|
+
def copy(self, updates: dict[str, Any] | None = None) -> Self:
|
|
58
|
+
"""Copy the resource with optional updates."""
|
|
59
|
+
data = self.model_dump()
|
|
60
|
+
if updates:
|
|
61
|
+
data.update(updates)
|
|
62
|
+
return type(self)(**data)
|
|
63
|
+
|
|
64
|
+
def __eq__(self, other: object) -> bool:
|
|
65
|
+
if not isinstance(other, Resource):
|
|
66
|
+
return False
|
|
67
|
+
return self.model_dump() == other.model_dump()
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
"""Resource manager functionality."""
|
|
2
2
|
|
|
3
|
-
from
|
|
3
|
+
from collections.abc import Callable
|
|
4
|
+
from typing import Any
|
|
4
5
|
|
|
5
6
|
from pydantic import AnyUrl
|
|
6
7
|
|
|
7
|
-
from fastmcp.resources.
|
|
8
|
-
from fastmcp.resources.
|
|
8
|
+
from fastmcp.resources.resource import Resource
|
|
9
|
+
from fastmcp.resources.template import ResourceTemplate
|
|
10
|
+
from fastmcp.settings import DuplicateBehavior
|
|
9
11
|
from fastmcp.utilities.logging import get_logger
|
|
10
12
|
|
|
11
13
|
logger = get_logger(__name__)
|
|
@@ -14,10 +16,10 @@ logger = get_logger(__name__)
|
|
|
14
16
|
class ResourceManager:
|
|
15
17
|
"""Manages FastMCP resources."""
|
|
16
18
|
|
|
17
|
-
def __init__(self,
|
|
18
|
-
self._resources:
|
|
19
|
-
self._templates:
|
|
20
|
-
self.
|
|
19
|
+
def __init__(self, duplicate_behavior: DuplicateBehavior = DuplicateBehavior.WARN):
|
|
20
|
+
self._resources: dict[str, Resource] = {}
|
|
21
|
+
self._templates: dict[str, ResourceTemplate] = {}
|
|
22
|
+
self.duplicate_behavior = duplicate_behavior
|
|
21
23
|
|
|
22
24
|
def add_resource(self, resource: Resource) -> Resource:
|
|
23
25
|
"""Add a resource to the manager.
|
|
@@ -34,37 +36,76 @@ class ResourceManager:
|
|
|
34
36
|
extra={
|
|
35
37
|
"uri": resource.uri,
|
|
36
38
|
"type": type(resource).__name__,
|
|
37
|
-
"
|
|
39
|
+
"resource_name": resource.name,
|
|
38
40
|
},
|
|
39
41
|
)
|
|
40
42
|
existing = self._resources.get(str(resource.uri))
|
|
41
43
|
if existing:
|
|
42
|
-
if self.
|
|
44
|
+
if self.duplicate_behavior == DuplicateBehavior.WARN:
|
|
43
45
|
logger.warning(f"Resource already exists: {resource.uri}")
|
|
44
|
-
|
|
46
|
+
self._resources[str(resource.uri)] = resource
|
|
47
|
+
elif self.duplicate_behavior == DuplicateBehavior.REPLACE:
|
|
48
|
+
self._resources[str(resource.uri)] = resource
|
|
49
|
+
elif self.duplicate_behavior == DuplicateBehavior.ERROR:
|
|
50
|
+
raise ValueError(f"Resource already exists: {resource.uri}")
|
|
51
|
+
elif self.duplicate_behavior == DuplicateBehavior.IGNORE:
|
|
52
|
+
pass
|
|
45
53
|
self._resources[str(resource.uri)] = resource
|
|
46
54
|
return resource
|
|
47
55
|
|
|
48
|
-
def
|
|
56
|
+
def add_template_from_fn(
|
|
49
57
|
self,
|
|
50
|
-
fn: Callable,
|
|
58
|
+
fn: Callable[..., Any],
|
|
51
59
|
uri_template: str,
|
|
52
|
-
name:
|
|
53
|
-
description:
|
|
54
|
-
mime_type:
|
|
60
|
+
name: str | None = None,
|
|
61
|
+
description: str | None = None,
|
|
62
|
+
mime_type: str | None = None,
|
|
63
|
+
tags: set[str] | None = None,
|
|
55
64
|
) -> ResourceTemplate:
|
|
56
|
-
"""
|
|
65
|
+
"""Create a template from a function."""
|
|
57
66
|
template = ResourceTemplate.from_function(
|
|
58
67
|
fn,
|
|
59
68
|
uri_template=uri_template,
|
|
60
69
|
name=name,
|
|
61
70
|
description=description,
|
|
62
71
|
mime_type=mime_type,
|
|
72
|
+
tags=tags,
|
|
63
73
|
)
|
|
74
|
+
return self.add_template(template)
|
|
75
|
+
|
|
76
|
+
def add_template(self, template: ResourceTemplate) -> ResourceTemplate:
|
|
77
|
+
"""Add a template to the manager.
|
|
78
|
+
|
|
79
|
+
Args:
|
|
80
|
+
template: A ResourceTemplate instance to add
|
|
81
|
+
|
|
82
|
+
Returns:
|
|
83
|
+
The added template. If a template with the same URI already exists,
|
|
84
|
+
returns the existing template.
|
|
85
|
+
"""
|
|
86
|
+
logger.debug(
|
|
87
|
+
"Adding resource",
|
|
88
|
+
extra={
|
|
89
|
+
"uri": template.uri_template,
|
|
90
|
+
"type": type(template).__name__,
|
|
91
|
+
"resource_name": template.name,
|
|
92
|
+
},
|
|
93
|
+
)
|
|
94
|
+
existing = self._templates.get(str(template.uri_template))
|
|
95
|
+
if existing:
|
|
96
|
+
if self.duplicate_behavior == DuplicateBehavior.WARN:
|
|
97
|
+
logger.warning(f"Resource already exists: {template.uri_template}")
|
|
98
|
+
self._templates[str(template.uri_template)] = template
|
|
99
|
+
elif self.duplicate_behavior == DuplicateBehavior.REPLACE:
|
|
100
|
+
self._templates[str(template.uri_template)] = template
|
|
101
|
+
elif self.duplicate_behavior == DuplicateBehavior.ERROR:
|
|
102
|
+
raise ValueError(f"Resource already exists: {template.uri_template}")
|
|
103
|
+
elif self.duplicate_behavior == DuplicateBehavior.IGNORE:
|
|
104
|
+
pass
|
|
64
105
|
self._templates[template.uri_template] = template
|
|
65
106
|
return template
|
|
66
107
|
|
|
67
|
-
async def get_resource(self, uri:
|
|
108
|
+
async def get_resource(self, uri: AnyUrl | str) -> Resource | None:
|
|
68
109
|
"""Get resource by URI, checking concrete resources first, then templates."""
|
|
69
110
|
uri_str = str(uri)
|
|
70
111
|
logger.debug("Getting resource", extra={"uri": uri_str})
|
|
@@ -92,3 +133,61 @@ class ResourceManager:
|
|
|
92
133
|
"""List all registered templates."""
|
|
93
134
|
logger.debug("Listing templates", extra={"count": len(self._templates)})
|
|
94
135
|
return list(self._templates.values())
|
|
136
|
+
|
|
137
|
+
def import_resources(
|
|
138
|
+
self, manager: "ResourceManager", prefix: str | None = None
|
|
139
|
+
) -> None:
|
|
140
|
+
"""Import resources from another resource manager.
|
|
141
|
+
|
|
142
|
+
Resources are imported with a prefixed URI if a prefix is provided. For example,
|
|
143
|
+
if a resource has URI "data://users" and you import it with prefix "app+", the
|
|
144
|
+
imported resource will have URI "app+data://users". If no prefix is provided,
|
|
145
|
+
the original URI is used.
|
|
146
|
+
|
|
147
|
+
Args:
|
|
148
|
+
manager: The ResourceManager to import from
|
|
149
|
+
prefix: A prefix to apply to the resource URIs, including the delimiter.
|
|
150
|
+
For example, "app+" would result in URIs like "app+data://users".
|
|
151
|
+
If None, the original URI is used.
|
|
152
|
+
"""
|
|
153
|
+
for uri, resource in manager._resources.items():
|
|
154
|
+
# Create prefixed URI and copy the resource with the new URI
|
|
155
|
+
prefixed_uri = f"{prefix}{uri}" if prefix else uri
|
|
156
|
+
|
|
157
|
+
new_resource = resource.copy(updates=dict(uri=prefixed_uri))
|
|
158
|
+
|
|
159
|
+
# Store directly in resources dictionary
|
|
160
|
+
self.add_resource(new_resource)
|
|
161
|
+
logger.debug(f'Imported resource "{uri}" as "{prefixed_uri}"')
|
|
162
|
+
|
|
163
|
+
def import_templates(
|
|
164
|
+
self, manager: "ResourceManager", prefix: str | None = None
|
|
165
|
+
) -> None:
|
|
166
|
+
"""Import resource templates from another resource manager.
|
|
167
|
+
|
|
168
|
+
Templates are imported with a prefixed URI template if a prefix is provided.
|
|
169
|
+
For example, if a template has URI template "data://users/{id}" and you import
|
|
170
|
+
it with prefix "app+", the imported template will have URI template
|
|
171
|
+
"app+data://users/{id}". If no prefix is provided, the original URI template is used.
|
|
172
|
+
|
|
173
|
+
Args:
|
|
174
|
+
manager: The ResourceManager to import templates from
|
|
175
|
+
prefix: A prefix to apply to the template URIs, including the delimiter.
|
|
176
|
+
For example, "app+" would result in URI templates like "app+data://users/{id}".
|
|
177
|
+
If None, the original URI template is used.
|
|
178
|
+
"""
|
|
179
|
+
for uri_template, template in manager._templates.items():
|
|
180
|
+
# Create prefixed URI template and copy the template with the new URI template
|
|
181
|
+
prefixed_uri_template = (
|
|
182
|
+
f"{prefix}{uri_template}" if prefix else uri_template
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
new_template = template.copy(
|
|
186
|
+
updates=dict(uri_template=prefixed_uri_template)
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
# Store directly in templates dictionary
|
|
190
|
+
self.add_template(new_template)
|
|
191
|
+
logger.debug(
|
|
192
|
+
f'Imported template "{uri_template}" as "{prefixed_uri_template}"'
|
|
193
|
+
)
|
|
@@ -1,12 +1,17 @@
|
|
|
1
1
|
"""Resource template functionality."""
|
|
2
2
|
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
3
5
|
import inspect
|
|
4
6
|
import re
|
|
5
|
-
from
|
|
7
|
+
from collections.abc import Callable
|
|
8
|
+
from typing import Annotated, Any
|
|
6
9
|
|
|
7
|
-
from pydantic import BaseModel, Field, TypeAdapter, validate_call
|
|
10
|
+
from pydantic import BaseModel, BeforeValidator, Field, TypeAdapter, validate_call
|
|
11
|
+
from typing_extensions import Self
|
|
8
12
|
|
|
9
13
|
from fastmcp.resources.types import FunctionResource, Resource
|
|
14
|
+
from fastmcp.utilities.types import _convert_set_defaults
|
|
10
15
|
|
|
11
16
|
|
|
12
17
|
class ResourceTemplate(BaseModel):
|
|
@@ -17,21 +22,27 @@ class ResourceTemplate(BaseModel):
|
|
|
17
22
|
)
|
|
18
23
|
name: str = Field(description="Name of the resource")
|
|
19
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
|
+
)
|
|
20
28
|
mime_type: str = Field(
|
|
21
29
|
default="text/plain", description="MIME type of the resource content"
|
|
22
30
|
)
|
|
23
|
-
fn: Callable
|
|
24
|
-
parameters: dict = Field(
|
|
31
|
+
fn: Callable[..., Any]
|
|
32
|
+
parameters: dict[str, Any] = Field(
|
|
33
|
+
description="JSON schema for function parameters"
|
|
34
|
+
)
|
|
25
35
|
|
|
26
36
|
@classmethod
|
|
27
37
|
def from_function(
|
|
28
38
|
cls,
|
|
29
|
-
fn: Callable,
|
|
39
|
+
fn: Callable[..., Any],
|
|
30
40
|
uri_template: str,
|
|
31
|
-
name:
|
|
32
|
-
description:
|
|
33
|
-
mime_type:
|
|
34
|
-
|
|
41
|
+
name: str | None = None,
|
|
42
|
+
description: str | None = None,
|
|
43
|
+
mime_type: str | None = None,
|
|
44
|
+
tags: set[str] | None = None,
|
|
45
|
+
) -> ResourceTemplate:
|
|
35
46
|
"""Create a template from a function."""
|
|
36
47
|
func_name = name or fn.__name__
|
|
37
48
|
if func_name == "<lambda>":
|
|
@@ -50,9 +61,10 @@ class ResourceTemplate(BaseModel):
|
|
|
50
61
|
mime_type=mime_type or "text/plain",
|
|
51
62
|
fn=fn,
|
|
52
63
|
parameters=parameters,
|
|
64
|
+
tags=tags or set(),
|
|
53
65
|
)
|
|
54
66
|
|
|
55
|
-
def matches(self, uri: str) ->
|
|
67
|
+
def matches(self, uri: str) -> dict[str, Any] | None:
|
|
56
68
|
"""Check if URI matches template and extract parameters."""
|
|
57
69
|
# Convert template to regex pattern
|
|
58
70
|
pattern = self.uri_template.replace("{", "(?P<").replace("}", ">[^/]+)")
|
|
@@ -61,7 +73,7 @@ class ResourceTemplate(BaseModel):
|
|
|
61
73
|
return match.groupdict()
|
|
62
74
|
return None
|
|
63
75
|
|
|
64
|
-
async def create_resource(self, uri: str, params:
|
|
76
|
+
async def create_resource(self, uri: str, params: dict[str, Any]) -> Resource:
|
|
65
77
|
"""Create a resource from the template with the given parameters."""
|
|
66
78
|
try:
|
|
67
79
|
# Call function and check if result is a coroutine
|
|
@@ -75,6 +87,19 @@ class ResourceTemplate(BaseModel):
|
|
|
75
87
|
description=self.description,
|
|
76
88
|
mime_type=self.mime_type,
|
|
77
89
|
fn=lambda: result, # Capture result in closure
|
|
90
|
+
tags=self.tags,
|
|
78
91
|
)
|
|
79
92
|
except Exception as e:
|
|
80
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
|
@@ -1,16 +1,19 @@
|
|
|
1
1
|
"""Concrete resource implementations."""
|
|
2
2
|
|
|
3
|
-
import
|
|
3
|
+
import inspect
|
|
4
4
|
import json
|
|
5
|
+
from collections.abc import Callable
|
|
5
6
|
from pathlib import Path
|
|
6
|
-
from typing import Any
|
|
7
|
+
from typing import Any
|
|
7
8
|
|
|
9
|
+
import anyio
|
|
10
|
+
import anyio.to_thread
|
|
8
11
|
import httpx
|
|
9
12
|
import pydantic.json
|
|
10
13
|
import pydantic_core
|
|
11
14
|
from pydantic import Field, ValidationInfo
|
|
12
15
|
|
|
13
|
-
from fastmcp.resources.
|
|
16
|
+
from fastmcp.resources.resource import Resource
|
|
14
17
|
|
|
15
18
|
|
|
16
19
|
class TextResource(Resource):
|
|
@@ -46,12 +49,14 @@ class FunctionResource(Resource):
|
|
|
46
49
|
- other types will be converted to JSON
|
|
47
50
|
"""
|
|
48
51
|
|
|
49
|
-
fn: Callable[[], Any]
|
|
52
|
+
fn: Callable[[], Any]
|
|
50
53
|
|
|
51
|
-
async def read(self) ->
|
|
54
|
+
async def read(self) -> str | bytes:
|
|
52
55
|
"""Read the resource by calling the wrapped function."""
|
|
53
56
|
try:
|
|
54
|
-
result =
|
|
57
|
+
result = (
|
|
58
|
+
await self.fn() if inspect.iscoroutinefunction(self.fn) else self.fn()
|
|
59
|
+
)
|
|
55
60
|
if isinstance(result, Resource):
|
|
56
61
|
return await result.read()
|
|
57
62
|
if isinstance(result, bytes):
|
|
@@ -100,12 +105,12 @@ class FileResource(Resource):
|
|
|
100
105
|
mime_type = info.data.get("mime_type", "text/plain")
|
|
101
106
|
return not mime_type.startswith("text/")
|
|
102
107
|
|
|
103
|
-
async def read(self) ->
|
|
108
|
+
async def read(self) -> str | bytes:
|
|
104
109
|
"""Read the file content."""
|
|
105
110
|
try:
|
|
106
111
|
if self.is_binary:
|
|
107
|
-
return await
|
|
108
|
-
return await
|
|
112
|
+
return await anyio.to_thread.run_sync(self.path.read_bytes)
|
|
113
|
+
return await anyio.to_thread.run_sync(self.path.read_text)
|
|
109
114
|
except Exception as e:
|
|
110
115
|
raise ValueError(f"Error reading file {self.path}: {e}")
|
|
111
116
|
|
|
@@ -114,11 +119,11 @@ class HttpResource(Resource):
|
|
|
114
119
|
"""A resource that reads from an HTTP endpoint."""
|
|
115
120
|
|
|
116
121
|
url: str = Field(description="URL to fetch content from")
|
|
117
|
-
mime_type: str
|
|
122
|
+
mime_type: str = Field(
|
|
118
123
|
default="application/json", description="MIME type of the resource content"
|
|
119
124
|
)
|
|
120
125
|
|
|
121
|
-
async def read(self) ->
|
|
126
|
+
async def read(self) -> str | bytes:
|
|
122
127
|
"""Read the HTTP content."""
|
|
123
128
|
async with httpx.AsyncClient() as client:
|
|
124
129
|
response = await client.get(self.url)
|
|
@@ -136,7 +141,7 @@ class DirectoryResource(Resource):
|
|
|
136
141
|
pattern: str | None = Field(
|
|
137
142
|
default=None, description="Optional glob pattern to filter files"
|
|
138
143
|
)
|
|
139
|
-
mime_type: str
|
|
144
|
+
mime_type: str = Field(
|
|
140
145
|
default="application/json", description="MIME type of the resource content"
|
|
141
146
|
)
|
|
142
147
|
|
|
@@ -173,7 +178,7 @@ class DirectoryResource(Resource):
|
|
|
173
178
|
async def read(self) -> str: # Always returns JSON string
|
|
174
179
|
"""Read the directory listing."""
|
|
175
180
|
try:
|
|
176
|
-
files = await
|
|
181
|
+
files = await anyio.to_thread.run_sync(self.list_files)
|
|
177
182
|
file_list = [str(f.relative_to(self.path)) for f in files if f.is_file()]
|
|
178
183
|
return json.dumps({"files": file_list}, indent=2)
|
|
179
184
|
except Exception as e:
|