|
1 | 1 | """Tests for component customization.""" |
| 2 | +import logging |
2 | 3 | from unittest.mock import Mock |
3 | 4 |
|
| 5 | +import pytest |
4 | 6 | from fastmcp.server.providers.openapi import ( |
5 | 7 | OpenAPIResource, |
6 | 8 | OpenAPIResourceTemplate, |
@@ -87,3 +89,71 @@ def test_preserve_original_description(self, sample_route): |
87 | 89 |
|
88 | 90 | assert original in tool.description |
89 | 91 | assert tool.description != original |
| 92 | + |
| 93 | + |
| 94 | +class TestToolAnnotations: |
| 95 | + """Hints that let clients approve read tools apart from write tools.""" |
| 96 | + |
| 97 | + @staticmethod |
| 98 | + def _tool(route): |
| 99 | + tool = Mock(spec=OpenAPITool) |
| 100 | + tool.description = "" |
| 101 | + tool.tags = set() |
| 102 | + customize_components(route, tool) |
| 103 | + return tool |
| 104 | + |
| 105 | + def test_get_is_read_only(self, sample_route): |
| 106 | + """Test that a GET without x-cortex-mcp-access is read-only.""" |
| 107 | + assert self._tool(sample_route).annotations.read_only_hint is True |
| 108 | + |
| 109 | + @pytest.mark.parametrize("method", ["POST", "PUT", "PATCH", "DELETE"]) |
| 110 | + def test_other_methods_default_to_destructive(self, sample_route, method): |
| 111 | + """Test that other methods without x-cortex-mcp-access are destructive.""" |
| 112 | + sample_route.method = method |
| 113 | + |
| 114 | + annotations = self._tool(sample_route).annotations |
| 115 | + |
| 116 | + assert annotations.read_only_hint is False |
| 117 | + assert annotations.destructive_hint is True |
| 118 | + assert annotations.idempotent_hint is None |
| 119 | + |
| 120 | + def test_access_extension_marks_a_post_read_only(self, sample_route): |
| 121 | + """Test that x-cortex-mcp-access: read makes a POST read-only.""" |
| 122 | + sample_route.method = "POST" |
| 123 | + sample_route.extensions = {"x-cortex-mcp-access": "read"} |
| 124 | + |
| 125 | + assert self._tool(sample_route).annotations.read_only_hint is True |
| 126 | + |
| 127 | + def test_access_extension_marks_a_write_additive(self, sample_route): |
| 128 | + """Test that x-cortex-mcp-access: additive is a non-destructive write.""" |
| 129 | + sample_route.method = "POST" |
| 130 | + sample_route.extensions = {"x-cortex-mcp-access": "additive"} |
| 131 | + |
| 132 | + annotations = self._tool(sample_route).annotations |
| 133 | + |
| 134 | + assert annotations.read_only_hint is False |
| 135 | + assert annotations.destructive_hint is False |
| 136 | + |
| 137 | + def test_access_extension_marks_a_get_destructive(self, sample_route): |
| 138 | + """Test that x-cortex-mcp-access: destructive overrides the GET default.""" |
| 139 | + sample_route.extensions = {"x-cortex-mcp-access": "destructive"} |
| 140 | + |
| 141 | + annotations = self._tool(sample_route).annotations |
| 142 | + |
| 143 | + assert annotations.read_only_hint is False |
| 144 | + assert annotations.destructive_hint is True |
| 145 | + |
| 146 | + def test_unknown_access_is_destructive(self, sample_route, caplog): |
| 147 | + """Test that an unknown x-cortex-mcp-access value warns and is destructive.""" |
| 148 | + caplog.set_level(logging.WARNING, logger="src.components.customizers") |
| 149 | + sample_route.extensions = {"x-cortex-mcp-access": "Read"} |
| 150 | + |
| 151 | + annotations = self._tool(sample_route).annotations |
| 152 | + |
| 153 | + assert annotations.read_only_hint is False |
| 154 | + assert annotations.destructive_hint is True |
| 155 | + assert "Unknown x-cortex-mcp-access 'Read'" in caplog.text |
| 156 | + |
| 157 | + def test_title_is_the_summary(self, sample_route): |
| 158 | + """Test that the tool title is the operation summary.""" |
| 159 | + assert self._tool(sample_route).title == "List resources" |
0 commit comments