From 9327bcceeccbe87162568933bfe2ef35e0558865 Mon Sep 17 00:00:00 2001 From: rinaldofesta Date: Mon, 24 Nov 2025 13:30:15 +0100 Subject: [PATCH] feat(fastmcp): add include_in_context parameter to resource decorator Add a user-friendly include_in_context boolean parameter to the @resource decorator that automatically sets priority=1.0 in annotations, making it easier for users to mark resources for context inclusion without manually managing annotation objects. Changes: - Add include_in_context parameter to resource decorator - Automatically create Annotations(priority=1.0) when enabled - Override explicit priority when include_in_context=True - Preserve other annotation fields (audience) - Support both regular and template resources - Update docstring with new parameter documentation - Add comprehensive test coverage (6 new tests) The parameter provides a simple API for context inclusion: @mcp.resource("resource://data", include_in_context=True) def get_data(): return "important data" --- src/mcp/server/fastmcp/server.py | 17 +++- .../fastmcp/resources/test_resources.py | 92 +++++++++++++++++++ 2 files changed, 107 insertions(+), 2 deletions(-) diff --git a/src/mcp/server/fastmcp/server.py b/src/mcp/server/fastmcp/server.py index 865b8e7e72..20bb5b3a4f 100644 --- a/src/mcp/server/fastmcp/server.py +++ b/src/mcp/server/fastmcp/server.py @@ -525,6 +525,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. @@ -543,6 +544,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") @@ -571,6 +575,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 @@ -600,7 +613,7 @@ def decorator(fn: AnyFunction) -> AnyFunction: description=description, mime_type=mime_type, icons=icons, - annotations=annotations, + annotations=processed_annotations, ) else: # Register as regular resource @@ -612,7 +625,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 diff --git a/tests/server/fastmcp/resources/test_resources.py b/tests/server/fastmcp/resources/test_resources.py index 32fc23b174..57b2ee8d5e 100644 --- a/tests/server/fastmcp/resources/test_resources.py +++ b/tests/server/fastmcp/resources/test_resources.py @@ -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