Trader MCP server
17 tools over a local stdio server — author and backtest Rhai strategies, read a live trading node, and place orders behind a mandatory preview gate, or fewer tools under a profile that withholds the writes outright.
vike-cli mcp is a local Model Context Protocol server that
exposes the Vike trading platform as 17 tools. It is a binary on your own machine speaking
newline-delimited JSON-RPC 2.0 over stdio — your MCP client launches it as a subprocess, the same
way it would any other local server.
Ten tools are read-only: compile a Rhai strategy, discover its knobs and the indicators it may
call, run backtests, sweeps and walk-forwards against a vike-datahub server, and read a running
vike-tradehub node's live state. Seven write to that node: six place, change or cancel orders
and the seventh sets its trading state. Each one previews first, and the preview is not a
convention it can skip: a write tool executes only against a preview_token this server issued —
unexpired, and bound to the same command — so a first-and-only call returns the command it would
send, plus the token that would confirm it.
Who this is for
Anyone running the Vike trading platform who would rather describe a strategy than write one, and who wants the loop — author, compile, backtest, sweep, read the OOS windows, adjust — to happen in a conversation instead of across six terminal invocations. Connect an agent is the narrative walkthrough; this page and its three siblings are the reference.
No Vike account is involved. Nothing here contacts vike.io; the server talks only to
services you run, and the credentials it uses are the HMAC keys of your own trading daemon. If you
do not have the platform yet, Install with Docker is the
one-container route and Quickstart: first backtest
gets a backtest running with zero credentials.
Connect
| Property | Value |
|---|---|
| Command | vike-cli mcp [--addr <host:port>] [--node <host:port>] [--profile <ring>] [--deny-tool <name>]… [--trace] |
| Transport | stdio, newline-delimited JSON-RPC 2.0 — hand-rolled over serde_json, no MCP SDK |
| Server identity | serverInfo.name = vike-cli, version = the version of the binary you installed |
| MCP version | the client's protocolVersion, echoed back; 2024-11-05 when the client declares none |
| Capabilities | tools, resources, prompts |
| Methods | initialize, notifications/initialized, ping, tools/list, tools/call, resources/list, resources/read, prompts/list, prompts/get |
| Guidance | an instructions string in the initialize result — see below |
Any other method carrying an id is a -32601; a notification naming one gets no response line at
all, because there is no id to answer. An unparsable line is a -32700 that answers rather than
killing the session. tools/list returns the registry in one response — every tool this server's
scope admits, with no pagination and no cursor.
Every successful tool result carries the same shape: the payload as pretty-printed JSON in one
text content block, plus the same value as structuredContent. A tool failure is an ordinary
result with isError: true and the message as text — not a JSON-RPC error — so the agent reads it
and retries. Even a tool that crashes internally arrives that way: Server::handle
(crates/vike-cli/src/cmd/mcp.rs) wraps every tool call in catch_unwind, so a panic comes back
as a tool error rather than taking the session down with it.
Client setup has the exact entry per client — Claude Desktop, Claude Code, Cursor, Gemini CLI, VS Code, Codex CLI, Amazon Q Developer CLI and Windsurf — plus the raw stdio protocol for scripting or for diagnosing a client that will not launch the binary.
What the server says about itself
The initialize result carries an instructions string, and a client shows it to the model as
guidance about this server. It is the one channel that reaches the model unasked — in front of
the session, before any call — and it carries a fact a tool description cannot: a description
describes its tool, so "there is no tool for this, and the command is vike-recorder" — a fact
about the absence of a tool — has nowhere in a roster to live. (The prompts below say a related
thing, but only to a client that asks for one.)
Its content is one line saying this server is part of a larger system, one line each for the things
that live outside it — recording a tape, getting bars into the history store, backtesting with no
data service, and which venue keys the box holds — each naming the command an operator types,
and two rules: invent no tool name, and never report an operator-side command as something you ran.
⚠ It is guidance, not capability. Nothing in that text is callable and no gate on this page
moves because of it; an agent gains only the ability to say what an operator should run when the
answer is that no tool here does it. It is also profile-scoped by the same ToolAccess as the
roster — read-only sends no preview-gate sentence, offline also drops the one naming
list_series — because prose that names a withheld tool advertises a withheld tool.
vike-cli mcp is the reference, and
Raw JSON-RPC shows where the field sits in the response.
Two flags decide what the tools can reach
| Flag | Default | What it reaches |
|---|---|---|
--addr | 127.0.0.1:7878 | the vike-datahub server the backtest and inventory tools run against |
--node | none | the running vike-tradehub node the snapshot and order-write tools observe and control |
--node has no default on purpose. Without it the nine authoring and backtesting tools work
exactly as they do with it, and node_snapshot returns a clean error naming the flag: the check
runs before any socket is opened, so you get an immediate answer rather than a connection timeout.
A write tool is quieter about it — a call that fails the two-call gate returns a preview whatever
the flag says, and even a confirming one only reaches the connection error after its
preview_token has been accepted. So no preview is evidence the flag arrived; a node snapshot
is.
Two more decide which tools exist at all
The preview gate makes a write take two calls. --profile answers the prior question — whether this
server offers the write tools in the first place — and only the operator can answer it, in the MCP
client's launch command.
| Profile | Serves | Withheld |
|---|---|---|
full (default) | all 17 | nothing — byte-identical to the surface that shipped before the flag existed |
read-only | 10 | the seven order-write tools |
offline | 4 | everything that opens a socket: the write seven, the five datahub tools, node_snapshot, and both resources |
--deny-tool NAME subtracts one more tool from whatever the profile allows, and is repeatable. An
unknown profile name or an unknown deny name is a usage error naming the valid set, never a fallback
to something wider.
A withheld tool is absent from tools/list as well as refused by tools/call, and the two
cannot disagree because they are one derivation: ToolAccess::new
(crates/vike-cli/src/cmd/mcp.rs) makes a single pass over the registry and produces two
complements — the advertised set and its withheld complement. Each ring is a predicate over data
that already existed rather than a second roster: read-only is the negation of is_write_tool,
the same WRITE_TOOLS array the preview gate routes on, and offline is each tool's own
openWorldHint annotation read back, with a tool that declares nothing withheld rather than
admitted.
The two resources follow the same predicate, because a resource is a second way in to a tool's
implementation rather than a second source — vike://node/snapshot routes into node_snapshot, so
an unscoped resource list would hand back a withheld tool's answer through a URI. Neither is a
write, so read-only withholds neither; offline withholds both. The prompts are re-worded
instead of withheld, because a prompt executes nothing and a sheet that vanishes teaches an agent
less than one saying the tools are not served here: a session admitting no write opens every prompt
with a banner saying so, and renders no two-call instructions for tools it has just said are absent.
The initialize instructions above answer to the same predicate, but by subtraction rather than
re-wording: a scoped session is simply sent fewer clauses, losing the sentence that names whatever
it cannot reach. Either way, every channel that reaches the model is a place a withheld tool could
otherwise be advertised.
A refusal names the profile, lists what is served, and says plainly that only the operator can widen it — otherwise an agent spends its next three turns hunting for the flag that turns the tool on.
The agent transcript
--trace (or --trace-dir <dir>) appends one JSON Lines record per tools/call:
the tool, whether it is a write, the profile, the arguments, the verdict, and for a gated write the
preview token's identity. It is off unless you ask for it — no flag, no file, no directory
created. crates/vike-cli/src/cmd/mcp_trace.rs is the writer.
The verdict is ok, preview, refused or error, and refused is the half no other record
holds: a refusal never reaches the node's audit trail, because it was refused before a byte left
this process. preview is kept apart from ok for the same kind of reason — "the agent called
submit_order" and "an order left this process" are different facts.
Nothing secret is written. Argument values are redacted through the workspace's one credential-name
authority, applied both to object keys and to credential-shaped SHOUTING_CASE identifiers inside
string values, so a reason quoting an API-key name has the whole value replaced. The preview_token argument is
redacted by that first rule like any other token-shaped key; what the record carries instead is its
own token field holding the token's identity — pv-3, the per-session counter — plus whether
this call minted it or presented it. That is a sequence rather than an authorization, which is
exactly why there is nothing there to withhold. Records land beside the change journal, under
<project>/settings/state/agent/, one file per month, pruned to the newest 24.
Running it on a schedule
Nothing about this server requires a human in the conversation. vike-agent-run — a second binary,
crates/vike-agent-eval/src/bin/vike-agent-run.rs — stands the same server up against your own
node, hands one prompt to one model, bounds the session, and writes a record of what happened. It
is read-only unless you pass --allow-writes, and the ring it asked for is cross-checked
against the roster the server advertised back before the model is asked anything. Scheduling itself
is delegated outright to the operating system — systemd unit and timer templates ship, and the
timer deliberately does not replay a firing missed while the box was down, because a review of a
moment that has passed is a record about nothing. No account, no hosted agent, nothing on a Vike
machine: your box, your node, your schedule.
Unattended runs is the page.
Node keys, and what is disabled without them
The node tools authenticate with two HMAC keys, resolved from the process environment first
and then from the same credential store the daemon itself reads
(<project>/settings/secrets.env) — resolve in crates/vike-cli/src/cmd/nodekeys.rs is the
whole rule:
| Key | Grants | Used by | Absent means |
|---|---|---|---|
VIKE_TRADEHUB_OBSERVE_KEY | read | node_snapshot | 1 tool disabled |
VIKE_TRADEHUB_CONTROL_KEY | write | the seven order-write tools | 7 tools disabled |
Each is resolved independently, so the two halves fail separately: an observe key with no control key gives an agent a read-only view of the live node and no way to touch it, which is a reasonable configuration to run on purpose. Neither key affects the other nine tools — authoring and backtesting never open a node connection.
An export wins over the store because it is the narrower, more deliberate source — that is what
makes "point at a different node for one command" possible. Values are trimmed on both sides — the
daemon trims too — so a key with a trailing newline would otherwise fail the handshake as an opaque
AuthDenied. The daemon's own reader is from_vars in crates/vike-tradehub-client/src/auth.rs,
and values_are_trimmed_to_match_the_servers_own_reader in nodekeys.rs holds the two readers to
the same bytes from the same map.
A key that is in neither place produces an error naming both places — run vike-cli secrets path to see which store this project resolves to and vike-cli secrets list to see whether the
key is in it (names only, never values). The two scopes are separate at the node as well as here:
an observe-scoped connection that sends a command is refused outright by the daemon, so an
observe key can never be escalated into a write. observe_scope_command_is_denied in
crates/vike-tradehub/tests/control_roundtrip.rs is that refusal under test: it authenticates as
observe against a node whose control key is enabled, sends a cancel, and requires the denial.
A GUI-launched client inherits none of your shell's exports
Claude Desktop, Cursor and VS Code start the server as a child of the application, not of your
terminal, so an export VIKE_TRADEHUB_CONTROL_KEY=... in your shell profile is invisible to it.
Put the keys in the credential store, or set them in the client's own config where it supports an
env block. The store is also where the daemon reads them, so one copy serves both halves.
The 17 tools
| Tool | Plane | Reaches |
|---|---|---|
validate_strategy | read | nothing — offline |
discover_params | read | nothing — offline |
list_templates | read | nothing — offline |
list_indicators | read | nothing — offline |
run_backtest | read | datahub (--addr) |
run_sweep | read | datahub (--addr) |
run_walk_forward | read | datahub (--addr) |
list_strategies | read | datahub (--addr) |
list_series | read | datahub (--addr) |
node_snapshot | read | node (--node + observe key) |
submit_order | write | node (--node + control key) |
cancel_order | write | node (--node + control key) |
modify | write | node (--node + control key) |
flatten | write | node (--node + control key) |
market_exit | write | node (--node + control key) |
mass_cancel | write | node (--node + control key) |
set_trading_state | write | node (--node + control key) |
Authoring — 4 tools
Compile a Rhai strategy, list its param() knobs, read the starter templates and the host-bound indicator roster. All offline.
Backtesting — 5 tools
Backtest, parameter sweep and anchored walk-forward on the datahub, plus the strategy roster and the stored-series inventory.
Live node — 8 tools
One read tool for the node's live state, and the seven order-write verbs behind the mandatory preview gate.
Read or write
The split is exactly ten and seven, and it is machine-pinned rather than conventional: every
read-only tool carries readOnlyHint: true and every write tool carries destructiveHint: true
in tools/list, and tools_list_has_read_and_write_tools_with_correct_hints
(crates/vike-cli/src/cmd/mcp.rs) walks every advertised tool and fails if one falls on the wrong
side — it also requires each write tool's description to mention confirm. The seven write tools
are exactly the write verbs of the human trade REPL, and both surfaces build their wire commands
through one construction site (Verb::to_wire_command in crates/vike-cli/src/cmd/verbs.rs, which
the tools reach through wire_command_for — JSON arguments resolved into the same Verb the REPL
parses a typed line into), so the two cannot drift apart.
Three properties follow from that, and they are what makes handing these tools to an agent defensible:
- A write executes only against a
preview_token.confirm: trueis one half of the gate and the token this tool's own preview returned is the other; either one missing returns a preview — the resolved wire command, a client-side guardrail check, the node's own dry-run when there is a node to ask, the rationale that would be recorded, and a fresh token. The token fires once, expires after 60 seconds, and is bound to the command it previewed, so a preview of one order cannot confirm another. - The client never enforces. The preview's guardrail is advisory UX. What refuses an oversized
or too-frequent command is server-side on the node —
ControlLimits::vetincrates/vike-tradehub/src/server.rs, a per-order notional ceiling plus a command rate limiter — and beneath it the execution core'sRiskGate(crates/vike-exec/src/risk.rs). Nothing an agent sends from here can loosen either. Both names come back in the tools' own output — every preview and every accepted response names them as the enforcing gate. - Each write reports its own command's fate. The server waits on the ticket its own send
returned (
executeincrates/vike-cli/src/cmd/mcp.rs), soaccepted/ a named refusal /unknownalways describes the command just issued, never a neighbouring one.
Failing the gate is silent — read `will_execute`
Every way of failing this gate produces a preview, not an error: a missing preview_token, a
confirm that is the string "true" rather than the JSON boolean (or 1, or "yes"), or both.
Nothing is sent, which is the safe outcome, but nothing says so either — an agent that believes it
confirmed and reads back will_execute: false has not. Only a token that is unknown, already used,
expired or bound to a different command comes back flagged.
The mechanics — the preview payload field by field, what the node's two limits actually reach,
client-order-id handling, and what unknown obliges a caller to do — are on
Live node.