import pytest from mcp_types import Annotations from mcp.server.mcpserver import MCPServer from mcp.server.mcpserver.resources import FunctionResource, Resource class TestResourceValidation: """Test base Resource validation.""" def test_resource_uri_accepts_any_string(self): """Test that URI field accepts any string per MCP spec.""" def dummy_func() -> str: # pragma: no cover return "data" # Valid URI resource = FunctionResource( uri="http://example.com/data", name="test", fn=dummy_func, ) assert resource.uri == "http://example.com/data" # Relative path - now accepted per MCP spec resource = FunctionResource( uri="users/me", name="test", fn=dummy_func, ) assert resource.uri == "users/me" # Custom scheme resource = FunctionResource( uri="custom://resource", name="test", fn=dummy_func, ) assert resource.uri == "custom://resource" def test_resource_name_from_uri(self): """Test name is extracted from URI if not provided.""" def dummy_func() -> str: # pragma: no cover return "data" resource = FunctionResource( uri="resource://my-resource", fn=dummy_func, ) assert resource.name == "resource://my-resource" def test_resource_name_validation(self): """Test name validation.""" def dummy_func() -> str: # pragma: no cover return "data" # Must provide either name or URI with pytest.raises(ValueError, match="Either name or uri must be provided"): FunctionResource( fn=dummy_func, ) # Explicit name takes precedence over URI resource = FunctionResource( uri="resource://uri-name", name="explicit-name", fn=dummy_func, ) assert resource.name == "explicit-name" def test_resource_mime_type(self): """Test mime type handling.""" def dummy_func() -> str: # pragma: no cover return "data" # Default mime type resource = FunctionResource( uri="resource://test", fn=dummy_func, ) assert resource.mime_type == "text/plain" # Custom mime type resource = FunctionResource( uri="resource://test", fn=dummy_func, mime_type="application/json", ) assert resource.mime_type == "application/json" # RFC 2045 quoted parameter value (gh-1756) resource = FunctionResource( uri="resource://test", fn=dummy_func, mime_type='text/plain; charset="utf-8"', ) assert resource.mime_type == 'text/plain; charset="utf-8"' @pytest.mark.anyio async def test_resource_read_abstract(self): """Test that Resource.read() is abstract.""" class ConcreteResource(Resource): pass with pytest.raises(TypeError, match="abstract method"): ConcreteResource(uri="test://test", name="test") # type: ignore class TestResourceAnnotations: """Test annotations on resources.""" def test_resource_with_annotations(self): """Test creating a resource with annotations.""" def get_data() -> str: # pragma: no cover return "data" annotations = Annotations(audience=["user"], priority=0.8) resource = FunctionResource.from_function(fn=get_data, uri="resource://test", annotations=annotations) assert resource.annotations is not None assert resource.annotations.audience == ["user"] assert resource.annotations.priority == 0.8 def test_resource_without_annotations(self): """Test that annotations are optional.""" def get_data() -> str: # pragma: no cover return "data" resource = FunctionResource.from_function(fn=get_data, uri="resource://test") assert resource.annotations is None @pytest.mark.anyio async def test_resource_annotations_in_mcpserver(self): """Test resource annotations via MCPServer decorator.""" mcp = MCPServer() @mcp.resource("resource://annotated", annotations=Annotations(audience=["assistant"], priority=0.5)) def get_annotated() -> str: # pragma: no cover """An annotated resource.""" return "annotated data" resources = await mcp.list_resources() assert len(resources) == 1 assert resources[0].annotations is not None assert resources[0].annotations.audience == ["assistant"] assert resources[0].annotations.priority == 0.5 @pytest.mark.anyio async def test_resource_annotations_with_both_audiences(self): """Test resource with both user and assistant audience.""" mcp = MCPServer() @mcp.resource("resource://both", annotations=Annotations(audience=["user", "assistant"], priority=1.0)) def get_both() -> str: # pragma: no cover return "for everyone" resources = await mcp.list_resources() assert resources[0].annotations is not None assert resources[0].annotations.audience == ["user", "assistant"] assert resources[0].annotations.priority == 1.0 class TestAnnotationsValidation: """Test validation of annotation values.""" def test_priority_validation(self): """Test that priority is validated to be between 0.0 and 1.0.""" # Valid priorities Annotations(priority=0.0) Annotations(priority=0.5) Annotations(priority=1.0) # Invalid priorities should raise validation error with pytest.raises(Exception): # Pydantic validation error Annotations(priority=-0.1) with pytest.raises(Exception): Annotations(priority=1.1) def test_audience_validation(self): """Test that audience only accepts valid roles.""" # Valid audiences Annotations(audience=["user"]) Annotations(audience=["assistant"]) Annotations(audience=["user", "assistant"]) Annotations(audience=[]) # 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="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="resource://test", name="test", fn=dummy_func, ) assert resource.meta is None