proteum 2.2.9 → 2.4.1

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.
Files changed (59) hide show
  1. package/AGENTS.md +10 -4
  2. package/README.md +58 -15
  3. package/agents/project/AGENTS.md +53 -10
  4. package/agents/project/DOCUMENTATION.md +1326 -0
  5. package/agents/project/app-root/AGENTS.md +2 -2
  6. package/agents/project/diagnostics.md +12 -7
  7. package/agents/project/optimizations.md +1 -0
  8. package/agents/project/root/AGENTS.md +24 -9
  9. package/agents/project/tests/AGENTS.md +7 -0
  10. package/agents/project/tests/e2e/AGENTS.md +13 -0
  11. package/agents/project/tests/e2e/REAL_WORLD_JOURNEY_TESTS.md +192 -0
  12. package/cli/commands/connect.ts +40 -4
  13. package/cli/commands/dev.ts +148 -25
  14. package/cli/commands/diagnose.ts +138 -5
  15. package/cli/commands/doctor.ts +24 -4
  16. package/cli/commands/explain.ts +134 -6
  17. package/cli/commands/mcp.ts +133 -0
  18. package/cli/commands/orient.ts +93 -3
  19. package/cli/commands/perf.ts +118 -13
  20. package/cli/commands/runtime.ts +234 -0
  21. package/cli/commands/trace.ts +116 -21
  22. package/cli/mcp/router.ts +1010 -0
  23. package/cli/presentation/commands.ts +93 -26
  24. package/cli/presentation/devSession.ts +2 -0
  25. package/cli/presentation/help.ts +1 -1
  26. package/cli/runtime/commands.ts +215 -24
  27. package/cli/runtime/devSessions.ts +328 -2
  28. package/cli/runtime/mcpDaemon.ts +288 -0
  29. package/cli/runtime/ports.ts +151 -0
  30. package/cli/utils/agentOutput.ts +46 -0
  31. package/cli/utils/agents.ts +194 -51
  32. package/cli/utils/appRoots.ts +232 -0
  33. package/common/dev/diagnostics.ts +1 -1
  34. package/common/dev/inspection.ts +22 -7
  35. package/common/dev/mcpPayloads.ts +1150 -0
  36. package/common/dev/mcpServer.ts +287 -0
  37. package/docs/agent-routing.md +137 -0
  38. package/docs/dev-commands.md +2 -0
  39. package/docs/dev-sessions.md +4 -1
  40. package/docs/diagnostics.md +70 -24
  41. package/docs/mcp.md +206 -0
  42. package/docs/migrate-from-2.1.3.md +14 -6
  43. package/docs/request-tracing.md +12 -6
  44. package/package.json +11 -3
  45. package/server/app/devMcp.ts +204 -0
  46. package/server/services/router/http/cache.ts +116 -0
  47. package/server/services/router/http/index.ts +94 -35
  48. package/server/services/router/index.ts +8 -11
  49. package/server/services/router/request/ip.test.cjs +0 -1
  50. package/tests/agents-utils.test.cjs +92 -14
  51. package/tests/cli-mcp-command.test.cjs +262 -0
  52. package/tests/codex-mcp-usage.test.cjs +307 -0
  53. package/tests/dev-sessions.test.cjs +113 -0
  54. package/tests/dev-transpile-watch.test.cjs +117 -9
  55. package/tests/eslint-rules.test.cjs +0 -1
  56. package/tests/inspection.test.cjs +66 -0
  57. package/tests/mcp.test.cjs +873 -0
  58. package/tests/router-cache-config.test.cjs +73 -0
  59. package/vitest.config.mjs +9 -0
@@ -0,0 +1,873 @@
1
+ const assert = require('node:assert/strict');
2
+ const fs = require('node:fs');
3
+ const http = require('node:http');
4
+ const os = require('node:os');
5
+ const path = require('node:path');
6
+
7
+ const coreRoot = path.resolve(__dirname, '..');
8
+ process.env.TS_NODE_PROJECT = path.join(coreRoot, 'cli', 'tsconfig.json');
9
+ process.env.TS_NODE_TRANSPILE_ONLY = '1';
10
+ require('ts-node/register/transpile-only');
11
+ require('../cli/context.ts');
12
+
13
+ const { Client } = require('@modelcontextprotocol/sdk/client/index.js');
14
+ const { InMemoryTransport } = require('@modelcontextprotocol/sdk/inMemory.js');
15
+ const {
16
+ createMcpPayload,
17
+ compactOrientationResponse,
18
+ compactRouteCandidatesResponse,
19
+ compactTraceResponse,
20
+ compactWorkflowStartResponse,
21
+ resolveInstructionRouting,
22
+ } = require('../common/dev/mcpPayloads.ts');
23
+ const { createProteumMcpServer } = require('../common/dev/mcpServer.ts');
24
+ const { createProteumMachineMcpServer } = require('../cli/mcp/router.ts');
25
+ const {
26
+ createDevSessionRecord,
27
+ listMachineDevSessionInspections,
28
+ resolveMachineDevSessionFilePath,
29
+ resolveProteumProjectId,
30
+ writeMachineDevSessionRecord,
31
+ } = require('../cli/runtime/devSessions.ts');
32
+ const {
33
+ createMachineMcpDaemonRecord,
34
+ inspectMachineMcpDaemonRecord,
35
+ resolveMachineMcpDaemonPort,
36
+ resolveMachineMcpDaemonRecordFilePath,
37
+ writeMachineMcpDaemonRecord,
38
+ } = require('../cli/runtime/mcpDaemon.ts');
39
+
40
+ const writeFile = (filepath, content) => {
41
+ fs.mkdirSync(path.dirname(filepath), { recursive: true });
42
+ fs.writeFileSync(filepath, content);
43
+ };
44
+
45
+ const listen = async (server, port = 0) =>
46
+ await new Promise((resolve, reject) => {
47
+ server.once('error', reject);
48
+ server.listen(port, '127.0.0.1', () => resolve(server.address().port));
49
+ });
50
+
51
+ const closeServer = async (server) =>
52
+ await new Promise((resolve, reject) => {
53
+ server.close((error) => (error ? reject(error) : resolve()));
54
+ });
55
+
56
+ const createManifest = (appRoot, overrides = {}) => ({
57
+ version: 10,
58
+ app: {
59
+ root: appRoot,
60
+ coreRoot,
61
+ identityFilepath: path.join(appRoot, 'identity.config.ts'),
62
+ setupFilepath: path.join(appRoot, 'proteum.config.ts'),
63
+ identity: {
64
+ name: overrides.name || 'Test App',
65
+ identifier: overrides.identifier || 'TestApp',
66
+ description: '',
67
+ },
68
+ setup: {},
69
+ },
70
+ conventions: { routeOptionKeys: [], reservedRouteOptionKeys: [] },
71
+ env: {
72
+ source: 'test',
73
+ loadedVariableKeys: [],
74
+ requiredVariables: [],
75
+ resolved: {
76
+ name: 'test',
77
+ profile: 'dev',
78
+ routerPort: overrides.routerPort || 3104,
79
+ routerCurrentDomain: 'localhost',
80
+ routerInternalUrl: `http://localhost:${overrides.routerPort || 3104}`,
81
+ },
82
+ },
83
+ connectedProjects: [],
84
+ services: { app: [], routerPlugins: [] },
85
+ controllers: overrides.controllers || [],
86
+ commands: [],
87
+ routes: {
88
+ client: overrides.clientRoutes || [],
89
+ server: overrides.serverRoutes || [],
90
+ },
91
+ layouts: [],
92
+ diagnostics: overrides.diagnostics || [],
93
+ });
94
+
95
+ const writeProteumAppFixture = (appRoot, manifestOverrides = {}) => {
96
+ writeFile(path.join(appRoot, 'package.json'), '{"name":"fixture"}\n');
97
+ writeFile(path.join(appRoot, 'identity.config.ts'), 'export default {};\n');
98
+ writeFile(path.join(appRoot, 'proteum.config.ts'), 'export default {};\n');
99
+ writeFile(path.join(appRoot, 'client', 'AGENTS.md'), '# Client\n');
100
+ writeFile(path.join(appRoot, 'client', 'pages', 'AGENTS.md'), '# Pages\n');
101
+ writeFile(path.join(appRoot, 'server', 'AGENTS.md'), '# Server\n');
102
+ writeFile(path.join(appRoot, 'server', 'routes', 'AGENTS.md'), '# Routes\n');
103
+ writeFile(path.join(appRoot, 'AGENTS.md'), '# App\n');
104
+ writeFile(path.join(appRoot, 'diagnostics.md'), '# Diagnostics\n');
105
+ writeFile(path.join(appRoot, '.proteum', 'manifest.json'), JSON.stringify(createManifest(appRoot, manifestOverrides), null, 2));
106
+ };
107
+
108
+ test('instruction routing returns compact selected files for a page query', () => {
109
+ const appRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'proteum-mcp-app-'));
110
+
111
+ writeFile(path.join(appRoot, 'AGENTS.md'), '# App Agents\n\n- root\n');
112
+ writeFile(path.join(appRoot, 'client', 'AGENTS.md'), '# Client Agents\n\n- client\n');
113
+ writeFile(path.join(appRoot, 'client', 'pages', 'AGENTS.md'), '# Page Agents\n\n- pages\n');
114
+ writeFile(path.join(appRoot, 'DOCUMENTATION.md'), '# Documentation\n\n- docs\n');
115
+ writeFile(path.join(appRoot, 'diagnostics.md'), '# Diagnostics\n\n- diagnose\n');
116
+
117
+ const payload = resolveInstructionRouting({ appRoot, query: '/domains/:slug client/pages/domain.tsx' });
118
+
119
+ assert.equal(payload.ok, true);
120
+ assert.equal(payload.format, 'proteum-mcp-v1');
121
+ assert.deepEqual(
122
+ payload.data.selected.map((entry) => path.relative(appRoot, entry.file)).sort(),
123
+ ['AGENTS.md', 'client/AGENTS.md', 'client/pages/AGENTS.md'],
124
+ );
125
+ assert.equal(payload.data.readWhen.some((entry) => entry.file && entry.file.endsWith('DOCUMENTATION.md')), true);
126
+ assert.equal(payload.data.readWhen.some((entry) => entry.file && entry.file.endsWith('diagnostics.md')), true);
127
+ });
128
+
129
+ test('instruction routing promotes triggered full instruction files', () => {
130
+ const appRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'proteum-mcp-trigger-app-'));
131
+ const fallbackRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'proteum-mcp-trigger-core-'));
132
+ const fallbackAgents = path.join(fallbackRoot, 'agents', 'project', 'AGENTS.md');
133
+
134
+ writeFile(fallbackAgents, '# Full Root Contract\n\n- Conventional Commits live here.\n');
135
+ writeFile(
136
+ path.join(appRoot, 'AGENTS.md'),
137
+ ['# App Router', '', `- Root contract fallback: ${fallbackAgents}`, ''].join('\n'),
138
+ );
139
+ writeFile(path.join(appRoot, 'CODING_STYLE.md'), '# Coding Style\n\n- Style\n');
140
+ writeFile(path.join(appRoot, 'diagnostics.md'), '# Diagnostics\n\n- Diagnose\n');
141
+ writeFile(path.join(appRoot, 'DOCUMENTATION.md'), '# Documentation\n\n- Docs\n');
142
+ writeFile(path.join(appRoot, 'optimizations.md'), '# Optimizations\n\n- Optimize\n');
143
+
144
+ const payload = resolveInstructionRouting({ appRoot, query: 'increase quota and commit' });
145
+ const selected = payload.data.selected.map((entry) => entry.file);
146
+
147
+ assert.equal(selected.includes(path.join(appRoot, 'AGENTS.md')), true);
148
+ assert.equal(selected.includes(fallbackAgents), true);
149
+ assert.equal(selected.includes(path.join(appRoot, 'CODING_STYLE.md')), true);
150
+ assert.equal(
151
+ payload.data.selected.some(
152
+ (entry) => entry.file === fallbackAgents && /Git lifecycle trigger/.test(entry.reason),
153
+ ),
154
+ true,
155
+ );
156
+ assert.equal(payload.data.fullReadPolicy.default.includes('read-only'), true);
157
+ assert.equal(payload.data.selected.some((entry) => entry.fullRead === 'full-before-action'), true);
158
+ });
159
+
160
+ test('workflow start payload combines compact runtime, instructions, owner, and duplicate guidance', () => {
161
+ const appRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'proteum-mcp-workflow-app-'));
162
+ const pageFile = path.join(appRoot, 'client/pages/domains.tsx');
163
+
164
+ writeFile(path.join(appRoot, 'AGENTS.md'), '# App Agents\n\n- root\n');
165
+ writeFile(path.join(appRoot, 'client', 'AGENTS.md'), '# Client Agents\n\n- client\n');
166
+ writeFile(path.join(appRoot, 'client', 'pages', 'AGENTS.md'), '# Page Agents\n\n- pages\n');
167
+ writeFile(pageFile, 'export default function Domains() { return null; }\n');
168
+
169
+ const manifest = {
170
+ version: 10,
171
+ app: {
172
+ root: appRoot,
173
+ coreRoot,
174
+ identityFilepath: path.join(appRoot, 'identity.config.ts'),
175
+ setupFilepath: path.join(appRoot, 'proteum.config.ts'),
176
+ identity: {
177
+ name: 'Workflow App',
178
+ identifier: 'WorkflowApp',
179
+ description: '',
180
+ },
181
+ setup: {},
182
+ },
183
+ conventions: { routeOptionKeys: [], reservedRouteOptionKeys: [] },
184
+ env: {
185
+ source: 'test',
186
+ loadedVariableKeys: [],
187
+ requiredVariables: [],
188
+ resolved: {
189
+ name: 'test',
190
+ profile: 'dev',
191
+ routerPort: 3104,
192
+ routerCurrentDomain: 'localhost',
193
+ routerInternalUrl: 'http://localhost:3104',
194
+ },
195
+ },
196
+ connectedProjects: [],
197
+ services: { app: [], routerPlugins: [] },
198
+ controllers: [],
199
+ commands: [],
200
+ routes: { client: [], server: [] },
201
+ layouts: [],
202
+ diagnostics: [],
203
+ };
204
+ const doctor = { summary: { errors: 0, warnings: 0, strictFailed: false }, diagnostics: [] };
205
+ const payload = compactWorkflowStartResponse({
206
+ contracts: doctor,
207
+ doctor,
208
+ manifest,
209
+ owner: {
210
+ matches: [
211
+ {
212
+ details: [],
213
+ kind: 'route',
214
+ label: '/domains',
215
+ matchedOn: ['path'],
216
+ originHint: 'manifest',
217
+ scopeLabel: 'local',
218
+ score: 100,
219
+ source: { filepath: pageFile, line: 1, column: 1 },
220
+ },
221
+ ],
222
+ normalizedQuery: '/domains',
223
+ query: '/domains',
224
+ },
225
+ route: '/domains',
226
+ runtime: { publicUrl: 'http://localhost:3104', mcpUrl: 'http://localhost:3104/__proteum/mcp' },
227
+ task: 'read-only runtime health pass',
228
+ });
229
+
230
+ assert.equal(payload.data.runtime.manifest.identifier, 'WorkflowApp');
231
+ assert.equal(payload.data.instructions.selected.length >= 2, true);
232
+ assert.equal(payload.data.owner.top.label, '/domains');
233
+ assert.equal(payload.nextActions[0].tool, 'diagnose');
234
+ assert.equal(payload.data.duplicateAvoidance.some((line) => /do not run CLI runtime status/.test(line)), true);
235
+ });
236
+
237
+ test('orientation payload suggests MCP owner and runtime next actions before CLI fallback', () => {
238
+ const appRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'proteum-mcp-orient-app-'));
239
+
240
+ writeFile(path.join(appRoot, 'AGENTS.md'), '# App Agents\n\n- root\n');
241
+ writeFile(path.join(appRoot, 'diagnostics.md'), '# Diagnostics\n\n- diagnose\n');
242
+ writeFile(path.join(appRoot, 'DOCUMENTATION.md'), '# Documentation\n\n- docs\n');
243
+ writeFile(path.join(appRoot, 'CODING_STYLE.md'), '# Coding Style\n\n- style\n');
244
+ writeFile(path.join(appRoot, 'optimizations.md'), '# Optimizations\n\n- optimize\n');
245
+
246
+ const payload = compactOrientationResponse({
247
+ app: { appRoot, identifier: 'TestApp', repoRoot: appRoot, routerPort: 3101 },
248
+ connected: { imports: [], producers: [] },
249
+ guidance: {
250
+ agents: path.join(appRoot, 'AGENTS.md'),
251
+ areaAgents: [],
252
+ codingStyle: path.join(appRoot, 'CODING_STYLE.md'),
253
+ diagnostics: path.join(appRoot, 'diagnostics.md'),
254
+ documentation: path.join(appRoot, 'DOCUMENTATION.md'),
255
+ optimizations: path.join(appRoot, 'optimizations.md'),
256
+ },
257
+ normalizedQuery: '/auth/login',
258
+ nextSteps: [{ command: 'proteum orient /auth/login', label: 'CLI Orient', reason: 'Fallback command.' }],
259
+ owner: {
260
+ matches: [
261
+ {
262
+ details: [],
263
+ kind: 'route',
264
+ label: '/auth/login',
265
+ matchedOn: ['path'],
266
+ originHint: 'manifest',
267
+ scopeLabel: 'local',
268
+ score: 100,
269
+ source: { filepath: path.join(appRoot, 'client/pages/auth.tsx'), line: 1, column: 1 },
270
+ },
271
+ ],
272
+ normalizedQuery: '/auth/login',
273
+ query: '/auth/login',
274
+ },
275
+ query: '/auth/login',
276
+ warnings: [],
277
+ });
278
+
279
+ assert.equal(payload.nextActions[0].tool, 'explain_summary');
280
+ assert.equal(payload.nextActions[1].tool, 'diagnose');
281
+ assert.equal(payload.nextActions[2].tool, 'perf_request');
282
+ assert.equal(payload.nextActions.some((action) => action.command === 'proteum orient /auth/login'), true);
283
+ });
284
+
285
+ test('route candidates payload avoids raw route dumps', () => {
286
+ const appRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'proteum-mcp-routes-app-'));
287
+ const routeFile = path.join(appRoot, 'client/pages/domains.tsx');
288
+ const manifest = createManifest(appRoot, {
289
+ clientRoutes: [
290
+ {
291
+ chunkId: 'domains',
292
+ filepath: routeFile,
293
+ hasData: false,
294
+ invalidOptionKeys: [],
295
+ kind: 'client-page',
296
+ methodName: 'page',
297
+ normalizedOptionKeys: [],
298
+ optionKeys: [],
299
+ path: '/domains',
300
+ scope: 'app',
301
+ serviceLocalName: 'Router',
302
+ sourceLocation: { line: 1, column: 1 },
303
+ targetResolution: 'literal',
304
+ },
305
+ ],
306
+ });
307
+
308
+ const payload = compactRouteCandidatesResponse({ manifest, query: '/domains' });
309
+
310
+ assert.equal(payload.data.candidates.length, 1);
311
+ assert.equal(payload.data.candidates[0].label, '/domains');
312
+ assert.equal(payload.nextActions[0].tool, 'explain_summary');
313
+ });
314
+
315
+ test('trace payload keeps default output compact and paginates full details', () => {
316
+ const request = {
317
+ id: 'req_1',
318
+ method: 'GET',
319
+ path: '/domains',
320
+ url: 'http://localhost:3000/domains',
321
+ capture: 'deep',
322
+ startedAt: new Date().toISOString(),
323
+ finishedAt: new Date().toISOString(),
324
+ durationMs: 42,
325
+ statusCode: 200,
326
+ droppedEvents: 0,
327
+ calls: Array.from({ length: 3 }, (_, index) => ({
328
+ id: `call_${index}`,
329
+ origin: 'server',
330
+ label: `Call ${index}`,
331
+ method: 'POST',
332
+ path: `/api/${index}`,
333
+ statusCode: 200,
334
+ durationMs: 10 + index,
335
+ requestDataKeys: [],
336
+ resultKeys: [],
337
+ })),
338
+ events: Array.from({ length: 12 }, (_, index) => ({
339
+ index,
340
+ elapsedMs: index,
341
+ type: index === 2 ? 'error' : 'mark',
342
+ details: { index, long: 'x'.repeat(300) },
343
+ })),
344
+ sqlQueries: Array.from({ length: 4 }, (_, index) => ({
345
+ id: `sql_${index}`,
346
+ callerLabel: 'Service.query',
347
+ callerMethod: 'query',
348
+ callerPath: 'server/services/Domain/index.ts',
349
+ kind: 'query',
350
+ operation: 'findMany',
351
+ model: 'Domain',
352
+ durationMs: index + 1,
353
+ fingerprint: `fp_${index}`,
354
+ query: 'select * from Domain where id = ?',
355
+ })),
356
+ };
357
+
358
+ const compact = compactTraceResponse({ request });
359
+ const full = compactTraceResponse({ detail: 'full', limit: 5, offset: 4, request });
360
+
361
+ assert.equal(compact.data.page, undefined);
362
+ assert.equal(compact.omitted.length, 1);
363
+ assert.equal(full.data.page.events.length, 5);
364
+ assert.equal(full.data.page.hasMore, true);
365
+ });
366
+
367
+ test('MCP server registers the Proteum read-only tool contract', async () => {
368
+ const payload = createMcpPayload({ summary: 'ok', data: { value: 1 } });
369
+ const provider = {
370
+ diagnose: async () => payload,
371
+ doctor: async () => payload,
372
+ explainSummary: async () => payload,
373
+ instructionsResolve: async () => payload,
374
+ logsTail: async () => payload,
375
+ orient: async () => payload,
376
+ perfRequest: async () => payload,
377
+ perfTop: async () => payload,
378
+ readResource: async () => payload,
379
+ routeCandidates: async () => payload,
380
+ runtimeStatus: async () => payload,
381
+ traceLatest: async () => payload,
382
+ traceShow: async () => payload,
383
+ workflowStart: async () => payload,
384
+ };
385
+ const server = createProteumMcpServer({ provider, version: 'test' });
386
+ const client = new Client({ name: 'mcp-test', version: '1.0.0' });
387
+ const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
388
+
389
+ await server.connect(serverTransport);
390
+ await client.connect(clientTransport);
391
+
392
+ const tools = await client.listTools();
393
+ const result = await client.callTool({ name: 'runtime_status', arguments: {} });
394
+ const resource = await client.readResource({ uri: 'proteum://runtime/status' });
395
+
396
+ assert.equal(tools.tools.some((tool) => tool.name === 'runtime_status'), true);
397
+ assert.equal(tools.tools.some((tool) => tool.name === 'workflow_start'), true);
398
+ assert.equal(tools.tools.some((tool) => tool.name === 'route_candidates'), true);
399
+ assert.match(result.content[0].text, /proteum-mcp-v1/);
400
+ assert.match(resource.contents[0].text, /proteum-mcp-v1/);
401
+
402
+ await client.close();
403
+ await server.close();
404
+ });
405
+
406
+ test('machine project id is deterministic for a canonical app root', async () => {
407
+ const appRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'proteum-project-id-'));
408
+
409
+ const firstProjectId = await resolveProteumProjectId(appRoot);
410
+ const secondProjectId = await resolveProteumProjectId(appRoot);
411
+
412
+ assert.equal(firstProjectId, secondProjectId);
413
+ assert.match(firstProjectId, /^prj_[a-f0-9]{12}$/);
414
+ });
415
+
416
+ test('machine registry cleans stale sessions and preserves live sessions', async (t) => {
417
+ const previousRegistryDir = process.env.PROTEUM_MACHINE_DEV_SESSION_DIR;
418
+ const registryDir = fs.mkdtempSync(path.join(os.tmpdir(), 'proteum-machine-registry-'));
419
+ process.env.PROTEUM_MACHINE_DEV_SESSION_DIR = registryDir;
420
+ t.onTestFinished(() => {
421
+ if (previousRegistryDir === undefined) delete process.env.PROTEUM_MACHINE_DEV_SESSION_DIR;
422
+ else process.env.PROTEUM_MACHINE_DEV_SESSION_DIR = previousRegistryDir;
423
+ });
424
+
425
+ const liveAppRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'proteum-machine-live-'));
426
+ const staleAppRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'proteum-machine-stale-'));
427
+ const liveRecord = createDevSessionRecord({
428
+ appRoot: liveAppRoot,
429
+ port: 3101,
430
+ sessionFilePath: path.join(liveAppRoot, 'var/run/proteum/dev/3101.json'),
431
+ });
432
+ const staleRecord = {
433
+ ...createDevSessionRecord({
434
+ appRoot: staleAppRoot,
435
+ port: 3102,
436
+ sessionFilePath: path.join(staleAppRoot, 'var/run/proteum/dev/3102.json'),
437
+ }),
438
+ pid: 999999,
439
+ };
440
+
441
+ const liveMachineRecord = await writeMachineDevSessionRecord(liveRecord);
442
+ const staleMachineRecord = await writeMachineDevSessionRecord(staleRecord);
443
+ const invalidFilePath = path.join(registryDir, 'invalid.json');
444
+ fs.writeFileSync(invalidFilePath, '{ invalid json');
445
+
446
+ const inspections = await listMachineDevSessionInspections();
447
+
448
+ assert.deepEqual(
449
+ inspections.map((inspection) => inspection.record.projectId),
450
+ [liveMachineRecord.projectId],
451
+ );
452
+ assert.equal(fs.existsSync(resolveMachineDevSessionFilePath(liveMachineRecord.projectId)), true);
453
+ assert.equal(fs.existsSync(resolveMachineDevSessionFilePath(staleMachineRecord.projectId)), false);
454
+ assert.equal(fs.existsSync(invalidFilePath), false);
455
+ });
456
+
457
+ test('machine MCP daemon registry cleans stale records and resolves port override', async (t) => {
458
+ const previousRegistryDir = process.env.PROTEUM_MACHINE_MCP_DIR;
459
+ const previousPort = process.env.PROTEUM_MCP_PORT;
460
+ const registryDir = fs.mkdtempSync(path.join(os.tmpdir(), 'proteum-mcp-daemon-'));
461
+ process.env.PROTEUM_MACHINE_MCP_DIR = registryDir;
462
+ process.env.PROTEUM_MCP_PORT = '4567';
463
+ t.onTestFinished(() => {
464
+ if (previousRegistryDir === undefined) delete process.env.PROTEUM_MACHINE_MCP_DIR;
465
+ else process.env.PROTEUM_MACHINE_MCP_DIR = previousRegistryDir;
466
+ if (previousPort === undefined) delete process.env.PROTEUM_MCP_PORT;
467
+ else process.env.PROTEUM_MCP_PORT = previousPort;
468
+ });
469
+
470
+ const liveRecord = createMachineMcpDaemonRecord({
471
+ command: [process.execPath, 'cli/bin.js', 'mcp', '--daemon'],
472
+ port: resolveMachineMcpDaemonPort(),
473
+ });
474
+ await writeMachineMcpDaemonRecord(liveRecord);
475
+
476
+ const liveInspection = await inspectMachineMcpDaemonRecord();
477
+
478
+ assert.equal(liveInspection.live, true);
479
+ assert.equal(liveInspection.record.port, 4567);
480
+ assert.equal(liveInspection.record.mcpUrl, 'http://127.0.0.1:4567/mcp');
481
+
482
+ await writeMachineMcpDaemonRecord({
483
+ ...liveRecord,
484
+ pid: 999999,
485
+ });
486
+
487
+ const staleInspection = await inspectMachineMcpDaemonRecord();
488
+
489
+ assert.equal(staleInspection.live, false);
490
+ assert.equal(staleInspection.stale, true);
491
+ assert.equal(fs.existsSync(resolveMachineMcpDaemonRecordFilePath()), false);
492
+ });
493
+
494
+ test('machine MCP router rejects app-bound tools without projectId', async () => {
495
+ const server = createProteumMachineMcpServer({ version: 'test' });
496
+ const client = new Client({ name: 'machine-mcp-test', version: '1.0.0' });
497
+ const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
498
+
499
+ await server.connect(serverTransport);
500
+ await client.connect(clientTransport);
501
+
502
+ const result = await client.callTool({ name: 'runtime_status', arguments: {} });
503
+
504
+ assert.equal(result.isError, true);
505
+ assert.match(result.content[0].text, /Missing required projectId/);
506
+ assert.match(result.content[0].text, /projects_list/);
507
+
508
+ await client.close();
509
+ await server.close();
510
+ });
511
+
512
+ test('machine MCP router forwards app tools without leaking projectId', async (t) => {
513
+ const previousRegistryDir = process.env.PROTEUM_MACHINE_DEV_SESSION_DIR;
514
+ const registryDir = fs.mkdtempSync(path.join(os.tmpdir(), 'proteum-machine-router-'));
515
+ process.env.PROTEUM_MACHINE_DEV_SESSION_DIR = registryDir;
516
+ t.onTestFinished(() => {
517
+ if (previousRegistryDir === undefined) delete process.env.PROTEUM_MACHINE_DEV_SESSION_DIR;
518
+ else process.env.PROTEUM_MACHINE_DEV_SESSION_DIR = previousRegistryDir;
519
+ });
520
+
521
+ const appRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'proteum-machine-app-'));
522
+ const sessionRecord = createDevSessionRecord({
523
+ appRoot,
524
+ port: 3103,
525
+ sessionFilePath: path.join(appRoot, 'var/run/proteum/dev/3103.json'),
526
+ });
527
+ const machineRecord = await writeMachineDevSessionRecord({
528
+ ...sessionRecord,
529
+ publicUrl: 'http://localhost:3103',
530
+ state: 'ready',
531
+ });
532
+ let forwardedCall = null;
533
+ let closeCount = 0;
534
+ const server = createProteumMachineMcpServer({
535
+ createDevMcpClient: async () => ({
536
+ callTool: async (input) => {
537
+ forwardedCall = input;
538
+ return {
539
+ content: [
540
+ {
541
+ type: 'text',
542
+ text: JSON.stringify({ ok: true, format: 'proteum-mcp-v1', summary: 'forwarded', data: {} }),
543
+ },
544
+ ],
545
+ };
546
+ },
547
+ close: async () => {
548
+ closeCount += 1;
549
+ },
550
+ }),
551
+ version: 'test',
552
+ });
553
+ const client = new Client({ name: 'machine-mcp-test', version: '1.0.0' });
554
+ const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
555
+
556
+ await server.connect(serverTransport);
557
+ await client.connect(clientTransport);
558
+
559
+ const projects = await client.callTool({ name: 'projects_list', arguments: {} });
560
+ await client.callTool({
561
+ name: 'orient',
562
+ arguments: {
563
+ projectId: machineRecord.projectId,
564
+ query: '/domains',
565
+ },
566
+ });
567
+
568
+ assert.equal(projects.content[0].text.includes(machineRecord.projectId), true);
569
+ assert.equal(forwardedCall.name, 'orient');
570
+ assert.deepEqual(forwardedCall.arguments, { query: '/domains' });
571
+
572
+ await client.close();
573
+ await server.close();
574
+ assert.equal(closeCount, 1);
575
+ });
576
+
577
+ test('machine MCP router resolves projects by cwd and bootstraps workflow without duplicate discovery', async (t) => {
578
+ const previousRegistryDir = process.env.PROTEUM_MACHINE_DEV_SESSION_DIR;
579
+ const registryDir = fs.mkdtempSync(path.join(os.tmpdir(), 'proteum-machine-workflow-router-'));
580
+ process.env.PROTEUM_MACHINE_DEV_SESSION_DIR = registryDir;
581
+ t.onTestFinished(() => {
582
+ if (previousRegistryDir === undefined) delete process.env.PROTEUM_MACHINE_DEV_SESSION_DIR;
583
+ else process.env.PROTEUM_MACHINE_DEV_SESSION_DIR = previousRegistryDir;
584
+ });
585
+
586
+ const repoRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'proteum-machine-monorepo-'));
587
+ const productRoot = path.join(repoRoot, 'apps', 'product');
588
+ const websiteRoot = path.join(repoRoot, 'apps', 'website');
589
+ const productCwd = path.join(productRoot, 'client', 'pages');
590
+ fs.mkdirSync(productCwd, { recursive: true });
591
+ fs.mkdirSync(websiteRoot, { recursive: true });
592
+
593
+ const productMachineRecord = await writeMachineDevSessionRecord({
594
+ ...createDevSessionRecord({
595
+ appRoot: productRoot,
596
+ port: 3105,
597
+ sessionFilePath: path.join(productRoot, 'var/run/proteum/dev/3105.json'),
598
+ }),
599
+ publicUrl: 'http://localhost:3105',
600
+ state: 'ready',
601
+ });
602
+ await writeMachineDevSessionRecord({
603
+ ...createDevSessionRecord({
604
+ appRoot: websiteRoot,
605
+ port: 3106,
606
+ sessionFilePath: path.join(websiteRoot, 'var/run/proteum/dev/3106.json'),
607
+ }),
608
+ publicUrl: 'http://localhost:3106',
609
+ state: 'ready',
610
+ });
611
+
612
+ let forwardedCall = null;
613
+ const server = createProteumMachineMcpServer({
614
+ createDevMcpClient: async () => ({
615
+ callTool: async (input) => {
616
+ forwardedCall = input;
617
+ return {
618
+ content: [
619
+ {
620
+ type: 'text',
621
+ text: JSON.stringify({
622
+ ok: true,
623
+ format: 'proteum-mcp-v1',
624
+ summary: 'workflow',
625
+ data: { runtime: { appRoot: productRoot } },
626
+ nextActions: [{ label: 'Diagnose Route', tool: 'diagnose', toolArgs: { path: '/domains' } }],
627
+ }),
628
+ },
629
+ ],
630
+ };
631
+ },
632
+ close: async () => {},
633
+ }),
634
+ version: 'test',
635
+ });
636
+ const client = new Client({ name: 'machine-mcp-test', version: '1.0.0' });
637
+ const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
638
+
639
+ await server.connect(serverTransport);
640
+ await client.connect(clientTransport);
641
+
642
+ const monorepoResolve = await client.callTool({ name: 'project_resolve', arguments: { cwd: repoRoot } });
643
+ const monorepoPayload = JSON.parse(monorepoResolve.content[0].text);
644
+ assert.equal(monorepoPayload.data.projects.length, 2);
645
+ assert.equal(monorepoPayload.data.projects.every((project) => project.matchReason === 'app-under-cwd'), true);
646
+
647
+ const directResolve = await client.callTool({ name: 'project_resolve', arguments: { cwd: productCwd } });
648
+ const directPayload = JSON.parse(directResolve.content[0].text);
649
+ assert.equal(directPayload.data.projects.length, 1);
650
+ assert.equal(directPayload.data.projects[0].projectId, productMachineRecord.projectId);
651
+ assert.equal(directPayload.data.projects[0].matchReason, 'cwd-inside-app');
652
+
653
+ const workflow = await client.callTool({
654
+ name: 'workflow_start',
655
+ arguments: { cwd: productCwd, route: '/domains', task: 'read-only runtime health pass' },
656
+ });
657
+ const workflowPayload = JSON.parse(workflow.content[0].text);
658
+
659
+ assert.equal(forwardedCall.name, 'workflow_start');
660
+ assert.deepEqual(forwardedCall.arguments, { route: '/domains', task: 'read-only runtime health pass' });
661
+ assert.equal(workflowPayload.data.project.projectId, productMachineRecord.projectId);
662
+ assert.equal(workflowPayload.nextActions[0].toolArgs.projectId, productMachineRecord.projectId);
663
+
664
+ await client.close();
665
+ await server.close();
666
+ });
667
+
668
+ test('machine MCP router resolves offline monorepo app candidates before dev is running', async (t) => {
669
+ const previousRegistryDir = process.env.PROTEUM_MACHINE_DEV_SESSION_DIR;
670
+ const registryDir = fs.mkdtempSync(path.join(os.tmpdir(), 'proteum-machine-offline-registry-'));
671
+ process.env.PROTEUM_MACHINE_DEV_SESSION_DIR = registryDir;
672
+ t.onTestFinished(() => {
673
+ if (previousRegistryDir === undefined) delete process.env.PROTEUM_MACHINE_DEV_SESSION_DIR;
674
+ else process.env.PROTEUM_MACHINE_DEV_SESSION_DIR = previousRegistryDir;
675
+ });
676
+
677
+ const repoRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'proteum-machine-offline-monorepo-'));
678
+ const productRoot = path.join(repoRoot, 'apps', 'product');
679
+ const websiteRoot = path.join(repoRoot, 'apps', 'website');
680
+ const domainsFile = path.join(productRoot, 'client/pages/domains.tsx');
681
+
682
+ writeProteumAppFixture(productRoot, {
683
+ identifier: 'ProductApp',
684
+ name: 'Product',
685
+ routerPort: 3020,
686
+ clientRoutes: [
687
+ {
688
+ chunkId: 'domains',
689
+ filepath: domainsFile,
690
+ hasData: false,
691
+ invalidOptionKeys: [],
692
+ kind: 'client-page',
693
+ methodName: 'page',
694
+ normalizedOptionKeys: [],
695
+ optionKeys: [],
696
+ path: '/domains',
697
+ scope: 'app',
698
+ serviceLocalName: 'Router',
699
+ sourceLocation: { line: 1, column: 1 },
700
+ targetResolution: 'literal',
701
+ },
702
+ ],
703
+ });
704
+ writeProteumAppFixture(websiteRoot, {
705
+ identifier: 'WebsiteApp',
706
+ name: 'Website',
707
+ routerPort: 3021,
708
+ });
709
+
710
+ const server = createProteumMachineMcpServer({ version: 'test' });
711
+ const client = new Client({ name: 'machine-mcp-test', version: '1.0.0' });
712
+ const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
713
+
714
+ await server.connect(serverTransport);
715
+ await client.connect(clientTransport);
716
+
717
+ const resolve = await client.callTool({ name: 'project_resolve', arguments: { cwd: repoRoot } });
718
+ const resolvePayload = JSON.parse(resolve.content[0].text);
719
+
720
+ assert.equal(resolvePayload.data.projects.length, 2);
721
+ assert.equal(resolvePayload.data.projects.every((project) => project.live === false), true);
722
+ const canonicalProductRoot = fs.realpathSync(productRoot);
723
+ assert.equal(
724
+ resolvePayload.data.projects.some(
725
+ (project) => project.appRoot === canonicalProductRoot && project.manifest.routerPort === 3020,
726
+ ),
727
+ true,
728
+ );
729
+ assert.match(resolvePayload.data.projects[0].nextAction.command, /npx proteum dev/);
730
+
731
+ const workflow = await client.callTool({
732
+ name: 'workflow_start',
733
+ arguments: { cwd: path.join(productRoot, 'client', 'pages'), route: '/domains', task: 'read-only runtime health pass' },
734
+ });
735
+ const workflowPayload = JSON.parse(workflow.content[0].text);
736
+
737
+ assert.equal(workflowPayload.ok, true);
738
+ assert.equal(workflowPayload.data.project.live, false);
739
+ assert.equal(workflowPayload.data.owner.top.label, '/domains');
740
+ assert.equal(workflowPayload.nextActions[0].label, 'Start Dev');
741
+ assert.equal(workflowPayload.nextActions.some((action) => action.tool === 'diagnose'), false);
742
+
743
+ await client.close();
744
+ await server.close();
745
+ });
746
+
747
+ test('machine MCP offline resolution inspects occupied ports before suggesting dev start', async (t) => {
748
+ const previousRegistryDir = process.env.PROTEUM_MACHINE_DEV_SESSION_DIR;
749
+ const registryDir = fs.mkdtempSync(path.join(os.tmpdir(), 'proteum-machine-offline-port-registry-'));
750
+ process.env.PROTEUM_MACHINE_DEV_SESSION_DIR = registryDir;
751
+ t.onTestFinished(() => {
752
+ if (previousRegistryDir === undefined) delete process.env.PROTEUM_MACHINE_DEV_SESSION_DIR;
753
+ else process.env.PROTEUM_MACHINE_DEV_SESSION_DIR = previousRegistryDir;
754
+ });
755
+
756
+ const repoRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'proteum-machine-offline-port-'));
757
+ const otherRoot = path.join(repoRoot, 'apps', 'other');
758
+ const productRoot = path.join(repoRoot, 'apps', 'product');
759
+ const ownerServer = http.createServer((req, res) => {
760
+ if (req.url && req.url.startsWith('/__proteum/explain')) {
761
+ res.setHeader('content-type', 'application/json');
762
+ res.end(
763
+ JSON.stringify({
764
+ app: {
765
+ root: otherRoot,
766
+ identity: { identifier: 'OtherApp', name: 'Other' },
767
+ },
768
+ }),
769
+ );
770
+ return;
771
+ }
772
+
773
+ res.setHeader('content-type', 'text/html');
774
+ res.end('<html><body>wrong app page body that should not be routed into MCP</body></html>');
775
+ });
776
+ const occupiedPort = await listen(ownerServer);
777
+
778
+ try {
779
+ writeProteumAppFixture(productRoot, {
780
+ identifier: 'ProductApp',
781
+ name: 'Product',
782
+ routerPort: occupiedPort,
783
+ });
784
+
785
+ const server = createProteumMachineMcpServer({ version: 'test' });
786
+ const client = new Client({ name: 'machine-mcp-test', version: '1.0.0' });
787
+ const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
788
+
789
+ await server.connect(serverTransport);
790
+ await client.connect(clientTransport);
791
+
792
+ const resolve = await client.callTool({ name: 'project_resolve', arguments: { cwd: productRoot } });
793
+ const payload = JSON.parse(resolve.content[0].text);
794
+ const project = payload.data.projects[0];
795
+
796
+ assert.equal(project.devPort.router.port, occupiedPort);
797
+ assert.equal(project.devPort.router.proteum, true);
798
+ assert.equal(project.devPort.router.matchesApp, false);
799
+ assert.equal(project.devPort.router.app.identifier, 'OtherApp');
800
+ assert.equal(project.nextAction.label, 'Start Dev');
801
+ assert.match(project.nextAction.command, /npx proteum dev/);
802
+ assert.doesNotMatch(project.nextAction.command, new RegExp(`--port ${occupiedPort}(\\D|$)`));
803
+ assert.match(project.nextAction.reason, /alternate free pair/);
804
+ assert.doesNotMatch(resolve.content[0].text, /wrong app page body/);
805
+
806
+ await client.close();
807
+ await server.close();
808
+ } finally {
809
+ await closeServer(ownerServer);
810
+ }
811
+ });
812
+
813
+ test('machine MCP offline resolution does not start a second server for an untracked same-app runtime', async (t) => {
814
+ const previousRegistryDir = process.env.PROTEUM_MACHINE_DEV_SESSION_DIR;
815
+ const registryDir = fs.mkdtempSync(path.join(os.tmpdir(), 'proteum-machine-same-port-registry-'));
816
+ process.env.PROTEUM_MACHINE_DEV_SESSION_DIR = registryDir;
817
+ t.onTestFinished(() => {
818
+ if (previousRegistryDir === undefined) delete process.env.PROTEUM_MACHINE_DEV_SESSION_DIR;
819
+ else process.env.PROTEUM_MACHINE_DEV_SESSION_DIR = previousRegistryDir;
820
+ });
821
+
822
+ const repoRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'proteum-machine-same-port-'));
823
+ const productRoot = path.join(repoRoot, 'apps', 'product');
824
+ const ownerServer = http.createServer((req, res) => {
825
+ if (req.url && req.url.startsWith('/__proteum/explain')) {
826
+ res.setHeader('content-type', 'application/json');
827
+ res.end(
828
+ JSON.stringify({
829
+ app: {
830
+ root: productRoot,
831
+ identity: { identifier: 'ProductApp', name: 'Product' },
832
+ },
833
+ }),
834
+ );
835
+ return;
836
+ }
837
+
838
+ res.setHeader('content-type', 'text/html');
839
+ res.end('<html><body>same app page body that should not be routed into MCP</body></html>');
840
+ });
841
+ const occupiedPort = await listen(ownerServer);
842
+
843
+ try {
844
+ writeProteumAppFixture(productRoot, {
845
+ identifier: 'ProductApp',
846
+ name: 'Product',
847
+ routerPort: occupiedPort,
848
+ });
849
+
850
+ const server = createProteumMachineMcpServer({ version: 'test' });
851
+ const client = new Client({ name: 'machine-mcp-test', version: '1.0.0' });
852
+ const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
853
+
854
+ await server.connect(serverTransport);
855
+ await client.connect(clientTransport);
856
+
857
+ const resolve = await client.callTool({ name: 'project_resolve', arguments: { cwd: productRoot } });
858
+ const payload = JSON.parse(resolve.content[0].text);
859
+ const project = payload.data.projects[0];
860
+
861
+ assert.equal(project.devPort.router.matchesApp, true);
862
+ assert.equal(project.nextAction.label, 'Repair Runtime Tracking');
863
+ assert.match(project.nextAction.command, /npx proteum runtime status/);
864
+ assert.doesNotMatch(project.nextAction.command, /npx proteum dev/);
865
+ assert.match(project.nextAction.reason, /Do not start a second dev server/);
866
+ assert.doesNotMatch(resolve.content[0].text, /same app page body/);
867
+
868
+ await client.close();
869
+ await server.close();
870
+ } finally {
871
+ await closeServer(ownerServer);
872
+ }
873
+ });