@coinrithm/mcp-trading 0.7.8 → 0.7.9

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.
@@ -0,0 +1,220 @@
1
+ import { bearerFromHeader, log } from "./client.js";
2
+ import { SERVER_VERSION } from "./version.js";
3
+ // Explicit, server-owned dimensions; tests compare this with actual tools/list.
4
+ // Never retain a caller's unknown tool name, even truncated or hashed.
5
+ export const COMPLETION_TOOL_NAMES = [
6
+ "whoami",
7
+ "get_portfolio",
8
+ "get_wallet",
9
+ "list_open_orders",
10
+ "get_positions",
11
+ "resolve_symbol",
12
+ "get_equity_curve",
13
+ "get_my_trades",
14
+ "get_market_context",
15
+ "get_candles",
16
+ "discover_pm_markets",
17
+ "get_performance",
18
+ "get_agent_ledger",
19
+ "export_agent_ledger",
20
+ "export_run_evidence",
21
+ "get_arena_leaderboard",
22
+ "get_arena_agent",
23
+ "futures_quote",
24
+ "pm_quote",
25
+ "spot_quote",
26
+ "place_spot_order",
27
+ "cancel_spot_order",
28
+ "open_futures_position",
29
+ "set_futures_sl_tp",
30
+ "close_futures_position",
31
+ "open_pm_position",
32
+ "report_pm_opportunity",
33
+ "pm_data_overview",
34
+ "pm_data_sources",
35
+ "pm_data_sources_health",
36
+ "pm_data_events",
37
+ "pm_data_event",
38
+ "pm_data_whales",
39
+ "pm_data_disagreements",
40
+ "pm_data_calibration",
41
+ "pm_data_canonical",
42
+ "pm_data_volume_history",
43
+ "get_crypto_movers",
44
+ ];
45
+ const toolNames = new Set(COMPLETION_TOOL_NAMES);
46
+ const object = (value) => value !== null && typeof value === "object" && !Array.isArray(value)
47
+ ? value
48
+ : undefined;
49
+ const status = (value) => typeof value === "number" &&
50
+ Number.isInteger(value) &&
51
+ (value === 0 || (value >= 100 && value <= 599))
52
+ ? value
53
+ : null;
54
+ const empty = (operation = "invalid", tool = null) => ({
55
+ operation,
56
+ tool,
57
+ rpc_outcome: "no_response",
58
+ result_http_status: null,
59
+ result_ok: null,
60
+ });
61
+ /** Attach before JSON parsing; malformed/rejected HTTP requests remain invalid,
62
+ * no_response, with their actual HTTP status. Do not read bodies or raw errors.
63
+ * Authentication and caller origin are deliberately NOT inferred from headers.
64
+ */
65
+ export function observeHttpCompletion(req, res, logger = (line) => log(line)) {
66
+ const started = performance.now();
67
+ const credentialSupplied = Boolean(bearerFromHeader(req.headers.authorization) ??
68
+ bearerFromHeader(req.headers["x-coinrithm-api-key"]));
69
+ const records = [];
70
+ // IDs are transient correlations only, never copied into records/logs. Clear
71
+ // on completion so a late tool callback cannot retain IDs or emit twice.
72
+ const pending = new Map();
73
+ let completed = false;
74
+ let delivery;
75
+ let sendsInFlight = 0;
76
+ const emit = () => {
77
+ if (completed || !delivery || (delivery === "finished" && sendsInFlight))
78
+ return;
79
+ completed = true;
80
+ res.off("finish", onFinish);
81
+ res.off("close", onClose);
82
+ res.off("error", onAbort);
83
+ const elapsed = performance.now() - started;
84
+ const duration = Number.isFinite(elapsed)
85
+ ? Math.max(0, Math.round(elapsed * 1000) / 1000)
86
+ : 0;
87
+ const completedAt = new Date().toISOString();
88
+ // A pre-header disconnect has no HTTP status; Node's default 200 is not one.
89
+ const httpStatus = res.headersSent ? status(res.statusCode) : null;
90
+ const snapshots = records.length ? records : [empty()];
91
+ for (const rpc of snapshots) {
92
+ const record = {
93
+ event: "mcp_completion",
94
+ schema_version: 1,
95
+ completed_at: completedAt,
96
+ duration_ms: duration,
97
+ service_version: SERVER_VERSION,
98
+ transport: "streamable_http",
99
+ ...rpc,
100
+ credential_supplied: credentialSupplied,
101
+ http_status: httpStatus,
102
+ delivery,
103
+ };
104
+ try {
105
+ logger(JSON.stringify(record));
106
+ }
107
+ catch {
108
+ // Observability must never change a tool response or recurse into logging.
109
+ }
110
+ }
111
+ records.length = 0;
112
+ pending.clear();
113
+ };
114
+ const onFinish = () => {
115
+ delivery = "finished";
116
+ emit();
117
+ };
118
+ const onAbort = () => {
119
+ delivery = "aborted";
120
+ // A computed response in an interrupted stream is not confirmed delivered.
121
+ // In particular, a still-pending send may reject after this close event.
122
+ for (const rpc of records) {
123
+ if (rpc.rpc_outcome !== "not_applicable") {
124
+ rpc.rpc_outcome = "no_response";
125
+ rpc.result_http_status = null;
126
+ rpc.result_ok = null;
127
+ }
128
+ }
129
+ emit();
130
+ };
131
+ const onClose = () => {
132
+ if (!res.writableFinished)
133
+ onAbort();
134
+ else
135
+ onFinish();
136
+ };
137
+ res.once("finish", onFinish);
138
+ res.once("close", onClose);
139
+ res.once("error", onAbort);
140
+ const incoming = (message) => {
141
+ if (completed)
142
+ return;
143
+ if (!("method" in message)) {
144
+ records.push({ ...empty("other"), rpc_outcome: "not_applicable" });
145
+ return;
146
+ }
147
+ if (!("id" in message)) {
148
+ records.push({ ...empty("notification"), rpc_outcome: "not_applicable" });
149
+ return;
150
+ }
151
+ const operation = message.method === "initialize"
152
+ ? "initialize"
153
+ : message.method === "tools/list"
154
+ ? "tools_list"
155
+ : message.method === "tools/call"
156
+ ? "tools_call"
157
+ : "other";
158
+ const name = operation === "tools_call" ? object(message.params)?.name : undefined;
159
+ const tool = operation !== "tools_call"
160
+ ? null
161
+ : typeof name === "string" && toolNames.has(name)
162
+ ? name
163
+ : "unknown";
164
+ const rpc = empty(operation, tool);
165
+ records.push(rpc);
166
+ pending.set(message.id, [...(pending.get(message.id) ?? []), rpc]);
167
+ };
168
+ return {
169
+ attach(transport) {
170
+ // Call AFTER server.connect (which installs SDK callbacks), before handling
171
+ // this request. Observe only SDK-accepted messages, not unvalidated bodies.
172
+ const onmessage = transport.onmessage;
173
+ transport.onmessage = (message, extra) => {
174
+ incoming(message);
175
+ onmessage?.(message, extra);
176
+ };
177
+ const send = transport.send.bind(transport);
178
+ transport.send = async (message, options) => {
179
+ const matches = "id" in message ? pending.get(message.id) : undefined;
180
+ // Duplicate IDs are ambiguous; never attribute one response twice.
181
+ const rpc = !completed && matches?.length === 1 ? matches[0] : undefined;
182
+ if (!rpc || (!("result" in message) && !("error" in message))) {
183
+ return send(message, options);
184
+ }
185
+ // This is the SDK's FINAL result, after input/output schema validation.
186
+ rpc.result_http_status = null;
187
+ rpc.result_ok = null;
188
+ if ("error" in message)
189
+ rpc.rpc_outcome = "protocol_error";
190
+ else {
191
+ const result = object(message.result);
192
+ const structured = object(result?.structuredContent);
193
+ rpc.rpc_outcome =
194
+ rpc.operation === "tools_call" && result?.isError === true
195
+ ? "tool_error"
196
+ : "result";
197
+ if (rpc.operation === "tools_call") {
198
+ rpc.result_http_status = status(structured?.httpStatus);
199
+ rpc.result_ok =
200
+ typeof structured?.ok === "boolean" ? structured.ok : null;
201
+ }
202
+ }
203
+ sendsInFlight += 1;
204
+ try {
205
+ return await send(message, options);
206
+ }
207
+ catch (error) {
208
+ rpc.rpc_outcome = "no_response";
209
+ rpc.result_http_status = null;
210
+ rpc.result_ok = null;
211
+ throw error;
212
+ }
213
+ finally {
214
+ sendsInFlight -= 1;
215
+ emit();
216
+ }
217
+ };
218
+ },
219
+ };
220
+ }
@@ -0,0 +1 @@
1
+ export declare function retryAfterSeconds(value: string | null, now?: number): number | undefined;
@@ -0,0 +1,16 @@
1
+ // Retry-After permits a delay in seconds or an HTTP date. Missing/invalid
2
+ // evidence stays absent, so each caller can apply its explicit fallback.
3
+ export function retryAfterSeconds(value, now = Date.now()) {
4
+ const raw = value?.trim();
5
+ if (!raw)
6
+ return undefined;
7
+ if (/^\d+(?:\.\d+)?$/.test(raw)) {
8
+ const seconds = Number(raw);
9
+ return Number.isFinite(seconds) ? seconds : undefined;
10
+ }
11
+ // Avoid Date.parse treating malformed numeric delays (e.g. "-1") as dates.
12
+ if (!/^[A-Za-z]{3}(?:,|day,|\s)/.test(raw))
13
+ return undefined;
14
+ const date = Date.parse(raw);
15
+ return Number.isFinite(date) ? Math.max(0, (date - now) / 1000) : undefined;
16
+ }
package/dist/tools.js CHANGED
@@ -196,6 +196,8 @@ const EVENT_SUMMARY_FIELDS = [
196
196
  "startDate",
197
197
  "endDate",
198
198
  "resolvedAt",
199
+ "resolvedAtBasis",
200
+ "settlementWindowClosedAt",
199
201
  "freshness",
200
202
  "volume",
201
203
  "volume24h",
@@ -205,6 +207,8 @@ const EVENT_SUMMARY_FIELDS = [
205
207
  "bestBid",
206
208
  "bestAsk",
207
209
  "spread",
210
+ "spreadPoints",
211
+ "probabilityBook",
208
212
  "marketsCount",
209
213
  "referenceProbability",
210
214
  "crossPlatform",
@@ -215,6 +219,7 @@ const OUTCOME_SUMMARY_FIELDS = [
215
219
  "externalMarketId",
216
220
  "name",
217
221
  "probability",
222
+ "normalizedProbability",
218
223
  "priceChange24h",
219
224
  ];
220
225
  const WHALE_TRADE_FIELDS = [
@@ -253,7 +258,17 @@ function eventSummary(value) {
253
258
  if (!isJsonRecord(value))
254
259
  return value;
255
260
  const source = isJsonRecord(value.source)
256
- ? pick(value.source, ["id", "name", "kind", "supportsTrading"])
261
+ ? pick(value.source, [
262
+ "id",
263
+ "name",
264
+ "kind",
265
+ "supportsTrading",
266
+ // Quotes remain venue-native. Magnitude cannot distinguish a 0.5-point
267
+ // bid from a 0.5 ratio; keep the API's explicit unit with the number.
268
+ "quoteScale",
269
+ "methodology",
270
+ "supportsMarketMetrics",
271
+ ])
257
272
  : value.source;
258
273
  const outcomes = Array.isArray(value.outcomes)
259
274
  ? value.outcomes
@@ -799,6 +814,9 @@ export function registerTools(server, client) {
799
814
  "1H=60x1-minute, 1D=288x5-minute, 1W=672x15-minute, 1M=720x1-hour, " +
800
815
  "3M=540x4-hour candles. Candles are oldest to newest with t in unix " +
801
816
  "SECONDS; o/h/l/c in fiat (default USD), v always in USD. " +
817
+ "These are sampled composite-price bars, not venue trade candles. " +
818
+ "v is the mean rolling 24-hour quote-volume observation in the bar, " +
819
+ "NOT volume traded during that candle; do not sum v across bars. " +
802
820
  PAPER_NOTE,
803
821
  inputSchema: {
804
822
  coinId: z
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@coinrithm/mcp-trading",
3
- "version": "0.7.8",
3
+ "version": "0.7.9",
4
4
  "mcpName": "io.github.CoinRithm/mcp-trading",
5
5
  "description": "CoinRithm paper-trading toolkit: an MCP server (coinrithm-mcp) AND a self-host agent runner (coinrithm-agent) for spot, futures, and prediction markets with a user-minted API key.",
6
6
  "type": "module",
@@ -31,6 +31,7 @@
31
31
  "build": "tsc -p tsconfig.json",
32
32
  "typecheck": "tsc -p tsconfig.json --noEmit",
33
33
  "test": "vitest run",
34
+ "test:coverage": "vitest run --coverage",
34
35
  "lint": "eslint src",
35
36
  "lint:fix": "eslint src --fix",
36
37
  "format": "prettier --write \"src/**/*.ts\"",
@@ -77,10 +78,11 @@
77
78
  "@eslint/js": "^9.13.0",
78
79
  "@types/express": "^4.17.21",
79
80
  "@types/node": "^20.14.0",
81
+ "@vitest/coverage-v8": "4.1.11",
80
82
  "eslint": "^9.13.0",
81
83
  "prettier": "^3.3.3",
82
84
  "typescript": "^5.5.0",
83
85
  "typescript-eslint": "^8.11.0",
84
- "vitest": "^2.1.0"
86
+ "vitest": "4.1.11"
85
87
  }
86
88
  }