memorix 1.8.4 → 1.8.5

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 (73) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/CLAUDE.md +3 -0
  3. package/README.md +4 -4
  4. package/README.zh-CN.md +5 -3
  5. package/dist/cli/index.js +1542 -626
  6. package/dist/dashboard/static/app.js +29 -885
  7. package/dist/dashboard/static/index.html +1 -5
  8. package/dist/dashboard/static/style.css +36 -503
  9. package/dist/index.d.ts +4 -4
  10. package/dist/index.js +213 -70
  11. package/dist/memcode/index.js +28035 -169
  12. package/dist/memcode-runtime/CHANGELOG.md +34 -0
  13. package/dist/sdk.js +30 -12
  14. package/docs/1.8.0-RELEASE-CANDIDATE-SPEC.md +22 -16
  15. package/docs/AGENT_OPERATOR_PLAYBOOK.md +2 -2
  16. package/docs/API_REFERENCE.md +10 -8
  17. package/docs/DASHBOARD.md +23 -0
  18. package/docs/DEVELOPMENT.md +2 -2
  19. package/docs/DOCKER.md +6 -3
  20. package/docs/INTEGRATIONS.md +1 -1
  21. package/docs/MODULES.md +46 -20
  22. package/docs/PERFORMANCE.md +1 -1
  23. package/docs/README.md +8 -7
  24. package/docs/SETUP.md +3 -3
  25. package/package.json +11 -11
  26. package/plugins/antigravity/memorix/plugin.json +1 -1
  27. package/plugins/antigravity/memorix/rules/memorix.md +5 -0
  28. package/plugins/claude/memorix/.claude-plugin/plugin.json +1 -1
  29. package/plugins/claude/memorix/skills/memorix-memory/SKILL.md +3 -0
  30. package/plugins/claude/memorix/skills/memorix-mini-skills/SKILL.md +2 -0
  31. package/plugins/claude/memorix/skills/memorix-orchestrate/SKILL.md +2 -0
  32. package/plugins/codebuddy/memorix/.codebuddy-plugin/plugin.json +1 -1
  33. package/plugins/codebuddy/memorix/skills/memorix-memory/SKILL.md +3 -0
  34. package/plugins/codebuddy/memorix/skills/memorix-mini-skills/SKILL.md +2 -0
  35. package/plugins/codebuddy/memorix/skills/memorix-orchestrate/SKILL.md +2 -0
  36. package/plugins/codex/memorix/.codex-plugin/plugin.json +1 -1
  37. package/plugins/codex/memorix/skills/memorix-memory/SKILL.md +3 -0
  38. package/plugins/codex/memorix/skills/memorix-mini-skills/SKILL.md +2 -0
  39. package/plugins/codex/memorix/skills/memorix-orchestrate/SKILL.md +2 -0
  40. package/plugins/copilot/memorix/plugin.json +1 -1
  41. package/plugins/copilot/memorix/skills/memorix-memory/SKILL.md +3 -0
  42. package/plugins/copilot/memorix/skills/memorix-mini-skills/SKILL.md +2 -0
  43. package/plugins/copilot/memorix/skills/memorix-orchestrate/SKILL.md +2 -0
  44. package/plugins/cursor/memorix/skills/memorix-memory/SKILL.md +3 -0
  45. package/plugins/cursor/memorix/skills/memorix-mini-skills/SKILL.md +2 -0
  46. package/plugins/cursor/memorix/skills/memorix-orchestrate/SKILL.md +2 -0
  47. package/plugins/gemini/memorix/GEMINI.md +5 -0
  48. package/plugins/gemini/memorix/gemini-extension.json +1 -1
  49. package/plugins/hermes/memorix/plugin.yaml +7 -1
  50. package/plugins/omp/memorix/package.json +1 -1
  51. package/plugins/openclaw/memorix/.codex-plugin/plugin.json +1 -1
  52. package/plugins/pi/memorix/package.json +1 -1
  53. package/plugins/pi/memorix/skills/memorix-memory/SKILL.md +3 -0
  54. package/plugins/pi/memorix/skills/memorix-mini-skills/SKILL.md +2 -0
  55. package/plugins/pi/memorix/skills/memorix-orchestrate/SKILL.md +2 -0
  56. package/server.json +2 -2
  57. package/src/cli/command-guide.ts +52 -1
  58. package/src/cli/commands/background.ts +166 -6
  59. package/src/cli/commands/serve-http.ts +73 -80
  60. package/src/cli/commands/serve.ts +95 -93
  61. package/src/cli/index.ts +1 -1
  62. package/src/dashboard/static/index.html +1 -5
  63. package/src/dashboard/static/style.css +36 -503
  64. package/src/hooks/installers/index.ts +3 -0
  65. package/src/hooks/official-skills.ts +13 -6
  66. package/src/memory/graph.ts +2 -2
  67. package/src/server/mcp-diagnostics.ts +9 -7
  68. package/src/server/modern-mcp-bridge.ts +108 -0
  69. package/src/server/stdio-startup-gate.ts +41 -2
  70. package/src/server/tool-profile.ts +7 -6
  71. package/src/server.ts +1 -1
  72. package/src/store/orama-store.ts +2 -2
  73. package/src/wiki/types.ts +2 -2
@@ -42,6 +42,10 @@ interface BackgroundState {
42
42
  startCwd?: string;
43
43
  }
44
44
 
45
+ const BACKGROUND_LOCK_STALE_MS = 15_000;
46
+ const BACKGROUND_LOCK_ACQUIRE_TIMEOUT_MS = 10_000;
47
+ const BACKGROUND_LOCK_HEARTBEAT_MS = 2_000;
48
+
45
49
  export function resolveBackgroundCwd(cwd: string, homeDir: string = os.homedir()): string {
46
50
  if (!isHomeDirectory(cwd, homeDir)) return cwd;
47
51
  const inherited = inheritProjectRootFromHome({
@@ -71,6 +75,10 @@ function getLogFilePath(): string {
71
75
  return getMemorixDir() + '/background.log';
72
76
  }
73
77
 
78
+ function getBackgroundLockPath(): string {
79
+ return getMemorixDir() + '/background.lock';
80
+ }
81
+
74
82
  function parseBoundedPositiveInteger(value: string | undefined, fallback: number, label: string, maximum: number): number {
75
83
  const raw = value ?? String(fallback);
76
84
  if (!/^[0-9]+$/u.test(raw)) {
@@ -276,20 +284,21 @@ async function startWindowsBackgroundService(options: WindowsBackgroundLaunchOpt
276
284
  }
277
285
 
278
286
  async function healthCheck(port: number, timeoutMs = 3000): Promise<{ ok: boolean; data?: any; error?: string }> {
287
+ const controller = new AbortController();
288
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
279
289
  try {
280
- const controller = new AbortController();
281
- const timer = setTimeout(() => controller.abort(), timeoutMs);
282
290
  // Use /health endpoint — lightweight, no TeamStore or heavy init required.
283
291
  // Responds immediately once the HTTP server has bound the port.
284
292
  const res = await fetch(`http://127.0.0.1:${port}/health`, {
285
293
  signal: controller.signal,
286
294
  });
287
- clearTimeout(timer);
288
295
  if (!res.ok) return { ok: false, error: `HTTP ${res.status}` };
289
296
  const data = await res.json();
290
297
  return { ok: true, data };
291
298
  } catch (err) {
292
299
  return { ok: false, error: err instanceof Error ? err.message : 'Unknown error' };
300
+ } finally {
301
+ clearTimeout(timer);
293
302
  }
294
303
  }
295
304
 
@@ -299,10 +308,95 @@ async function isPortInUse(port: number): Promise<boolean> {
299
308
  }
300
309
 
301
310
  // ============================================================
311
+ // Background start lock
312
+ // ============================================================
313
+
314
+ interface BackgroundLock {
315
+ fd: number | null;
316
+ acquired: boolean;
317
+ path: string;
318
+ token: string;
319
+ heartbeat?: ReturnType<typeof setInterval>;
320
+ }
321
+
322
+ /**
323
+ * Hold an exclusive lock across the complete check/spawn/readiness sequence.
324
+ * The heartbeat prevents a normal slow startup from being mistaken for a
325
+ * crashed caller; an old lock without a live heartbeat can still be reclaimed.
326
+ */
327
+ async function acquireBackgroundLock(
328
+ lockPath: string,
329
+ staleMs: number = BACKGROUND_LOCK_STALE_MS,
330
+ timeoutMs: number = BACKGROUND_LOCK_ACQUIRE_TIMEOUT_MS,
331
+ ): Promise<BackgroundLock> {
332
+ const lockDir = path.dirname(lockPath);
333
+ if (!fs.existsSync(lockDir)) fs.mkdirSync(lockDir, { recursive: true });
334
+
335
+ const deadline = Date.now() + timeoutMs;
336
+ while (Date.now() < deadline) {
337
+ try {
338
+ const fd = fs.openSync(lockPath, 'wx');
339
+ const token = `${process.pid}:${randomBytes(12).toString('hex')}`;
340
+ fs.writeSync(fd, JSON.stringify({ pid: process.pid, acquiredAt: Date.now(), token }));
341
+ const heartbeat = setInterval(() => {
342
+ try {
343
+ const now = new Date();
344
+ // Update the inode held by this descriptor. If an operator or a
345
+ // recovering caller replaced the path, an old owner must not touch
346
+ // the replacement lock's mtime and keep it alive forever.
347
+ fs.futimesSync(fd, now, now);
348
+ } catch {
349
+ // The owner will fail naturally if its lock was removed by an operator.
350
+ }
351
+ }, BACKGROUND_LOCK_HEARTBEAT_MS);
352
+ heartbeat.unref();
353
+ return { fd, acquired: true, path: lockPath, token, heartbeat };
354
+ } catch (err) {
355
+ if (!(err && (err as NodeJS.ErrnoException).code === 'EEXIST')) throw err;
356
+ try {
357
+ if (Date.now() - fs.statSync(lockPath).mtimeMs > staleMs) {
358
+ let ownerAlive = false;
359
+ try {
360
+ const raw = fs.readFileSync(lockPath, 'utf8');
361
+ const owner = JSON.parse(raw) as { pid?: unknown };
362
+ ownerAlive = typeof owner.pid === 'number'
363
+ && owner.pid > 0
364
+ && isProcessRunning(owner.pid);
365
+ } catch {
366
+ // A malformed old lock has no trustworthy owner and can be reclaimed.
367
+ }
368
+ if (!ownerAlive) {
369
+ fs.unlinkSync(lockPath);
370
+ continue;
371
+ }
372
+ }
373
+ } catch {
374
+ // Another caller may have released or reclaimed it; retry the exclusive open.
375
+ }
376
+ await new Promise(resolve => setTimeout(resolve, 200));
377
+ }
378
+ }
379
+
380
+ throw new Error(
381
+ `Another Memorix background start is still in progress (${lockPath}). `
382
+ + 'Wait a few seconds and retry; no second control plane was started.',
383
+ );
384
+ }
385
+
386
+ function releaseBackgroundLock(lock: BackgroundLock): void {
387
+ if (!lock.acquired) return;
388
+ if (lock.heartbeat) clearInterval(lock.heartbeat);
389
+ try { if (lock.fd !== null) fs.closeSync(lock.fd); } catch { /* already closed */ }
390
+ try {
391
+ const current = JSON.parse(fs.readFileSync(lock.path, 'utf8')) as { token?: unknown };
392
+ if (current.token === lock.token) fs.unlinkSync(lock.path);
393
+ } catch { /* already released, reclaimed, or malformed */ }
394
+ }
395
+
302
396
  // Subcommands
303
397
  // ============================================================
304
398
 
305
- export async function doStart(port: number): Promise<void> {
399
+ async function doStartUnlocked(port: number): Promise<void> {
306
400
  // 1. Check if already running — validate both PID existence AND HTTP health
307
401
  const state = loadState();
308
402
  if (state) {
@@ -491,6 +585,15 @@ export async function doStart(port: number): Promise<void> {
491
585
  process.stderr.write(footer + '\n');
492
586
  }
493
587
 
588
+ export async function doStart(port: number): Promise<void> {
589
+ const lock = await acquireBackgroundLock(getBackgroundLockPath());
590
+ try {
591
+ await doStartUnlocked(port);
592
+ } finally {
593
+ releaseBackgroundLock(lock);
594
+ }
595
+ }
596
+
494
597
  export async function doStop(): Promise<void> {
495
598
  const state = loadState();
496
599
 
@@ -559,13 +662,24 @@ export async function doStop(): Promise<void> {
559
662
  console.log('[OK] Control plane stopped.');
560
663
  }
561
664
 
562
- async function doStatus(): Promise<void> {
665
+ async function doStatus(asJson = false): Promise<void> {
563
666
  const state = loadState();
564
667
 
565
668
  if (!state) {
566
669
  // No background.json — but check if port has an unmanaged foreground instance
567
670
  const portHealth = await healthCheck(3211, 2000);
568
671
  if (portHealth.ok) {
672
+ if (asJson) {
673
+ console.log(JSON.stringify({
674
+ status: 'unmanaged',
675
+ managed: false,
676
+ port: 3211,
677
+ dashboard: 'http://127.0.0.1:3211/',
678
+ mcp: 'http://127.0.0.1:3211/mcp',
679
+ health: portHealth.data ?? null,
680
+ }, null, 2));
681
+ return;
682
+ }
569
683
  console.log('');
570
684
  console.log('No background control plane is registered,');
571
685
  console.log('but a Memorix instance IS running on port 3211 (likely a foreground "memorix serve-http").');
@@ -585,6 +699,10 @@ async function doStatus(): Promise<void> {
585
699
  console.log(' 2. Run "memorix background start"');
586
700
  console.log('');
587
701
  } else {
702
+ if (asJson) {
703
+ console.log(JSON.stringify({ status: 'stopped', managed: false, port: 3211 }, null, 2));
704
+ return;
705
+ }
588
706
  console.log('No background control plane is registered.');
589
707
  console.log('');
590
708
  console.log('Start one with: memorix background start');
@@ -600,6 +718,36 @@ async function doStatus(): Promise<void> {
600
718
  // PID reuse detection: PID alive but /health fails or returns different PID
601
719
  const pidMismatch = health.ok && health.data?.pid && health.data.pid !== state.pid;
602
720
  const probablyReused = running && !health.ok;
721
+ const status = health.ok && !pidMismatch
722
+ ? 'running'
723
+ : pidMismatch
724
+ ? 'pid-mismatch'
725
+ : probablyReused
726
+ ? 'pid-reused'
727
+ : running
728
+ ? 'starting'
729
+ : 'stopped';
730
+
731
+ if (asJson) {
732
+ if (pidMismatch || probablyReused || !running) {
733
+ clearState();
734
+ try { fs.unlinkSync(getMemorixDir() + '/background.ready'); } catch { /* best effort */ }
735
+ }
736
+ console.log(JSON.stringify({
737
+ status,
738
+ managed: true,
739
+ pid: state.pid,
740
+ port: state.port,
741
+ startedAt: state.startedAt,
742
+ instance: state.instanceToken ? `${state.instanceToken.slice(0, 8)}…` : null,
743
+ dashboard: `http://127.0.0.1:${state.port}/`,
744
+ mcp: `http://127.0.0.1:${state.port}/mcp`,
745
+ logFile: normalizePath(state.logFile),
746
+ health: health.ok ? health.data ?? null : null,
747
+ error: health.ok ? null : health.error ?? (running ? 'Health check failed' : 'Process not running'),
748
+ }, null, 2));
749
+ return;
750
+ }
603
751
 
604
752
  console.log('');
605
753
  console.log('Memorix Background Control Plane');
@@ -629,6 +777,9 @@ async function doStatus(): Promise<void> {
629
777
  console.log(` PID: ${d.pid ?? 'unknown'}`);
630
778
  console.log(` Uptime: ${d.uptime ?? 0}s`);
631
779
  console.log(` Mode: ${d.mode ?? 'unknown'}`);
780
+ console.log(` Profile: ${d.profile ?? 'unknown'}`);
781
+ console.log(` Project: ${d.projectId ?? 'unresolved'}${d.projectName ? ` (${d.projectName})` : ''}`);
782
+ console.log(` Ready at: ${d.readyAt ?? 'unknown'}`);
632
783
  }
633
784
 
634
785
  if (pidMismatch) {
@@ -770,6 +921,11 @@ export default defineCommand({
770
921
  description: 'Number of log lines to show (default: 50)',
771
922
  required: false,
772
923
  },
924
+ json: {
925
+ type: 'boolean',
926
+ description: 'Emit a machine-readable status receipt',
927
+ required: false,
928
+ },
773
929
  },
774
930
  run: async ({ args }) => {
775
931
  try {
@@ -784,7 +940,7 @@ export default defineCommand({
784
940
  await doStop();
785
941
  break;
786
942
  case 'status':
787
- await doStatus();
943
+ await doStatus(!!args.json);
788
944
  break;
789
945
  case 'restart':
790
946
  await doRestart(port);
@@ -813,6 +969,7 @@ export default defineCommand({
813
969
  console.log(' --port <port> HTTP port (default: 3211)');
814
970
  console.log(' --follow, -f Follow log output in real-time (for "logs")');
815
971
  console.log(' --lines, -n Number of log lines to show (default: 50)');
972
+ console.log(' --json Emit a machine-readable status receipt (for "status")');
816
973
  console.log('');
817
974
  console.log('Mode distinction:');
818
975
  console.log(' Quick Mode = stdio / single project / zero friction');
@@ -838,4 +995,7 @@ export const _testing = {
838
995
  resolveBackgroundCliEntry,
839
996
  resolveBackgroundCliEntryFrom,
840
997
  resolveBackgroundCwd,
998
+ acquireBackgroundLock,
999
+ releaseBackgroundLock,
1000
+ getBackgroundLockPath,
841
1001
  };
@@ -27,6 +27,7 @@ import { scopeKnowledgeGraphToProject } from '../../memory/graph-scope.js';
27
27
  import { projectObservationRetention, summarizeRetentionProjections } from '../../memory/retention.js';
28
28
  import { canManageObservation, filterReadableObservations } from '../../memory/visibility.js';
29
29
  import { CURRENT_MCP_PROTOCOL_VERSION, MCP_SERVER_NAME, getMcpDiagnostics } from '../../server/mcp-diagnostics.js';
30
+ import { createMcpStartupDiscoverResult, getMcpServerInfo } from '../../server/mcp-discovery.js';
30
31
  import { parseTcpPortOrReport } from '../port.js';
31
32
  import { mcpFileUriToPath } from '../mcp-root-path.js';
32
33
 
@@ -130,6 +131,9 @@ export default defineCommand({
130
131
  // node:fs from being evaluated before their mock factories initialize.
131
132
  const { McpBindingStore } = await import('../../server/mcp-binding-store.js');
132
133
  const { MemoryFeedbackStore } = await import('../../memory/feedback.js');
134
+ const { createModernMcpBridge } = await import('../../server/modern-mcp-bridge.js');
135
+ const { createMcpHandler } = await import('@modelcontextprotocol/server');
136
+ const { toNodeHandler } = await import('@modelcontextprotocol/node');
133
137
 
134
138
  // The [server] config section feeds startup defaults; CLI flags win.
135
139
  let configuredDefaults: ServeHttpConfiguredDefaults = {};
@@ -197,6 +201,32 @@ export default defineCommand({
197
201
  const statelessBindingStore = new McpBindingStore();
198
202
  await statelessBindingStore.init(persistMod.getBaseDataDir());
199
203
 
204
+ // Modern MCP (2026-07-28) is stateless at the protocol layer. The official
205
+ // v2 handler owns discovery, per-request envelopes, result metadata,
206
+ // subscriptions, and version errors. Product tools are bridged to the
207
+ // existing v1 implementation so business semantics stay in one place.
208
+ const modernMcpHandler = createMcpHandler(async ({ requestInfo }) => {
209
+ const handleId = requestInfo?.headers.get('mcp-project-handle') ?? undefined;
210
+ const persisted = handleId ? statelessBindingStore.touch(handleId) : undefined;
211
+ const requestedRoot = requestInfo?.headers.get('mcp-project-root') ?? undefined;
212
+ const root = persisted?.projectRoot ?? requestedRoot ?? projectRoot;
213
+ return createModernMcpBridge({
214
+ projectRoot: root,
215
+ allowUntrackedFallback: false,
216
+ deferProjectInitUntilBound: true,
217
+ deferProjectRuntimeInit: true,
218
+ dashboardMode: 'control-plane',
219
+ dashboardPort: port,
220
+ toolProfile,
221
+ });
222
+ }, {
223
+ // Legacy requests are routed to the existing sessionful implementation
224
+ // below; this handler is strict modern-only by construction.
225
+ legacy: 'reject',
226
+ responseMode: 'auto',
227
+ });
228
+ const modernNodeHandler = toNodeHandler(modernMcpHandler);
229
+
200
230
  type SessionState = {
201
231
  transport: InstanceType<typeof StreamableHTTPServerTransport>;
202
232
  server: Awaited<ReturnType<typeof createMemorixServer>>['server'];
@@ -418,85 +448,6 @@ export default defineCommand({
418
448
  res.setHeader('Mcp-Name', MCP_SERVER_NAME);
419
449
  }
420
450
 
421
- /** Handle one 2026 stateless MCP request with a fresh server/transport. */
422
- async function handleStatelessPost(req: IncomingMessage, res: ServerResponse, body: unknown): Promise<void> {
423
- const handleId = typeof req.headers['mcp-project-handle'] === 'string'
424
- ? req.headers['mcp-project-handle']
425
- : undefined;
426
- const persisted = handleId ? statelessBindingStore.touch(handleId) : undefined;
427
- if (handleId && !persisted) {
428
- res.writeHead(404, { 'Content-Type': 'application/json' });
429
- res.end(JSON.stringify({ jsonrpc: '2.0', error: { code: -32001, message: 'MCP project handle not found or expired; initialize again.' }, id: null }));
430
- return;
431
- }
432
- const requestedRoot = typeof req.headers['mcp-project-root'] === 'string'
433
- ? req.headers['mcp-project-root']
434
- : undefined;
435
- const root = persisted?.projectRoot ?? requestedRoot ?? projectRoot;
436
-
437
- const { createMemorixServer } = await import('../../server.js');
438
- const { createProjectBindingController } = await import('../../server/request-context.js');
439
- const binding = createProjectBindingController(root);
440
- const { server, projectId, handleTransportClose } = await createMemorixServer(
441
- root,
442
- undefined,
443
- undefined,
444
- {
445
- allowUntrackedFallback: false,
446
- deferProjectInitUntilBound: true,
447
- deferProjectRuntimeInit: true,
448
- dashboardMode: 'control-plane',
449
- dashboardPort: port,
450
- toolProfile,
451
- projectBinding: binding,
452
- },
453
- );
454
- const transport = new StreamableHTTPServerTransport({
455
- // No Mcp-Session-Id: the protocol state is carried by our explicit
456
- // durable project handle and reconstructed on every request.
457
- sessionIdGenerator: undefined,
458
- enableJsonResponse: true,
459
- });
460
- try {
461
- await server.connect(transport);
462
- let effectiveHandle = persisted;
463
- if (!effectiveHandle && projectId !== '__unresolved__' && isInitializeRequest(body)) {
464
- effectiveHandle = statelessBindingStore.create({
465
- projectId,
466
- projectRoot: root,
467
- dataDir: await persistMod.getProjectDataDir(projectId),
468
- });
469
- }
470
- if (effectiveHandle) res.setHeader('Mcp-Project-Handle', effectiveHandle.handleId);
471
- // The 1.30 SDK predates the 2026-07-28 version string. Keep the
472
- // external stateless headers at the current contract, but normalize
473
- // only the SDK-facing request and initialize params to its newest
474
- // accepted wire version. This is a real compatibility adapter, not a
475
- // claim that the pinned SDK implements newer optional extensions.
476
- const sdkBody = body && typeof body === 'object'
477
- ? JSON.parse(JSON.stringify(body)) as Record<string, any>
478
- : body;
479
- const sdkRequest = sdkBody as Record<string, any> | undefined;
480
- if (sdkRequest && typeof sdkRequest === 'object' && sdkRequest.method === 'initialize' && sdkRequest.params) {
481
- sdkRequest.params.protocolVersion = MCP_SDK_COMPAT_PROTOCOL_VERSION;
482
- }
483
- if (req.headers['mcp-protocol-version']) {
484
- req.headers['mcp-protocol-version'] = MCP_SDK_COMPAT_PROTOCOL_VERSION;
485
- for (let i = 0; i < req.rawHeaders.length; i += 2) {
486
- if (req.rawHeaders[i].toLowerCase() === 'mcp-protocol-version') {
487
- req.rawHeaders[i + 1] = MCP_SDK_COMPAT_PROTOCOL_VERSION;
488
- break;
489
- }
490
- }
491
- }
492
- patchMcpResponse(res);
493
- await transport.handleRequest(req, res, sdkBody);
494
- } finally {
495
- handleTransportClose();
496
- await transport.close().catch(() => undefined);
497
- }
498
- }
499
-
500
451
  /**
501
452
  * Handle POST /mcp — JSON-RPC requests from agents
502
453
  */
@@ -504,13 +455,49 @@ export default defineCommand({
504
455
  const sessionId = req.headers['mcp-session-id'] as string | undefined;
505
456
  const body = await parseBody(req);
506
457
 
458
+ // Keep the pre-2026 startup probe compatible. Older MCP clients sent a
459
+ // claim-less server/discover before initialize and expect a plain JSON
460
+ // response; modern requests carry the _meta envelope and go to v2.
461
+ const params = body && typeof body === 'object' && !Array.isArray(body)
462
+ ? (body as { params?: unknown }).params
463
+ : undefined;
464
+ const claimlessDiscovery = !sessionId
465
+ && isServerDiscoverRequest(body)
466
+ && !req.headers['mcp-protocol-version']
467
+ && !req.headers['mcp-method']
468
+ && req.headers['mcp-stateless'] !== 'true'
469
+ && (params === undefined
470
+ || (typeof params === 'object' && params !== null && !Array.isArray(params)
471
+ && !Object.prototype.hasOwnProperty.call(params, '_meta')));
472
+ if (claimlessDiscovery) {
473
+ res.writeHead(200, { 'Content-Type': 'application/json' });
474
+ res.end(JSON.stringify({
475
+ jsonrpc: '2.0',
476
+ id: (body as { id?: string | number | null }).id ?? null,
477
+ result: createMcpStartupDiscoverResult(getMcpServerInfo()),
478
+ }));
479
+ return;
480
+ }
481
+
507
482
  const requestedProtocol = req.headers['mcp-protocol-version'];
508
483
  const statelessRequested = req.headers['mcp-stateless'] === 'true'
509
484
  || requestedProtocol === CURRENT_MCP_PROTOCOL_VERSION
510
485
  || typeof req.headers['mcp-project-handle'] === 'string'
511
486
  || isServerDiscoverRequest(body);
512
487
  if (!sessionId && statelessRequested) {
513
- await handleStatelessPost(req, res, body);
488
+ const handleId = typeof req.headers['mcp-project-handle'] === 'string'
489
+ ? req.headers['mcp-project-handle']
490
+ : undefined;
491
+ if (handleId && !statelessBindingStore.touch(handleId)) {
492
+ res.writeHead(404, { 'Content-Type': 'application/json' });
493
+ res.end(JSON.stringify({
494
+ jsonrpc: '2.0',
495
+ error: { code: -32001, message: 'MCP project handle not found or expired; initialize again.' },
496
+ id: null,
497
+ }));
498
+ return;
499
+ }
500
+ await modernNodeHandler(req, res, body);
514
501
  return;
515
502
  }
516
503
 
@@ -1684,6 +1671,7 @@ export default defineCommand({
1684
1671
  }
1685
1672
  }
1686
1673
 
1674
+ let httpReadyAt: string | null = null;
1687
1675
  const httpServer = createServer(async (req, res) => {
1688
1676
  setCorsHeaders(req, res);
1689
1677
 
@@ -1704,9 +1692,13 @@ export default defineCommand({
1704
1692
  res.end(JSON.stringify({
1705
1693
  status: 'ok',
1706
1694
  mode: 'control-plane',
1695
+ profile: toolProfile,
1707
1696
  port,
1708
1697
  uptime: Math.round(process.uptime()),
1709
1698
  pid: process.pid,
1699
+ projectId: defaultProject?.id ?? '__unresolved__',
1700
+ projectName: defaultProject?.name ?? null,
1701
+ readyAt: httpReadyAt,
1710
1702
  embedding: getEmbeddingRuntimeHealth(),
1711
1703
  }));
1712
1704
  return;
@@ -1791,6 +1783,7 @@ export default defineCommand({
1791
1783
  httpServer.headersTimeout = 65_000;
1792
1784
 
1793
1785
  httpServer.listen(port, host, () => {
1786
+ httpReadyAt = new Date().toISOString();
1794
1787
  // Write readiness file — background start polls this for out-of-band readiness detection
1795
1788
  try {
1796
1789
  const memorixDir = (process.env.HOME || process.env.USERPROFILE || '').replace(/\\/g, '/') + '/.memorix';