Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 15 additions & 2 deletions src/mcp/server/fastmcp/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -529,6 +529,7 @@ def resource(
mime_type: str | None = None,
icons: list[Icon] | None = None,
annotations: Annotations | None = None,
include_in_context: bool = False,
) -> Callable[[AnyFunction], AnyFunction]:
"""Decorator to register a function as a resource.

Expand All @@ -547,6 +548,9 @@ def resource(
title: Optional human-readable title for the resource
description: Optional description of the resource
mime_type: Optional MIME type for the resource
icons: Optional list of icons for the resource
annotations: Optional annotations for the resource (audience, priority)
include_in_context: If True, automatically sets priority to 1.0 for context inclusion

Example:
@server.resource("resource://my-resource")
Expand Down Expand Up @@ -575,6 +579,15 @@ async def get_weather(city: str) -> str:
)

def decorator(fn: AnyFunction) -> AnyFunction:
# Handle include_in_context parameter
processed_annotations = annotations
if include_in_context:
if processed_annotations is None:
processed_annotations = Annotations(priority=1.0)
else:
# Override priority to 1.0, preserving other fields
processed_annotations = Annotations(audience=processed_annotations.audience, priority=1.0)

# Check if this should be a template
sig = inspect.signature(fn)
has_uri_params = "{" in uri and "}" in uri
Expand Down Expand Up @@ -604,7 +617,7 @@ def decorator(fn: AnyFunction) -> AnyFunction:
description=description,
mime_type=mime_type,
icons=icons,
annotations=annotations,
annotations=processed_annotations,
)
else:
# Register as regular resource
Expand All @@ -616,7 +629,7 @@ def decorator(fn: AnyFunction) -> AnyFunction:
description=description,
mime_type=mime_type,
icons=icons,
annotations=annotations,
annotations=processed_annotations,
)
self.add_resource(resource)
return fn
Expand Down
92 changes: 92 additions & 0 deletions tests/server/fastmcp/resources/test_resources.py
Original file line number Diff line number Diff line change
Expand Up @@ -193,3 +193,95 @@ def test_audience_validation(self):
# Invalid roles should raise validation error
with pytest.raises(Exception): # Pydantic validation error
Annotations(audience=["invalid_role"]) # type: ignore


class TestIncludeInContext:
"""Test the include_in_context parameter."""

@pytest.mark.anyio
async def test_include_in_context_sets_priority(self):
"""Test that include_in_context=True sets priority to 1.0."""
mcp = FastMCP()

@mcp.resource("resource://important", include_in_context=True)
def get_important() -> str: # pragma: no cover
return "important data"

resources = await mcp.list_resources()
assert len(resources) == 1
assert resources[0].annotations is not None
assert resources[0].annotations.priority == 1.0

@pytest.mark.anyio
async def test_include_in_context_false_no_priority(self):
"""Test that include_in_context=False doesn't set priority."""
mcp = FastMCP()

@mcp.resource("resource://normal", include_in_context=False)
def get_normal() -> str: # pragma: no cover
return "normal data"

resources = await mcp.list_resources()
assert len(resources) == 1
assert resources[0].annotations is None

@pytest.mark.anyio
async def test_include_in_context_overrides_explicit_priority(self):
"""Test that include_in_context=True overrides explicit priority."""
mcp = FastMCP()

@mcp.resource("resource://override", include_in_context=True, annotations=Annotations(priority=0.3))
def get_override() -> str: # pragma: no cover
return "overridden"

resources = await mcp.list_resources()
assert len(resources) == 1
assert resources[0].annotations is not None
assert resources[0].annotations.priority == 1.0

@pytest.mark.anyio
async def test_include_in_context_preserves_audience(self):
"""Test that include_in_context preserves existing audience."""
mcp = FastMCP()

@mcp.resource(
"resource://preserve",
include_in_context=True,
annotations=Annotations(audience=["user"], priority=0.5),
)
def get_preserve() -> str: # pragma: no cover
return "preserved audience"

resources = await mcp.list_resources()
assert len(resources) == 1
assert resources[0].annotations is not None
assert resources[0].annotations.priority == 1.0
assert resources[0].annotations.audience == ["user"]

@pytest.mark.anyio
async def test_include_in_context_with_template_resource(self):
"""Test that include_in_context works with template resources."""
mcp = FastMCP()

@mcp.resource("resource://{id}/data", include_in_context=True)
def get_template_data(id: str) -> str: # pragma: no cover
return f"data for {id}"

templates = await mcp.list_resource_templates()
assert len(templates) == 1
assert templates[0].annotations is not None
assert templates[0].annotations.priority == 1.0

@pytest.mark.anyio
async def test_include_in_context_with_async_function(self):
"""Test that include_in_context works with async functions."""
mcp = FastMCP()

@mcp.resource("resource://async", include_in_context=True)
async def get_async() -> str: # pragma: no cover
return "async data"

resources = await mcp.list_resources()
assert len(resources) == 1
assert resources[0].annotations is not None
assert resources[0].annotations.priority == 1.0