@coinrithm/mcp-trading 0.7.14 → 0.7.16
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 +811 -613
- package/README.md +415 -366
- package/dist/agent/bench/bench.d.ts +59 -0
- package/dist/agent/bench/bench.js +319 -0
- package/dist/agent/bench/cassette.d.ts +97 -0
- package/dist/agent/bench/cassette.js +181 -0
- package/dist/agent/bench/costs.d.ts +28 -0
- package/dist/agent/bench/costs.js +60 -0
- package/dist/agent/bench/labelBuilder.d.ts +123 -0
- package/dist/agent/bench/labelBuilder.js +282 -0
- package/dist/agent/bench/labels.d.ts +75 -0
- package/dist/agent/bench/labels.js +259 -0
- package/dist/agent/bench/recordingClient.d.ts +51 -0
- package/dist/agent/bench/recordingClient.js +193 -0
- package/dist/agent/bench/replayClient.d.ts +41 -0
- package/dist/agent/bench/replayClient.js +262 -0
- package/dist/agent/bench/report.d.ts +115 -0
- package/dist/agent/bench/report.js +435 -0
- package/dist/agent/cli.d.ts +25 -0
- package/dist/agent/cli.js +300 -2
- package/dist/agent/client.d.ts +14 -0
- package/dist/agent/client.js +113 -4
- package/dist/agent/configurationWarnings.js +11 -0
- package/dist/agent/decision.d.ts +3 -0
- package/dist/agent/decision.js +85 -3
- package/dist/agent/decisionReceipt.d.ts +5 -1
- package/dist/agent/decisionReceipt.js +52 -3
- package/dist/agent/decisionValidator.d.ts +30 -0
- package/dist/agent/decisionValidator.js +147 -2
- package/dist/agent/engine.d.ts +3 -3
- package/dist/agent/engine.js +3 -3
- package/dist/agent/futuresEligibility.d.ts +36 -0
- package/dist/agent/futuresEligibility.js +90 -0
- package/dist/agent/observe.d.ts +35 -2
- package/dist/agent/observe.js +969 -128
- package/dist/agent/pmContext.d.ts +10 -1
- package/dist/agent/pmContext.js +163 -1
- package/dist/agent/prompt.d.ts +18 -1
- package/dist/agent/prompt.js +229 -33
- package/dist/agent/promptTables.d.ts +1 -0
- package/dist/agent/promptTables.js +33 -0
- package/dist/agent/providerCapabilities.d.ts +9 -0
- package/dist/agent/providerCapabilities.js +57 -4
- package/dist/agent/providers.d.ts +28 -7
- package/dist/agent/providers.js +200 -50
- package/dist/agent/resolve.js +3 -0
- package/dist/agent/runner.d.ts +8 -0
- package/dist/agent/runner.js +159 -26
- package/dist/agent/setups.d.ts +2 -1
- package/dist/agent/setups.js +17 -16
- package/dist/agent/signals.d.ts +33 -0
- package/dist/agent/signals.js +89 -0
- package/dist/agent/skill.js +37 -0
- package/dist/agent/skillValidator.js +33 -0
- package/dist/agent/strictLint.js +29 -0
- package/dist/agent/templates.js +30 -30
- package/dist/agent/types.d.ts +266 -13
- package/dist/agent/types.js +4 -0
- package/dist/agent/universe.d.ts +78 -0
- package/dist/agent/universe.js +215 -0
- package/dist/client.d.ts +9 -2
- package/dist/client.js +22 -10
- package/dist/http.d.ts +1 -0
- package/dist/http.js +17 -2
- package/dist/httpCompletion.d.ts +1 -1
- package/dist/httpCompletion.js +1 -0
- package/dist/index.js +2 -1
- package/dist/instructions.d.ts +1 -0
- package/dist/instructions.js +16 -0
- package/dist/tools.d.ts +1 -0
- package/dist/tools.js +150 -66
- package/package.json +98 -97
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
// Agent market BOUNDARIES (owner 2026-10-07, Telegram 56641/56644). Instead of
|
|
2
|
+
// a fixed coin list, a bundle declares where its agent may look:
|
|
3
|
+
//
|
|
4
|
+
// universe:
|
|
5
|
+
// rank: { min: 1, max: 100 } # market-cap rank band
|
|
6
|
+
// minVolume24hUsd: 1000000 # liquidity floor
|
|
7
|
+
// excludeStablecoins: true
|
|
8
|
+
// includeSectors: [defi, ai] # curated ids, GET /api/agent/universe/sectors
|
|
9
|
+
// excludeSectors: [meme]
|
|
10
|
+
// sort: abs_change_24h # what "an opportunity" means first
|
|
11
|
+
// resolveTop: 6 # rows resolved into full watch entries
|
|
12
|
+
//
|
|
13
|
+
// Each cycle observe() asks GET /api/agent/universe for the rows inside those
|
|
14
|
+
// boundaries, resolves the top few into full watch entries (marked
|
|
15
|
+
// `discovered: true`) and passes the rest as compact context. The validator's
|
|
16
|
+
// tradable set is watch membership, so a discovered row is tradable for that
|
|
17
|
+
// cycle and every cap still applies; the blocklist still wins. The server
|
|
18
|
+
// filters, and filterUniverseRows re-checks what the row says about itself,
|
|
19
|
+
// so a wrong or older server can narrow the set but never widen it.
|
|
20
|
+
export const UNIVERSE_SORTS = [
|
|
21
|
+
"gainers_24h",
|
|
22
|
+
"losers_24h",
|
|
23
|
+
"abs_change_24h",
|
|
24
|
+
"abs_change_1h",
|
|
25
|
+
"volume_24h",
|
|
26
|
+
"rank",
|
|
27
|
+
];
|
|
28
|
+
export const UNIVERSE_DEFAULTS = {
|
|
29
|
+
rankMin: 1,
|
|
30
|
+
rankMax: 100,
|
|
31
|
+
minVolume24hUsd: 0,
|
|
32
|
+
sort: "abs_change_24h",
|
|
33
|
+
resolveTop: 6,
|
|
34
|
+
scanLimit: 15,
|
|
35
|
+
};
|
|
36
|
+
export const UNIVERSE_MAX_RESOLVE_TOP = 10;
|
|
37
|
+
export const UNIVERSE_MAX_SCAN_LIMIT = 50;
|
|
38
|
+
const SECTOR_ID_RE = /^[a-z0-9][a-z0-9-]{0,63}$/;
|
|
39
|
+
/** True when the agent discovers candidates beyond its watchlist. */
|
|
40
|
+
export function scansUniverse(spec) {
|
|
41
|
+
return (spec.universe !== undefined || spec.capabilities.includes("universe_scan"));
|
|
42
|
+
}
|
|
43
|
+
/** The screener query a validated universe block asks for. */
|
|
44
|
+
export function universeQuery(u) {
|
|
45
|
+
return {
|
|
46
|
+
rankMin: u.rank?.min ?? UNIVERSE_DEFAULTS.rankMin,
|
|
47
|
+
rankMax: u.rank?.max ?? UNIVERSE_DEFAULTS.rankMax,
|
|
48
|
+
minVolume24hUsd: u.minVolume24hUsd ?? UNIVERSE_DEFAULTS.minVolume24hUsd,
|
|
49
|
+
excludeStablecoins: u.excludeStablecoins ?? false,
|
|
50
|
+
includeSectors: [...(u.includeSectors ?? [])],
|
|
51
|
+
excludeSectors: [...(u.excludeSectors ?? [])],
|
|
52
|
+
sort: u.sort ?? UNIVERSE_DEFAULTS.sort,
|
|
53
|
+
limit: u.scanLimit ?? UNIVERSE_DEFAULTS.scanLimit,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
/** How many screener rows become full watch entries this cycle. */
|
|
57
|
+
export function universeResolveTop(u) {
|
|
58
|
+
return Math.min(u.resolveTop ?? UNIVERSE_DEFAULTS.resolveTop, UNIVERSE_MAX_RESOLVE_TOP);
|
|
59
|
+
}
|
|
60
|
+
/** Wire form: csv sectors, string numbers; empty filters are omitted. With
|
|
61
|
+
* `ucids`, the same boundaries are applied to just those coins (membership). */
|
|
62
|
+
export function universeQueryParams(q, ucids) {
|
|
63
|
+
return {
|
|
64
|
+
...(ucids && ucids.length > 0 ? { ucids: ucids.join(",") } : {}),
|
|
65
|
+
rankMin: q.rankMin,
|
|
66
|
+
rankMax: q.rankMax,
|
|
67
|
+
minVolume24hUsd: q.minVolume24hUsd,
|
|
68
|
+
excludeStablecoins: q.excludeStablecoins ? "true" : "false",
|
|
69
|
+
...(q.includeSectors.length
|
|
70
|
+
? { includeSectors: q.includeSectors.join(",") }
|
|
71
|
+
: {}),
|
|
72
|
+
...(q.excludeSectors.length
|
|
73
|
+
? { excludeSectors: q.excludeSectors.join(",") }
|
|
74
|
+
: {}),
|
|
75
|
+
sort: q.sort,
|
|
76
|
+
limit: q.limit,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
/** Keep malformed evidence unknown; dropping bad elements can widen a screen. */
|
|
80
|
+
export function universeSectorsOf(raw) {
|
|
81
|
+
return Array.isArray(raw) &&
|
|
82
|
+
raw.every((s) => typeof s === "string" && SECTOR_ID_RE.test(s))
|
|
83
|
+
? raw
|
|
84
|
+
: undefined;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Defence in depth on server rows (never widen): a row is kept only when its
|
|
88
|
+
* own fields PROVE it is inside every enabled boundary. The rank band is
|
|
89
|
+
* always enabled, so a row without a rank is dropped; a volume floor needs a
|
|
90
|
+
* volume; any sector rule (stablecoins out, sectors out, sectors in) needs the
|
|
91
|
+
* row's sector list. Missing or malformed evidence drops the row (root review
|
|
92
|
+
* 2026-10-07: unverifiable is not inside).
|
|
93
|
+
*/
|
|
94
|
+
export function filterUniverseRows(rows, q) {
|
|
95
|
+
const excluded = new Set([
|
|
96
|
+
...q.excludeSectors,
|
|
97
|
+
...(q.excludeStablecoins ? ["stablecoins"] : []),
|
|
98
|
+
]);
|
|
99
|
+
const included = q.includeSectors.filter((s) => !excluded.has(s));
|
|
100
|
+
const sectorRules = excluded.size > 0 || q.includeSectors.length > 0;
|
|
101
|
+
return rows.filter((r) => {
|
|
102
|
+
const rank = r.marketCapRank;
|
|
103
|
+
if (typeof rank !== "number" || !Number.isFinite(rank))
|
|
104
|
+
return false;
|
|
105
|
+
if (rank < q.rankMin || rank > q.rankMax)
|
|
106
|
+
return false;
|
|
107
|
+
if (q.minVolume24hUsd > 0) {
|
|
108
|
+
const vol = r.volume24hUsd;
|
|
109
|
+
if (typeof vol !== "number" || !Number.isFinite(vol))
|
|
110
|
+
return false;
|
|
111
|
+
if (vol < q.minVolume24hUsd)
|
|
112
|
+
return false;
|
|
113
|
+
}
|
|
114
|
+
if (sectorRules) {
|
|
115
|
+
const sectors = universeSectorsOf(r.sectors);
|
|
116
|
+
if (sectors === undefined)
|
|
117
|
+
return false;
|
|
118
|
+
if (sectors.some((s) => excluded.has(s)))
|
|
119
|
+
return false;
|
|
120
|
+
if (q.includeSectors.length > 0 &&
|
|
121
|
+
!sectors.some((s) => included.includes(s)))
|
|
122
|
+
return false;
|
|
123
|
+
}
|
|
124
|
+
return true;
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Entry eligibility under declared boundaries (root review 2026-10-07): with a
|
|
129
|
+
* `universe` block, a NEW position may only be opened on a coin proven inside
|
|
130
|
+
* the boundaries this cycle: a discovered row, or a watchlist coin the
|
|
131
|
+
* screener confirmed (`withinBoundaries: true`). Anything else (outside, or
|
|
132
|
+
* unverifiable because the check failed) stays visible as context and can
|
|
133
|
+
* still be closed or sold, but not entered. Returns the rejection reason, or
|
|
134
|
+
* null when the entry is allowed. No universe block = no extra rule.
|
|
135
|
+
*/
|
|
136
|
+
export function universeEntryBlock(spec, entry) {
|
|
137
|
+
if (!spec.universe)
|
|
138
|
+
return null;
|
|
139
|
+
if (entry.discovered === true || entry.withinBoundaries === true)
|
|
140
|
+
return null;
|
|
141
|
+
return entry.withinBoundaries === false
|
|
142
|
+
? "outside the declared market boundaries"
|
|
143
|
+
: "not verified inside the declared market boundaries this cycle";
|
|
144
|
+
}
|
|
145
|
+
/** Validation issues for a raw `universe` block ([] when valid or absent). */
|
|
146
|
+
export function universeIssues(raw) {
|
|
147
|
+
if (raw === undefined)
|
|
148
|
+
return [];
|
|
149
|
+
const issues = [];
|
|
150
|
+
const add = (code, reason) => issues.push([code, reason]);
|
|
151
|
+
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
|
|
152
|
+
add("skill_universe", "universe must be a mapping");
|
|
153
|
+
return issues;
|
|
154
|
+
}
|
|
155
|
+
const u = raw;
|
|
156
|
+
const int = (v, lo, hi) => typeof v === "number" && Number.isInteger(v) && v >= lo && v <= hi;
|
|
157
|
+
if (u.rank !== undefined) {
|
|
158
|
+
const rank = u.rank;
|
|
159
|
+
if (rank === null || typeof rank !== "object" || Array.isArray(rank)) {
|
|
160
|
+
add("skill_universe_rank", "universe.rank must be { min, max }");
|
|
161
|
+
}
|
|
162
|
+
else {
|
|
163
|
+
if (rank.min !== undefined && !int(rank.min, 1, 10_000))
|
|
164
|
+
add("skill_universe_rank", "universe.rank.min must be a whole number between 1 and 10000");
|
|
165
|
+
if (rank.max !== undefined && !int(rank.max, 1, 10_000))
|
|
166
|
+
add("skill_universe_rank", "universe.rank.max must be a whole number between 1 and 10000");
|
|
167
|
+
const min = rank.min ?? UNIVERSE_DEFAULTS.rankMin;
|
|
168
|
+
const max = rank.max ?? UNIVERSE_DEFAULTS.rankMax;
|
|
169
|
+
if (int(min, 1, 10_000) && int(max, 1, 10_000) && min > max)
|
|
170
|
+
add("skill_universe_rank", "universe.rank.min must be <= rank.max");
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
if (u.minVolume24hUsd !== undefined &&
|
|
174
|
+
!(typeof u.minVolume24hUsd === "number" &&
|
|
175
|
+
Number.isFinite(u.minVolume24hUsd) &&
|
|
176
|
+
u.minVolume24hUsd >= 0))
|
|
177
|
+
add("skill_universe_volume", "universe.minVolume24hUsd must be a number >= 0");
|
|
178
|
+
if (u.excludeStablecoins !== undefined &&
|
|
179
|
+
typeof u.excludeStablecoins !== "boolean")
|
|
180
|
+
add("skill_universe_stablecoins", "universe.excludeStablecoins must be true or false");
|
|
181
|
+
for (const key of ["includeSectors", "excludeSectors"]) {
|
|
182
|
+
const v = u[key];
|
|
183
|
+
if (v === undefined)
|
|
184
|
+
continue;
|
|
185
|
+
if (!Array.isArray(v) ||
|
|
186
|
+
v.length > 50 ||
|
|
187
|
+
!v.every((s) => typeof s === "string" && SECTOR_ID_RE.test(s)))
|
|
188
|
+
add("skill_universe_sectors", `universe.${key} must be a list of sector ids like "defi" or "eco-solana" (GET /api/agent/universe/sectors)`);
|
|
189
|
+
}
|
|
190
|
+
if (u.sort !== undefined &&
|
|
191
|
+
!UNIVERSE_SORTS.includes(u.sort))
|
|
192
|
+
add("skill_universe_sort", `universe.sort must be one of ${UNIVERSE_SORTS.join(", ")}`);
|
|
193
|
+
if (u.resolveTop !== undefined &&
|
|
194
|
+
!int(u.resolveTop, 1, UNIVERSE_MAX_RESOLVE_TOP))
|
|
195
|
+
add("skill_universe_resolve_top", `universe.resolveTop must be a whole number between 1 and ${UNIVERSE_MAX_RESOLVE_TOP}`);
|
|
196
|
+
if (u.scanLimit !== undefined &&
|
|
197
|
+
!int(u.scanLimit, 1, UNIVERSE_MAX_SCAN_LIMIT))
|
|
198
|
+
add("skill_universe_scan_limit", `universe.scanLimit must be a whole number between 1 and ${UNIVERSE_MAX_SCAN_LIMIT}`);
|
|
199
|
+
return issues;
|
|
200
|
+
}
|
|
201
|
+
/** One line for the prompt: the boundaries in plain words. */
|
|
202
|
+
export function describeUniverse(u) {
|
|
203
|
+
const q = universeQuery(u);
|
|
204
|
+
const parts = [`market-cap rank ${q.rankMin}-${q.rankMax}`];
|
|
205
|
+
if (q.minVolume24hUsd > 0)
|
|
206
|
+
parts.push(`24h volume >= $${Math.round(q.minVolume24hUsd).toLocaleString("en-US")}`);
|
|
207
|
+
if (q.excludeStablecoins)
|
|
208
|
+
parts.push("no stablecoins");
|
|
209
|
+
if (q.includeSectors.length)
|
|
210
|
+
parts.push(`only sectors: ${q.includeSectors.join(", ")}`);
|
|
211
|
+
if (q.excludeSectors.length)
|
|
212
|
+
parts.push(`excluding sectors: ${q.excludeSectors.join(", ")}`);
|
|
213
|
+
parts.push(`ranked by ${q.sort.replace(/_/g, " ")}`);
|
|
214
|
+
return parts.join("; ");
|
|
215
|
+
}
|
package/dist/client.d.ts
CHANGED
|
@@ -39,6 +39,7 @@ export type ProvenanceReport = {
|
|
|
39
39
|
sourceCapturedAt?: string;
|
|
40
40
|
};
|
|
41
41
|
};
|
|
42
|
+
export declare const API_KEY_MINT_URL = "https://www.coinrithm.com/en/settings/api-keys";
|
|
42
43
|
export declare class CoinRithmClient {
|
|
43
44
|
private readonly defaultApiKey?;
|
|
44
45
|
private readonly baseUrl;
|
|
@@ -113,6 +114,12 @@ export declare class CoinRithmClient {
|
|
|
113
114
|
range?: "1H" | "1D" | "1W" | "1M" | "3M";
|
|
114
115
|
fiat?: string;
|
|
115
116
|
}, apiKey?: string, agentTrace?: AgentTrace): Promise<ApiResult>;
|
|
117
|
+
getNews(query: {
|
|
118
|
+
coins: string;
|
|
119
|
+
limit?: number;
|
|
120
|
+
hours?: number;
|
|
121
|
+
minImportance?: number;
|
|
122
|
+
}, apiKey?: string, agentTrace?: AgentTrace): Promise<ApiResult>;
|
|
116
123
|
discoverPmMarkets(query?: {
|
|
117
124
|
q?: string;
|
|
118
125
|
source?: "all" | "kalshi" | "polymarket";
|
|
@@ -145,8 +152,8 @@ export declare class CoinRithmClient {
|
|
|
145
152
|
page?: number;
|
|
146
153
|
pageSize?: number;
|
|
147
154
|
window?: "7d" | "30d" | "all";
|
|
148
|
-
}
|
|
149
|
-
getArenaAgent(handle: string
|
|
155
|
+
}): Promise<ApiResult>;
|
|
156
|
+
getArenaAgent(handle: string): Promise<ApiResult>;
|
|
150
157
|
listOpenOrders(query?: {
|
|
151
158
|
coinId?: string;
|
|
152
159
|
limit?: number;
|
package/dist/client.js
CHANGED
|
@@ -71,6 +71,9 @@ const applyAgentTraceHeaders = (headers, trace) => {
|
|
|
71
71
|
const traceFromBody = (body) => body && typeof body === "object" && "agentTrace" in body
|
|
72
72
|
? body.agentTrace
|
|
73
73
|
: undefined;
|
|
74
|
+
// Where a user mints an API key (Settings -> API Keys). Named in every
|
|
75
|
+
// missing-key error so an assistant can tell its user where to go.
|
|
76
|
+
export const API_KEY_MINT_URL = "https://www.coinrithm.com/en/settings/api-keys";
|
|
74
77
|
export class CoinRithmClient {
|
|
75
78
|
// Default key for the stdio (single-user) path. Undefined in the multi-user
|
|
76
79
|
// HTTP path, where every call must pass a per-request `apiKey` override.
|
|
@@ -88,8 +91,10 @@ export class CoinRithmClient {
|
|
|
88
91
|
status: 401,
|
|
89
92
|
data: {
|
|
90
93
|
error: "missing_api_key",
|
|
91
|
-
message: "No API key for this request.
|
|
92
|
-
|
|
94
|
+
message: "No API key for this request. Mint a free key at " +
|
|
95
|
+
`${API_KEY_MINT_URL}, then on the hosted MCP send ` +
|
|
96
|
+
"`Authorization: Bearer crk_live_…`. Public data tools " +
|
|
97
|
+
"(pm_data_*, get_arena_*, get_crypto_movers) need no key.",
|
|
93
98
|
},
|
|
94
99
|
};
|
|
95
100
|
}
|
|
@@ -337,6 +342,13 @@ export class CoinRithmClient {
|
|
|
337
342
|
getCandles(coinId, query, apiKey, agentTrace) {
|
|
338
343
|
return this.request("GET", `/api/agent/market/${encodeURIComponent(coinId)}/candles`, { query, apiKey, agentTrace });
|
|
339
344
|
}
|
|
345
|
+
getNews(query, apiKey, agentTrace) {
|
|
346
|
+
return this.request("GET", "/api/agent/news", {
|
|
347
|
+
query,
|
|
348
|
+
apiKey,
|
|
349
|
+
agentTrace,
|
|
350
|
+
});
|
|
351
|
+
}
|
|
340
352
|
discoverPmMarkets(query, apiKey, agentTrace) {
|
|
341
353
|
return this.request("GET", "/api/agent/pm/discover", {
|
|
342
354
|
query,
|
|
@@ -364,15 +376,15 @@ export class CoinRithmClient {
|
|
|
364
376
|
agentTrace,
|
|
365
377
|
});
|
|
366
378
|
}
|
|
367
|
-
// Agent Arena (public leaderboard)
|
|
368
|
-
//
|
|
369
|
-
|
|
370
|
-
|
|
379
|
+
// Agent Arena (public leaderboard): public agent names + realized
|
|
380
|
+
// performance only, served keyless by the API. These used to go through
|
|
381
|
+
// the keyed path, so a keyless MCP client got a 401 for public data
|
|
382
|
+
// (21 of 44 keyless tool calls failed in 14.6h, audit 2026-09-29).
|
|
383
|
+
getArenaLeaderboard(query) {
|
|
384
|
+
return this.publicRequest("/api/arena", query);
|
|
371
385
|
}
|
|
372
|
-
getArenaAgent(handle
|
|
373
|
-
return this.
|
|
374
|
-
apiKey,
|
|
375
|
-
});
|
|
386
|
+
getArenaAgent(handle) {
|
|
387
|
+
return this.publicRequest(`/api/arena/${encodeURIComponent(handle)}`);
|
|
376
388
|
}
|
|
377
389
|
listOpenOrders(query, apiKey, agentTrace) {
|
|
378
390
|
return this.request("GET", "/api/agent/orders/open", {
|
package/dist/http.d.ts
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
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
|
+
export declare const MALFORMED_MCP_LINK: RegExp;
|
|
5
6
|
export declare function createHttpApp(client: CoinRithmClient, options?: {
|
|
6
7
|
completionLogger?: CompletionLogger;
|
|
7
8
|
createServer?: () => McpServer;
|
package/dist/http.js
CHANGED
|
@@ -34,6 +34,7 @@
|
|
|
34
34
|
// Transport is stateless: a fresh McpServer + transport per request, which is
|
|
35
35
|
// the correct isolation model for a multi-user, per-request-keyed surface — no
|
|
36
36
|
// session state is shared between users.
|
|
37
|
+
import { SERVER_INSTRUCTIONS } from "./instructions.js";
|
|
37
38
|
import express from "express";
|
|
38
39
|
import { pathToFileURL } from "node:url";
|
|
39
40
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
@@ -42,6 +43,9 @@ import { CoinRithmClient, bearerFromHeader, loadHttpConfig, log, } from "./clien
|
|
|
42
43
|
import { registerTools } from "./tools.js";
|
|
43
44
|
import { SERVER_VERSION } from "./version.js";
|
|
44
45
|
import { observeHttpCompletion, } from "./httpCompletion.js";
|
|
46
|
+
// "/mcp" immediately followed by the markdown "](" of a pasted link, literal or
|
|
47
|
+
// percent-encoded. Anything else under /mcp is not this family.
|
|
48
|
+
export const MALFORMED_MCP_LINK = /^\/mcp(?:\]|%5d)(?:\(|%28)/i;
|
|
45
49
|
// Factory permits isolated localhost SDK tests without opening a listener on import.
|
|
46
50
|
export function createHttpApp(client, options = {}) {
|
|
47
51
|
const app = express();
|
|
@@ -108,13 +112,24 @@ export function createHttpApp(client, options = {}) {
|
|
|
108
112
|
app.get("/robots.txt", (_req, res) => {
|
|
109
113
|
res.type("text/plain").send("User-agent: *\nDisallow: /\n");
|
|
110
114
|
});
|
|
115
|
+
// Recover one observed malformed URL family. A markdown link to this
|
|
116
|
+
// endpoint circulates with its "](https://…)" suffix pasted into the path,
|
|
117
|
+
// e.g. GET /mcp](https:/mcp.coinrithm.com/mcp): 408 such requests on
|
|
118
|
+
// 2026-09-25, all Claude-User web fetches that received Express's default
|
|
119
|
+
// 404. GET/HEAD go to the service descriptor. POST is served as MCP below:
|
|
120
|
+
// on 2026-09-26, 127 Claude-User connector POSTs in 6 h hit the same path
|
|
121
|
+
// and got 404, so a user who pasted that URL into a connector could never
|
|
122
|
+
// initialize. Every other unknown path behaves as before.
|
|
123
|
+
app.get(MALFORMED_MCP_LINK, (_req, res) => {
|
|
124
|
+
res.redirect(302, "/");
|
|
125
|
+
});
|
|
111
126
|
app.get("/mcp", (_req, res) => {
|
|
112
127
|
res.status(405).json({
|
|
113
128
|
error: "method_not_allowed",
|
|
114
129
|
message: "This is a POST-only streamable-HTTP MCP endpoint. Connect with an MCP client, or GET / for service info.",
|
|
115
130
|
});
|
|
116
131
|
});
|
|
117
|
-
app.post("/mcp", async (req, res) => {
|
|
132
|
+
app.post(["/mcp", MALFORMED_MCP_LINK], async (req, res) => {
|
|
118
133
|
// Per-request auth: read THIS caller's key from the Authorization header,
|
|
119
134
|
// or from Smithery's non-reserved forwarding header.
|
|
120
135
|
// It is optional at the transport layer so registries can initialize the
|
|
@@ -132,7 +147,7 @@ export function createHttpApp(client, options = {}) {
|
|
|
132
147
|
new McpServer({
|
|
133
148
|
name: "coinrithm-trading",
|
|
134
149
|
version: SERVER_VERSION,
|
|
135
|
-
});
|
|
150
|
+
}, { instructions: SERVER_INSTRUCTIONS });
|
|
136
151
|
if (!options.createServer)
|
|
137
152
|
registerTools(server, client);
|
|
138
153
|
const transport = new StreamableHTTPServerTransport({
|
package/dist/httpCompletion.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
2
2
|
import type { Transport } from "@modelcontextprotocol/sdk/shared/transport.js";
|
|
3
|
-
export declare const COMPLETION_TOOL_NAMES: readonly ["whoami", "get_portfolio", "get_wallet", "list_open_orders", "get_positions", "resolve_symbol", "get_equity_curve", "get_my_trades", "get_market_context", "get_candles", "discover_pm_markets", "get_performance", "get_agent_ledger", "export_agent_ledger", "export_run_evidence", "get_arena_leaderboard", "get_arena_agent", "futures_quote", "pm_quote", "spot_quote", "place_spot_order", "cancel_spot_order", "open_futures_position", "set_futures_sl_tp", "close_futures_position", "open_pm_position", "report_pm_opportunity", "pm_data_overview", "pm_data_sources", "pm_data_sources_health", "pm_data_events", "pm_data_event", "pm_data_whales", "pm_data_whale_wallets", "pm_data_whale_wallet", "pm_data_disagreements", "pm_data_calibration", "pm_data_canonical", "pm_data_volume_history", "get_crypto_movers"];
|
|
3
|
+
export declare const COMPLETION_TOOL_NAMES: readonly ["whoami", "get_portfolio", "get_wallet", "list_open_orders", "get_positions", "resolve_symbol", "get_equity_curve", "get_my_trades", "get_market_context", "get_candles", "get_news", "discover_pm_markets", "get_performance", "get_agent_ledger", "export_agent_ledger", "export_run_evidence", "get_arena_leaderboard", "get_arena_agent", "futures_quote", "pm_quote", "spot_quote", "place_spot_order", "cancel_spot_order", "open_futures_position", "set_futures_sl_tp", "close_futures_position", "open_pm_position", "report_pm_opportunity", "pm_data_overview", "pm_data_sources", "pm_data_sources_health", "pm_data_events", "pm_data_event", "pm_data_whales", "pm_data_whale_wallets", "pm_data_whale_wallet", "pm_data_disagreements", "pm_data_calibration", "pm_data_canonical", "pm_data_volume_history", "get_crypto_movers"];
|
|
4
4
|
type Operation = "initialize" | "tools_list" | "tools_call" | "notification" | "other" | "invalid";
|
|
5
5
|
type Outcome = "result" | "tool_error" | "protocol_error" | "no_response" | "not_applicable";
|
|
6
6
|
type ToolName = (typeof COMPLETION_TOOL_NAMES)[number] | "unknown" | null;
|
package/dist/httpCompletion.js
CHANGED
package/dist/index.js
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
// COINRITHM_API_URL (optional) base URL; defaults to https://api.coinrithm.com
|
|
11
11
|
//
|
|
12
12
|
// stdout is the JSON-RPC channel — we log ONLY to stderr.
|
|
13
|
+
import { SERVER_INSTRUCTIONS } from "./instructions.js";
|
|
13
14
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
14
15
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
15
16
|
import { CoinRithmClient, loadConfig, log } from "./client.js";
|
|
@@ -21,7 +22,7 @@ async function main() {
|
|
|
21
22
|
const server = new McpServer({
|
|
22
23
|
name: "coinrithm-trading",
|
|
23
24
|
version: SERVER_VERSION,
|
|
24
|
-
});
|
|
25
|
+
}, { instructions: SERVER_INSTRUCTIONS });
|
|
25
26
|
registerTools(server, client);
|
|
26
27
|
const transport = new StdioServerTransport();
|
|
27
28
|
await server.connect(transport);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const SERVER_INSTRUCTIONS: string;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { API_KEY_MINT_URL } from "./client.js";
|
|
2
|
+
import { PAPER_NOTE } from "./tools.js";
|
|
3
|
+
// Sent once in the MCP `initialize` result. Guidance that used to be repeated
|
|
4
|
+
// on every tool description lives here, so the tool list stays small enough to
|
|
5
|
+
// fit an assistant's context (tools/list was 114,668 bytes with the ~512-char
|
|
6
|
+
// paper disclaimer on 25 tools, audit 2026-09-29).
|
|
7
|
+
export const SERVER_INSTRUCTIONS = [
|
|
8
|
+
"CoinRithm MCP: crypto and prediction-market data from 12 venues, plus paper trading.",
|
|
9
|
+
"Start keyless: the pm_data_* tools, get_arena_leaderboard, get_arena_agent and get_crypto_movers need no API key.",
|
|
10
|
+
"pm_data_events returns open markets by default; pass status=closed or status=all for history.",
|
|
11
|
+
`Tools that read or act on an account need a key. The user mints one at ${API_KEY_MINT_URL}; start with a read-only key.`,
|
|
12
|
+
"Every tool returns { httpStatus, ok, ledgerEventId, ledgerStatus, body }: httpStatus is CoinRithm's HTTP status (0 = network error), ok means 2xx, body is the parsed response.",
|
|
13
|
+
"Always quote before a paper write (spot_quote, futures_quote or pm_quote), then open with that quote.",
|
|
14
|
+
"Prediction-market probabilities are venue prices, not CoinRithm forecasts. referenceProbability is a cross-venue consensus with a disagreement band (spreadPoints).",
|
|
15
|
+
PAPER_NOTE,
|
|
16
|
+
].join("\n");
|
package/dist/tools.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
2
|
import { CoinRithmClient } from "./client.js";
|
|
3
3
|
export declare const PAPER_NOTE: string;
|
|
4
|
+
export declare const PAPER_NOTE_SHORT: string;
|
|
4
5
|
/**
|
|
5
6
|
* Keep keyless discovery calls small enough for an agent context window.
|
|
6
7
|
* Full event evidence remains available from pm_data_event.
|