|  | 
| 1 | 1 | import pytest | 
| 2 | 2 | from pydantic import AnyUrl | 
| 3 | 3 | 
 | 
|  | 4 | +from mcp.server.fastmcp import FastMCP | 
| 4 | 5 | from mcp.server.fastmcp.resources import FunctionResource, Resource | 
|  | 6 | +from mcp.types import Annotations | 
| 5 | 7 | 
 | 
| 6 | 8 | 
 | 
| 7 | 9 | class TestResourceValidation: | 
| @@ -99,3 +101,95 @@ class ConcreteResource(Resource): | 
| 99 | 101 | 
 | 
| 100 | 102 |         with pytest.raises(TypeError, match="abstract method"): | 
| 101 | 103 |             ConcreteResource(uri=AnyUrl("test://test"), name="test")  # type: ignore | 
|  | 104 | + | 
|  | 105 | + | 
|  | 106 | +class TestResourceAnnotations: | 
|  | 107 | +    """Test annotations on resources.""" | 
|  | 108 | + | 
|  | 109 | +    def test_resource_with_annotations(self): | 
|  | 110 | +        """Test creating a resource with annotations.""" | 
|  | 111 | + | 
|  | 112 | +        def get_data() -> str: | 
|  | 113 | +            return "data" | 
|  | 114 | + | 
|  | 115 | +        annotations = Annotations(audience=["user"], priority=0.8) | 
|  | 116 | + | 
|  | 117 | +        resource = FunctionResource.from_function(fn=get_data, uri="resource://test", annotations=annotations) | 
|  | 118 | + | 
|  | 119 | +        assert resource.annotations is not None | 
|  | 120 | +        assert resource.annotations.audience == ["user"] | 
|  | 121 | +        assert resource.annotations.priority == 0.8 | 
|  | 122 | + | 
|  | 123 | +    def test_resource_without_annotations(self): | 
|  | 124 | +        """Test that annotations are optional.""" | 
|  | 125 | + | 
|  | 126 | +        def get_data() -> str: | 
|  | 127 | +            return "data" | 
|  | 128 | + | 
|  | 129 | +        resource = FunctionResource.from_function(fn=get_data, uri="resource://test") | 
|  | 130 | + | 
|  | 131 | +        assert resource.annotations is None | 
|  | 132 | + | 
|  | 133 | +    @pytest.mark.anyio | 
|  | 134 | +    async def test_resource_annotations_in_fastmcp(self): | 
|  | 135 | +        """Test resource annotations via FastMCP decorator.""" | 
|  | 136 | + | 
|  | 137 | +        mcp = FastMCP() | 
|  | 138 | + | 
|  | 139 | +        @mcp.resource("resource://annotated", annotations=Annotations(audience=["assistant"], priority=0.5)) | 
|  | 140 | +        def get_annotated() -> str: | 
|  | 141 | +            """An annotated resource.""" | 
|  | 142 | +            return "annotated data" | 
|  | 143 | + | 
|  | 144 | +        resources = await mcp.list_resources() | 
|  | 145 | +        assert len(resources) == 1 | 
|  | 146 | +        assert resources[0].annotations is not None | 
|  | 147 | +        assert resources[0].annotations.audience == ["assistant"] | 
|  | 148 | +        assert resources[0].annotations.priority == 0.5 | 
|  | 149 | + | 
|  | 150 | +    @pytest.mark.anyio | 
|  | 151 | +    async def test_resource_annotations_with_both_audiences(self): | 
|  | 152 | +        """Test resource with both user and assistant audience.""" | 
|  | 153 | + | 
|  | 154 | +        mcp = FastMCP() | 
|  | 155 | + | 
|  | 156 | +        @mcp.resource("resource://both", annotations=Annotations(audience=["user", "assistant"], priority=1.0)) | 
|  | 157 | +        def get_both() -> str: | 
|  | 158 | +            return "for everyone" | 
|  | 159 | + | 
|  | 160 | +        resources = await mcp.list_resources() | 
|  | 161 | +        assert resources[0].annotations is not None | 
|  | 162 | +        assert resources[0].annotations.audience == ["user", "assistant"] | 
|  | 163 | +        assert resources[0].annotations.priority == 1.0 | 
|  | 164 | + | 
|  | 165 | + | 
|  | 166 | +class TestAnnotationsValidation: | 
|  | 167 | +    """Test validation of annotation values.""" | 
|  | 168 | + | 
|  | 169 | +    def test_priority_validation(self): | 
|  | 170 | +        """Test that priority is validated to be between 0.0 and 1.0.""" | 
|  | 171 | + | 
|  | 172 | +        # Valid priorities | 
|  | 173 | +        Annotations(priority=0.0) | 
|  | 174 | +        Annotations(priority=0.5) | 
|  | 175 | +        Annotations(priority=1.0) | 
|  | 176 | + | 
|  | 177 | +        # Invalid priorities should raise validation error | 
|  | 178 | +        with pytest.raises(Exception):  # Pydantic validation error | 
|  | 179 | +            Annotations(priority=-0.1) | 
|  | 180 | + | 
|  | 181 | +        with pytest.raises(Exception): | 
|  | 182 | +            Annotations(priority=1.1) | 
|  | 183 | + | 
|  | 184 | +    def test_audience_validation(self): | 
|  | 185 | +        """Test that audience only accepts valid roles.""" | 
|  | 186 | + | 
|  | 187 | +        # Valid audiences | 
|  | 188 | +        Annotations(audience=["user"]) | 
|  | 189 | +        Annotations(audience=["assistant"]) | 
|  | 190 | +        Annotations(audience=["user", "assistant"]) | 
|  | 191 | +        Annotations(audience=[]) | 
|  | 192 | + | 
|  | 193 | +        # Invalid roles should raise validation error | 
|  | 194 | +        with pytest.raises(Exception):  # Pydantic validation error | 
|  | 195 | +            Annotations(audience=["invalid_role"])  # type: ignore | 
0 commit comments