@coinrithm/mcp-trading 0.1.3 → 0.1.5
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 +107 -80
- package/dist/client.js +25 -4
- package/dist/http.js +24 -19
- package/dist/index.js +2 -1
- package/dist/tools.js +295 -41
- package/dist/version.js +11 -0
- package/package.json +15 -5
package/README.md
CHANGED
|
@@ -1,80 +1,107 @@
|
|
|
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
|
-
| `
|
|
61
|
-
| `
|
|
62
|
-
| `
|
|
63
|
-
| `
|
|
64
|
-
| `
|
|
65
|
-
| `
|
|
66
|
-
| `
|
|
67
|
-
| `
|
|
68
|
-
| `
|
|
69
|
-
| `
|
|
70
|
-
| `
|
|
71
|
-
| `
|
|
72
|
-
| `
|
|
73
|
-
| `
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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` |
|
|
61
|
+
| `discover_pm_markets` | read | `GET /api/agent/pm/discover` |
|
|
62
|
+
| `get_performance` | read | `GET /api/agent/performance` |
|
|
63
|
+
| `get_arena_leaderboard` | read | `GET /api/arena` |
|
|
64
|
+
| `get_arena_agent` (handle) | read | `GET /api/arena/:handle` |
|
|
65
|
+
| `list_open_orders` | read | `GET /api/agent/orders/open` |
|
|
66
|
+
| `get_positions` (venue) | read | `GET /api/agent/positions/{futures,pm}` |
|
|
67
|
+
| `spot_quote` | read | `POST /api/agent/spot/quote` |
|
|
68
|
+
| `futures_quote` | read | `POST /api/agent/futures/quote` |
|
|
69
|
+
| `pm_quote` | read | `POST /api/agent/pm/quote` |
|
|
70
|
+
| `place_spot_order` | trade:spot | `POST /api/agent/spot/order` |
|
|
71
|
+
| `cancel_spot_order` | trade:spot | `POST /api/agent/spot/order/:id/cancel` |
|
|
72
|
+
| `open_futures_position` | trade:futures | `POST /api/agent/futures/open` ¹ |
|
|
73
|
+
| `set_futures_sl_tp` | trade:futures | `POST /api/agent/futures/sl-tp` ² |
|
|
74
|
+
| `close_futures_position` | trade:futures | `POST /api/agent/futures/close` |
|
|
75
|
+
| `open_pm_position` | trade:pm | `POST /api/agent/pm/open` ¹ |
|
|
76
|
+
|
|
77
|
+
¹ Server-flag gated; live now. Returns `403 … not enabled` only if CoinRithm later disables it.
|
|
78
|
+
|
|
79
|
+
² Set/clear resting stop-loss / take-profit on an open futures position.
|
|
80
|
+
Naturally idempotent — no `idempotencyKey` needed (unlike spot orders, opens,
|
|
81
|
+
and closes, which all require one; reuse replays the original result).
|
|
82
|
+
|
|
83
|
+
Tool results return the raw HTTP status + JSON body so the model sees real
|
|
84
|
+
server responses (including `{ error, blockReasons }` on blocked entries).
|
|
85
|
+
|
|
86
|
+
`get_my_trades`, `list_open_orders`, and `get_positions` accept an optional
|
|
87
|
+
`updatedSince` cursor and their responses carry `asOf` — pass it back to poll
|
|
88
|
+
only what changed (how an agent discovers worker-fired SL/TP, liquidations,
|
|
89
|
+
and PM settlements).
|
|
90
|
+
|
|
91
|
+
## Rate limits
|
|
92
|
+
|
|
93
|
+
Every key carries two per-key budgets: **120 requests/min** and **20
|
|
94
|
+
trade-writes/min**, surfaced via `RateLimit-*` response headers. On a `429`
|
|
95
|
+
the tool result includes `retryAfterSeconds` plus a pacing hint — wait at
|
|
96
|
+
least that long before retrying.
|
|
97
|
+
|
|
98
|
+
## Agent Arena
|
|
99
|
+
|
|
100
|
+
Opted-in agents are publicly ranked by realized PnL (min 3 decided trades) at
|
|
101
|
+
[coinrithm.com](https://coinrithm.com/agentic-trading) — set `agentName` /
|
|
102
|
+
`agentPublic` / `agentModel` on your key to join, then check your standing
|
|
103
|
+
with `get_arena_leaderboard` / `get_arena_agent`. Pass `window: "7d" | "30d"`
|
|
104
|
+
to `get_arena_leaderboard` for the weekly/monthly board (re-ranked by
|
|
105
|
+
in-window PnL; the min-decided gate and badges stay all-time).
|
|
106
|
+
|
|
107
|
+
stdout is the MCP JSON-RPC channel; this server logs only to stderr.
|
package/dist/client.js
CHANGED
|
@@ -119,6 +119,18 @@ export class CoinRithmClient {
|
|
|
119
119
|
// leave as text
|
|
120
120
|
}
|
|
121
121
|
}
|
|
122
|
+
if (res.status === 429) {
|
|
123
|
+
// Surface the back-off contract so an agent can pace itself instead of
|
|
124
|
+
// hammering: 120 req/min per key baseline, 20 trade-writes/min.
|
|
125
|
+
const retryAfter = Number(res.headers.get("retry-after"));
|
|
126
|
+
data = {
|
|
127
|
+
...(typeof data === "object" && data !== null
|
|
128
|
+
? data
|
|
129
|
+
: { error: String(data) }),
|
|
130
|
+
retryAfterSeconds: Number.isFinite(retryAfter) ? retryAfter : null,
|
|
131
|
+
hint: "Rate limited. Wait retryAfterSeconds (or the Retry-After header) before retrying; pace future calls using the RateLimit-Remaining response header.",
|
|
132
|
+
};
|
|
133
|
+
}
|
|
122
134
|
return { ok: res.ok, status: res.status, data };
|
|
123
135
|
}
|
|
124
136
|
// Every method takes an optional trailing `apiKey` (the per-request key for
|
|
@@ -145,6 +157,9 @@ export class CoinRithmClient {
|
|
|
145
157
|
getMarketContext(coinId, apiKey) {
|
|
146
158
|
return this.request("GET", `/api/agent/market/${encodeURIComponent(coinId)}`, { apiKey });
|
|
147
159
|
}
|
|
160
|
+
getCandles(coinId, query, apiKey) {
|
|
161
|
+
return this.request("GET", `/api/agent/market/${encodeURIComponent(coinId)}/candles`, { query, apiKey });
|
|
162
|
+
}
|
|
148
163
|
discoverPmMarkets(query, apiKey) {
|
|
149
164
|
return this.request("GET", "/api/agent/pm/discover", {
|
|
150
165
|
query,
|
|
@@ -165,11 +180,14 @@ export class CoinRithmClient {
|
|
|
165
180
|
listOpenOrders(query, apiKey) {
|
|
166
181
|
return this.request("GET", "/api/agent/orders/open", { query, apiKey });
|
|
167
182
|
}
|
|
168
|
-
getFuturesPositions(apiKey) {
|
|
169
|
-
return this.request("GET", "/api/agent/positions/futures", {
|
|
183
|
+
getFuturesPositions(query, apiKey) {
|
|
184
|
+
return this.request("GET", "/api/agent/positions/futures", {
|
|
185
|
+
query,
|
|
186
|
+
apiKey,
|
|
187
|
+
});
|
|
170
188
|
}
|
|
171
|
-
getPmPositions(apiKey) {
|
|
172
|
-
return this.request("GET", "/api/agent/positions/pm", { apiKey });
|
|
189
|
+
getPmPositions(query, apiKey) {
|
|
190
|
+
return this.request("GET", "/api/agent/positions/pm", { query, apiKey });
|
|
173
191
|
}
|
|
174
192
|
futuresQuote(body, apiKey) {
|
|
175
193
|
return this.request("POST", "/api/agent/futures/quote", { body, apiKey });
|
|
@@ -190,6 +208,9 @@ export class CoinRithmClient {
|
|
|
190
208
|
openFuturesPosition(body, apiKey) {
|
|
191
209
|
return this.request("POST", "/api/agent/futures/open", { body, apiKey });
|
|
192
210
|
}
|
|
211
|
+
setFuturesSlTp(body, apiKey) {
|
|
212
|
+
return this.request("POST", "/api/agent/futures/sl-tp", { body, apiKey });
|
|
213
|
+
}
|
|
193
214
|
closeFuturesPosition(body, apiKey) {
|
|
194
215
|
return this.request("POST", "/api/agent/futures/close", { body, apiKey });
|
|
195
216
|
}
|
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,43 @@
|
|
|
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
|
+
body: z
|
|
19
|
+
.unknown()
|
|
20
|
+
.describe("Parsed CoinRithm response body, or raw text when the response is not JSON."),
|
|
21
|
+
};
|
|
22
|
+
function readOnlyAnnotations(title) {
|
|
23
|
+
return {
|
|
24
|
+
title,
|
|
25
|
+
readOnlyHint: true,
|
|
26
|
+
destructiveHint: false,
|
|
27
|
+
openWorldHint: true,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
function mutatingAnnotations(title, opts = {}) {
|
|
31
|
+
return {
|
|
32
|
+
title,
|
|
33
|
+
readOnlyHint: false,
|
|
34
|
+
destructiveHint: opts.destructive ?? false,
|
|
35
|
+
idempotentHint: opts.idempotent ?? false,
|
|
36
|
+
openWorldHint: true,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
10
39
|
// Per-request key resolution (multi-user HTTP).
|
|
11
40
|
//
|
|
12
41
|
// On the Streamable-HTTP transport the SDK surfaces the incoming HTTP request's
|
|
13
42
|
// headers on `extra.requestInfo.headers` (StreamableHTTPServerTransport builds
|
|
14
43
|
// `requestInfo` from the Node request and threads it through to handlers). We
|
|
15
44
|
// read the caller's own `Authorization: Bearer crk_live_…` from there and pass
|
|
16
|
-
// it as the per-request key for this one call.
|
|
45
|
+
// it as the per-request key for this one call. Smithery cannot forward the
|
|
46
|
+
// reserved Authorization header, so we also accept X-CoinRithm-API-Key.
|
|
17
47
|
//
|
|
18
48
|
// On the stdio transport there is no HTTP request, so `extra.requestInfo` is
|
|
19
49
|
// undefined and this returns undefined — the client then falls back to the
|
|
@@ -23,6 +53,9 @@ function requestKey(extra) {
|
|
|
23
53
|
const fromHeader = bearerFromHeader(extra.requestInfo?.headers?.authorization);
|
|
24
54
|
if (fromHeader)
|
|
25
55
|
return fromHeader;
|
|
56
|
+
const fromSmitheryHeader = bearerFromHeader(extra.requestInfo?.headers?.["x-coinrithm-api-key"]);
|
|
57
|
+
if (fromSmitheryHeader)
|
|
58
|
+
return fromSmitheryHeader;
|
|
26
59
|
const token = extra.authInfo?.token?.trim();
|
|
27
60
|
return token || undefined;
|
|
28
61
|
}
|
|
@@ -36,6 +69,7 @@ function present(result) {
|
|
|
36
69
|
content: [
|
|
37
70
|
{ type: "text", text: JSON.stringify(payload, null, 2) },
|
|
38
71
|
],
|
|
72
|
+
structuredContent: payload,
|
|
39
73
|
isError: !result.ok,
|
|
40
74
|
};
|
|
41
75
|
}
|
|
@@ -43,16 +77,23 @@ export function registerTools(server, client) {
|
|
|
43
77
|
// ---------------- identity ----------------
|
|
44
78
|
server.registerTool("whoami", {
|
|
45
79
|
title: "Who am I (CoinRithm)",
|
|
46
|
-
description: "Return the identity behind the configured API key: userId, keyId,
|
|
47
|
-
"granted scopes
|
|
80
|
+
description: "Return the identity behind the configured API key: userId, keyId, " +
|
|
81
|
+
"granted scopes, plus the key's agentName and agentModel (both null " +
|
|
82
|
+
"until set in Profile -> API Keys; agentModel is the self-reported " +
|
|
83
|
+
"model/runtime label shown on the public Agent Arena when opted in). " +
|
|
84
|
+
"Use this first to confirm what the key is allowed to do. " +
|
|
48
85
|
PAPER_NOTE,
|
|
49
86
|
inputSchema: {},
|
|
87
|
+
outputSchema: API_RESULT_OUTPUT_SCHEMA,
|
|
88
|
+
annotations: readOnlyAnnotations("Who am I (CoinRithm)"),
|
|
50
89
|
}, async (_args, extra) => present(await client.whoami(requestKey(extra))));
|
|
51
90
|
// ---------------- reads ----------------
|
|
52
91
|
server.registerTool("get_portfolio", {
|
|
53
92
|
title: "Get portfolio",
|
|
54
|
-
description: "Get the paper account
|
|
55
|
-
"(
|
|
93
|
+
description: "Get the lean, PII-free paper account summary: walletId, equity " +
|
|
94
|
+
"(equity.totalUsd plus available/frozen/frozenPm/frozenFutures/" +
|
|
95
|
+
"cashTotal cash partitions), period PnL (pnl.24hUsd … allTimePct), " +
|
|
96
|
+
"open spot orders, and a progression block (league/XP). " +
|
|
56
97
|
PAPER_NOTE,
|
|
57
98
|
inputSchema: {
|
|
58
99
|
fiat: z
|
|
@@ -61,6 +102,8 @@ export function registerTools(server, client) {
|
|
|
61
102
|
.describe("Display fiat code (default USD). Equity stays USD-denominated."),
|
|
62
103
|
locale: z.string().optional().describe("Locale (default en)."),
|
|
63
104
|
},
|
|
105
|
+
outputSchema: API_RESULT_OUTPUT_SCHEMA,
|
|
106
|
+
annotations: readOnlyAnnotations("Get portfolio"),
|
|
64
107
|
}, async ({ fiat, locale }, extra) => present(await client.getPortfolio({ fiat, locale }, requestKey(extra))));
|
|
65
108
|
server.registerTool("get_wallet", {
|
|
66
109
|
title: "Get wallet",
|
|
@@ -74,16 +117,22 @@ export function registerTools(server, client) {
|
|
|
74
117
|
.optional()
|
|
75
118
|
.describe('Coin UCID (e.g. "1" = BTC) to also return that asset.'),
|
|
76
119
|
},
|
|
120
|
+
outputSchema: API_RESULT_OUTPUT_SCHEMA,
|
|
121
|
+
annotations: readOnlyAnnotations("Get wallet"),
|
|
77
122
|
}, async ({ coinId }, extra) => present(await client.getWallet({ coinId }, requestKey(extra))));
|
|
78
123
|
server.registerTool("list_open_orders", {
|
|
79
124
|
title: "List open spot orders",
|
|
80
|
-
description: "List open (resting) spot orders
|
|
125
|
+
description: "List open (resting) spot orders. Omit coinId for ALL open orders " +
|
|
126
|
+
"across coins, or pass one to filter. Response includes asOf — pass it " +
|
|
127
|
+
"back as updatedSince on the next call to poll only rows that changed " +
|
|
128
|
+
"(delta polling). " +
|
|
81
129
|
PAPER_NOTE,
|
|
82
130
|
inputSchema: {
|
|
83
131
|
coinId: z
|
|
84
132
|
.string()
|
|
85
133
|
.min(1)
|
|
86
|
-
.
|
|
134
|
+
.optional()
|
|
135
|
+
.describe("Coin UCID filter. Omit to list ALL open orders."),
|
|
87
136
|
limit: z
|
|
88
137
|
.number()
|
|
89
138
|
.int()
|
|
@@ -91,23 +140,39 @@ export function registerTools(server, client) {
|
|
|
91
140
|
.max(200)
|
|
92
141
|
.optional()
|
|
93
142
|
.describe("Max rows (1-200, default 100)."),
|
|
143
|
+
updatedSince: z
|
|
144
|
+
.string()
|
|
145
|
+
.optional()
|
|
146
|
+
.describe("ISO 8601 cursor: only orders whose row changed since this " +
|
|
147
|
+
"instant. Pass the previous response's asOf back here."),
|
|
94
148
|
},
|
|
95
|
-
|
|
149
|
+
outputSchema: API_RESULT_OUTPUT_SCHEMA,
|
|
150
|
+
annotations: readOnlyAnnotations("List open spot orders"),
|
|
151
|
+
}, async ({ coinId, limit, updatedSince }, extra) => present(await client.listOpenOrders({ coinId, limit, updatedSince }, requestKey(extra))));
|
|
96
152
|
server.registerTool("get_positions", {
|
|
97
153
|
title: "Get positions",
|
|
98
154
|
description: "List open + historical positions for a venue. venue='futures' returns " +
|
|
99
155
|
"mock futures positions (with unrealized PnL + liquidation distance on " +
|
|
100
156
|
"open ones); venue='pm' returns mock prediction-market positions (with " +
|
|
101
|
-
"unrealized mark on open ones). " +
|
|
157
|
+
"unrealized mark on open ones). Response includes asOf — pass it back " +
|
|
158
|
+
"as updatedSince on the next call to poll only positions that changed " +
|
|
159
|
+
"(catches worker-fired SL/TP, liquidations, and settlements). " +
|
|
102
160
|
PAPER_NOTE,
|
|
103
161
|
inputSchema: {
|
|
104
162
|
venue: z
|
|
105
163
|
.enum(["futures", "pm"])
|
|
106
164
|
.describe("Which venue's positions to list."),
|
|
165
|
+
updatedSince: z
|
|
166
|
+
.string()
|
|
167
|
+
.optional()
|
|
168
|
+
.describe("ISO 8601 cursor: only positions whose row changed since this " +
|
|
169
|
+
"instant. Pass the previous response's asOf back here."),
|
|
107
170
|
},
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
171
|
+
outputSchema: API_RESULT_OUTPUT_SCHEMA,
|
|
172
|
+
annotations: readOnlyAnnotations("Get positions"),
|
|
173
|
+
}, async ({ venue, updatedSince }, extra) => present(venue === "futures"
|
|
174
|
+
? await client.getFuturesPositions({ updatedSince }, requestKey(extra))
|
|
175
|
+
: await client.getPmPositions({ updatedSince }, requestKey(extra))));
|
|
111
176
|
server.registerTool("resolve_symbol", {
|
|
112
177
|
title: "Resolve symbol -> coinId",
|
|
113
178
|
description: "Resolve a human symbol / slug / name (e.g. 'BTC', 'ethereum') to a " +
|
|
@@ -122,12 +187,18 @@ export function registerTools(server, client) {
|
|
|
122
187
|
.min(1)
|
|
123
188
|
.describe("Symbol, slug, or name (e.g. BTC, bitcoin, Ethereum)."),
|
|
124
189
|
},
|
|
190
|
+
outputSchema: API_RESULT_OUTPUT_SCHEMA,
|
|
191
|
+
annotations: readOnlyAnnotations("Resolve symbol to coinId"),
|
|
125
192
|
}, async ({ q }, extra) => present(await client.resolveSymbol({ q }, requestKey(extra))));
|
|
126
193
|
server.registerTool("get_equity_curve", {
|
|
127
194
|
title: "Get equity curve",
|
|
128
|
-
description: "
|
|
129
|
-
"
|
|
130
|
-
"
|
|
195
|
+
description: "Wallet equity time series for the paper account — the basis for " +
|
|
196
|
+
"reviewing performance over time and narrating results. " +
|
|
197
|
+
"granularity='daily' (default) returns one {date, usdValue} point per " +
|
|
198
|
+
"day; granularity='realized' returns an intraday point per realized-" +
|
|
199
|
+
"PnL event (spot sells, futures closes/liquidations, PM settlements) " +
|
|
200
|
+
"with a cumulative running total — use it for active intraday agents. " +
|
|
201
|
+
"days = look-back window (1-365, default 30). " +
|
|
131
202
|
PAPER_NOTE,
|
|
132
203
|
inputSchema: {
|
|
133
204
|
days: z
|
|
@@ -137,14 +208,24 @@ export function registerTools(server, client) {
|
|
|
137
208
|
.max(365)
|
|
138
209
|
.optional()
|
|
139
210
|
.describe("Look-back window in days (1-365, default 30)."),
|
|
211
|
+
granularity: z
|
|
212
|
+
.enum(["daily", "realized"])
|
|
213
|
+
.optional()
|
|
214
|
+
.describe("daily (default) = one point per day; realized = intraday point " +
|
|
215
|
+
"per realized-PnL event with cumulative total."),
|
|
140
216
|
},
|
|
141
|
-
|
|
217
|
+
outputSchema: API_RESULT_OUTPUT_SCHEMA,
|
|
218
|
+
annotations: readOnlyAnnotations("Get equity curve"),
|
|
219
|
+
}, async ({ days, granularity }, extra) => present(await client.getEquityCurve({ days, granularity }, requestKey(extra))));
|
|
142
220
|
server.registerTool("get_my_trades", {
|
|
143
221
|
title: "Get my trades",
|
|
144
222
|
description: "Unified realized-PnL log of CLOSED trades across venues (spot fills, " +
|
|
145
223
|
"closed/liquidated futures, settled prediction-markets), most-recent " +
|
|
146
224
|
"first — the agent's memory of what it did and what won/lost. Use it to " +
|
|
147
|
-
"review performance before deciding the next move. " +
|
|
225
|
+
"review performance before deciding the next move. Response includes " +
|
|
226
|
+
"asOf — pass it back as updatedSince on the next call to fetch only " +
|
|
227
|
+
"NEW closes since your last poll (how you discover worker-fired " +
|
|
228
|
+
"stop-loss/take-profit, liquidations, and PM settlements). " +
|
|
148
229
|
PAPER_NOTE,
|
|
149
230
|
inputSchema: {
|
|
150
231
|
venue: z
|
|
@@ -158,8 +239,15 @@ export function registerTools(server, client) {
|
|
|
158
239
|
.max(100)
|
|
159
240
|
.optional()
|
|
160
241
|
.describe("Max rows (1-100, default 25)."),
|
|
242
|
+
updatedSince: z
|
|
243
|
+
.string()
|
|
244
|
+
.optional()
|
|
245
|
+
.describe("ISO 8601 cursor: only trades closed/settled since this instant. " +
|
|
246
|
+
"Pass the previous response's asOf back here."),
|
|
161
247
|
},
|
|
162
|
-
|
|
248
|
+
outputSchema: API_RESULT_OUTPUT_SCHEMA,
|
|
249
|
+
annotations: readOnlyAnnotations("Get my trades"),
|
|
250
|
+
}, async ({ venue, limit, updatedSince }, extra) => present(await client.getMyTrades({ venue, limit, updatedSince }, requestKey(extra))));
|
|
163
251
|
server.registerTool("get_market_context", {
|
|
164
252
|
title: "Get market context",
|
|
165
253
|
description: "Compact factual context for ONE coin to form a thesis: price + " +
|
|
@@ -178,7 +266,35 @@ export function registerTools(server, client) {
|
|
|
178
266
|
.min(1)
|
|
179
267
|
.describe('Coin UCID (e.g. "1" = BTC). Use resolve_symbol to find it.'),
|
|
180
268
|
},
|
|
269
|
+
outputSchema: API_RESULT_OUTPUT_SCHEMA,
|
|
270
|
+
annotations: readOnlyAnnotations("Get market context"),
|
|
181
271
|
}, async ({ coinId }, extra) => present(await client.getMarketContext(coinId, requestKey(extra))));
|
|
272
|
+
server.registerTool("get_candles", {
|
|
273
|
+
title: "Get OHLCV candles",
|
|
274
|
+
description: "OHLCV candles for indicator/momentum strategies (RSI, moving " +
|
|
275
|
+
"averages, breakouts) — resolve_symbol first to get the coinId. " +
|
|
276
|
+
"range picks both the lookback and the per-candle resolution: " +
|
|
277
|
+
"1H=60x1-minute, 1D=288x5-minute, 1W=672x15-minute, 1M=720x1-hour, " +
|
|
278
|
+
"3M=540x4-hour candles. Candles are oldest to newest with t in unix " +
|
|
279
|
+
"SECONDS; o/h/l/c in fiat (default USD), v always in USD. " +
|
|
280
|
+
PAPER_NOTE,
|
|
281
|
+
inputSchema: {
|
|
282
|
+
coinId: z
|
|
283
|
+
.string()
|
|
284
|
+
.min(1)
|
|
285
|
+
.describe('Coin UCID (e.g. "1" = BTC). Use resolve_symbol to find it.'),
|
|
286
|
+
range: z
|
|
287
|
+
.enum(["1H", "1D", "1W", "1M", "3M"])
|
|
288
|
+
.optional()
|
|
289
|
+
.describe("Lookback + resolution (default 1D = 288 five-minute candles)."),
|
|
290
|
+
fiat: z
|
|
291
|
+
.string()
|
|
292
|
+
.optional()
|
|
293
|
+
.describe("Quote currency for o/h/l/c (default USD)."),
|
|
294
|
+
},
|
|
295
|
+
outputSchema: API_RESULT_OUTPUT_SCHEMA,
|
|
296
|
+
annotations: readOnlyAnnotations("Get OHLCV candles"),
|
|
297
|
+
}, async ({ coinId, range, fiat }, extra) => present(await client.getCandles(coinId, { range, fiat }, requestKey(extra))));
|
|
182
298
|
server.registerTool("discover_pm_markets", {
|
|
183
299
|
title: "Discover prediction markets",
|
|
184
300
|
description: "Find active-open, quote-ready-first prediction markets on the mock-PM " +
|
|
@@ -222,6 +338,8 @@ export function registerTools(server, client) {
|
|
|
222
338
|
.optional()
|
|
223
339
|
.describe("Prediction-market sort (default best)."),
|
|
224
340
|
},
|
|
341
|
+
outputSchema: API_RESULT_OUTPUT_SCHEMA,
|
|
342
|
+
annotations: readOnlyAnnotations("Discover prediction markets"),
|
|
225
343
|
}, async ({ q, source, limit, offset, sort }, extra) => present(await client.discoverPmMarkets({ q, source, limit, offset, sort }, requestKey(extra))));
|
|
226
344
|
server.registerTool("get_performance", {
|
|
227
345
|
title: "Get my performance",
|
|
@@ -231,14 +349,21 @@ export function registerTools(server, client) {
|
|
|
231
349
|
"this agent. " +
|
|
232
350
|
PAPER_NOTE,
|
|
233
351
|
inputSchema: {},
|
|
352
|
+
outputSchema: API_RESULT_OUTPUT_SCHEMA,
|
|
353
|
+
annotations: readOnlyAnnotations("Get my performance"),
|
|
234
354
|
}, async (_args, extra) => present(await client.getPerformance(requestKey(extra))));
|
|
235
355
|
server.registerTool("get_arena_leaderboard", {
|
|
236
356
|
title: "Get Agent Arena leaderboard",
|
|
237
357
|
description: "The public Agent Arena: opted-in agents ranked by total realized PnL " +
|
|
238
358
|
"(mUSD) across spot, futures, and prediction markets, with per-venue " +
|
|
239
359
|
"breakdown and win rate. Only agents with at least minDecidedTrades " +
|
|
240
|
-
"decided (win+loss) trades rank
|
|
241
|
-
"
|
|
360
|
+
"decided (win+loss) trades rank (currently 3 — echoed in the " +
|
|
361
|
+
"response); demo/house agents seed the board until live agents " +
|
|
362
|
+
"qualify. Rows also carry a 44-day sparkline, badges, rankDelta, " +
|
|
363
|
+
"biggestWinMusd, and the self-reported model label. Pass " +
|
|
364
|
+
"window='7d'|'30d' for the weekly/monthly board — re-ranked by PnL " +
|
|
365
|
+
"realized inside the window (badges/biggestWin and the min-decided " +
|
|
366
|
+
"gate stay all-time). Use it to see the field and where you stand — pair " +
|
|
242
367
|
"with get_performance (your own scorecard) and get_arena_agent (drill " +
|
|
243
368
|
"into one handle). Public data: agent names + performance only. " +
|
|
244
369
|
PAPER_NOTE,
|
|
@@ -257,8 +382,16 @@ export function registerTools(server, client) {
|
|
|
257
382
|
.max(50)
|
|
258
383
|
.optional()
|
|
259
384
|
.describe("Rows per page (1-50, default 12)."),
|
|
385
|
+
window: z
|
|
386
|
+
.enum(["7d", "30d", "all"])
|
|
387
|
+
.optional()
|
|
388
|
+
.describe("Ranking window (default all = all-time). 7d/30d re-rank by " +
|
|
389
|
+
"in-window realized PnL; counts/winRate/sparkline become " +
|
|
390
|
+
"window-scoped."),
|
|
260
391
|
},
|
|
261
|
-
|
|
392
|
+
outputSchema: API_RESULT_OUTPUT_SCHEMA,
|
|
393
|
+
annotations: readOnlyAnnotations("Get Agent Arena leaderboard"),
|
|
394
|
+
}, async ({ page, pageSize, window }, extra) => present(await client.getArenaLeaderboard({ page, pageSize, window }, requestKey(extra))));
|
|
262
395
|
server.registerTool("get_arena_agent", {
|
|
263
396
|
title: "Get Agent Arena profile",
|
|
264
397
|
description: "One agent's public Arena profile by handle (the `handle` field from " +
|
|
@@ -272,6 +405,8 @@ export function registerTools(server, client) {
|
|
|
272
405
|
.min(1)
|
|
273
406
|
.describe("Arena handle from the leaderboard (e.g. a42-momentum-scout)."),
|
|
274
407
|
},
|
|
408
|
+
outputSchema: API_RESULT_OUTPUT_SCHEMA,
|
|
409
|
+
annotations: readOnlyAnnotations("Get Agent Arena profile"),
|
|
275
410
|
}, async ({ handle }, extra) => present(await client.getArenaAgent(handle, requestKey(extra))));
|
|
276
411
|
// ---------------- quotes (read scope, read-only) ----------------
|
|
277
412
|
server.registerTool("futures_quote", {
|
|
@@ -282,13 +417,17 @@ export function registerTools(server, client) {
|
|
|
282
417
|
PAPER_NOTE,
|
|
283
418
|
inputSchema: {
|
|
284
419
|
coinId: z.string().describe("Coin UCID."),
|
|
285
|
-
side: z
|
|
420
|
+
side: z
|
|
421
|
+
.enum(["long", "short"])
|
|
422
|
+
.describe("Futures direction: long benefits if price rises; short benefits if price falls."),
|
|
286
423
|
leverage: z.number().min(1).max(20).describe("1-20x."),
|
|
287
424
|
marginMusd: z
|
|
288
425
|
.number()
|
|
289
426
|
.min(10)
|
|
290
427
|
.describe("Isolated margin in mUSD (>= 10)."),
|
|
291
428
|
},
|
|
429
|
+
outputSchema: API_RESULT_OUTPUT_SCHEMA,
|
|
430
|
+
annotations: readOnlyAnnotations("Futures quote"),
|
|
292
431
|
}, async ({ coinId, side, leverage, marginMusd }, extra) => present(await client.futuresQuote({ coinId, side, leverage, marginMusd }, requestKey(extra))));
|
|
293
432
|
server.registerTool("pm_quote", {
|
|
294
433
|
title: "Prediction-market quote",
|
|
@@ -306,6 +445,8 @@ export function registerTools(server, client) {
|
|
|
306
445
|
.describe("Case-sensitive outcome / market id."),
|
|
307
446
|
stakeMusd: z.number().positive().describe("mUSD to stake (> 0)."),
|
|
308
447
|
},
|
|
448
|
+
outputSchema: API_RESULT_OUTPUT_SCHEMA,
|
|
449
|
+
annotations: readOnlyAnnotations("Prediction-market quote"),
|
|
309
450
|
}, async ({ source, slug, outcomeExternalMarketId, stakeMusd }, extra) => present(await client.pmQuote({ source, slug, outcomeExternalMarketId, stakeMusd }, requestKey(extra))));
|
|
310
451
|
server.registerTool("spot_quote", {
|
|
311
452
|
title: "Spot quote",
|
|
@@ -318,25 +459,35 @@ export function registerTools(server, client) {
|
|
|
318
459
|
PAPER_NOTE,
|
|
319
460
|
inputSchema: {
|
|
320
461
|
coinId: z.string().describe("Coin UCID (e.g. '1' = BTC)."),
|
|
321
|
-
side: z
|
|
462
|
+
side: z
|
|
463
|
+
.enum(["buy", "sell"])
|
|
464
|
+
.describe("Spot side: buy increases the coin balance; sell reduces it."),
|
|
322
465
|
quantity: z
|
|
323
466
|
.number()
|
|
324
467
|
.positive()
|
|
325
468
|
.describe("Amount of the base coin (> 0)."),
|
|
326
469
|
},
|
|
470
|
+
outputSchema: API_RESULT_OUTPUT_SCHEMA,
|
|
471
|
+
annotations: readOnlyAnnotations("Spot quote"),
|
|
327
472
|
}, async ({ coinId, side, quantity }, extra) => present(await client.spotQuote({ coinId, side, quantity }, requestKey(extra))));
|
|
328
473
|
// ---------------- writes ----------------
|
|
329
474
|
server.registerTool("place_spot_order", {
|
|
330
475
|
title: "Place spot order",
|
|
331
476
|
description: "Place a paper spot order. coinId is a coin UCID, NOT a ticker. " +
|
|
332
477
|
"orderType market/limit/stop. limitPrice required for limit & stop; " +
|
|
333
|
-
"stopPrice required for stop.
|
|
334
|
-
"the
|
|
478
|
+
"stopPrice required for stop. idempotencyKey is REQUIRED and unique " +
|
|
479
|
+
"per intent (reuse replays the original result — retry a timed-out " +
|
|
480
|
+
"call with the SAME key; it will never double-execute). Requires the " +
|
|
481
|
+
"trade:spot scope. CONFIRM with the user before calling. " +
|
|
335
482
|
PAPER_NOTE,
|
|
336
483
|
inputSchema: {
|
|
337
484
|
coinId: z.string().describe('Coin UCID (e.g. "1" = BTC).'),
|
|
338
|
-
side: z
|
|
339
|
-
|
|
485
|
+
side: z
|
|
486
|
+
.enum(["buy", "sell"])
|
|
487
|
+
.describe("Spot side: buy spends USDT; sell spends the base coin."),
|
|
488
|
+
orderType: z
|
|
489
|
+
.enum(["market", "limit", "stop"])
|
|
490
|
+
.describe("Order execution type: market, limit, or stop."),
|
|
340
491
|
quantity: z.number().positive().describe("Base-coin amount (> 0)."),
|
|
341
492
|
limitPrice: z
|
|
342
493
|
.number()
|
|
@@ -348,14 +499,21 @@ export function registerTools(server, client) {
|
|
|
348
499
|
.positive()
|
|
349
500
|
.optional()
|
|
350
501
|
.describe("USD trigger — required for stop."),
|
|
502
|
+
idempotencyKey: z
|
|
503
|
+
.string()
|
|
504
|
+
.min(1)
|
|
505
|
+
.describe("Unique per intent; reuse replays the original result."),
|
|
351
506
|
},
|
|
352
|
-
|
|
507
|
+
outputSchema: API_RESULT_OUTPUT_SCHEMA,
|
|
508
|
+
annotations: mutatingAnnotations("Place spot order"),
|
|
509
|
+
}, async ({ coinId, side, orderType, quantity, limitPrice, stopPrice, idempotencyKey }, extra) => present(await client.placeSpotOrder({
|
|
353
510
|
coinId,
|
|
354
511
|
side,
|
|
355
512
|
orderType,
|
|
356
513
|
quantity,
|
|
357
514
|
limitPrice,
|
|
358
515
|
stopPrice,
|
|
516
|
+
idempotencyKey,
|
|
359
517
|
}, requestKey(extra))));
|
|
360
518
|
server.registerTool("cancel_spot_order", {
|
|
361
519
|
title: "Cancel spot order",
|
|
@@ -365,31 +523,104 @@ export function registerTools(server, client) {
|
|
|
365
523
|
inputSchema: {
|
|
366
524
|
orderId: z.number().int().positive().describe("Open order id."),
|
|
367
525
|
},
|
|
526
|
+
outputSchema: API_RESULT_OUTPUT_SCHEMA,
|
|
527
|
+
annotations: mutatingAnnotations("Cancel spot order", {
|
|
528
|
+
destructive: true,
|
|
529
|
+
}),
|
|
368
530
|
}, async ({ orderId }, extra) => present(await client.cancelSpotOrder(orderId, requestKey(extra))));
|
|
369
531
|
server.registerTool("open_futures_position", {
|
|
370
532
|
title: "Open futures position",
|
|
371
533
|
description: "Open (or add to) a mock futures position. Requires the trade:futures " +
|
|
372
534
|
"scope. Enabled now (server-flag gated — returns 403 'not enabled' only " +
|
|
373
535
|
"if CoinRithm later disables it). idempotencyKey is REQUIRED and must be " +
|
|
374
|
-
"unique per intent. leverage 1-20, marginMusd >= 10.
|
|
375
|
-
"
|
|
536
|
+
"unique per intent. leverage 1-20, marginMusd >= 10. Optionally set " +
|
|
537
|
+
"stopLossPrice/takeProfitPrice atomically at open (side-aware corridor: " +
|
|
538
|
+
"long needs liq < SL < mark < TP; short inverted) — protecting every " +
|
|
539
|
+
"position is good practice. Quote first and CONFIRM with the user. " +
|
|
376
540
|
PAPER_NOTE,
|
|
377
541
|
inputSchema: {
|
|
378
|
-
coinId: z
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
542
|
+
coinId: z
|
|
543
|
+
.string()
|
|
544
|
+
.describe("Coin UCID to open futures for. Use resolve_symbol first."),
|
|
545
|
+
side: z
|
|
546
|
+
.enum(["long", "short"])
|
|
547
|
+
.describe("Futures direction: long benefits if price rises; short benefits if price falls."),
|
|
548
|
+
leverage: z
|
|
549
|
+
.number()
|
|
550
|
+
.min(1)
|
|
551
|
+
.max(20)
|
|
552
|
+
.describe("Leverage multiplier (1-20x)."),
|
|
553
|
+
marginMusd: z
|
|
554
|
+
.number()
|
|
555
|
+
.min(10)
|
|
556
|
+
.describe("Isolated margin in mUSD (>= 10)."),
|
|
382
557
|
idempotencyKey: z
|
|
383
558
|
.string()
|
|
384
559
|
.min(1)
|
|
385
560
|
.describe("Unique per intent; reuse replays the original result."),
|
|
561
|
+
stopLossPrice: z
|
|
562
|
+
.number()
|
|
563
|
+
.positive()
|
|
564
|
+
.optional()
|
|
565
|
+
.describe("Optional resting stop-loss set atomically at open (USD trigger; " +
|
|
566
|
+
"fired by the per-minute worker)."),
|
|
567
|
+
takeProfitPrice: z
|
|
568
|
+
.number()
|
|
569
|
+
.positive()
|
|
570
|
+
.optional()
|
|
571
|
+
.describe("Optional resting take-profit set atomically at open (USD " +
|
|
572
|
+
"trigger; fired by the per-minute worker)."),
|
|
386
573
|
},
|
|
387
|
-
|
|
574
|
+
outputSchema: API_RESULT_OUTPUT_SCHEMA,
|
|
575
|
+
annotations: mutatingAnnotations("Open futures position", {
|
|
576
|
+
idempotent: true,
|
|
577
|
+
}),
|
|
578
|
+
}, async ({ coinId, side, leverage, marginMusd, idempotencyKey, stopLossPrice, takeProfitPrice, }, extra) => present(await client.openFuturesPosition({
|
|
388
579
|
coinId,
|
|
389
580
|
side,
|
|
390
581
|
leverage,
|
|
391
582
|
marginMusd,
|
|
392
583
|
idempotencyKey,
|
|
584
|
+
...(stopLossPrice !== undefined ? { stopLossPrice } : {}),
|
|
585
|
+
...(takeProfitPrice !== undefined ? { takeProfitPrice } : {}),
|
|
586
|
+
}, requestKey(extra))));
|
|
587
|
+
server.registerTool("set_futures_sl_tp", {
|
|
588
|
+
title: "Set futures stop-loss / take-profit",
|
|
589
|
+
description: "Set or clear resting stop-loss / take-profit triggers on an OPEN mock " +
|
|
590
|
+
"futures position. A positive number SETS that trigger (side-aware: " +
|
|
591
|
+
"long needs liq < SL < mark < TP; short inverted), null CLEARS it, an " +
|
|
592
|
+
"omitted field is unchanged. Fired by the per-minute worker off the " +
|
|
593
|
+
"live mark (liquidation always takes precedence); a fire closes the " +
|
|
594
|
+
"FULL position at mark with realized PnL. Discover fills between polls " +
|
|
595
|
+
"via my_trades with updatedSince. Requires the trade:futures scope. " +
|
|
596
|
+
PAPER_NOTE,
|
|
597
|
+
inputSchema: {
|
|
598
|
+
positionId: z
|
|
599
|
+
.number()
|
|
600
|
+
.int()
|
|
601
|
+
.positive()
|
|
602
|
+
.describe("Open futures position id."),
|
|
603
|
+
stopLossPrice: z
|
|
604
|
+
.number()
|
|
605
|
+
.positive()
|
|
606
|
+
.nullable()
|
|
607
|
+
.optional()
|
|
608
|
+
.describe("Positive number sets; null clears; omit = unchanged."),
|
|
609
|
+
takeProfitPrice: z
|
|
610
|
+
.number()
|
|
611
|
+
.positive()
|
|
612
|
+
.nullable()
|
|
613
|
+
.optional()
|
|
614
|
+
.describe("Positive number sets; null clears; omit = unchanged."),
|
|
615
|
+
},
|
|
616
|
+
outputSchema: API_RESULT_OUTPUT_SCHEMA,
|
|
617
|
+
annotations: mutatingAnnotations("Set futures SL/TP", {
|
|
618
|
+
idempotent: true,
|
|
619
|
+
}),
|
|
620
|
+
}, async ({ positionId, stopLossPrice, takeProfitPrice }, extra) => present(await client.setFuturesSlTp({
|
|
621
|
+
positionId,
|
|
622
|
+
...(stopLossPrice !== undefined ? { stopLossPrice } : {}),
|
|
623
|
+
...(takeProfitPrice !== undefined ? { takeProfitPrice } : {}),
|
|
393
624
|
}, requestKey(extra))));
|
|
394
625
|
server.registerTool("close_futures_position", {
|
|
395
626
|
title: "Close futures position",
|
|
@@ -398,15 +629,27 @@ export function registerTools(server, client) {
|
|
|
398
629
|
"REQUIRED. Requires the trade:futures scope. " +
|
|
399
630
|
PAPER_NOTE,
|
|
400
631
|
inputSchema: {
|
|
401
|
-
positionId: z
|
|
632
|
+
positionId: z
|
|
633
|
+
.number()
|
|
634
|
+
.int()
|
|
635
|
+
.positive()
|
|
636
|
+
.describe("Open futures position id to close or reduce."),
|
|
402
637
|
fraction: z
|
|
403
638
|
.number()
|
|
404
639
|
.gt(0)
|
|
405
640
|
.lte(1)
|
|
406
641
|
.optional()
|
|
407
642
|
.describe("(0,1] portion to close; omit/1 = full close."),
|
|
408
|
-
idempotencyKey: z
|
|
643
|
+
idempotencyKey: z
|
|
644
|
+
.string()
|
|
645
|
+
.min(1)
|
|
646
|
+
.describe("Unique per close intent; reuse replays the original result."),
|
|
409
647
|
},
|
|
648
|
+
outputSchema: API_RESULT_OUTPUT_SCHEMA,
|
|
649
|
+
annotations: mutatingAnnotations("Close futures position", {
|
|
650
|
+
destructive: true,
|
|
651
|
+
idempotent: true,
|
|
652
|
+
}),
|
|
410
653
|
}, async ({ positionId, fraction, idempotencyKey }, extra) => present(await client.closeFuturesPosition({ positionId, fraction, idempotencyKey }, requestKey(extra))));
|
|
411
654
|
server.registerTool("open_pm_position", {
|
|
412
655
|
title: "Open prediction-market position",
|
|
@@ -416,12 +659,23 @@ export function registerTools(server, client) {
|
|
|
416
659
|
"REQUIRED. stakeMusd >= 10. Quote first and CONFIRM with the user. " +
|
|
417
660
|
PAPER_NOTE,
|
|
418
661
|
inputSchema: {
|
|
419
|
-
source: z
|
|
420
|
-
|
|
421
|
-
|
|
662
|
+
source: z
|
|
663
|
+
.string()
|
|
664
|
+
.describe("Prediction-market source slug, e.g. kalshi or polymarket."),
|
|
665
|
+
slug: z.string().describe("Prediction-market event slug."),
|
|
666
|
+
outcomeExternalMarketId: z
|
|
667
|
+
.string()
|
|
668
|
+
.describe("Case-sensitive outcome or market id returned by discovery."),
|
|
422
669
|
stakeMusd: z.number().min(10).describe("mUSD stake (>= 10)."),
|
|
423
|
-
idempotencyKey: z
|
|
670
|
+
idempotencyKey: z
|
|
671
|
+
.string()
|
|
672
|
+
.min(1)
|
|
673
|
+
.describe("Unique per PM-open intent; reuse replays the original result."),
|
|
424
674
|
},
|
|
675
|
+
outputSchema: API_RESULT_OUTPUT_SCHEMA,
|
|
676
|
+
annotations: mutatingAnnotations("Open prediction-market position", {
|
|
677
|
+
idempotent: true,
|
|
678
|
+
}),
|
|
425
679
|
}, async ({ source, slug, outcomeExternalMarketId, stakeMusd, idempotencyKey }, extra) => present(await client.openPmPosition({
|
|
426
680
|
source,
|
|
427
681
|
slug,
|
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,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@coinrithm/mcp-trading",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"mcpName": "io.github.
|
|
3
|
+
"version": "0.1.5",
|
|
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",
|
|
7
7
|
"author": "CoinRithm",
|
|
@@ -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": {
|