immune-brain 3.3.0 → 3.5.0
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/README.md +2 -2
- package/README.zh-CN.md +3 -3
- package/package.json +1 -1
- package/plugins/immune-brain/.claude-plugin/plugin.json +1 -1
- package/plugins/immune-brain/.pi-extension/imm-canary-work.ts +84 -1
- package/plugins/immune-brain/.pi-extension/pi-canary-interaction.ts +124 -6
- package/plugins/immune-brain/dist/BASELINE.md +1 -1
- package/plugins/immune-brain/dist/claude/mcp-server.mjs +307 -242
- package/plugins/immune-brain/dist/imm-brainstorm.md +1 -1
- package/plugins/immune-brain/dist/imm-loop.md +8 -7
- package/plugins/immune-brain/dist/imm-planner.md +22 -17
- package/plugins/immune-brain/hooks/hooks.json +0 -10
- package/plugins/immune-brain/runtime/assurance/coordinator.ts +7 -1
- package/plugins/immune-brain/runtime/claude/capability.ts +4 -10
- package/plugins/immune-brain/runtime/claude/interaction.ts +55 -21
- package/plugins/immune-brain/runtime/claude/kernel_ports.ts +59 -50
- package/plugins/immune-brain/runtime/claude/mcp_server.ts +222 -48
- package/plugins/immune-brain/runtime/claude/review_host.ts +2 -138
- package/plugins/immune-brain/runtime/plugin_version.ts +1 -1
- package/plugins/immune-brain/skills/BASELINE.md +1 -1
- package/plugins/immune-brain/skills/imm-brainstorm/SKILL.md +4 -0
- package/plugins/immune-brain/skills/imm-loop/SKILL.md +4 -0
- package/plugins/immune-brain/skills/imm-planner/SKILL.md +6 -0
|
@@ -1,13 +1,22 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
1
2
|
import { createInterface } from "node:readline";
|
|
2
3
|
import { stdin, stdout } from "node:process";
|
|
3
4
|
import type { Readable, Writable } from "node:stream";
|
|
4
5
|
import { CORE_CONTRACT, HOST_ID, MIN_CLAUDE_CODE_VERSION, probeHost } from "./capability";
|
|
5
6
|
import { PLUGIN_VERSION } from "../plugin_version";
|
|
6
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
isPrivilegedOperation,
|
|
9
|
+
NativeAuthorityError,
|
|
10
|
+
privilegedAnnotations,
|
|
11
|
+
type NativeConfirmationInput,
|
|
12
|
+
type NativeConfirmationPort,
|
|
13
|
+
} from "./interaction";
|
|
7
14
|
import { ClaudeReviewHost, FileHookEventLog, parseHookStdin } from "./review_host";
|
|
8
15
|
import { ClaudeRuntime, type ToolMeta } from "./kernel_ports";
|
|
9
16
|
import type { AssuranceCoordinatorPorts } from "../assurance/coordinator";
|
|
10
17
|
|
|
18
|
+
export const MCP_PROTOCOL_VERSION = "2025-06-18";
|
|
19
|
+
|
|
11
20
|
export const TOOLS = [
|
|
12
21
|
{ name: "status", description: "Read the Kernel Assurance Projection for an exact task.", privileged: false },
|
|
13
22
|
{ name: "enroll", description: "Enroll a Git-tracked TaskIntent after native confirmation.", privileged: true },
|
|
@@ -37,13 +46,17 @@ export function listMcpTools() {
|
|
|
37
46
|
}));
|
|
38
47
|
}
|
|
39
48
|
|
|
49
|
+
export function supportsElicitationProtocol(value: unknown): boolean {
|
|
50
|
+
return typeof value === "string" && /^\d{4}-\d{2}-\d{2}$/.test(value) && value >= MCP_PROTOCOL_VERSION;
|
|
51
|
+
}
|
|
52
|
+
|
|
40
53
|
export interface McpRuntimeOptions {
|
|
41
54
|
cwd?: string;
|
|
42
55
|
env?: Record<string, string | undefined>;
|
|
43
56
|
ports?: AssuranceCoordinatorPorts;
|
|
44
57
|
host?: ClaudeReviewHost;
|
|
45
58
|
interactive?: boolean;
|
|
46
|
-
|
|
59
|
+
requestConfirmation?: NativeConfirmationPort;
|
|
47
60
|
}
|
|
48
61
|
|
|
49
62
|
export function createMcpRuntime(options: McpRuntimeOptions = {}) {
|
|
@@ -54,21 +67,26 @@ export function createMcpRuntime(options: McpRuntimeOptions = {}) {
|
|
|
54
67
|
host,
|
|
55
68
|
ports: options.ports,
|
|
56
69
|
interactive: options.interactive,
|
|
57
|
-
|
|
70
|
+
requestConfirmation: options.requestConfirmation,
|
|
58
71
|
});
|
|
59
72
|
// Trusted Host evidence negotiated on this JSON-RPC connection. Absent
|
|
60
73
|
// handshake evidence means unversioned and non-interactive: fail closed.
|
|
61
74
|
let negotiatedVersion: string | undefined;
|
|
62
75
|
let negotiatedInteractive = false;
|
|
76
|
+
const connectionId = randomUUID();
|
|
63
77
|
return {
|
|
64
78
|
runtime,
|
|
65
79
|
host,
|
|
80
|
+
connectionId,
|
|
66
81
|
listTools: listMcpTools,
|
|
67
|
-
bindClientHandshake(identity: { version: string; interactive: boolean }) {
|
|
82
|
+
bindClientHandshake(identity: { version: string; interactive: boolean; protocolVersion?: string }) {
|
|
68
83
|
negotiatedVersion = identity.version || undefined;
|
|
69
|
-
negotiatedInteractive = identity.interactive;
|
|
84
|
+
negotiatedInteractive = identity.interactive && supportsElicitationProtocol(identity.protocolVersion);
|
|
70
85
|
runtime.bindHostVersion(negotiatedVersion);
|
|
71
86
|
},
|
|
87
|
+
bindNativeConfirmation(port: NativeConfirmationPort) {
|
|
88
|
+
runtime.bindNativeConfirmation(port);
|
|
89
|
+
},
|
|
72
90
|
sessionInteractive: () => negotiatedInteractive,
|
|
73
91
|
async callTool(name: string, args: Record<string, unknown>, meta: Partial<ToolMeta> = {}) {
|
|
74
92
|
const taskId = String(args.task_id ?? "");
|
|
@@ -80,20 +98,20 @@ export function createMcpRuntime(options: McpRuntimeOptions = {}) {
|
|
|
80
98
|
// The environment version fallback is disabled on this connection:
|
|
81
99
|
// only a version bound from the trusted initialize handshake may
|
|
82
100
|
// reach authority-mutating tools.
|
|
83
|
-
if (!negotiatedVersion) throw new
|
|
101
|
+
if (!negotiatedVersion) throw new NativeAuthorityError("unsupported_host", "Claude Code version is unavailable");
|
|
84
102
|
// Automatic stale-claim repair is deterministic and needs no native
|
|
85
103
|
// interaction; other mutations retain the Host capability requirement.
|
|
86
|
-
if (!negotiatedInteractive && name !== "repair_authority_state")
|
|
104
|
+
if (!negotiatedInteractive && name !== "repair_authority_state") {
|
|
105
|
+
throw new NativeAuthorityError("unsupported_host", "interactive MCP elicitation is unavailable");
|
|
106
|
+
}
|
|
87
107
|
const probe = probeHost(options.env ?? process.env, process.platform, negotiatedVersion);
|
|
88
|
-
if (!probe.ok) throw new
|
|
108
|
+
if (!probe.ok) throw new NativeAuthorityError("unsupported_host", probe.reason);
|
|
89
109
|
const toolMeta: ToolMeta = {
|
|
90
|
-
sessionId: meta.sessionId ??
|
|
110
|
+
sessionId: meta.sessionId ?? connectionId,
|
|
91
111
|
toolCallId: meta.toolCallId ?? `call-${name}`,
|
|
92
112
|
taskId,
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
interactive: meta.interactive ?? options.interactive ?? false,
|
|
96
|
-
decision: meta.decision,
|
|
113
|
+
interactive: meta.interactive ?? options.interactive ?? negotiatedInteractive,
|
|
114
|
+
signal: meta.signal,
|
|
97
115
|
};
|
|
98
116
|
if (name === "enroll") return runtime.enroll(taskId, toolMeta);
|
|
99
117
|
if (name === "advance_assurance") return runtime.advance(taskId, toolMeta.signal);
|
|
@@ -107,7 +125,6 @@ export function createMcpRuntime(options: McpRuntimeOptions = {}) {
|
|
|
107
125
|
throw new Error(`unknown tool ${name}`);
|
|
108
126
|
},
|
|
109
127
|
observe: (event: Parameters<ClaudeReviewHost["observe"]>[0]) => host.observe(event),
|
|
110
|
-
sessionOfElicitation: (toolCallId: string) => host.sessionOfElicitation(toolCallId),
|
|
111
128
|
shutdown: () => runtime.shutdown(),
|
|
112
129
|
aborts: new Map<string | number, AbortController>(),
|
|
113
130
|
};
|
|
@@ -119,25 +136,24 @@ interface JsonRpc {
|
|
|
119
136
|
method?: string;
|
|
120
137
|
params?: unknown;
|
|
121
138
|
result?: unknown;
|
|
122
|
-
error?: { code: number; message: string };
|
|
139
|
+
error?: { code: number; message: string; data?: Record<string, unknown> };
|
|
123
140
|
}
|
|
124
141
|
|
|
125
|
-
function hostCallIdentity(
|
|
126
|
-
meta: Record<string, unknown> | undefined,
|
|
127
|
-
resolveSession?: (toolCallId: string) => string | undefined,
|
|
128
|
-
): { sessionId: string; toolCallId: string } | undefined {
|
|
142
|
+
function hostCallIdentity(meta: Record<string, unknown> | undefined): { toolCallId: string } | undefined {
|
|
129
143
|
if (!meta) return undefined;
|
|
130
|
-
// Real Claude Code wire correlation metadata carries ONLY the namespaced
|
|
131
|
-
// claudecode/toolUseId key; any other correlation key (tool_use_id,
|
|
132
|
-
// toolUseId, toolCallId) is noncanonical and fails closed. The Host session
|
|
133
|
-
// binding is always derived from the sole unconsumed ElicitationResult
|
|
134
|
-
// record for that exact call; no record, or an ambiguous record, means no
|
|
135
|
-
// native interaction: fail closed. A supplied session_id is never trusted
|
|
136
|
-
// and can never bypass ambiguity resolution.
|
|
137
144
|
const toolCallId = meta["claudecode/toolUseId"];
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
145
|
+
return typeof toolCallId === "string" && toolCallId ? { toolCallId } : undefined;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function rpcError(error: unknown): { code: number; message: string; data?: Record<string, unknown> } {
|
|
149
|
+
if (error instanceof NativeAuthorityError) {
|
|
150
|
+
return {
|
|
151
|
+
code: -32000,
|
|
152
|
+
message: error.message,
|
|
153
|
+
data: { reason_code: error.reasonCode, recovery_action: error.recoveryAction },
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
return { code: -32000, message: error instanceof Error ? error.message : String(error) };
|
|
141
157
|
}
|
|
142
158
|
|
|
143
159
|
function encodeMessage(message: JsonRpc): Buffer {
|
|
@@ -147,6 +163,7 @@ function encodeMessage(message: JsonRpc): Buffer {
|
|
|
147
163
|
export async function handleJsonRpc(message: JsonRpc, mcp = createMcpRuntime()): Promise<JsonRpc | null> {
|
|
148
164
|
if (message.method === "initialize") {
|
|
149
165
|
const params = (message.params ?? {}) as {
|
|
166
|
+
protocolVersion?: unknown;
|
|
150
167
|
clientInfo?: { name?: unknown; version?: unknown };
|
|
151
168
|
capabilities?: Record<string, unknown>;
|
|
152
169
|
};
|
|
@@ -157,16 +174,14 @@ export async function handleJsonRpc(message: JsonRpc, mcp = createMcpRuntime()):
|
|
|
157
174
|
const elicitation = params.capabilities?.elicitation;
|
|
158
175
|
mcp.bindClientHandshake({
|
|
159
176
|
version: trustedClient && typeof params.clientInfo?.version === "string" ? params.clientInfo.version : "",
|
|
160
|
-
|
|
161
|
-
// the native interactions privileged authority requires; only a
|
|
162
|
-
// valid capability object counts, not any non-null value.
|
|
177
|
+
protocolVersion: typeof params.protocolVersion === "string" ? params.protocolVersion : undefined,
|
|
163
178
|
interactive: trustedClient && elicitation !== null && typeof elicitation === "object" && !Array.isArray(elicitation),
|
|
164
179
|
});
|
|
165
180
|
return {
|
|
166
181
|
jsonrpc: "2.0",
|
|
167
182
|
id: message.id ?? null,
|
|
168
183
|
result: {
|
|
169
|
-
protocolVersion:
|
|
184
|
+
protocolVersion: MCP_PROTOCOL_VERSION,
|
|
170
185
|
serverInfo: {
|
|
171
186
|
name: HOST_ID,
|
|
172
187
|
version: PLUGIN_VERSION,
|
|
@@ -195,13 +210,13 @@ export async function handleJsonRpc(message: JsonRpc, mcp = createMcpRuntime()):
|
|
|
195
210
|
const name = String(params.name ?? "");
|
|
196
211
|
const interactive = mcp.sessionInteractive();
|
|
197
212
|
if (isPrivilegedOperation(name) && !interactive) {
|
|
198
|
-
throw new
|
|
213
|
+
throw new NativeAuthorityError("unsupported_host", "interactive MCP elicitation is unavailable");
|
|
199
214
|
}
|
|
200
|
-
const identity = hostCallIdentity(params._meta
|
|
215
|
+
const identity = hostCallIdentity(params._meta);
|
|
201
216
|
if (isPrivilegedOperation(name) && !identity) {
|
|
202
|
-
throw new
|
|
217
|
+
throw new NativeAuthorityError("correlation_missing", "canonical claudecode/toolUseId metadata is missing");
|
|
203
218
|
}
|
|
204
|
-
const sessionId =
|
|
219
|
+
const sessionId = mcp.connectionId;
|
|
205
220
|
const toolCallId = identity?.toolCallId ?? String(message.id ?? "stdio");
|
|
206
221
|
const result = await mcp.callTool(name, params.arguments ?? {}, {
|
|
207
222
|
sessionId,
|
|
@@ -216,11 +231,10 @@ export async function handleJsonRpc(message: JsonRpc, mcp = createMcpRuntime()):
|
|
|
216
231
|
result: { content: [{ type: "text", text: JSON.stringify(result) }] },
|
|
217
232
|
};
|
|
218
233
|
} catch (error) {
|
|
219
|
-
const reason = error instanceof Error ? error.message : String(error);
|
|
220
234
|
return {
|
|
221
235
|
jsonrpc: "2.0",
|
|
222
236
|
id: message.id ?? null,
|
|
223
|
-
error:
|
|
237
|
+
error: rpcError(error),
|
|
224
238
|
};
|
|
225
239
|
} finally {
|
|
226
240
|
mcp.aborts.delete(requestId);
|
|
@@ -232,8 +246,69 @@ export async function handleJsonRpc(message: JsonRpc, mcp = createMcpRuntime()):
|
|
|
232
246
|
|
|
233
247
|
async function writeReply(output: Writable, reply: JsonRpc): Promise<void> {
|
|
234
248
|
const payload = encodeMessage(reply);
|
|
235
|
-
|
|
236
|
-
|
|
249
|
+
await new Promise<void>((resolve, reject) => {
|
|
250
|
+
if (!output.writable) {
|
|
251
|
+
reject(new Error("output stream is not writable"));
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
let settled = false;
|
|
255
|
+
const cleanup = () => {
|
|
256
|
+
output.off("drain", onDrain);
|
|
257
|
+
output.off("error", onError);
|
|
258
|
+
output.off("close", onClose);
|
|
259
|
+
};
|
|
260
|
+
const onDrain = () => {
|
|
261
|
+
if (settled) return;
|
|
262
|
+
settled = true;
|
|
263
|
+
cleanup();
|
|
264
|
+
resolve();
|
|
265
|
+
};
|
|
266
|
+
const onError = (error: Error) => {
|
|
267
|
+
if (settled) return;
|
|
268
|
+
settled = true;
|
|
269
|
+
cleanup();
|
|
270
|
+
reject(error);
|
|
271
|
+
};
|
|
272
|
+
const onClose = () => {
|
|
273
|
+
if (settled) return;
|
|
274
|
+
settled = true;
|
|
275
|
+
cleanup();
|
|
276
|
+
reject(new Error("output stream closed before write drained"));
|
|
277
|
+
};
|
|
278
|
+
output.once("error", onError);
|
|
279
|
+
output.once("close", onClose);
|
|
280
|
+
const ok = output.write(payload, (error) => {
|
|
281
|
+
if (settled) return;
|
|
282
|
+
if (error) {
|
|
283
|
+
settled = true;
|
|
284
|
+
cleanup();
|
|
285
|
+
reject(error);
|
|
286
|
+
}
|
|
287
|
+
});
|
|
288
|
+
if (ok) {
|
|
289
|
+
settled = true;
|
|
290
|
+
cleanup();
|
|
291
|
+
resolve();
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
output.once("drain", onDrain);
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
export function elicitationParams(input: NativeConfirmationInput) {
|
|
299
|
+
const details = [
|
|
300
|
+
`Operation: ${input.operation}`,
|
|
301
|
+
`Task: ${input.taskId}`,
|
|
302
|
+
input.risk ? `Risk: ${input.risk}` : null,
|
|
303
|
+
input.intentRevision !== undefined ? `Intent revision: ${input.intentRevision}` : null,
|
|
304
|
+
input.intentContentHash ? `Intent hash: ${input.intentContentHash}` : null,
|
|
305
|
+
input.bindingDigest ? `Binding digest: ${input.bindingDigest}` : null,
|
|
306
|
+
].filter(Boolean);
|
|
307
|
+
return {
|
|
308
|
+
mode: "form",
|
|
309
|
+
message: `Authorize this exact Immune-Brain operation?\n\n${details.join("\n")}`,
|
|
310
|
+
requestedSchema: { type: "object", properties: {} },
|
|
311
|
+
};
|
|
237
312
|
}
|
|
238
313
|
|
|
239
314
|
export async function serveStdio(options: {
|
|
@@ -248,17 +323,93 @@ export async function serveStdio(options: {
|
|
|
248
323
|
const exit = options.exit ?? ((code: number) => { process.exit(code); });
|
|
249
324
|
let buffer = Buffer.alloc(0);
|
|
250
325
|
let accepting = true;
|
|
326
|
+
let requestSequence = 0;
|
|
251
327
|
const inFlight = new Set<Promise<void>>();
|
|
328
|
+
const pending = new Map<string, {
|
|
329
|
+
resolve: (response: JsonRpc) => void;
|
|
330
|
+
reject: (error: Error) => void;
|
|
331
|
+
}>();
|
|
252
332
|
let chain = Promise.resolve();
|
|
253
333
|
|
|
334
|
+
const rejectPending = (error: Error): void => {
|
|
335
|
+
for (const item of pending.values()) item.reject(error);
|
|
336
|
+
pending.clear();
|
|
337
|
+
};
|
|
338
|
+
|
|
339
|
+
mcp.bindNativeConfirmation(async (input) => {
|
|
340
|
+
if (!accepting) throw new NativeAuthorityError("interaction_not_opened", "MCP connection is closed");
|
|
341
|
+
if (input.signal?.aborted) throw new NativeAuthorityError("user_cancelled", "Tool call was cancelled");
|
|
342
|
+
const requestId = `immune-brain:elicitation:${mcp.connectionId}:${++requestSequence}`;
|
|
343
|
+
let abortListener: (() => void) | undefined;
|
|
344
|
+
const response = new Promise<JsonRpc>((resolve, reject) => {
|
|
345
|
+
pending.set(requestId, { resolve, reject });
|
|
346
|
+
abortListener = () => {
|
|
347
|
+
pending.delete(requestId);
|
|
348
|
+
reject(new NativeAuthorityError("user_cancelled", "Tool call was cancelled"));
|
|
349
|
+
};
|
|
350
|
+
input.signal?.addEventListener("abort", abortListener, { once: true });
|
|
351
|
+
});
|
|
352
|
+
try {
|
|
353
|
+
await writeReply(output, {
|
|
354
|
+
jsonrpc: "2.0",
|
|
355
|
+
id: requestId,
|
|
356
|
+
method: "elicitation/create",
|
|
357
|
+
params: elicitationParams(input),
|
|
358
|
+
});
|
|
359
|
+
const reply = await response;
|
|
360
|
+
if (reply.error) {
|
|
361
|
+
if (reply.error.code === -32601) {
|
|
362
|
+
throw new NativeAuthorityError("unsupported_host", "Claude Code rejected MCP elicitation/create");
|
|
363
|
+
}
|
|
364
|
+
throw new NativeAuthorityError("correlation_missing", `MCP elicitation failed: ${reply.error.message}`);
|
|
365
|
+
}
|
|
366
|
+
const result = reply.result;
|
|
367
|
+
const action = typeof result === "object" && result !== null && !Array.isArray(result)
|
|
368
|
+
? (result as { action?: unknown }).action
|
|
369
|
+
: undefined;
|
|
370
|
+
if (action !== "accept" && action !== "decline" && action !== "cancel") {
|
|
371
|
+
throw new NativeAuthorityError("correlation_missing", "MCP elicitation returned an invalid action");
|
|
372
|
+
}
|
|
373
|
+
if (action === "accept") {
|
|
374
|
+
const content = (result as { content?: unknown }).content;
|
|
375
|
+
if (typeof content !== "object" || content === null || Array.isArray(content) || Object.keys(content).length !== 0) {
|
|
376
|
+
throw new NativeAuthorityError("correlation_missing", "MCP elicitation accept content did not match the requested schema");
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
return { decision: action, requestId };
|
|
380
|
+
} finally {
|
|
381
|
+
pending.delete(requestId);
|
|
382
|
+
if (abortListener) input.signal?.removeEventListener("abort", abortListener);
|
|
383
|
+
}
|
|
384
|
+
});
|
|
385
|
+
|
|
254
386
|
const runCall = (parsed: JsonRpc): void => {
|
|
255
387
|
const task = handleJsonRpc(parsed, mcp).then(async (reply) => {
|
|
256
388
|
if (reply) await writeReply(output, reply);
|
|
389
|
+
}).catch(() => {
|
|
390
|
+
void executeShutdown(1);
|
|
257
391
|
});
|
|
258
392
|
inFlight.add(task);
|
|
259
393
|
void task.finally(() => inFlight.delete(task));
|
|
260
394
|
};
|
|
261
395
|
|
|
396
|
+
const routeResponse = (obj: Record<string, unknown>): boolean => {
|
|
397
|
+
const id = typeof obj.id === "string" ? obj.id : "";
|
|
398
|
+
const waiter = id ? pending.get(id) : undefined;
|
|
399
|
+
if (waiter) {
|
|
400
|
+
pending.delete(id);
|
|
401
|
+
const hasResult = Object.hasOwn(obj, "result");
|
|
402
|
+
const hasError = Object.hasOwn(obj, "error");
|
|
403
|
+
if (obj.jsonrpc !== "2.0" || obj.method !== undefined || hasResult === hasError) {
|
|
404
|
+
waiter.reject(new NativeAuthorityError("correlation_missing", "malformed MCP elicitation response"));
|
|
405
|
+
} else {
|
|
406
|
+
waiter.resolve(obj as unknown as JsonRpc);
|
|
407
|
+
}
|
|
408
|
+
return true;
|
|
409
|
+
}
|
|
410
|
+
return obj.method === undefined && obj.id !== undefined;
|
|
411
|
+
};
|
|
412
|
+
|
|
262
413
|
const drainStdio = async (): Promise<void> => {
|
|
263
414
|
while (true) {
|
|
264
415
|
const newline = buffer.indexOf("\n");
|
|
@@ -278,10 +429,12 @@ export async function serveStdio(options: {
|
|
|
278
429
|
continue;
|
|
279
430
|
}
|
|
280
431
|
const obj = parsed as Record<string, unknown>;
|
|
432
|
+
if (routeResponse(obj)) continue;
|
|
281
433
|
if (obj.jsonrpc !== "2.0") {
|
|
282
434
|
await writeReply(output, { jsonrpc: "2.0", id: null, error: { code: -32600, message: "Invalid Request: jsonrpc must be '2.0'" } });
|
|
283
435
|
continue;
|
|
284
436
|
}
|
|
437
|
+
if (routeResponse(obj)) continue;
|
|
285
438
|
if (typeof obj.method !== "string" || !obj.method) {
|
|
286
439
|
const id = obj.id !== undefined && (typeof obj.id === "string" || typeof obj.id === "number") ? obj.id : null;
|
|
287
440
|
await writeReply(output, { jsonrpc: "2.0", id, error: { code: -32600, message: "Invalid Request: missing method" } });
|
|
@@ -291,7 +444,6 @@ export async function serveStdio(options: {
|
|
|
291
444
|
await writeReply(output, { jsonrpc: "2.0", id: null, error: { code: -32600, message: "Invalid Request: invalid id type" } });
|
|
292
445
|
continue;
|
|
293
446
|
}
|
|
294
|
-
// tools/call is a request requiring a reply; notification-form tools/call (no id) is an invalid request
|
|
295
447
|
if (obj.method === "tools/call" && obj.id === undefined) {
|
|
296
448
|
await writeReply(output, { jsonrpc: "2.0", id: null, error: { code: -32600, message: "Invalid Request: tools/call requires an id" } });
|
|
297
449
|
continue;
|
|
@@ -299,10 +451,15 @@ export async function serveStdio(options: {
|
|
|
299
451
|
const rpc = parsed as JsonRpc;
|
|
300
452
|
if (rpc.method === "notifications/cancelled") {
|
|
301
453
|
const requestId = (rpc.params as { requestId?: string | number } | undefined)?.requestId;
|
|
302
|
-
if (requestId
|
|
454
|
+
if (typeof requestId === "string" && pending.has(requestId)) {
|
|
455
|
+
pending.get(requestId)?.reject(new NativeAuthorityError("user_cancelled", "native interaction was cancelled"));
|
|
456
|
+
pending.delete(requestId);
|
|
457
|
+
} else if (requestId !== undefined) {
|
|
458
|
+
mcp.aborts.get(requestId)?.abort(new Error("notifications/cancelled"));
|
|
459
|
+
}
|
|
303
460
|
continue;
|
|
304
461
|
}
|
|
305
|
-
if (
|
|
462
|
+
if (rpc.method === "tools/call") {
|
|
306
463
|
if (!accepting) {
|
|
307
464
|
if (rpc.id !== undefined) await writeReply(output, { jsonrpc: "2.0", id: rpc.id, error: { code: -32000, message: "stdio closed" } });
|
|
308
465
|
continue;
|
|
@@ -315,38 +472,55 @@ export async function serveStdio(options: {
|
|
|
315
472
|
}
|
|
316
473
|
};
|
|
317
474
|
|
|
475
|
+
let resolveStdio = () => {};
|
|
318
476
|
let shutdownPromise: Promise<void> | null = null;
|
|
319
477
|
const executeShutdown = (code = 0): Promise<void> => {
|
|
320
478
|
if (shutdownPromise) return shutdownPromise;
|
|
321
479
|
shutdownPromise = (async () => {
|
|
322
480
|
accepting = false;
|
|
323
481
|
for (const ac of mcp.aborts.values()) ac.abort(new Error("stdio closed"));
|
|
324
|
-
|
|
482
|
+
rejectPending(new NativeAuthorityError("user_cancelled", "MCP connection closed during native interaction"));
|
|
483
|
+
await Promise.race([
|
|
484
|
+
Promise.allSettled([...inFlight]),
|
|
485
|
+
new Promise<void>((resolve) => setTimeout(resolve, 200)),
|
|
486
|
+
]);
|
|
325
487
|
await mcp.shutdown();
|
|
326
488
|
if (output.writable && typeof output.end === "function") {
|
|
327
489
|
await new Promise<void>((cb) => output.end(() => cb()));
|
|
328
490
|
}
|
|
329
491
|
process.exitCode = code;
|
|
330
492
|
exit(code);
|
|
493
|
+
resolveStdio();
|
|
331
494
|
})();
|
|
332
495
|
return shutdownPromise;
|
|
333
496
|
};
|
|
334
497
|
|
|
335
498
|
await new Promise<void>((resolve) => {
|
|
499
|
+
resolveStdio = resolve;
|
|
500
|
+
output.on("error", () => {
|
|
501
|
+
void executeShutdown(1);
|
|
502
|
+
});
|
|
503
|
+
output.on("close", () => {
|
|
504
|
+
void executeShutdown(0);
|
|
505
|
+
});
|
|
336
506
|
input.on("data", (chunk: Buffer | string) => {
|
|
507
|
+
if (!accepting) return;
|
|
337
508
|
chain = chain.then(async () => {
|
|
509
|
+
if (!accepting) return;
|
|
338
510
|
buffer = Buffer.concat([buffer, Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)]);
|
|
339
511
|
await drainStdio();
|
|
512
|
+
}).catch(() => {
|
|
513
|
+
void executeShutdown(1);
|
|
340
514
|
});
|
|
341
515
|
});
|
|
342
516
|
input.on("end", () => {
|
|
343
|
-
void chain.then(() => executeShutdown(0))
|
|
517
|
+
void chain.then(() => executeShutdown(0));
|
|
344
518
|
});
|
|
345
519
|
input.on("close", () => {
|
|
346
|
-
void chain.then(() => executeShutdown(0))
|
|
520
|
+
void chain.then(() => executeShutdown(0));
|
|
347
521
|
});
|
|
348
522
|
input.on("error", () => {
|
|
349
|
-
void
|
|
523
|
+
void executeShutdown(1);
|
|
350
524
|
});
|
|
351
525
|
});
|
|
352
526
|
}
|