@tradejs/app 3.1.29-beta.273 → 3.1.29-beta.275
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/package.json +6 -6
- package/src/app/lib/mcp/artifacts.ts +122 -8
- package/src/app/lib/mcp/server.ts +25 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tradejs/app",
|
|
3
|
-
"version": "3.1.29-beta.
|
|
3
|
+
"version": "3.1.29-beta.275",
|
|
4
4
|
"description": "Installable Next.js UI for the TradeJS TypeScript framework: dashboards, backtests, charts, and runtime data.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"tradejs",
|
|
@@ -59,11 +59,11 @@
|
|
|
59
59
|
"@emotion/react": "^11.14.0",
|
|
60
60
|
"@modelcontextprotocol/sdk": "^1.32.1",
|
|
61
61
|
"@node-oauth/oauth2-server": "^5.3.0",
|
|
62
|
-
"@tradejs/core": "^3.1.29-beta.
|
|
63
|
-
"@tradejs/indicators": "^3.1.29-beta.
|
|
64
|
-
"@tradejs/infra": "^3.1.29-beta.
|
|
65
|
-
"@tradejs/node": "^3.1.29-beta.
|
|
66
|
-
"@tradejs/types": "^3.1.29-beta.
|
|
62
|
+
"@tradejs/core": "^3.1.29-beta.275",
|
|
63
|
+
"@tradejs/indicators": "^3.1.29-beta.275",
|
|
64
|
+
"@tradejs/infra": "^3.1.29-beta.275",
|
|
65
|
+
"@tradejs/node": "^3.1.29-beta.275",
|
|
66
|
+
"@tradejs/types": "^3.1.29-beta.275",
|
|
67
67
|
"@types/bcryptjs": "2.4.6",
|
|
68
68
|
"@types/lodash": "4.17.24",
|
|
69
69
|
"@types/node": "24.13.3",
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
1
2
|
import { open, readdir, readFile, realpath, stat } from 'node:fs/promises';
|
|
2
3
|
import path from 'node:path';
|
|
3
4
|
import {
|
|
@@ -10,6 +11,7 @@ import {
|
|
|
10
11
|
mcpId,
|
|
11
12
|
mcpProjectRoot,
|
|
12
13
|
readMcpDeployments,
|
|
14
|
+
redactMcpJobLog,
|
|
13
15
|
} from '@tradejs/node/mcp';
|
|
14
16
|
|
|
15
17
|
interface CatalogEntry {
|
|
@@ -107,6 +109,8 @@ export interface McpArtifactListOptions {
|
|
|
107
109
|
kind?: CatalogEntry['kind'];
|
|
108
110
|
offset?: number;
|
|
109
111
|
limit?: number;
|
|
112
|
+
startTime?: number;
|
|
113
|
+
endTime?: number;
|
|
110
114
|
}
|
|
111
115
|
|
|
112
116
|
export const listMcpArtifacts = async (
|
|
@@ -123,6 +127,15 @@ export const listMcpArtifacts = async (
|
|
|
123
127
|
limit > 100
|
|
124
128
|
)
|
|
125
129
|
throw new Error('Invalid artifact list pagination');
|
|
130
|
+
if (
|
|
131
|
+
(options.startTime !== undefined &&
|
|
132
|
+
!Number.isSafeInteger(options.startTime)) ||
|
|
133
|
+
(options.endTime !== undefined && !Number.isSafeInteger(options.endTime)) ||
|
|
134
|
+
(options.startTime !== undefined &&
|
|
135
|
+
options.endTime !== undefined &&
|
|
136
|
+
options.startTime >= options.endTime)
|
|
137
|
+
)
|
|
138
|
+
throw new Error('Invalid artifact window');
|
|
126
139
|
const { deployments } = await readMcpDeployments(userName);
|
|
127
140
|
const allowed = new Set(deployments.map((row) => row.id));
|
|
128
141
|
const pending = [
|
|
@@ -151,6 +164,20 @@ export const listMcpArtifacts = async (
|
|
|
151
164
|
)
|
|
152
165
|
continue;
|
|
153
166
|
if (options.kind && artifact.kind !== options.kind) continue;
|
|
167
|
+
const window = artifact.manifest.window as {
|
|
168
|
+
startTime: number;
|
|
169
|
+
endTime: number;
|
|
170
|
+
};
|
|
171
|
+
if (
|
|
172
|
+
options.startTime !== undefined &&
|
|
173
|
+
window.endTime <= options.startTime
|
|
174
|
+
)
|
|
175
|
+
continue;
|
|
176
|
+
if (
|
|
177
|
+
options.endTime !== undefined &&
|
|
178
|
+
window.startTime >= options.endTime
|
|
179
|
+
)
|
|
180
|
+
continue;
|
|
154
181
|
await mcpStorage.put(storageKey(userName, artifact.id), artifact, 3600);
|
|
155
182
|
items.push({
|
|
156
183
|
id: artifact.id,
|
|
@@ -158,7 +185,10 @@ export const listMcpArtifacts = async (
|
|
|
158
185
|
deploymentId: artifact.deploymentId,
|
|
159
186
|
sha256: artifact.sha256,
|
|
160
187
|
bytes: artifact.bytes,
|
|
161
|
-
window
|
|
188
|
+
window,
|
|
189
|
+
artifactId: artifact.manifest.artifactId,
|
|
190
|
+
deploymentCompositionId: artifact.manifest.deploymentCompositionId,
|
|
191
|
+
runtimeEvidence: artifact.manifest.runtimeEvidence,
|
|
162
192
|
});
|
|
163
193
|
} catch {
|
|
164
194
|
invalidBundles++;
|
|
@@ -173,7 +203,11 @@ export const listMcpArtifacts = async (
|
|
|
173
203
|
});
|
|
174
204
|
}
|
|
175
205
|
}
|
|
176
|
-
items.sort(
|
|
206
|
+
items.sort(
|
|
207
|
+
(left, right) =>
|
|
208
|
+
right.window.endTime - left.window.endTime ||
|
|
209
|
+
left.id.localeCompare(right.id),
|
|
210
|
+
);
|
|
177
211
|
const page = items.slice(offset, offset + limit);
|
|
178
212
|
const nextOffset =
|
|
179
213
|
offset + page.length < items.length ? offset + page.length : null;
|
|
@@ -186,7 +220,12 @@ export const listMcpArtifacts = async (
|
|
|
186
220
|
truncated: pending.length > 0 || nextOffset != null,
|
|
187
221
|
scanTruncated: pending.length > 0,
|
|
188
222
|
invalidOrInaccessibleBundles: invalidBundles,
|
|
189
|
-
|
|
223
|
+
order: 'window.endTime_desc_id_asc',
|
|
224
|
+
coverage: {
|
|
225
|
+
retentionStartTime: null,
|
|
226
|
+
completeness: 'host_local_verified_bundles_only',
|
|
227
|
+
},
|
|
228
|
+
note: 'Only verified, user-owned bundles available on this host are included. Excluded bundle counts combine inaccessible and invalid data; reasons and paths are withheld to protect other users. Missing bundles do not prove no activity.',
|
|
190
229
|
};
|
|
191
230
|
};
|
|
192
231
|
|
|
@@ -205,28 +244,84 @@ export const resolveMcpArtifact = async (userName: string, id: string) => {
|
|
|
205
244
|
return verified;
|
|
206
245
|
};
|
|
207
246
|
|
|
247
|
+
type ArtifactFile = 'payload' | 'manifest.json' | '.complete' | 'replay.log';
|
|
248
|
+
|
|
249
|
+
const artifactFile = async (artifact: CatalogEntry, file: ArtifactFile) => {
|
|
250
|
+
const target =
|
|
251
|
+
file === 'payload'
|
|
252
|
+
? artifact.payloadPath
|
|
253
|
+
: path.join(artifact.bundleDir, file);
|
|
254
|
+
if (
|
|
255
|
+
!['payload', 'manifest.json', '.complete', 'replay.log'].includes(file) ||
|
|
256
|
+
(file === 'replay.log' && artifact.kind !== 'runtime-feedback-replay')
|
|
257
|
+
)
|
|
258
|
+
throw new Error('Invalid artifact file');
|
|
259
|
+
const resolved = await checkPayload(target);
|
|
260
|
+
if (file === 'payload')
|
|
261
|
+
return {
|
|
262
|
+
path: resolved,
|
|
263
|
+
file,
|
|
264
|
+
bytes: artifact.bytes,
|
|
265
|
+
sha256: artifact.sha256,
|
|
266
|
+
};
|
|
267
|
+
const bytes = await readFile(resolved);
|
|
268
|
+
// Log files are original bundle bytes. Refuse export if credential redaction
|
|
269
|
+
// would be required; never silently change bytes advertised for verification.
|
|
270
|
+
if (
|
|
271
|
+
file === 'replay.log' &&
|
|
272
|
+
bytes
|
|
273
|
+
.toString('utf8')
|
|
274
|
+
.split('\n')
|
|
275
|
+
.some(
|
|
276
|
+
(line) =>
|
|
277
|
+
(line.length <= 1000 && redactMcpJobLog(line) !== line) ||
|
|
278
|
+
line.length > 1000,
|
|
279
|
+
)
|
|
280
|
+
)
|
|
281
|
+
throw new Error(
|
|
282
|
+
'Invalid artifact file: log requires redaction; use job_get_logs',
|
|
283
|
+
);
|
|
284
|
+
return {
|
|
285
|
+
path: resolved,
|
|
286
|
+
file,
|
|
287
|
+
bytes: bytes.length,
|
|
288
|
+
sha256: createHash('sha256').update(bytes).digest('hex'),
|
|
289
|
+
};
|
|
290
|
+
};
|
|
291
|
+
|
|
208
292
|
export const readMcpArtifactChunk = async (
|
|
209
293
|
userName: string,
|
|
210
294
|
id: string,
|
|
211
295
|
offset: number,
|
|
212
296
|
length: number,
|
|
297
|
+
file: ArtifactFile = 'payload',
|
|
213
298
|
) => {
|
|
299
|
+
if (
|
|
300
|
+
!Number.isSafeInteger(offset) ||
|
|
301
|
+
offset < 0 ||
|
|
302
|
+
!Number.isSafeInteger(length) ||
|
|
303
|
+
length < 1
|
|
304
|
+
)
|
|
305
|
+
throw new Error('Invalid artifact chunk range');
|
|
214
306
|
const artifact = await resolveMcpArtifact(userName, id);
|
|
215
|
-
const
|
|
307
|
+
const selected = await artifactFile(artifact, file);
|
|
308
|
+
if (offset > selected.bytes) throw new Error('Invalid artifact chunk offset');
|
|
309
|
+
const handle = await open(selected.path, 'r');
|
|
216
310
|
try {
|
|
217
311
|
const buffer = Buffer.alloc(
|
|
218
|
-
Math.min(length, Math.max(0,
|
|
312
|
+
Math.min(length, Math.max(0, selected.bytes - offset)),
|
|
219
313
|
);
|
|
220
314
|
const { bytesRead } = await handle.read(buffer, 0, buffer.length, offset);
|
|
221
315
|
return {
|
|
222
316
|
id,
|
|
223
317
|
offset,
|
|
224
|
-
|
|
225
|
-
|
|
318
|
+
file,
|
|
319
|
+
bytes: selected.bytes,
|
|
320
|
+
sha256: selected.sha256,
|
|
226
321
|
encoding: 'base64',
|
|
227
322
|
data: buffer.subarray(0, bytesRead).toString('base64'),
|
|
228
323
|
nextOffset:
|
|
229
|
-
offset + bytesRead <
|
|
324
|
+
offset + bytesRead < selected.bytes ? offset + bytesRead : null,
|
|
230
325
|
};
|
|
231
326
|
} finally {
|
|
232
327
|
await handle.close();
|
|
@@ -236,10 +331,29 @@ export const readMcpArtifactChunk = async (
|
|
|
236
331
|
export const readMcpDiagnosticReport = async (userName: string, id: string) => {
|
|
237
332
|
const artifact = await resolveMcpArtifact(userName, id);
|
|
238
333
|
const payload = JSON.parse(await readFile(artifact.payloadPath, 'utf8'));
|
|
334
|
+
const files = [];
|
|
335
|
+
for (const file of [
|
|
336
|
+
'payload',
|
|
337
|
+
'manifest.json',
|
|
338
|
+
'.complete',
|
|
339
|
+
...(artifact.kind === 'runtime-feedback-replay' ? ['replay.log'] : []),
|
|
340
|
+
] as ArtifactFile[]) {
|
|
341
|
+
try {
|
|
342
|
+
const { path: _path, ...descriptor } = await artifactFile(artifact, file);
|
|
343
|
+
files.push({ ...descriptor, available: true });
|
|
344
|
+
} catch {
|
|
345
|
+
files.push({
|
|
346
|
+
file,
|
|
347
|
+
available: false,
|
|
348
|
+
reason: 'file_unavailable_or_requires_redaction',
|
|
349
|
+
});
|
|
350
|
+
}
|
|
351
|
+
}
|
|
239
352
|
// Large reports stay available through artifact_get with original checksums.
|
|
240
353
|
return {
|
|
241
354
|
id,
|
|
242
355
|
manifest: artifact.manifest,
|
|
356
|
+
files,
|
|
243
357
|
bytes: artifact.bytes,
|
|
244
358
|
sha256: artifact.sha256,
|
|
245
359
|
...(artifact.bytes <= 96000
|
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
cancelMcpJob,
|
|
7
7
|
enqueueMcpJob,
|
|
8
8
|
mcpId,
|
|
9
|
+
McpCursorError,
|
|
9
10
|
publicMcpJob,
|
|
10
11
|
readAuthorizedMcpJob,
|
|
11
12
|
readMcpBacktestPage,
|
|
@@ -82,6 +83,7 @@ const runtimePage = {
|
|
|
82
83
|
endTime: timestamp,
|
|
83
84
|
strategy: identifier.optional(),
|
|
84
85
|
symbol: symbol.optional(),
|
|
86
|
+
limit: z.number().int().min(1).max(50).default(50),
|
|
85
87
|
};
|
|
86
88
|
|
|
87
89
|
export const createTradejsMcpServer = (principal: McpPrincipal) => {
|
|
@@ -165,6 +167,21 @@ export const createTradejsMcpServer = (principal: McpPrincipal) => {
|
|
|
165
167
|
structuredContent: { data: result },
|
|
166
168
|
};
|
|
167
169
|
} catch (error) {
|
|
170
|
+
if (error instanceof McpCursorError) {
|
|
171
|
+
const data = {
|
|
172
|
+
code: error.code,
|
|
173
|
+
message: error.message,
|
|
174
|
+
recovery:
|
|
175
|
+
error.code === 'CURSOR_EXPIRED'
|
|
176
|
+
? 'Restart cursor=0 with the same window and filters; deduplicate record identities. Do not move startTime to the last record: storage order is not chronological.'
|
|
177
|
+
: 'Restore the original user, account and filters or start a new traversal.',
|
|
178
|
+
};
|
|
179
|
+
return {
|
|
180
|
+
isError: true,
|
|
181
|
+
content: [{ type: 'text' as const, text: JSON.stringify(data) }],
|
|
182
|
+
structuredContent: { data },
|
|
183
|
+
};
|
|
184
|
+
}
|
|
168
185
|
const message = error instanceof Error ? error.message : '';
|
|
169
186
|
// Filesystem, connector and database errors may contain secrets or paths.
|
|
170
187
|
const safe =
|
|
@@ -268,7 +285,7 @@ export const createTradejsMcpServer = (principal: McpPrincipal) => {
|
|
|
268
285
|
for (const kind of ['signals', 'evaluations', 'orders'] as const) {
|
|
269
286
|
tool(
|
|
270
287
|
`runtime_list_${kind}`,
|
|
271
|
-
`Read a bounded storage page of ${kind}. Window is half-open [startTime,endTime), at most 7 days. Continue even after an empty page until cursor=0.`,
|
|
288
|
+
`Read a bounded storage page of ${kind}. Window is half-open [startTime,endTime), at most 7 days. Continue even after an empty page until cursor=0. Shared cursors last 24 hours; retry the identical request after transport errors. Storage order is not chronological; CURSOR_EXPIRED requires restarting the same window and deduplicating identities.`,
|
|
272
289
|
['runtime:read'],
|
|
273
290
|
runtimePage,
|
|
274
291
|
async (args) => {
|
|
@@ -401,6 +418,8 @@ export const createTradejsMcpServer = (principal: McpPrincipal) => {
|
|
|
401
418
|
kind: z.enum(['runtime-evidence', 'runtime-feedback-replay']).optional(),
|
|
402
419
|
offset: z.number().int().min(0).default(0),
|
|
403
420
|
limit: z.number().int().min(1).max(100).default(20),
|
|
421
|
+
startTime: timestamp.optional(),
|
|
422
|
+
endTime: timestamp.optional(),
|
|
404
423
|
},
|
|
405
424
|
async (args) => listMcpArtifacts(principal.userName, args),
|
|
406
425
|
);
|
|
@@ -414,12 +433,15 @@ export const createTradejsMcpServer = (principal: McpPrincipal) => {
|
|
|
414
433
|
);
|
|
415
434
|
tool(
|
|
416
435
|
'artifact_get',
|
|
417
|
-
'Read an owned verified artifact in base64 chunks. Concatenate decoded bytes, verify total size and SHA-256 before local analysis. No filesystem path is accepted.',
|
|
436
|
+
'Read an owned verified artifact in base64 chunks. Concatenate decoded bytes, verify total size and SHA-256 before local analysis. Select a file listed by diagnostics_get_report (payload by default). Each file has its own size and SHA-256. No filesystem path is accepted.',
|
|
418
437
|
['diagnostics:read'],
|
|
419
438
|
{
|
|
420
439
|
artifactId,
|
|
421
440
|
offset: z.number().int().min(0).default(0),
|
|
422
441
|
length: z.number().int().min(1).max(48000).default(48000),
|
|
442
|
+
file: z
|
|
443
|
+
.enum(['payload', 'manifest.json', '.complete', 'replay.log'])
|
|
444
|
+
.default('payload'),
|
|
423
445
|
},
|
|
424
446
|
async (args) =>
|
|
425
447
|
readMcpArtifactChunk(
|
|
@@ -427,6 +449,7 @@ export const createTradejsMcpServer = (principal: McpPrincipal) => {
|
|
|
427
449
|
args.artifactId,
|
|
428
450
|
args.offset,
|
|
429
451
|
args.length,
|
|
452
|
+
args.file,
|
|
430
453
|
),
|
|
431
454
|
);
|
|
432
455
|
tool(
|