fix: preserve annotations, meta, title, icons when creating resources from templates (#4061)

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Bill Easton 2026-05-20 08:38:19 -05:00 committed by GitHub
commit 01b971d80d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 99 additions and 0 deletions

View file

@ -442,6 +442,10 @@ class FunctionResourceTemplate(ResourceTemplate):
description=self.description,
mime_type=self.mime_type,
tags=self.tags,
annotations=self.annotations,
meta=self.meta,
title=self.title,
icons=self.icons,
task=self.task_config,
auth=self.auth,
)

View file

@ -381,6 +381,11 @@ class FastMCPProviderResourceTemplate(ResourceTemplate):
name=self.name,
description=self.description,
mime_type=self.mime_type,
tags=self.tags,
annotations=self.annotations,
meta=self.meta,
title=self.title,
icons=self.icons,
)
@overload

View file

@ -1,4 +1,11 @@
from mcp.types import Annotations, Icon
from fastmcp import FastMCP
from fastmcp.resources import ResourceTemplate
from fastmcp.server.providers.fastmcp_provider import (
FastMCPProvider,
FastMCPProviderResourceTemplate,
)
class TestResourceTemplateMeta:
@ -23,3 +30,86 @@ class TestResourceTemplateMeta:
# MCP template includes fastmcp meta, so check that our meta is included
assert mcp_template.meta is not None
assert meta_data.items() <= mcp_template.meta.items()
class TestResourceTemplateFieldPreservation:
"""Regression for #4061: annotations/meta/title/icons must survive
materialization of a Resource from a ResourceTemplate."""
def _template(self) -> ResourceTemplate:
def fn(param: str) -> str:
return f"value-{param}"
return ResourceTemplate.from_function(
fn=fn,
uri_template="data://{param}",
name="t",
title="Human Title",
meta={"owner": "team-a"},
icons=[Icon(src="https://example.com/icon.png", mimeType="image/png")],
annotations=Annotations(priority=0.5, audience=["user"]),
)
async def test_function_template_create_resource_preserves_all_fields(self):
template = self._template()
resource = await template.create_resource("data://x", {"param": "x"})
assert resource.title == "Human Title"
assert resource.meta == {"owner": "team-a"}
assert resource.annotations is not None
assert resource.annotations.priority == 0.5
assert resource.annotations.audience == ["user"]
assert resource.icons is not None
assert len(resource.icons) == 1
assert str(resource.icons[0].src) == "https://example.com/icon.png"
async def test_created_resource_meta_is_not_aliased_to_template(self):
"""Mutating the materialized resource's meta must not bleed back into
the template (each materialization must be independent)."""
template = self._template()
resource = await template.create_resource("data://x", {"param": "x"})
assert resource.meta is not None
resource.meta["mutated"] = True
assert "mutated" not in (template.meta or {})
async def test_fastmcp_provider_template_preserves_fields_without_double_wrap(
self,
):
"""The FastMCPProvider path wraps template.get_meta() (already
namespaced). Re-materializing must not nest fastmcp under fastmcp."""
sub = FastMCP("sub")
@sub.resource(
"data://{param}",
title="Sub Title",
meta={"owner": "team-b"},
icons=[Icon(src="https://example.com/s.png", mimeType="image/png")],
annotations=Annotations(priority=0.9),
tags={"alpha"},
)
def fn(param: str) -> str:
return f"sub-{param}"
provider = FastMCPProvider(sub)
templates = await provider._list_resource_templates()
assert len(templates) == 1
wrapped = templates[0]
assert isinstance(wrapped, FastMCPProviderResourceTemplate)
resource = await wrapped.create_resource("data://x", {"param": "x"})
assert resource.title == "Sub Title"
assert resource.annotations is not None
assert resource.annotations.priority == 0.9
assert resource.icons is not None and len(resource.icons) == 1
mcp_resource = resource.to_mcp_resource()
assert mcp_resource.meta is not None
# User meta survives end-to-end.
assert mcp_resource.meta.get("owner") == "team-b"
# Exactly one fastmcp namespace, and it is NOT double-wrapped.
fastmcp_ns = mcp_resource.meta.get("fastmcp")
assert isinstance(fastmcp_ns, dict)
assert "fastmcp" not in fastmcp_ns
assert "alpha" in fastmcp_ns.get("tags", [])