Drive Vike Trader from Claude
Two captured MCP sessions, annotated — the handshake, the templates, a compile error, a tool call that failed because the agent guessed, and the backtest report that came back.
Every response on this page is copied verbatim from two captured sessions against
vike-cli mcp, driven over stdio with newline-delimited JSON-RPC 2.0. A local vike-datahub at
127.0.0.1:7878 served the backtest. Nothing is illustrative: where a number looks unflattering,
it is the number the run produced. The one exception is the handshake below, which is re-rendered
whenever the server's initialize result gains a field — that result is fixed text assembled from
constants rather than anything a run produces, so a stale capture of it is simply a wrong payload.
It is pretty-printed here and arrives on one line; the key order is not a presentation choice —
the server builds its objects with serde_json's default map, so the block below is in the order
the wire has it (sorted) rather than the order the code writes the fields.
Client setup has the launch entry for your client. This page is what happens after it launches.
The handshake
{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"docs-capture","version":"1"}}}{
"capabilities": {
"prompts": {},
"resources": {},
"tools": {}
},
"instructions": "This is `vike-cli`'s agent surface: ... \n\n ... Never invent a tool name, and never report an operator-side command as something you ran.",
"protocolVersion": "2024-11-05",
"serverInfo": {
"name": "vike-cli",
"version": "0.1.0"
}
}Four things the server just told the client:
serverInfo.nameisvike-cli. The MCP server is not a separate binary — it is a subcommand of the same CLI you use at a prompt, and the version is that crate's own.capabilitiesadvertisestools,resourcesandprompts. No sampling to negotiate: the server asks the client for nothing.- The
protocolVersioncame back echoed, because the client declared one. instructionsis a paragraph the client puts in front of the model — elided here, and the only part of the handshake aimed at the model rather than at the client. It says this server is one surface of a larger system and names the commands an operator runs for the rest: record a tape withvike-recorder, get bars in withvike-cli data fetch, backtest with no server at all withvike-cli backtest --local, and see which venue keys this box holds withvike-cli secrets list. None of those is a tool, and the text says so — the point is that an agent asked for one of them can now name the command instead of refusing blind.
⚠ The instructions grant nothing. Nothing in that paragraph is callable, and the roster below
is unchanged by it; it is also scoped by the same ToolAccess that decides the roster, so a
session launched --profile read-only is told less than this one — no preview gate, because it has
no write tool to gate. vike-cli mcp has the scoping rule and what
each profile's text drops.
A notifications/initialized follows, carries no id, and gets no response.
What the server offers
tools/list returned the whole registry in one message — 9,859 characters, because every tool
ships its full inputSchema alongside its description. The names, in the order they arrived:
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_stateTen read-only, seven that write to a node. Authoring, backtesting and trading are the reference pages for the three groups.
Those 9,859 characters are a real cost: they land in the model's context whether a tool is called
or not. It is why the descriptions are terse, and why list_indicators — whose bound set is most
of the indicator catalog — names none of that set in its own description and makes you ask.
Terseness has a price of its own, though, and the number above is a floor rather than a budget: two
descriptions have been deliberately lengthened since this capture, each paid for by a measured
mistake. discover_params read as a tool for inspecting somebody else's script, so an agent that
had just written one answered "which parameters can I tune" from authorship instead of calling it;
set_trading_state said only "the kill switch", and an agent reached for it to put a venue live.
Both now say what the tool is not — the change is in tools_spec
(crates/vike-cli/src/cmd/mcp.rs), and it is a straight trade of context bytes for a failure that
had already happened.
Starting from a template
{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"list_templates","arguments":{}}}Three starters came back, each a row of two fields — a name and its source:
- SMA cross
- RSI reversion
- Donchian breakout
The first, verbatim:
let fast = param("fast", 5.0);
let slow = param("slow", 20.0);
fn on_bar() {
let f = sma(fast.to_int()); let s = sma(slow.to_int());
if s.is_nan() { return; }
let target = if f > s { 1.0 } else { -1.0 };
let delta = target - position();
if abs(delta) > 1e-12 { market(if delta > 0.0 { 1 } else { -1 }, abs(delta)); }
}The third template ships a comment worth more than the strategy it belongs to. It uses an SMA as a symmetric proxy, and says why:
donchianis NOT bound: it is multi-output (upper/mid/lower) and the bridge binds single-output indicators only, sodonchian(len)is a function-not-found at the first call rather than a silent upper-band read.
That is the failure mode the offline tools exist to catch. Rhai resolves a function name when the line runs, so a script naming an unbound indicator compiles perfectly and then fails on every single bar. See list_templates.
The script this session wrote
// quiet_breakout — buy a breakout, but only out of a quiet bar.
//
// Two user indicators do the work: breakout_level for the level the bar has to clear, and
// range_pct to insist the bar that clears it is not already a wide, over-extended one.
let lookback = param("lookback", 48.0);
let calm = param("calm", 3.0);
let qty = param("qty", 0.05);
fn on_bar() {
// Read both every bar, before any branch — an indicator only advances when it is called.
let level = breakout_level(lookback.to_int());
let rng = range_pct();
if level.is_nan() || rng.is_nan() { return; }
if close() > level && rng < calm && position() <= 0.0 { market(1, qty); }
if close() < level * 0.97 && position() > 0.0 { market(-1, position()); }
}Passed to validate_strategy — the wire form carries it as a JSON string with escaped newlines,
one message per line — the answer was two words long:
{
"ok": true
}ok: true is a narrower claim than it looks. The check runs through
vike_script::discover_params, the broker-free compile path: it compiles the script and runs its
top level exactly once, the same error surface as mounting the strategy. So the three param()
lines ran. The body of fn on_bar() did not.
breakout_level and range_pct are not built-ins. They are the author's own files, loaded from
user_data/indicators/ and installed process-wide before any strategy compiles
(install_user_indicators in crates/vike-script/src/engine.rs). No compile check can vouch for
them; the backtest further down is what actually proved those two calls resolve.
One thing to carry into that backtest: the install that binds a name is the install on the box that
compiles the script. validate_strategy compiles in the CLI, so the CLI's set answers. A
run_backtest compiles on the datahub, so the server's set answers — install_user_indicators in
crates/vike-datahub/src/datahub_cli.rs reads that box's own user_data/indicators/, and its doc
says a client script binds to whatever the server holds there. Here the two agreed because the
datahub was the same machine.
Writing an indicator and
loading indicators cover that half.
The knobs
discover_params on the same script:
{
"params": [
{
"default": 48.0,
"name": "lookback"
},
{
"default": 3.0,
"name": "calm"
},
{
"default": 0.05,
"name": "qty"
}
]
}Those three names are exactly what a profile's [strategy.params] or a [sweep] table may
override. Same compile path as the tool above, opposite disposition on failure: a broken script is
an ok: false answer from validate_strategy and a tool error from discover_params,
because there are no parameters left to report.
What a compile error looks like
A second session opened by handing over a deliberately broken script:
{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"validate_strategy","arguments":{"script":"fn on_bar( { market(1 0.5) }"}}}{
"error": "Expecting ')' to close the parameters list of function 'on_bar' (line 1, position 12)",
"ok": false
}That is the Rhai compiler's own message, passed through with the position it reported. It arrives
as an ordinary successful result — not a JSON-RPC error, not an isError result — so an agent
reads it, fixes the script and calls again. That loop is what
validate_strategy is for.
Asking about one indicator
{"jsonrpc":"2.0","id":7,"method":"tools/call","params":{"name":"list_indicators","arguments":{"name":"rsi"}}}{
"count": 1,
"indicators": [
{
"accessors": [],
"bare_call": true,
"category": "Momentum",
"name": "rsi",
"outputs": [
"rsi"
],
"params": [
{
"default": 14.0,
"max": 1000.0,
"min": 1.0,
"name": "length"
}
],
"pretty": "RSI"
}
]
}Called with no arguments the tool returns the compact roster — name and category only — and detail
is pulled one name or one category at a time. The tiering is deliberate: the bound set was
three names when the tool was written and is nearly the whole catalog now.
The reason the tool exists at all is that the host-bound set is not the registry.
crates/vike-cli/src/cmd/indicators.rs's find_bound separates the two ways a name can miss — a
typo, which gets "no indicator named x", and a real registry entry the host holds back, which
gets the host's own reason for holding it back. Different mistakes, different repairs.
The call that failed
Now the honest part of the session. The agent had a script, had its knobs, and asked for a backtest by assembling arguments that looked like a backtest. The wire form was one line; the script argument is elided here for length:
{"name":"run_backtest","arguments":{
"script": "// quiet_breakout …",
"venue": "binance",
"symbol": "BTCUSDT",
"interval": "1h",
"from": "2025-01-01T00",
"to": "2026-01-01T00",
"cash": 10000,
"fee_rate": 0.001,
"params": {"lookback":48,"calm":3,"qty":0.05}
}}Every one of those keys is plausible. Only script is real: the other eight are ignored, and the
call failed on the one argument the agent never sent. The answer:
run_backtest requires a `profile` string argumentThe real inputSchema, which had been sitting in the tools/list response the whole time:
{
"properties": {
"profile": {
"type": "string"
},
"script": {
"type": "string"
}
},
"required": [
"profile"
],
"type": "object"
}Two strings. Venue, symbol, interval, range, cash, fees and params all live inside the profile
TOML, because the server's own BacktestProfile::from_toml_str is what reads them. With no script
argument the profile crosses the wire byte for byte. With one, the client parses the TOML once to
inject the script and re-serializes it — but it still reads none of those fields, and the server
remains the only thing that interprets the profile.
Leave this failure in your transcript
Guessing an argument list is what any agent does when it pattern-matches a tool name instead of reading its schema. What makes it cheap here is the answer: one line, naming the missing argument, so the retry is obvious and costs one round trip. A tool error worth shipping is one an agent can act on without a human.
The corrected call
Same tool, same script, arguments rebuilt around the profile:
name = "quiet_breakout — BTCUSDT 1h"
[data]
kind = "bar"
interval = "1h"
venue = "binance"
symbols = ["BTCUSDT"]
from = "2025-01-01T00"
to = "2026-01-01T00"
[engine]
cash = 10000.0
fee_rate = 0.001
slippage = 0.0
[strategy]
name = "rhai"
[strategy.params]
lookback = 48.0
calm = 3.0
qty = 0.05The script rides in the optional second argument and is injected as [strategy.params].src. Note
strategy.name = "rhai" — that is the one arm which reads an injected src, and getting it wrong
runs a built-in strategy while carrying the script along as an unread parameter.
Run profiles documents the file.
The report, verbatim:
{
"report": {
"final_equity": 8334.568427500006,
"funding_paid": 0.0,
"max_drawdown": 0.1839635835571801,
"n_trades": 48,
"name": "quiet_breakout — BTCUSDT 1h",
"per_symbol_pnl": [
[
"BTCUSDT",
-1180.6035000000015
]
],
"profit_factor": 0.687335413608655,
"sharpe": -1.0475904521415624,
"total_return": -0.16654315724999946,
"win_rate": 0.3125
}
}Forty-eight trades, a win rate of 0.3125, a profit factor below one, a Sharpe of −1.05 and a total return of −0.167. A losing run, reported plainly. The profile requested a year of hourly bars; the report carries no bar count or coverage field, so how much of that year the store actually held is not something this capture states. Read a report walks the numbers; the report defines each field.
Why the agent's report is checkable
The report above is not an MCP-flavoured summary of a run. It is the run.
- One function does the script injection —
inject_script_srcincrates/vike-cli/src/cmd/backtest.rs— and thebacktestsubcommand and this tool both reach it. So dorun_sweepandrun_walk_forward, throughcrates/vike-cli/src/cmd/mcp.rs's sharedprofile_from_args. - Both then ship the resulting TOML text verbatim to
DatahubClient::run_backtest. - The server parses it once with
BacktestProfile::from_toml_strand returns the report JSON, which the tool wraps underreportand hands back untouched.
So vike-cli backtest with the same profile and script, against the same vike-datahub, returns
the same numbers. The agent surface performs no arithmetic of its own, and that is the property
which makes it trustworthy: anything an agent quotes back to you can be reproduced at a prompt, by
you, without the agent.
The half this session does not show
Seven of the seventeen tools write to a node. Six of them place, change or cancel orders —
submit_order, cancel_order, modify, flatten, market_exit and mass_cancel — and the
seventh, set_trading_state, places no order at all: it moves the account's trading state, the kill
switch. This session called none of them. No vike-tradehub node was running.
That absence does not, on its own, stop a call. Called the ordinary way — without confirm: true —
a write tool returns its preview, and with no node configured it opens no socket; the connection is
attempted only once a call has cleared the write gate, which is where the error naming the missing
--node arrives.
There is therefore no captured paper submit_order into flatten pair to show here, and
fabricating one would defeat the purpose of the page. What holds for those tools regardless:
- A write tool executes nothing until a call carries both
confirm: trueand thepreview_tokenthat tool's own preview returned. Anything less 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, a note, and a fresh token — which the agent and the human watching it can both read before anything is sent. The token fires once, expires after 60 seconds and is bound to the command it previewed, so the two calls cannot be about two different orders. ⚠ Failing the gate is not an error: aconfirm: truewith no token comes back as another preview, sowill_executeis the field to read, not the absence ofisError. - To execute, they need a running node named by
--node, plusVIKE_TRADEHUB_CONTROL_KEY. The read-onlynode_snapshotneeds that same--node, withVIKE_TRADEHUB_OBSERVE_KEYin place of the control key — and unlike a preview, it has nothing to answer with until the node is there. - Nothing an MCP tool does can arm a venue. A venue with no credentials mounts as paper, and the
per-venue ceiling
policy.venues.<venue>defaults topaperand can only ever refuse.
Read submit_order and flatten for the argument shapes, and trading for the preview gate and how outcomes are reported.
Next
- Connect an agent — the same loop as a narrative, from zero.
- Authoring — the four offline tools in full.
- Backtesting — sweeps and walk-forwards, which take the same profile.
- Client setup — the launch entry, per client.
Raw JSON-RPC
There is no URL to curl — the trader server is a pipe. The stdio wire protocol, the result envelope, every error shape, and the two that mean "it may have executed".
MCP tools
The seventeen tools vike-cli mcp advertises, what each one reaches, the profile that can withhold whole rings of them, and the gates — confirm plus a single-use preview_token — that stand between an agent and a real order.