Repository: PrefectHQ/fastmcp (python)

Issue:
ResourcesAsTools: no way to set ToolAnnotations on generated tools

### Enhancement

`ResourcesAsTools` generates `list_resources` and `read_resource` without `ToolAnnotations`, and there's no simple way to add them.

**Use case:** I want to set `readOnlyHint=True` on the generated resource-tools so clients like Cursor auto-confirm them instead of prompting every time.

**What I'd expect to work (but doesn't):**

```python
# Option A: pass annotations directly to ResourcesAsTools
mcp.add_transform(ResourcesAsTools(mcp, annotations=ToolAnnotations(readOnlyHint=True)))

# Option B: override via ToolTransformConfig
mcp.add_transform(ToolTransform({
    "read_resource": ToolTransformConfig(annotations=ToolAnnotations(readOnlyHint=True)),
}))
```

Option A doesn't exist — `ResourcesAsTools.__init__` only takes `provider`.
Option B fails — `ToolTransformConfig` doesn't have an `annotations` field (`ValidationError: Extra inputs are not permitted`), even though the underlying `TransformedTool.from_tool()` already supports it.

**Current workaround:** Subclassing `ResourcesAsTools` and wrapping with `Tool.from_tool(..., annotations=...)` works, but feels heavy for what should be a one-liner.

Either exposing `annotations` on `ResourcesAsTools.__init__` (option A) or adding `annotations` to `ToolTransformConfig` (option B) would solve this.


Patch:
diff --git a/docs/servers/transforms/resources-as-tools.mdx b/docs/servers/transforms/resources-as-tools.mdx
index 8c7e8bbed..2251d9b49 100644
--- a/docs/servers/transforms/resources-as-tools.mdx
+++ b/docs/servers/transforms/resources-as-tools.mdx
@@ -45,6 +45,8 @@ mcp.add_transform(ResourcesAsTools(mcp))
 
 Clients now see three tools: whatever tools you defined directly, plus `list_resources` and `read_resource`.
 
+Both generated tools are annotated with `readOnlyHint=True`, since they only read data. Clients that respect tool annotations (like Cursor) can use this to auto-confirm these tool calls without prompting the user.
+
 ## Static Resources vs Templates
 
 Resources come in two forms, and the `list_resources` tool distinguishes between them in its JSON output.
diff --git a/src/fastmcp/server/transforms/resources_as_tools.py b/src/fastmcp/server/transforms/resources_as_tools.py
index 00fc43822..007b9bda6 100644
--- a/src/fastmcp/server/transforms/resources_as_tools.py
+++ b/src/fastmcp/server/transforms/resources_as_tools.py
@@ -21,10 +21,14 @@
 from collections.abc import Sequence
 from typing import TYPE_CHECKING, Annotated, Any
 
+from mcp.types import ToolAnnotations
+
 from fastmcp.server.transforms import GetToolNext, Transform
 from fastmcp.tools.tool import Tool
 from fastmcp.utilities.versions import VersionSpec
 
+_DEFAULT_ANNOTATIONS = ToolAnnotations(readOnlyHint=True)
+
 if TYPE_CHECKING:
     from fastmcp.server.providers.base import Provider
 
@@ -119,7 +123,7 @@ async def list_resources() -> str:
 
             return json.dumps(result, indent=2)
 
-        return Tool.from_function(fn=list_resources)
+        return Tool.from_function(fn=list_resources, annotations=_DEFAULT_ANNOTATIONS)
 
     def _make_read_resource_tool(self) -> Tool:
         """Create the read_resource tool."""
@@ -158,7 +162,7 @@ async def read_resource(
 
             raise ValueError(f"Resource not found: {uri}")
 
-        return Tool.from_function(fn=read_resource)
+        return Tool.from_function(fn=read_resource, annotations=_DEFAULT_ANNOTATIONS)
 
 
 def _format_result(result: Any) -> str:


Test patch:
diff --git a/tests/server/transforms/test_resources_as_tools.py b/tests/server/transforms/test_resources_as_tools.py
index 49297f98a..fd10d2d88 100644
--- a/tests/server/transforms/test_resources_as_tools.py
+++ b/tests/server/transforms/test_resources_as_tools.py
@@ -225,3 +225,29 @@ def test_repr(self):
         mcp = FastMCP("Test")
         transform = ResourcesAsTools(mcp)
         assert "ResourcesAsTools" in repr(transform)
+
+
+class TestResourcesAsToolsAnnotations:
+    """Test ToolAnnotations on generated resource tools."""
+
+    async def test_list_resources_is_read_only(self):
+        """list_resources is annotated as read-only by default."""
+        mcp = FastMCP("Test")
+        mcp.add_transform(ResourcesAsTools(mcp))
+
+        async with Client(mcp) as client:
+            tools = await client.list_tools()
+            tool = next(t for t in tools if t.name == "list_resources")
+            assert tool.annotations is not None
+            assert tool.annotations.readOnlyHint is True
+
+    async def test_read_resource_is_read_only(self):
+        """read_resource is annotated as read-only by default."""
+        mcp = FastMCP("Test")
+        mcp.add_transform(ResourcesAsTools(mcp))
+
+        async with Client(mcp) as client:
+            tools = await client.list_tools()
+            tool = next(t for t in tools if t.name == "read_resource")
+            assert tool.annotations is not None
+            assert tool.annotations.readOnlyHint is True