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.

Free. Source-available under FSL-1.1; every release becomes Apache-2.0 two years on. The agent surface is part of the same download — there is no separate tier, key or subscription for it.

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 returnsWhat it is
will_executefalse 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_commandthe exact command that would be sent, resolved — not the arguments the agent typed.
guardraila client-side estimate against your configured ceiling. Advisory, and it says so.
node_verdictthe node's own dry-run of this command against the real control limits and risk gate.
verified_by_nodewhether that verdict was actually asked for AND answered. An absent verdict is not an approval, and this field is what says so.
preview_tokenwhat 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.

RingServesWhy 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.

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.

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.