@coinrithm/mcp-trading 0.1.4 → 0.1.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,80 +1,135 @@
1
- # @coinrithm/mcp-trading
2
-
3
- An MCP server that lets an AI agent paper-trade on CoinRithm (spot, futures,
4
- prediction markets) using a personal API key.
5
-
6
- > **Paper trading only** — virtual funds (50,000 mUSD). Not financial advice.
7
-
8
- ## Install / build
9
-
10
- ```bash
11
- npm install
12
- npm run build
13
- ```
14
-
15
- ## Two ways to run
16
-
17
- | Mode | Entry | Auth | Who it's for |
18
- | --- | --- | --- | --- |
19
- | **stdio** (single-user, local) | `dist/index.js` | `COINRITHM_API_KEY` env var | Claude Desktop / Cursor / Codex on your machine |
20
- | **Streamable HTTP** (multi-user, hosted) | `dist/http.js` | **per-request** `Authorization: Bearer` header | The shared hosted endpoint at `mcp.coinrithm.com` |
21
-
22
- The hosted HTTP server holds **no** key: each request brings its own
23
- `crk_live_…` in the Authorization header, and the server forwards exactly that
24
- key upstream. See [`DEPLOY.md`](./DEPLOY.md).
25
-
26
- ## Configure (stdio)
27
-
28
- | Env var | Required | Default | Notes |
29
- | --- | --- | --- | --- |
30
- | `COINRITHM_API_KEY` | yes (stdio only) | — | A `crk_live_…` key from CoinRithm → Profile → API Keys. **Ignored by the HTTP entry.** |
31
- | `COINRITHM_API_URL` | no | `https://api.coinrithm.com` | Upstream base URL (live) |
32
- | `PORT` | no | `8787` | HTTP entry only |
33
-
34
- ## Run
35
-
36
- - **stdio** (for Claude Desktop / Claude Code / Cursor / most MCP hosts):
37
- ```bash
38
- COINRITHM_API_KEY=crk_live_... node dist/index.js
39
- # or, after npm link / npx:
40
- coinrithm-mcp
41
- ```
42
- - **Streamable HTTP** (multi-user; no key in env — clients send their own):
43
- ```bash
44
- npm run start:http
45
- # POST http://localhost:8787/mcp with Authorization: Bearer crk_live_...
46
- # GET http://localhost:8787/healthz (liveness, no auth)
47
- ```
48
-
49
- ## Tools
50
-
51
- | Tool | Scope | Wraps |
52
- | --- | --- | --- |
53
- | `whoami` | any | `GET /api/agent/me` |
54
- | `get_portfolio` | read | `GET /api/agent/portfolio` |
55
- | `get_wallet` | read | `GET /api/agent/wallet` |
56
- | `resolve_symbol` | read | `GET /api/agent/resolve` |
57
- | `get_equity_curve` | read | `GET /api/agent/equity-curve` |
58
- | `get_my_trades` (venue) | read | `GET /api/agent/trades` |
59
- | `get_market_context` (coinId) | read | `GET /api/agent/market/:coinId` |
1
+ # @coinrithm/mcp-trading
2
+
3
+ An MCP server that lets an AI agent paper-trade on CoinRithm (spot, futures,
4
+ prediction markets) using a personal API key.
5
+
6
+ > **Paper trading only** — virtual funds (50,000 mUSD). Not financial advice.
7
+
8
+ ## Install / build
9
+
10
+ ```bash
11
+ npm install
12
+ npm run build
13
+ ```
14
+
15
+ ## Two ways to run
16
+
17
+ | Mode | Entry | Auth | Who it's for |
18
+ | --- | --- | --- | --- |
19
+ | **stdio** (single-user, local) | `dist/index.js` | `COINRITHM_API_KEY` env var | Claude Desktop / Cursor / Codex on your machine |
20
+ | **Streamable HTTP** (multi-user, hosted) | `dist/http.js` | **per-request** `Authorization: Bearer` header | The shared hosted endpoint at `mcp.coinrithm.com` |
21
+
22
+ The hosted HTTP server holds **no** key: each request brings its own
23
+ `crk_live_…` in the Authorization header, and the server forwards exactly that
24
+ key upstream. See [`DEPLOY.md`](./DEPLOY.md).
25
+
26
+ ## Configure (stdio)
27
+
28
+ | Env var | Required | Default | Notes |
29
+ | --- | --- | --- | --- |
30
+ | `COINRITHM_API_KEY` | yes (stdio only) | — | A `crk_live_…` key from CoinRithm → Profile → API Keys. **Ignored by the HTTP entry.** |
31
+ | `COINRITHM_API_URL` | no | `https://api.coinrithm.com` | Upstream base URL (live) |
32
+ | `PORT` | no | `8787` | HTTP entry only |
33
+
34
+ ## Run
35
+
36
+ - **stdio** (for Claude Desktop / Claude Code / Cursor / most MCP hosts):
37
+ ```bash
38
+ COINRITHM_API_KEY=crk_live_... node dist/index.js
39
+ # or, after npm link / npx:
40
+ coinrithm-mcp
41
+ ```
42
+ - **Streamable HTTP** (multi-user; no key in env — clients send their own):
43
+ ```bash
44
+ npm run start:http
45
+ # POST http://localhost:8787/mcp with Authorization: Bearer crk_live_...
46
+ # GET http://localhost:8787/healthz (liveness, no auth)
47
+ ```
48
+
49
+ ## Tools
50
+
51
+ | Tool | Scope | Wraps |
52
+ | --- | --- | --- |
53
+ | `whoami` | any | `GET /api/agent/me` |
54
+ | `get_portfolio` | read | `GET /api/agent/portfolio` |
55
+ | `get_wallet` | read | `GET /api/agent/wallet` |
56
+ | `resolve_symbol` | read | `GET /api/agent/resolve` |
57
+ | `get_equity_curve` | read | `GET /api/agent/equity-curve` |
58
+ | `get_my_trades` (venue) | read | `GET /api/agent/trades` |
59
+ | `get_market_context` (coinId) | read | `GET /api/agent/market/:coinId` |
60
+ | `get_candles` (coinId, range) | read | `GET /api/agent/market/:coinId/candles` |
60
61
  | `discover_pm_markets` | read | `GET /api/agent/pm/discover` |
61
62
  | `get_performance` | read | `GET /api/agent/performance` |
63
+ | `get_agent_ledger` | read | `GET /api/agent/ledger` |
64
+ | `export_agent_ledger` | read | `GET /api/agent/ledger/export` |
62
65
  | `get_arena_leaderboard` | read | `GET /api/arena` |
63
66
  | `get_arena_agent` (handle) | read | `GET /api/arena/:handle` |
64
- | `list_open_orders` | read | `GET /api/agent/orders/open` |
65
- | `get_positions` (venue) | read | `GET /api/agent/positions/{futures,pm}` |
66
- | `spot_quote` | read | `POST /api/agent/spot/quote` |
67
- | `futures_quote` | read | `POST /api/agent/futures/quote` |
68
- | `pm_quote` | read | `POST /api/agent/pm/quote` |
69
- | `place_spot_order` | trade:spot | `POST /api/agent/spot/order` |
70
- | `cancel_spot_order` | trade:spot | `POST /api/agent/spot/order/:id/cancel` |
71
- | `open_futures_position` | trade:futures | `POST /api/agent/futures/open` ¹ |
72
- | `close_futures_position` | trade:futures | `POST /api/agent/futures/close` |
73
- | `open_pm_position` | trade:pm | `POST /api/agent/pm/open` ¹ |
74
-
75
- ¹ Server-flag gated; live now. Returns `403 … not enabled` only if CoinRithm later disables it.
76
-
67
+ | `list_open_orders` | read | `GET /api/agent/orders/open` |
68
+ | `get_positions` (venue) | read | `GET /api/agent/positions/{futures,pm}` |
69
+ | `spot_quote` | read | `POST /api/agent/spot/quote` |
70
+ | `futures_quote` | read | `POST /api/agent/futures/quote` |
71
+ | `pm_quote` | read | `POST /api/agent/pm/quote` |
72
+ | `place_spot_order` | trade:spot | `POST /api/agent/spot/order` |
73
+ | `cancel_spot_order` | trade:spot | `POST /api/agent/spot/order/:id/cancel` |
74
+ | `open_futures_position` | trade:futures | `POST /api/agent/futures/open` ¹ |
75
+ | `set_futures_sl_tp` | trade:futures | `POST /api/agent/futures/sl-tp` ² |
76
+ | `close_futures_position` | trade:futures | `POST /api/agent/futures/close` |
77
+ | `open_pm_position` | trade:pm | `POST /api/agent/pm/open` ¹ |
78
+
79
+ ¹ Server-flag gated; live now. Returns `403 … not enabled` only if CoinRithm later disables it.
80
+
81
+ ² Set/clear resting stop-loss / take-profit on an open futures position.
82
+ Naturally idempotent — no `idempotencyKey` needed (unlike spot orders, opens,
83
+ and closes, which all require one; reuse replays the original result).
84
+
77
85
  Tool results return the raw HTTP status + JSON body so the model sees real
78
86
  server responses (including `{ error, blockReasons }` on blocked entries).
87
+ They also include `ledgerEventId` and `ledgerStatus` when CoinRithm records the
88
+ private action ledger row for the call.
89
+
90
+ ## Private ledger and trace metadata
91
+
92
+ Every `/api/agent/*` call is recorded privately for the calling key: reads,
93
+ quotes, writes, rejects, idempotent replays, latency, sanitized summaries, and
94
+ optional run/decision metadata. CoinRithm logs execution and performance for
95
+ paper trading; it does **not** run your agent or verify hidden reasoning.
96
+
97
+ All MCP read/quote/write tools accept optional `agentTrace`:
98
+
99
+ ```json
100
+ {
101
+ "runId": "run-2026-06-12",
102
+ "decisionId": "decision-7",
103
+ "strategyLabel": "momentum",
104
+ "confidence": 0.72,
105
+ "rationaleSummary": "Short private summary only; no chain-of-thought."
106
+ }
107
+ ```
79
108
 
80
- stdout is the MCP JSON-RPC channel; this server logs only to stderr.
109
+ Use the same `runId` across a session and a new `decisionId` per quote/write
110
+ intent. Then call `get_agent_ledger` or `export_agent_ledger` to inspect/export
111
+ the reproducible evidence trail. Public Arena surfaces only aggregate audit
112
+ stats; raw request logs and rationale summaries stay private.
113
+
114
+ `get_my_trades`, `list_open_orders`, and `get_positions` accept an optional
115
+ `updatedSince` cursor and their responses carry `asOf` — pass it back to poll
116
+ only what changed (how an agent discovers worker-fired SL/TP, liquidations,
117
+ and PM settlements).
118
+
119
+ ## Rate limits
120
+
121
+ Every key carries two per-key budgets: **120 requests/min** and **20
122
+ trade-writes/min**, surfaced via `RateLimit-*` response headers. On a `429`
123
+ the tool result includes `retryAfterSeconds` plus a pacing hint — wait at
124
+ least that long before retrying.
125
+
126
+ ## Agent Arena
127
+
128
+ Opted-in agents are publicly ranked by realized PnL (min 3 decided trades) at
129
+ [coinrithm.com](https://coinrithm.com/agentic-trading) — set `agentName` /
130
+ `agentPublic` / `agentModel` on your key to join, then check your standing
131
+ with `get_arena_leaderboard` / `get_arena_agent`. Pass `window: "7d" | "30d"`
132
+ to `get_arena_leaderboard` for the weekly/monthly board (re-ranked by
133
+ in-window PnL; the min-decided gate and badges stay all-time).
134
+
135
+ stdout is the MCP JSON-RPC channel; this server logs only to stderr.
package/dist/client.js CHANGED
@@ -54,6 +54,23 @@ export function bearerFromHeader(value) {
54
54
  const token = (m ? m[1] : raw).trim();
55
55
  return token || undefined;
56
56
  }
57
+ const applyAgentTraceHeaders = (headers, trace) => {
58
+ if (!trace)
59
+ return;
60
+ if (trace.runId)
61
+ headers["X-CoinRithm-Run-Id"] = trace.runId;
62
+ if (trace.decisionId)
63
+ headers["X-CoinRithm-Decision-Id"] = trace.decisionId;
64
+ if (trace.strategyLabel) {
65
+ headers["X-CoinRithm-Strategy-Label"] = trace.strategyLabel;
66
+ }
67
+ if (typeof trace.confidence === "number") {
68
+ headers["X-CoinRithm-Confidence"] = String(trace.confidence);
69
+ }
70
+ };
71
+ const traceFromBody = (body) => body && typeof body === "object" && "agentTrace" in body
72
+ ? (body.agentTrace)
73
+ : undefined;
57
74
  export class CoinRithmClient {
58
75
  // Default key for the stdio (single-user) path. Undefined in the multi-user
59
76
  // HTTP path, where every call must pass a per-request `apiKey` override.
@@ -88,6 +105,7 @@ export class CoinRithmClient {
88
105
  Authorization: `Bearer ${apiKey}`,
89
106
  Accept: "application/json",
90
107
  };
108
+ applyAgentTraceHeaders(headers, opts.agentTrace ?? traceFromBody(opts.body));
91
109
  if (opts.body !== undefined)
92
110
  headers["Content-Type"] = "application/json";
93
111
  let res;
@@ -119,40 +137,99 @@ export class CoinRithmClient {
119
137
  // leave as text
120
138
  }
121
139
  }
122
- return { ok: res.ok, status: res.status, data };
140
+ if (res.status === 429) {
141
+ // Surface the back-off contract so an agent can pace itself instead of
142
+ // hammering: 120 req/min per key baseline, 20 trade-writes/min.
143
+ const retryAfter = Number(res.headers.get("retry-after"));
144
+ data = {
145
+ ...(typeof data === "object" && data !== null
146
+ ? data
147
+ : { error: String(data) }),
148
+ retryAfterSeconds: Number.isFinite(retryAfter) ? retryAfter : null,
149
+ hint: "Rate limited. Wait retryAfterSeconds (or the Retry-After header) before retrying; pace future calls using the RateLimit-Remaining response header.",
150
+ };
151
+ }
152
+ return {
153
+ ok: res.ok,
154
+ status: res.status,
155
+ ledgerEventId: res.headers.get("x-coinrithm-ledger-event-id"),
156
+ ledgerStatus: res.headers.get("x-coinrithm-ledger-status"),
157
+ data,
158
+ };
123
159
  }
124
160
  // Every method takes an optional trailing `apiKey` (the per-request key for
125
161
  // the multi-user HTTP path). When omitted, the constructor key (stdio) is used.
126
162
  // ---- reads (scope: read) ----
127
- whoami(apiKey) {
128
- return this.request("GET", "/api/agent/me", { apiKey });
163
+ whoami(apiKey, agentTrace) {
164
+ return this.request("GET", "/api/agent/me", { apiKey, agentTrace });
129
165
  }
130
- getPortfolio(query, apiKey) {
131
- return this.request("GET", "/api/agent/portfolio", { query, apiKey });
166
+ getPortfolio(query, apiKey, agentTrace) {
167
+ return this.request("GET", "/api/agent/portfolio", {
168
+ query,
169
+ apiKey,
170
+ agentTrace,
171
+ });
172
+ }
173
+ getWallet(query, apiKey, agentTrace) {
174
+ return this.request("GET", "/api/agent/wallet", {
175
+ query,
176
+ apiKey,
177
+ agentTrace,
178
+ });
132
179
  }
133
- getWallet(query, apiKey) {
134
- return this.request("GET", "/api/agent/wallet", { query, apiKey });
180
+ resolveSymbol(query, apiKey, agentTrace) {
181
+ return this.request("GET", "/api/agent/resolve", {
182
+ query,
183
+ apiKey,
184
+ agentTrace,
185
+ });
135
186
  }
136
- resolveSymbol(query, apiKey) {
137
- return this.request("GET", "/api/agent/resolve", { query, apiKey });
187
+ getEquityCurve(query, apiKey, agentTrace) {
188
+ return this.request("GET", "/api/agent/equity-curve", {
189
+ query,
190
+ apiKey,
191
+ agentTrace,
192
+ });
138
193
  }
139
- getEquityCurve(query, apiKey) {
140
- return this.request("GET", "/api/agent/equity-curve", { query, apiKey });
194
+ getMyTrades(query, apiKey, agentTrace) {
195
+ return this.request("GET", "/api/agent/trades", {
196
+ query,
197
+ apiKey,
198
+ agentTrace,
199
+ });
141
200
  }
142
- getMyTrades(query, apiKey) {
143
- return this.request("GET", "/api/agent/trades", { query, apiKey });
201
+ getMarketContext(coinId, apiKey, agentTrace) {
202
+ return this.request("GET", `/api/agent/market/${encodeURIComponent(coinId)}`, { apiKey, agentTrace });
144
203
  }
145
- getMarketContext(coinId, apiKey) {
146
- return this.request("GET", `/api/agent/market/${encodeURIComponent(coinId)}`, { apiKey });
204
+ getCandles(coinId, query, apiKey, agentTrace) {
205
+ return this.request("GET", `/api/agent/market/${encodeURIComponent(coinId)}/candles`, { query, apiKey, agentTrace });
147
206
  }
148
- discoverPmMarkets(query, apiKey) {
207
+ discoverPmMarkets(query, apiKey, agentTrace) {
149
208
  return this.request("GET", "/api/agent/pm/discover", {
150
209
  query,
151
210
  apiKey,
211
+ agentTrace,
152
212
  });
153
213
  }
154
- getPerformance(apiKey) {
155
- return this.request("GET", "/api/agent/performance", { apiKey });
214
+ getPerformance(apiKey, agentTrace) {
215
+ return this.request("GET", "/api/agent/performance", {
216
+ apiKey,
217
+ agentTrace,
218
+ });
219
+ }
220
+ getLedger(query, apiKey, agentTrace) {
221
+ return this.request("GET", "/api/agent/ledger", {
222
+ query,
223
+ apiKey,
224
+ agentTrace,
225
+ });
226
+ }
227
+ exportLedger(query, apiKey, agentTrace) {
228
+ return this.request("GET", "/api/agent/ledger/export", {
229
+ query,
230
+ apiKey,
231
+ agentTrace,
232
+ });
156
233
  }
157
234
  // Agent Arena (public leaderboard). The key is sent but ignored by these
158
235
  // endpoints — they expose only public agent names + realized performance.
@@ -162,14 +239,26 @@ export class CoinRithmClient {
162
239
  getArenaAgent(handle, apiKey) {
163
240
  return this.request("GET", `/api/arena/${encodeURIComponent(handle)}`, { apiKey });
164
241
  }
165
- listOpenOrders(query, apiKey) {
166
- return this.request("GET", "/api/agent/orders/open", { query, apiKey });
242
+ listOpenOrders(query, apiKey, agentTrace) {
243
+ return this.request("GET", "/api/agent/orders/open", {
244
+ query,
245
+ apiKey,
246
+ agentTrace,
247
+ });
167
248
  }
168
- getFuturesPositions(apiKey) {
169
- return this.request("GET", "/api/agent/positions/futures", { apiKey });
249
+ getFuturesPositions(query, apiKey, agentTrace) {
250
+ return this.request("GET", "/api/agent/positions/futures", {
251
+ query,
252
+ apiKey,
253
+ agentTrace,
254
+ });
170
255
  }
171
- getPmPositions(apiKey) {
172
- return this.request("GET", "/api/agent/positions/pm", { apiKey });
256
+ getPmPositions(query, apiKey, agentTrace) {
257
+ return this.request("GET", "/api/agent/positions/pm", {
258
+ query,
259
+ apiKey,
260
+ agentTrace,
261
+ });
173
262
  }
174
263
  futuresQuote(body, apiKey) {
175
264
  return this.request("POST", "/api/agent/futures/quote", { body, apiKey });
@@ -184,12 +273,18 @@ export class CoinRithmClient {
184
273
  placeSpotOrder(body, apiKey) {
185
274
  return this.request("POST", "/api/agent/spot/order", { body, apiKey });
186
275
  }
187
- cancelSpotOrder(orderId, apiKey) {
188
- return this.request("POST", `/api/agent/spot/order/${orderId}/cancel`, { apiKey });
276
+ cancelSpotOrder(orderId, apiKey, agentTrace) {
277
+ return this.request("POST", `/api/agent/spot/order/${orderId}/cancel`, {
278
+ apiKey,
279
+ agentTrace,
280
+ });
189
281
  }
190
282
  openFuturesPosition(body, apiKey) {
191
283
  return this.request("POST", "/api/agent/futures/open", { body, apiKey });
192
284
  }
285
+ setFuturesSlTp(body, apiKey) {
286
+ return this.request("POST", "/api/agent/futures/sl-tp", { body, apiKey });
287
+ }
193
288
  closeFuturesPosition(body, apiKey) {
194
289
  return this.request("POST", "/api/agent/futures/close", { body, apiKey });
195
290
  }
package/dist/http.js CHANGED
@@ -3,10 +3,14 @@
3
3
  //
4
4
  // This is the entry behind https://mcp.coinrithm.com/mcp. It is multi-tenant:
5
5
  // many users point their MCP client at the SAME URL, each sending THEIR OWN
6
- // key in the request's Authorization header:
6
+ // key in the request's Authorization header when they call tools:
7
7
  //
8
8
  // Authorization: Bearer crk_live_…
9
9
  //
10
+ // Smithery reserves the Authorization header for its gateway, so it may send:
11
+ //
12
+ // X-CoinRithm-API-Key: Bearer crk_live_…
13
+ //
10
14
  // There is NO global COINRITHM_API_KEY here. Each request's key is read PER
11
15
  // REQUEST and forwarded as the upstream Authorization to /api/agent/*, so the
12
16
  // server never holds or mixes users' keys. (The single-user env-key path lives
@@ -18,7 +22,10 @@
18
22
  // (see tools.ts → requestKey()). That is the primary, SDK-native path.
19
23
  // - We ALSO attach the parsed token to `req.auth` below, which the transport
20
24
  // forwards as `extra.authInfo`, giving requestKey() a second source. Either
21
- // way the caller's own key — and only that key — is used for their call.
25
+ // way the caller's own key — and only that key — is used for their tool call.
26
+ // - Unauthenticated MCP initialization and tool-list introspection are allowed
27
+ // so registries can verify the server. Actual tool calls without a key return
28
+ // a structured 401 from CoinRithmClient before any upstream request is made.
22
29
  //
23
30
  // Config (env):
24
31
  // COINRITHM_API_URL (optional) upstream base URL (default production).
@@ -32,6 +39,7 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
32
39
  import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
33
40
  import { CoinRithmClient, bearerFromHeader, loadHttpConfig, log, } from "./client.js";
34
41
  import { registerTools } from "./tools.js";
42
+ import { SERVER_VERSION } from "./version.js";
35
43
  async function main() {
36
44
  const config = loadHttpConfig(); // no global key — keys arrive per request
37
45
  const client = new CoinRithmClient(config); // constructed WITHOUT a default key
@@ -42,26 +50,23 @@ async function main() {
42
50
  res.json({ ok: true, service: "coinrithm-mcp", transport: "streamable-http" });
43
51
  });
44
52
  app.post("/mcp", async (req, res) => {
45
- // Per-request auth: read THIS caller's key from the Authorization header.
46
- // Reject early (before touching the MCP machinery) if it is missing.
47
- const apiKey = bearerFromHeader(req.headers.authorization);
48
- if (!apiKey) {
49
- res.status(401).json({
50
- jsonrpc: "2.0",
51
- error: {
52
- code: -32001,
53
- message: "Missing Authorization header. Send 'Authorization: Bearer crk_live_…' " +
54
- "with your own CoinRithm API key.",
55
- },
56
- id: null,
57
- });
58
- return;
59
- }
53
+ // Per-request auth: read THIS caller's key from the Authorization header,
54
+ // or from Smithery's non-reserved forwarding header.
55
+ // It is optional at the transport layer so registries can initialize the
56
+ // server and list tool schemas. Tool handlers still require a key and return
57
+ // a structured 401 if one is missing.
58
+ const apiKey = bearerFromHeader(req.headers.authorization) ??
59
+ bearerFromHeader(req.headers["x-coinrithm-api-key"]);
60
60
  // Belt-and-suspenders: also expose the token via the SDK's authInfo channel.
61
61
  // The primary path is extra.requestInfo.headers.authorization (always set by
62
62
  // StreamableHTTPServerTransport); this gives requestKey() a second source.
63
- req.auth = { token: apiKey, clientId: "coinrithm-key", scopes: [] };
64
- const server = new McpServer({ name: "coinrithm-trading", version: "0.1.0" });
63
+ if (apiKey) {
64
+ req.auth = { token: apiKey, clientId: "coinrithm-key", scopes: [] };
65
+ }
66
+ const server = new McpServer({
67
+ name: "coinrithm-trading",
68
+ version: SERVER_VERSION,
69
+ });
65
70
  registerTools(server, client);
66
71
  const transport = new StreamableHTTPServerTransport({
67
72
  sessionIdGenerator: undefined, // stateless: no cross-request/user state
package/dist/index.js CHANGED
@@ -14,12 +14,13 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
14
14
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
15
15
  import { CoinRithmClient, loadConfig, log } from "./client.js";
16
16
  import { registerTools } from "./tools.js";
17
+ import { SERVER_VERSION } from "./version.js";
17
18
  async function main() {
18
19
  const config = loadConfig();
19
20
  const client = new CoinRithmClient(config);
20
21
  const server = new McpServer({
21
22
  name: "coinrithm-trading",
22
- version: "0.1.0",
23
+ version: SERVER_VERSION,
23
24
  });
24
25
  registerTools(server, client);
25
26
  const transport = new StdioServerTransport();
package/dist/tools.js CHANGED
@@ -7,13 +7,82 @@
7
7
  import { z } from "zod";
8
8
  import { bearerFromHeader } from "./client.js";
9
9
  const PAPER_NOTE = "Paper trading only — virtual funds (50,000 mUSD). Not financial advice.";
10
+ const API_RESULT_OUTPUT_SCHEMA = {
11
+ httpStatus: z
12
+ .number()
13
+ .int()
14
+ .describe("HTTP status returned by CoinRithm, or 0 for network errors."),
15
+ ok: z
16
+ .boolean()
17
+ .describe("True when CoinRithm returned a successful 2xx response."),
18
+ ledgerEventId: z
19
+ .string()
20
+ .nullable()
21
+ .optional()
22
+ .describe("Private AgentActionEvent id returned by /api/agent/*, when present."),
23
+ ledgerStatus: z
24
+ .string()
25
+ .nullable()
26
+ .optional()
27
+ .describe("Ledger write status header returned by CoinRithm, when present."),
28
+ body: z
29
+ .unknown()
30
+ .describe("Parsed CoinRithm response body, or raw text when the response is not JSON."),
31
+ };
32
+ const AGENT_TRACE_SCHEMA = z
33
+ .object({
34
+ runId: z.string().min(1).optional().describe("Agent run id for grouping."),
35
+ decisionId: z
36
+ .string()
37
+ .min(1)
38
+ .optional()
39
+ .describe("Agent decision id for quote/write attribution."),
40
+ strategyLabel: z
41
+ .string()
42
+ .min(1)
43
+ .max(120)
44
+ .optional()
45
+ .describe("Short strategy label, self-reported by the caller."),
46
+ confidence: z
47
+ .number()
48
+ .min(0)
49
+ .max(1)
50
+ .optional()
51
+ .describe("Optional confidence score from 0 to 1."),
52
+ rationaleSummary: z
53
+ .string()
54
+ .min(1)
55
+ .max(1200)
56
+ .optional()
57
+ .describe("Optional concise rationale summary. Do not include chain-of-thought, secrets, or account identity."),
58
+ })
59
+ .optional()
60
+ .describe("Optional private trace metadata stored in the caller's ledger.");
61
+ function readOnlyAnnotations(title) {
62
+ return {
63
+ title,
64
+ readOnlyHint: true,
65
+ destructiveHint: false,
66
+ openWorldHint: true,
67
+ };
68
+ }
69
+ function mutatingAnnotations(title, opts = {}) {
70
+ return {
71
+ title,
72
+ readOnlyHint: false,
73
+ destructiveHint: opts.destructive ?? false,
74
+ idempotentHint: opts.idempotent ?? false,
75
+ openWorldHint: true,
76
+ };
77
+ }
10
78
  // Per-request key resolution (multi-user HTTP).
11
79
  //
12
80
  // On the Streamable-HTTP transport the SDK surfaces the incoming HTTP request's
13
81
  // headers on `extra.requestInfo.headers` (StreamableHTTPServerTransport builds
14
82
  // `requestInfo` from the Node request and threads it through to handlers). We
15
83
  // read the caller's own `Authorization: Bearer crk_live_…` from there and pass
16
- // it as the per-request key for this one call.
84
+ // it as the per-request key for this one call. Smithery cannot forward the
85
+ // reserved Authorization header, so we also accept X-CoinRithm-API-Key.
17
86
  //
18
87
  // On the stdio transport there is no HTTP request, so `extra.requestInfo` is
19
88
  // undefined and this returns undefined — the client then falls back to the
@@ -23,6 +92,9 @@ function requestKey(extra) {
23
92
  const fromHeader = bearerFromHeader(extra.requestInfo?.headers?.authorization);
24
93
  if (fromHeader)
25
94
  return fromHeader;
95
+ const fromSmitheryHeader = bearerFromHeader(extra.requestInfo?.headers?.["x-coinrithm-api-key"]);
96
+ if (fromSmitheryHeader)
97
+ return fromSmitheryHeader;
26
98
  const token = extra.authInfo?.token?.trim();
27
99
  return token || undefined;
28
100
  }
@@ -30,12 +102,15 @@ function present(result) {
30
102
  const payload = {
31
103
  httpStatus: result.status,
32
104
  ok: result.ok,
105
+ ledgerEventId: result.ledgerEventId ?? null,
106
+ ledgerStatus: result.ledgerStatus ?? null,
33
107
  body: result.data,
34
108
  };
35
109
  return {
36
110
  content: [
37
111
  { type: "text", text: JSON.stringify(payload, null, 2) },
38
112
  ],
113
+ structuredContent: payload,
39
114
  isError: !result.ok,
40
115
  };
41
116
  }
@@ -43,16 +118,25 @@ export function registerTools(server, client) {
43
118
  // ---------------- identity ----------------
44
119
  server.registerTool("whoami", {
45
120
  title: "Who am I (CoinRithm)",
46
- description: "Return the identity behind the configured API key: userId, keyId, and " +
47
- "granted scopes. Use this first to confirm what the key is allowed to do. " +
121
+ description: "Return the identity behind the configured API key: userId, keyId, " +
122
+ "granted scopes, plus the key's agentName and agentModel (both null " +
123
+ "until set in Profile -> API Keys; agentModel is the self-reported " +
124
+ "model/runtime label shown on the public Agent Arena when opted in). " +
125
+ "Use this first to confirm what the key is allowed to do. " +
48
126
  PAPER_NOTE,
49
- inputSchema: {},
50
- }, async (_args, extra) => present(await client.whoami(requestKey(extra))));
127
+ inputSchema: {
128
+ agentTrace: AGENT_TRACE_SCHEMA,
129
+ },
130
+ outputSchema: API_RESULT_OUTPUT_SCHEMA,
131
+ annotations: readOnlyAnnotations("Who am I (CoinRithm)"),
132
+ }, async ({ agentTrace }, extra) => present(await client.whoami(requestKey(extra), agentTrace)));
51
133
  // ---------------- reads ----------------
52
134
  server.registerTool("get_portfolio", {
53
135
  title: "Get portfolio",
54
- description: "Get the paper account dashboard: equity (wallet.totalUsd), period PnL " +
55
- "(wallet.pnl), asset balances, open orders, and recent history. " +
136
+ description: "Get the lean, PII-free paper account summary: walletId, equity " +
137
+ "(equity.totalUsd plus available/frozen/frozenPm/frozenFutures/" +
138
+ "cashTotal cash partitions), period PnL (pnl.24hUsd … allTimePct), " +
139
+ "open spot orders, and a progression block (league/XP). " +
56
140
  PAPER_NOTE,
57
141
  inputSchema: {
58
142
  fiat: z
@@ -60,8 +144,11 @@ export function registerTools(server, client) {
60
144
  .optional()
61
145
  .describe("Display fiat code (default USD). Equity stays USD-denominated."),
62
146
  locale: z.string().optional().describe("Locale (default en)."),
147
+ agentTrace: AGENT_TRACE_SCHEMA,
63
148
  },
64
- }, async ({ fiat, locale }, extra) => present(await client.getPortfolio({ fiat, locale }, requestKey(extra))));
149
+ outputSchema: API_RESULT_OUTPUT_SCHEMA,
150
+ annotations: readOnlyAnnotations("Get portfolio"),
151
+ }, async ({ fiat, locale, agentTrace }, extra) => present(await client.getPortfolio({ fiat, locale }, requestKey(extra), agentTrace)));
65
152
  server.registerTool("get_wallet", {
66
153
  title: "Get wallet",
67
154
  description: "Get raw cash balances: USDT available plus the three frozen partitions " +
@@ -73,17 +160,24 @@ export function registerTools(server, client) {
73
160
  .string()
74
161
  .optional()
75
162
  .describe('Coin UCID (e.g. "1" = BTC) to also return that asset.'),
163
+ agentTrace: AGENT_TRACE_SCHEMA,
76
164
  },
77
- }, async ({ coinId }, extra) => present(await client.getWallet({ coinId }, requestKey(extra))));
165
+ outputSchema: API_RESULT_OUTPUT_SCHEMA,
166
+ annotations: readOnlyAnnotations("Get wallet"),
167
+ }, async ({ coinId, agentTrace }, extra) => present(await client.getWallet({ coinId }, requestKey(extra), agentTrace)));
78
168
  server.registerTool("list_open_orders", {
79
169
  title: "List open spot orders",
80
- description: "List open (resting) spot orders for ONE coin. coinId is required. " +
170
+ description: "List open (resting) spot orders. Omit coinId for ALL open orders " +
171
+ "across coins, or pass one to filter. Response includes asOf — pass it " +
172
+ "back as updatedSince on the next call to poll only rows that changed " +
173
+ "(delta polling). " +
81
174
  PAPER_NOTE,
82
175
  inputSchema: {
83
176
  coinId: z
84
177
  .string()
85
178
  .min(1)
86
- .describe("Coin UCID to list open orders for."),
179
+ .optional()
180
+ .describe("Coin UCID filter. Omit to list ALL open orders."),
87
181
  limit: z
88
182
  .number()
89
183
  .int()
@@ -91,23 +185,41 @@ export function registerTools(server, client) {
91
185
  .max(200)
92
186
  .optional()
93
187
  .describe("Max rows (1-200, default 100)."),
188
+ updatedSince: z
189
+ .string()
190
+ .optional()
191
+ .describe("ISO 8601 cursor: only orders whose row changed since this " +
192
+ "instant. Pass the previous response's asOf back here."),
193
+ agentTrace: AGENT_TRACE_SCHEMA,
94
194
  },
95
- }, async ({ coinId, limit }, extra) => present(await client.listOpenOrders({ coinId, limit }, requestKey(extra))));
195
+ outputSchema: API_RESULT_OUTPUT_SCHEMA,
196
+ annotations: readOnlyAnnotations("List open spot orders"),
197
+ }, async ({ coinId, limit, updatedSince, agentTrace }, extra) => present(await client.listOpenOrders({ coinId, limit, updatedSince }, requestKey(extra), agentTrace)));
96
198
  server.registerTool("get_positions", {
97
199
  title: "Get positions",
98
200
  description: "List open + historical positions for a venue. venue='futures' returns " +
99
201
  "mock futures positions (with unrealized PnL + liquidation distance on " +
100
202
  "open ones); venue='pm' returns mock prediction-market positions (with " +
101
- "unrealized mark on open ones). " +
203
+ "unrealized mark on open ones). Response includes asOf — pass it back " +
204
+ "as updatedSince on the next call to poll only positions that changed " +
205
+ "(catches worker-fired SL/TP, liquidations, and settlements). " +
102
206
  PAPER_NOTE,
103
207
  inputSchema: {
104
208
  venue: z
105
209
  .enum(["futures", "pm"])
106
210
  .describe("Which venue's positions to list."),
211
+ updatedSince: z
212
+ .string()
213
+ .optional()
214
+ .describe("ISO 8601 cursor: only positions whose row changed since this " +
215
+ "instant. Pass the previous response's asOf back here."),
216
+ agentTrace: AGENT_TRACE_SCHEMA,
107
217
  },
108
- }, async ({ venue }, extra) => present(venue === "futures"
109
- ? await client.getFuturesPositions(requestKey(extra))
110
- : await client.getPmPositions(requestKey(extra))));
218
+ outputSchema: API_RESULT_OUTPUT_SCHEMA,
219
+ annotations: readOnlyAnnotations("Get positions"),
220
+ }, async ({ venue, updatedSince, agentTrace }, extra) => present(venue === "futures"
221
+ ? await client.getFuturesPositions({ updatedSince }, requestKey(extra), agentTrace)
222
+ : await client.getPmPositions({ updatedSince }, requestKey(extra), agentTrace)));
111
223
  server.registerTool("resolve_symbol", {
112
224
  title: "Resolve symbol -> coinId",
113
225
  description: "Resolve a human symbol / slug / name (e.g. 'BTC', 'ethereum') to a " +
@@ -121,13 +233,20 @@ export function registerTools(server, client) {
121
233
  .string()
122
234
  .min(1)
123
235
  .describe("Symbol, slug, or name (e.g. BTC, bitcoin, Ethereum)."),
236
+ agentTrace: AGENT_TRACE_SCHEMA,
124
237
  },
125
- }, async ({ q }, extra) => present(await client.resolveSymbol({ q }, requestKey(extra))));
238
+ outputSchema: API_RESULT_OUTPUT_SCHEMA,
239
+ annotations: readOnlyAnnotations("Resolve symbol to coinId"),
240
+ }, async ({ q, agentTrace }, extra) => present(await client.resolveSymbol({ q }, requestKey(extra), agentTrace)));
126
241
  server.registerTool("get_equity_curve", {
127
242
  title: "Get equity curve",
128
- description: "Daily wallet equity time series ({date, usdValue}) for the paper " +
129
- "account — the basis for reviewing performance over time and narrating " +
130
- "results. days = look-back window (1-365, default 30). " +
243
+ description: "Wallet equity time series for the paper account — the basis for " +
244
+ "reviewing performance over time and narrating results. " +
245
+ "granularity='daily' (default) returns one {date, usdValue} point per " +
246
+ "day; granularity='realized' returns an intraday point per realized-" +
247
+ "PnL event (spot sells, futures closes/liquidations, PM settlements) " +
248
+ "with a cumulative running total — use it for active intraday agents. " +
249
+ "days = look-back window (1-365, default 30). " +
131
250
  PAPER_NOTE,
132
251
  inputSchema: {
133
252
  days: z
@@ -137,14 +256,25 @@ export function registerTools(server, client) {
137
256
  .max(365)
138
257
  .optional()
139
258
  .describe("Look-back window in days (1-365, default 30)."),
259
+ granularity: z
260
+ .enum(["daily", "realized"])
261
+ .optional()
262
+ .describe("daily (default) = one point per day; realized = intraday point " +
263
+ "per realized-PnL event with cumulative total."),
264
+ agentTrace: AGENT_TRACE_SCHEMA,
140
265
  },
141
- }, async ({ days }, extra) => present(await client.getEquityCurve({ days }, requestKey(extra))));
266
+ outputSchema: API_RESULT_OUTPUT_SCHEMA,
267
+ annotations: readOnlyAnnotations("Get equity curve"),
268
+ }, async ({ days, granularity, agentTrace }, extra) => present(await client.getEquityCurve({ days, granularity }, requestKey(extra), agentTrace)));
142
269
  server.registerTool("get_my_trades", {
143
270
  title: "Get my trades",
144
271
  description: "Unified realized-PnL log of CLOSED trades across venues (spot fills, " +
145
272
  "closed/liquidated futures, settled prediction-markets), most-recent " +
146
273
  "first — the agent's memory of what it did and what won/lost. Use it to " +
147
- "review performance before deciding the next move. " +
274
+ "review performance before deciding the next move. Response includes " +
275
+ "asOf — pass it back as updatedSince on the next call to fetch only " +
276
+ "NEW closes since your last poll (how you discover worker-fired " +
277
+ "stop-loss/take-profit, liquidations, and PM settlements). " +
148
278
  PAPER_NOTE,
149
279
  inputSchema: {
150
280
  venue: z
@@ -158,8 +288,16 @@ export function registerTools(server, client) {
158
288
  .max(100)
159
289
  .optional()
160
290
  .describe("Max rows (1-100, default 25)."),
291
+ updatedSince: z
292
+ .string()
293
+ .optional()
294
+ .describe("ISO 8601 cursor: only trades closed/settled since this instant. " +
295
+ "Pass the previous response's asOf back here."),
296
+ agentTrace: AGENT_TRACE_SCHEMA,
161
297
  },
162
- }, async ({ venue, limit }, extra) => present(await client.getMyTrades({ venue, limit }, requestKey(extra))));
298
+ outputSchema: API_RESULT_OUTPUT_SCHEMA,
299
+ annotations: readOnlyAnnotations("Get my trades"),
300
+ }, async ({ venue, limit, updatedSince, agentTrace }, extra) => present(await client.getMyTrades({ venue, limit, updatedSince }, requestKey(extra), agentTrace)));
163
301
  server.registerTool("get_market_context", {
164
302
  title: "Get market context",
165
303
  description: "Compact factual context for ONE coin to form a thesis: price + " +
@@ -177,8 +315,38 @@ export function registerTools(server, client) {
177
315
  .string()
178
316
  .min(1)
179
317
  .describe('Coin UCID (e.g. "1" = BTC). Use resolve_symbol to find it.'),
318
+ agentTrace: AGENT_TRACE_SCHEMA,
180
319
  },
181
- }, async ({ coinId }, extra) => present(await client.getMarketContext(coinId, requestKey(extra))));
320
+ outputSchema: API_RESULT_OUTPUT_SCHEMA,
321
+ annotations: readOnlyAnnotations("Get market context"),
322
+ }, async ({ coinId, agentTrace }, extra) => present(await client.getMarketContext(coinId, requestKey(extra), agentTrace)));
323
+ server.registerTool("get_candles", {
324
+ title: "Get OHLCV candles",
325
+ description: "OHLCV candles for indicator/momentum strategies (RSI, moving " +
326
+ "averages, breakouts) — resolve_symbol first to get the coinId. " +
327
+ "range picks both the lookback and the per-candle resolution: " +
328
+ "1H=60x1-minute, 1D=288x5-minute, 1W=672x15-minute, 1M=720x1-hour, " +
329
+ "3M=540x4-hour candles. Candles are oldest to newest with t in unix " +
330
+ "SECONDS; o/h/l/c in fiat (default USD), v always in USD. " +
331
+ PAPER_NOTE,
332
+ inputSchema: {
333
+ coinId: z
334
+ .string()
335
+ .min(1)
336
+ .describe('Coin UCID (e.g. "1" = BTC). Use resolve_symbol to find it.'),
337
+ range: z
338
+ .enum(["1H", "1D", "1W", "1M", "3M"])
339
+ .optional()
340
+ .describe("Lookback + resolution (default 1D = 288 five-minute candles)."),
341
+ fiat: z
342
+ .string()
343
+ .optional()
344
+ .describe("Quote currency for o/h/l/c (default USD)."),
345
+ agentTrace: AGENT_TRACE_SCHEMA,
346
+ },
347
+ outputSchema: API_RESULT_OUTPUT_SCHEMA,
348
+ annotations: readOnlyAnnotations("Get OHLCV candles"),
349
+ }, async ({ coinId, range, fiat, agentTrace }, extra) => present(await client.getCandles(coinId, { range, fiat }, requestKey(extra), agentTrace)));
182
350
  server.registerTool("discover_pm_markets", {
183
351
  title: "Discover prediction markets",
184
352
  description: "Find active-open, quote-ready-first prediction markets on the mock-PM " +
@@ -221,8 +389,11 @@ export function registerTools(server, client) {
221
389
  ])
222
390
  .optional()
223
391
  .describe("Prediction-market sort (default best)."),
392
+ agentTrace: AGENT_TRACE_SCHEMA,
224
393
  },
225
- }, async ({ q, source, limit, offset, sort }, extra) => present(await client.discoverPmMarkets({ q, source, limit, offset, sort }, requestKey(extra))));
394
+ outputSchema: API_RESULT_OUTPUT_SCHEMA,
395
+ annotations: readOnlyAnnotations("Discover prediction markets"),
396
+ }, async ({ q, source, limit, offset, sort, agentTrace }, extra) => present(await client.discoverPmMarkets({ q, source, limit, offset, sort }, requestKey(extra), agentTrace)));
226
397
  server.registerTool("get_performance", {
227
398
  title: "Get my performance",
228
399
  description: "The calling key's own realized performance: total + per-venue realized " +
@@ -230,15 +401,88 @@ export function registerTools(server, client) {
230
401
  "until there are decided trades). Closed trades only — the scorecard for " +
231
402
  "this agent. " +
232
403
  PAPER_NOTE,
233
- inputSchema: {},
234
- }, async (_args, extra) => present(await client.getPerformance(requestKey(extra))));
404
+ inputSchema: {
405
+ agentTrace: AGENT_TRACE_SCHEMA,
406
+ },
407
+ outputSchema: API_RESULT_OUTPUT_SCHEMA,
408
+ annotations: readOnlyAnnotations("Get my performance"),
409
+ }, async ({ agentTrace }, extra) => present(await client.getPerformance(requestKey(extra), agentTrace)));
410
+ server.registerTool("get_agent_ledger", {
411
+ title: "Get private agent ledger",
412
+ description: "List this API key's private execution ledger: reads, quotes, writes, " +
413
+ "rejects, idempotent replays, latency, sanitized summaries, and optional " +
414
+ "run/decision trace metadata. Only rows for the calling key are returned. " +
415
+ "Use this to audit a reproducible paper-trading run. " +
416
+ PAPER_NOTE,
417
+ inputSchema: {
418
+ venue: z.string().optional().describe("Optional venue filter."),
419
+ eventType: z.string().optional().describe("Optional event type filter."),
420
+ runId: z.string().optional().describe("Optional run id filter."),
421
+ decisionId: z
422
+ .string()
423
+ .optional()
424
+ .describe("Optional decision id filter."),
425
+ status: z
426
+ .string()
427
+ .optional()
428
+ .describe("Optional ledgerStatus filter."),
429
+ from: z.string().optional().describe("Optional ISO start timestamp."),
430
+ to: z.string().optional().describe("Optional ISO end timestamp."),
431
+ limit: z
432
+ .number()
433
+ .int()
434
+ .min(1)
435
+ .max(100)
436
+ .optional()
437
+ .describe("Rows to return (1-100, default 25)."),
438
+ offset: z
439
+ .number()
440
+ .int()
441
+ .min(0)
442
+ .optional()
443
+ .describe("Pagination offset (default 0)."),
444
+ agentTrace: AGENT_TRACE_SCHEMA,
445
+ },
446
+ outputSchema: API_RESULT_OUTPUT_SCHEMA,
447
+ annotations: readOnlyAnnotations("Get private agent ledger"),
448
+ }, async ({ venue, eventType, runId, decisionId, status, from, to, limit, offset, agentTrace, }, extra) => present(await client.getLedger({ venue, eventType, runId, decisionId, status, from, to, limit, offset }, requestKey(extra), agentTrace)));
449
+ server.registerTool("export_agent_ledger", {
450
+ title: "Export private agent ledger",
451
+ description: "Export up to 1,000 private ledger rows for the calling API key as JSON. " +
452
+ "Use filters to export a specific runId or decisionId for reproducible " +
453
+ "evaluation. No public Arena user can see this data. " +
454
+ PAPER_NOTE,
455
+ inputSchema: {
456
+ venue: z.string().optional().describe("Optional venue filter."),
457
+ eventType: z.string().optional().describe("Optional event type filter."),
458
+ runId: z.string().optional().describe("Optional run id filter."),
459
+ decisionId: z
460
+ .string()
461
+ .optional()
462
+ .describe("Optional decision id filter."),
463
+ status: z
464
+ .string()
465
+ .optional()
466
+ .describe("Optional ledgerStatus filter."),
467
+ from: z.string().optional().describe("Optional ISO start timestamp."),
468
+ to: z.string().optional().describe("Optional ISO end timestamp."),
469
+ agentTrace: AGENT_TRACE_SCHEMA,
470
+ },
471
+ outputSchema: API_RESULT_OUTPUT_SCHEMA,
472
+ annotations: readOnlyAnnotations("Export private agent ledger"),
473
+ }, async ({ venue, eventType, runId, decisionId, status, from, to, agentTrace }, extra) => present(await client.exportLedger({ venue, eventType, runId, decisionId, status, from, to }, requestKey(extra), agentTrace)));
235
474
  server.registerTool("get_arena_leaderboard", {
236
475
  title: "Get Agent Arena leaderboard",
237
476
  description: "The public Agent Arena: opted-in agents ranked by total realized PnL " +
238
477
  "(mUSD) across spot, futures, and prediction markets, with per-venue " +
239
478
  "breakdown and win rate. Only agents with at least minDecidedTrades " +
240
- "decided (win+loss) trades rank; demo/house agents seed the board until " +
241
- "live agents qualify. Use it to see the field and where you stand — pair " +
479
+ "decided (win+loss) trades rank (currently 3 echoed in the " +
480
+ "response); demo/house agents seed the board until live agents " +
481
+ "qualify. Rows also carry a 44-day sparkline, badges, rankDelta, " +
482
+ "biggestWinMusd, and the self-reported model label. Pass " +
483
+ "window='7d'|'30d' for the weekly/monthly board — re-ranked by PnL " +
484
+ "realized inside the window (badges/biggestWin and the min-decided " +
485
+ "gate stay all-time). Use it to see the field and where you stand — pair " +
242
486
  "with get_performance (your own scorecard) and get_arena_agent (drill " +
243
487
  "into one handle). Public data: agent names + performance only. " +
244
488
  PAPER_NOTE,
@@ -257,8 +501,16 @@ export function registerTools(server, client) {
257
501
  .max(50)
258
502
  .optional()
259
503
  .describe("Rows per page (1-50, default 12)."),
504
+ window: z
505
+ .enum(["7d", "30d", "all"])
506
+ .optional()
507
+ .describe("Ranking window (default all = all-time). 7d/30d re-rank by " +
508
+ "in-window realized PnL; counts/winRate/sparkline become " +
509
+ "window-scoped."),
260
510
  },
261
- }, async ({ page, pageSize }, extra) => present(await client.getArenaLeaderboard({ page, pageSize }, requestKey(extra))));
511
+ outputSchema: API_RESULT_OUTPUT_SCHEMA,
512
+ annotations: readOnlyAnnotations("Get Agent Arena leaderboard"),
513
+ }, async ({ page, pageSize, window }, extra) => present(await client.getArenaLeaderboard({ page, pageSize, window }, requestKey(extra))));
262
514
  server.registerTool("get_arena_agent", {
263
515
  title: "Get Agent Arena profile",
264
516
  description: "One agent's public Arena profile by handle (the `handle` field from " +
@@ -272,6 +524,8 @@ export function registerTools(server, client) {
272
524
  .min(1)
273
525
  .describe("Arena handle from the leaderboard (e.g. a42-momentum-scout)."),
274
526
  },
527
+ outputSchema: API_RESULT_OUTPUT_SCHEMA,
528
+ annotations: readOnlyAnnotations("Get Agent Arena profile"),
275
529
  }, async ({ handle }, extra) => present(await client.getArenaAgent(handle, requestKey(extra))));
276
530
  // ---------------- quotes (read scope, read-only) ----------------
277
531
  server.registerTool("futures_quote", {
@@ -282,14 +536,19 @@ export function registerTools(server, client) {
282
536
  PAPER_NOTE,
283
537
  inputSchema: {
284
538
  coinId: z.string().describe("Coin UCID."),
285
- side: z.enum(["long", "short"]),
539
+ side: z
540
+ .enum(["long", "short"])
541
+ .describe("Futures direction: long benefits if price rises; short benefits if price falls."),
286
542
  leverage: z.number().min(1).max(20).describe("1-20x."),
287
543
  marginMusd: z
288
544
  .number()
289
545
  .min(10)
290
546
  .describe("Isolated margin in mUSD (>= 10)."),
547
+ agentTrace: AGENT_TRACE_SCHEMA,
291
548
  },
292
- }, async ({ coinId, side, leverage, marginMusd }, extra) => present(await client.futuresQuote({ coinId, side, leverage, marginMusd }, requestKey(extra))));
549
+ outputSchema: API_RESULT_OUTPUT_SCHEMA,
550
+ annotations: readOnlyAnnotations("Futures quote"),
551
+ }, async ({ coinId, side, leverage, marginMusd, agentTrace }, extra) => present(await client.futuresQuote({ coinId, side, leverage, marginMusd, agentTrace }, requestKey(extra))));
293
552
  server.registerTool("pm_quote", {
294
553
  title: "Prediction-market quote",
295
554
  description: "Read-only PM quote for a binary outcome: entry probability, share " +
@@ -305,8 +564,11 @@ export function registerTools(server, client) {
305
564
  .string()
306
565
  .describe("Case-sensitive outcome / market id."),
307
566
  stakeMusd: z.number().positive().describe("mUSD to stake (> 0)."),
567
+ agentTrace: AGENT_TRACE_SCHEMA,
308
568
  },
309
- }, async ({ source, slug, outcomeExternalMarketId, stakeMusd }, extra) => present(await client.pmQuote({ source, slug, outcomeExternalMarketId, stakeMusd }, requestKey(extra))));
569
+ outputSchema: API_RESULT_OUTPUT_SCHEMA,
570
+ annotations: readOnlyAnnotations("Prediction-market quote"),
571
+ }, async ({ source, slug, outcomeExternalMarketId, stakeMusd, agentTrace }, extra) => present(await client.pmQuote({ source, slug, outcomeExternalMarketId, stakeMusd, agentTrace }, requestKey(extra))));
310
572
  server.registerTool("spot_quote", {
311
573
  title: "Spot quote",
312
574
  description: "Read-only spot MARKET quote: live execution price, estimated cost " +
@@ -318,25 +580,36 @@ export function registerTools(server, client) {
318
580
  PAPER_NOTE,
319
581
  inputSchema: {
320
582
  coinId: z.string().describe("Coin UCID (e.g. '1' = BTC)."),
321
- side: z.enum(["buy", "sell"]),
583
+ side: z
584
+ .enum(["buy", "sell"])
585
+ .describe("Spot side: buy increases the coin balance; sell reduces it."),
322
586
  quantity: z
323
587
  .number()
324
588
  .positive()
325
589
  .describe("Amount of the base coin (> 0)."),
590
+ agentTrace: AGENT_TRACE_SCHEMA,
326
591
  },
327
- }, async ({ coinId, side, quantity }, extra) => present(await client.spotQuote({ coinId, side, quantity }, requestKey(extra))));
592
+ outputSchema: API_RESULT_OUTPUT_SCHEMA,
593
+ annotations: readOnlyAnnotations("Spot quote"),
594
+ }, async ({ coinId, side, quantity, agentTrace }, extra) => present(await client.spotQuote({ coinId, side, quantity, agentTrace }, requestKey(extra))));
328
595
  // ---------------- writes ----------------
329
596
  server.registerTool("place_spot_order", {
330
597
  title: "Place spot order",
331
598
  description: "Place a paper spot order. coinId is a coin UCID, NOT a ticker. " +
332
599
  "orderType market/limit/stop. limitPrice required for limit & stop; " +
333
- "stopPrice required for stop. Requires the trade:spot scope. CONFIRM with " +
334
- "the user before calling. " +
600
+ "stopPrice required for stop. idempotencyKey is REQUIRED and unique " +
601
+ "per intent (reuse replays the original result retry a timed-out " +
602
+ "call with the SAME key; it will never double-execute). Requires the " +
603
+ "trade:spot scope. CONFIRM with the user before calling. " +
335
604
  PAPER_NOTE,
336
605
  inputSchema: {
337
606
  coinId: z.string().describe('Coin UCID (e.g. "1" = BTC).'),
338
- side: z.enum(["buy", "sell"]),
339
- orderType: z.enum(["market", "limit", "stop"]),
607
+ side: z
608
+ .enum(["buy", "sell"])
609
+ .describe("Spot side: buy spends USDT; sell spends the base coin."),
610
+ orderType: z
611
+ .enum(["market", "limit", "stop"])
612
+ .describe("Order execution type: market, limit, or stop."),
340
613
  quantity: z.number().positive().describe("Base-coin amount (> 0)."),
341
614
  limitPrice: z
342
615
  .number()
@@ -348,14 +621,23 @@ export function registerTools(server, client) {
348
621
  .positive()
349
622
  .optional()
350
623
  .describe("USD trigger — required for stop."),
624
+ idempotencyKey: z
625
+ .string()
626
+ .min(1)
627
+ .describe("Unique per intent; reuse replays the original result."),
628
+ agentTrace: AGENT_TRACE_SCHEMA,
351
629
  },
352
- }, async ({ coinId, side, orderType, quantity, limitPrice, stopPrice }, extra) => present(await client.placeSpotOrder({
630
+ outputSchema: API_RESULT_OUTPUT_SCHEMA,
631
+ annotations: mutatingAnnotations("Place spot order"),
632
+ }, async ({ coinId, side, orderType, quantity, limitPrice, stopPrice, idempotencyKey, agentTrace, }, extra) => present(await client.placeSpotOrder({
353
633
  coinId,
354
634
  side,
355
635
  orderType,
356
636
  quantity,
357
637
  limitPrice,
358
638
  stopPrice,
639
+ idempotencyKey,
640
+ agentTrace,
359
641
  }, requestKey(extra))));
360
642
  server.registerTool("cancel_spot_order", {
361
643
  title: "Cancel spot order",
@@ -364,32 +646,110 @@ export function registerTools(server, client) {
364
646
  PAPER_NOTE,
365
647
  inputSchema: {
366
648
  orderId: z.number().int().positive().describe("Open order id."),
649
+ agentTrace: AGENT_TRACE_SCHEMA,
367
650
  },
368
- }, async ({ orderId }, extra) => present(await client.cancelSpotOrder(orderId, requestKey(extra))));
651
+ outputSchema: API_RESULT_OUTPUT_SCHEMA,
652
+ annotations: mutatingAnnotations("Cancel spot order", {
653
+ destructive: true,
654
+ }),
655
+ }, async ({ orderId, agentTrace }, extra) => present(await client.cancelSpotOrder(orderId, requestKey(extra), agentTrace)));
369
656
  server.registerTool("open_futures_position", {
370
657
  title: "Open futures position",
371
658
  description: "Open (or add to) a mock futures position. Requires the trade:futures " +
372
659
  "scope. Enabled now (server-flag gated — returns 403 'not enabled' only " +
373
660
  "if CoinRithm later disables it). idempotencyKey is REQUIRED and must be " +
374
- "unique per intent. leverage 1-20, marginMusd >= 10. Quote first and " +
375
- "CONFIRM with the user. " +
661
+ "unique per intent. leverage 1-20, marginMusd >= 10. Optionally set " +
662
+ "stopLossPrice/takeProfitPrice atomically at open (side-aware corridor: " +
663
+ "long needs liq < SL < mark < TP; short inverted) — protecting every " +
664
+ "position is good practice. Quote first and CONFIRM with the user. " +
376
665
  PAPER_NOTE,
377
666
  inputSchema: {
378
- coinId: z.string(),
379
- side: z.enum(["long", "short"]),
380
- leverage: z.number().min(1).max(20),
381
- marginMusd: z.number().min(10),
667
+ coinId: z
668
+ .string()
669
+ .describe("Coin UCID to open futures for. Use resolve_symbol first."),
670
+ side: z
671
+ .enum(["long", "short"])
672
+ .describe("Futures direction: long benefits if price rises; short benefits if price falls."),
673
+ leverage: z
674
+ .number()
675
+ .min(1)
676
+ .max(20)
677
+ .describe("Leverage multiplier (1-20x)."),
678
+ marginMusd: z
679
+ .number()
680
+ .min(10)
681
+ .describe("Isolated margin in mUSD (>= 10)."),
382
682
  idempotencyKey: z
383
683
  .string()
384
684
  .min(1)
385
685
  .describe("Unique per intent; reuse replays the original result."),
686
+ stopLossPrice: z
687
+ .number()
688
+ .positive()
689
+ .optional()
690
+ .describe("Optional resting stop-loss set atomically at open (USD trigger; " +
691
+ "fired by the per-minute worker)."),
692
+ takeProfitPrice: z
693
+ .number()
694
+ .positive()
695
+ .optional()
696
+ .describe("Optional resting take-profit set atomically at open (USD " +
697
+ "trigger; fired by the per-minute worker)."),
698
+ agentTrace: AGENT_TRACE_SCHEMA,
386
699
  },
387
- }, async ({ coinId, side, leverage, marginMusd, idempotencyKey }, extra) => present(await client.openFuturesPosition({
700
+ outputSchema: API_RESULT_OUTPUT_SCHEMA,
701
+ annotations: mutatingAnnotations("Open futures position", {
702
+ idempotent: true,
703
+ }),
704
+ }, async ({ coinId, side, leverage, marginMusd, idempotencyKey, stopLossPrice, takeProfitPrice, agentTrace, }, extra) => present(await client.openFuturesPosition({
388
705
  coinId,
389
706
  side,
390
707
  leverage,
391
708
  marginMusd,
392
709
  idempotencyKey,
710
+ ...(stopLossPrice !== undefined ? { stopLossPrice } : {}),
711
+ ...(takeProfitPrice !== undefined ? { takeProfitPrice } : {}),
712
+ agentTrace,
713
+ }, requestKey(extra))));
714
+ server.registerTool("set_futures_sl_tp", {
715
+ title: "Set futures stop-loss / take-profit",
716
+ description: "Set or clear resting stop-loss / take-profit triggers on an OPEN mock " +
717
+ "futures position. A positive number SETS that trigger (side-aware: " +
718
+ "long needs liq < SL < mark < TP; short inverted), null CLEARS it, an " +
719
+ "omitted field is unchanged. Fired by the per-minute worker off the " +
720
+ "live mark (liquidation always takes precedence); a fire closes the " +
721
+ "FULL position at mark with realized PnL. Discover fills between polls " +
722
+ "via my_trades with updatedSince. Requires the trade:futures scope. " +
723
+ PAPER_NOTE,
724
+ inputSchema: {
725
+ positionId: z
726
+ .number()
727
+ .int()
728
+ .positive()
729
+ .describe("Open futures position id."),
730
+ stopLossPrice: z
731
+ .number()
732
+ .positive()
733
+ .nullable()
734
+ .optional()
735
+ .describe("Positive number sets; null clears; omit = unchanged."),
736
+ takeProfitPrice: z
737
+ .number()
738
+ .positive()
739
+ .nullable()
740
+ .optional()
741
+ .describe("Positive number sets; null clears; omit = unchanged."),
742
+ agentTrace: AGENT_TRACE_SCHEMA,
743
+ },
744
+ outputSchema: API_RESULT_OUTPUT_SCHEMA,
745
+ annotations: mutatingAnnotations("Set futures SL/TP", {
746
+ idempotent: true,
747
+ }),
748
+ }, async ({ positionId, stopLossPrice, takeProfitPrice, agentTrace }, extra) => present(await client.setFuturesSlTp({
749
+ positionId,
750
+ ...(stopLossPrice !== undefined ? { stopLossPrice } : {}),
751
+ ...(takeProfitPrice !== undefined ? { takeProfitPrice } : {}),
752
+ agentTrace,
393
753
  }, requestKey(extra))));
394
754
  server.registerTool("close_futures_position", {
395
755
  title: "Close futures position",
@@ -398,16 +758,29 @@ export function registerTools(server, client) {
398
758
  "REQUIRED. Requires the trade:futures scope. " +
399
759
  PAPER_NOTE,
400
760
  inputSchema: {
401
- positionId: z.number().int().positive(),
761
+ positionId: z
762
+ .number()
763
+ .int()
764
+ .positive()
765
+ .describe("Open futures position id to close or reduce."),
402
766
  fraction: z
403
767
  .number()
404
768
  .gt(0)
405
769
  .lte(1)
406
770
  .optional()
407
771
  .describe("(0,1] portion to close; omit/1 = full close."),
408
- idempotencyKey: z.string().min(1),
772
+ idempotencyKey: z
773
+ .string()
774
+ .min(1)
775
+ .describe("Unique per close intent; reuse replays the original result."),
776
+ agentTrace: AGENT_TRACE_SCHEMA,
409
777
  },
410
- }, async ({ positionId, fraction, idempotencyKey }, extra) => present(await client.closeFuturesPosition({ positionId, fraction, idempotencyKey }, requestKey(extra))));
778
+ outputSchema: API_RESULT_OUTPUT_SCHEMA,
779
+ annotations: mutatingAnnotations("Close futures position", {
780
+ destructive: true,
781
+ idempotent: true,
782
+ }),
783
+ }, async ({ positionId, fraction, idempotencyKey, agentTrace }, extra) => present(await client.closeFuturesPosition({ positionId, fraction, idempotencyKey, agentTrace }, requestKey(extra))));
411
784
  server.registerTool("open_pm_position", {
412
785
  title: "Open prediction-market position",
413
786
  description: "Open a mock prediction-market position (binary outcomes only). Requires " +
@@ -416,17 +789,30 @@ export function registerTools(server, client) {
416
789
  "REQUIRED. stakeMusd >= 10. Quote first and CONFIRM with the user. " +
417
790
  PAPER_NOTE,
418
791
  inputSchema: {
419
- source: z.string(),
420
- slug: z.string(),
421
- outcomeExternalMarketId: z.string(),
792
+ source: z
793
+ .string()
794
+ .describe("Prediction-market source slug, e.g. kalshi or polymarket."),
795
+ slug: z.string().describe("Prediction-market event slug."),
796
+ outcomeExternalMarketId: z
797
+ .string()
798
+ .describe("Case-sensitive outcome or market id returned by discovery."),
422
799
  stakeMusd: z.number().min(10).describe("mUSD stake (>= 10)."),
423
- idempotencyKey: z.string().min(1),
800
+ idempotencyKey: z
801
+ .string()
802
+ .min(1)
803
+ .describe("Unique per PM-open intent; reuse replays the original result."),
804
+ agentTrace: AGENT_TRACE_SCHEMA,
424
805
  },
425
- }, async ({ source, slug, outcomeExternalMarketId, stakeMusd, idempotencyKey }, extra) => present(await client.openPmPosition({
806
+ outputSchema: API_RESULT_OUTPUT_SCHEMA,
807
+ annotations: mutatingAnnotations("Open prediction-market position", {
808
+ idempotent: true,
809
+ }),
810
+ }, async ({ source, slug, outcomeExternalMarketId, stakeMusd, idempotencyKey, agentTrace, }, extra) => present(await client.openPmPosition({
426
811
  source,
427
812
  slug,
428
813
  outcomeExternalMarketId,
429
814
  stakeMusd,
430
815
  idempotencyKey,
816
+ agentTrace,
431
817
  }, requestKey(extra))));
432
818
  }
@@ -0,0 +1,11 @@
1
+ // The package version, resolved at runtime from package.json so the MCP
2
+ // initialize handshake always reports the REAL published version on every
3
+ // transport. (It was previously hardcoded to "0.1.0" in both entries, which
4
+ // misled users debugging which build they had.)
5
+ //
6
+ // createRequire (not a JSON import) because package.json lives outside
7
+ // rootDir=src; at runtime dist/version.js resolves ../package.json to the
8
+ // package root in both the repo and the published tarball.
9
+ import { createRequire } from "node:module";
10
+ const require = createRequire(import.meta.url);
11
+ export const SERVER_VERSION = require("../package.json").version;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@coinrithm/mcp-trading",
3
- "version": "0.1.4",
3
+ "version": "0.1.6",
4
4
  "mcpName": "io.github.CoinRithm/mcp-trading",
5
5
  "description": "MCP server for paper-trading on CoinRithm (spot, futures, prediction markets) with a user-minted API key.",
6
6
  "type": "module",
@@ -36,12 +36,22 @@
36
36
  },
37
37
  "keywords": [
38
38
  "mcp",
39
+ "mcp-server",
39
40
  "model-context-protocol",
40
- "coinrithm",
41
+ "ai-agent",
42
+ "agent-trading",
43
+ "trading",
41
44
  "paper-trading",
42
- "agent",
43
45
  "crypto",
44
- "prediction-markets"
46
+ "futures",
47
+ "prediction-markets",
48
+ "polymarket",
49
+ "kalshi",
50
+ "claude",
51
+ "chatgpt",
52
+ "cursor",
53
+ "leaderboard",
54
+ "coinrithm"
45
55
  ],
46
56
  "license": "MIT",
47
57
  "dependencies": {