# MCP tools

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

Every tool of the MCP server, the scope it needs, what it takes and what it calls.

Trading tools go to the account's terminal as [REST calls](/api/place-an-order); account and hook tools do what the dashboard does; `build_alert_message` runs on its own. Your app sees every tool your best role across the connection's workspaces allows: someone who is only ever a member sees the read tools.

## Answers

Every tool answers the same shape, as structured content and as the same JSON in text:

```json
{ "status": 202, "retry_after": null, "body": { "request": "req_5f0c2a9e41d7b3a8c6e2f190", "state": "pending", "...": "..." } }
```

- `status`: the HTTP status the REST API (or, for account and hook tools, the dashboard) answers.
- `retry_after`: the REST answer's `Retry-After` header in seconds, or `null` when it has none. Account and hook tools always have `null`.
- `body`: the REST (or dashboard) body, as it is.

A `4xx` or `5xx` status is a tool error (`isError`). A `2xx` never means filled: read the body's `state`, as over [REST](/errors).

## Ids and workspaces

Accounts (`acc_...`) and hooks (`hk_...`) are named by their own id: ids are unique across workspaces, so a call acts in the workspace the account or hook belongs to and takes no workspace. Only `create_hook` and `connect_account_link` name a workspace (`ws_...`). `list_accounts` and `list_hooks` cover every workspace where the connection can read, each item naming its workspace, or just the `workspace` you pass.

Each tool is marked for your app: **read-only** tools change nothing, **destructive** ones can close, cancel, stop or send live trades, and the rest (**changes**) change setup without trading. Your app may ask you before the destructive ones.

## Read

Needs `read` in the workspace of the account or hook, unless it says otherwise.

| Tool | Scope | Marked | Takes | Does |
| --- | --- | --- | --- | --- |
| `list_workspaces` | none | read-only | nothing | The connection's workspaces: id, name, your role and the scopes it has there now. |
| `list_accounts` | read | read-only | `workspace`? | MT5 accounts: id, workspace, login, server, status and host location. |
| `get_account_status` | read | read-only | `account` | The account's record, and what its engine reports now (`live`: `null` when the host can't be reached). |
| `get_account` | read | read-only | `account` | [`GET /v1/accounts/{account}`](/api/account): balance, equity, margin. |
| `list_positions` | read | read-only | `account` | [`GET .../positions`](/api/positions) |
| `list_pending_orders` | read | read-only | `account` | [`GET .../orders`](/api/pending-orders) |
| `list_history_deals` | read | read-only | `account`, `from`, `to` | [`GET .../history/deals`](/api/history) |
| `list_history_orders` | read | read-only | `account`, `from`, `to` | [`GET .../history/orders`](/api/history) |
| `list_symbols` | read | read-only | `account` | [`GET .../symbols`](/api/symbols) |
| `get_symbol` | read | read-only | `account`, `symbol` | [`GET .../symbols/{symbol}`](/api/symbols) |
| `get_tick` | read | read-only | `account`, `symbol` | [`GET .../symbols/{symbol}/tick`](/api/symbols) |
| `get_request` | read | read-only | `account`, `request` | [`GET .../requests/{request}`](/api/look-up-a-request): a trade request and how it ended. |
| `get_kill_switch` | read | read-only | `account` | [`GET .../kill-switch`](/api/kill-switch) |
| `list_hooks` | read | read-only | `workspace`? | TradingView hooks, with status, applied, revision and targets. The URL is masked. |
| `get_hook` | read | read-only | `hook` | One hook, URL masked. |
| `list_hook_alerts` | read | read-only | `hook`, `state`?, `reason`?, `account`?, `symbol`?, `before`?, `limit`? | The hook's [alert log](/tradingview-outcomes), newest first, up to 100 a page. Pass the answer's `next` as `before` for the next page. |
| `get_hook_alert` | read | read-only | `hook`, `alert` | One alert, its outcome on each target account and each engine's trace. |
| `list_symbol_maps` | read | read-only | `account` | The account's TradingView symbol mappings. |
| `list_broker_symbols` | read | read-only | `account`, `names`? | The broker's symbols on the account; with `names` (comma-separated), their specifications and default pip sizes. |
| `list_owned` | read | read-only | `account` | What the account's hooks own on it: positions, orders, lanes and managed stops. |
| `connect_account_link` | read | read-only | `workspace`, `server`?, `login`? | A dashboard link that opens the connect dialog for the workspace, filled in. You connect the account there. |
| `build_alert_message` | none | read-only | the builder's choices, all optional ([below](#building-an-alert-message)) | The JSON message for a TradingView alert, as the dashboard's builder writes it, with problems and warnings. |

`?` marks an optional argument. `from` and `to` are broker server time in seconds, as over REST.

## Trade

Needs `trade` in the account's (or, for `resend_alert`, the hook's) workspace. A trade tool takes the REST call's path values and body fields as arguments, named as on its REST page, plus `account`.

| Tool | Scope | Marked | Takes | Does |
| --- | --- | --- | --- | --- |
| `check_order` | trade | read-only | `account` and the order's fields | [`POST .../orders/check`](/api/check-an-order): MT5's dry run. Sends nothing to the trade server. |
| `place_order` | trade | destructive | `account`, `idempotency_key` and the order's fields | [`POST .../orders`](/api/place-an-order) |
| `modify_order` | trade | destructive | `account`, `order`, `idempotency_key` and the new values | [`PATCH .../orders/{order}`](/api/modify-an-order) |
| `cancel_order` | trade | destructive | `account`, `order`, `idempotency_key` | [`DELETE .../orders/{order}`](/api/cancel-an-order) |
| `set_position_levels` | trade | destructive | `account`, `position`, `idempotency_key`, `sl`, `tp` | [`PATCH .../positions/{position}`](/api/set-stops) |
| `close_position` | trade | destructive | `account`, `position`, `idempotency_key` and the close's fields | [`POST .../positions/{position}/close`](/api/close-a-position) |
| `close_by` | trade | destructive | `account`, `position`, `idempotency_key`, `position_by` | [`POST .../positions/{position}/close-by`](/api/close-by) |
| `engage_kill_switch` | trade | destructive | `account`, `deviation`? | [`POST .../kill-switch`](/api/kill-switch) |
| `lift_kill_switch` | trade | destructive | `account` | [`DELETE .../kill-switch`](/api/kill-switch) |
| `resend_alert` | trade | destructive | `hook`, `alert`, `accounts`, `idempotency_key` | Sends an earlier alert again, as a new one, to these target accounts (1 to 20): live trades. Only targets whose delivery is settled and wasn't partly done. |

For the account tools, `idempotency_key` is your own key for the request, sent as the `Idempotency-Key` header and scoped to the account, exactly as over REST. For `resend_alert` it's 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 the same key with a different alert or accounts is refused `409` (`key_reused`). It's required wherever it's listed. How to retry is on [retries and errors](/mcp-errors).

## Manage

Needs `manage` in the account's or hook's workspace (`create_hook`: the workspace it names).

| Tool | Scope | Marked | Takes | Does |
| --- | --- | --- | --- | --- |
| `start_account` | manage | changes | `account` | Starts the account's terminal. |
| `stop_account` | manage | destructive | `account` | Stops the terminal: Pipsocket sends nothing to the account until it's started. Positions, pending orders and their stops stay at the broker as they are; to close them, use the kill switch first. |
| `restart_account` | manage | changes | `account` | Restarts the terminal. |
| `create_hook` | manage | changes | `workspace`, `name`, `maxAgeS`?, `tvIpsOnly`?, `quota`?, `notify`?, `targets`? | Makes a hook in the workspace. Every target must be an account of that workspace. Answers the hook and its `url` (below). |
| `update_hook` | manage | changes | `hook`, `name`?, `maxAgeS`?, `tvIpsOnly`?, `quota`?, `notify`? | The hook's own settings. |
| `pause_hook` | manage | changes | `hook` | Alerts are recorded, not acted on. `status` is `pausing` until the hook and every engine have it, then `paused`. |
| `resume_hook` | manage | changes | `hook` | New alerts trade again. |
| `delete_hook` | manage | destructive | `hook` | Deletes the hook. `done: false` means removal goes on in the background. Owned positions and managed stops stay until they close (`list_owned`). |
| `rotate_hook_url` | manage | destructive | `hook` | A new secret: the old URL stops once the hook has it. Answers the new `url` (below). |
| `get_new_hook_url` | manage | read-only | `hook` | The URL this connection just made (below). |
| `add_hook_target` | manage | changes | `hook`, `account`, `shortName`?, `enabled`?, `settings`? | Adds a target account of the hook's workspace, with its [settings](/tradingview#target-settings). |
| `update_hook_target` | manage | changes | `hook`, `account`, `shortName`?, `enabled`?, `settings`? | Changes a target. |
| `remove_hook_target` | manage | destructive | `hook`, `account` | Removes a target. |
| `test_hook` | manage | read-only | `hook`, `message` | A [dry run](/tradingview-messages#dry-runs) of a message on the hook's targets: checked and sized by each engine, never traded. The message is sent as written: give real values, not `{{placeholders}}`. |
| `set_symbol_map` | manage | changes | `account`, `from`, `to`, `pipSize`? | Maps a TradingView symbol or ticker (`BITSTAMP:BTCUSD`) to a broker symbol on the account. |
| `delete_symbol_map` | manage | changes | `account`, `from` | Removes a mapping. |
| `release_owned` | manage | destructive | `account`, `hook`, `tag`?, `symbol` | Releases a hook lane's ownership on the account: its positions are no longer the hook's. |

Hook settings and targets take the dashboard's own fields; anything a tool doesn't know is refused, not ignored. Each account and hook tool answers the dashboard's status and body for the same action.

Pausing or deleting a hook doesn't close what it already opened: its positions and managed stops stay until they close.

`start_account`, `stop_account` and `restart_account` are asked once. After a `503` the outcome is unknown, and the account stays busy until its lease runs out: read `get_account_status` rather than asking again. `409` means another change to the account is still going: wait, then read it.

## Building an alert message

`build_alert_message` takes the dashboard builder's choices, not the message's own fields (those are on [alert messages](/tradingview-messages)). Every argument is optional: left out, it builds a buy at market with each account's own size, on the chart's symbol. Values may be numbers or TradingView placeholders such as `"{{close}}"`.

| Argument | Takes |
| --- | --- |
| `mode` | `indicator` or `strategy` |
| `action` | `open`, `close`, `reverse`, `modify`, `cancel`, `close_all` or `kill_switch` |
| `side` | `buy`, `sell` or `all` |
| `symbol`, `tag` | Text. `symbol` is the chart's by default. |
| `size` | `{ "kind": "default" }`, or `{ "kind": "lots" \| "risk_percent" \| "risk_amount", "value": ... }` |
| `sl`, `tp` | `{ "unit": "pips" \| "points" \| "price" \| "percent", "value": ... }`, or `null` |
| `ladder` | Take-profit rungs: `[{ "unit": ..., "value": ..., "part": ... }]`, or `null` |
| `entry` | A pending entry: `{ "type": "limit" \| "stop" \| "stop_limit", "at": { "unit": "price" \| "offset_pips" \| "offset_points", "value": ... }, "limit"?: ..., "expires"?: ... }`, or `null` |
| `trailing` | In pips: `{ "kind": "set", "value": { "distance": ..., "step"?: ..., "start"?: ... } }`, `{ "kind": "clear" }` or `{ "kind": "off" }` |
| `breakEven` | In pips: `{ "kind": "set", "value": { "trigger": ..., "offset"?: ... } }`, `{ "kind": "clear" }` or `{ "kind": "off" }` |
| `portion` | For a close: `{ "kind": "all" }`, or `{ "kind": "percent" \| "lots", "value": ... }` |
| `accounts` | The hook's targets the alert is for (account ids or target short names); every target when left out |
| `maxAge`, `slippage` | The alert's age limit for entries, in seconds, and the slippage for market orders, in points |
| `src` | The template's short id in its event ids (up to 8 lowercase letters and digits); a new one when left out |

A buy risking 1% with a 25 pip stop:

```json
{ "action": "open", "side": "buy", "size": { "kind": "risk_percent", "value": 1 }, "sl": { "unit": "pips", "value": 25 } }
```

It answers `{ "message": "...", "problems": [...], "warnings": [...] }` in `body`: paste `message` into the alert as it is, placeholders and all. A message with problems would be refused on every alert.

To dry-run it with `test_hook`, send a copy with each `{{placeholder}}` replaced by what TradingView would send: the symbol, and the current UTC time for `{{timenow}}` (written like `2026-11-08T14:00:00Z`). The time must be now: an entry older than its [age limit](/tradingview-messages#age-limits) (30 seconds unless the hook or message sets another) is refused `stale`, as a real alert would be. The test is sent as written: a placeholder left in a time or a number is refused before anything is checked, and one left in text goes through as those characters, so replace them all.

## Hook URLs

A hook's URL carries its secret: whoever holds it can send trades. Over MCP it's shown only to the connection that made it, and only briefly:

- **`create_hook` and `rotate_hook_url`** answer the URL once the hook has the new secret, usually at once. Until then `url` is `null`: call `get_new_hook_url` a few seconds later.
- **`get_new_hook_url`** answers the URL only if this connection made the hook's current secret in the last 15 minutes. Otherwise it answers `409` with a `reason`:
  - `not_applied_yet`: the hook doesn't have the secret yet. Ask again in a few seconds.
  - `superseded`: another rotation replaced it. Tell the person; don't rotate again.
  - `expired`: more than 15 minutes, or this connection didn't make it. Reveal it in the dashboard (it asks for your password).
- **A lost answer** to `create_hook` or `rotate_hook_url` is recovered with `list_hooks` and `get_new_hook_url`, never by creating or rotating again.

The URL is never in a list, a hook's details or the audit log. It does go into your chat with the app, so treat that chat as you would the URL.

## Set up TradingView from a chat

Ask for the whole thing at once: "Set up a hook that trades my EMA strategy on BTCUSD on my FBS account, 1% risk, 25 pip stop." With manage and trade ticked, an app can:

1. `list_accounts` to find the account and its workspace.
2. `list_broker_symbols` for the broker's name for BTCUSD, and `set_symbol_map` to map TradingView's symbol to it.
3. `create_hook` in that workspace, with the account as its target; `get_new_hook_url` if `url` was `null`.
4. `build_alert_message` for the message, and `test_hook` to dry-run a copy of it, with real values in place of the placeholders, on the account.

Then you paste the URL and the message into the TradingView alert. See [TradingView alerts](/tradingview).

