from pathlib import Path from tempfile import NamedTemporaryFile import pytest from pydantic import AnyUrl, FileUrl from fastmcp.resources import ( FileResource, FunctionResource, ResourceManager, ResourceTemplate, ) from fastmcp.settings import DuplicateBehavior @pytest.fixture def temp_file(): """Create a temporary file for testing. File is automatically cleaned up after the test if it still exists. """ content = "test content" with NamedTemporaryFile(mode="w", delete=False) as f: f.write(content) path = Path(f.name).resolve() yield path try: path.unlink() except FileNotFoundError: pass # File was already deleted by the test class TestResourceManager: """Test ResourceManager functionality.""" def test_add_resource(self, temp_file: Path): """Test adding a resource.""" manager = ResourceManager() resource = FileResource( uri=FileUrl(f"file://{temp_file}"), name="test", path=temp_file, ) added = manager.add_resource(resource) assert added == resource assert manager.list_resources() == [resource] def test_add_duplicate_resource(self, temp_file: Path): """Test adding the same resource twice.""" manager = ResourceManager() resource = FileResource( uri=FileUrl(f"file://{temp_file}"), name="test", path=temp_file, ) first = manager.add_resource(resource) second = manager.add_resource(resource) assert first == second assert manager.list_resources() == [resource] def test_warn_on_duplicate_resources(self, temp_file: Path, caplog): """Test warning on duplicate resources.""" manager = ResourceManager(duplicate_behavior=DuplicateBehavior.WARN) resource = FileResource( uri=FileUrl(f"file://{temp_file}"), name="test", path=temp_file, ) manager.add_resource(resource) manager.add_resource(resource) assert "Resource already exists" in caplog.text def test_disable_warn_on_duplicate_resources(self, temp_file: Path, caplog): """Test disabling warning on duplicate resources.""" manager = ResourceManager(duplicate_behavior=DuplicateBehavior.IGNORE) resource = FileResource( uri=FileUrl(f"file://{temp_file}"), name="test", path=temp_file, ) manager.add_resource(resource) manager.add_resource(resource) assert "Resource already exists" not in caplog.text def test_error_on_duplicate_resources(self, temp_file: Path): """Test error on duplicate resources.""" manager = ResourceManager(duplicate_behavior=DuplicateBehavior.ERROR) resource = FileResource( uri=FileUrl(f"file://{temp_file}"), name="test", path=temp_file, ) manager.add_resource(resource) with pytest.raises(ValueError, match="Resource already exists"): manager.add_resource(resource) def test_replace_duplicate_resources(self, temp_file: Path): """Test replacing duplicate resources.""" manager = ResourceManager(duplicate_behavior=DuplicateBehavior.REPLACE) resource1 = FileResource( uri=FileUrl(f"file://{temp_file}"), name="test1", path=temp_file, ) resource2 = FileResource( uri=FileUrl(f"file://{temp_file}"), name="test2", # Different name path=temp_file, ) manager.add_resource(resource1) manager.add_resource(resource2) # Should have replaced the first resource with the second resources = manager.list_resources() assert len(resources) == 1 assert resources[0].name == "test2" @pytest.mark.anyio async def test_get_resource(self, temp_file: Path): """Test getting a resource by URI.""" manager = ResourceManager() resource = FileResource( uri=FileUrl(f"file://{temp_file}"), name="test", path=temp_file, ) manager.add_resource(resource) retrieved = await manager.get_resource(resource.uri) assert retrieved == resource @pytest.mark.anyio async def test_get_resource_from_template(self): """Test getting a resource through a template.""" manager = ResourceManager() def greet(name: str) -> str: return f"Hello, {name}!" template = ResourceTemplate.from_function( fn=greet, uri_template="greet://{name}", name="greeter", ) manager._templates[template.uri_template] = template resource = await manager.get_resource(AnyUrl("greet://world")) assert isinstance(resource, FunctionResource) content = await resource.read() assert content == "Hello, world!" @pytest.mark.anyio async def test_get_unknown_resource(self): """Test getting a non-existent resource.""" manager = ResourceManager() with pytest.raises(ValueError, match="Unknown resource"): await manager.get_resource(AnyUrl("unknown://test")) def test_list_resources(self, temp_file: Path): """Test listing all resources.""" manager = ResourceManager() resource1 = FileResource( uri=FileUrl(f"file://{temp_file}"), name="test1", path=temp_file, ) resource2 = FileResource( uri=FileUrl(f"file://{temp_file}2"), name="test2", path=temp_file, ) manager.add_resource(resource1) manager.add_resource(resource2) resources = manager.list_resources() assert len(resources) == 2 assert resources == [resource1, resource2] class TestResourceTags: """Test functionality related to resource tags.""" def test_add_resource_with_tags(self, temp_file: Path): """Test adding a resource with tags.""" manager = ResourceManager() resource = FileResource( uri=FileUrl(f"file://{temp_file}"), name="weather_data", path=temp_file, tags={"weather", "data"}, ) manager.add_resource(resource) # Check that tags are preserved resources = manager.list_resources() assert len(resources) == 1 assert resources[0].tags == {"weather", "data"} def test_add_function_resource_with_tags(self): """Test adding a function resource with tags.""" manager = ResourceManager() async def get_data(): return "Sample data" resource = FunctionResource( uri=AnyUrl("data://sample"), name="sample_data", description="Sample data resource", mime_type="text/plain", fn=get_data, tags={"sample", "test", "data"}, ) manager.add_resource(resource) resources = manager.list_resources() assert len(resources) == 1 assert resources[0].tags == {"sample", "test", "data"} def test_add_template_with_tags(self): """Test adding a resource template with tags.""" manager = ResourceManager() def user_data(user_id: str) -> str: return f"Data for user {user_id}" template = ResourceTemplate.from_function( fn=user_data, uri_template="users://{user_id}", name="user_template", description="Get user data by ID", tags={"users", "template", "data"}, ) manager.add_template(template) templates = manager.list_templates() assert len(templates) == 1 assert templates[0].tags == {"users", "template", "data"} def test_filter_resources_by_tags(self, temp_file: Path): """Test filtering resources by tags.""" manager = ResourceManager() # Create multiple resources with different tags resource1 = FileResource( uri=FileUrl(f"file://{temp_file}1"), name="weather_data", path=temp_file, tags={"weather", "external"}, ) async def get_user_data(): return "User data" resource2 = FunctionResource( uri=AnyUrl("data://users"), name="user_data", fn=get_user_data, tags={"users", "internal"}, ) async def get_system_data(): return "System data" resource3 = FunctionResource( uri=AnyUrl("data://system"), name="system_data", fn=get_system_data, tags={"system", "internal"}, ) manager.add_resource(resource1) manager.add_resource(resource2) manager.add_resource(resource3) # Filter resources by tags internal_resources = [ r for r in manager.list_resources() if "internal" in r.tags ] assert len(internal_resources) == 2 assert {r.name for r in internal_resources} == {"user_data", "system_data"} external_resources = [ r for r in manager.list_resources() if "external" in r.tags ] assert len(external_resources) == 1 assert external_resources[0].name == "weather_data" def test_import_resources_preserves_tags(self): """Test that importing resources preserves their tags.""" source_manager = ResourceManager() async def get_data(): return "Tagged data" resource = FunctionResource( uri=AnyUrl("data://tagged"), name="tagged_data", fn=get_data, tags={"test", "example", "data"}, ) source_manager.add_resource(resource) target_manager = ResourceManager() target_manager.import_resources(source_manager, "imported+") imported_resources = target_manager.list_resources() assert len(imported_resources) == 1 assert imported_resources[0].tags == {"test", "example", "data"} def test_import_templates_preserves_tags(self): """Test that importing templates preserves their tags.""" source_manager = ResourceManager() def user_template(user_id: str) -> str: return f"User {user_id}" template = ResourceTemplate.from_function( fn=user_template, uri_template="users://{user_id}", name="user_template", tags={"users", "template", "test"}, ) source_manager.add_template(template) target_manager = ResourceManager() target_manager.import_templates(source_manager, "imported+") imported_templates = target_manager.list_templates() assert len(imported_templates) == 1 assert imported_templates[0].tags == {"users", "template", "test"} class TestImports: def test_import_resources(self): """Test importing resources from one manager to another with a prefix.""" # Setup source manager with resources source_manager = ResourceManager() # Create mock resource functions async def weather_fn(): return "Weather data" async def traffic_fn(): return "Traffic data" # Add resources to source manager weather_resource = FunctionResource( uri=AnyUrl("weather://forecast"), name="weather_forecast", description="Get weather forecast", mime_type="application/json", fn=weather_fn, ) source_manager._resources["weather://forecast"] = weather_resource traffic_resource = FunctionResource( uri=AnyUrl("traffic://status"), name="traffic_status", description="Get traffic status", mime_type="application/json", fn=traffic_fn, ) source_manager._resources["traffic://status"] = traffic_resource # Create target manager target_manager = ResourceManager() # Import resources from source to target prefix = "data+" target_manager.import_resources(source_manager, prefix) # Verify resources were imported with prefixes assert "data+weather://forecast" in target_manager._resources assert "data+traffic://status" in target_manager._resources # Verify the original resources still exist in source manager assert "weather://forecast" in source_manager._resources assert "traffic://status" in source_manager._resources # Verify the imported resources have the correct properties assert ( target_manager._resources["data+weather://forecast"].name == "weather_forecast" ) assert ( target_manager._resources["data+weather://forecast"].description == "Get weather forecast" ) assert ( target_manager._resources["data+weather://forecast"].mime_type == "application/json" ) assert ( target_manager._resources["data+traffic://status"].name == "traffic_status" ) assert ( target_manager._resources["data+traffic://status"].description == "Get traffic status" ) assert ( target_manager._resources["data+traffic://status"].mime_type == "application/json" ) # Since we're dealing with FunctionResource type, we can safely check function attributes assert isinstance( target_manager._resources["data+weather://forecast"], FunctionResource ) assert isinstance( target_manager._resources["data+traffic://status"], FunctionResource ) weather_resource = target_manager._resources["data+weather://forecast"] traffic_resource = target_manager._resources["data+traffic://status"] if hasattr(weather_resource, "fn") and hasattr(traffic_resource, "fn"): assert weather_resource.fn.__name__ == weather_fn.__name__ assert traffic_resource.fn.__name__ == traffic_fn.__name__ def test_import_templates(self): """Test importing resource templates from one manager to another with a prefix.""" # Setup source manager with templates source_manager = ResourceManager() # Create mock template functions async def user_fn(**params): return f"User data for id {params.get('id')}" async def product_fn(**params): return f"Product data for id {params.get('id')}" # Add templates to source manager user_template = ResourceTemplate( uri_template="api://users/{id}", name="user_template", description="Get user by ID", mime_type="application/json", fn=user_fn, parameters={"id": {"type": "string", "description": "User ID"}}, ) source_manager._templates["api://users/{id}"] = user_template product_template = ResourceTemplate( uri_template="api://products/{id}", name="product_template", description="Get product by ID", mime_type="application/json", fn=product_fn, parameters={"id": {"type": "string", "description": "Product ID"}}, ) source_manager._templates["api://products/{id}"] = product_template # Create target manager target_manager = ResourceManager() # Import templates from source to target prefix = "shop+" target_manager.import_templates(source_manager, prefix) # Verify templates were imported with prefixes assert "shop+api://users/{id}" in target_manager._templates assert "shop+api://products/{id}" in target_manager._templates # Verify the original templates still exist in source manager assert "api://users/{id}" in source_manager._templates assert "api://products/{id}" in source_manager._templates # Verify the imported templates have the correct properties assert ( target_manager._templates["shop+api://users/{id}"].name == "user_template" ) assert ( target_manager._templates["shop+api://users/{id}"].description == "Get user by ID" ) assert ( target_manager._templates["shop+api://users/{id}"].mime_type == "application/json" ) assert target_manager._templates["shop+api://users/{id}"].parameters == { "id": {"type": "string", "description": "User ID"} } assert ( target_manager._templates["shop+api://products/{id}"].name == "product_template" ) assert ( target_manager._templates["shop+api://products/{id}"].description == "Get product by ID" ) assert ( target_manager._templates["shop+api://products/{id}"].mime_type == "application/json" ) assert target_manager._templates["shop+api://products/{id}"].parameters == { "id": {"type": "string", "description": "Product ID"} } # Verify the template functions were properly copied (only if the fn attribute exists) user_template = target_manager._templates["shop+api://users/{id}"] product_template = target_manager._templates["shop+api://products/{id}"] if hasattr(user_template, "fn") and hasattr(product_template, "fn"): assert user_template.fn.__name__ == user_fn.__name__ assert product_template.fn.__name__ == product_fn.__name__ def test_import_multiple_resource_types(self): """Test importing both resources and templates with the same prefix.""" # Setup source manager with both resources and templates source_manager = ResourceManager() # Create mock functions async def resource_fn(): return "Resource data" async def template_fn(**params): return f"Template data for id {params.get('id')}" # Add a resource to source manager resource = FunctionResource( uri=AnyUrl("data://resource"), name="test_resource", description="Test resource", mime_type="application/json", fn=resource_fn, ) source_manager._resources["data://resource"] = resource # Add a template to source manager template = ResourceTemplate( uri_template="data://template/{id}", name="test_template", description="Test template", mime_type="application/json", fn=template_fn, parameters={"id": {"type": "string", "description": "ID parameter"}}, ) source_manager._templates["data://template/{id}"] = template # Create target manager target_manager = ResourceManager() # Import both resources and templates prefix = "test+" target_manager.import_resources(source_manager, prefix) target_manager.import_templates(source_manager, prefix) # Verify both resource types were imported with prefixes assert "test+data://resource" in target_manager._resources assert "test+data://template/{id}" in target_manager._templates