Backport: Support for Resource and ResourceTemplate metadata (#1928)

Co-authored-by: Jacem Elwaar <jacem@mcpappsbuilders.com>
This commit is contained in:
Max Isbey 2026-01-22 14:02:25 +00:00 • committed by GitHub
commit d891525958
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 316 additions and 4 deletions

View file

@ -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

View file

@ -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

View file

@ -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:

View file

@ -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,
)

View file

@ -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

View file

@ -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

View file

@ -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(