> ## Documentation Index
> Fetch the complete documentation index at: https://docs.closient.com/llms.txt
> Use this file to discover all available pages before exploring further.

# MCP Tool Annotations

> How Closient's MCP tools advertise their side-effect profile — readOnlyHint and destructiveHint.

Every tool exposed by the Closient MCP server carries a structured
`annotations` object that tells the host (Claude, Claude Desktop, the
Connectors Directory validator, third-party MCP clients) whether the tool
mutates state and whether repeated calls are safe to retry.

This is required by the [MCP specification][spec] and by the
[Anthropic Connectors Directory submission process][submission] — servers
that omit the annotations are rejected at submission time.

[spec]: https://modelcontextprotocol.io/specification/2025-06-18/server/tools#tool-annotations

[submission]: https://claude.com/docs/connectors/building/submission

## Annotation fields

Each tool sets a subset of the following hints. They are advisory — agents
and hosts use them to decide whether to gate a call behind a confirmation
prompt, or whether retrying after a transport error is safe.

| Field             | Meaning                                                                                 |
| ----------------- | --------------------------------------------------------------------------------------- |
| `readOnlyHint`    | `true` → the tool performs no state changes. Hosts may invoke without confirmation.     |
| `destructiveHint` | `true` → the tool may *destructively* modify user-visible state (delete, overwrite).    |
| `idempotentHint`  | `true` → repeated calls with identical arguments yield identical effects.               |
| `openWorldHint`   | `false` → the tool reads only from Closient-controlled data (catalog, resolver rules).  |
| `title`           | Human-readable label for UIs that prefer a polished name over the snake-case tool name. |

`readOnlyHint=true` and `destructiveHint=true` are mutually exclusive.
A CI test (`test_destructive_hint_implies_not_read_only`) enforces this.

## Closient's policy

We use a narrow reading of `destructiveHint`: it means **destructive**
changes — deletes, overwrites, revocations, cancellations — not "any
mutation." A tool that *adds* a record without erasing prior state is
flagged `readOnlyHint=false` *without* `destructiveHint`.

The trade-off: `destructiveHint=true` causes most hosts to render an extra
"are you sure?" prompt before the call. That's the right UX before
deleting a brand claim or canceling a subscription, and the wrong UX
before generating a QR code or recording an audit event. We reserve the
hint for the former.

## Per-tool annotations

| Tool              | `readOnlyHint` | `destructiveHint` | `idempotentHint` | Notes                                                                                                      |
| ----------------- | -------------- | ----------------- | ---------------- | ---------------------------------------------------------------------------------------------------------- |
| `ping`            | `true`         | —                 | `true`           | Health probe; no auth, no I/O.                                                                             |
| `validate_gtin`   | `true`         | —                 | `true`           | Pure validation — no DB access.                                                                            |
| `resolve_gtin`    | `true`         | —                 | `true`           | DB read against the resolver-rule hierarchy.                                                               |
| `lookup_product`  | `true`         | —                 | `true`           | DB read against the product catalog.                                                                       |
| `generate_qr_url` | `false`        | —                 | `true`           | Builds a Digital Link URL and records the call via `@user_action`. State-mutating but **not** destructive. |

`generate_qr_url` is the lone non-read-only tool today. The decision to
omit `destructiveHint` is intentional and pinned by
`test_no_tool_currently_marked_destructive` in
`backend/closient/tests/test_mcp_tool_annotations.py` — the test fails
loudly if a future contributor flips the bit without updating policy.

## Wire format

Annotations are serialized as a nested object on each entry of the
`tools/list` response. Pydantic's `exclude_none=True` mode drops fields
that weren't set, so the wire payload is minimal:

```json theme={null}
{
  "name": "lookup_product",
  "title": "Look Up Product",
  "description": "Look up structured product data by GTIN. ...",
  "inputSchema": { "type": "object", "properties": { ... } },
  "annotations": {
    "title": "Look Up Product",
    "readOnlyHint": true,
    "idempotentHint": true,
    "openWorldHint": false
  }
}
```

You can verify the shape against a running server with the MCP Inspector
or a one-off `curl`:

```bash theme={null}
curl -sS https://www.closient.com/mcp/http \
  -H 'Accept: application/json, text/event-stream' \
  -H 'Content-Type: application/json' \
  -d '{"jsonrpc":"2.0","method":"tools/list","id":1}'
```

## Adding a new tool

When you register a new tool with `@mcp.tool()` in
`backend/closient/mcp_tools.py`:

1. Pass `annotations=ToolAnnotations(...)` with at least one of
   `readOnlyHint` / `destructiveHint` set.
2. Add a row to the per-tool table above and to the parametrized
   `test_tool_read_only_hint_matches_policy` test.
3. If the tool is destructive, update
   `test_no_tool_currently_marked_destructive` so the assertion still
   reflects ground truth.

The CI test `test_every_registered_tool_has_annotations` is the safety
net — a missing annotation fails the suite before it can ship.
