@tradejs/app 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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tradejs/app",
3
- "version": "3.1.29-beta.261",
3
+ "version": "3.1.29-beta.263",
4
4
  "description": "Installable Next.js UI for the TradeJS TypeScript framework: dashboards, backtests, charts, and runtime data.",
5
5
  "keywords": [
6
6
  "tradejs",
@@ -16,7 +16,8 @@
16
16
  "coinbase",
17
17
  "ai",
18
18
  "claude",
19
- "codex"
19
+ "codex",
20
+ "jev"
20
21
  ],
21
22
  "files": [
22
23
  "README.md",
@@ -56,11 +57,13 @@
56
57
  "@chakra-ui/charts": "3.36.1",
57
58
  "@chakra-ui/react": "3.36.1",
58
59
  "@emotion/react": "^11.14.0",
59
- "@tradejs/core": "^3.1.29-beta.261",
60
- "@tradejs/indicators": "^3.1.29-beta.261",
61
- "@tradejs/infra": "^3.1.29-beta.261",
62
- "@tradejs/node": "^3.1.29-beta.261",
63
- "@tradejs/types": "^3.1.29-beta.261",
60
+ "@modelcontextprotocol/sdk": "^1.32.1",
61
+ "@node-oauth/oauth2-server": "^5.3.0",
62
+ "@tradejs/core": "^3.1.29-beta.263",
63
+ "@tradejs/indicators": "^3.1.29-beta.263",
64
+ "@tradejs/infra": "^3.1.29-beta.263",
65
+ "@tradejs/node": "^3.1.29-beta.263",
66
+ "@tradejs/types": "^3.1.29-beta.263",
64
67
  "@types/bcryptjs": "2.4.6",
65
68
  "@types/lodash": "4.17.24",
66
69
  "@types/node": "24.13.3",
@@ -86,6 +89,7 @@
86
89
  "shiki": "3.23.0",
87
90
  "technicalindicators": "^3.1.0",
88
91
  "typescript": "5.9.3",
92
+ "zod": "^4.6.5",
89
93
  "zustand": "5.0.14"
90
94
  },
91
95
  "devDependencies": {
@@ -0,0 +1,3 @@
1
+ import { oauthMetadata, oauthResponse } from '#app/lib/mcp/oauth';
2
+ export const dynamic = 'force-dynamic';
3
+ export const GET = () => oauthResponse(oauthMetadata());
@@ -0,0 +1,3 @@
1
+ import { protectedResourceMetadata, oauthResponse } from '#app/lib/mcp/oauth';
2
+ export const dynamic = 'force-dynamic';
3
+ export const GET = () => oauthResponse(protectedResourceMetadata());
@@ -0,0 +1 @@
1
+ export { dynamic, GET } from './mcp/route';
@@ -5,6 +5,7 @@ import { useRouter, usePathname } from 'next/navigation';
5
5
  import { signOut } from 'next-auth/react';
6
6
  import {
7
7
  FiActivity,
8
+ FiLink,
8
9
  FiBarChart2,
9
10
  FiLayers,
10
11
  FiLogOut,
@@ -17,6 +18,7 @@ export const Sidebar = () => {
17
18
  const pathname = usePathname();
18
19
 
19
20
  const navItems = [
21
+ { icon: FiLink, label: 'MCP connections', path: '/routes/mcp' },
20
22
  {
21
23
  icon: FiBarChart2,
22
24
  label: 'Dashboard',
@@ -0,0 +1,63 @@
1
+ .main {
2
+ max-width: 800px;
3
+ margin: 40px auto;
4
+ padding: 24px;
5
+ }
6
+ .main h1 {
7
+ font-size: 28px;
8
+ font-weight: 600;
9
+ margin-bottom: 20px;
10
+ }
11
+ .main h2 {
12
+ font-size: 20px;
13
+ font-weight: 600;
14
+ margin-top: 28px;
15
+ margin-bottom: 12px;
16
+ }
17
+ .main h3 {
18
+ font-size: 17px;
19
+ font-weight: 600;
20
+ margin-bottom: 8px;
21
+ }
22
+ .main p {
23
+ line-height: 1.6;
24
+ margin: 12px 0;
25
+ overflow-wrap: anywhere;
26
+ }
27
+ .main pre {
28
+ padding: 16px;
29
+ border: 1px solid var(--chakra-colors-border, #444);
30
+ border-radius: 8px;
31
+ overflow-x: auto;
32
+ font-size: 13px;
33
+ }
34
+ .main button {
35
+ padding: 8px 16px;
36
+ border: 1px solid var(--chakra-colors-border, #555);
37
+ border-radius: 6px;
38
+ cursor: pointer;
39
+ margin: 8px 8px 8px 0;
40
+ }
41
+ .main button:hover {
42
+ background: var(--chakra-colors-bg-muted, #333);
43
+ }
44
+ .main button:focus-visible,
45
+ .main input:focus-visible {
46
+ outline: 2px solid #60a5fa;
47
+ outline-offset: 3px;
48
+ }
49
+ .main fieldset {
50
+ margin: 20px 0;
51
+ padding: 16px;
52
+ border: 1px solid var(--chakra-colors-border, #444);
53
+ border-radius: 8px;
54
+ }
55
+ .main a {
56
+ text-decoration: underline;
57
+ }
58
+ .connection {
59
+ margin-top: 20px;
60
+ padding: 20px;
61
+ border: 1px solid var(--chakra-colors-border, #444);
62
+ border-radius: 8px;
63
+ }
@@ -0,0 +1,225 @@
1
+ import { open, readdir, readFile, realpath, stat } from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import {
4
+ verifyRuntimeEvidenceBundle,
5
+ verifyRuntimeFeedbackReplayBundle,
6
+ } from '@tradejs/node/diagnostics';
7
+ import { mcpStorage } from '@tradejs/infra/redis';
8
+ import {
9
+ isMcpRecord,
10
+ mcpId,
11
+ mcpProjectRoot,
12
+ readMcpDeployments,
13
+ } from '@tradejs/node/mcp';
14
+
15
+ interface CatalogEntry {
16
+ id: string;
17
+ userName: string;
18
+ deploymentId: string;
19
+ kind: 'runtime-evidence' | 'runtime-feedback-replay';
20
+ bundleDir: string;
21
+ payloadPath: string;
22
+ sha256: string;
23
+ bytes: number;
24
+ manifest: Record<string, unknown>;
25
+ }
26
+ const storageKey = (userName: string, id: string) =>
27
+ `artifacts:${mcpId(userName)}:${id}`;
28
+ const MAX_CATALOG_PAYLOAD_BYTES = 64 * 1024 * 1024;
29
+ const checkPayload = async (file: string) => {
30
+ const resolved = await confinedPath(file);
31
+ if ((await stat(resolved)).size > MAX_CATALOG_PAYLOAD_BYTES)
32
+ throw new Error('Artifact exceeds catalog size limit');
33
+ return resolved;
34
+ };
35
+ const dataRoot = () => path.join(mcpProjectRoot(), 'data');
36
+
37
+ const confinedPath = async (file: string) => {
38
+ const [root, resolved] = await Promise.all([
39
+ realpath(dataRoot()),
40
+ realpath(file),
41
+ ]);
42
+ if (!resolved.startsWith(`${root}${path.sep}`))
43
+ throw new Error('Artifact outside data root');
44
+ return resolved;
45
+ };
46
+
47
+ const verifyEntry = async (
48
+ bundleDir: string,
49
+ userName: string,
50
+ allowed: Set<string>,
51
+ ): Promise<CatalogEntry> => {
52
+ const manifestPath = await confinedPath(
53
+ path.join(bundleDir, 'manifest.json'),
54
+ );
55
+ const manifestStat = await stat(manifestPath);
56
+ if (manifestStat.size > 256 * 1024) throw new Error('Manifest too large');
57
+ const header: unknown = JSON.parse(await readFile(manifestPath, 'utf8'));
58
+ if (
59
+ !isMcpRecord(header) ||
60
+ header.userName !== userName ||
61
+ typeof header.deploymentId !== 'string' ||
62
+ !allowed.has(header.deploymentId)
63
+ )
64
+ throw new Error('Artifact unavailable for this user');
65
+ if (header.reportType === 'runtime-evidence') {
66
+ await checkPayload(path.join(bundleDir, 'runtime-evidence.json'));
67
+ const verified = await verifyRuntimeEvidenceBundle(bundleDir);
68
+ if (verified.artifact.userName !== userName)
69
+ throw new Error('Artifact unavailable for this user');
70
+ return {
71
+ id: mcpId(
72
+ `${verified.manifest.artifactId}:${verified.manifest.payload.sha256}`,
73
+ ),
74
+ userName,
75
+ deploymentId: verified.manifest.deploymentId,
76
+ kind: 'runtime-evidence',
77
+ bundleDir,
78
+ payloadPath: verified.payloadPath,
79
+ sha256: verified.manifest.payload.sha256,
80
+ bytes: verified.manifest.payload.bytes,
81
+ manifest: verified.manifest,
82
+ };
83
+ }
84
+ if (header.reportType === 'runtime-feedback-replay') {
85
+ await checkPayload(path.join(bundleDir, 'replay-runtime-evidence.json'));
86
+ await checkPayload(path.join(bundleDir, 'replay.log'));
87
+ const verified = await verifyRuntimeFeedbackReplayBundle(bundleDir);
88
+ return {
89
+ id: mcpId(
90
+ `${verified.manifest.artifactId}:${verified.manifest.payloads.replayEvidence.sha256}`,
91
+ ),
92
+ userName,
93
+ deploymentId: verified.manifest.deploymentId,
94
+ kind: 'runtime-feedback-replay',
95
+ bundleDir,
96
+ payloadPath: verified.replayEvidencePath,
97
+ sha256: verified.manifest.payloads.replayEvidence.sha256,
98
+ bytes: verified.manifest.payloads.replayEvidence.bytes,
99
+ manifest: verified.manifest,
100
+ };
101
+ }
102
+ throw new Error('Unsupported artifact');
103
+ };
104
+
105
+ export const listMcpArtifacts = async (userName: string) => {
106
+ const { deployments } = await readMcpDeployments(userName);
107
+ const allowed = new Set(deployments.map((row) => row.id));
108
+ const pending = [
109
+ 'runtime-evidence',
110
+ 'runtime-feedback',
111
+ `mcp/${mcpId(userName)}`,
112
+ ].map((dir) => ({ dir: path.join(dataRoot(), dir), depth: 0 }));
113
+ const items = [];
114
+ let visited = 0;
115
+ let invalidBundles = 0;
116
+ while (pending.length && visited < 2000 && items.length < 100) {
117
+ const current = pending.shift()!;
118
+ visited++;
119
+ let entries;
120
+ try {
121
+ entries = await readdir(current.dir, { withFileTypes: true });
122
+ } catch {
123
+ continue;
124
+ }
125
+ if (entries.some((entry) => entry.name === '.complete' && entry.isFile())) {
126
+ try {
127
+ const artifact = await verifyEntry(current.dir, userName, allowed);
128
+ await mcpStorage.put(storageKey(userName, artifact.id), artifact, 3600);
129
+ items.push({
130
+ id: artifact.id,
131
+ kind: artifact.kind,
132
+ deploymentId: artifact.deploymentId,
133
+ sha256: artifact.sha256,
134
+ bytes: artifact.bytes,
135
+ manifest: artifact.manifest,
136
+ });
137
+ } catch {
138
+ invalidBundles++;
139
+ }
140
+ }
141
+ if (current.depth < 12)
142
+ for (const entry of entries) {
143
+ if (entry.isDirectory() && !entry.isSymbolicLink())
144
+ pending.push({
145
+ dir: path.join(current.dir, entry.name),
146
+ depth: current.depth + 1,
147
+ });
148
+ }
149
+ }
150
+ return {
151
+ items,
152
+ observedAt: Date.now(),
153
+ source: 'verified_local_bundles',
154
+ truncated: pending.length > 0,
155
+ invalidOrInaccessibleBundles: invalidBundles,
156
+ note: 'Only verified, user-owned bundles available on this host are included.',
157
+ };
158
+ };
159
+
160
+ export const resolveMcpArtifact = async (userName: string, id: string) => {
161
+ if (!/^[a-f0-9]{64}$/.test(id)) throw new Error('Invalid artifact id');
162
+ const stored = await mcpStorage.get<CatalogEntry>(storageKey(userName, id));
163
+ if (!stored || stored.userName !== userName)
164
+ throw new Error('Artifact not found; refresh diagnostics_list_reports');
165
+ const { deployments } = await readMcpDeployments(userName);
166
+ const verified = await verifyEntry(
167
+ stored.bundleDir,
168
+ userName,
169
+ new Set(deployments.map((row) => row.id)),
170
+ );
171
+ if (verified.id !== id) throw new Error('Artifact changed since listing');
172
+ return verified;
173
+ };
174
+
175
+ export const readMcpArtifactChunk = async (
176
+ userName: string,
177
+ id: string,
178
+ offset: number,
179
+ length: number,
180
+ ) => {
181
+ const artifact = await resolveMcpArtifact(userName, id);
182
+ const handle = await open(await confinedPath(artifact.payloadPath), 'r');
183
+ try {
184
+ const buffer = Buffer.alloc(
185
+ Math.min(length, Math.max(0, artifact.bytes - offset)),
186
+ );
187
+ const { bytesRead } = await handle.read(buffer, 0, buffer.length, offset);
188
+ return {
189
+ id,
190
+ offset,
191
+ bytes: artifact.bytes,
192
+ sha256: artifact.sha256,
193
+ encoding: 'base64',
194
+ data: buffer.subarray(0, bytesRead).toString('base64'),
195
+ nextOffset:
196
+ offset + bytesRead < artifact.bytes ? offset + bytesRead : null,
197
+ };
198
+ } finally {
199
+ await handle.close();
200
+ }
201
+ };
202
+
203
+ export const readMcpDiagnosticReport = async (userName: string, id: string) => {
204
+ const artifact = await resolveMcpArtifact(userName, id);
205
+ const payload = JSON.parse(await readFile(artifact.payloadPath, 'utf8'));
206
+ // Large reports stay available through artifact_get with original checksums.
207
+ return {
208
+ id,
209
+ manifest: artifact.manifest,
210
+ bytes: artifact.bytes,
211
+ sha256: artifact.sha256,
212
+ ...(artifact.bytes <= 96000
213
+ ? { report: payload }
214
+ : {
215
+ summary: {
216
+ reportType: payload.reportType,
217
+ window: payload.window,
218
+ deployment: payload.deployment,
219
+ comparison: payload.comparison,
220
+ counts: payload.counts,
221
+ },
222
+ fullReport: 'Use artifact_get to read the complete original payload.',
223
+ }),
224
+ };
225
+ };
@@ -0,0 +1,24 @@
1
+ /** Bound allocations even when Content-Length is missing or untrusted. */
2
+ export const readMcpBody = async (request: Request, limit: number) => {
3
+ if (Number(request.headers.get('content-length') || 0) > limit)
4
+ throw new Error('Request too large');
5
+ const reader = request.body?.getReader();
6
+ if (!reader) return '';
7
+ const chunks: Uint8Array[] = [];
8
+ let bytes = 0;
9
+ try {
10
+ for (;;) {
11
+ const { done, value } = await reader.read();
12
+ if (done) break;
13
+ bytes += value.byteLength;
14
+ if (bytes > limit) {
15
+ await reader.cancel();
16
+ throw new Error('Request too large');
17
+ }
18
+ chunks.push(value);
19
+ }
20
+ return Buffer.concat(chunks).toString('utf8');
21
+ } finally {
22
+ reader.releaseLock();
23
+ }
24
+ };