proteum 2.3.0 → 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 (49) hide show
  1. package/AGENTS.md +8 -3
  2. package/README.md +20 -15
  3. package/agents/project/AGENTS.md +15 -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 +9 -8
  7. package/agents/project/root/AGENTS.md +14 -8
  8. package/agents/project/tests/AGENTS.md +1 -0
  9. package/cli/commands/dev.ts +148 -25
  10. package/cli/commands/diagnose.ts +2 -0
  11. package/cli/commands/explain.ts +38 -9
  12. package/cli/commands/mcp.ts +126 -9
  13. package/cli/commands/orient.ts +44 -17
  14. package/cli/commands/runtime.ts +100 -17
  15. package/cli/mcp/router.ts +1010 -0
  16. package/cli/presentation/commands.ts +34 -24
  17. package/cli/presentation/help.ts +1 -1
  18. package/cli/runtime/commands.ts +129 -21
  19. package/cli/runtime/devSessions.ts +328 -2
  20. package/cli/runtime/mcpDaemon.ts +288 -0
  21. package/cli/runtime/ports.ts +151 -0
  22. package/cli/utils/agents.ts +93 -17
  23. package/cli/utils/appRoots.ts +232 -0
  24. package/common/dev/diagnostics.ts +1 -1
  25. package/common/dev/inspection.ts +8 -1
  26. package/common/dev/mcpPayloads.ts +431 -17
  27. package/common/dev/mcpServer.ts +33 -0
  28. package/docs/agent-routing.md +32 -21
  29. package/docs/dev-commands.md +1 -1
  30. package/docs/dev-sessions.md +3 -1
  31. package/docs/diagnostics.md +21 -20
  32. package/docs/mcp.md +109 -52
  33. package/docs/migrate-from-2.1.3.md +3 -5
  34. package/docs/request-tracing.md +3 -3
  35. package/package.json +10 -3
  36. package/server/app/devMcp.ts +45 -0
  37. package/server/services/router/request/ip.test.cjs +0 -1
  38. package/tests/agents-utils.test.cjs +58 -3
  39. package/tests/cli-mcp-command.test.cjs +262 -0
  40. package/tests/codex-mcp-usage.test.cjs +307 -0
  41. package/tests/dev-sessions.test.cjs +113 -0
  42. package/tests/dev-transpile-watch.test.cjs +0 -1
  43. package/tests/eslint-rules.test.cjs +0 -1
  44. package/tests/inspection.test.cjs +0 -1
  45. package/tests/mcp.test.cjs +748 -2
  46. package/tests/router-cache-config.test.cjs +0 -1
  47. package/vitest.config.mjs +9 -0
  48. package/cli/mcp/provider.ts +0 -365
  49. package/cli/mcp/stdio.ts +0 -16
@@ -1,8 +1,8 @@
1
1
  const assert = require('node:assert/strict');
2
2
  const fs = require('node:fs');
3
+ const http = require('node:http');
3
4
  const os = require('node:os');
4
5
  const path = require('node:path');
5
- const test = require('node:test');
6
6
 
7
7
  const coreRoot = path.resolve(__dirname, '..');
8
8
  process.env.TS_NODE_PROJECT = path.join(coreRoot, 'cli', 'tsconfig.json');
@@ -12,20 +12,106 @@ require('../cli/context.ts');
12
12
 
13
13
  const { Client } = require('@modelcontextprotocol/sdk/client/index.js');
14
14
  const { InMemoryTransport } = require('@modelcontextprotocol/sdk/inMemory.js');
15
- const { createMcpPayload, compactTraceResponse, resolveInstructionRouting } = require('../common/dev/mcpPayloads.ts');
15
+ const {
16
+ createMcpPayload,
17
+ compactOrientationResponse,
18
+ compactRouteCandidatesResponse,
19
+ compactTraceResponse,
20
+ compactWorkflowStartResponse,
21
+ resolveInstructionRouting,
22
+ } = require('../common/dev/mcpPayloads.ts');
16
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');
17
39
 
18
40
  const writeFile = (filepath, content) => {
19
41
  fs.mkdirSync(path.dirname(filepath), { recursive: true });
20
42
  fs.writeFileSync(filepath, content);
21
43
  };
22
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
+
23
108
  test('instruction routing returns compact selected files for a page query', () => {
24
109
  const appRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'proteum-mcp-app-'));
25
110
 
26
111
  writeFile(path.join(appRoot, 'AGENTS.md'), '# App Agents\n\n- root\n');
27
112
  writeFile(path.join(appRoot, 'client', 'AGENTS.md'), '# Client Agents\n\n- client\n');
28
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');
29
115
  writeFile(path.join(appRoot, 'diagnostics.md'), '# Diagnostics\n\n- diagnose\n');
30
116
 
31
117
  const payload = resolveInstructionRouting({ appRoot, query: '/domains/:slug client/pages/domain.tsx' });
@@ -36,9 +122,196 @@ test('instruction routing returns compact selected files for a page query', () =
36
122
  payload.data.selected.map((entry) => path.relative(appRoot, entry.file)).sort(),
37
123
  ['AGENTS.md', 'client/AGENTS.md', 'client/pages/AGENTS.md'],
38
124
  );
125
+ assert.equal(payload.data.readWhen.some((entry) => entry.file && entry.file.endsWith('DOCUMENTATION.md')), true);
39
126
  assert.equal(payload.data.readWhen.some((entry) => entry.file && entry.file.endsWith('diagnostics.md')), true);
40
127
  });
41
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
+
42
315
  test('trace payload keeps default output compact and paginates full details', () => {
43
316
  const request = {
44
317
  id: 'req_1',
@@ -103,9 +376,11 @@ test('MCP server registers the Proteum read-only tool contract', async () => {
103
376
  perfRequest: async () => payload,
104
377
  perfTop: async () => payload,
105
378
  readResource: async () => payload,
379
+ routeCandidates: async () => payload,
106
380
  runtimeStatus: async () => payload,
107
381
  traceLatest: async () => payload,
108
382
  traceShow: async () => payload,
383
+ workflowStart: async () => payload,
109
384
  };
110
385
  const server = createProteumMcpServer({ provider, version: 'test' });
111
386
  const client = new Client({ name: 'mcp-test', version: '1.0.0' });
@@ -119,9 +394,480 @@ test('MCP server registers the Proteum read-only tool contract', async () => {
119
394
  const resource = await client.readResource({ uri: 'proteum://runtime/status' });
120
395
 
121
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);
122
399
  assert.match(result.content[0].text, /proteum-mcp-v1/);
123
400
  assert.match(resource.contents[0].text, /proteum-mcp-v1/);
124
401
 
125
402
  await client.close();
126
403
  await server.close();
127
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
+ });