Backport: Support for Resource and ResourceTemplate metadata (#1928)
Co-authored-by: Jacem Elwaar <jacem@mcpappsbuilders.com>
This commit is contained in:
parent
d3787c96b4
commit
d891525958
7 changed files with 316 additions and 4 deletions
|
|
@ -1,7 +1,7 @@
|
|||
"""Base classes and interfaces for FastMCP resources."""
|
||||
|
||||
import abc
|
||||
from typing import Annotated
|
||||
from typing import Annotated, Any
|
||||
|
||||
from pydantic import (
|
||||
AnyUrl,
|
||||
|
|
@ -32,6 +32,7 @@ class Resource(BaseModel, abc.ABC):
|
|||
)
|
||||
icons: list[Icon] | None = Field(default=None, description="Optional list of icons for this resource")
|
||||
annotations: Annotations | None = Field(default=None, description="Optional annotations for the resource")
|
||||
meta: dict[str, Any] | None = Field(default=None, description="Optional metadata for this resource")
|
||||
|
||||
@field_validator("name", mode="before")
|
||||
@classmethod
|
||||
|
|
|
|||
|
|
@ -64,6 +64,7 @@ class ResourceManager:
|
|||
mime_type: str | None = None,
|
||||
icons: list[Icon] | None = None,
|
||||
annotations: Annotations | None = None,
|
||||
meta: dict[str, Any] | None = None,
|
||||
) -> ResourceTemplate:
|
||||
"""Add a template from a function."""
|
||||
template = ResourceTemplate.from_function(
|
||||
|
|
@ -75,6 +76,7 @@ class ResourceManager:
|
|||
mime_type=mime_type,
|
||||
icons=icons,
|
||||
annotations=annotations,
|
||||
meta=meta,
|
||||
)
|
||||
self._templates[template.uri_template] = template
|
||||
return template
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ class ResourceTemplate(BaseModel):
|
|||
mime_type: str = Field(default="text/plain", description="MIME type of the resource content")
|
||||
icons: list[Icon] | None = Field(default=None, description="Optional list of icons for the resource template")
|
||||
annotations: Annotations | None = Field(default=None, description="Optional annotations for the resource template")
|
||||
meta: dict[str, Any] | None = Field(default=None, description="Optional metadata for this resource template")
|
||||
fn: Callable[..., Any] = Field(exclude=True)
|
||||
parameters: dict[str, Any] = Field(description="JSON schema for function parameters")
|
||||
context_kwarg: str | None = Field(None, description="Name of the kwarg that should receive context")
|
||||
|
|
@ -45,6 +46,7 @@ class ResourceTemplate(BaseModel):
|
|||
mime_type: str | None = None,
|
||||
icons: list[Icon] | None = None,
|
||||
annotations: Annotations | None = None,
|
||||
meta: dict[str, Any] | None = None,
|
||||
context_kwarg: str | None = None,
|
||||
) -> ResourceTemplate:
|
||||
"""Create a template from a function."""
|
||||
|
|
@ -74,6 +76,7 @@ class ResourceTemplate(BaseModel):
|
|||
mime_type=mime_type or "text/plain",
|
||||
icons=icons,
|
||||
annotations=annotations,
|
||||
meta=meta,
|
||||
fn=fn,
|
||||
parameters=parameters,
|
||||
context_kwarg=context_kwarg,
|
||||
|
|
@ -112,6 +115,7 @@ class ResourceTemplate(BaseModel):
|
|||
mime_type=self.mime_type,
|
||||
icons=self.icons,
|
||||
annotations=self.annotations,
|
||||
meta=self.meta,
|
||||
fn=lambda: result, # Capture result in closure
|
||||
)
|
||||
except Exception as e:
|
||||
|
|
|
|||
|
|
@ -83,6 +83,7 @@ class FunctionResource(Resource):
|
|||
mime_type: str | None = None,
|
||||
icons: list[Icon] | None = None,
|
||||
annotations: Annotations | None = None,
|
||||
meta: dict[str, Any] | None = None,
|
||||
) -> "FunctionResource":
|
||||
"""Create a FunctionResource from a function."""
|
||||
func_name = name or fn.__name__
|
||||
|
|
@ -101,6 +102,7 @@ class FunctionResource(Resource):
|
|||
fn=fn,
|
||||
icons=icons,
|
||||
annotations=annotations,
|
||||
meta=meta,
|
||||
)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -358,6 +358,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|||
mimeType=resource.mime_type,
|
||||
icons=resource.icons,
|
||||
annotations=resource.annotations,
|
||||
_meta=resource.meta,
|
||||
)
|
||||
for resource in resources
|
||||
]
|
||||
|
|
@ -373,6 +374,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|||
mimeType=template.mime_type,
|
||||
icons=template.icons,
|
||||
annotations=template.annotations,
|
||||
_meta=template.meta,
|
||||
)
|
||||
for template in templates
|
||||
]
|
||||
|
|
@ -387,7 +389,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|||
|
||||
try:
|
||||
content = await resource.read()
|
||||
return [ReadResourceContents(content=content, mime_type=resource.mime_type)]
|
||||
return [ReadResourceContents(content=content, mime_type=resource.mime_type, meta=resource.meta)]
|
||||
except Exception as e: # pragma: no cover
|
||||
logger.exception(f"Error reading resource {uri}")
|
||||
raise ResourceError(str(e))
|
||||
|
|
@ -539,6 +541,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|||
mime_type: str | None = None,
|
||||
icons: list[Icon] | None = None,
|
||||
annotations: Annotations | None = None,
|
||||
meta: dict[str, Any] | None = None,
|
||||
) -> Callable[[AnyFunction], AnyFunction]:
|
||||
"""Decorator to register a function as a resource.
|
||||
|
||||
|
|
@ -557,6 +560,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|||
title: Optional human-readable title for the resource
|
||||
description: Optional description of the resource
|
||||
mime_type: Optional MIME type for the resource
|
||||
meta: Optional metadata dictionary for the resource
|
||||
|
||||
Example:
|
||||
@server.resource("resource://my-resource")
|
||||
|
|
@ -615,6 +619,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|||
mime_type=mime_type,
|
||||
icons=icons,
|
||||
annotations=annotations,
|
||||
meta=meta,
|
||||
)
|
||||
else:
|
||||
# Register as regular resource
|
||||
|
|
@ -627,6 +632,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|||
mime_type=mime_type,
|
||||
icons=icons,
|
||||
annotations=annotations,
|
||||
meta=meta,
|
||||
)
|
||||
self.add_resource(resource)
|
||||
return fn
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
|
||||
|
||||
@dataclass
|
||||
|
|
@ -7,3 +8,4 @@ class ReadResourceContents:
|
|||
|
||||
content: str | bytes
|
||||
mime_type: str | None = None
|
||||
meta: dict[str, Any] | None = None
|
||||
|
|
|
|||
|
|
@ -338,19 +338,23 @@ class Server(Generic[LifespanResultT, RequestT]):
|
|||
async def handler(req: types.ReadResourceRequest):
|
||||
result = await func(req.params.uri)
|
||||
|
||||
def create_content(data: str | bytes, mime_type: str | None):
|
||||
def create_content(data: str | bytes, mime_type: str | None, meta: dict[str, Any] | None = None):
|
||||
# Note: ResourceContents uses Field(alias="_meta"), so we must use the alias key
|
||||
meta_kwargs: dict[str, Any] = {"_meta": meta} if meta is not None else {}
|
||||
match data:
|
||||
case str() as data:
|
||||
return types.TextResourceContents(
|
||||
uri=req.params.uri,
|
||||
text=data,
|
||||
mimeType=mime_type or "text/plain",
|
||||
**meta_kwargs,
|
||||
)
|
||||
case bytes() as data: # pragma: no cover
|
||||
return types.BlobResourceContents(
|
||||
uri=req.params.uri,
|
||||
blob=base64.b64encode(data).decode(),
|
||||
mimeType=mime_type or "application/octet-stream",
|
||||
**meta_kwargs,
|
||||
)
|
||||
|
||||
match result:
|
||||
|
|
@ -364,7 +368,10 @@ class Server(Generic[LifespanResultT, RequestT]):
|
|||
content = create_content(data, None)
|
||||
case Iterable() as contents:
|
||||
contents_list = [
|
||||
create_content(content_item.content, content_item.mime_type) for content_item in contents
|
||||
create_content(
|
||||
content_item.content, content_item.mime_type, getattr(content_item, "meta", None)
|
||||
)
|
||||
for content_item in contents
|
||||
]
|
||||
return types.ServerResult(
|
||||
types.ReadResourceResult(
|
||||
|
|
|
|||
Loading…
Reference in a new issue