# Errors and limits

What each kind of failure looks like, and how to retry safely.

## Connection errors

| Response | Meaning | What to do |
| --- | --- | --- |
| HTTP `401` with `WWW-Authenticate` | No token, or the token expired or was revoked | Let your client refresh, or sign in again. |
| HTTP `401` for a member who left Gravity | Access ended with their membership | Nothing. The account can't be used. |
| HTTP `403` with `error="insufficient_scope"` | The token lacks the read or edit permission for this operation | Continue within the granted permissions, or authorize the scopes in the challenge if you want the additional capability. |
| HTTP `429` with `error="rate_limit_exceeded"` | A message other than a single tool call went over the member's read or write limit | Respect `Retry-After` before retrying. Nothing in the request ran. |
| HTTP `503` with `"MCP authorization is temporarily unavailable."` | Gravity's sign-in service is down | Try again later. |

## Tool errors

Input validation failures return `isError: true` with text beginning `Input validation error`; nothing runs. Unknown tools produce a protocol error. These protocol failures do not use the application error envelope.

Executed-tool errors return `{ status: "error", code, message, recovery }` as both `structuredContent` and JSON text, with `isError: true`. Conflicts include `currentState` when the owned goal is available. Known goal/run IDs may also be included. Messages explain the situation; codes and recovery values are stable.

| Code | Recovery | Meaning and next step |
| --- | --- | --- |
| `unavailable` | `resolve_id` | An exact group or goal is missing or inaccessible. Resolve from current authorized lists; do not guess IDs. |
| `version_conflict` | `read_before_retry` | The goal's scope or context revision changed. The edit was rejected without changing wording, private context, or audience. Read current state before preparing a revised action. |
| `request_id_conflict` | `new_request` | The member already used this request ID with different arguments. Nothing changed. Recover the original action with its original arguments, or use a new ID for a new action. |
| `clarification_required` | `clarify_goal` | The goal needs clarification before a search can start. Read its question and help answer it; no search started. |
| `request_rejected` | `read_before_retry` | A goal or search rule rejected the request before changes. Use the message to correct the request after reading current state. |
| `operation_failed` | `retry_read` for reads; `read_before_retry` for writes | An unexpected failure left the outcome unconfirmed. Reads can be retried. Read current state before considering another write. |
| `invalid_response` | `retry_read` for reads; `read_before_retry` for writes | The application's response did not meet the tool contract. No malformed/private payload is returned. The outcome is unconfirmed; use the same recovery as an unexpected failure. |
| `rate_limited` | `retry_later` | The member went over the read or write limit before this tool ran. Nothing changed and no search started. Wait `retryAfterSeconds`, then repeat the same call with the same request ID. |

An accessible empty list is successful. Missing and inaccessible exact resources both return `unavailable`, without disclosing which condition applies.

Example conflict:

```json error:common
{
  "status": "error",
  "code": "version_conflict",
  "message": "The goal has changed since this edit was prepared. Review its current state.",
  "recovery": "read_before_retry"
}
```

## Retries and request IDs

Use a fresh UUID `requestId` per new action. Request identity is scoped to the authenticated member and operation; reuse with different arguments returns `request_id_conflict`.

A goal save compares only supplied fields. If they already match current state, return `no_change` with current state and perform no write or search. If an already-applied request would now change the goal, return `version_conflict` and current state. A supplied stale version also rejects a new edit that would change state. Otherwise apply the patch atomically. There is no historical receipt replay.

An edit's `expectedVersion` is optional and checks the complete editable state. `scopeVersion` and `contextRevisionId` identify matching inputs and support optional narrower preconditions. Omitting a field preserves its current value, including when other actors edit concurrently.

Search requests capture current matching inputs atomically. Repeating the same request reuses its admitted run without another workflow, even if the first admission reused a different request's run. Changed arguments conflict; superseded goal inputs conflict. A separately authorized search retry is a new action.

Goal wording, private details, audience, and visibility commit together. Rejected edits change nothing. MCP saving starts no search, so search failure cannot turn a save into a partial save/search outcome.

## Limits

| Limit | Value |
| --- | --- |
| Goal `text` | 2,000 characters |
| Private details (`additionalContext`) | 4,000 characters |
| Groups per goal | 100 |
| Running searches per goal | 1 |
| MCP reads per member | 100 per minute, shared across clients |
| MCP writes per member | 30 per minute, shared across clients |
| Access token lifetime | 1 hour |
| Staying connected (`offline_access`) | Until 90 days without use |

Sign-in endpoints are rate limited. If you see HTTP `429` while signing in, wait a minute and try again.

MCP discovery and transport messages count toward the read limit. A single tool call over the limit returns the `rate_limited` tool error, so the agent can wait and retry. Other messages over the limit, and any batch, receive HTTP `429`. Each operation in a batch counts separately, and a batch above either limit is rejected before any tool runs. Both responses include `Retry-After: 60`.

Each search does real work for every network it covers. Start one only when you're asked, and never in a loop. A `full` search on a goal that was already searched replaces matches nobody has acted on.

Results are plain JSON. [`get_goal`](/docs/mcp/tools/get_goal) returns goal state and versions; [`get_goal_matches`](/docs/mcp/tools/get_goal_matches) returns saved matches. Reads reflect the current search lifecycle, including failed or cancelled background executions, without starting new matching work. If a request is rate limited, respect the returned retry guidance and do not start searches in a loop.

## Private note changes

`save_person_notes` requires the exact person's current `expectedVersion` and 1–12 changes. Each saved fact is one single-line claim of at most 280 characters. A rejected batch saves nothing. Notes stay private and saving starts no search. `find_people` requires a 2–200 character name or email query and returns at most 20 identities from your own Rolodex.

Note conflicts can include `personId` and `noteState` with the current owned identity, version, and note history. Re-read before preparing a new action. Reusing a request ID with different arguments returns `request_id_conflict`. An identical retry returns `no_change` only while its original effects still hold; an intervening correction or forget returns `version_conflict`. A new action needs a new ID and current version. Forget removes a fact from active use without restoring a predecessor; retained history is not erased.

## Person saves

`get_person` reads one exact owned person with an opaque version. `save_person` requires that version for an edit and uses a member-scoped UUID request ID. Create with a name and how the member knows them. Supplied sections replace whole sections; source basis is required. Email addresses are additive, up to 20 per call. Profile claim sections allow up to 40 single-line claims of 400 characters; summary allows 4,000 characters. Name allows 120 characters; LinkedIn URL 2,048; company and position 200 each; basis 500.

Identity conflicts reject the atomic save; an existing email on creation returns the owned current `personState` for review rather than merging. A stale version or changed original retry effect returns `version_conflict`. Changed arguments with an existing ID return `request_id_conflict`. Review current details and prepare a new action rather than restoring older values. See [`save_person`](/docs/mcp/tools/save_person) for clearing and reset semantics.
