memorix 1.8.2 → 1.8.4

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.
@@ -1,5 +1,5 @@
1
1
  name: memorix
2
- version: "1.8.2"
2
+ version: "1.8.4"
3
3
  description: Shared workspace memory bridge for Hermes Agent.
4
4
  author: AVIDS2
5
5
  license: Apache-2.0
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "memorix-omp-package",
3
- "version": "1.8.2",
3
+ "version": "1.8.4",
4
4
  "description": "Memorix shared workspace memory package for Oh-my-Pi.",
5
5
  "license": "Apache-2.0",
6
6
  "keywords": [
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "memorix",
3
- "version": "1.8.2",
3
+ "version": "1.8.4",
4
4
  "description": "Shared workspace memory for OpenClaw and other AI coding agents.",
5
5
  "author": {
6
6
  "name": "AVIDS2",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "memorix-pi-package",
3
- "version": "1.8.2",
3
+ "version": "1.8.4",
4
4
  "description": "Memorix shared workspace memory package for Pi coding agent.",
5
5
  "license": "Apache-2.0",
6
6
  "keywords": [
package/server.json CHANGED
@@ -7,7 +7,7 @@
7
7
  "url": "https://github.com/AVIDS2/memorix",
8
8
  "source": "github"
9
9
  },
10
- "version": "1.8.2",
10
+ "version": "1.8.4",
11
11
  "websiteUrl": "https://github.com/AVIDS2/memorix#readme",
12
12
  "icons": [
13
13
  {
@@ -22,7 +22,7 @@
22
22
  {
23
23
  "registryType": "npm",
24
24
  "identifier": "memorix",
25
- "version": "1.8.2",
25
+ "version": "1.8.4",
26
26
  "runtimeHint": "npx",
27
27
  "packageArguments": [
28
28
  {
@@ -413,7 +413,7 @@ export default defineCommand({
413
413
  res.setHeader('Access-Control-Allow-Methods', 'GET, POST, DELETE, OPTIONS');
414
414
  res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Accept, Mcp-Session-Id, Mcp-Project-Handle, Mcp-Project-Root, Mcp-Stateless, Last-Event-Id');
415
415
  res.setHeader('Access-Control-Expose-Headers', 'Mcp-Session-Id, Mcp-Project-Handle, Mcp-Protocol-Version, Mcp-Method, Mcp-Name');
416
- res.setHeader('Mcp-Protocol-Version', req.headers['mcp-protocol-version'] || CURRENT_MCP_PROTOCOL_VERSION);
416
+ res.setHeader('Mcp-Protocol-Version', MCP_SDK_COMPAT_PROTOCOL_VERSION);
417
417
  res.setHeader('Mcp-Method', req.method || 'UNKNOWN');
418
418
  res.setHeader('Mcp-Name', MCP_SERVER_NAME);
419
419
  }
@@ -433,11 +433,6 @@ export default defineCommand({
433
433
  ? req.headers['mcp-project-root']
434
434
  : undefined;
435
435
  const root = persisted?.projectRoot ?? requestedRoot ?? projectRoot;
436
- if (!persisted && !isInitializeRequest(body) && !isServerDiscoverRequest(body)) {
437
- res.writeHead(400, { 'Content-Type': 'application/json' });
438
- res.end(JSON.stringify({ jsonrpc: '2.0', error: { code: -32000, message: 'Stateless MCP requests require Mcp-Project-Handle; initialize first.' }, id: null }));
439
- return;
440
- }
441
436
 
442
437
  const { createMemorixServer } = await import('../../server.js');
443
438
  const { createProjectBindingController } = await import('../../server/request-context.js');
@@ -465,7 +460,7 @@ export default defineCommand({
465
460
  try {
466
461
  await server.connect(transport);
467
462
  let effectiveHandle = persisted;
468
- if (!effectiveHandle && projectId !== '__unresolved__') {
463
+ if (!effectiveHandle && projectId !== '__unresolved__' && isInitializeRequest(body)) {
469
464
  effectiveHandle = statelessBindingStore.create({
470
465
  projectId,
471
466
  projectRoot: root,
@@ -4,6 +4,8 @@
4
4
 
5
5
  import { defineCommand } from 'citty';
6
6
  import { resolveToolProfile } from '../../server/tool-profile.js';
7
+ import { getMcpServerInfo } from '../../server/mcp-discovery.js';
8
+ import { StdioStartupGate } from '../../server/stdio-startup-gate.js';
7
9
  import { mcpFileUriToPath } from '../mcp-root-path.js';
8
10
 
9
11
  export default defineCommand({
@@ -29,6 +31,28 @@ export default defineCommand({
29
31
  },
30
32
  },
31
33
  run: async ({ args }) => {
34
+ // Take ownership of stdin before project/config/runtime initialization. The
35
+ // gate answers handshake-free discovery and queues every other JSON-RPC line
36
+ // until the SDK transport is connected, so cold starts cannot lose requests.
37
+ const startupGate = new StdioStartupGate({
38
+ stdin: process.stdin,
39
+ stdout: process.stdout,
40
+ serverInfo: getMcpServerInfo(),
41
+ onError: (error) => {
42
+ console.error(`[memorix] stdio startup gate error: ${error.message}`);
43
+ // The stream is no longer safe to replay after an input-limit or I/O
44
+ // failure. Exit instead of leaving a half-connected MCP process alive.
45
+ process.exit(1);
46
+ },
47
+ onEnd: () => {
48
+ console.error('[memorix] stdin closed — exiting');
49
+ // The gate has already ended the transport input when ready. Set the
50
+ // exit code and let pending JSON-RPC stdout writes drain naturally.
51
+ process.exitCode = 0;
52
+ },
53
+ });
54
+ startupGate.start();
55
+
32
56
  const { StdioServerTransport } = await import(
33
57
  '@modelcontextprotocol/sdk/server/stdio.js'
34
58
  );
@@ -37,12 +61,6 @@ export default defineCommand({
37
61
  const { homedir } = await import('node:os');
38
62
  const { resolveServeProject } = await import('./serve-shared.js');
39
63
 
40
- // Auto-exit when stdio pipe breaks (IDE closed) to prevent orphaned processes
41
- process.stdin.on('end', () => {
42
- console.error('[memorix] stdin closed — exiting');
43
- process.exit(0);
44
- });
45
-
46
64
  // Priority: explicit --cwd arg > MEMORIX_PROJECT_ROOT env > INIT_CWD (npm lifecycle) > process.cwd()
47
65
  let safeCwd: string;
48
66
  try { safeCwd = process.cwd(); } catch { safeCwd = homedir(); }
@@ -85,8 +103,9 @@ export default defineCommand({
85
103
  ? { toolProfile, deferProjectRuntimeInit: true }
86
104
  : { allowUntrackedFallback: allowUntracked, deferProjectInitUntilBound: !allowUntracked, deferProjectRuntimeInit: true, toolProfile };
87
105
  const { server, projectId, deferredInit, switchProject } = await createMemorixServer(projectRoot, undefined, undefined, serverOptions);
88
- const transport = new StdioServerTransport();
106
+ const transport = new StdioServerTransport(startupGate.input, process.stdout);
89
107
  await server.connect(transport);
108
+ startupGate.markReady();
90
109
 
91
110
  console.error(`[memorix] MCP Server running on stdio (project: ${projectId})`);
92
111
  console.error(`[memorix] Project root: ${detected?.rootPath ?? projectRoot}`);
@@ -11,33 +11,29 @@ import { RequestSchema, type ServerCapabilities } from '@modelcontextprotocol/sd
11
11
  import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
12
12
  import { z } from 'zod/v4';
13
13
 
14
- export const MCP_MODERN_PROTOCOL_VERSION = '2026-07-28';
15
- export const MCP_LEGACY_PROTOCOL_VERSION = '2025-11-25';
16
- export const MCP_OLDEST_LEGACY_PROTOCOL_VERSION = '2024-11-05';
17
- export const MCP_SERVER_DISCOVER_METHOD = 'server/discover';
18
- export const MCP_SERVER_INFO_META_KEY = 'io.modelcontextprotocol/serverInfo';
14
+ import {
15
+ createMcpDiscoverResult,
16
+ MCP_SERVER_DISCOVER_METHOD,
17
+ type McpDiscoverResult,
18
+ type McpServerInfo,
19
+ } from './mcp-discovery.js';
20
+
21
+ export {
22
+ createMcpDiscoverResult,
23
+ getMcpServerInfo,
24
+ MCP_LEGACY_PROTOCOL_VERSION,
25
+ MCP_MODERN_PROTOCOL_VERSION,
26
+ MCP_OLDEST_LEGACY_PROTOCOL_VERSION,
27
+ MCP_SERVER_DISCOVER_METHOD,
28
+ MCP_SERVER_INFO_META_KEY,
29
+ } from './mcp-discovery.js';
30
+ export type { McpDiscoverResult, McpServerInfo } from './mcp-discovery.js';
19
31
 
20
32
  const ServerDiscoverRequestSchema = RequestSchema.extend({
21
33
  method: z.literal(MCP_SERVER_DISCOVER_METHOD),
22
34
  params: z.looseObject({}).optional(),
23
35
  });
24
36
 
25
- export interface McpServerInfo {
26
- name: string;
27
- version: string;
28
- }
29
-
30
- export interface McpDiscoverResult {
31
- resultType: 'complete';
32
- supportedVersions: string[];
33
- capabilities: ServerCapabilities;
34
- _meta: {
35
- [MCP_SERVER_INFO_META_KEY]: McpServerInfo;
36
- };
37
- ttlMs: number;
38
- cacheScope: 'public' | 'private';
39
- }
40
-
41
37
  const registeredServers = new WeakSet<McpServer>();
42
38
 
43
39
  /** Build the wire result without exposing unsupported cache/task features. */
@@ -48,22 +44,7 @@ export function buildMcpDiscoverResult(server: McpServer, serverInfo: McpServerI
48
44
  const getCapabilities = (server.server as unknown as {
49
45
  getCapabilities: () => ServerCapabilities;
50
46
  }).getCapabilities.bind(server.server);
51
- return {
52
- resultType: 'complete',
53
- supportedVersions: [
54
- MCP_MODERN_PROTOCOL_VERSION,
55
- MCP_LEGACY_PROTOCOL_VERSION,
56
- MCP_OLDEST_LEGACY_PROTOCOL_VERSION,
57
- ],
58
- capabilities: getCapabilities(),
59
- _meta: {
60
- [MCP_SERVER_INFO_META_KEY]: { ...serverInfo },
61
- },
62
- // v1 has no response-cache negotiation. A zero private TTL is the
63
- // conservative result and prevents clients from inventing a shared cache.
64
- ttlMs: 0,
65
- cacheScope: 'private',
66
- };
47
+ return createMcpDiscoverResult(getCapabilities(), serverInfo);
67
48
  }
68
49
 
69
50
  /**
@@ -0,0 +1,60 @@
1
+ import type { ServerCapabilities } from '@modelcontextprotocol/sdk/types.js';
2
+
3
+ export const MCP_MODERN_PROTOCOL_VERSION = '2026-07-28';
4
+ export const MCP_LEGACY_PROTOCOL_VERSION = '2025-11-25';
5
+ export const MCP_OLDEST_LEGACY_PROTOCOL_VERSION = '2024-11-05';
6
+ export const MCP_SERVER_DISCOVER_METHOD = 'server/discover';
7
+ export const MCP_SERVER_INFO_META_KEY = 'io.modelcontextprotocol/serverInfo';
8
+
9
+ export interface McpServerInfo {
10
+ name: string;
11
+ version: string;
12
+ }
13
+
14
+ export interface McpDiscoverResult {
15
+ resultType: 'complete';
16
+ supportedVersions: string[];
17
+ capabilities: ServerCapabilities;
18
+ _meta: {
19
+ [MCP_SERVER_INFO_META_KEY]: McpServerInfo;
20
+ };
21
+ ttlMs: number;
22
+ cacheScope: 'public' | 'private';
23
+ }
24
+
25
+ export function getMcpServerInfo(): McpServerInfo {
26
+ return {
27
+ name: 'memorix',
28
+ version: typeof __MEMORIX_VERSION__ !== 'undefined' ? __MEMORIX_VERSION__ : '1.0.1',
29
+ };
30
+ }
31
+
32
+ /** Build the deliberately partial modern-discovery result shared by all stdio paths. */
33
+ export function createMcpDiscoverResult(
34
+ capabilities: ServerCapabilities,
35
+ serverInfo: McpServerInfo,
36
+ ): McpDiscoverResult {
37
+ return {
38
+ resultType: 'complete',
39
+ supportedVersions: [
40
+ MCP_LEGACY_PROTOCOL_VERSION,
41
+ MCP_OLDEST_LEGACY_PROTOCOL_VERSION,
42
+ ],
43
+ capabilities,
44
+ _meta: {
45
+ [MCP_SERVER_INFO_META_KEY]: { ...serverInfo },
46
+ },
47
+ // v1 has no response-cache negotiation. A zero private TTL prevents clients
48
+ // from inventing a shared cache for an implementation that cannot invalidate it.
49
+ ttlMs: 0,
50
+ cacheScope: 'private',
51
+ };
52
+ }
53
+
54
+ /**
55
+ * The startup gate can answer discovery before McpServer exists. These are the
56
+ * only capabilities the production stdio profile advertises at that boundary.
57
+ */
58
+ export function createMcpStartupDiscoverResult(serverInfo: McpServerInfo): McpDiscoverResult {
59
+ return createMcpDiscoverResult({ tools: { listChanged: true } }, serverInfo);
60
+ }
@@ -0,0 +1,263 @@
1
+ import { PassThrough, type Readable, type Writable } from 'node:stream';
2
+
3
+ import {
4
+ createMcpStartupDiscoverResult,
5
+ MCP_SERVER_DISCOVER_METHOD,
6
+ type McpServerInfo,
7
+ } from './mcp-discovery.js';
8
+
9
+ /** Keep the gate aligned with @modelcontextprotocol/sdk's stdio ReadBuffer limit. */
10
+ export const STDIO_STARTUP_GATE_MAX_BUFFER_SIZE = 10 * 1024 * 1024;
11
+
12
+ export interface StdioStartupGateOptions {
13
+ stdin: Readable;
14
+ stdout: Writable;
15
+ serverInfo: McpServerInfo;
16
+ maxBufferSize?: number;
17
+ onError?: (error: Error) => void;
18
+ onEnd?: () => void;
19
+ }
20
+
21
+ type JsonRpcEnvelope = Record<string, unknown>;
22
+ type ParsedLine = { message: JsonRpcEnvelope } | { error: { code: number; message: string } } | null;
23
+
24
+ /**
25
+ * Reads stdio before the server's expensive project setup finishes.
26
+ *
27
+ * Only the handshake-free discovery request is answered at the front door. All
28
+ * other complete JSON-RPC lines, including notifications and responses, stay in
29
+ * an ordered queue and are replayed through the SDK transport once it is ready.
30
+ */
31
+ export class StdioStartupGate {
32
+ readonly input = new PassThrough();
33
+
34
+ private readonly stdin: Readable;
35
+ private readonly stdout: Writable;
36
+ private readonly serverInfo: McpServerInfo;
37
+ private readonly maxBufferSize: number;
38
+ private readonly onError?: (error: Error) => void;
39
+ private readonly onEnd?: () => void;
40
+ private readonly queuedMessages: string[] = [];
41
+ private pendingInput = '';
42
+ private queuedBytes = 0;
43
+ private started = false;
44
+ private ready = false;
45
+ private stopped = false;
46
+ private stdinEnded = false;
47
+ private endNotified = false;
48
+
49
+ private readonly onData = (chunk: Buffer | string): void => {
50
+ if (this.stopped) return;
51
+
52
+ if (this.ready) {
53
+ if (Buffer.byteLength(chunk, 'utf8') > this.maxBufferSize) {
54
+ this.fail(new Error(`stdio input chunk exceeded ${this.maxBufferSize} bytes`));
55
+ return;
56
+ }
57
+ this.input.write(chunk);
58
+ return;
59
+ }
60
+
61
+ const text = typeof chunk === 'string' ? chunk : chunk.toString('utf8');
62
+ if (
63
+ this.queuedBytes
64
+ + Buffer.byteLength(this.pendingInput, 'utf8')
65
+ + Buffer.byteLength(text, 'utf8')
66
+ > this.maxBufferSize
67
+ ) {
68
+ this.fail(new Error(`stdio startup input exceeded ${this.maxBufferSize} bytes before a complete JSON-RPC line`));
69
+ return;
70
+ }
71
+
72
+ this.pendingInput += text;
73
+ let newline = this.pendingInput.indexOf('\n');
74
+ while (newline >= 0 && !this.stopped) {
75
+ const line = this.pendingInput.slice(0, newline + 1);
76
+ this.pendingInput = this.pendingInput.slice(newline + 1);
77
+ this.handleLine(line);
78
+ newline = this.pendingInput.indexOf('\n');
79
+ }
80
+
81
+ if (!this.stopped && Buffer.byteLength(this.pendingInput, 'utf8') > this.maxBufferSize) {
82
+ this.fail(new Error(`stdio startup input exceeded ${this.maxBufferSize} bytes`));
83
+ }
84
+ };
85
+
86
+ private readonly onStdinError = (error: Error): void => {
87
+ this.fail(error);
88
+ };
89
+
90
+ private readonly onStdinEnd = (): void => {
91
+ this.stdinEnded = true;
92
+ if (this.ready) this.finishInput();
93
+ };
94
+
95
+ constructor(options: StdioStartupGateOptions) {
96
+ this.stdin = options.stdin;
97
+ this.stdout = options.stdout;
98
+ this.serverInfo = { ...options.serverInfo };
99
+ this.maxBufferSize = options.maxBufferSize ?? STDIO_STARTUP_GATE_MAX_BUFFER_SIZE;
100
+ this.onError = options.onError;
101
+ this.onEnd = options.onEnd;
102
+ }
103
+
104
+ start(): void {
105
+ if (this.started || this.stopped) return;
106
+ this.started = true;
107
+ this.stdin.on('data', this.onData);
108
+ this.stdin.on('error', this.onStdinError);
109
+ this.stdin.on('end', this.onStdinEnd);
110
+ }
111
+
112
+ /** Release queued messages only after StdioServerTransport has installed its listener. */
113
+ markReady(): void {
114
+ if (this.stopped || this.ready) return;
115
+ this.ready = true;
116
+ for (const message of this.queuedMessages) this.input.write(message);
117
+ this.queuedMessages.length = 0;
118
+ this.queuedBytes = 0;
119
+ if (this.pendingInput) {
120
+ this.input.write(this.pendingInput);
121
+ this.pendingInput = '';
122
+ }
123
+ if (this.stdinEnded) this.finishInput();
124
+ }
125
+
126
+ stop(): void {
127
+ if (this.stopped) return;
128
+ this.stopped = true;
129
+ this.stdin.off('data', this.onData);
130
+ this.stdin.off('error', this.onStdinError);
131
+ this.stdin.off('end', this.onStdinEnd);
132
+ this.queuedMessages.length = 0;
133
+ this.pendingInput = '';
134
+ this.queuedBytes = 0;
135
+ if (!this.input.destroyed) this.input.end();
136
+ }
137
+
138
+ private handleLine(line: string): void {
139
+ const parsed = this.parseLine(line);
140
+ if (!parsed) return;
141
+ if ('error' in parsed) {
142
+ this.writeJsonRpcError(null, parsed.error.code, parsed.error.message);
143
+ return;
144
+ }
145
+
146
+ if (!this.ready && this.isDiscoveryRequest(parsed.message)) {
147
+ const response = {
148
+ jsonrpc: '2.0',
149
+ id: parsed.message.id,
150
+ result: createMcpStartupDiscoverResult(this.serverInfo),
151
+ };
152
+ try {
153
+ this.stdout.write(`${JSON.stringify(response)}\n`);
154
+ } catch (error) {
155
+ this.fail(error instanceof Error ? error : new Error(String(error)));
156
+ }
157
+ return;
158
+ }
159
+
160
+ if (this.ready) {
161
+ this.input.write(line);
162
+ return;
163
+ }
164
+
165
+ const bytes = Buffer.byteLength(line, 'utf8');
166
+ if (this.queuedBytes + bytes > this.maxBufferSize) {
167
+ this.fail(new Error(`stdio startup queue exceeded ${this.maxBufferSize} bytes`));
168
+ return;
169
+ }
170
+ this.queuedMessages.push(line);
171
+ this.queuedBytes += bytes;
172
+ }
173
+
174
+ private parseLine(line: string): ParsedLine {
175
+ const trimmed = line.endsWith('\n') ? line.slice(0, -1).replace(/\r$/, '') : line;
176
+ if (!trimmed.trim()) return null;
177
+
178
+ let value: unknown;
179
+ try {
180
+ value = JSON.parse(trimmed);
181
+ } catch {
182
+ return { error: { code: -32700, message: 'Parse error' } };
183
+ }
184
+
185
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
186
+ return { error: { code: -32600, message: 'Invalid Request' } };
187
+ }
188
+ const message = value as JsonRpcEnvelope;
189
+ if (message.jsonrpc !== '2.0') {
190
+ return { error: { code: -32600, message: 'Invalid Request' } };
191
+ }
192
+
193
+ const hasId = Object.prototype.hasOwnProperty.call(message, 'id');
194
+ const hasMethod = typeof message.method === 'string';
195
+ const hasResult = Object.prototype.hasOwnProperty.call(message, 'result');
196
+ const hasError = Object.prototype.hasOwnProperty.call(message, 'error');
197
+ if (hasMethod && (hasResult || hasError)) {
198
+ return { error: { code: -32600, message: 'Invalid Request' } };
199
+ }
200
+ if (!hasMethod && !hasResult && !hasError) {
201
+ return { error: { code: -32600, message: 'Invalid Request' } };
202
+ }
203
+ if (hasId && !this.isValidRequestId(message.id)) {
204
+ return { error: { code: -32600, message: 'Invalid Request' } };
205
+ }
206
+ if (!hasMethod && !hasId) {
207
+ return { error: { code: -32600, message: 'Invalid Request' } };
208
+ }
209
+ return { message };
210
+ }
211
+
212
+ private isDiscoveryRequest(message: JsonRpcEnvelope): boolean {
213
+ if (message.method !== MCP_SERVER_DISCOVER_METHOD) return false;
214
+ if (!Object.prototype.hasOwnProperty.call(message, 'id')) return false;
215
+ const params = message.params;
216
+ return params === undefined || (typeof params === 'object' && params !== null && !Array.isArray(params));
217
+ }
218
+
219
+ private isValidRequestId(id: unknown): id is string | number | null {
220
+ return id === null
221
+ || typeof id === 'string'
222
+ || (typeof id === 'number' && Number.isFinite(id));
223
+ }
224
+
225
+ private writeJsonRpcError(id: string | number | null, code: number, message: string): void {
226
+ try {
227
+ this.stdout.write(`${JSON.stringify({
228
+ jsonrpc: '2.0',
229
+ id,
230
+ error: { code, message },
231
+ })}\n`);
232
+ } catch (error) {
233
+ this.fail(error instanceof Error ? error : new Error(String(error)));
234
+ }
235
+ }
236
+
237
+ private finishInput(): void {
238
+ if (!this.ready || this.input.destroyed || this.input.writableEnded) return;
239
+ this.input.end();
240
+ this.input.once('end', () => this.notifyEnd());
241
+ }
242
+
243
+ private notifyEnd(): void {
244
+ if (this.endNotified) return;
245
+ this.endNotified = true;
246
+ this.onEnd?.();
247
+ }
248
+
249
+ private fail(error: Error): void {
250
+ if (this.stopped) return;
251
+ this.stopped = true;
252
+ this.stdin.off('data', this.onData);
253
+ this.stdin.off('error', this.onStdinError);
254
+ this.stdin.off('end', this.onStdinEnd);
255
+ this.queuedMessages.length = 0;
256
+ this.pendingInput = '';
257
+ this.queuedBytes = 0;
258
+ // Destroy without an error argument so a not-yet-connected SDK transport
259
+ // cannot turn the gate failure into an unhandled EventEmitter error.
260
+ if (!this.input.destroyed) this.input.destroy();
261
+ this.onError?.(error);
262
+ }
263
+ }
package/src/server.ts CHANGED
@@ -62,7 +62,7 @@ import {
62
62
  type ProjectBindingController,
63
63
  type ProjectBindingSource,
64
64
  } from './server/request-context.js';
65
- import { registerMcpDiscovery } from './server/mcp-compat.js';
65
+ import { getMcpServerInfo, registerMcpDiscovery } from './server/mcp-compat.js';
66
66
 
67
67
  // ── Timeout budgets for LLM-heavy paths ──────────────────────────
68
68
  const FORMATION_TIMEOUT_MS = parseFormationTimeoutMs(process.env.MEMORIX_FORMATION_TIMEOUT_MS); // Formation pipeline (extract+resolve+evaluate)
@@ -594,10 +594,7 @@ export async function createMemorixServer(
594
594
  };
595
595
 
596
596
  // Create MCP server (or use existing one from roots-aware flow)
597
- const serverInfo = {
598
- name: 'memorix',
599
- version: typeof __MEMORIX_VERSION__ !== 'undefined' ? __MEMORIX_VERSION__ : '1.0.1',
600
- };
597
+ const serverInfo = getMcpServerInfo();
601
598
  const server = existingServer ?? new McpServer(serverInfo);
602
599
  registerMcpDiscovery(server, serverInfo);
603
600