memorix 1.8.1 → 1.8.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +36 -0
- package/dist/cli/index.js +577 -58
- package/dist/index.js +832 -321
- package/dist/maintenance-runner.js +31 -25
- package/dist/media-audio-runner.js +31 -25
- package/dist/media-video-runner.js +31 -25
- package/dist/memcode-runtime/CHANGELOG.md +36 -0
- package/dist/sdk.js +585 -315
- package/dist/vector-backfill-runner.js +31 -25
- package/docs/1.2.0-PROVIDER-QUALITY.md +15 -6
- package/docs/1.3.3-MULTIMODAL-MEDIA-AND-MCP-COMPAT.md +7 -6
- package/docs/1.8.0-RELEASE-CANDIDATE-SPEC.md +13 -8
- package/docs/API_REFERENCE.md +14 -11
- package/docs/CONFIGURATION.md +2 -2
- package/docs/DEVELOPMENT.md +3 -2
- package/docs/README.md +13 -7
- package/package.json +2 -1
- package/plugins/antigravity/memorix/plugin.json +2 -1
- package/plugins/claude/memorix/.claude-plugin/plugin.json +1 -1
- package/plugins/codebuddy/memorix/.codebuddy-plugin/plugin.json +1 -1
- package/plugins/codex/memorix/.codex-plugin/plugin.json +1 -1
- package/plugins/copilot/memorix/plugin.json +1 -1
- package/plugins/gemini/memorix/gemini-extension.json +1 -1
- package/plugins/hermes/memorix/plugin.yaml +1 -1
- package/plugins/omp/memorix/package.json +1 -1
- package/plugins/openclaw/memorix/.codex-plugin/plugin.json +1 -1
- package/plugins/pi/memorix/package.json +1 -1
- package/server.json +3 -3
- package/src/cli/commands/serve-http.ts +10 -2
- package/src/cli/commands/serve.ts +26 -7
- package/src/codegraph/external-provider.ts +299 -19
- package/src/config/dotenv-loader.ts +9 -4
- package/src/config.ts +16 -16
- package/src/dashboard/maintenance.ts +4 -1
- package/src/llm/provider.ts +17 -4
- package/src/server/mcp-compat.ts +63 -0
- package/src/server/mcp-diagnostics.ts +13 -4
- package/src/server/mcp-discovery.ts +61 -0
- package/src/server/stdio-startup-gate.ts +263 -0
- package/src/server.ts +4 -4
|
@@ -0,0 +1,61 @@
|
|
|
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_MODERN_PROTOCOL_VERSION,
|
|
41
|
+
MCP_LEGACY_PROTOCOL_VERSION,
|
|
42
|
+
MCP_OLDEST_LEGACY_PROTOCOL_VERSION,
|
|
43
|
+
],
|
|
44
|
+
capabilities,
|
|
45
|
+
_meta: {
|
|
46
|
+
[MCP_SERVER_INFO_META_KEY]: { ...serverInfo },
|
|
47
|
+
},
|
|
48
|
+
// v1 has no response-cache negotiation. A zero private TTL prevents clients
|
|
49
|
+
// from inventing a shared cache for an implementation that cannot invalidate it.
|
|
50
|
+
ttlMs: 0,
|
|
51
|
+
cacheScope: 'private',
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* The startup gate can answer discovery before McpServer exists. These are the
|
|
57
|
+
* only capabilities the production stdio profile advertises at that boundary.
|
|
58
|
+
*/
|
|
59
|
+
export function createMcpStartupDiscoverResult(serverInfo: McpServerInfo): McpDiscoverResult {
|
|
60
|
+
return createMcpDiscoverResult({ tools: { listChanged: true } }, serverInfo);
|
|
61
|
+
}
|
|
@@ -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,6 +62,7 @@ import {
|
|
|
62
62
|
type ProjectBindingController,
|
|
63
63
|
type ProjectBindingSource,
|
|
64
64
|
} from './server/request-context.js';
|
|
65
|
+
import { getMcpServerInfo, registerMcpDiscovery } from './server/mcp-compat.js';
|
|
65
66
|
|
|
66
67
|
// ── Timeout budgets for LLM-heavy paths ──────────────────────────
|
|
67
68
|
const FORMATION_TIMEOUT_MS = parseFormationTimeoutMs(process.env.MEMORIX_FORMATION_TIMEOUT_MS); // Formation pipeline (extract+resolve+evaluate)
|
|
@@ -593,10 +594,9 @@ export async function createMemorixServer(
|
|
|
593
594
|
};
|
|
594
595
|
|
|
595
596
|
// Create MCP server (or use existing one from roots-aware flow)
|
|
596
|
-
const
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
});
|
|
597
|
+
const serverInfo = getMcpServerInfo();
|
|
598
|
+
const server = existingServer ?? new McpServer(serverInfo);
|
|
599
|
+
registerMcpDiscovery(server, serverInfo);
|
|
600
600
|
|
|
601
601
|
const originalRegisterTool = server.registerTool.bind(server);
|
|
602
602
|
server.registerTool = ((name: string, ...args: unknown[]) => {
|