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
6 changed files with 316 additions and 4 deletions

View file

@ -155,3 +155,38 @@ class TestFunctionResource:
assert resource.mime_type == "text/plain"
assert resource.name == "test"
assert resource.uri == AnyUrl("function://test")
class TestFunctionResourceMetadata:
def test_from_function_with_metadata(self):
# from_function() accepts meta dict and stores it on the resource for static resources
def get_data() -> str: # pragma: no cover
return "test data"
metadata = {"cache_ttl": 300, "tags": ["data", "readonly"]}
resource = FunctionResource.from_function(
fn=get_data,
uri="resource://data",
meta=metadata,
)
assert resource.meta is not None
assert resource.meta == metadata
assert resource.meta["cache_ttl"] == 300
assert "data" in resource.meta["tags"]
assert "readonly" in resource.meta["tags"]
def test_from_function_without_metadata(self):
# meta parameter is optional and defaults to None for backward compatibility
def get_data() -> str: # pragma: no cover
return "test data"
resource = FunctionResource.from_function(
fn=get_data,
uri="resource://data",
)
assert resource.meta is None

View file

@ -134,3 +134,43 @@ class TestResourceManager:
resources = manager.list_resources()
assert len(resources) == 2
assert resources == [resource1, resource2]
class TestResourceManagerMetadata:
"""Test ResourceManager Metadata"""
def test_add_template_with_metadata(self):
"""Test that ResourceManager.add_template() accepts and passes meta parameter."""
manager = ResourceManager()
def get_item(id: str) -> str: # pragma: no cover
return f"Item {id}"
metadata = {"source": "database", "cached": True}
template = manager.add_template(
fn=get_item,
uri_template="resource://items/{id}",
meta=metadata,
)
assert template.meta is not None
assert template.meta == metadata
assert template.meta["source"] == "database"
assert template.meta["cached"] is True
def test_add_template_without_metadata(self):
"""Test that ResourceManager.add_template() works without meta parameter."""
manager = ResourceManager()
def get_item(id: str) -> str: # pragma: no cover
return f"Item {id}"
template = manager.add_template(
fn=get_item,
uri_template="resource://items/{id}",
)
assert template.meta is None

View file

@ -258,3 +258,50 @@ class TestResourceTemplateAnnotations:
# Verify the resource works correctly
content = await resource.read()
assert content == "Item 123"
class TestResourceTemplateMetadata:
"""Test ResourceTemplate meta handling."""
def test_template_from_function_with_metadata(self):
"""Test that ResourceTemplate.from_function() accepts and stores meta parameter."""
def get_user(user_id: str) -> str: # pragma: no cover
return f"User {user_id}"
metadata = {"requires_auth": True, "rate_limit": 100}
template = ResourceTemplate.from_function(
fn=get_user,
uri_template="resource://users/{user_id}",
meta=metadata,
)
assert template.meta is not None
assert template.meta == metadata
assert template.meta["requires_auth"] is True
assert template.meta["rate_limit"] == 100
@pytest.mark.anyio
async def test_template_created_resources_inherit_metadata(self):
"""Test that resources created from templates inherit meta from template."""
def get_item(item_id: str) -> str:
return f"Item {item_id}"
metadata = {"category": "inventory", "cacheable": True}
template = ResourceTemplate.from_function(
fn=get_item,
uri_template="resource://items/{item_id}",
meta=metadata,
)
# Create a resource from the template
resource = await template.create_resource("resource://items/123", {"item_id": "123"})
# The resource should inherit the template's metadata
assert resource.meta is not None
assert resource.meta == metadata
assert resource.meta["category"] == "inventory"
assert resource.meta["cacheable"] is True

View file

@ -193,3 +193,41 @@ class TestAnnotationsValidation:
# Invalid roles should raise validation error
with pytest.raises(Exception): # Pydantic validation error
Annotations(audience=["invalid_role"]) # type: ignore
class TestResourceMetadata:
"""Test metadata field on base Resource class."""
def test_resource_with_metadata(self):
"""Test that Resource base class accepts meta parameter."""
def dummy_func() -> str: # pragma: no cover
return "data"
metadata = {"version": "1.0", "category": "test"}
resource = FunctionResource(
uri=AnyUrl("resource://test"),
name="test",
fn=dummy_func,
meta=metadata,
)
assert resource.meta is not None
assert resource.meta == metadata
assert resource.meta["version"] == "1.0"
assert resource.meta["category"] == "test"
def test_resource_without_metadata(self):
"""Test that meta field defaults to None."""
def dummy_func() -> str: # pragma: no cover
return "data"
resource = FunctionResource(
uri=AnyUrl("resource://test"),
name="test",
fn=dummy_func,
)
assert resource.meta is None

View file

@ -953,6 +953,74 @@ class TestServerResourceTemplates:
assert result.contents[0].text == "csv for bob"
class TestServerResourceMetadata:
"""Test FastMCP @resource decorator meta parameter for list operations.
Meta flows: @resource decorator -> resource/template storage -> list_resources/list_resource_templates.
Note: read_resource does NOT pass meta to protocol response (lowlevel/server.py only extracts content/mime_type).
"""
@pytest.mark.anyio
async def test_resource_decorator_with_metadata(self):
"""Test that @resource decorator accepts and passes meta parameter."""
# Tests static resource flow: decorator -> FunctionResource -> list_resources (server.py:544,635,361)
mcp = FastMCP()
metadata = {"ui": {"component": "file-viewer"}, "priority": "high"}
@mcp.resource("resource://config", meta=metadata)
def get_config() -> str: # pragma: no cover
return '{"debug": false}'
resources = await mcp.list_resources()
assert len(resources) == 1
assert resources[0].meta is not None
assert resources[0].meta == metadata
assert resources[0].meta["ui"]["component"] == "file-viewer"
assert resources[0].meta["priority"] == "high"
@pytest.mark.anyio
async def test_resource_template_decorator_with_metadata(self):
"""Test that @resource decorator passes meta to templates."""
# Tests template resource flow: decorator -> add_template() -> list_resource_templates (server.py:544,622,377)
mcp = FastMCP()
metadata = {"api_version": "v2", "deprecated": False}
@mcp.resource("resource://{city}/weather", meta=metadata)
def get_weather(city: str) -> str: # pragma: no cover
return f"Weather for {city}"
templates = await mcp.list_resource_templates()
assert len(templates) == 1
assert templates[0].meta is not None
assert templates[0].meta == metadata
assert templates[0].meta["api_version"] == "v2"
@pytest.mark.anyio
async def test_read_resource_returns_meta(self):
"""Test that read_resource includes meta in response."""
# Tests end-to-end: Resource.meta -> ReadResourceContents.meta -> protocol _meta (lowlevel/server.py:341,371)
mcp = FastMCP()
metadata = {"version": "1.0", "category": "config"}
@mcp.resource("resource://data", meta=metadata)
def get_data() -> str:
return "test data"
async with client_session(mcp._mcp_server) as client:
result = await client.read_resource(AnyUrl("resource://data"))
# Verify content and metadata in protocol response
assert isinstance(result.contents[0], TextResourceContents)
assert result.contents[0].text == "test data"
assert result.contents[0].meta is not None
assert result.contents[0].meta == metadata
assert result.contents[0].meta["version"] == "1.0"
assert result.contents[0].meta["category"] == "config"
class TestContextInjection:
"""Test context injection in tools, resources, and prompts."""

View file

@ -0,0 +1,60 @@
"""Test helper_types.py meta field.
These tests verify the changes made to helper_types.py:11 where we added:
meta: dict[str, Any] | None = field(default=None)
ReadResourceContents is the return type for resource read handlers. It's used internally
by the low-level server to package resource content before sending it over the MCP protocol.
"""
from mcp.server.lowlevel.helper_types import ReadResourceContents
class TestReadResourceContentsMetadata:
"""Test ReadResourceContents meta field.
ReadResourceContents is an internal helper type used by the low-level MCP server.
When a resource is read, the server creates a ReadResourceContents instance that
contains the content, mime type, and now metadata. The low-level server then
extracts the meta field and includes it in the protocol response as _meta.
"""
def test_read_resource_contents_with_metadata(self):
"""Test that ReadResourceContents accepts meta parameter."""
# Bridge between Resource.meta and MCP protocol _meta field (helper_types.py:11)
metadata = {"version": "1.0", "cached": True}
contents = ReadResourceContents(
content="test content",
mime_type="text/plain",
meta=metadata,
)
assert contents.meta is not None
assert contents.meta == metadata
assert contents.meta["version"] == "1.0"
assert contents.meta["cached"] is True
def test_read_resource_contents_without_metadata(self):
"""Test that ReadResourceContents meta defaults to None."""
# Ensures backward compatibility - meta defaults to None, _meta omitted from protocol (helper_types.py:11)
contents = ReadResourceContents(
content="test content",
mime_type="text/plain",
)
assert contents.meta is None
def test_read_resource_contents_with_bytes(self):
"""Test that ReadResourceContents works with bytes content and meta."""
# Verifies meta works with both str and bytes content (binary resources like images, PDFs)
metadata = {"encoding": "utf-8"}
contents = ReadResourceContents(
content=b"binary content",
mime_type="application/octet-stream",
meta=metadata,
)
assert contents.content == b"binary content"
assert contents.meta == metadata