@tradejs/node 3.1.29-beta.261 → 3.1.29-beta.263

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/dist/mcp.mjs ADDED
@@ -0,0 +1,584 @@
1
+ import "./chunk-JQOLTLEY.mjs";
2
+ import "./chunk-ZUMUI37V.mjs";
3
+ import {
4
+ getConnectorCreatorByProvider
5
+ } from "./chunk-QVKUROR2.mjs";
6
+ import {
7
+ listRuntimeDeployments,
8
+ loadResolvedRuntimeStrategies
9
+ } from "./chunk-BO42QWAB.mjs";
10
+ import "./chunk-SKY2HNZU.mjs";
11
+ import {
12
+ ensureIndicatorPluginsLoaded
13
+ } from "./chunk-UL5Z46JL.mjs";
14
+ import "./chunk-LZDXRXIU.mjs";
15
+ import "./chunk-YR55Q355.mjs";
16
+ import "./chunk-GHSLOJLE.mjs";
17
+ import "./chunk-Y6FXYEAI.mjs";
18
+
19
+ // src/mcp.ts
20
+ import { createHash as createHash2, randomUUID } from "crypto";
21
+ import { createIndicators } from "@tradejs/core/indicators";
22
+ import { intervalToMs } from "@tradejs/core/data";
23
+ import {
24
+ getDataStrict as getDataStrict2,
25
+ getHashData,
26
+ getHashJsonField,
27
+ mcpStorage as mcpStorage2,
28
+ redisKeys as redisKeys2,
29
+ scanDataKeys,
30
+ scanHashJson
31
+ } from "@tradejs/infra/redis";
32
+ import { getRuntimeDeploymentHeartbeat } from "@tradejs/infra/runtimeHeartbeats";
33
+ import { getTradingAccount } from "@tradejs/infra/tradingAccounts";
34
+ import { getRuntimeStorageDayKey } from "@tradejs/core/time";
35
+
36
+ // src/mcpJobs.ts
37
+ import { createHash } from "crypto";
38
+ import { readFile } from "fs/promises";
39
+ import path from "path";
40
+ import { getDataStrict, mcpStorage, redisKeys } from "@tradejs/infra/redis";
41
+ var MCP_JOB_TTL = 7 * 86400;
42
+ var hash = (value) => createHash("sha256").update(value).digest("hex");
43
+ var mcpJobStorageId = (userName, id) => `jobs:${hash(userName)}:${id}`;
44
+ var mcpJobDirectory = (job) => path.join(
45
+ String(process.env.PROJECT_CWD || process.cwd()),
46
+ "data/mcp",
47
+ hash(job.userName),
48
+ job.id
49
+ );
50
+ var mcpPackageManifestHash = async () => hash(
51
+ await readFile(
52
+ path.join(
53
+ String(process.env.PROJECT_CWD || process.cwd()),
54
+ "runtime-package-manifest.json"
55
+ ),
56
+ "utf8"
57
+ )
58
+ );
59
+ var assertWindow = (startTime, endTime, maxDays) => {
60
+ if (!Number.isSafeInteger(startTime) || !Number.isSafeInteger(endTime) || startTime <= 0 || endTime <= startTime || endTime > Date.now() || endTime - startTime > maxDays * 864e5)
61
+ throw new Error(`Select a past window of at most ${maxDays} days`);
62
+ };
63
+ var enqueueMcpJob = async (principal, kind, request, idempotencyKey) => {
64
+ if (!/^[A-Za-z0-9_-]{8,100}$/.test(idempotencyKey))
65
+ throw new Error(
66
+ "Provide an idempotencyKey of 8\u2013100 letters, digits, underscores or hyphens"
67
+ );
68
+ const id = hash(
69
+ `${principal.userName}:${principal.clientId}:${idempotencyKey}`
70
+ );
71
+ const storageId = mcpJobStorageId(principal.userName, id);
72
+ const existing = await mcpStorage.get(storageId);
73
+ if (existing) {
74
+ if (existing.kind !== kind || existing.request.requestFingerprint !== hash(JSON.stringify(request)))
75
+ throw new Error("Idempotency key was used for another request");
76
+ return existing;
77
+ }
78
+ const worker = await mcpStorage.get(
79
+ "worker:heartbeat"
80
+ );
81
+ if (!worker) throw new Error("MCP worker is unavailable; no job was queued");
82
+ if (!await mcpStorage.rateLimit(`jobs:${hash(principal.userName)}`, 10, 3600))
83
+ throw new Error("Hourly job limit reached");
84
+ const job = {
85
+ id,
86
+ userName: principal.userName,
87
+ clientId: principal.clientId,
88
+ kind,
89
+ status: "queued",
90
+ request: { ...request, requestFingerprint: hash(JSON.stringify(request)) },
91
+ args: [],
92
+ createdAt: Date.now(),
93
+ updatedAt: Date.now(),
94
+ logs: [],
95
+ outputId: id,
96
+ packageManifestSha256: await mcpPackageManifestHash()
97
+ };
98
+ const outputDir = mcpJobDirectory(job);
99
+ if (kind === "backtest") {
100
+ assertWindow(Number(request.startTime), Number(request.endTime), 90);
101
+ const configId = String(request.configId || "");
102
+ if (!/^[A-Za-z][A-Za-z0-9]*:[A-Za-z0-9_-]+$/.test(configId))
103
+ throw new Error("Select a named Strategy:config");
104
+ const grid = await getDataStrict(
105
+ redisKeys.backtestConfig(principal.userName, configId)
106
+ );
107
+ if (!grid || typeof grid !== "object" || Array.isArray(grid))
108
+ throw new Error("Backtest config not found");
109
+ const cells = Object.values(grid);
110
+ if (!cells.length || !cells.every((cell) => Array.isArray(cell) && cell.length > 0) || cells.reduce((n, cell) => n * cell.length, 1) > 8)
111
+ throw new Error("MCP backtests allow at most eight config combinations");
112
+ const symbols = request.symbols;
113
+ if (!Array.isArray(symbols) || !symbols.length || symbols.length > 20 || !symbols.every(
114
+ (symbol) => typeof symbol === "string" && /^[A-Z0-9._-]{1,40}$/.test(symbol)
115
+ ))
116
+ throw new Error("Select 1\u201320 symbols");
117
+ const frozenConfigId = `${configId.split(":")[0]}:mcp_${id}`;
118
+ job.request = {
119
+ ...job.request,
120
+ resolvedConfig: grid,
121
+ configSha256: hash(JSON.stringify(grid)),
122
+ frozenConfigId
123
+ };
124
+ job.args = [
125
+ "backtest",
126
+ "--user",
127
+ principal.userName,
128
+ "--config",
129
+ frozenConfigId,
130
+ "--connector",
131
+ String(request.provider),
132
+ "--timeframe",
133
+ String(request.interval),
134
+ "--startTime",
135
+ String(request.startTime),
136
+ "--endTime",
137
+ String(request.endTime),
138
+ "--tickers",
139
+ symbols.join(","),
140
+ "--parallel",
141
+ "1",
142
+ "--cacheOnly",
143
+ "--fast",
144
+ "--persistResults"
145
+ ];
146
+ } else if (kind === "runtime-evidence") {
147
+ assertWindow(Number(request.startTime), Number(request.endTime), 7);
148
+ job.args = [
149
+ "runtime-evidence",
150
+ "--user",
151
+ principal.userName,
152
+ "--deployment",
153
+ String(request.deploymentId),
154
+ "--startTime",
155
+ String(request.startTime),
156
+ "--endTime",
157
+ String(request.endTime),
158
+ "--publishDir",
159
+ outputDir
160
+ ];
161
+ } else if (kind === "runtime-feedback-replay") {
162
+ if (!worker.replayAvailable)
163
+ throw new Error("MCP replay worker requires an isolated Redis host");
164
+ if (typeof request.verifiedEvidencePath !== "string")
165
+ throw new Error("Select verified runtime evidence");
166
+ job.args = [
167
+ "runtime-feedback-replay",
168
+ "--runtimeEvidence",
169
+ request.verifiedEvidencePath,
170
+ "--outDir",
171
+ outputDir,
172
+ "--runId",
173
+ id
174
+ ];
175
+ } else {
176
+ throw new Error(
177
+ "Unsupported diagnostic kind; use verified runtime feedback for deployment-bound parity"
178
+ );
179
+ }
180
+ if (!await mcpStorage.put(storageId, job, MCP_JOB_TTL, true)) {
181
+ const winner = await readMcpJob(principal.userName, id);
182
+ if (winner.kind !== kind || winner.request.requestFingerprint !== job.request.requestFingerprint)
183
+ throw new Error("Idempotency key was used for another request");
184
+ return winner;
185
+ }
186
+ return job;
187
+ };
188
+ var readMcpJob = async (userName, id) => {
189
+ if (!/^[a-f0-9]{64}$/.test(id)) throw new Error("Invalid job id");
190
+ const job = await mcpStorage.get(mcpJobStorageId(userName, id));
191
+ if (!job || job.userName !== userName) throw new Error("Job not found");
192
+ return job;
193
+ };
194
+ var cancelMcpJob = async (principal, id) => {
195
+ for (let attempt = 0; attempt < 4; attempt++) {
196
+ const job = await readMcpJob(principal.userName, id);
197
+ if (job.clientId !== principal.clientId)
198
+ throw new Error("Only the submitting client can cancel this job");
199
+ if (!["queued", "running"].includes(job.status)) return job;
200
+ const next = {
201
+ ...job,
202
+ cancelRequested: true,
203
+ updatedAt: Date.now(),
204
+ ...job.status === "queued" ? { status: "cancelled", finishedAt: Date.now() } : {}
205
+ };
206
+ if (await mcpStorage.compare(
207
+ mcpJobStorageId(job.userName, job.id),
208
+ job,
209
+ next,
210
+ MCP_JOB_TTL
211
+ ))
212
+ return next;
213
+ }
214
+ throw new Error("Job changed; retry cancellation");
215
+ };
216
+ var publicMcpJob = (job) => {
217
+ const { args: _args, logs: _logs, request: _request, ...view } = job;
218
+ return {
219
+ ...view,
220
+ request: Object.fromEntries(
221
+ Object.entries(job.request).filter(
222
+ ([key]) => !["verifiedEvidencePath", "resolvedConfig"].includes(key)
223
+ )
224
+ ),
225
+ logLines: job.logs.length
226
+ };
227
+ };
228
+
229
+ // src/mcp.ts
230
+ var mcpProjectRoot = () => String(process.env.PROJECT_CWD || process.cwd());
231
+ var isMcpRecord = (value) => Boolean(value) && typeof value === "object" && !Array.isArray(value);
232
+ var mcpId = (value) => createHash2("sha256").update(value).digest("hex");
233
+ var redactMcpData = (value) => {
234
+ if (Array.isArray(value)) return value.map(redactMcpData);
235
+ if (!isMcpRecord(value)) return value;
236
+ return Object.fromEntries(
237
+ Object.entries(value).filter(
238
+ ([key]) => !/secret|password|authorization|api.?key|private.?key|access.?token|refresh.?token|bot.?token|cookie|credential/i.test(
239
+ key
240
+ )
241
+ ).map(([key, nested]) => [key, redactMcpData(nested)])
242
+ );
243
+ };
244
+ var connectorFor = async (userName, provider, universe) => {
245
+ const creator = await getConnectorCreatorByProvider(
246
+ provider,
247
+ mcpProjectRoot()
248
+ );
249
+ if (!creator) throw new Error("Unknown market provider");
250
+ return creator({ userName, universe });
251
+ };
252
+ var readMcpMarket = async (userName, provider, universe, offset, limit, symbol) => {
253
+ const connector = await connectorFor(userName, provider, universe);
254
+ const tickers = await connector.getTickers();
255
+ const rows = symbol ? tickers.filter((row) => row.symbol === symbol) : tickers;
256
+ return {
257
+ provider,
258
+ universe,
259
+ items: rows.slice(offset, offset + limit),
260
+ total: rows.length,
261
+ nextOffset: offset + limit < rows.length ? offset + limit : null,
262
+ observedAt: Date.now(),
263
+ source: "connector"
264
+ };
265
+ };
266
+ var readMcpChart = async (userName, request) => {
267
+ const connector = await connectorFor(
268
+ userName,
269
+ request.provider,
270
+ request.universe
271
+ );
272
+ const width = intervalToMs(request.interval);
273
+ const end = Math.floor(Math.min(request.endTime, Date.now()) / width) * width;
274
+ const visibleStart = end - request.bars * width;
275
+ const start = Math.max(0, visibleStart - 200 * width);
276
+ const fetched = await connector.kline({
277
+ symbol: request.symbol,
278
+ interval: request.interval,
279
+ start,
280
+ end,
281
+ cacheOnly: request.cacheOnly
282
+ });
283
+ const candles = fetched.filter(
284
+ (candle) => candle.timestamp >= start && candle.timestamp < end
285
+ );
286
+ const btc = request.universe === "tradfi" ? [] : request.symbol === "BTCUSDT" ? candles : await connector.kline({
287
+ symbol: "BTCUSDT",
288
+ interval: request.interval,
289
+ start,
290
+ end,
291
+ cacheOnly: request.cacheOnly
292
+ });
293
+ const visibleCandles = candles.filter((candle) => candle.timestamp >= visibleStart).slice(-request.bars);
294
+ await ensureIndicatorPluginsLoaded(mcpProjectRoot());
295
+ const allIndicators = createIndicators(
296
+ candles,
297
+ btc.filter((candle) => candle.timestamp < end),
298
+ {
299
+ includeMlPayload: false,
300
+ pluginRegistryScope: mcpProjectRoot()
301
+ }
302
+ ).result();
303
+ return {
304
+ ...request,
305
+ startTime: visibleStart,
306
+ historyStartTime: start,
307
+ warmupBars: 200,
308
+ endTime: end,
309
+ candles: visibleCandles,
310
+ indicators: Object.fromEntries(
311
+ (request.indicatorNames || ["maFast", "maSlow", "atr", "macd"]).map(
312
+ (key) => [
313
+ key,
314
+ Array.isArray(allIndicators[key]) ? allIndicators[key].slice(
315
+ -request.bars
316
+ ) : allIndicators[key] ?? null
317
+ ]
318
+ )
319
+ ),
320
+ availableIndicators: Object.keys(allIndicators),
321
+ observedAt: Date.now(),
322
+ source: request.cacheOnly ? "cached_history" : "connector",
323
+ closedCandlesOnly: true,
324
+ coverage: {
325
+ requestedBars: request.bars,
326
+ availableBars: candles.filter(
327
+ (candle) => candle.timestamp >= visibleStart
328
+ ).length,
329
+ complete: candles.filter((candle) => candle.timestamp >= visibleStart).length >= request.bars
330
+ }
331
+ };
332
+ };
333
+ var readMcpDeployments = async (userName) => {
334
+ const deployments = await listRuntimeDeployments({
335
+ userName,
336
+ projectRoot: mcpProjectRoot()
337
+ });
338
+ const allowed = [];
339
+ for (const deployment of deployments) {
340
+ if (!await getTradingAccount(userName, deployment.accountId)) continue;
341
+ allowed.push({
342
+ ...deployment,
343
+ heartbeat: await getRuntimeDeploymentHeartbeat(userName, deployment.id)
344
+ });
345
+ }
346
+ return {
347
+ deployments: allowed,
348
+ observedAt: Date.now(),
349
+ source: "project_declaration_and_runtime_storage"
350
+ };
351
+ };
352
+ var requireMcpDeployment = async (userName, deploymentId) => {
353
+ const { deployments } = await readMcpDeployments(userName);
354
+ const deployment = deployments.find((row) => row.id === deploymentId);
355
+ if (!deployment) throw new Error("Deployment unavailable for this user");
356
+ return deployment;
357
+ };
358
+ var readMcpStrategies = async (userName, deploymentId) => {
359
+ const deployment = await requireMcpDeployment(userName, deploymentId);
360
+ const rows = await loadResolvedRuntimeStrategies({
361
+ userName,
362
+ projectRoot: mcpProjectRoot(),
363
+ deploymentId
364
+ });
365
+ return {
366
+ deploymentId,
367
+ accountId: deployment.accountId,
368
+ strategies: rows.map(({ strategyCreator: _creator, ...row }) => row),
369
+ observedAt: Date.now()
370
+ };
371
+ };
372
+ var readMcpRuntimePage = async (userName, kind, request) => {
373
+ const deployment = await requireMcpDeployment(userName, request.deploymentId);
374
+ const prefix = kind === "signals" ? redisKeys2.runtimeSignalBuckets(userName) : kind === "evaluations" ? redisKeys2.runtimeSignalEvaluationBuckets(userName) : redisKeys2.runtimeTrades(userName);
375
+ const { cursor: _cursor, ...filters } = request;
376
+ const binding = mcpId(JSON.stringify({ userName, kind, ...filters }));
377
+ let state = {
378
+ binding,
379
+ scan: "0",
380
+ keys: [],
381
+ hash: "0",
382
+ values: [],
383
+ scanned: false
384
+ };
385
+ if (request.cursor !== "0") {
386
+ const saved = await mcpStorage2.get(
387
+ `cursor:${request.cursor}`
388
+ );
389
+ if (!saved || saved.binding !== binding)
390
+ throw new Error("Invalid storage cursor");
391
+ state = saved;
392
+ }
393
+ if (!state.keys.length && !state.values.length && (!state.scanned || state.scan !== "0")) {
394
+ const page = await scanDataKeys(prefix, state.scan);
395
+ state = { ...state, scan: page.cursor, keys: page.keys, scanned: true };
396
+ }
397
+ if (!state.values.length && state.keys.length) {
398
+ const key = state.keys[0];
399
+ if (kind === "orders") {
400
+ state.keys.shift();
401
+ if (!key.slice(prefix.length).startsWith("days:"))
402
+ state.values = [await getDataStrict2(key)];
403
+ } else {
404
+ const page = await scanHashJson(key, state.hash);
405
+ state.values = page.values;
406
+ state.hash = page.cursor;
407
+ if (page.cursor === "0") state.keys.shift();
408
+ }
409
+ }
410
+ const items = [];
411
+ for (let row of state.values.splice(0, 50)) {
412
+ if (kind === "signals" && isMcpRecord(row) && typeof row.symbol === "string" && typeof row.signalId === "string")
413
+ row = await getDataStrict2(
414
+ redisKeys2.storeSignal(row.symbol, row.signalId)
415
+ );
416
+ if (!isMcpRecord(row) || row.deploymentId !== request.deploymentId || row.accountId !== deployment.accountId || row.userName !== void 0 && row.userName !== userName || request.strategy && row.strategy !== request.strategy || request.symbol && row.symbol !== request.symbol)
417
+ continue;
418
+ const timestamp = Number(
419
+ row.timestamp ?? row.entryTimestamp ?? row.createdAt
420
+ );
421
+ if (timestamp < request.startTime || timestamp >= request.endTime || !Number.isFinite(timestamp))
422
+ continue;
423
+ const {
424
+ figures: _figures,
425
+ indicators: _indicators,
426
+ additionalIndicators: _additional,
427
+ ...summary
428
+ } = row;
429
+ items.push(summary);
430
+ }
431
+ let next = "0";
432
+ if (state.values.length || state.keys.length || state.scan !== "0") {
433
+ next = mcpId(randomUUID());
434
+ await mcpStorage2.put(`cursor:${next}`, state, 600);
435
+ }
436
+ return {
437
+ items,
438
+ cursor: next,
439
+ deploymentId: request.deploymentId,
440
+ observedAt: Date.now(),
441
+ source: "runtime_storage",
442
+ window: { startTime: request.startTime, endTime: request.endTime },
443
+ coverage: "stored_records_only",
444
+ note: "Missing records do not prove no evaluation occurred. Continue until cursor is 0; cursors expire after 10 minutes."
445
+ };
446
+ };
447
+ var readMcpSignal = async (userName, signalId, deploymentId, timestamp, strategy) => {
448
+ const deployment = await requireMcpDeployment(userName, deploymentId);
449
+ const ref = await getHashJsonField(
450
+ redisKeys2.runtimeSignalBucket(
451
+ userName,
452
+ getRuntimeStorageDayKey(timestamp),
453
+ strategy
454
+ ),
455
+ signalId,
456
+ null
457
+ );
458
+ if (!isMcpRecord(ref) || typeof ref.symbol !== "string")
459
+ throw new Error("Signal not found");
460
+ const signal = await getDataStrict2(
461
+ redisKeys2.storeSignal(ref.symbol, signalId)
462
+ );
463
+ if (!isMcpRecord(signal) || signal.deploymentId !== deploymentId || signal.accountId !== deployment.accountId || signal.userName !== void 0 && signal.userName !== userName)
464
+ throw new Error("Signal not found");
465
+ return { signal, observedAt: Date.now(), source: "runtime_storage" };
466
+ };
467
+ var readMcpEvaluationStats = async (userName, deploymentId, cursor) => {
468
+ await requireMcpDeployment(userName, deploymentId);
469
+ const page = await scanDataKeys(
470
+ redisKeys2.runtimeSignalEvaluationStatsBuckets(userName),
471
+ cursor
472
+ );
473
+ const buckets = [];
474
+ for (const key of page.keys) {
475
+ const parts = key.slice(redisKeys2.runtimeSignalEvaluationStatsBuckets(userName).length).split(":");
476
+ if (parts.slice(1, -1).join(":") !== deploymentId) continue;
477
+ buckets.push({
478
+ bucket: key.slice(
479
+ redisKeys2.runtimeSignalEvaluationStatsBuckets(userName).length
480
+ ),
481
+ counters: await getHashData(key)
482
+ });
483
+ }
484
+ return {
485
+ buckets,
486
+ cursor: page.cursor,
487
+ observedAt: Date.now(),
488
+ source: "debug_telemetry",
489
+ immutableEvidence: false,
490
+ note: "Aggregate counters are not composition-bound evidence. Detailed skip records may not have been captured."
491
+ };
492
+ };
493
+ var readMcpBacktestPage = async (userName, kind, cursor) => {
494
+ const prefix = kind === "configs" ? `users:${userName}:backtests:configs:` : `users:${userName}:backtests:results:`;
495
+ const page = await scanDataKeys(prefix, cursor);
496
+ const items = [];
497
+ for (const key of page.keys) {
498
+ const value = await getDataStrict2(key);
499
+ items.push({
500
+ id: key.slice(prefix.length),
501
+ value: kind === "results" && isMcpRecord(value) ? {
502
+ config: value.config,
503
+ mode: value.mode,
504
+ startedAt: value.startedAt,
505
+ finishedAt: value.finishedAt,
506
+ successTests: value.successTests,
507
+ errorTests: value.errorTests,
508
+ resultCount: Array.isArray(value.results) ? value.results.length : 0
509
+ } : value
510
+ });
511
+ }
512
+ return {
513
+ items,
514
+ cursor: page.cursor,
515
+ observedAt: Date.now(),
516
+ source: "backtest_storage"
517
+ };
518
+ };
519
+ var readMcpBacktestResult = async (userName, id, offset = 0, limit = 20) => {
520
+ if (id.includes("*") || id.includes("?") || id.includes("["))
521
+ throw new Error("Invalid result identifier");
522
+ const result = await getDataStrict2(
523
+ `users:${userName}:backtests:results:${id}`
524
+ );
525
+ if (!result) throw new Error("Result not found");
526
+ const record = isMcpRecord(result) ? result : { results: result };
527
+ const groups = ["results", "resultsByTickers", "resultsByStrategies"];
528
+ const pageGroup = (group) => Array.isArray(record[group]) ? record[group].slice(offset, offset + limit) : record[group];
529
+ const paged = {
530
+ results: pageGroup("results"),
531
+ resultsByTickers: pageGroup("resultsByTickers"),
532
+ resultsByStrategies: pageGroup("resultsByStrategies")
533
+ };
534
+ const total = Math.max(
535
+ 0,
536
+ ...groups.map(
537
+ (group) => Array.isArray(record[group]) ? record[group].length : 0
538
+ )
539
+ );
540
+ return {
541
+ id,
542
+ result: {
543
+ config: record.config,
544
+ mode: record.mode,
545
+ startedAt: record.startedAt,
546
+ finishedAt: record.finishedAt,
547
+ successTests: record.successTests,
548
+ errorTests: record.errorTests,
549
+ bestConfig: record.bestConfig,
550
+ mergedConfig: record.mergedConfig,
551
+ runtimeComparison: record.runtimeComparison,
552
+ ...paged
553
+ },
554
+ offset,
555
+ total,
556
+ nextOffset: offset + limit < total ? offset + limit : null,
557
+ observedAt: Date.now(),
558
+ source: "backtest_storage"
559
+ };
560
+ };
561
+ export {
562
+ MCP_JOB_TTL,
563
+ cancelMcpJob,
564
+ enqueueMcpJob,
565
+ isMcpRecord,
566
+ mcpId,
567
+ mcpJobDirectory,
568
+ mcpJobStorageId,
569
+ mcpPackageManifestHash,
570
+ mcpProjectRoot,
571
+ publicMcpJob,
572
+ readMcpBacktestPage,
573
+ readMcpBacktestResult,
574
+ readMcpChart,
575
+ readMcpDeployments,
576
+ readMcpEvaluationStats,
577
+ readMcpJob,
578
+ readMcpMarket,
579
+ readMcpRuntimePage,
580
+ readMcpSignal,
581
+ readMcpStrategies,
582
+ redactMcpData,
583
+ requireMcpDeployment
584
+ };