MCP server

Remote MCP for AXIS — Streamable HTTP /mcp, Bearer keys, Worker tools, live-app capabilities, McpBridgeDO.

This page

Abstract

AXIS exposes a Model Context Protocol server on the Cloudflare® Worker so agents can request and respond across the data plane and a connected PWA.

PlaneLive tab?Tools
WorkerNoaxis_health, axis_request, axis_run, axis_scripts_*, axis_keys_*, axis_usage, axis_onchain, axis_market
AppYesapp_invoke, app_get, app_set, app_capabilities, app_session_status

Transport: Streamable HTTP JSON-RPC 2.0 on POST /mcp (JSON body in / JSON body out). Protocol versions accepted: 2025-03-26, 2025-06-18, 2024-11-05.

Auth: same Bearer API key as /api/scripts (pn_…). GET /mcp is public discovery. Production fail-closes when D1 is bound without API_KEYS KV. Rate limit: 60 RPC methods / minute / key (JSON-RPC batches charge per item, max 10; bodies above 1 MiB are rejected before parse).

End-user connect steps: MCP (agents).

Conceptual model

Diagram

Rendering…

Endpoints

Canonical host: https://worker.axis.hoox.sh.

PathMethodAuthRole
/mcpPOSTBearerJSON-RPC: initialize, tools/*, resources/*, prompts/*, ping
/mcpGETpublicDiscovery JSON (server name, tool names)
/mcpDELETE—No-op 204 (stateless)
/.well-known/oauth-protected-resourceGETpublicBearer resource metadata
/api/mcp/bridgeWS?ticket= (one-time, minted below) or Authorization headerPWA control plane → McpBridgeDO. Browser sockets cannot set Authorization; they never put the long-lived pn_… key in the query string.
/api/mcp/bridgeGETBearerNo Upgrade header → DO /status: { status, connected } tab count (the app status bar polls this)
/api/mcp/bridge?issue=ticketGETBearerMint a single-use WS ticket ({ ticket, expiresIn }, 30s TTL)

Health JSON includes features.mcp: true and features.mcpBridge when the DO is bound.

Bindings

MCP_BRIDGE Durable Object (class_name = "McpBridgeDO", migration tag v1 new_sqlite_classes). Declared in worker/wrangler.toml.example. App-plane tools return NO_MCP_BRIDGE until this is deployed. Cloudflare® no longer creates KV-backed DO namespaces (new_classes).

Session name is the API-key partition (userId = first 32 hex chars of SHA-256 of the key). Optional suffix userId:label is allowed; foreign session ids are ignored.

Worker tools

ToolRole
axis_healthSame payload as GET /health
axis_requestAllowlisted Worker HTTP; returns { ok, status, body }
axis_runPOST /api/run — script required; optional data, ticker, timeframe, inputs
axis_scripts_listCloud library metadata
axis_scripts_get{ id } full script
axis_scripts_putCreate/upsert { name, content, id?, description?, path?, revision? }
axis_scripts_delete{ id }
axis_keys_validateCurrent Bearer or { key }
axis_keys_create{ adminToken, tier? } — same as X-Admin-Token
axis_usageUsage stub / KV counters
axis_onchain{ path, query? } suffix after /api/onchain
axis_market{ path, query? } suffix after /api/market
app_session_statusConnected PWA count for this key

axis_request arguments: method (GET default), path (required, starts with /), optional query, body, headers (Authorization is injected from the MCP session).

axis_request allowlist

Allowed: GET / /health; POST /api/run; GET|POST /api/keys; GET /api/usage; scripts CRUD (/api/scripts, /_draft, /:id, versions); GET /api/onchain…; GET /api/market….

Blocked: /mcp recursion, /api/stream, /api/git/oauth, path traversal, unknown methods.

Resource axis://allowlist returns the live table.

App tools

ToolRole
app_capabilitiesCapability catalog from the connected tab
app_invoke{ capability, payload? } — wait for JSON result (default 20s)
app_getSanitized snapshot; optional dotted path
app_setAllowlisted dotted path + value

No tab → SESSION_OFFLINE. Timeouts → TIMEOUT. Snapshots redact keys matching apiKey|secret|passphrase|token|password|authorization|cookie and summarize OHLCV unless includeBars: true.

Capability ids (app_invoke)

IdPayloadMutates
app.snapshot{ includeBars? }no
app.get{ path?, includeBars? }no
app.set{ path, value }yes
app.command{ id } palette commandyes
app.shortcut{ id } shortcut idyes
app.event{ name, detail? } — name must start with axis-yes
chart.get—no
chart.set{ symbol?, interval?, exchange?, source?, chartType? }yes
chart.load{ symbol?, interval?, source? }yes
chart.reload—yes
chart.live{ active? }yes
chart.layout{ op: get|grid|save|load|delete, mode?, name?, id? }yes
chart.theme{ presetId? }yes
chart.type{ type }yes
chart.screenshot{ scope?: price|panes|workspace, scale?: 1|2 }no
chart.zoom{ op: in|out|reset|pan, delta?, bars? }yes
chart.bars{ limit?, head?, gaps? } — bounded tail/head rows + missing-slot mapno
chart.lag{ engine? } — lag ledger: chart vs venue clock vs engine series + verdict (engine read is in-tab, 6s budget)no
editor.get—no
editor.set{ doc, open? }yes
editor.run{ doc? }yes
editor.diagnostics—no
indicators.list—no
indicators.add{ name, code, paneId? }yes
indicators.remove{ id }yes
indicators.update{ id, name?, visible?, code?, inputValues? }yes
indicators.runre-run visibleyes
alerts.list—no
alerts.create{ name, kind, symbol?, params?, interval?, webhookUrl?, enabled? }yes
alerts.update{ id, patch }yes
alerts.remove{ id }yes
watchlist.get—no
watchlist.add / remove{ symbol }yes
watchlist.lists{ op: create|rename|delete|select, id?, name?, symbols? }yes
library.list{ prefix? }no
library.read{ id }no
library.write{ name, content, id?, description? }yes
library.remove{ id }yes
results.getlast run + strategy UIno
logs.get{ limit?, source?, level? }no
logs.append{ message, level?, source? }yes
logs.clear—yes
panels.get—no
panels.set{ id, open?, dock?, chartOnly? }yes
plugins.list—no
plugins.activate{ kind: source|stream|engine|storage, id }yes
theme.get / theme.set{ theme?, presetId?, uiScale?, toggle? }set mutates
workspace.export{ includeBars? }no
workspace.import{ json } or snapshot objectyes
drawings.list—no
drawings.add{ kind, points: [{time, price}…], style?, meta?, symbol?, id? } — kind arity validated, symbol defaults to activeyes
drawings.update{ id, points?, style?, meta?, visible? } — kind immutableyes
drawings.remove{ id }yes
drawings.clear{ symbol? }yes
drawings.tool{ tool }yes
settings.get{ key? } — sanitized settings, secrets never exposedno
settings.setallowlisted patch (endpoint, engine, interval, historyBars, refreshSec, live.*, uiScale, autoload, label/strategy/telemetry prefs); interval/history changes reloadyes
engine.get— engine plane: config (names only, key as hasApiKey), capabilities, route, health, chart session, telemetryno
engine.series{ exchange?, market?, symbol?, interval? } — engine-side series bounds + staleness (chart default)no
engine.tick{ symbol?, interval?, marketNow?, historyBars? } — force one drive tick; before/after bounds + envelopeyes
engine.repair{ symbol?, interval?, historyBars? } — force a stale-series repair (local upload or engine fetch)yes
stream.get— live plane: route, engine socket, subscriptions, feed chips, telemetryno
stream.reset{ resubscribe? } — drop the socket + subscriptions; probes one re-subscribe (default true)yes
status.getstatus bar + telemetryno

app_set paths: symbol, interval, source, engine, exchange, theme, chartType, uiScale, live.active, live.preferAfterLoad, editor.doc, editor.open, watchlist.open.

Resources and prompts

URIContents
axis://healthHealth JSON
axis://capabilitiesTool names + planes
axis://allowlistaxis_request paths
axis://sessionBridge status for this key

Prompts (seed the agent; they still call tools):

NameArgs
run_scriptscript (required), symbol?, interval?
debug_last_run—
analyze_strategy—
load_symbolsymbol, interval?
audit_workspace—

Client config

bash
axis mcp config --key pn_…
axis mcp --key pn_…                 # stdio proxy
json
{
  "mcpServers": {
    "axis": {
      "url": "https://worker.axis.hoox.sh/mcp",
      "headers": { "Authorization": "Bearer pn_…" }
    }
  }
}

Local wrangler: http://127.0.0.1:8787/mcp with ALLOW_OPEN_KEYS=1 still requires a non-empty Bearer.

Errors

CodeWhereMeaning
NO_KEY / INVALID_KEYHTTP 401Missing or unknown Bearer
API_KEYS_REQUIREDHTTP 503D1 without KV
RATEHTTP 42960/min/key
MCP_ALLOWLISTtoolPath not allowed
NO_MCP_BRIDGEtoolDO unbound
SESSION_OFFLINEapp toolNo PWA socket
TIMEOUTapp toolTab did not reply in 20s
UNKNOWN_CAPABILITY / PATH_DENIEDappBad id or set path
-32601JSON-RPCUnknown method

Implementation

PathRole
worker/src/mcp/handler.tsStreamable HTTP JSON-RPC
worker/src/mcp/catalog.tsTools / resources / prompts
worker/src/mcp/allowlist.tsaxis_request gate
worker/src/mcp/proxy.tsIn-isolate dispatch to existing handlers
worker/src/mcp/bridge.tsMcpBridgeDO
src/mcp/PWA host, snapshot, dispatch, WS client
packages/cli/src/commands/mcp.tsaxis mcp

In-page: window.__AXIS_MCP__.invoke('chart.get'). Tests: worker/tests/mcp-handler.test.ts, tests/mcp-dispatch.test.ts, packages/cli/tests/mcp.test.ts.

This is a stateless MCP handler (no deprecated McpAgent session DO). App state lives in the PWA + McpBridgeDO, not in an MCP session id.

See also