Spaces:
Running
Running
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()
|