@coinrithm/mcp-trading 0.7.7 → 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.
- package/CHANGELOG.md +178 -0
- package/README.md +86 -69
- package/dist/agent/act.js +19 -6
- package/dist/agent/capitalSizing.d.ts +32 -0
- package/dist/agent/capitalSizing.js +270 -0
- package/dist/agent/client.d.ts +8 -1
- package/dist/agent/client.js +98 -39
- package/dist/agent/decision.d.ts +392 -0
- package/dist/agent/decision.js +177 -0
- package/dist/agent/decisionReceipt.d.ts +48 -0
- package/dist/agent/decisionReceipt.js +619 -0
- package/dist/agent/decisionValidator.d.ts +19 -2
- package/dist/agent/decisionValidator.js +74 -3
- package/dist/agent/engine.d.ts +1 -0
- package/dist/agent/engine.js +1 -0
- package/dist/agent/gate.js +14 -11
- package/dist/agent/observe.js +175 -34
- package/dist/agent/pmContext.d.ts +13 -0
- package/dist/agent/pmContext.js +136 -0
- package/dist/agent/prompt.d.ts +11 -1
- package/dist/agent/prompt.js +135 -7
- package/dist/agent/providerCapabilities.d.ts +3 -0
- package/dist/agent/providerCapabilities.js +41 -3
- package/dist/agent/providers.d.ts +2 -1
- package/dist/agent/providers.js +222 -86
- package/dist/agent/resolve.js +1 -0
- package/dist/agent/runner.d.ts +4 -1
- package/dist/agent/runner.js +369 -38
- package/dist/agent/scorecard.js +7 -1
- package/dist/agent/skill.js +17 -0
- package/dist/agent/skillValidator.d.ts +1 -0
- package/dist/agent/skillValidator.js +56 -0
- package/dist/agent/state.js +23 -2
- package/dist/agent/strictLint.js +19 -0
- package/dist/agent/templates.js +4 -0
- package/dist/agent/thesis.d.ts +40 -0
- package/dist/agent/thesis.js +319 -0
- package/dist/agent/types.d.ts +127 -0
- package/dist/client.js +3 -2
- package/dist/http.d.ts +7 -1
- package/dist/http.js +36 -16
- package/dist/httpCompletion.d.ts +33 -0
- package/dist/httpCompletion.js +220 -0
- package/dist/retryAfter.d.ts +1 -0
- package/dist/retryAfter.js +16 -0
- package/dist/tools.js +19 -1
- package/package.json +4 -2
package/dist/agent/types.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { IndicatorSet } from "./indicators.js";
|
|
2
|
+
import type { DecisionInputRecord } from "./decisionReceipt.js";
|
|
2
3
|
export declare const SPEC_VERSION = "coinrithm.agent.v1";
|
|
3
4
|
export type Venue = "spot" | "futures" | "pm";
|
|
4
5
|
export declare const VENUES: readonly Venue[];
|
|
@@ -39,6 +40,38 @@ export interface LimitsConfig {
|
|
|
39
40
|
maxDailyLossMusd: number;
|
|
40
41
|
maxOpenMarginMusd: number;
|
|
41
42
|
}
|
|
43
|
+
/** Opt-in paper experiment. Percent fields are percentage POINTS, not fractions. */
|
|
44
|
+
export interface CapitalSizingPolicy {
|
|
45
|
+
version: "equity_fraction_v1";
|
|
46
|
+
futuresRiskPct: number;
|
|
47
|
+
pmMaxLossPct: number;
|
|
48
|
+
perTicketCapitalPct: number;
|
|
49
|
+
totalCapitalPct: number;
|
|
50
|
+
cashReservePct: number;
|
|
51
|
+
minRewardRisk: number;
|
|
52
|
+
}
|
|
53
|
+
export type CapitalBook = {
|
|
54
|
+
status: "unavailable";
|
|
55
|
+
reason: string;
|
|
56
|
+
} | {
|
|
57
|
+
status: "ready";
|
|
58
|
+
walletId: number;
|
|
59
|
+
/** Marked spot + collateral, reduced by known negative position marks.
|
|
60
|
+
* Positive open-position gains are excluded; this is NOT complete MTM. */
|
|
61
|
+
conservativeEquityMusd: number;
|
|
62
|
+
cashAvailableMusd: number;
|
|
63
|
+
committedCapitalMusd: number;
|
|
64
|
+
};
|
|
65
|
+
export interface CapitalSizingAdjustment {
|
|
66
|
+
version: "equity_fraction_v1";
|
|
67
|
+
basis: "owned_collateral_spot_marked_negative_position_marks_only";
|
|
68
|
+
proposedAmountMusd?: number;
|
|
69
|
+
sizedAmountMusd?: number;
|
|
70
|
+
conservativeEquityMusd?: number;
|
|
71
|
+
riskBudgetMusd?: number;
|
|
72
|
+
/** Runner safety estimate, not a claim that the venue charges this fee. */
|
|
73
|
+
feeBufferBps?: number;
|
|
74
|
+
}
|
|
42
75
|
export interface AbstentionConfig {
|
|
43
76
|
onStaleData: boolean;
|
|
44
77
|
onWeakSignal: boolean;
|
|
@@ -72,6 +105,7 @@ export interface AgentSpec {
|
|
|
72
105
|
model?: ModelConfig;
|
|
73
106
|
venues: Venue[];
|
|
74
107
|
risk: RiskConfig;
|
|
108
|
+
capitalSizing?: CapitalSizingPolicy;
|
|
75
109
|
limits: LimitsConfig;
|
|
76
110
|
abstention: AbstentionConfig;
|
|
77
111
|
sync: SyncConfig;
|
|
@@ -95,6 +129,8 @@ export declare const fail: (code: string, reason: string) => ValidationResult;
|
|
|
95
129
|
export interface Freshness {
|
|
96
130
|
status: string;
|
|
97
131
|
ageSeconds?: number;
|
|
132
|
+
asOf?: string;
|
|
133
|
+
basis?: string;
|
|
98
134
|
}
|
|
99
135
|
export interface WatchEntry {
|
|
100
136
|
symbol: string;
|
|
@@ -108,10 +144,25 @@ export interface WatchEntry {
|
|
|
108
144
|
freshness?: Freshness;
|
|
109
145
|
indicators?: IndicatorSet;
|
|
110
146
|
discovered?: boolean;
|
|
147
|
+
slug?: string;
|
|
148
|
+
fundamentals?: CoinFundamentals;
|
|
149
|
+
}
|
|
150
|
+
export interface CoinFundamentals {
|
|
151
|
+
categories?: string[];
|
|
152
|
+
marketCapRank?: number;
|
|
153
|
+
marketCapUsd?: number;
|
|
154
|
+
volume24hUsd?: number;
|
|
155
|
+
headlines?: Array<{
|
|
156
|
+
title: string;
|
|
157
|
+
at?: string;
|
|
158
|
+
importance?: number;
|
|
159
|
+
sentiment?: string;
|
|
160
|
+
}>;
|
|
111
161
|
}
|
|
112
162
|
export interface OpenPosition {
|
|
113
163
|
venue: Venue;
|
|
114
164
|
id: number;
|
|
165
|
+
walletId?: number;
|
|
115
166
|
coinId?: string;
|
|
116
167
|
symbol?: string;
|
|
117
168
|
side?: string;
|
|
@@ -124,6 +175,8 @@ export interface OpenPosition {
|
|
|
124
175
|
liquidationPrice?: number;
|
|
125
176
|
stopLossPrice?: number;
|
|
126
177
|
takeProfitPrice?: number;
|
|
178
|
+
openedAt?: string;
|
|
179
|
+
thesis?: ThesisView;
|
|
127
180
|
}
|
|
128
181
|
export interface SpotOrder {
|
|
129
182
|
id: number;
|
|
@@ -136,12 +189,19 @@ export interface SpotOrder {
|
|
|
136
189
|
}
|
|
137
190
|
export interface PmPosition {
|
|
138
191
|
id: number;
|
|
192
|
+
walletId?: number;
|
|
139
193
|
source?: string;
|
|
140
194
|
slug?: string;
|
|
141
195
|
outcomeExternalMarketId?: string;
|
|
142
196
|
stakeMusd?: number;
|
|
143
197
|
unrealizedPnlMusd?: number;
|
|
144
198
|
status?: string;
|
|
199
|
+
title?: string;
|
|
200
|
+
side?: string;
|
|
201
|
+
entryProbability?: number;
|
|
202
|
+
currentProbability?: number;
|
|
203
|
+
openedAt?: string;
|
|
204
|
+
thesis?: ThesisView;
|
|
145
205
|
}
|
|
146
206
|
export interface PmResolution {
|
|
147
207
|
id: number;
|
|
@@ -162,6 +222,27 @@ export interface PmMarket {
|
|
|
162
222
|
title?: string;
|
|
163
223
|
freshness?: Freshness;
|
|
164
224
|
volumeUsd?: number;
|
|
225
|
+
endDate?: string;
|
|
226
|
+
liquidityUsd?: number;
|
|
227
|
+
quality?: PmQuality;
|
|
228
|
+
decisionSupport?: PmDecisionSupport;
|
|
229
|
+
}
|
|
230
|
+
export interface PmQuality {
|
|
231
|
+
decisionEligible?: boolean;
|
|
232
|
+
warningReasons: string[];
|
|
233
|
+
blockReasons: string[];
|
|
234
|
+
policyVersion?: string;
|
|
235
|
+
assessedAt?: string;
|
|
236
|
+
reasonsOmitted?: boolean;
|
|
237
|
+
}
|
|
238
|
+
export interface PmDecisionSupport {
|
|
239
|
+
qualityScore?: number;
|
|
240
|
+
qualityTier?: string;
|
|
241
|
+
qualityCapReason?: string | null;
|
|
242
|
+
spreadTier?: string;
|
|
243
|
+
liquidityTier?: string;
|
|
244
|
+
volumeTier?: string;
|
|
245
|
+
flags?: Partial<Record<"thinMarket" | "inactiveMarket" | "highAmbiguity" | "nearResolution" | "staleData", boolean>>;
|
|
165
246
|
}
|
|
166
247
|
export interface SetupSignal {
|
|
167
248
|
symbol: string;
|
|
@@ -177,6 +258,7 @@ export interface NewsItem {
|
|
|
177
258
|
sentiment?: string;
|
|
178
259
|
importance?: number;
|
|
179
260
|
ageHours?: number;
|
|
261
|
+
publishedAt?: string;
|
|
180
262
|
coins?: string[];
|
|
181
263
|
}
|
|
182
264
|
export interface Observation {
|
|
@@ -184,6 +266,7 @@ export interface Observation {
|
|
|
184
266
|
scopes: string[];
|
|
185
267
|
cashAvailableMusd: number | null;
|
|
186
268
|
equityMusd: number | null;
|
|
269
|
+
capitalBook?: CapitalBook;
|
|
187
270
|
openPositions: OpenPosition[];
|
|
188
271
|
openOrders: SpotOrder[];
|
|
189
272
|
pmPositions: PmPosition[];
|
|
@@ -206,6 +289,35 @@ export interface Observation {
|
|
|
206
289
|
priceUsd?: number;
|
|
207
290
|
}>;
|
|
208
291
|
}
|
|
292
|
+
export interface ThesisInvalidation {
|
|
293
|
+
priceBelow?: number;
|
|
294
|
+
priceAbove?: number;
|
|
295
|
+
probabilityBelow?: number;
|
|
296
|
+
probabilityAbove?: number;
|
|
297
|
+
maxHoldMinutes?: number;
|
|
298
|
+
catalyst?: string;
|
|
299
|
+
}
|
|
300
|
+
export interface Thesis {
|
|
301
|
+
summary: string;
|
|
302
|
+
invalidation: ThesisInvalidation;
|
|
303
|
+
}
|
|
304
|
+
export interface PositionThesis extends Thesis {
|
|
305
|
+
venue: Venue;
|
|
306
|
+
positionId: number;
|
|
307
|
+
symbol?: string;
|
|
308
|
+
side?: string;
|
|
309
|
+
source?: string;
|
|
310
|
+
slug?: string;
|
|
311
|
+
outcomeExternalMarketId?: string;
|
|
312
|
+
openedAt: string;
|
|
313
|
+
entryPrice?: number;
|
|
314
|
+
entryProbability?: number;
|
|
315
|
+
}
|
|
316
|
+
export interface ThesisView extends Thesis {
|
|
317
|
+
holdMinutes: number;
|
|
318
|
+
status: "intact" | "invalidated";
|
|
319
|
+
invalidatedBy?: string;
|
|
320
|
+
}
|
|
209
321
|
export type ProposedAction = {
|
|
210
322
|
type: "futures_open";
|
|
211
323
|
symbol: string;
|
|
@@ -216,6 +328,7 @@ export type ProposedAction = {
|
|
|
216
328
|
takeProfitPrice?: number | null;
|
|
217
329
|
confidence?: number;
|
|
218
330
|
rationaleSummary?: string;
|
|
331
|
+
thesis?: Thesis;
|
|
219
332
|
} | {
|
|
220
333
|
type: "futures_close";
|
|
221
334
|
positionId: number;
|
|
@@ -237,6 +350,7 @@ export type ProposedAction = {
|
|
|
237
350
|
stopPrice?: number;
|
|
238
351
|
confidence?: number;
|
|
239
352
|
rationaleSummary?: string;
|
|
353
|
+
thesis?: Thesis;
|
|
240
354
|
} | {
|
|
241
355
|
type: "spot_cancel";
|
|
242
356
|
orderId: number;
|
|
@@ -250,6 +364,7 @@ export type ProposedAction = {
|
|
|
250
364
|
confidence?: number;
|
|
251
365
|
rationaleSummary?: string;
|
|
252
366
|
forecastProbability?: number;
|
|
367
|
+
thesis?: Thesis;
|
|
253
368
|
};
|
|
254
369
|
export type ActionVenue = Venue;
|
|
255
370
|
export declare function actionVenue(a: ProposedAction): ActionVenue;
|
|
@@ -273,7 +388,14 @@ export interface QuoteEvidence {
|
|
|
273
388
|
entryPrice?: number;
|
|
274
389
|
liquidationPrice?: number;
|
|
275
390
|
executionPrice?: number;
|
|
391
|
+
entryProbability?: number;
|
|
392
|
+
stakeMusd?: number;
|
|
393
|
+
sharesEstimate?: number;
|
|
276
394
|
estimatedCostMusd?: number;
|
|
395
|
+
estimatedFeeMusd?: number;
|
|
396
|
+
futuresFeeBps?: number;
|
|
397
|
+
estimatedEntryFeeMusd?: number;
|
|
398
|
+
cashRequiredMusd?: number;
|
|
277
399
|
freshness?: Freshness;
|
|
278
400
|
openBlocked?: boolean;
|
|
279
401
|
openBlockReasons?: unknown;
|
|
@@ -282,6 +404,7 @@ export interface RunState {
|
|
|
282
404
|
runId: string;
|
|
283
405
|
cyclesRun: number;
|
|
284
406
|
writesToday: number;
|
|
407
|
+
riskIncreasesToday: number;
|
|
285
408
|
realizedPnlMusd: number;
|
|
286
409
|
peakRealizedMusd: number;
|
|
287
410
|
consecutiveRejectCycles: number;
|
|
@@ -304,6 +427,7 @@ export interface RunState {
|
|
|
304
427
|
at: string;
|
|
305
428
|
did: string;
|
|
306
429
|
}>;
|
|
430
|
+
theses?: Record<string, PositionThesis>;
|
|
307
431
|
}
|
|
308
432
|
export interface AgentTrace {
|
|
309
433
|
runId?: string;
|
|
@@ -330,8 +454,11 @@ export interface PlannedAction {
|
|
|
330
454
|
quote?: QuoteEvidence;
|
|
331
455
|
executed?: boolean;
|
|
332
456
|
result?: unknown;
|
|
457
|
+
capitalSizing?: CapitalSizingAdjustment;
|
|
333
458
|
}
|
|
334
459
|
export interface CycleResult {
|
|
460
|
+
/** Private non-enumerable evidence. Never forward through AgentTrace/public APIs. */
|
|
461
|
+
decisionInputRecord?: DecisionInputRecord;
|
|
335
462
|
decision: "skip" | "act";
|
|
336
463
|
skipReason?: string;
|
|
337
464
|
rationale?: string;
|
package/dist/client.js
CHANGED
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
//
|
|
18
18
|
// IMPORTANT: this module must NEVER write to stdout (stdout is the MCP JSON-RPC
|
|
19
19
|
// channel). All diagnostics go to stderr via the logger below.
|
|
20
|
+
import { retryAfterSeconds } from "./retryAfter.js";
|
|
20
21
|
export const DEFAULT_BASE_URL = "https://api.coinrithm.com";
|
|
21
22
|
export function log(...args) {
|
|
22
23
|
// stderr only — stdout is reserved for the MCP protocol.
|
|
@@ -139,12 +140,12 @@ export class CoinRithmClient {
|
|
|
139
140
|
if (res.status === 429) {
|
|
140
141
|
// Surface the back-off contract so an agent can pace itself instead of
|
|
141
142
|
// hammering: 120 req/min per key baseline, 20 trade-writes/min.
|
|
142
|
-
const retryAfter =
|
|
143
|
+
const retryAfter = retryAfterSeconds(res.headers.get("retry-after"));
|
|
143
144
|
data = {
|
|
144
145
|
...(typeof data === "object" && data !== null
|
|
145
146
|
? data
|
|
146
147
|
: { error: String(data) }),
|
|
147
|
-
retryAfterSeconds:
|
|
148
|
+
retryAfterSeconds: retryAfter ?? null,
|
|
148
149
|
hint: "Rate limited. Wait retryAfterSeconds (or the Retry-After header) before retrying; pace future calls using the RateLimit-Remaining response header.",
|
|
149
150
|
};
|
|
150
151
|
}
|
package/dist/http.d.ts
CHANGED
|
@@ -1,2 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
|
|
2
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
|
+
import { CoinRithmClient } from "./client.js";
|
|
4
|
+
import { type CompletionLogger } from "./httpCompletion.js";
|
|
5
|
+
export declare function createHttpApp(client: CoinRithmClient, options?: {
|
|
6
|
+
completionLogger?: CompletionLogger;
|
|
7
|
+
createServer?: () => McpServer;
|
|
8
|
+
}): import("express-serve-static-core").Express;
|
package/dist/http.js
CHANGED
|
@@ -24,8 +24,8 @@
|
|
|
24
24
|
// forwards as `extra.authInfo`, giving requestKey() a second source. Either
|
|
25
25
|
// way the caller's own key — and only that key — is used for their tool call.
|
|
26
26
|
// - Unauthenticated MCP initialization and tool-list introspection are allowed
|
|
27
|
-
// so registries can verify the server.
|
|
28
|
-
// a structured 401
|
|
27
|
+
// so registries can verify the server. Public data tools are also keyless;
|
|
28
|
+
// protected tools return a structured 401 when their key is missing.
|
|
29
29
|
//
|
|
30
30
|
// Config (env):
|
|
31
31
|
// COINRITHM_API_URL (optional) upstream base URL (default production).
|
|
@@ -35,15 +35,23 @@
|
|
|
35
35
|
// the correct isolation model for a multi-user, per-request-keyed surface — no
|
|
36
36
|
// session state is shared between users.
|
|
37
37
|
import express from "express";
|
|
38
|
+
import { pathToFileURL } from "node:url";
|
|
38
39
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
39
40
|
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
40
41
|
import { CoinRithmClient, bearerFromHeader, loadHttpConfig, log, } from "./client.js";
|
|
41
42
|
import { registerTools } from "./tools.js";
|
|
42
43
|
import { SERVER_VERSION } from "./version.js";
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
44
|
+
import { observeHttpCompletion, } from "./httpCompletion.js";
|
|
45
|
+
// Factory permits isolated localhost SDK tests without opening a listener on import.
|
|
46
|
+
export function createHttpApp(client, options = {}) {
|
|
46
47
|
const app = express();
|
|
48
|
+
const completions = new WeakMap();
|
|
49
|
+
app.use((req, res, next) => {
|
|
50
|
+
if (req.method === "POST" && /^\/mcp\/?$/i.test(req.path)) {
|
|
51
|
+
completions.set(res, observeHttpCompletion(req, res, options.completionLogger));
|
|
52
|
+
}
|
|
53
|
+
next();
|
|
54
|
+
});
|
|
47
55
|
app.use(express.json());
|
|
48
56
|
// Lightweight, unauthenticated liveness probe (handy for Coolify/uptime checks).
|
|
49
57
|
// Keep `/healthz` as the deployment contract and expose `/health` as a
|
|
@@ -110,8 +118,8 @@ async function main() {
|
|
|
110
118
|
// Per-request auth: read THIS caller's key from the Authorization header,
|
|
111
119
|
// or from Smithery's non-reserved forwarding header.
|
|
112
120
|
// It is optional at the transport layer so registries can initialize the
|
|
113
|
-
// server and list tool schemas.
|
|
114
|
-
// a structured 401 if
|
|
121
|
+
// server and list tool schemas. Public data tools are keyless; protected
|
|
122
|
+
// tool handlers return a structured 401 if their key is missing.
|
|
115
123
|
const apiKey = bearerFromHeader(req.headers.authorization) ??
|
|
116
124
|
bearerFromHeader(req.headers["x-coinrithm-api-key"]);
|
|
117
125
|
// Belt-and-suspenders: also expose the token via the SDK's authInfo channel.
|
|
@@ -120,11 +128,13 @@ async function main() {
|
|
|
120
128
|
if (apiKey) {
|
|
121
129
|
req.auth = { token: apiKey, clientId: "coinrithm-key", scopes: [] };
|
|
122
130
|
}
|
|
123
|
-
const server =
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
131
|
+
const server = options.createServer?.() ??
|
|
132
|
+
new McpServer({
|
|
133
|
+
name: "coinrithm-trading",
|
|
134
|
+
version: SERVER_VERSION,
|
|
135
|
+
});
|
|
136
|
+
if (!options.createServer)
|
|
137
|
+
registerTools(server, client);
|
|
128
138
|
const transport = new StreamableHTTPServerTransport({
|
|
129
139
|
sessionIdGenerator: undefined, // stateless: no cross-request/user state
|
|
130
140
|
});
|
|
@@ -134,6 +144,7 @@ async function main() {
|
|
|
134
144
|
});
|
|
135
145
|
try {
|
|
136
146
|
await server.connect(transport);
|
|
147
|
+
completions.get(res)?.attach(transport);
|
|
137
148
|
// The transport reads req.headers (→ extra.requestInfo) and req.auth
|
|
138
149
|
// (→ extra.authInfo); tools.ts picks up the caller's key from there.
|
|
139
150
|
await transport.handleRequest(req, res, req.body);
|
|
@@ -149,13 +160,22 @@ async function main() {
|
|
|
149
160
|
}
|
|
150
161
|
}
|
|
151
162
|
});
|
|
163
|
+
return app;
|
|
164
|
+
}
|
|
165
|
+
async function main() {
|
|
166
|
+
const config = loadHttpConfig(); // no global key — keys arrive per request
|
|
167
|
+
const client = new CoinRithmClient(config); // constructed WITHOUT a default key
|
|
168
|
+
const app = createHttpApp(client);
|
|
152
169
|
const port = Number(process.env.PORT) || 8787;
|
|
153
170
|
app.listen(port, () => {
|
|
154
171
|
log(`HTTP MCP listening on :${port}/mcp (multi-user, per-request key). ` +
|
|
155
172
|
`upstream=${config.baseUrl}. Paper only.`);
|
|
156
173
|
});
|
|
157
174
|
}
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
175
|
+
if (process.argv[1] &&
|
|
176
|
+
import.meta.url === pathToFileURL(process.argv[1]).href) {
|
|
177
|
+
main().catch((err) => {
|
|
178
|
+
log("fatal:", err instanceof Error ? err.message : err);
|
|
179
|
+
process.exit(1);
|
|
180
|
+
});
|
|
181
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
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_disagreements", "pm_data_calibration", "pm_data_canonical", "pm_data_volume_history", "get_crypto_movers"];
|
|
4
|
+
type Operation = "initialize" | "tools_list" | "tools_call" | "notification" | "other" | "invalid";
|
|
5
|
+
type Outcome = "result" | "tool_error" | "protocol_error" | "no_response" | "not_applicable";
|
|
6
|
+
type ToolName = (typeof COMPLETION_TOOL_NAMES)[number] | "unknown" | null;
|
|
7
|
+
type RpcCompletion = {
|
|
8
|
+
operation: Operation;
|
|
9
|
+
tool: ToolName;
|
|
10
|
+
rpc_outcome: Outcome;
|
|
11
|
+
result_http_status: number | null;
|
|
12
|
+
result_ok: boolean | null;
|
|
13
|
+
};
|
|
14
|
+
export type HttpCompletionRecord = Readonly<RpcCompletion & {
|
|
15
|
+
event: "mcp_completion";
|
|
16
|
+
schema_version: 1;
|
|
17
|
+
completed_at: string;
|
|
18
|
+
duration_ms: number;
|
|
19
|
+
service_version: string;
|
|
20
|
+
transport: "streamable_http";
|
|
21
|
+
credential_supplied: boolean;
|
|
22
|
+
http_status: number | null;
|
|
23
|
+
delivery: "finished" | "aborted";
|
|
24
|
+
}>;
|
|
25
|
+
export type CompletionLogger = (line: string) => void;
|
|
26
|
+
/** Attach before JSON parsing; malformed/rejected HTTP requests remain invalid,
|
|
27
|
+
* no_response, with their actual HTTP status. Do not read bodies or raw errors.
|
|
28
|
+
* Authentication and caller origin are deliberately NOT inferred from headers.
|
|
29
|
+
*/
|
|
30
|
+
export declare function observeHttpCompletion(req: IncomingMessage, res: ServerResponse, logger?: CompletionLogger): {
|
|
31
|
+
attach: (transport: Transport) => void;
|
|
32
|
+
};
|
|
33
|
+
export {};
|
|
@@ -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
|
+
}
|