vike

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

PropertyValue
Commandvike-cli mcp [--addr <host:port>] [--node <host:port>] [--profile <ring>] [--deny-tool <name>]… [--trace]
Transportstdio, newline-delimited JSON-RPC 2.0 — hand-rolled over serde_json, no MCP SDK
Server identityserverInfo.name = vike-cli, version = the version of the binary you installed
MCP versionthe client's protocolVersion, echoed back; 2024-11-05 when the client declares none
Capabilitiestools, resources, prompts
Methodsinitialize, notifications/initialized, ping, tools/list, tools/call, resources/list, resources/read, prompts/list, prompts/get
Guidancean 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

FlagDefaultWhat it reaches
--addr127.0.0.1:7878the vike-datahub server the backtest and inventory tools run against
--nodenonethe 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.

ProfileServesWithheld
full (default)all 17nothing — byte-identical to the surface that shipped before the flag existed
read-only10the seven order-write tools
offline4everything 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:

KeyGrantsUsed byAbsent means
VIKE_TRADEHUB_OBSERVE_KEYreadnode_snapshot1 tool disabled
VIKE_TRADEHUB_CONTROL_KEYwritethe seven order-write tools7 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

ToolPlaneReaches
validate_strategyreadnothing — offline
discover_paramsreadnothing — offline
list_templatesreadnothing — offline
list_indicatorsreadnothing — offline
run_backtestreaddatahub (--addr)
run_sweepreaddatahub (--addr)
run_walk_forwardreaddatahub (--addr)
list_strategiesreaddatahub (--addr)
list_seriesreaddatahub (--addr)
node_snapshotreadnode (--node + observe key)
submit_orderwritenode (--node + control key)
cancel_orderwritenode (--node + control key)
modifywritenode (--node + control key)
flattenwritenode (--node + control key)
market_exitwritenode (--node + control key)
mass_cancelwritenode (--node + control key)
set_trading_statewritenode (--node + control key)

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: true is 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::vet in crates/vike-tradehub/src/server.rs, a per-order notional ceiling plus a command rate limiter — and beneath it the execution core's RiskGate (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 (execute in crates/vike-cli/src/cmd/mcp.rs), so accepted / a named refusal / unknown always 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.

On this page