diff --git a/src/mcp/server/fastmcp/server.py b/src/mcp/server/fastmcp/server.py index 2e596c9f9a..6a44977687 100644 --- a/src/mcp/server/fastmcp/server.py +++ b/src/mcp/server/fastmcp/server.py @@ -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. @@ -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") @@ -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 @@ -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 @@ -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 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