@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 +128 -73
- package/dist/client.js +121 -26
- package/dist/http.js +24 -19
- package/dist/index.js +2 -1
- package/dist/tools.js +442 -56
- package/dist/version.js +11 -0
- package/package.json +14 -4
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
|
-
| `
|
|
73
|
-
| `
|
|
74
|
-
|
|
75
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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", {
|
|
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
|
-
|
|
134
|
-
return this.request("GET", "/api/agent/
|
|
180
|
+
resolveSymbol(query, apiKey, agentTrace) {
|
|
181
|
+
return this.request("GET", "/api/agent/resolve", {
|
|
182
|
+
query,
|
|
183
|
+
apiKey,
|
|
184
|
+
agentTrace,
|
|
185
|
+
});
|
|
135
186
|
}
|
|
136
|
-
|
|
137
|
-
return this.request("GET", "/api/agent/
|
|
187
|
+
getEquityCurve(query, apiKey, agentTrace) {
|
|
188
|
+
return this.request("GET", "/api/agent/equity-curve", {
|
|
189
|
+
query,
|
|
190
|
+
apiKey,
|
|
191
|
+
agentTrace,
|
|
192
|
+
});
|
|
138
193
|
}
|
|
139
|
-
|
|
140
|
-
return this.request("GET", "/api/agent/
|
|
194
|
+
getMyTrades(query, apiKey, agentTrace) {
|
|
195
|
+
return this.request("GET", "/api/agent/trades", {
|
|
196
|
+
query,
|
|
197
|
+
apiKey,
|
|
198
|
+
agentTrace,
|
|
199
|
+
});
|
|
141
200
|
}
|
|
142
|
-
|
|
143
|
-
return this.request("GET",
|
|
201
|
+
getMarketContext(coinId, apiKey, agentTrace) {
|
|
202
|
+
return this.request("GET", `/api/agent/market/${encodeURIComponent(coinId)}`, { apiKey, agentTrace });
|
|
144
203
|
}
|
|
145
|
-
|
|
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", {
|
|
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", {
|
|
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", {
|
|
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", {
|
|
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`, {
|
|
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
|
-
//
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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
|
-
|
|
64
|
-
|
|
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:
|
|
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,
|
|
47
|
-
"granted scopes
|
|
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
|
-
|
|
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
|
|
55
|
-
"(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
.
|
|
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
|
-
|
|
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
|
-
|
|
109
|
-
|
|
110
|
-
|
|
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
|
-
|
|
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: "
|
|
129
|
-
"
|
|
130
|
-
"
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
241
|
-
"
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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.
|
|
334
|
-
"the
|
|
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
|
|
339
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
375
|
-
"
|
|
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
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
420
|
-
|
|
421
|
-
|
|
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
|
|
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
|
-
|
|
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
|
}
|
package/dist/version.js
ADDED
|
@@ -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.
|
|
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
|
-
"
|
|
41
|
+
"ai-agent",
|
|
42
|
+
"agent-trading",
|
|
43
|
+
"trading",
|
|
41
44
|
"paper-trading",
|
|
42
|
-
"agent",
|
|
43
45
|
"crypto",
|
|
44
|
-
"
|
|
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": {
|