# MCP retries and errors

Source: https://docs.pipsocket.com/mcp-errors

When to send a trade again over MCP, and every refusal the server answers.

A trading tool's answer is the REST answer, so its retry rules are the [REST API's](/errors). Here they are in MCP's terms, with the refusals that come from the MCP server itself.

## Retrying a trade

`place_order`, `modify_order`, `cancel_order`, `set_position_levels`, `close_position` and `close_by` need an `idempotency_key` you choose: a new one for each trade you mean to make. Sent again with the same key, a trade returns the first request instead of trading twice. A key protects a request for 7 days after its outcome is known.

| Answer | Do |
| --- | --- |
| `200` | MT5 took it: the body's `state` is `completed`, `placed` or `partial`. `partial` is final for that request. Or `executed`, for the same key sent again: an owner or admin settled it after checking the broker (a person's finding, not MT5's answer). |
| `202` | `pending` or `unknown`: never send it again. Follow it with `get_request`. |
| `422 trade_rejected` | MT5 rejected it: `result.retcode` says why. Change it, then send it with a **new** key. |
| `503 not_sent` | The terminal didn't send it. Send it again with a **new** key. |
| `409 symbol_busy`, `retry_after` a number | Nothing was sent. Send it again with the **same** key after that many seconds. |
| `409 symbol_busy`, `retry_after` `null` | Nothing was sent, and an earlier request is `unknown`. Don't retry: look up the request in `blocking` with `get_request`. |
| `409 not_executed` | The same key, for a request later settled as never executed. Check `list_positions` and the history before anything else, above all when `settlement` is `operator`. |
| Any other `503` or `504` | Send it again with the **same** key: if the first was recorded you get it back, and nothing is sent twice. |

`get_request` answers `200` whatever the request's state, `pending`, `unknown` and `rejected` included: the `200` says the lookup worked, and `state` says how the trade went.

The others:

- **`check_order`** sends nothing to the trade server: safe to repeat.
- **`engage_kill_switch`** and **`lift_kill_switch`** take no key and are safe to repeat: engaging again retries whatever is left of the sweep. `503 account_not_connected` from `engage_kill_switch` still means engaged: read `get_kill_switch`, the positions and the pending orders, then engage again.
- **`resend_alert`** needs an `idempotency_key`, a key of the hook's: the same key with the same alert and accounts answers the one resend it made, for 7 days, and never makes another, even when two calls race. The same key for a different alert or accounts is refused `409` with `reason` `key_reused`. A new resend needs a new key.

## Retrying account and hook tools

These answer the dashboard's status and body, and their `retry_after` is always `null`.

- **`start_account`, `stop_account`, `restart_account`** are asked once. A `503` means the outcome is unknown: read `get_account_status` rather than asking again. `409` means the account is busy with another change: wait, then read it.
- **A `503` from a change** (creating, changing, pausing or deleting a hook, a target or a symbol mapping) means it may or may not have happened: read it back (`get_hook`, `list_hooks`, `list_symbol_maps`) before trying again. A lost `create_hook` or `rotate_hook_url` is recovered with `get_new_hook_url`, never by doing it again (see [hook URLs](/mcp-tools#hook-urls)).
- **A `503` from a read** can be retried shortly.

## Refusals

Some refusals are answered by the server as HTTP errors, before any tool runs. The rest are tool errors: a tool result with its `status` and a `body`, marked `isError`.

### HTTP errors

| Status | When | Do |
| --- | --- | --- |
| `401` | No access token. `WWW-Authenticate` names the metadata and the scopes to ask for. | Connect: your app starts the sign-in. |
| `401` `invalid_token` | The token has expired, isn't valid, isn't a Pipsocket MCP token, isn't sent as `Authorization: Bearer <token>`, or is DPoP-bound. | Refresh it, or connect again. DPoP-bound tokens are never accepted: connect without DPoP. |
| `403` `insufficient_scope` | The connection lacks the scope in the workspace the call acts in, where your role would allow it. Nothing ran. | Connect again from the app and tick that scope for that workspace. |
| `403`, no body | The request had an `Origin` header: the server doesn't answer web pages. | Call it from an app or a server. |
| `503`, `Retry-After: 5` | Pipsocket couldn't fetch the keys to check the token. The token may well be good. | Try again shortly; don't sign in again. |

Their bodies are JSON-RPC errors, with a `message` that says what to do. The `insufficient_scope` challenge names the scope and the workspace, so the app can tell you which box to tick:

```text
HTTP/1.1 403 Forbidden
WWW-Authenticate: Bearer error="insufficient_scope", scope="read trade offline_access", resource_metadata="https://mcp.pipsocket.com/.well-known/oauth-protected-resource/mcp", error_description="needs trade in Acme Trading"

{ "jsonrpc": "2.0", "error": { "code": -32000, "message": "This connection needs trade in Acme Trading. Reconnect and tick trade for Acme Trading." }, "id": 7 }
```

`scope` is everything the connection was approved for, plus the scope it needs. Scopes can't say which workspace, so the approval page shows your current connection ticked, and you tick the box the message names.

### Tool errors

| Status | `error` | When | Do |
| --- | --- | --- | --- |
| `404` | `no_such_account`, or the dashboard's `NoSuchAccount`, `NoSuchHook`, `NoSuchWorkspace` | No such account, hook or workspace, or one in a workspace you're not in: the two look the same. | Check the id with `list_accounts` or `list_hooks`. |
| `403` | `forbidden_role` | Your role in the workspace doesn't allow the scope: only its owners and admins can trade or manage. The message names the workspace. | Ask an owner. Connecting again can't fix it. |
| `503` | `accounts_unavailable`, `access_unavailable` | Pipsocket couldn't read the account's entry, or check what the connection may do there. Nothing was sent. | Try again. |
| `503` | `control_unavailable`, `unavailable`, or the dashboard's `AuthenticationUnavailable` | Pipsocket couldn't answer, run or check an account or hook tool. | A read: try again shortly. A change: it may or may not have happened, read it back first. |

Everything else is the REST API's own answer, with its [error codes](/errors#error-codes), or the dashboard's for account and hook tools. Their bodies name the error as the dashboard does, in `_tag` (`{ "_tag": "HookUrlUnavailable", "reason": "expired", "message": "..." }`).

### Arguments a tool can't use

Missing or wrong arguments are refused before anything is sent, as a tool error whose text says what's wrong (it has no `status`), so the model can correct the call: a missing `idempotency_key`, an `account` that isn't an account id, a missing path value. Account and hook tools also refuse arguments they don't know, so a misspelt setting never passes as a change that wasn't asked for. Trading tools pass the body's fields on as they are, so the engine refuses one it doesn't know, as over REST.

