@coinrithm/mcp-trading 0.1.0
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 +96 -0
- package/dist/client.js +193 -0
- package/dist/http.js +99 -0
- package/dist/index.js +32 -0
- package/dist/tools.js +387 -0
- package/package.json +56 -0
package/README.md
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
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_performance` | read | `GET /api/agent/performance` |
|
|
61
|
+
| `get_arena_leaderboard` | read | `GET /api/arena` |
|
|
62
|
+
| `get_arena_agent` (handle) | read | `GET /api/arena/:handle` |
|
|
63
|
+
| `list_open_orders` | read | `GET /api/agent/orders/open` |
|
|
64
|
+
| `get_positions` (venue) | read | `GET /api/agent/positions/{futures,pm}` |
|
|
65
|
+
| `spot_quote` | read | `POST /api/agent/spot/quote` |
|
|
66
|
+
| `futures_quote` | read | `POST /api/agent/futures/quote` |
|
|
67
|
+
| `pm_quote` | read | `POST /api/agent/pm/quote` |
|
|
68
|
+
| `place_spot_order` | trade:spot | `POST /api/agent/spot/order` |
|
|
69
|
+
| `cancel_spot_order` | trade:spot | `POST /api/agent/spot/order/:id/cancel` |
|
|
70
|
+
| `open_futures_position` | trade:futures | `POST /api/agent/futures/open` ¹ |
|
|
71
|
+
| `close_futures_position` | trade:futures | `POST /api/agent/futures/close` |
|
|
72
|
+
| `open_pm_position` | trade:pm | `POST /api/agent/pm/open` ¹ |
|
|
73
|
+
|
|
74
|
+
¹ Server-flag gated; live now. Returns `403 … not enabled` only if CoinRithm later disables it.
|
|
75
|
+
|
|
76
|
+
Tool results return the raw HTTP status + JSON body so the model sees real
|
|
77
|
+
server responses (including `{ error, blockReasons }` on blocked entries).
|
|
78
|
+
|
|
79
|
+
stdout is the MCP JSON-RPC channel; this server logs only to stderr.
|
|
80
|
+
|
|
81
|
+
## Publishing (maintainers)
|
|
82
|
+
|
|
83
|
+
Published to npm as **`@coinrithm/mcp-trading`** (public scope) so users can run
|
|
84
|
+
`npx -y @coinrithm/mcp-trading` without cloning.
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
cd packages/mcp-trading
|
|
88
|
+
npm run build
|
|
89
|
+
npm pack --dry-run # verify the tarball: dist/*.js + README.md + package.json only
|
|
90
|
+
npm publish --access public
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
`prepare` rebuilds on publish and `publishConfig.access` is `public`, so once you
|
|
94
|
+
are authenticated (`npm login`) with publish rights on the `@coinrithm` org,
|
|
95
|
+
`npm publish` is enough. The first publish requires the `@coinrithm` npm org/scope
|
|
96
|
+
to exist and a one-time OTP if 2FA is enabled on the account.
|
package/dist/client.js
ADDED
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
// Thin HTTP client for the CoinRithm agent surface (/api/agent/*).
|
|
2
|
+
//
|
|
3
|
+
// Auth: a user-minted API key (crk_live_…) passed as `Authorization: Bearer …`.
|
|
4
|
+
//
|
|
5
|
+
// Two auth modes:
|
|
6
|
+
// - stdio (single-user, local): the key comes from COINRITHM_API_KEY at
|
|
7
|
+
// startup and is the client's default for every request.
|
|
8
|
+
// - Streamable HTTP (multi-user, hosted at mcp.coinrithm.com): each request
|
|
9
|
+
// carries the *caller's own* key in its Authorization header. The HTTP entry
|
|
10
|
+
// reads that header per request and passes it to every client call as a
|
|
11
|
+
// per-request override, so one shared server serves many users without a
|
|
12
|
+
// global key. COINRITHM_API_KEY is NOT used (and need not be set) in this mode.
|
|
13
|
+
//
|
|
14
|
+
// Config comes from the environment:
|
|
15
|
+
// - COINRITHM_API_KEY (stdio only) — the crk_live_… key.
|
|
16
|
+
// - COINRITHM_API_URL (optional) — base URL; defaults to production.
|
|
17
|
+
//
|
|
18
|
+
// IMPORTANT: this module must NEVER write to stdout (stdout is the MCP JSON-RPC
|
|
19
|
+
// channel). All diagnostics go to stderr via the logger below.
|
|
20
|
+
export const DEFAULT_BASE_URL = "https://api.coinrithm.com";
|
|
21
|
+
export function log(...args) {
|
|
22
|
+
// stderr only — stdout is reserved for the MCP protocol.
|
|
23
|
+
// eslint-disable-next-line no-console
|
|
24
|
+
console.error("[coinrithm-mcp]", ...args);
|
|
25
|
+
}
|
|
26
|
+
function resolveBaseUrl() {
|
|
27
|
+
return (process.env.COINRITHM_API_URL?.trim() || DEFAULT_BASE_URL).replace(/\/+$/, "");
|
|
28
|
+
}
|
|
29
|
+
// stdio path: a startup key is REQUIRED (single user).
|
|
30
|
+
export function loadConfig() {
|
|
31
|
+
const apiKey = process.env.COINRITHM_API_KEY?.trim();
|
|
32
|
+
if (!apiKey) {
|
|
33
|
+
throw new Error("COINRITHM_API_KEY is not set. Mint a key in CoinRithm → Profile → API Keys " +
|
|
34
|
+
"and expose it to this MCP server as the COINRITHM_API_KEY environment variable.");
|
|
35
|
+
}
|
|
36
|
+
if (!apiKey.startsWith("crk_live_")) {
|
|
37
|
+
log("warning: COINRITHM_API_KEY does not start with 'crk_live_' — it will likely be rejected.");
|
|
38
|
+
}
|
|
39
|
+
return { apiKey, baseUrl: resolveBaseUrl() };
|
|
40
|
+
}
|
|
41
|
+
// HTTP (multi-user) path: NO startup key. Each request brings its own.
|
|
42
|
+
// COINRITHM_API_KEY is intentionally ignored here even if present.
|
|
43
|
+
export function loadHttpConfig() {
|
|
44
|
+
return { baseUrl: resolveBaseUrl() };
|
|
45
|
+
}
|
|
46
|
+
// Extract a bare crk_live_… token from an incoming `Authorization` header value.
|
|
47
|
+
// Accepts "Bearer <key>" (case-insensitive scheme) or a raw key. Header values
|
|
48
|
+
// from the SDK's IsomorphicHeaders may be string | string[] | undefined.
|
|
49
|
+
export function bearerFromHeader(value) {
|
|
50
|
+
const raw = (Array.isArray(value) ? value[0] : value)?.trim();
|
|
51
|
+
if (!raw)
|
|
52
|
+
return undefined;
|
|
53
|
+
const m = /^bearer\s+(.+)$/i.exec(raw);
|
|
54
|
+
const token = (m ? m[1] : raw).trim();
|
|
55
|
+
return token || undefined;
|
|
56
|
+
}
|
|
57
|
+
export class CoinRithmClient {
|
|
58
|
+
// Default key for the stdio (single-user) path. Undefined in the multi-user
|
|
59
|
+
// HTTP path, where every call must pass a per-request `apiKey` override.
|
|
60
|
+
defaultApiKey;
|
|
61
|
+
baseUrl;
|
|
62
|
+
constructor(config) {
|
|
63
|
+
this.defaultApiKey = config.apiKey;
|
|
64
|
+
this.baseUrl = config.baseUrl;
|
|
65
|
+
}
|
|
66
|
+
async request(method, path, opts = {}) {
|
|
67
|
+
const apiKey = opts.apiKey ?? this.defaultApiKey;
|
|
68
|
+
if (!apiKey) {
|
|
69
|
+
return {
|
|
70
|
+
ok: false,
|
|
71
|
+
status: 401,
|
|
72
|
+
data: {
|
|
73
|
+
error: "missing_api_key",
|
|
74
|
+
message: "No API key for this request. On the hosted MCP, send " +
|
|
75
|
+
"`Authorization: Bearer crk_live_…` with your own key.",
|
|
76
|
+
},
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
const url = new URL(this.baseUrl + path);
|
|
80
|
+
if (opts.query) {
|
|
81
|
+
for (const [k, v] of Object.entries(opts.query)) {
|
|
82
|
+
if (v !== undefined && v !== null && v !== "") {
|
|
83
|
+
url.searchParams.set(k, String(v));
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
const headers = {
|
|
88
|
+
Authorization: `Bearer ${apiKey}`,
|
|
89
|
+
Accept: "application/json",
|
|
90
|
+
};
|
|
91
|
+
if (opts.body !== undefined)
|
|
92
|
+
headers["Content-Type"] = "application/json";
|
|
93
|
+
let res;
|
|
94
|
+
try {
|
|
95
|
+
res = await fetch(url.toString(), {
|
|
96
|
+
method,
|
|
97
|
+
headers,
|
|
98
|
+
body: opts.body !== undefined ? JSON.stringify(opts.body) : undefined,
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
catch (err) {
|
|
102
|
+
log(`network error calling ${method} ${path}:`, err);
|
|
103
|
+
return {
|
|
104
|
+
ok: false,
|
|
105
|
+
status: 0,
|
|
106
|
+
data: {
|
|
107
|
+
error: "network_error",
|
|
108
|
+
message: err instanceof Error ? err.message : String(err),
|
|
109
|
+
},
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
const text = await res.text();
|
|
113
|
+
let data = text;
|
|
114
|
+
if (text) {
|
|
115
|
+
try {
|
|
116
|
+
data = JSON.parse(text);
|
|
117
|
+
}
|
|
118
|
+
catch {
|
|
119
|
+
// leave as text
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return { ok: res.ok, status: res.status, data };
|
|
123
|
+
}
|
|
124
|
+
// Every method takes an optional trailing `apiKey` (the per-request key for
|
|
125
|
+
// the multi-user HTTP path). When omitted, the constructor key (stdio) is used.
|
|
126
|
+
// ---- reads (scope: read) ----
|
|
127
|
+
whoami(apiKey) {
|
|
128
|
+
return this.request("GET", "/api/agent/me", { apiKey });
|
|
129
|
+
}
|
|
130
|
+
getPortfolio(query, apiKey) {
|
|
131
|
+
return this.request("GET", "/api/agent/portfolio", { query, apiKey });
|
|
132
|
+
}
|
|
133
|
+
getWallet(query, apiKey) {
|
|
134
|
+
return this.request("GET", "/api/agent/wallet", { query, apiKey });
|
|
135
|
+
}
|
|
136
|
+
resolveSymbol(query, apiKey) {
|
|
137
|
+
return this.request("GET", "/api/agent/resolve", { query, apiKey });
|
|
138
|
+
}
|
|
139
|
+
getEquityCurve(query, apiKey) {
|
|
140
|
+
return this.request("GET", "/api/agent/equity-curve", { query, apiKey });
|
|
141
|
+
}
|
|
142
|
+
getMyTrades(query, apiKey) {
|
|
143
|
+
return this.request("GET", "/api/agent/trades", { query, apiKey });
|
|
144
|
+
}
|
|
145
|
+
getMarketContext(coinId, apiKey) {
|
|
146
|
+
return this.request("GET", `/api/agent/market/${encodeURIComponent(coinId)}`, { apiKey });
|
|
147
|
+
}
|
|
148
|
+
getPerformance(apiKey) {
|
|
149
|
+
return this.request("GET", "/api/agent/performance", { apiKey });
|
|
150
|
+
}
|
|
151
|
+
// Agent Arena (public leaderboard). The key is sent but ignored by these
|
|
152
|
+
// endpoints — they expose only public agent names + realized performance.
|
|
153
|
+
getArenaLeaderboard(query, apiKey) {
|
|
154
|
+
return this.request("GET", "/api/arena", { query, apiKey });
|
|
155
|
+
}
|
|
156
|
+
getArenaAgent(handle, apiKey) {
|
|
157
|
+
return this.request("GET", `/api/arena/${encodeURIComponent(handle)}`, { apiKey });
|
|
158
|
+
}
|
|
159
|
+
listOpenOrders(query, apiKey) {
|
|
160
|
+
return this.request("GET", "/api/agent/orders/open", { query, apiKey });
|
|
161
|
+
}
|
|
162
|
+
getFuturesPositions(apiKey) {
|
|
163
|
+
return this.request("GET", "/api/agent/positions/futures", { apiKey });
|
|
164
|
+
}
|
|
165
|
+
getPmPositions(apiKey) {
|
|
166
|
+
return this.request("GET", "/api/agent/positions/pm", { apiKey });
|
|
167
|
+
}
|
|
168
|
+
futuresQuote(body, apiKey) {
|
|
169
|
+
return this.request("POST", "/api/agent/futures/quote", { body, apiKey });
|
|
170
|
+
}
|
|
171
|
+
pmQuote(body, apiKey) {
|
|
172
|
+
return this.request("POST", "/api/agent/pm/quote", { body, apiKey });
|
|
173
|
+
}
|
|
174
|
+
spotQuote(body, apiKey) {
|
|
175
|
+
return this.request("POST", "/api/agent/spot/quote", { body, apiKey });
|
|
176
|
+
}
|
|
177
|
+
// ---- writes (scope: trade:<venue>) ----
|
|
178
|
+
placeSpotOrder(body, apiKey) {
|
|
179
|
+
return this.request("POST", "/api/agent/spot/order", { body, apiKey });
|
|
180
|
+
}
|
|
181
|
+
cancelSpotOrder(orderId, apiKey) {
|
|
182
|
+
return this.request("POST", `/api/agent/spot/order/${orderId}/cancel`, { apiKey });
|
|
183
|
+
}
|
|
184
|
+
openFuturesPosition(body, apiKey) {
|
|
185
|
+
return this.request("POST", "/api/agent/futures/open", { body, apiKey });
|
|
186
|
+
}
|
|
187
|
+
closeFuturesPosition(body, apiKey) {
|
|
188
|
+
return this.request("POST", "/api/agent/futures/close", { body, apiKey });
|
|
189
|
+
}
|
|
190
|
+
openPmPosition(body, apiKey) {
|
|
191
|
+
return this.request("POST", "/api/agent/pm/open", { body, apiKey });
|
|
192
|
+
}
|
|
193
|
+
}
|
package/dist/http.js
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// CoinRithm trading MCP server — Streamable HTTP transport (MULTI-USER, hosted).
|
|
3
|
+
//
|
|
4
|
+
// This is the entry behind https://mcp.coinrithm.com/mcp. It is multi-tenant:
|
|
5
|
+
// many users point their MCP client at the SAME URL, each sending THEIR OWN
|
|
6
|
+
// key in the request's Authorization header:
|
|
7
|
+
//
|
|
8
|
+
// Authorization: Bearer crk_live_…
|
|
9
|
+
//
|
|
10
|
+
// There is NO global COINRITHM_API_KEY here. Each request's key is read PER
|
|
11
|
+
// REQUEST and forwarded as the upstream Authorization to /api/agent/*, so the
|
|
12
|
+
// server never holds or mixes users' keys. (The single-user env-key path lives
|
|
13
|
+
// in src/index.ts / stdio and is unchanged.)
|
|
14
|
+
//
|
|
15
|
+
// How the per-request key reaches the tool handlers:
|
|
16
|
+
// - The MCP SDK's StreamableHTTPServerTransport surfaces the incoming HTTP
|
|
17
|
+
// request's headers to tool handlers via `extra.requestInfo.headers`
|
|
18
|
+
// (see tools.ts → requestKey()). That is the primary, SDK-native path.
|
|
19
|
+
// - We ALSO attach the parsed token to `req.auth` below, which the transport
|
|
20
|
+
// 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.
|
|
22
|
+
//
|
|
23
|
+
// Config (env):
|
|
24
|
+
// COINRITHM_API_URL (optional) upstream base URL (default production).
|
|
25
|
+
// PORT (optional) HTTP port (default 8787).
|
|
26
|
+
//
|
|
27
|
+
// Transport is stateless: a fresh McpServer + transport per request, which is
|
|
28
|
+
// the correct isolation model for a multi-user, per-request-keyed surface — no
|
|
29
|
+
// session state is shared between users.
|
|
30
|
+
import express from "express";
|
|
31
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
32
|
+
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
33
|
+
import { CoinRithmClient, bearerFromHeader, loadHttpConfig, log, } from "./client.js";
|
|
34
|
+
import { registerTools } from "./tools.js";
|
|
35
|
+
async function main() {
|
|
36
|
+
const config = loadHttpConfig(); // no global key — keys arrive per request
|
|
37
|
+
const client = new CoinRithmClient(config); // constructed WITHOUT a default key
|
|
38
|
+
const app = express();
|
|
39
|
+
app.use(express.json());
|
|
40
|
+
// Lightweight, unauthenticated liveness probe (handy for Coolify/uptime checks).
|
|
41
|
+
app.get("/healthz", (_req, res) => {
|
|
42
|
+
res.json({ ok: true, service: "coinrithm-mcp", transport: "streamable-http" });
|
|
43
|
+
});
|
|
44
|
+
app.post("/mcp", async (req, res) => {
|
|
45
|
+
// Per-request auth: read THIS caller's key from the Authorization header.
|
|
46
|
+
// Reject early (before touching the MCP machinery) if it is missing.
|
|
47
|
+
const apiKey = bearerFromHeader(req.headers.authorization);
|
|
48
|
+
if (!apiKey) {
|
|
49
|
+
res.status(401).json({
|
|
50
|
+
jsonrpc: "2.0",
|
|
51
|
+
error: {
|
|
52
|
+
code: -32001,
|
|
53
|
+
message: "Missing Authorization header. Send 'Authorization: Bearer crk_live_…' " +
|
|
54
|
+
"with your own CoinRithm API key.",
|
|
55
|
+
},
|
|
56
|
+
id: null,
|
|
57
|
+
});
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
// Belt-and-suspenders: also expose the token via the SDK's authInfo channel.
|
|
61
|
+
// The primary path is extra.requestInfo.headers.authorization (always set by
|
|
62
|
+
// StreamableHTTPServerTransport); this gives requestKey() a second source.
|
|
63
|
+
req.auth = { token: apiKey, clientId: "coinrithm-key", scopes: [] };
|
|
64
|
+
const server = new McpServer({ name: "coinrithm-trading", version: "0.1.0" });
|
|
65
|
+
registerTools(server, client);
|
|
66
|
+
const transport = new StreamableHTTPServerTransport({
|
|
67
|
+
sessionIdGenerator: undefined, // stateless: no cross-request/user state
|
|
68
|
+
});
|
|
69
|
+
res.on("close", () => {
|
|
70
|
+
void transport.close();
|
|
71
|
+
void server.close();
|
|
72
|
+
});
|
|
73
|
+
try {
|
|
74
|
+
await server.connect(transport);
|
|
75
|
+
// The transport reads req.headers (→ extra.requestInfo) and req.auth
|
|
76
|
+
// (→ extra.authInfo); tools.ts picks up the caller's key from there.
|
|
77
|
+
await transport.handleRequest(req, res, req.body);
|
|
78
|
+
}
|
|
79
|
+
catch (err) {
|
|
80
|
+
log("http request error:", err);
|
|
81
|
+
if (!res.headersSent) {
|
|
82
|
+
res.status(500).json({
|
|
83
|
+
jsonrpc: "2.0",
|
|
84
|
+
error: { code: -32603, message: "Internal server error" },
|
|
85
|
+
id: null,
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
const port = Number(process.env.PORT) || 8787;
|
|
91
|
+
app.listen(port, () => {
|
|
92
|
+
log(`HTTP MCP listening on :${port}/mcp (multi-user, per-request key). ` +
|
|
93
|
+
`upstream=${config.baseUrl}. Paper only.`);
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
main().catch((err) => {
|
|
97
|
+
log("fatal:", err instanceof Error ? err.message : err);
|
|
98
|
+
process.exit(1);
|
|
99
|
+
});
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// CoinRithm trading MCP server — stdio transport.
|
|
3
|
+
//
|
|
4
|
+
// Exposes paper-trading tools over the Model Context Protocol so MCP clients
|
|
5
|
+
// (Claude Desktop, Claude Code, and any other MCP host) can read your CoinRithm
|
|
6
|
+
// paper account and place simulated trades using a user-minted API key.
|
|
7
|
+
//
|
|
8
|
+
// Config (env):
|
|
9
|
+
// COINRITHM_API_KEY (required) crk_live_… key minted in your profile.
|
|
10
|
+
// COINRITHM_API_URL (optional) base URL; defaults to https://api.coinrithm.com
|
|
11
|
+
//
|
|
12
|
+
// stdout is the JSON-RPC channel — we log ONLY to stderr.
|
|
13
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
14
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
15
|
+
import { CoinRithmClient, loadConfig, log } from "./client.js";
|
|
16
|
+
import { registerTools } from "./tools.js";
|
|
17
|
+
async function main() {
|
|
18
|
+
const config = loadConfig();
|
|
19
|
+
const client = new CoinRithmClient(config);
|
|
20
|
+
const server = new McpServer({
|
|
21
|
+
name: "coinrithm-trading",
|
|
22
|
+
version: "0.1.0",
|
|
23
|
+
});
|
|
24
|
+
registerTools(server, client);
|
|
25
|
+
const transport = new StdioServerTransport();
|
|
26
|
+
await server.connect(transport);
|
|
27
|
+
log(`connected (stdio). base=${config.baseUrl}. Paper trading only — not advice.`);
|
|
28
|
+
}
|
|
29
|
+
main().catch((err) => {
|
|
30
|
+
log("fatal:", err instanceof Error ? err.message : err);
|
|
31
|
+
process.exit(1);
|
|
32
|
+
});
|
package/dist/tools.js
ADDED
|
@@ -0,0 +1,387 @@
|
|
|
1
|
+
// Registers the CoinRithm trading tools on an MCP server instance.
|
|
2
|
+
//
|
|
3
|
+
// Every tool wraps exactly one /api/agent/* call. Tool results return the raw
|
|
4
|
+
// JSON body as text so the model sees the real server response (incl. error
|
|
5
|
+
// shapes like { error, blockReasons }). HTTP-level failures are surfaced as
|
|
6
|
+
// isError results rather than thrown so the model can react.
|
|
7
|
+
import { z } from "zod";
|
|
8
|
+
import { bearerFromHeader } from "./client.js";
|
|
9
|
+
const PAPER_NOTE = "Paper trading only — virtual funds (50,000 mUSD). Not financial advice.";
|
|
10
|
+
// Per-request key resolution (multi-user HTTP).
|
|
11
|
+
//
|
|
12
|
+
// On the Streamable-HTTP transport the SDK surfaces the incoming HTTP request's
|
|
13
|
+
// headers on `extra.requestInfo.headers` (StreamableHTTPServerTransport builds
|
|
14
|
+
// `requestInfo` from the Node request and threads it through to handlers). We
|
|
15
|
+
// read the caller's own `Authorization: Bearer crk_live_…` from there and pass
|
|
16
|
+
// it as the per-request key for this one call.
|
|
17
|
+
//
|
|
18
|
+
// On the stdio transport there is no HTTP request, so `extra.requestInfo` is
|
|
19
|
+
// undefined and this returns undefined — the client then falls back to the
|
|
20
|
+
// COINRITHM_API_KEY it was constructed with. `authInfo.token` is also honoured
|
|
21
|
+
// in case a future auth middleware populates it.
|
|
22
|
+
function requestKey(extra) {
|
|
23
|
+
const fromHeader = bearerFromHeader(extra.requestInfo?.headers?.authorization);
|
|
24
|
+
if (fromHeader)
|
|
25
|
+
return fromHeader;
|
|
26
|
+
const token = extra.authInfo?.token?.trim();
|
|
27
|
+
return token || undefined;
|
|
28
|
+
}
|
|
29
|
+
function present(result) {
|
|
30
|
+
const payload = {
|
|
31
|
+
httpStatus: result.status,
|
|
32
|
+
ok: result.ok,
|
|
33
|
+
body: result.data,
|
|
34
|
+
};
|
|
35
|
+
return {
|
|
36
|
+
content: [
|
|
37
|
+
{ type: "text", text: JSON.stringify(payload, null, 2) },
|
|
38
|
+
],
|
|
39
|
+
isError: !result.ok,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
export function registerTools(server, client) {
|
|
43
|
+
// ---------------- identity ----------------
|
|
44
|
+
server.registerTool("whoami", {
|
|
45
|
+
title: "Who am I (CoinRithm)",
|
|
46
|
+
description: "Return the identity behind the configured API key: userId, keyId, and " +
|
|
47
|
+
"granted scopes. Use this first to confirm what the key is allowed to do. " +
|
|
48
|
+
PAPER_NOTE,
|
|
49
|
+
inputSchema: {},
|
|
50
|
+
}, async (_args, extra) => present(await client.whoami(requestKey(extra))));
|
|
51
|
+
// ---------------- reads ----------------
|
|
52
|
+
server.registerTool("get_portfolio", {
|
|
53
|
+
title: "Get portfolio",
|
|
54
|
+
description: "Get the paper account dashboard: equity (wallet.totalUsd), period PnL " +
|
|
55
|
+
"(wallet.pnl), asset balances, open orders, and recent history. " +
|
|
56
|
+
PAPER_NOTE,
|
|
57
|
+
inputSchema: {
|
|
58
|
+
fiat: z
|
|
59
|
+
.string()
|
|
60
|
+
.optional()
|
|
61
|
+
.describe("Display fiat code (default USD). Equity stays USD-denominated."),
|
|
62
|
+
locale: z.string().optional().describe("Locale (default en)."),
|
|
63
|
+
},
|
|
64
|
+
}, async ({ fiat, locale }, extra) => present(await client.getPortfolio({ fiat, locale }, requestKey(extra))));
|
|
65
|
+
server.registerTool("get_wallet", {
|
|
66
|
+
title: "Get wallet",
|
|
67
|
+
description: "Get raw cash balances: USDT available plus the three frozen partitions " +
|
|
68
|
+
"(frozen = spot orders, frozenPm = PM, frozenFutures = futures margin). " +
|
|
69
|
+
"Optionally include one coin asset. " +
|
|
70
|
+
PAPER_NOTE,
|
|
71
|
+
inputSchema: {
|
|
72
|
+
coinId: z
|
|
73
|
+
.string()
|
|
74
|
+
.optional()
|
|
75
|
+
.describe('Coin UCID (e.g. "1" = BTC) to also return that asset.'),
|
|
76
|
+
},
|
|
77
|
+
}, async ({ coinId }, extra) => present(await client.getWallet({ coinId }, requestKey(extra))));
|
|
78
|
+
server.registerTool("list_open_orders", {
|
|
79
|
+
title: "List open spot orders",
|
|
80
|
+
description: "List open (resting) spot orders for ONE coin. coinId is required. " +
|
|
81
|
+
PAPER_NOTE,
|
|
82
|
+
inputSchema: {
|
|
83
|
+
coinId: z
|
|
84
|
+
.string()
|
|
85
|
+
.min(1)
|
|
86
|
+
.describe("Coin UCID to list open orders for."),
|
|
87
|
+
limit: z
|
|
88
|
+
.number()
|
|
89
|
+
.int()
|
|
90
|
+
.min(1)
|
|
91
|
+
.max(200)
|
|
92
|
+
.optional()
|
|
93
|
+
.describe("Max rows (1-200, default 100)."),
|
|
94
|
+
},
|
|
95
|
+
}, async ({ coinId, limit }, extra) => present(await client.listOpenOrders({ coinId, limit }, requestKey(extra))));
|
|
96
|
+
server.registerTool("get_positions", {
|
|
97
|
+
title: "Get positions",
|
|
98
|
+
description: "List open + historical positions for a venue. venue='futures' returns " +
|
|
99
|
+
"mock futures positions (with unrealized PnL + liquidation distance on " +
|
|
100
|
+
"open ones); venue='pm' returns mock prediction-market positions (with " +
|
|
101
|
+
"unrealized mark on open ones). " +
|
|
102
|
+
PAPER_NOTE,
|
|
103
|
+
inputSchema: {
|
|
104
|
+
venue: z
|
|
105
|
+
.enum(["futures", "pm"])
|
|
106
|
+
.describe("Which venue's positions to list."),
|
|
107
|
+
},
|
|
108
|
+
}, async ({ venue }, extra) => present(venue === "futures"
|
|
109
|
+
? await client.getFuturesPositions(requestKey(extra))
|
|
110
|
+
: await client.getPmPositions(requestKey(extra))));
|
|
111
|
+
server.registerTool("resolve_symbol", {
|
|
112
|
+
title: "Resolve symbol -> coinId",
|
|
113
|
+
description: "Resolve a human symbol / slug / name (e.g. 'BTC', 'ethereum') to a " +
|
|
114
|
+
"CoinRithm coinId (UCID) plus disambiguating alternatives, each with its " +
|
|
115
|
+
"CoinGecko category tags. Use this FIRST to get the coinId that the " +
|
|
116
|
+
"wallet / quote / order tools need — don't guess UCIDs (symbols are not " +
|
|
117
|
+
"unique). " +
|
|
118
|
+
PAPER_NOTE,
|
|
119
|
+
inputSchema: {
|
|
120
|
+
q: z
|
|
121
|
+
.string()
|
|
122
|
+
.min(1)
|
|
123
|
+
.describe("Symbol, slug, or name (e.g. BTC, bitcoin, Ethereum)."),
|
|
124
|
+
},
|
|
125
|
+
}, async ({ q }, extra) => present(await client.resolveSymbol({ q }, requestKey(extra))));
|
|
126
|
+
server.registerTool("get_equity_curve", {
|
|
127
|
+
title: "Get equity curve",
|
|
128
|
+
description: "Daily wallet equity time series ({date, usdValue}) for the paper " +
|
|
129
|
+
"account — the basis for reviewing performance over time and narrating " +
|
|
130
|
+
"results. days = look-back window (1-365, default 30). " +
|
|
131
|
+
PAPER_NOTE,
|
|
132
|
+
inputSchema: {
|
|
133
|
+
days: z
|
|
134
|
+
.number()
|
|
135
|
+
.int()
|
|
136
|
+
.min(1)
|
|
137
|
+
.max(365)
|
|
138
|
+
.optional()
|
|
139
|
+
.describe("Look-back window in days (1-365, default 30)."),
|
|
140
|
+
},
|
|
141
|
+
}, async ({ days }, extra) => present(await client.getEquityCurve({ days }, requestKey(extra))));
|
|
142
|
+
server.registerTool("get_my_trades", {
|
|
143
|
+
title: "Get my trades",
|
|
144
|
+
description: "Unified realized-PnL log of CLOSED trades across venues (spot fills, " +
|
|
145
|
+
"closed/liquidated futures, settled prediction-markets), most-recent " +
|
|
146
|
+
"first — the agent's memory of what it did and what won/lost. Use it to " +
|
|
147
|
+
"review performance before deciding the next move. " +
|
|
148
|
+
PAPER_NOTE,
|
|
149
|
+
inputSchema: {
|
|
150
|
+
venue: z
|
|
151
|
+
.enum(["all", "spot", "futures", "pm"])
|
|
152
|
+
.optional()
|
|
153
|
+
.describe("Filter by venue (default all)."),
|
|
154
|
+
limit: z
|
|
155
|
+
.number()
|
|
156
|
+
.int()
|
|
157
|
+
.min(1)
|
|
158
|
+
.max(100)
|
|
159
|
+
.optional()
|
|
160
|
+
.describe("Max rows (1-100, default 25)."),
|
|
161
|
+
},
|
|
162
|
+
}, async ({ venue, limit }, extra) => present(await client.getMyTrades({ venue, limit }, requestKey(extra))));
|
|
163
|
+
server.registerTool("get_market_context", {
|
|
164
|
+
title: "Get market context",
|
|
165
|
+
description: "Compact factual context for ONE coin to form a thesis: price + " +
|
|
166
|
+
"1h/24h/7d change + market cap, the coin's CoinGecko category tags, " +
|
|
167
|
+
"per-coin sentiment votes, the global Fear & Greed value, up to 3 " +
|
|
168
|
+
"directly-related OPEN prediction markets — each with its leading " +
|
|
169
|
+
"outcome + probability, 24h volume, liquidity, and decisionSupport " +
|
|
170
|
+
"(quality/liquidity/volume/spread tiers + flags) so you can gauge a " +
|
|
171
|
+
"market's depth/tradability — and up to 6 similar coins (shared category " +
|
|
172
|
+
"/ market-cap peers). Facts only — no generated thesis. Call " +
|
|
173
|
+
"resolve_symbol first to get the coinId. " +
|
|
174
|
+
PAPER_NOTE,
|
|
175
|
+
inputSchema: {
|
|
176
|
+
coinId: z
|
|
177
|
+
.string()
|
|
178
|
+
.min(1)
|
|
179
|
+
.describe('Coin UCID (e.g. "1" = BTC). Use resolve_symbol to find it.'),
|
|
180
|
+
},
|
|
181
|
+
}, async ({ coinId }, extra) => present(await client.getMarketContext(coinId, requestKey(extra))));
|
|
182
|
+
server.registerTool("get_performance", {
|
|
183
|
+
title: "Get my performance",
|
|
184
|
+
description: "The calling key's own realized performance: total + per-venue realized " +
|
|
185
|
+
"PnL (mUSD), trade count, win/loss/neutral counts, and win rate (null " +
|
|
186
|
+
"until there are decided trades). Closed trades only — the scorecard for " +
|
|
187
|
+
"this agent. " +
|
|
188
|
+
PAPER_NOTE,
|
|
189
|
+
inputSchema: {},
|
|
190
|
+
}, async (_args, extra) => present(await client.getPerformance(requestKey(extra))));
|
|
191
|
+
server.registerTool("get_arena_leaderboard", {
|
|
192
|
+
title: "Get Agent Arena leaderboard",
|
|
193
|
+
description: "The public Agent Arena: opted-in agents ranked by total realized PnL " +
|
|
194
|
+
"(mUSD) across spot, futures, and prediction markets, with per-venue " +
|
|
195
|
+
"breakdown and win rate. Only agents with at least minDecidedTrades " +
|
|
196
|
+
"decided (win+loss) trades rank; demo/house agents seed the board until " +
|
|
197
|
+
"live agents qualify. Use it to see the field and where you stand — pair " +
|
|
198
|
+
"with get_performance (your own scorecard) and get_arena_agent (drill " +
|
|
199
|
+
"into one handle). Public data: agent names + performance only. " +
|
|
200
|
+
PAPER_NOTE,
|
|
201
|
+
inputSchema: {
|
|
202
|
+
page: z
|
|
203
|
+
.number()
|
|
204
|
+
.int()
|
|
205
|
+
.min(1)
|
|
206
|
+
.max(100)
|
|
207
|
+
.optional()
|
|
208
|
+
.describe("Page number (1-100, default 1)."),
|
|
209
|
+
pageSize: z
|
|
210
|
+
.number()
|
|
211
|
+
.int()
|
|
212
|
+
.min(1)
|
|
213
|
+
.max(50)
|
|
214
|
+
.optional()
|
|
215
|
+
.describe("Rows per page (1-50, default 12)."),
|
|
216
|
+
},
|
|
217
|
+
}, async ({ page, pageSize }, extra) => present(await client.getArenaLeaderboard({ page, pageSize }, requestKey(extra))));
|
|
218
|
+
server.registerTool("get_arena_agent", {
|
|
219
|
+
title: "Get Agent Arena profile",
|
|
220
|
+
description: "One agent's public Arena profile by handle (the `handle` field from " +
|
|
221
|
+
"get_arena_leaderboard, e.g. 'a42-momentum-scout'): rank, total + " +
|
|
222
|
+
"per-venue realized PnL, decided/total trade counts, and win rate. " +
|
|
223
|
+
"Public data only — no account or key identity. " +
|
|
224
|
+
PAPER_NOTE,
|
|
225
|
+
inputSchema: {
|
|
226
|
+
handle: z
|
|
227
|
+
.string()
|
|
228
|
+
.min(1)
|
|
229
|
+
.describe("Arena handle from the leaderboard (e.g. a42-momentum-scout)."),
|
|
230
|
+
},
|
|
231
|
+
}, async ({ handle }, extra) => present(await client.getArenaAgent(handle, requestKey(extra))));
|
|
232
|
+
// ---------------- quotes (read scope, read-only) ----------------
|
|
233
|
+
server.registerTool("futures_quote", {
|
|
234
|
+
title: "Futures quote",
|
|
235
|
+
description: "Read-only futures quote: entry price, notional, size, liquidation price, " +
|
|
236
|
+
"and eligibility. Never mutates state — always quote before opening. " +
|
|
237
|
+
"leverage 1-20, marginMusd >= 10. " +
|
|
238
|
+
PAPER_NOTE,
|
|
239
|
+
inputSchema: {
|
|
240
|
+
coinId: z.string().describe("Coin UCID."),
|
|
241
|
+
side: z.enum(["long", "short"]),
|
|
242
|
+
leverage: z.number().min(1).max(20).describe("1-20x."),
|
|
243
|
+
marginMusd: z
|
|
244
|
+
.number()
|
|
245
|
+
.min(10)
|
|
246
|
+
.describe("Isolated margin in mUSD (>= 10)."),
|
|
247
|
+
},
|
|
248
|
+
}, async ({ coinId, side, leverage, marginMusd }, extra) => present(await client.futuresQuote({ coinId, side, leverage, marginMusd }, requestKey(extra))));
|
|
249
|
+
server.registerTool("pm_quote", {
|
|
250
|
+
title: "Prediction-market quote",
|
|
251
|
+
description: "Read-only PM quote for a binary outcome: entry probability, share " +
|
|
252
|
+
"estimate, max payout, eligibility, freshness, and decisionSupport " +
|
|
253
|
+
"(market quality/liquidity/volume/spread tiers + flags) so you can " +
|
|
254
|
+
"quote and gauge tradability in one call. Never mutates state. " +
|
|
255
|
+
"stakeMusd must be > 0 (min to open is 10). " +
|
|
256
|
+
PAPER_NOTE,
|
|
257
|
+
inputSchema: {
|
|
258
|
+
source: z.string().describe("Source slug (e.g. kalshi, polymarket)."),
|
|
259
|
+
slug: z.string().describe("Event slug."),
|
|
260
|
+
outcomeExternalMarketId: z
|
|
261
|
+
.string()
|
|
262
|
+
.describe("Case-sensitive outcome / market id."),
|
|
263
|
+
stakeMusd: z.number().positive().describe("mUSD to stake (> 0)."),
|
|
264
|
+
},
|
|
265
|
+
}, async ({ source, slug, outcomeExternalMarketId, stakeMusd }, extra) => present(await client.pmQuote({ source, slug, outcomeExternalMarketId, stakeMusd }, requestKey(extra))));
|
|
266
|
+
server.registerTool("spot_quote", {
|
|
267
|
+
title: "Spot quote",
|
|
268
|
+
description: "Read-only spot MARKET quote: live execution price, estimated cost " +
|
|
269
|
+
"(price x quantity), your available balance for the side, and whether " +
|
|
270
|
+
"the fill is eligible (with blockReasons). Never mutates state — quote " +
|
|
271
|
+
"before place_spot_order instead of buying/selling blind. Price age is " +
|
|
272
|
+
"informational only (a market order fills regardless). coinId is a UCID, " +
|
|
273
|
+
"NOT a ticker — use resolve_symbol first. " +
|
|
274
|
+
PAPER_NOTE,
|
|
275
|
+
inputSchema: {
|
|
276
|
+
coinId: z.string().describe("Coin UCID (e.g. '1' = BTC)."),
|
|
277
|
+
side: z.enum(["buy", "sell"]),
|
|
278
|
+
quantity: z
|
|
279
|
+
.number()
|
|
280
|
+
.positive()
|
|
281
|
+
.describe("Amount of the base coin (> 0)."),
|
|
282
|
+
},
|
|
283
|
+
}, async ({ coinId, side, quantity }, extra) => present(await client.spotQuote({ coinId, side, quantity }, requestKey(extra))));
|
|
284
|
+
// ---------------- writes ----------------
|
|
285
|
+
server.registerTool("place_spot_order", {
|
|
286
|
+
title: "Place spot order",
|
|
287
|
+
description: "Place a paper spot order. coinId is a coin UCID, NOT a ticker. " +
|
|
288
|
+
"orderType market/limit/stop. limitPrice required for limit & stop; " +
|
|
289
|
+
"stopPrice required for stop. Requires the trade:spot scope. CONFIRM with " +
|
|
290
|
+
"the user before calling. " +
|
|
291
|
+
PAPER_NOTE,
|
|
292
|
+
inputSchema: {
|
|
293
|
+
coinId: z.string().describe('Coin UCID (e.g. "1" = BTC).'),
|
|
294
|
+
side: z.enum(["buy", "sell"]),
|
|
295
|
+
orderType: z.enum(["market", "limit", "stop"]),
|
|
296
|
+
quantity: z.number().positive().describe("Base-coin amount (> 0)."),
|
|
297
|
+
limitPrice: z
|
|
298
|
+
.number()
|
|
299
|
+
.positive()
|
|
300
|
+
.optional()
|
|
301
|
+
.describe("USD/coin — required for limit & stop."),
|
|
302
|
+
stopPrice: z
|
|
303
|
+
.number()
|
|
304
|
+
.positive()
|
|
305
|
+
.optional()
|
|
306
|
+
.describe("USD trigger — required for stop."),
|
|
307
|
+
},
|
|
308
|
+
}, async ({ coinId, side, orderType, quantity, limitPrice, stopPrice }, extra) => present(await client.placeSpotOrder({
|
|
309
|
+
coinId,
|
|
310
|
+
side,
|
|
311
|
+
orderType,
|
|
312
|
+
quantity,
|
|
313
|
+
limitPrice,
|
|
314
|
+
stopPrice,
|
|
315
|
+
}, requestKey(extra))));
|
|
316
|
+
server.registerTool("cancel_spot_order", {
|
|
317
|
+
title: "Cancel spot order",
|
|
318
|
+
description: "Cancel an open spot order by id (releases frozen funds). Requires the " +
|
|
319
|
+
"trade:spot scope. " +
|
|
320
|
+
PAPER_NOTE,
|
|
321
|
+
inputSchema: {
|
|
322
|
+
orderId: z.number().int().positive().describe("Open order id."),
|
|
323
|
+
},
|
|
324
|
+
}, async ({ orderId }, extra) => present(await client.cancelSpotOrder(orderId, requestKey(extra))));
|
|
325
|
+
server.registerTool("open_futures_position", {
|
|
326
|
+
title: "Open futures position",
|
|
327
|
+
description: "Open (or add to) a mock futures position. Requires the trade:futures " +
|
|
328
|
+
"scope AND is server-flag gated (currently returns 403 'not enabled'). " +
|
|
329
|
+
"idempotencyKey is REQUIRED and must be unique per intent. leverage 1-20, " +
|
|
330
|
+
"marginMusd >= 10. Quote first and CONFIRM with the user. " +
|
|
331
|
+
PAPER_NOTE,
|
|
332
|
+
inputSchema: {
|
|
333
|
+
coinId: z.string(),
|
|
334
|
+
side: z.enum(["long", "short"]),
|
|
335
|
+
leverage: z.number().min(1).max(20),
|
|
336
|
+
marginMusd: z.number().min(10),
|
|
337
|
+
idempotencyKey: z
|
|
338
|
+
.string()
|
|
339
|
+
.min(1)
|
|
340
|
+
.describe("Unique per intent; reuse replays the original result."),
|
|
341
|
+
},
|
|
342
|
+
}, async ({ coinId, side, leverage, marginMusd, idempotencyKey }, extra) => present(await client.openFuturesPosition({
|
|
343
|
+
coinId,
|
|
344
|
+
side,
|
|
345
|
+
leverage,
|
|
346
|
+
marginMusd,
|
|
347
|
+
idempotencyKey,
|
|
348
|
+
}, requestKey(extra))));
|
|
349
|
+
server.registerTool("close_futures_position", {
|
|
350
|
+
title: "Close futures position",
|
|
351
|
+
description: "Close or partially reduce a mock futures position. fraction in (0,1] " +
|
|
352
|
+
"reduces partially; omit (or 1) for a full close. idempotencyKey is " +
|
|
353
|
+
"REQUIRED. Requires the trade:futures scope. " +
|
|
354
|
+
PAPER_NOTE,
|
|
355
|
+
inputSchema: {
|
|
356
|
+
positionId: z.number().int().positive(),
|
|
357
|
+
fraction: z
|
|
358
|
+
.number()
|
|
359
|
+
.gt(0)
|
|
360
|
+
.lte(1)
|
|
361
|
+
.optional()
|
|
362
|
+
.describe("(0,1] portion to close; omit/1 = full close."),
|
|
363
|
+
idempotencyKey: z.string().min(1),
|
|
364
|
+
},
|
|
365
|
+
}, async ({ positionId, fraction, idempotencyKey }, extra) => present(await client.closeFuturesPosition({ positionId, fraction, idempotencyKey }, requestKey(extra))));
|
|
366
|
+
server.registerTool("open_pm_position", {
|
|
367
|
+
title: "Open prediction-market position",
|
|
368
|
+
description: "Open a mock prediction-market position (binary outcomes only). Requires " +
|
|
369
|
+
"the trade:pm scope AND is server-flag gated (currently returns 403 'not " +
|
|
370
|
+
"enabled'). idempotencyKey is REQUIRED. stakeMusd >= 10. Quote first and " +
|
|
371
|
+
"CONFIRM with the user. " +
|
|
372
|
+
PAPER_NOTE,
|
|
373
|
+
inputSchema: {
|
|
374
|
+
source: z.string(),
|
|
375
|
+
slug: z.string(),
|
|
376
|
+
outcomeExternalMarketId: z.string(),
|
|
377
|
+
stakeMusd: z.number().min(10).describe("mUSD stake (>= 10)."),
|
|
378
|
+
idempotencyKey: z.string().min(1),
|
|
379
|
+
},
|
|
380
|
+
}, async ({ source, slug, outcomeExternalMarketId, stakeMusd, idempotencyKey }, extra) => present(await client.openPmPosition({
|
|
381
|
+
source,
|
|
382
|
+
slug,
|
|
383
|
+
outcomeExternalMarketId,
|
|
384
|
+
stakeMusd,
|
|
385
|
+
idempotencyKey,
|
|
386
|
+
}, requestKey(extra))));
|
|
387
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@coinrithm/mcp-trading",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "MCP server for paper-trading on CoinRithm (spot, futures, prediction markets) with a user-minted API key.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"author": "CoinRithm",
|
|
7
|
+
"homepage": "https://coinrithm.com/agentic-trading",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/CoinRithm/coinrithm-agent-trading.git",
|
|
11
|
+
"directory": "packages/mcp-trading"
|
|
12
|
+
},
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/CoinRithm/coinrithm-agent-trading/issues"
|
|
15
|
+
},
|
|
16
|
+
"publishConfig": {
|
|
17
|
+
"access": "public"
|
|
18
|
+
},
|
|
19
|
+
"bin": {
|
|
20
|
+
"coinrithm-mcp": "dist/index.js"
|
|
21
|
+
},
|
|
22
|
+
"main": "dist/index.js",
|
|
23
|
+
"files": [
|
|
24
|
+
"dist",
|
|
25
|
+
"README.md"
|
|
26
|
+
],
|
|
27
|
+
"scripts": {
|
|
28
|
+
"build": "tsc -p tsconfig.json",
|
|
29
|
+
"start": "node dist/index.js",
|
|
30
|
+
"start:http": "node dist/http.js",
|
|
31
|
+
"prepare": "npm run build"
|
|
32
|
+
},
|
|
33
|
+
"engines": {
|
|
34
|
+
"node": ">=18"
|
|
35
|
+
},
|
|
36
|
+
"keywords": [
|
|
37
|
+
"mcp",
|
|
38
|
+
"model-context-protocol",
|
|
39
|
+
"coinrithm",
|
|
40
|
+
"paper-trading",
|
|
41
|
+
"agent",
|
|
42
|
+
"crypto",
|
|
43
|
+
"prediction-markets"
|
|
44
|
+
],
|
|
45
|
+
"license": "MIT",
|
|
46
|
+
"dependencies": {
|
|
47
|
+
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
48
|
+
"express": "^4.21.0",
|
|
49
|
+
"zod": "^3.23.8"
|
|
50
|
+
},
|
|
51
|
+
"devDependencies": {
|
|
52
|
+
"@types/express": "^4.17.21",
|
|
53
|
+
"@types/node": "^20.14.0",
|
|
54
|
+
"typescript": "^5.5.0"
|
|
55
|
+
}
|
|
56
|
+
}
|