<!-- Source: https://docs.geopera.com/api-reference/sdks/mcp/safety · Markdown for LLMs -->

# Safety & side-effects

Every Geopera operation declares a side-effect tier, and each MCP tool carries standard MCP tool annotations derived from that tier so an agent can reason about a call's blast radius _before_ it makes it.

## Where the hints come from

Each operation declares a `side_effect`, one of five tiers (see [Core concepts](/api-reference/concepts) for the full pipeline view):

| Tier             | Meaning                                                            |
| ---------------- | ------------------------------------------------------------------ |
| `read`           | Returns data; changes nothing.                                     |
| `compute`        | Runs work (analytics, processing) but mints no external charge.    |
| `external_spend` | Moves real money or credits with a third party.                    |
| `share_export`   | Issues a share link or export, which may reveal a one-time secret. |
| `destructive`    | Mutates or deletes durable state.                                  |

Each tool's [MCP `ToolAnnotations`](https://modelcontextprotocol.io/) are stamped from its tier by a single mechanical rule, applied to every tool with no per-operation hand-tuning:

| Tier             | `readOnlyHint` | `destructiveHint` | `idempotentHint` | `openWorldHint` |
| ---------------- | -------------- | ----------------- | ---------------- | --------------- |
| `read`           | `true`         | `false`           | `false`          | `true`          |
| `compute`        | `false`        | `false`           | `false`          | `false`         |
| `external_spend` | `false`        | `false`           | `true`           | `true`          |
| `share_export`   | `false`        | `false`           | `false`          | `true`          |
| `destructive`    | `false`        | `true`            | `false`          | `false`         |

Because the annotations follow directly from the tier, they stay consistent: add an operation with a given tier and its tool is annotated correctly.

### What each hint means here

- **`readOnlyHint`** is `true` only for `read` operations. A `true` value is the agent's signal that the tool is safe to call freely — it cannot change state. `compute`, `external_spend`, `share_export`, and `destructive` tools all report `readOnlyHint: false`.
- **`destructiveHint`** is `true` only for `destructive` operations — those that mutate or delete durable state (cancelling, archiving, deleting). Treat these as not reversible by simply calling the tool again.
- **`idempotentHint`** is `true` only for `external_spend` operations. These carry an `Idempotency-Key` contract on their route, so a repeat with the _same_ key is side-effect-free (it returns the original result instead of charging twice). See [Idempotency](/api-reference/idempotency).
- **`openWorldHint`** is `true` for `read`, `external_spend`, and `share_export` — the tiers whose result depends on or affects systems outside your own account state (live catalog data, a payment processor, an externally reachable share URL). `compute` and `destructive` operate on the closed world of your own account state and report `openWorldHint: false`.

Note that `compute` and `share_export` tools carry no `true` mutation hint of their own: `compute` is `readOnlyHint: false` with every other hint `false`, and `share_export` is flagged only via `openWorldHint`. The absence of `destructiveHint`/`idempotentHint` is itself information — see the gotchas below.

### Structured metadata

Beyond the boolean hints, each tool's `meta` carries the raw tier and governance facts so a client can filter or route on them without parsing the description text:

```json
{
	"x-side-effect": "external_spend",
	"x-required-scope": "orders:write",
	"x-produces": ["order"]
}
```

Use `x-side-effect` for precise gating (it distinguishes all five tiers, where the boolean hints collapse `compute` and `share_export`), and `x-required-scope` to predict whether a call will pass [scope authorization](/api-reference/scopes) at all.

## `external_spend`: money moves, and idempotency protects it

`external_spend` tools are the ones that cost real money or credits — placing an order, topping up a balance, dispatching paid processing. They are annotated `idempotentHint: true` because their route honours the `Idempotency-Key` contract.

What that means for an agent:

- **Generate one key per intended charge.** Before calling a spend tool, mint a fresh idempotency key (a UUID is fine) and reuse it on retries for _that same_ intended action.
- **Retrying with the same key is safe.** If a network blip or timeout leaves you unsure whether the charge landed, replaying with the same key returns the original result rather than charging again.
- **A new key means a new charge.** Generating a fresh key for a retry defeats the protection and can double-spend.

How the key reaches Geopera depends on your MCP client. If your client can attach request headers to a tool call, send `Idempotency-Key`. If it can only pass tool arguments, supply the key in the operation's input where the operation accepts one. Full mechanics, including the response semantics on a replay, are on the [Idempotency](/api-reference/idempotency) page — do not re-derive them.

## `share_export`: a secret you may see only once

`share_export` operations (creating a share link, issuing an export) can return a credential — a signed URL or token — that is shown **once** in the tool result and is not retrievable afterward. For an agent this has two consequences:

- **Capture the result immediately.** If the returned URL or token is discarded, re-running the tool typically mints a _new_ secret rather than returning the old one; the first is then unrecoverable.
- **Treat the result as sensitive.** A share URL is a bearer capability. Don't echo it into logs, transcripts, or downstream prompts that you wouldn't trust with the underlying data.

These tools are flagged `openWorldHint: true` (the resulting URL is reachable from outside) but carry no destructive hint, so a naive "only confirm destructive tools" policy will miss them. Gate them on `x-side-effect == "share_export"` explicitly.

## `destructive`: mutate or delete

`destructive` tools change or remove durable state and are annotated `destructiveHint: true`. They do **not** carry the idempotency contract that spend tools do, so re-calling one is a fresh mutation, not a safe replay. Surface these for confirmation and avoid speculative retries.

## Recommended gating pattern for agents

The hints describe risk; they do not enforce it. Enforcement still happens server-side — an agent can never exceed its token's [scopes](/api-reference/scopes), and a payment problem comes back as a `402` [problem+json](/api-reference/errors) the agent can reason about. But a well-behaved agent should _also_ gate at the client, before the call, using the annotations:

1. **Free-call the safe tier.** `readOnlyHint: true` (the `read` tier) → call without prompting.
2. **Confirm before spending.** `idempotentHint: true` / `x-side-effect == "external_spend"` → pause for explicit user confirmation. Where possible, call the matching `*.estimate` read tool first to show the cost, then place the charge with a stable idempotency key.
3. **Confirm before destroying.** `destructiveHint: true` → pause for confirmation; never auto-retry on ambiguity.
4. **Confirm before sharing.** `x-side-effect == "share_export"` → pause for confirmation and handle the returned secret as one-time and sensitive.
5. **Let `compute` run, within reason.** `compute` is not free of cost in time/resources but mints no external charge; gate on a budget or quota policy rather than per-call confirmation if you choose to.

A concrete confirmation flow for a spend tool, estimate-then-place:

```bash
Agent: catalog/items search           → read,  no prompt
Agent: orders.archive.estimate        → read,  no prompt   (shows price: 240 credits)
Agent: "Place this order for 240 credits? (y/n)"           ← confirmation gate
User:  y
Agent: orders.archive.place           → external_spend
       with Idempotency-Key: 6f1c…    (reused verbatim on any retry)
```

Pseudocode for a gate keyed off the annotations and metadata your client receives per tool:

```typescript
type SideEffect = 'read' | 'compute' | 'external_spend' | 'share_export' | 'destructive';

function needsConfirmation(tool: {
	annotations: { readOnlyHint?: boolean };
	meta: { 'x-side-effect'?: SideEffect };
}): boolean {
	if (tool.annotations.readOnlyHint) return false; // read → safe
	const tier = tool.meta['x-side-effect'];
	return (
		tier === 'external_spend' || // money
		tier === 'destructive' || // mutate/delete
		tier === 'share_export'
	); // one-time secret
	// compute falls through to false here — gate it on budget/quota instead.
}
```

## Gotchas

- **The boolean hints collapse two tiers.** `compute` and `share_export` both report `false` for `destructiveHint` and `idempotentHint`. If your policy only inspects the booleans, it will treat a `share_export` tool as "harmless." Always consult `x-side-effect` in `meta` for the precise tier.
- **`idempotentHint` is not a license to retry blindly.** It is only meaningful when you send the _same_ `Idempotency-Key`. A retry with a new key is a new charge.
- **`destructive` tools are not idempotent.** They lack the spend tier's replay contract; a repeated call mutates again.
- **Annotations are advisory, scopes are enforced.** A hint never blocks a call. The scope check does — an agent calling a tool it lacks the [scope](/api-reference/scopes) for is rejected regardless of what its policy decided.
- **Read and list tools are `readOnlyHint: true` — call them freely.** The surface now includes the read/list operations an agent uses to read back what it creates (list orders, fetch a collection, list notifications, check a balance or usage, poll a job list). They report `readOnlyHint: true`; they change nothing and need no confirmation gate.
- **Some operations have no tool at all.** A few categories are excluded by the gateway and carry no annotation to reason about: raster tiles and rendered images (frontend map plumbing), binary downloads such as exports, reports, and clip downloads (large egress, better served by a signed URL), streaming NDJSON/SSE listings (a single-result tool can't carry a stream — use the paged sibling), and admin/cron operations plus a small set of untyped destructive or external-spend mutations (withheld for safety). The governed, typed mutations you need stay available with their hints intact. See [Tool reference](/api-reference/sdks/mcp/tools) for the full breakdown.

## Related

- [Core concepts](/api-reference/concepts) — the side-effect tiers and the invoke pipeline they belong to.
- [Idempotency](/api-reference/idempotency) — the `Idempotency-Key` contract that backs `idempotentHint`.
- [Scopes](/api-reference/scopes) — what actually enforces authorization on every tool call.
- [Errors](/api-reference/errors) — the problem+json shape an agent gets back when a call is rejected.
