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 for the full pipeline view):

TierMeaning
readReturns data; changes nothing.
computeRuns work (analytics, processing) but mints no external charge.
external_spendMoves real money or credits with a third party.
share_exportIssues a share link or export, which may reveal a one-time secret.
destructiveMutates or deletes durable state.

Each tool’s MCP ToolAnnotations are stamped from its tier by a single mechanical rule, applied to every tool with no per-operation hand-tuning:

TierreadOnlyHintdestructiveHintidempotentHintopenWorldHint
readtruefalsefalsetrue
computefalsefalsefalsefalse
external_spendfalsefalsetruetrue
share_exportfalsefalsefalsetrue
destructivefalsetruefalsefalse

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.
  • 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 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 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, and a payment problem comes back as a 402 problem+json 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 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 for the full breakdown.

Related

  • Core concepts — the side-effect tiers and the invoke pipeline they belong to.
  • Idempotency — the Idempotency-Key contract that backs idempotentHint.
  • Scopes — what actually enforces authorization on every tool call.
  • Errors — the problem+json shape an agent gets back when a call is rejected.