MCP retries and 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. 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_ordersends nothing to the trade server: safe to repeat.engage_kill_switchandlift_kill_switchtake no key and are safe to repeat: engaging again retries whatever is left of the sweep.503 account_not_connectedfromengage_kill_switchstill means engaged: readget_kill_switch, the positions and the pending orders, then engage again.resend_alertneeds anidempotency_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 refused409withreasonkey_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_accountare asked once. A503means the outcome is unknown: readget_account_statusrather than asking again.409means the account is busy with another change: wait, then read it.- A
503from 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 lostcreate_hookorrotate_hook_urlis recovered withget_new_hook_url, never by doing it again (see hook URLs). - A
503from 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:
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, 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.