File size: 6,727 Bytes
48f7143
 
3d30e82
 
48f7143
582f82a
 
3d30e82
48f7143
582f82a
e3f57a4
c5b3684
48f7143
 
 
 
 
 
08e1190
48f7143
08e1190
48f7143
582f82a
c7184ac
582f82a
 
 
316c336
3d30e82
1534a61
3d30e82
48f7143
c7184ac
48f7143
 
 
 
 
 
 
08e1190
48f7143
 
 
 
 
e3f57a4
 
 
 
48f7143
6fe9090
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
582f82a
 
3c230a8
582f82a
 
226fdf7
582f82a
 
 
55471df
e3f57a4
0907e77
582f82a
 
 
 
 
226fdf7
582f82a
 
 
55471df
e3f57a4
0907e77
582f82a
 
c5b3684
 
 
 
 
 
 
 
08e1190
 
48f7143
08e1190
 
 
 
 
 
 
48f7143
 
1534a61
48f7143
 
fa364d7
957b451
 
 
 
 
 
c5b3684
 
 
 
 
 
e98b7a0
e3f57a4
957b451
c5b3684
 
582f82a
596281e
 
 
0fdb55c
 
 
 
 
 
 
 
 
 
582f82a
 
 
 
 
 
 
 
 
 
 
 
 
 
3c230a8
582f82a
 
 
 
3c230a8
582f82a
 
226fdf7
582f82a
 
 
55471df
e3f57a4
0907e77
582f82a
 
 
 
 
 
 
 
226fdf7
06b166f
582f82a
 
55471df
e3f57a4
0907e77
582f82a
 
 
 
 
 
 
 
 
 
 
 
78cdd55
582f82a
 
 
 
 
 
 
 
 
ff32168
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
"""Base classes and interfaces for FastMCP resources."""

from __future__ import annotations

import abc
import inspect
from collections.abc import Callable
from typing import TYPE_CHECKING, Annotated, Any

import pydantic_core
from mcp.types import Annotations
from mcp.types import Resource as MCPResource
from pydantic import (
    AnyUrl,
    ConfigDict,
    Field,
    UrlConstraints,
    field_validator,
    model_validator,
)
from typing_extensions import Self

from fastmcp.server.dependencies import get_context
from fastmcp.utilities.components import FastMCPComponent
from fastmcp.utilities.types import (
    find_kwarg_by_type,
)

if TYPE_CHECKING:
    pass


class Resource(FastMCPComponent, abc.ABC):
    """Base class for all resources."""

    model_config = ConfigDict(validate_default=True)

    uri: Annotated[AnyUrl, UrlConstraints(host_required=False)] = Field(
        default=..., description="URI of the resource"
    )
    name: str = Field(default="", description="Name of the resource")
    mime_type: str = Field(
        default="text/plain",
        description="MIME type of the resource content",
        pattern=r"^[a-zA-Z0-9]+/[a-zA-Z0-9\-+.]+$",
    )
    annotations: Annotated[
        Annotations | None,
        Field(description="Optional annotations about the resource's behavior"),
    ] = None

    def enable(self) -> None:
        super().enable()
        try:
            context = get_context()
            context._queue_resource_list_changed()  # type: ignore[private-use]
        except RuntimeError:
            pass  # No context available

    def disable(self) -> None:
        super().disable()
        try:
            context = get_context()
            context._queue_resource_list_changed()  # type: ignore[private-use]
        except RuntimeError:
            pass  # No context available

    @staticmethod
    def from_function(
        fn: Callable[..., Any],
        uri: str | AnyUrl,
        name: str | None = None,
        title: str | None = None,
        description: str | None = None,
        mime_type: str | None = None,
        tags: set[str] | None = None,
        enabled: bool | None = None,
        annotations: Annotations | None = None,
        meta: dict[str, Any] | None = None,
    ) -> FunctionResource:
        return FunctionResource.from_function(
            fn=fn,
            uri=uri,
            name=name,
            title=title,
            description=description,
            mime_type=mime_type,
            tags=tags,
            enabled=enabled,
            annotations=annotations,
            meta=meta,
        )

    @field_validator("mime_type", mode="before")
    @classmethod
    def set_default_mime_type(cls, mime_type: str | None) -> str:
        """Set default MIME type if not provided."""
        if mime_type:
            return mime_type
        return "text/plain"

    @model_validator(mode="after")
    def set_default_name(self) -> Self:
        """Set default name from URI if not provided."""
        if self.name:
            pass
        elif self.uri:
            self.name = str(self.uri)
        else:
            raise ValueError("Either name or uri must be provided")
        return self

    @abc.abstractmethod
    async def read(self) -> str | bytes:
        """Read the resource content."""
        pass

    def to_mcp_resource(
        self,
        *,
        include_fastmcp_meta: bool | None = None,
        **overrides: Any,
    ) -> MCPResource:
        """Convert the resource to an MCPResource."""
        kwargs = {
            "uri": self.uri,
            "name": self.name,
            "description": self.description,
            "mimeType": self.mime_type,
            "title": self.title,
            "annotations": self.annotations,
            "_meta": self.get_meta(include_fastmcp_meta=include_fastmcp_meta),
        }
        return MCPResource(**kwargs | overrides)

    def __repr__(self) -> str:
        return f"{self.__class__.__name__}(uri={self.uri!r}, name={self.name!r}, description={self.description!r}, tags={self.tags})"

    @property
    def key(self) -> str:
        """
        The key of the component. This is used for internal bookkeeping
        and may reflect e.g. prefixes or other identifiers. You should not depend on
        keys having a certain value, as the same tool loaded from different
        hierarchies of servers may have different keys.
        """
        return self._key or str(self.uri)


class FunctionResource(Resource):
    """A resource that defers data loading by wrapping a function.

    The function is only called when the resource is read, allowing for lazy loading
    of potentially expensive data. This is particularly useful when listing resources,
    as the function won't be called until the resource is actually accessed.

    The function can return:
    - str for text content (default)
    - bytes for binary content
    - other types will be converted to JSON
    """

    fn: Callable[..., Any]

    @classmethod
    def from_function(
        cls,
        fn: Callable[..., Any],
        uri: str | AnyUrl,
        name: str | None = None,
        title: str | None = None,
        description: str | None = None,
        mime_type: str | None = None,
        tags: set[str] | None = None,
        enabled: bool | None = None,
        annotations: Annotations | None = None,
        meta: dict[str, Any] | None = None,
    ) -> FunctionResource:
        """Create a FunctionResource from a function."""
        if isinstance(uri, str):
            uri = AnyUrl(uri)
        return cls(
            fn=fn,
            uri=uri,
            name=name or fn.__name__,
            title=title,
            description=description or inspect.getdoc(fn),
            mime_type=mime_type or "text/plain",
            tags=tags or set(),
            enabled=enabled if enabled is not None else True,
            annotations=annotations,
            meta=meta,
        )

    async def read(self) -> str | bytes:
        """Read the resource by calling the wrapped function."""
        from fastmcp.server.context import Context

        kwargs = {}
        context_kwarg = find_kwarg_by_type(self.fn, kwarg_type=Context)
        if context_kwarg is not None:
            kwargs[context_kwarg] = get_context()

        result = self.fn(**kwargs)
        if inspect.isawaitable(result):
            result = await result

        if isinstance(result, Resource):
            return await result.read()
        elif isinstance(result, bytes):
            return result
        elif isinstance(result, str):
            return result
        else:
            return pydantic_core.to_json(result, fallback=str).decode()