# Implementing Read-Only Tools in FastMCP Using Annotations: A Complete Guide

> Learn to implement read-only tools in FastMCP using annotations. Mark functions as read-only with readOnlyHint=True for client UIs. Guide for cr2007/mcp-wordle-python.

- Repository: [CSK/mcp-wordle-python](https://github.com/cr2007/mcp-wordle-python)
- Tags: how-to-guide
- Published: 2026-02-28

---

**You mark a FastMCP tool as read-only by passing `annotations={"readOnlyHint": True}` to the `@mcp.tool()` decorator, which signals to client UIs that the function performs only data retrieval without side-effects.**

The **FastMCP** framework streamlines building Model Context Protocol (MCP) servers in Python, but distinguishing between mutating and non-mutating operations requires explicit metadata. In the **Wordle MCP (Python)** repository (`cr2007/mcp-wordle-python`), developers demonstrate implementing read-only tools using annotation parameters to flag HTTP fetch operations as safe, side-effect-free queries.

## Understanding FastMCP Annotations for Read-Only Tools

FastMCP utilizes an `annotations` dictionary within the `@mcp.tool()` decorator to communicate tool behavior metadata to downstream clients. While the Python runtime does not enforce these constraints, the protocol serializes this metadata for UI interpretation.

The `readOnlyHint` key specifically indicates that a tool executes only **read operations**—fetching external data or querying state without creating, updating, or deleting resources. This distinction prevents conversational agents from accidentally invoking data-retrieval functions in write-oriented workflows.

### The Architecture of Read-Only Tool Registration

| Component | Function | Source Location |
| --- | --- | --- |
| **FastMCP runtime** | Parses [`fastmcp.json`](https://github.com/cr2007/mcp-wordle-python/blob/main/fastmcp.json) to locate entry-points and manage dependencies | [`fastmcp.json`](https://github.com/cr2007/mcp-wordle-python/blob/main/fastmcp.json) |
| **Entry-point script** | Instantiates `FastMCP`, defines tools, launches server | [`src/mcp_wordle/main.py`](https://github.com/cr2007/mcp-wordle-python/blob/main/src/mcp_wordle/main.py) |
| **Tool definition** | Registers callable with metadata via `@mcp.tool()` | Lines 30-38 of [`main.py`](https://github.com/cr2007/mcp-wordle-python/blob/main/main.py) |
| **Read-only signal** | `annotations={"readOnlyHint": True}` marks tool as non-mutating | Decorator block in [`main.py`](https://github.com/cr2007/mcp-wordle-python/blob/main/main.py) |
| **HTTP implementation** | `requests.get()` fetches Wordle data from NYTimes API | Lines 64-67 of [`main.py`](https://github.com/cr2007/mcp-wordle-python/blob/main/main.py) |

## Implementing readOnlyHint in src/mcp_wordle/main.py

According to the source code in `cr2007/mcp-wordle-python`, the `get_wordle_solution` tool implements the read-only pattern by combining the annotation with an HTTP GET request. The decorator registers the function while the `annotations` parameter advertises its non-mutating nature to MCP clients like Claude Desktop.

```python
from fastmcp import FastMCP
import requests

mcp = FastMCP("WordleMCP")

@mcp.tool(
    name="get_wordle_solution",
    description="Fetch the Wordle solution for a given date (2021-05-19 … +23 days).",
    annotations={"readOnlyHint": True},
)
async def get_wordle_data(target_date: str = "2024-01-01"):
    """Return the JSON payload from the official Wordle endpoint."""
    url = f"https://www.nytimes.com/svc/wordle/v2/{target_date}.json"
    return requests.get(url, timeout=10).json()

if __name__ == "__main__":
    mcp.run()

```

When `mcp.run()` executes, FastMCP serializes the `readOnlyHint: true` flag into the tool's capability description. Clients receiving this metadata understand that invoking `get_wordle_solution` with a `target_date` argument performs only external data retrieval and never modifies server state.

## How the readOnlyHint Annotation Propagates

The annotation mechanism operates through three distinct phases:

1. **Definition Phase** – The `@mcp.tool()` decorator receives the `annotations` dictionary containing `readOnlyHint: True`. FastMCP stores this metadata alongside the function reference.

2. **Protocol Serialization** – Upon server startup, FastMCP advertises available tools through the MCP protocol, embedding the annotations into the tool's JSON description.

3. **UI Interpretation** – Front-end applications like Claude Desktop parse the `readOnlyHint` flag and render the tool in read-only palettes or restrict its usage in write-heavy prompt contexts, preventing accidental side-effects.

This separation of concerns allows developers to flag pure-fetch functionality without modifying the underlying Python logic or adding runtime checks.

## Client Integration and Deployment Patterns

The Wordle MCP server supports multiple deployment methods, each preserving the read-only annotations through the protocol handshake.

### Docker-Based Deployment

Add this configuration to your MCP client settings to run the containerized server:

```json
{
  "mcpServers": {
    "Wordle MCP (Python)": {
      "command": "docker",
      "args": [
        "run", "--rm", "-i", "--init",
        "-e", "DOCKER_CONTAINER=true",
        "ghcr.io/cr2007/mcp-wordle-python:latest"
      ]
    }
  }
}

```

### Direct uvx Execution

For Python environments with `uv` installed, invoke the tool directly from the GitHub repository:

```json
{
  "mcpServers": {
    "Wordle MCP (Python)": {
      "command": "uvx",
      "args": [
        "--from",
        "git+https://github.com/cr2007/mcp-wordle-python",
        "mcp-wordle"
      ]
    }
  }
}

```

### Testing the Read-Only Tool

You can verify the implementation locally without the full server infrastructure:

```python
from src.mcp_wordle.main import get_wordle_data

result = get_wordle_data("2023-09-20")
print(result["solution"])   # => e.g., "crane"

```

The function returns the Wordle solution JSON without any state mutation, consistent with the `readOnlyHint` contract.

## Summary

- **FastMCP annotations** use the `annotations` parameter in `@mcp.tool()` to communicate tool behavior metadata.
- **`readOnlyHint: True`** explicitly marks tools as non-mutating data retrieval operations.
- **Implementation** requires only adding the dictionary to the decorator, without changing function logic.
- **Client UIs** consume these hints to prevent read-only tools from appearing in write-oriented workflows.
- **Deployment** via Docker or `uvx` preserves annotation metadata through the MCP protocol handshake.

## Frequently Asked Questions

### What is the readOnlyHint annotation in FastMCP?

The `readOnlyHint` is a boolean flag within the `annotations` dictionary of the `@mcp.tool()` decorator. It signals to MCP clients that the associated function performs only data retrieval operations and never modifies server state or external resources.

### How do I mark a FastMCP tool as read-only?

Pass `annotations={"readOnlyHint": True}` as an argument to the `@mcp.tool()` decorator when defining your function. FastMCP will include this metadata in the tool's protocol description, allowing clients to categorize the tool appropriately.

### Does FastMCP enforce read-only behavior server-side?

No, FastMCP does not enforce the `readOnlyHint` constraint at runtime. The annotation serves as metadata for client-side UI interpretation only. Developers must ensure their tool implementation actually avoids side-effects, as the flag operates on an honor system.

### Can I combine readOnlyHint with other annotations in FastMCP?

Yes, the `annotations` dictionary accepts multiple key-value pairs. You can combine `readOnlyHint` with other metadata flags like `experimental` or custom hints to provide additional context to MCP clients about tool maturity or specific behavioral characteristics.