MCP Server (Live)
Connect an AI agent directly to the Live Execution API via the Model Context Protocol — manage live subscriptions and receive signed webhooks, no custom integration code required.
#Overview
emidlabs-live-mcp is a hosted Model Context Protocol (MCP) server that wraps the Live Execution API as agent-callable tools — the live counterpart to emidlabs-backtest-mcp. Point any MCP-compatible client or coding agent at it and it can create and manage live subscriptions and read emitted signals directly, without you writing a REST integration.
Endpoint: https://mcp.live.emidlabs.com/mcp (Streamable HTTP transport).
live.emidlabs.com. Every subscription it creates still uses your own EmidLabs API key and counts against your account's own concurrent-subscription limit, the same as calling the REST API directly.#Connecting an agent
Pick your client
Claude and ChatGPT discover this server automatically over OAuth. Cursor and most coding agents instead want the raw JSON config block below.
Add the connector
Add the server to your MCP client's configuration. The exact file differs by client, but the shape is the same everywhere — a name and a URL:
{
"mcpServers": {
"emidlabs-live": {
"url": "https://mcp.live.emidlabs.com/mcp"
}
}
}/mcp path — https://mcp.live.emidlabs.com/mcp, not just mcp.live.emidlabs.com. Dropping the path is the most common reason a connector fails to register, and the resulting error rarely makes the real cause obvious.Authenticate
Three ways to authenticate, and you can combine more than one — the connector tries them in the order below:
| Method | How | Best for |
|---|---|---|
| OAuth login (recommended) | Just add the connector with no extra config. Claude/ChatGPT discover the login flow automatically and prompt you to sign in with your EmidLabs account — a dedicated API key is created for your account the first time and reused after that. | Personal Claude.ai / ChatGPT connectors. Nothing to copy, paste, or type — ever. |
| x-api-key header | Configure a specific EmidLabs API key once when adding the connector (e.g. Claude’s "custom header" / static_headers option) — attached automatically to every request from then on. | Org-managed connectors where an admin wants every member sharing one specific key instead of individual OAuth logins. |
| Tool argument | Pass an API key as liveApiKey on each call — the agent is told the key directly (e.g. in conversation). | Coding agents, local/CLI use, or any client without connector-level auth support. |
#Tools
create_subscription
Subscribes a strategy to live evaluation on one asset pair. The strategy re-evaluates every time a candle closes on its own configured timeframe, emitting an Entry or Exit signal whenever decision.entry/decision.exit turns true — no position tracking, signal-only.
No fixed asset list to consult first — any pair the chosen exchange actually lists can be subscribed. That's broader than submit_backtest, which only covers assets EmidLabs has already extracted and stored historical candles for.
| Argument | Type | Description |
|---|---|---|
| liveApiKey | string (optional) | Not needed if you connected via OAuth login or the x-api-key header is configured on this connector. |
| assetPair | string | Must be in the exact format the chosen provider expects — no translation happens anywhere. Coinbase: "BTC-USDC" (hyphenated). Binance: "BTCUSDC" (no hyphen). |
| provider | string (optional) | Which market data provider to listen on: "coinbase" or "binance". Defaults to "coinbase" when omitted. |
| strategySnapshotJson | object | The Strategy DSL object — see the strategy-dsl-spec resource below. Identical shape to submit_backtest. |
| webhookUrl | string (optional) | Absolute https:// URL. When set, every emitted signal is also POSTed here — see Webhooks below. |
| model | string (optional) | "stateless" (default when omitted) or "stateful" — see Signal Model below. |
| isPositioned | boolean (optional) | Only meaningful when model is "stateful". Defaults to false (not positioned yet) when omitted. |
| liveBaseUrl | string (optional) | Defaults to the public production API. |
There's no check that assetPairactually exists on the chosen provider at creation time — an invalid pair, or the right pair in the wrong provider's format, is accepted immediately and then the subscription's status moves to "Errored" within about a minute. Check get_subscriptionshortly after creating if you're not sure the format was right.
webhookSecret is only ever returned in this one response — save it immediately. No tool or endpoint echoes it back later, the same one-time-reveal rule EmidLabs API keys already follow.create_subscriptions_batch
Subscribes the SAME strategy to live evaluation on multiple asset pairs at once, one subscription per entry in assetPairs. Best-effort per item — one bad asset pair never fails the rest of the batch, check each item's own status/error.
| Argument | Type | Description |
|---|---|---|
| liveApiKey | string (optional) | Not needed if you connected via OAuth login or the x-api-key header is configured on this connector. |
| assetPairs | string[] | One entry per subscription to create. Max 50 per call. |
| strategySnapshotJson | object | Shared by every subscription in this batch — identical shape to create_subscription's own field. |
| provider | string (optional) | Shared by every subscription in this batch. Defaults to "coinbase" when omitted. |
| webhookUrl | string (optional) | Shared by every subscription in this batch — each still gets its own signed deliveries and its own secret. |
| model | string (optional) | Shared by every subscription in this batch. "stateless" (default when omitted) or "stateful" — see Signal Model below. |
| isPositioned | boolean (optional) | Shared by every subscription in this batch, only meaningful when model is "stateful". Defaults to false when omitted. |
| liveBaseUrl | string (optional) | Defaults to the public production API. |
webhookSecret when webhookUrl is set — returned once per item in this response, same one-time-reveal rule as create_subscription.stop_subscription
Stops a live subscription. Idempotent — stopping an already-stopped subscription just returns its current status.
| Argument | Type | Description |
|---|---|---|
| liveApiKey | string (optional) | Not needed if you connected via OAuth login or the x-api-key header is configured on this connector. |
| subscriptionId | string | The id returned by create_subscription. |
| liveBaseUrl | string (optional) | Defaults to the public production API. |
stop_subscriptions
Stops a specific list of live subscriptions, in one call. Idempotent per id — an already-stopped subscription is simply skipped, not an error.
| Argument | Type | Description |
|---|---|---|
| liveApiKey | string (optional) | Not needed if you connected via OAuth login or the x-api-key header is configured on this connector. |
| ids | string[] | Subscription ids to stop, as returned by create_subscription/list_subscriptions. Max 200 per call. |
| liveBaseUrl | string (optional) | Defaults to the public production API. |
stop_all_subscriptions
Stops every currently active live subscription on this account, in one call. Idempotent — already-stopped subscriptions are simply skipped.
| Argument | Type | Description |
|---|---|---|
| liveApiKey | string (optional) | Not needed if you connected via OAuth login or the x-api-key header is configured on this connector. |
| liveBaseUrl | string (optional) | Defaults to the public production API. |
stop_subscriptions with an explicit id list if you only want some stopped.update_subscription
Updates a subscription's model/isPositioned/webhookUrl. assetPair, timeframe, provider, and the strategy itself can never be changed after creation — stop_subscription and create_subscription for that.
| Argument | Type | Description |
|---|---|---|
| liveApiKey | string (optional) | Not needed if you connected via OAuth login or the x-api-key header is configured on this connector. |
| subscriptionId | string | The id returned by create_subscription. |
| model | string (optional) | "stateless" or "stateful". Omit to leave unchanged — see Signal Model below. |
| isPositioned | boolean (optional) | Only meaningful when this subscription's model is (or is being set to) "stateful". Omit to leave unchanged. |
| webhookUrl | string (optional) | A new absolute https:// URL, or an empty string to remove the existing webhook. Omit to leave unchanged. |
| liveBaseUrl | string (optional) | Defaults to the public production API. |
webhookUrl to a non-empty value generates a fresh webhookSecret, returned once in this response only — save it immediately.get_subscription
Fetches a live subscription's current status by id — asset pair, provider, timeframe, status, model/isPositioned, and the last candle it evaluated.
| Argument | Type | Description |
|---|---|---|
| liveApiKey | string (optional) | Not needed if you connected via OAuth login or the x-api-key header is configured on this connector. |
| subscriptionId | string | The id returned by create_subscription. |
| liveBaseUrl | string (optional) | Defaults to the public production API. |
list_subscriptions
Lists live subscriptions on this account, paginated.
| Argument | Type | Description |
|---|---|---|
| liveApiKey | string (optional) | Not needed if you connected via OAuth login or the x-api-key header is configured on this connector. |
| page | integer (optional) | Page number, starting at 1. Defaults to 1. |
| pageSize | integer (optional) | Results per page, 1-100. Defaults to 20. |
| status | string (optional) | Exact-match filter: "Active", "Stopped", or "Errored". Omit to return every status. |
| liveBaseUrl | string (optional) | Defaults to the public production API. |
get_subscription_signals
Lists Entry/Exit signals a live subscription has emitted, paginated, each with the candle it fired on, which named conditions were true, the score breakdown, and — for Entry signals — the computed stop-loss/take-profit price levels.
| Argument | Type | Description |
|---|---|---|
| liveApiKey | string (optional) | Not needed if you connected via OAuth login or the x-api-key header is configured on this connector. |
| subscriptionId | string | The id returned by create_subscription. |
| page | integer (optional) | Page number, starting at 1. Defaults to 1. |
| pageSize | integer (optional) | Results per page, 1-100. Defaults to 20. |
| type | string (optional) | Exact-match filter: "Entry" or "Exit". Omit to return both. |
| liveBaseUrl | string (optional) | Defaults to the public production API. |
#Signal Model: Stateless vs Stateful
Every subscription starts "stateless" — a signal is recorded/delivered every time decision.entry/decision.exitevaluates true, even repeatedly, if the underlying condition isn't edge-triggered (e.g. a plain rsi14 > 55 instead of crossUp(...)/crossDown(...) — it stays true for many consecutive candles). Switch a subscription to "stateful" via update_subscription to have it track its own Entry/Exit position internally (never queried from your exchange) and only fire on an actual transition — everything else is silently suppressed.
isPositioned is a manual correction, never inferred from a real exchange balance — this server has no broker integration of its own. Fix drift yourself via update_subscription when it happens (e.g. a trade placed manually outside this system).Full behavior table: Live Execution API — Signal Model.
#Webhooks
Pass webhookUrl to create_subscription and every signal that subscription emits is also POSTed there as JSON — no polling required. Each delivery is signed so you can verify it really came from EmidLabs:
| Header | Format | Verification |
|---|---|---|
| X-Emidlabs-Signature | t={unix timestamp},v1={hex HMAC-SHA256} | Recompute HMAC-SHA256(webhookSecret, `${timestamp}.${rawBody}`) and compare to v1 — same scheme EmidLabs itself uses to verify inbound Stripe webhooks, applied here on the sending side. |
Delivery retries up to 3 times (short backoff) on network errors, timeouts, 429s, and 5xx responses from your endpoint — a slow or unreachable webhook never blocks or delays signal evaluation itself.
#Resources
strategy-dsl-spec
A condensed, markdown reference for the exact Strategy DSL JSON shape expected by create_subscription's strategySnapshotJson field (emidlabs://strategy-dsl-spec) — identical shape to emidlabs-backtest-mcp's own copy and the Copy Spec for AI block on the Strategy System page.
inputs/conditions/score section is fetched live from emidlabs-strategy-mcp's domain owner (strategy-api's own GET /dsl-core-reference) and cached for a few minutes, rather than hand-copied — the same core emidlabs-backtest-mcp and emidlabs-strategy-mcp fetch too, so it can't drift out of sync between them. This server's own local part deliberately omits entryFeePct/exitFeePct (backtest-only), and documents riskManagement as advisory-only — a live subscription never auto-closes anything, unlike a backtest.Reading this resource first is optional, not required — every field in create_subscription's own tool schema already documents its exact shape directly, since MCP clients don't reliably fetch a separate resource before generating a call.
#Rate limits
Two independent limits apply, and they surface differently on purpose so they're never confused for each other:
| Limit | Scope | What it protects | What you see |
|---|---|---|---|
| Live-api quota / concurrent-subscription limit | Per API key | Your account’s real usage/billing limit | A 4xx from live-api, passed through as an MCP tool error. |
| MCP infra guard | Per IP/connection | This MCP process itself, against runaway loops or malformed floods | A 429 with a message explicitly identifying it as the MCP layer’s own guard, not your account quota. |
#Scope
This server only exposes live subscription management — creating, stopping, inspecting subscriptions, and reading their signals. It does not expose any Console actions (creating or rotating API keys, changing billing plans, account management). Those remain human-only actions in the Console UI, deliberately kept out of what an autonomous agent can do unsupervised.