Point your agent at your own trading platform
Connect Claude, Cursor, Codex — or any client that speaks the Model Context Protocol — to the trading platform running on your own machine. The agent reads your stored market data, writes and compiles a strategy, backtests it, watches your live node, and drafts the orders. You confirm them.
The server is a binary on your box. Nothing is uploaded, no account is involved, and no vike service sits between your agent and your venue.
Research → Test → Watch → Act
The whole loop, in one conversation. Each tool below is a real tool the server serves; the grouping is how a session actually moves through them.
Research 5
Read what is already on the box — the stored series, the indicator roster the engine binds, the starter strategies, and the knobs a script actually declares.
-
list_series— what is in the store — venue, symbol, timeframe, coverage -
list_indicators— the indicator set a strategy can call, with each one's parameters and defaults -
list_templates— the starter strategies that ship with the platform -
discover_params— the knobs one script declares — read off the script, never guessed -
list_strategies— the strategies the data service already holds
Test 4
Compile the script, then run it: a backtest, a parameter grid, an anchored walk-forward. Nothing here can move an order — a backtest mutates nothing.
-
validate_strategy— compile it; compiling is the validation -
run_backtest— one run over stored bars, on the same engine that trades live -
run_sweep— a parameter grid, ranked -
run_walk_forward— anchored walk-forward — the out-of-sample windows, not one flattering number
Watch 1
Read a running node: its orders, positions, per-venue equity and recent events. Two resources route into the same implementation, so a URI can never answer what a tool was withheld from answering.
-
node_snapshot— live orders, positions, per-venue equity and recent events
Act 7
Place, change, cancel and stop. Every one of these takes two calls, and the second has to carry a token the first one issued.
-
submit_order— place one order -
cancel_order— cancel one order by its client order id -
modify— amend a resting order's price or quantity -
flatten— close a position on one venue and symbol -
market_exit— exit at market -
mass_cancel— cancel every resting order on a venue, or on every engine -
set_trading_state— set what the node is allowed to do next
Watch also answers as 2 resources — vike://node/snapshot and vike://backtest/last — which route into the same
implementations the tools use rather than being a second source. That is why a withheld tool
cannot be reached through a URI instead.
You are always in control
An agent that can place orders is only as safe as what stands between its intent and the wire. Here is everything that does — stated as mechanism, because a promise is not a control.
Every write takes two calls
The first call never sends anything. It returns what the command would be, and a token. A second call has to carry that token back before a byte leaves the process.
| The preview returns | What it is |
|---|---|
will_execute | false on the preview. The one field to read: every way of failing the gate returns a preview rather than an error, so a call that believes it confirmed and did not looks otherwise identical. |
wire_command | the exact command that would be sent, resolved — not the arguments the agent typed. |
guardrail | a client-side estimate against your configured ceiling. Advisory, and it says so. |
node_verdict | the node's own dry-run of this command against the real control limits and risk gate. |
verified_by_node | whether that verdict was actually asked for AND answered. An absent verdict is not an approval, and this field is what says so. |
preview_token | what a confirming call has to carry back. |
The token fires at most once, expires after 60 seconds, and is bound to the command it previewed — presenting it for different arguments is refused, not quietly honoured. And a confirmation flag on its own is explicitly not a confirmation: a call that sets it without a matching token gets another preview and sends nothing.
The node checks it, not the client
verified_by_node is true only when the node itself dry-ran that exact command
against the real control limits and the execution core's risk gate. The client-side
estimate beside it is advisory and labelled as such — it cannot even price a market order,
because a market order carries no price.
An absent verdict is not an approval, and the surface says so in the response rather than leaving it to be inferred. Nothing an agent sends can loosen either limit; they are enforced where the orders are, not where the agent is.
Decide what it can reach before it starts
Scope is set by whoever launches the server, in the client's own launch command, and only they can widen it. The rings nest: each one serves a subset of the one before.
| Ring | Serves | Why it holds |
|---|---|---|
--profile full default |
every tool | The default. An absent flag is byte-identical to the surface that shipped before the flag existed. |
--profile read-only |
the read tools | An agent that may read the node, run backtests and write scripts, and cannot place an order. It is the negation of the write roster — the very list the preview gate routes on — so an eighth write tool would join the restriction by construction rather than by somebody remembering. |
--profile offline |
the tools that open no socket | Script authoring with no network at all. It keys off each tool's own declaration that it opens no socket, and a tool that declares nothing is withheld rather than admitted — it fails closed. |
A withheld tool is absent from the tool listing as well as refused when called, and
the two cannot disagree, because they are one derivation: a single pass over the tool
registry produces the served set and its complement. An advertised tool cannot be refused,
and a withheld tool cannot be routed. --deny-tool subtracts one more by name,
and is repeatable.
A record of what the agent attempted
Your node already logs the commands it accepted. The agent transcript is the other half: one appended record per tool call — the tool, its arguments, the scope the server was running under, and the verdict.
Refusals are the reason it exists. A refused call never reaches the node's own trail, because it was refused before anything was sent — so without this record the most important events would be the ones nothing captured. A preview is recorded separately from an execution, too: the agent called a write tool and an order left this process are different facts. It is off unless you turn it on, no secret values are written, and the file lives beside your other local state.
And the platform's own gates are still underneath
None of this replaces how the platform arms a venue in the first place. Absent credentials are the live gate — no keys, and a venue stays on paper. Above that, a per-venue setting in your policy file caps what each venue may do, and that ceiling can only ever refuse: it can lower a venue to paper, and it can never promote one.
So an agent handed a full-scope server on a box with no credentials configured is an agent trading on paper, with no extra step required to make that true. See what each venue arms.
A real session, end to end
Captured from the end-to-end harness that builds the real binaries and drives the real CLI, a real MCP client and a real paper node in one process tree. Nothing below is retyped, reordered or tidied — including the parts that are less flattering.
The agent, over MCP
Follow the three write calls: the first returns will_execute: false with a
token, the second quotes that token and is accepted, and the identical confirmation
replayed afterwards is refused — a token fires once.
# Vike Trader over MCP — a real stdio session, captured verbatim.
#
# Captured by scripts/cli_mcp_smoke.sh (the CLI+MCP end-to-end smoke) on Linux, from binaries built
# out of this repository. The server is `vike-cli mcp --node <host:port>`, speaking newline-delimited
# JSON-RPC 2.0 over stdin/stdout, pointed at a real vike-tradehub node running in the same throwaway
# project directory. That project holds no venue credentials, so the node's mount is PAPER and the
# order placed below rests in a paper book. No venue is contacted at any point. The symbol the order
# names, CLI_MCP_SMOKE_TOKEN, is that throwaway mount's own token id — it resolves against nothing.
#
# FRAMING. `client →` is the exact line that was written to the server's stdin, as sent. `server ←`
# is that request's response, shown formatted with `jq`; where a `jq` FILTER was applied it is
# NAMED on the line, and the filter is the only reason a response is ever not shown whole. No field
# is added, removed or altered. The one thing routinely not repeated is `result.content[0].text`,
# which on a successful tool call is `result.structuredContent` rendered for a human to read.
#
# Nothing here is retyped or paraphrased. Every id, every preview token and the client order id are
# the real ones this session produced. Absolute paths of the throwaway directory are shown as
# <project>; ANSI is stripped. Lines beginning with `#` are this header and nothing else.
──── 1. handshake
The client opens the server over stdio and declares the protocol version it speaks.
client → {"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"vike-transcript","version":"1"}}}
server ← the whole response, formatted with `jq .`
{
"id": 1,
"jsonrpc": "2.0",
"result": {
"capabilities": {
"prompts": {},
"resources": {},
"tools": {}
},
"protocolVersion": "2024-11-05",
"serverInfo": {
"name": "vike-cli",
"version": "0.1.0"
}
}
}
──── 2. what this server serves
One request. Two views of its one response: the tool names, then in full the entry for the
tool the rest of this session uses. The response as received carries every tool's JSON Schema,
which is why the first view is a filter rather than the whole thing.
client → {"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}}
server ← 17 tools — `jq '[.result.tools[].name]'`
[
"validate_strategy",
"discover_params",
"list_templates",
"list_indicators",
"run_backtest",
"run_sweep",
"run_walk_forward",
"list_strategies",
"list_series",
"node_snapshot",
"submit_order",
"cancel_order",
"modify",
"flatten",
"market_exit",
"mass_cancel",
"set_trading_state"
]
server ← `jq '.result.tools[] | select(.name == "submit_order")'`
{
"annotations": {
"destructiveHint": true,
"idempotentHint": false,
"openWorldHint": true
},
"description": "Submit an order on the vike-tradehub node. WITHOUT confirm:true this only PREVIEWS (executes nothing). Requires --node + VIKE_TRADEHUB_CONTROL_KEY.",
"inputSchema": {
"properties": {
"confirm": {
"description": "must be true AND accompanied by a valid `preview_token` to execute; either one missing returns a preview instead",
"type": "boolean"
},
"order_type": {
"description": "market | limit | stop | take_profit (default market)",
"type": "string"
},
"preview_token": {
"description": "the `preview_token` returned by this tool's preview call. Fires ONCE, expires after 60s, and is BOUND to the exact command it previewed — confirming a different command with it is refused.",
"type": "string"
},
"price": {
"type": "number"
},
"qty": {
"type": "number"
},
"reason": {
"description": "optional rationale — WHY this command is being issued. Recorded in the node's audit trail (control characters stripped, capped at 512 chars); never reaches the order, the core, or the venue.",
"type": "string"
},
"reduce_only": {
"type": "boolean"
},
"side": {
"description": "1 buy / -1 sell",
"type": "integer"
},
"symbol": {
"type": "string"
},
"trigger_price": {
"type": "number"
},
"venue": {
"type": "string"
}
},
"required": [
"venue",
"symbol",
"side",
"qty"
],
"type": "object"
},
"name": "submit_order"
}
──── 3. a READ tool
`node_snapshot` asks the live node what it currently holds. Reads need no confirmation:
nothing changes, so there is nothing to gate.
client → {"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"node_snapshot","arguments":{}}}
server ← `jq '.result.structuredContent'` (isError: false)
{
"balance": 0.0,
"bars": [],
"equity_total": 0.0,
"fault": null,
"held_exits": [],
"identity": {
"build": "git f7e6f2f9 dirty, built 2026-09-06T08:57:10Z, rustc 1.97.1 (8bab26f4f 2026-07-14), x86_64-unknown-linux-gnu",
"live": false,
"name": "tradehub",
"params": "qty=20 tick_size=0.01 min_half_spread_ticks=0 max_half_spread_ticks=0 round_trip_fee_rate=None kappa_default=50 tau_hold_ms=3600000 price_domain=UnitInterval variance_mode=LocalCapped horizon_mode=TimeToResolution spread_model=AvellanedaStoikov gamma=0.1 resolution_ts=None",
"strategy": "spread_maker"
},
"orders": [],
"positions": [],
"recent_events": [],
"seq": 0,
"symbol": "CLI_MCP_SMOKE_TOKEN",
"trading_state": "Active",
"venue": "polymarket",
"venues": []
}
──── 4. a WRITE tool, first call — THE MANDATORY PREVIEW
The same request an agent would make to place an order. It carries no `confirm`, and the
server does not execute it. Read three fields: `will_execute` is false, `node_verdict` is the
NODE's own dry-run against the real limits, and `preview_token` is the single-use token the second
call must quote back. `wire_command.Submit.client_order_id` was minted HERE — it is the id that
will be sent, so it is known before anything is.
client → {"jsonrpc":"2.0","id":4,"method":"tools/call","params":{"name":"submit_order","arguments":{"venue":"polymarket","symbol":"CLI_MCP_SMOKE_TOKEN","side":1,"qty":12,"order_type":"limit","price":0.42,"reason":"transcript capture: the two-call preview gate"}}}
server ← `jq '.result | {isError, structuredContent}'`
{
"isError": false,
"structuredContent": {
"guardrail": {
"max_notional": null,
"max_qty": null,
"notional": 5.04,
"qty": 12.0,
"within_limits": true
},
"node_verdict": {
"accepted": true,
"checked_by": "node",
"reason": null
},
"note": "PREVIEW ONLY — nothing was sent. `node_verdict` is the NODE's own dry-run against the real ControlLimits + RiskGate; it is the verdict that counts. To execute, call again with BOTH \"confirm\": true AND this exact \"preview_token\". The token fires once, expires after 60s, and is bound to THIS command — confirming a different command with it is refused. For submit_order, `wire_command.Submit.client_order_id` was minted here and is the id that will be sent. Any `reason` is recorded in the node's audit trail (sanitized) and never reaches the order.",
"preview_token": "pv-1",
"reason": "transcript capture: the two-call preview gate",
"tool": "submit_order",
"verified_by_node": true,
"will_execute": false,
"wire_command": {
"Submit": {
"client_order_id": "95106ad40",
"order_type": "limit",
"price": 0.42,
"qty": 12.0,
"reduce_only": false,
"side": 1,
"symbol": "CLI_MCP_SMOKE_TOKEN",
"trigger_price": null,
"venue": "polymarket"
}
}
}
}
──── 5. the WRITE tool, second call — CONFIRMED
The same arguments again, plus `confirm: true` and that exact token. Only now is anything
sent. The answer is the node's: `sent: true`, `outcome: accepted`, and the client order id is the
one the preview already showed.
client → {"jsonrpc":"2.0","id":5,"method":"tools/call","params":{"name":"submit_order","arguments":{"venue":"polymarket","symbol":"CLI_MCP_SMOKE_TOKEN","side":1,"qty":12,"order_type":"limit","price":0.42,"reason":"transcript capture: the two-call preview gate","confirm":true,"preview_token":"pv-1"}}}
server ← `jq '.result | {isError, structuredContent}'`
{
"isError": false,
"structuredContent": {
"client_order_id": "95106ad40",
"note": "the node ACCEPTED this command (empty client_order_id = an account-wide verb); call node_snapshot to observe the result. The node's server-side ControlLimits + RiskGate are the enforcing gate.",
"outcome": "accepted",
"sent": true
}
}
──── 6. the order is at the node
The same read tool as step 3, against the same node. The node folds a command and republishes
on its own thread, so the client POLLS this tool until the order appears rather than sleeping — the
request below is the poll that answered, and its `id` is therefore how many polls it took.
client → {"jsonrpc":"2.0","id":6,"method":"tools/call","params":{"name":"node_snapshot","arguments":{}}}
server ← `jq '.result.structuredContent.orders'`
[
{
"avg_fill_px": 0.0,
"client_order_id": "95106ad40",
"filled_qty": 0.0,
"order_type": "limit",
"price": 0.42,
"qty": 12.0,
"side": 1,
"status": "Accepted",
"symbol": "CLI_MCP_SMOKE_TOKEN",
"trigger_price": null,
"venue": "polymarket",
"venue_order_id": "95106ad40"
}
]
──── 7. the token has already fired
The confirming call again, byte for byte. A preview token is single-use: it is removed before
the command executes, so a duplicated or replayed confirmation cannot place a second order.
client → {"jsonrpc":"2.0","id":7,"method":"tools/call","params":{"name":"submit_order","arguments":{"venue":"polymarket","symbol":"CLI_MCP_SMOKE_TOKEN","side":1,"qty":12,"order_type":"limit","price":0.42,"reason":"transcript capture: the two-call preview gate","confirm":true,"preview_token":"pv-1"}}}
server ← isError: true, and `.result.content[0].text` reads:
preview_token "pv-1" is unknown or already used — a token fires at most once. Call this tool WITHOUT `confirm` to get a fresh preview, then confirm with the `preview_token` it returns.
The same engine, from the command line
# vike-cli — a real terminal session, captured verbatim.
#
# Captured by scripts/cli_mcp_smoke.sh (the CLI+MCP end-to-end smoke) on Linux, from binaries built
# out of this repository. It runs inside a throwaway project directory holding no venue credentials
# at all, so every venue is PAPER and no venue is contacted: absent credentials ARE the live gate.
#
# Nothing below is retyped, reordered or paraphrased. The only rewriting: the throwaway directory's
# absolute path is shown as <project>, and ANSI colour codes are stripped (the site renders this as
# a code block). Lines beginning with `#` are the capture harness annotating — a shell prints no
# such line — and carry each command's exit code and wall-clock time. Everything else is the
# binary's own output, stdout and stderr merged the way a terminal merges them.
$ vike-cli --version
vike-cli 0.1.0 (git f7e6f2f9 dirty, built 2026-09-06T08:57:10Z, rustc 1.97.1 (8bab26f4f 2026-07-14), x86_64-unknown-linux-gnu)
# exit 0 — 0.010s
$ vike-cli --help
vike-cli — the unified vike command-line surface
usage: vike-cli <command> [args…]
commands:
backtest run a backtest on a remote vike-datahub server and print the report
sweep run a parameter-grid search on a remote vike-datahub server and print the ranked grid
walkforward run an anchored walk-forward validation on a remote vike-datahub server
data get market data into the hist store: fetch real bars, or seed the demo tape
config settings provenance (`show`) and a validating pre-flight (`check`) for this box
mcp serve the create+backtest tools over stdio MCP (for Claude / an agent)
trade interactive REPL to observe + control a running vike-tradehub node (for a human)
strategy-status ask a running vike-tradehub node what it is running (read-only; --json for machines)
secrets inspect the credential store <project>/settings/secrets.env, and set ONE key in it (list | path | template | set)
init create <project>/user_data — strategies, profiles, results — with examples
indicators print the indicators a Rhai strategy can call, with their parameters
run `vike-cli <command> --help` for a command's own options
run `vike-cli --version` to print this build's version
# exit 0 — 0.009s
$ vike-cli data seed-demo --store <project>/data/demo
2026-09-06T08:57:30.000626Z INFO vike_backtest::binutil: hist store root resolved: stated for this run (--store / store = / store_root) store=<project>/data/demo rung="explicit"
seeded SYNTHETIC demo bars (venue `demo` — a closed-form curve, NOT market data) into <project>/data/demo
demo/BTCUSDT 1h 4344 bars
demo/BTCUSDT 1m 10080 bars
# exit 0 — 2.664s
$ vike-cli backtest --local --profile user_data/profiles/backtest.toml --script user_data/strategies/rhai/sma_cross/sma_cross.rhai --store <project>/data/demo
2026-09-06T08:57:32.680918Z INFO vike_backtest::binutil: hist store root resolved: stated for this run (--store / store = / store_root) store=<project>/data/demo rung="explicit"
name: sma_cross — demo BTCUSDT 1h
final_equity: 40757.94
total_return: 307.5794%
n_trades: 155
win_rate: 32.9032%
sharpe: 3.3616
max_drawdown: 36.5717%
per_symbol_pnl:
BTCUSDT: 50240.95
backtest: run saved to <project>/user_data/runs/1788685052-2741260-0
# exit 0 — 2.695s
What this session is and is not:
- The bars are the shipped SYNTHETIC demo tape, not market data — the engine says so inside the transcript, and that line was deliberately left in. Read the equity and Sharpe figures as a demonstration that the pipeline runs end to end, never as a result.
- The order is PAPER. `outcome: accepted` is the node accepting it, not a venue filling it — visible as `identity.live: false` earlier in the same session.
- `CLI_MCP_SMOKE_TOKEN` is the throwaway mount's own token id. It is real rather than prettified, which is the point.
Three ways in — and the local one leads
Local is not the fallback here; it is the design. The server runs beside your platform, on your hardware, and talks only to services you are already running.
Local MCP server start here
vike-cli mcp
A binary on your own machine, speaking JSON-RPC over stdin and stdout. Your client launches it as a subprocess, the way it would any other local server.
Nothing leaves the box. No vike server sits between the agent and the venue, no account is involved, and no hosted relay holds your keys — the server talks only to services you run, and the credentials it uses are your own daemon's.
The command line
vike-cli
The same surface a human drives, and the same one a shell script or a scheduled job drives.
Every write an agent can reach is a verb the interactive REPL already had, and both resolve their commands through one construction site — so the two surfaces cannot drift apart about what a command means.
The desktop app
vike-app
Charts, order entry and the research workspace, with the agent alongside rather than instead.
For the work that is faster to see than to describe — you keep the chart in front of you and the confirmation in your hands.
The command line, verb by verb
Everything an agent can do, a human or a script can do the same way.
vike-cli backtest— run a backtest on a remote data service and print the reportvike-cli sweep— run a parameter-grid search and print the ranked gridvike-cli walkforward— run an anchored walk-forward validationvike-cli data— get market data into the store: fetch real bars, or seed the demo tapevike-cli config— settings provenance, and a validating pre-flight for this boxvike-cli mcp— serve the tools over stdio MCP, for an agentvike-cli trade— interactive REPL to observe and control a running node, for a humanvike-cli strategy-status— ask a running node what it is running — read-onlyvike-cli secrets— inspect the credential store, and set one key in itvike-cli init— create the project's strategies, profiles and results directoryvike-cli indicators— print the indicators a strategy can call, with their parameters
Why 17 tools, and not eighty
An agent choosing among dozens of thin, near-identical endpoints chooses wrong, and it does so confidently. Fewer tools that each answer a whole question is not a smaller surface — it is the same surface, arranged so the choice is obvious.
Discovery replaces enumeration
discover_params reads a strategy's knobs off the script
itself rather than making each one its own endpoint. The agent asks what this script
takes; it does not guess from a catalogue.
3 prompts carry the procedure
The multi-step work that would otherwise be split across a dozen tools ships as guided
sheets instead:
backtest_a_strategy, arm_a_venue, triage_a_stuck_order
— author, compile, backtest, read the report, adjust; what has to be true before a venue can leave paper; find the order in the live snapshot, then decide.
2 resources carry the state
Things the agent wants to re-read rather than re-run are resources, not extra tools —
vike://node/snapshot (the live node read, reachable as a resource), and vike://backtest/last (the last backtest this session ran, for the rest of the session).
4 of them need no network at all
Authoring, compiling, reading the indicator roster and discovering a script's parameters all run with no socket open — which is what makes the most restrictive ring a useful place to work rather than a punishment.
Connect your client
Each one is a single entry in that client's own config. The setup pages carry the exact block, and the trap specific to that client.
Claude Code
One claude mcp add, with the double dash that hands the binary its own flags.
Claude Desktop
A local command in the desktop config, and where the node keys have to live for a GUI-launched process to see them.
Cursor
The command and args entry in mcp.json, global or per project.
Codex CLI
The one client whose config is TOML rather than JSON.
Gemini CLI
gemini mcp add, on the default stdio transport.
Amazon Q Developer CLI
An entry in the Amazon Q config, global or per workspace.
Full reference: the agent surface documentation · the server and its flags · every command-line verb · every supported client.
Your agent. Your machine. Your confirmation.
Free. Source-available under FSL-1.1; every release becomes Apache-2.0 two years on.
Trading involves substantial risk of loss and is not suitable for every investor. Nothing on this page is investment, financial or trading advice. An automated or agent-assisted workflow does not reduce market risk, and the controls described above govern what software will send — not whether a trade is a good one. Orders sent from your accounts remain your responsibility.