@coinrithm/mcp-trading 0.7.18 → 0.7.19
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/CHANGELOG.md +19 -0
- package/README.md +23 -12
- package/dist/client.d.ts +1 -0
- package/dist/client.js +36 -20
- package/dist/http.d.ts +4 -0
- package/dist/http.js +16 -5
- package/dist/httpLimits.d.ts +10 -0
- package/dist/httpLimits.js +76 -0
- package/dist/httpShutdown.d.ts +13 -0
- package/dist/httpShutdown.js +34 -0
- package/dist/index.js +2 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,25 @@ ships two binaries — `coinrithm-mcp` (the MCP server) and `coinrithm-agent` (t
|
|
|
5
5
|
self-host agent runner) — versioned together. The CoinRithm **API contract** is
|
|
6
6
|
versioned separately (see `openapi.yaml` `info.version`, currently `1.9.0`).
|
|
7
7
|
|
|
8
|
+
## 0.7.19 (2026-10-16)
|
|
9
|
+
|
|
10
|
+
MCP server:
|
|
11
|
+
|
|
12
|
+
- The local stdio server starts without `COINRITHM_API_KEY` in data-only
|
|
13
|
+
mode, like the hosted server: `npx -y @coinrithm/mcp-trading` works with no
|
|
14
|
+
account. Keyless data tools (`pm_data_*`, `get_arena_*`,
|
|
15
|
+
`get_crypto_movers`) answer; every keyed tool returns the structured
|
|
16
|
+
`missing_api_key` result, which now also names `COINRITHM_API_KEY` for the
|
|
17
|
+
local server. `server.json` marks the key optional.
|
|
18
|
+
- Upstream requests time out after 35 s and report `upstream_timeout`
|
|
19
|
+
instead of hanging the tool call.
|
|
20
|
+
- Hosted HTTP entry: per-client admission (120 requests a minute) and an
|
|
21
|
+
in-flight cap (64), answered as JSON-RPC 429/503 with `Retry-After`; a stop
|
|
22
|
+
signal drains open requests for up to 8 s while `/healthz` answers 503;
|
|
23
|
+
`/health` and `/healthz` report `version`.
|
|
24
|
+
|
|
25
|
+
The default surface stays at 41 tools. Generated against API contract 1.9.0.
|
|
26
|
+
|
|
8
27
|
## 0.7.18 (2026-10-15)
|
|
9
28
|
|
|
10
29
|
MCP server:
|
package/README.md
CHANGED
|
@@ -44,10 +44,18 @@ This package ships two binaries:
|
|
|
44
44
|
|
|
45
45
|
> **Paper trading only** — virtual funds (50,000 mUSD). Not financial advice.
|
|
46
46
|
|
|
47
|
-
## Version 0.7.
|
|
48
|
-
|
|
49
|
-
The
|
|
50
|
-
|
|
47
|
+
## Version 0.7.19
|
|
48
|
+
|
|
49
|
+
The local server starts without `COINRITHM_API_KEY` in data-only mode, so
|
|
50
|
+
`npx -y @coinrithm/mcp-trading` gives prediction-market and crypto data with
|
|
51
|
+
no account; keyed tools return a structured `missing_api_key` result until a
|
|
52
|
+
key is set. Upstream calls time out after 35 s (`upstream_timeout`). The
|
|
53
|
+
hosted server adds per-client limits (120 requests a minute, 64 in flight),
|
|
54
|
+
drains open requests on deploy and reports its version on `/health`. The
|
|
55
|
+
default surface stays at 41 tools. See [CHANGELOG.md](./CHANGELOG.md).
|
|
56
|
+
|
|
57
|
+
Version 0.7.17 kept the default tool surface at 41 tools. Two server-wide
|
|
58
|
+
opt-in flags add tools that stay off unless enabled: the
|
|
51
59
|
[PM paper v2 tools](#opt-in-pm-paper-v2-tools) for configured house keys and
|
|
52
60
|
the [spot reference tool](#opt-in-spot-reference-tool).
|
|
53
61
|
|
|
@@ -95,7 +103,10 @@ for the exact opt-in policy and persistence contract.
|
|
|
95
103
|
## Quick start
|
|
96
104
|
|
|
97
105
|
```bash
|
|
98
|
-
#
|
|
106
|
+
# Prediction-market and crypto data only, no account or key needed:
|
|
107
|
+
npx -y @coinrithm/mcp-trading
|
|
108
|
+
|
|
109
|
+
# Add your CoinRithm key to enable paper trading (no install needed):
|
|
99
110
|
COINRITHM_API_KEY=crk_live_… npx -y @coinrithm/mcp-trading
|
|
100
111
|
```
|
|
101
112
|
|
|
@@ -178,13 +189,13 @@ agent file.
|
|
|
178
189
|
|
|
179
190
|
## Configure (stdio)
|
|
180
191
|
|
|
181
|
-
| Env var | Required
|
|
182
|
-
| ---------------------------------------- |
|
|
183
|
-
| `COINRITHM_API_KEY` |
|
|
184
|
-
| `COINRITHM_API_URL` | no
|
|
185
|
-
| `PORT` | no
|
|
186
|
-
| `COINRITHM_PM_PAPER_V2_TOOLS_ENABLED` | no
|
|
187
|
-
| `COINRITHM_SPOT_REFERENCE_TOOLS_ENABLED` | no
|
|
192
|
+
| Env var | Required | Default | Notes |
|
|
193
|
+
| ---------------------------------------- | --------------- | --------------------------- | -------------------------------------------------------------------------------------------------------------------- |
|
|
194
|
+
| `COINRITHM_API_KEY` | no (stdio only) | — | A `crk_live_…` key from CoinRithm → Settings → API Keys. Without it, data tools only. **Ignored by the HTTP entry.** |
|
|
195
|
+
| `COINRITHM_API_URL` | no | `https://api.coinrithm.com` | Upstream base URL (live) |
|
|
196
|
+
| `PORT` | no | `8787` | HTTP entry only |
|
|
197
|
+
| `COINRITHM_PM_PAPER_V2_TOOLS_ENABLED` | no | off | `true` adds the [PM paper v2 tools](#opt-in-pm-paper-v2-tools) for configured house keys. |
|
|
198
|
+
| `COINRITHM_SPOT_REFERENCE_TOOLS_ENABLED` | no | off | `true` adds [`get_spot_reference`](#opt-in-spot-reference-tool). |
|
|
188
199
|
|
|
189
200
|
## Run
|
|
190
201
|
|
package/dist/client.d.ts
CHANGED
|
@@ -41,6 +41,7 @@ export type ProvenanceReport = {
|
|
|
41
41
|
};
|
|
42
42
|
};
|
|
43
43
|
export declare const API_KEY_MINT_URL = "https://www.coinrithm.com/en/settings/api-keys";
|
|
44
|
+
export declare const UPSTREAM_TIMEOUT_MS = 35000;
|
|
44
45
|
export declare class CoinRithmClient {
|
|
45
46
|
private readonly defaultApiKey?;
|
|
46
47
|
private readonly baseUrl;
|
package/dist/client.js
CHANGED
|
@@ -27,12 +27,17 @@ export function log(...args) {
|
|
|
27
27
|
function resolveBaseUrl() {
|
|
28
28
|
return (process.env.COINRITHM_API_URL?.trim() || DEFAULT_BASE_URL).replace(/\/+$/, "");
|
|
29
29
|
}
|
|
30
|
-
// stdio path
|
|
30
|
+
// stdio path (single user). The key is OPTIONAL since 2026-10-16 (audit):
|
|
31
|
+
// without one the server starts in data-only mode, exactly like the hosted
|
|
32
|
+
// server: keyless tools (pm_data_*, get_arena_*, get_crypto_movers) work and
|
|
33
|
+
// every keyed tool returns the structured missing_api_key result. Requiring a
|
|
34
|
+
// key blocked the one-line `npx -y @coinrithm/mcp-trading` data install.
|
|
31
35
|
export function loadConfig() {
|
|
32
36
|
const apiKey = process.env.COINRITHM_API_KEY?.trim();
|
|
33
37
|
if (!apiKey) {
|
|
34
|
-
|
|
35
|
-
"
|
|
38
|
+
log("no COINRITHM_API_KEY: data-only mode. Keyless data tools work; paper-trading " +
|
|
39
|
+
"tools need a key from CoinRithm → Settings → API Keys.");
|
|
40
|
+
return { baseUrl: resolveBaseUrl() };
|
|
36
41
|
}
|
|
37
42
|
if (!apiKey.startsWith("crk_live_")) {
|
|
38
43
|
log("warning: COINRITHM_API_KEY does not start with 'crk_live_' — it will likely be rejected.");
|
|
@@ -75,6 +80,28 @@ const traceFromBody = (body) => body && typeof body === "object" && "agentTrace"
|
|
|
75
80
|
// Where a user mints an API key (Settings -> API Keys). Named in every
|
|
76
81
|
// missing-key error so an assistant can tell its user where to go.
|
|
77
82
|
export const API_KEY_MINT_URL = "https://www.coinrithm.com/en/settings/api-keys";
|
|
83
|
+
// Upstream ceiling for every API call (audit 2026-10-16: none existed, so a
|
|
84
|
+
// hung upstream held the tool call open until undici's ~300 s default).
|
|
85
|
+
// Measured before choosing it: the slowest successful hosted calls in 24 h
|
|
86
|
+
// took 26-29 s (cold heavy reads), so 35 s cuts hangs without failing them.
|
|
87
|
+
export const UPSTREAM_TIMEOUT_MS = 35_000;
|
|
88
|
+
const upstreamFailure = (err) => {
|
|
89
|
+
const timedOut = err instanceof Error &&
|
|
90
|
+
(err.name === "TimeoutError" || err.name === "AbortError");
|
|
91
|
+
return {
|
|
92
|
+
ok: false,
|
|
93
|
+
status: 0,
|
|
94
|
+
data: timedOut
|
|
95
|
+
? {
|
|
96
|
+
error: "upstream_timeout",
|
|
97
|
+
message: `CoinRithm API did not answer within ${UPSTREAM_TIMEOUT_MS / 1000} s. Retry shortly.`,
|
|
98
|
+
}
|
|
99
|
+
: {
|
|
100
|
+
error: "network_error",
|
|
101
|
+
message: err instanceof Error ? err.message : String(err),
|
|
102
|
+
},
|
|
103
|
+
};
|
|
104
|
+
};
|
|
78
105
|
export class CoinRithmClient {
|
|
79
106
|
// Default key for the stdio (single-user) path. Undefined in the multi-user
|
|
80
107
|
// HTTP path, where every call must pass a per-request `apiKey` override.
|
|
@@ -94,7 +121,8 @@ export class CoinRithmClient {
|
|
|
94
121
|
error: "missing_api_key",
|
|
95
122
|
message: "No API key for this request. Mint a free key at " +
|
|
96
123
|
`${API_KEY_MINT_URL}, then on the hosted MCP send ` +
|
|
97
|
-
"`Authorization: Bearer crk_live_
|
|
124
|
+
"`Authorization: Bearer crk_live_…`, or for the local server " +
|
|
125
|
+
"set COINRITHM_API_KEY. Public data tools " +
|
|
98
126
|
"(pm_data_*, get_arena_*, get_crypto_movers) need no key.",
|
|
99
127
|
},
|
|
100
128
|
};
|
|
@@ -120,18 +148,12 @@ export class CoinRithmClient {
|
|
|
120
148
|
method,
|
|
121
149
|
headers,
|
|
122
150
|
body: opts.body !== undefined ? JSON.stringify(opts.body) : undefined,
|
|
151
|
+
signal: AbortSignal.timeout(UPSTREAM_TIMEOUT_MS),
|
|
123
152
|
});
|
|
124
153
|
}
|
|
125
154
|
catch (err) {
|
|
126
155
|
log(`network error calling ${method} ${path}:`, err);
|
|
127
|
-
return
|
|
128
|
-
ok: false,
|
|
129
|
-
status: 0,
|
|
130
|
-
data: {
|
|
131
|
-
error: "network_error",
|
|
132
|
-
message: err instanceof Error ? err.message : String(err),
|
|
133
|
-
},
|
|
134
|
-
};
|
|
156
|
+
return upstreamFailure(err);
|
|
135
157
|
}
|
|
136
158
|
const text = await res.text();
|
|
137
159
|
let data = text;
|
|
@@ -181,18 +203,12 @@ export class CoinRithmClient {
|
|
|
181
203
|
res = await fetch(url.toString(), {
|
|
182
204
|
method: "GET",
|
|
183
205
|
headers: { Accept: "application/json" },
|
|
206
|
+
signal: AbortSignal.timeout(UPSTREAM_TIMEOUT_MS),
|
|
184
207
|
});
|
|
185
208
|
}
|
|
186
209
|
catch (err) {
|
|
187
210
|
log(`network error calling GET ${path}:`, err);
|
|
188
|
-
return
|
|
189
|
-
ok: false,
|
|
190
|
-
status: 0,
|
|
191
|
-
data: {
|
|
192
|
-
error: "network_error",
|
|
193
|
-
message: err instanceof Error ? err.message : String(err),
|
|
194
|
-
},
|
|
195
|
-
};
|
|
211
|
+
return upstreamFailure(err);
|
|
196
212
|
}
|
|
197
213
|
const text = await res.text();
|
|
198
214
|
let data = text;
|
package/dist/http.d.ts
CHANGED
|
@@ -2,8 +2,12 @@
|
|
|
2
2
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
3
|
import { CoinRithmClient } from "./client.js";
|
|
4
4
|
import { type CompletionLogger } from "./httpCompletion.js";
|
|
5
|
+
import { type McpLimitOptions } from "./httpLimits.js";
|
|
6
|
+
import { type DrainState } from "./httpShutdown.js";
|
|
5
7
|
export declare const MALFORMED_MCP_LINK: RegExp;
|
|
6
8
|
export declare function createHttpApp(client: CoinRithmClient, options?: {
|
|
7
9
|
completionLogger?: CompletionLogger;
|
|
8
10
|
createServer?: () => McpServer;
|
|
11
|
+
limits?: McpLimitOptions;
|
|
12
|
+
drain?: DrainState;
|
|
9
13
|
}): import("express-serve-static-core").Express;
|
package/dist/http.js
CHANGED
|
@@ -43,12 +43,16 @@ import { CoinRithmClient, bearerFromHeader, loadHttpConfig, log, } from "./clien
|
|
|
43
43
|
import { registerTools } from "./tools.js";
|
|
44
44
|
import { SERVER_VERSION } from "./version.js";
|
|
45
45
|
import { observeHttpCompletion, } from "./httpCompletion.js";
|
|
46
|
+
import { createMcpAdmission } from "./httpLimits.js";
|
|
47
|
+
import { installGracefulShutdown } from "./httpShutdown.js";
|
|
46
48
|
// "/mcp" immediately followed by the markdown "](" of a pasted link, literal or
|
|
47
49
|
// percent-encoded. Anything else under /mcp is not this family.
|
|
48
50
|
export const MALFORMED_MCP_LINK = /^\/mcp(?:\]|%5d)(?:\(|%28)/i;
|
|
49
51
|
// Factory permits isolated localhost SDK tests without opening a listener on import.
|
|
50
52
|
export function createHttpApp(client, options = {}) {
|
|
51
53
|
const app = express();
|
|
54
|
+
// Behind Cloudflare and Traefik (audit 2026-10-16: no limit existed).
|
|
55
|
+
const admission = createMcpAdmission(options.limits);
|
|
52
56
|
const completions = new WeakMap();
|
|
53
57
|
app.use((req, res, next) => {
|
|
54
58
|
if (req.method === "POST" && /^\/mcp\/?$/i.test(req.path)) {
|
|
@@ -60,11 +64,16 @@ export function createHttpApp(client, options = {}) {
|
|
|
60
64
|
// Lightweight, unauthenticated liveness probe (handy for Coolify/uptime checks).
|
|
61
65
|
// Keep `/healthz` as the deployment contract and expose `/health` as a
|
|
62
66
|
// compatibility alias for agents and generic uptime monitors.
|
|
67
|
+
// While a stop signal drains the process it answers 503, so the proxy and
|
|
68
|
+
// the image HEALTHCHECK stop sending new work here (httpShutdown.ts).
|
|
63
69
|
app.get(["/health", "/healthz"], (_req, res) => {
|
|
64
|
-
|
|
65
|
-
|
|
70
|
+
const draining = options.drain?.draining === true;
|
|
71
|
+
res.status(draining ? 503 : 200).json({
|
|
72
|
+
ok: !draining,
|
|
66
73
|
service: "coinrithm-mcp",
|
|
67
74
|
transport: "streamable-http",
|
|
75
|
+
version: SERVER_VERSION,
|
|
76
|
+
...(draining ? { draining: true } : {}),
|
|
68
77
|
});
|
|
69
78
|
});
|
|
70
79
|
// Self-describing landing for humans and validate-before-recommend agents.
|
|
@@ -129,7 +138,7 @@ export function createHttpApp(client, options = {}) {
|
|
|
129
138
|
message: "This is a POST-only streamable-HTTP MCP endpoint. Connect with an MCP client, or GET / for service info.",
|
|
130
139
|
});
|
|
131
140
|
});
|
|
132
|
-
app.post(["/mcp", MALFORMED_MCP_LINK], async (req, res) => {
|
|
141
|
+
app.post(["/mcp", MALFORMED_MCP_LINK], admission, async (req, res) => {
|
|
133
142
|
// Per-request auth: read THIS caller's key from the Authorization header,
|
|
134
143
|
// or from Smithery's non-reserved forwarding header.
|
|
135
144
|
// It is optional at the transport layer so registries can initialize the
|
|
@@ -180,12 +189,14 @@ export function createHttpApp(client, options = {}) {
|
|
|
180
189
|
async function main() {
|
|
181
190
|
const config = loadHttpConfig(); // no global key — keys arrive per request
|
|
182
191
|
const client = new CoinRithmClient(config); // constructed WITHOUT a default key
|
|
183
|
-
const
|
|
192
|
+
const drain = { draining: false };
|
|
193
|
+
const app = createHttpApp(client, { drain });
|
|
184
194
|
const port = Number(process.env.PORT) || 8787;
|
|
185
|
-
app.listen(port, () => {
|
|
195
|
+
const listener = app.listen(port, () => {
|
|
186
196
|
log(`HTTP MCP listening on :${port}/mcp (multi-user, per-request key). ` +
|
|
187
197
|
`upstream=${config.baseUrl}. Paper only.`);
|
|
188
198
|
});
|
|
199
|
+
installGracefulShutdown(listener, { state: drain, log });
|
|
189
200
|
}
|
|
190
201
|
if (process.argv[1] &&
|
|
191
202
|
import.meta.url === pathToFileURL(process.argv[1]).href) {
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { NextFunction, Request, Response } from "express";
|
|
2
|
+
export type McpLimitOptions = {
|
|
3
|
+
perClientPerMinute?: number;
|
|
4
|
+
maxInFlight?: number;
|
|
5
|
+
now?: () => number;
|
|
6
|
+
};
|
|
7
|
+
export declare const DEFAULT_PER_CLIENT_PER_MINUTE = 120;
|
|
8
|
+
export declare const DEFAULT_MAX_IN_FLIGHT = 64;
|
|
9
|
+
export declare function clientKey(req: Request): string;
|
|
10
|
+
export declare function createMcpAdmission(options?: McpLimitOptions): (req: Request, res: Response, next: NextFunction) => void;
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
// Admission limits for the hosted multi-user MCP (POST /mcp).
|
|
2
|
+
//
|
|
3
|
+
// Audit 2026-10-16: POST /mcp had no rate limit and no concurrency cap, and
|
|
4
|
+
// keyless pm_data_* tools reach the internal API directly (past Cloudflare),
|
|
5
|
+
// so one anonymous client could drive unbounded load into the single-host API.
|
|
6
|
+
// Measured before choosing the numbers (container logs, 24 h to 2026-10-16
|
|
7
|
+
// 01:30 UTC): 3,861 requests, peak about 16 per minute across ALL clients.
|
|
8
|
+
// A per-client allowance of 120 per minute (the API's own per-key baseline)
|
|
9
|
+
// leaves ordinary agents untouched; the in-flight cap bounds a burst.
|
|
10
|
+
//
|
|
11
|
+
// In-memory and per-process on purpose: the service runs as one container.
|
|
12
|
+
// No dependency is added for this (supply-chain surface of an npx package).
|
|
13
|
+
export const DEFAULT_PER_CLIENT_PER_MINUTE = 120;
|
|
14
|
+
export const DEFAULT_MAX_IN_FLIGHT = 64;
|
|
15
|
+
const WINDOW_MS = 60_000;
|
|
16
|
+
const firstHeader = (value) => {
|
|
17
|
+
const raw = Array.isArray(value) ? value[0] : value;
|
|
18
|
+
const first = raw?.split(",")[0]?.trim();
|
|
19
|
+
return first ? first : null;
|
|
20
|
+
};
|
|
21
|
+
// Behind Cloudflare and Traefik: Cloudflare's connecting-IP header names the
|
|
22
|
+
// real client; X-Forwarded-For is the fallback; the socket address last.
|
|
23
|
+
export function clientKey(req) {
|
|
24
|
+
return (firstHeader(req.headers["cf-connecting-ip"]) ??
|
|
25
|
+
firstHeader(req.headers["x-forwarded-for"]) ??
|
|
26
|
+
req.socket?.remoteAddress ??
|
|
27
|
+
"unknown");
|
|
28
|
+
}
|
|
29
|
+
const rpcError = (res, status, retryAfterSeconds, message) => {
|
|
30
|
+
res.setHeader("Retry-After", String(retryAfterSeconds));
|
|
31
|
+
res.status(status).json({
|
|
32
|
+
jsonrpc: "2.0",
|
|
33
|
+
error: { code: -32000, message, data: { retryAfterSeconds } },
|
|
34
|
+
id: null,
|
|
35
|
+
});
|
|
36
|
+
};
|
|
37
|
+
export function createMcpAdmission(options = {}) {
|
|
38
|
+
const perClient = options.perClientPerMinute ?? DEFAULT_PER_CLIENT_PER_MINUTE;
|
|
39
|
+
const maxInFlight = options.maxInFlight ?? DEFAULT_MAX_IN_FLIGHT;
|
|
40
|
+
const now = options.now ?? Date.now;
|
|
41
|
+
let windowStart = now();
|
|
42
|
+
let counts = new Map();
|
|
43
|
+
let inFlight = 0;
|
|
44
|
+
return function mcpAdmission(req, res, next) {
|
|
45
|
+
const t = now();
|
|
46
|
+
if (t - windowStart >= WINDOW_MS) {
|
|
47
|
+
// Fixed window: dropping the whole map bounds memory to one minute of
|
|
48
|
+
// distinct clients.
|
|
49
|
+
windowStart = t;
|
|
50
|
+
counts = new Map();
|
|
51
|
+
}
|
|
52
|
+
const key = clientKey(req);
|
|
53
|
+
const used = (counts.get(key) ?? 0) + 1;
|
|
54
|
+
counts.set(key, used);
|
|
55
|
+
if (used > perClient) {
|
|
56
|
+
const retry = Math.max(1, Math.ceil((windowStart + WINDOW_MS - t) / 1000));
|
|
57
|
+
rpcError(res, 429, retry, `Rate limited: at most ${perClient} MCP requests per minute per client. Retry after ${retry} s.`);
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
if (inFlight >= maxInFlight) {
|
|
61
|
+
rpcError(res, 503, 2, "Server busy: too many MCP requests in flight. Retry shortly.");
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
inFlight += 1;
|
|
65
|
+
let released = false;
|
|
66
|
+
const release = () => {
|
|
67
|
+
if (released)
|
|
68
|
+
return;
|
|
69
|
+
released = true;
|
|
70
|
+
inFlight -= 1;
|
|
71
|
+
};
|
|
72
|
+
res.on("finish", release);
|
|
73
|
+
res.on("close", release);
|
|
74
|
+
next();
|
|
75
|
+
};
|
|
76
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { Server } from "node:http";
|
|
2
|
+
export declare const SHUTDOWN_GRACE_MS = 8000;
|
|
3
|
+
export type DrainState = {
|
|
4
|
+
draining: boolean;
|
|
5
|
+
};
|
|
6
|
+
export declare function installGracefulShutdown(server: Pick<Server, "close" | "closeAllConnections">, options: {
|
|
7
|
+
state: DrainState;
|
|
8
|
+
graceMs?: number;
|
|
9
|
+
log?: (...args: unknown[]) => void;
|
|
10
|
+
exit?: (code: number) => void;
|
|
11
|
+
signals?: NodeJS.Signals[];
|
|
12
|
+
onSignal?: (signal: NodeJS.Signals, handler: () => void) => void;
|
|
13
|
+
}): (signal: NodeJS.Signals) => void;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
export const SHUTDOWN_GRACE_MS = 8_000;
|
|
2
|
+
export function installGracefulShutdown(server, options) {
|
|
3
|
+
const graceMs = options.graceMs ?? SHUTDOWN_GRACE_MS;
|
|
4
|
+
const exit = options.exit ?? ((code) => process.exit(code));
|
|
5
|
+
const say = options.log ?? (() => { });
|
|
6
|
+
let done = false;
|
|
7
|
+
const finish = (reason) => {
|
|
8
|
+
if (done)
|
|
9
|
+
return;
|
|
10
|
+
done = true;
|
|
11
|
+
say(`shutdown: ${reason}`);
|
|
12
|
+
exit(0);
|
|
13
|
+
};
|
|
14
|
+
const begin = (signal) => {
|
|
15
|
+
if (options.state.draining)
|
|
16
|
+
return;
|
|
17
|
+
options.state.draining = true;
|
|
18
|
+
say(`${signal}: draining, up to ${graceMs} ms for open requests`);
|
|
19
|
+
server.close(() => finish("all requests closed"));
|
|
20
|
+
const timer = setTimeout(() => {
|
|
21
|
+
server.closeAllConnections();
|
|
22
|
+
finish("grace period ended");
|
|
23
|
+
}, graceMs);
|
|
24
|
+
timer.unref?.();
|
|
25
|
+
};
|
|
26
|
+
const listen = options.onSignal ??
|
|
27
|
+
((signal, handler) => {
|
|
28
|
+
process.once(signal, handler);
|
|
29
|
+
});
|
|
30
|
+
for (const signal of options.signals ?? ["SIGTERM", "SIGINT"]) {
|
|
31
|
+
listen(signal, () => begin(signal));
|
|
32
|
+
}
|
|
33
|
+
return begin;
|
|
34
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -6,7 +6,8 @@
|
|
|
6
6
|
// paper account and place simulated trades using a user-minted API key.
|
|
7
7
|
//
|
|
8
8
|
// Config (env):
|
|
9
|
-
// COINRITHM_API_KEY (
|
|
9
|
+
// COINRITHM_API_KEY (optional) crk_live_… key minted in your profile.
|
|
10
|
+
// Without it: data-only mode (keyless tools).
|
|
10
11
|
// COINRITHM_API_URL (optional) base URL; defaults to https://api.coinrithm.com
|
|
11
12
|
//
|
|
12
13
|
// stdout is the JSON-RPC channel — we log ONLY to stderr.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@coinrithm/mcp-trading",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.19",
|
|
4
4
|
"mcpName": "io.github.CoinRithm/mcp-trading",
|
|
5
5
|
"description": "CoinRithm MCP server (coinrithm-mcp) and self-host agent runner (coinrithm-agent): keyless prediction-market data across 12 venues, plus paper trading of crypto spot, futures, and prediction markets with a user-minted API key.",
|
|
6
6
|
"type": "module",
|