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.
- package/CHANGELOG.md +35 -0
- package/dist/cli/index.js +277 -39
- package/dist/index.js +268 -25
- package/dist/memcode-runtime/CHANGELOG.md +35 -0
- package/dist/sdk.js +25 -16
- package/docs/1.8.0-RELEASE-CANDIDATE-SPEC.md +5 -2
- package/docs/API_REFERENCE.md +9 -8
- package/docs/DEVELOPMENT.md +3 -2
- package/docs/README.md +12 -9
- package/package.json +2 -1
- package/plugins/antigravity/memorix/plugin.json +1 -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 +2 -2
- package/src/cli/commands/serve-http.ts +2 -7
- package/src/cli/commands/serve.ts +26 -7
- package/src/server/mcp-compat.ts +18 -37
- package/src/server/mcp-discovery.ts +60 -0
- package/src/server/stdio-startup-gate.ts +263 -0
- package/src/server.ts +2 -5
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.
|
|
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.
|
|
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',
|
|
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}`);
|
package/src/server/mcp-compat.ts
CHANGED
|
@@ -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
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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
|
|