immune-brain 2.8.3 → 3.0.2
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/.claude-plugin/marketplace.json +16 -0
- package/README.md +2 -2
- package/README.zh-CN.md +2 -2
- package/package.json +9 -2
- package/plugins/immune-brain/.claude-plugin/plugin.json +8 -0
- package/plugins/immune-brain/.mcp.json +8 -0
- package/plugins/immune-brain/.pi-extension/imm-canary-work.ts +354 -64
- package/plugins/immune-brain/.pi-extension/pi-canary-assurance-progression.ts +74 -706
- package/plugins/immune-brain/.pi-extension/pi-canary-invocations.ts +1 -90
- package/plugins/immune-brain/.pi-extension/pi-canary-native-review.ts +13 -160
- package/plugins/immune-brain/.pi-extension/pi-canary-qa-findings.ts +1 -50
- package/plugins/immune-brain/.pi-extension/pi-canary-review-bundle.ts +1 -262
- package/plugins/immune-brain/.pi-extension/pi-canary-tool-failure.ts +3 -2
- package/plugins/immune-brain/.pi-extension/pi-canary-verification.ts +8 -229
- package/plugins/immune-brain/.pi-extension/runtime-stub.ts +23 -5
- package/plugins/immune-brain/agents/immune-brain-reviewer.md +11 -0
- package/plugins/immune-brain/dist/claude/mcp-server.mjs +7514 -0
- package/plugins/immune-brain/dist/docs/reference/code-quality-guard.md +58 -0
- package/plugins/immune-brain/dist/docs/reference/immune-brain-config.md +1 -1
- package/plugins/immune-brain/dist/docs/reference/planning-quality-gate.md +1 -1
- package/plugins/immune-brain/dist/docs/reference/subagent-dispatch-protocol.md +19 -13
- package/plugins/immune-brain/dist/imm-loop.md +6 -7
- package/plugins/immune-brain/dist/imm-planner.md +1 -1
- package/plugins/immune-brain/dist/imm-pr-fix.md +9 -0
- package/plugins/immune-brain/dist/role-prompts/code-review.md +33 -13
- package/plugins/immune-brain/dist/role-prompts/executor.md +14 -0
- package/plugins/immune-brain/dist/role-prompts/pr-fix.md +9 -0
- package/plugins/immune-brain/dist/role-prompts/test-fixer.md +7 -0
- package/plugins/immune-brain/hooks/hooks.json +55 -0
- package/plugins/immune-brain/runtime/assurance/coordinator.ts +836 -0
- package/plugins/immune-brain/runtime/assurance/enrollment.ts +6 -0
- package/plugins/immune-brain/runtime/assurance/host_port.ts +18 -0
- package/plugins/immune-brain/runtime/assurance/invocations.ts +90 -0
- package/plugins/immune-brain/runtime/assurance/qa_findings.ts +50 -0
- package/plugins/immune-brain/runtime/assurance/review_evidence.ts +596 -0
- package/plugins/immune-brain/runtime/assurance/verification.ts +233 -0
- package/plugins/immune-brain/runtime/claude/capability.ts +67 -0
- package/plugins/immune-brain/runtime/claude/interaction.ts +70 -0
- package/plugins/immune-brain/runtime/claude/kernel_ports.ts +789 -0
- package/plugins/immune-brain/runtime/claude/mcp_server.ts +363 -0
- package/plugins/immune-brain/runtime/claude/review_host.ts +645 -0
- package/plugins/immune-brain/runtime/commands/kernel.ts +221 -3
- package/plugins/immune-brain/runtime/github_issue_tracker.ts +2 -2
- package/plugins/immune-brain/runtime/kernel/application.ts +8 -5
- package/plugins/immune-brain/runtime/kernel/assurance_projection.ts +24 -13
- package/plugins/immune-brain/runtime/kernel/authority_port.ts +78 -115
- package/plugins/immune-brain/runtime/kernel/canary_application.ts +6 -4
- package/plugins/immune-brain/runtime/kernel/capability_registry.ts +89 -0
- package/plugins/immune-brain/runtime/kernel/completion.ts +64 -6
- package/plugins/immune-brain/runtime/kernel/enrollment.ts +189 -100
- package/plugins/immune-brain/runtime/kernel/enrollment_authority.ts +37 -80
- package/plugins/immune-brain/runtime/kernel/intent.ts +24 -0
- package/plugins/immune-brain/runtime/kernel/pi_canary_prepare.ts +31 -0
- package/plugins/immune-brain/runtime/kernel/reducer.ts +36 -13
- package/plugins/immune-brain/runtime/kernel/storage.ts +16 -16
- package/plugins/immune-brain/runtime/kernel/types.ts +32 -2
- package/plugins/immune-brain/runtime/kernel/validation.ts +107 -16
- package/plugins/immune-brain/runtime/loop_contract.ts +17 -2
- package/plugins/immune-brain/runtime/prompts/code-review.md +33 -13
- package/plugins/immune-brain/runtime/prompts/executor.md +14 -0
- package/plugins/immune-brain/runtime/prompts/pr-fix.md +9 -0
- package/plugins/immune-brain/runtime/prompts/test-fixer.md +7 -0
- package/plugins/immune-brain/runtime/v4_runtime.ts +5 -2
- package/plugins/immune-brain/runtime/workspace_scope.ts +191 -5
- package/plugins/immune-brain/skills/imm-loop/SKILL.md +3 -4
- package/plugins/immune-brain/skills/imm-planner/SKILL.md +2 -0
|
@@ -0,0 +1,363 @@
|
|
|
1
|
+
import { createInterface } from "node:readline";
|
|
2
|
+
import { stdin, stdout } from "node:process";
|
|
3
|
+
import type { Readable, Writable } from "node:stream";
|
|
4
|
+
import { CORE_CONTRACT, HOST_ID, MIN_CLAUDE_CODE_VERSION, probeHost } from "./capability";
|
|
5
|
+
import { isPrivilegedOperation, privilegedAnnotations, type NativeDecision } from "./interaction";
|
|
6
|
+
import { ClaudeReviewHost, FileHookEventLog, parseHookStdin } from "./review_host";
|
|
7
|
+
import { ClaudeRuntime, type ToolMeta } from "./kernel_ports";
|
|
8
|
+
import type { AssuranceCoordinatorPorts } from "../assurance/coordinator";
|
|
9
|
+
|
|
10
|
+
export const TOOLS = [
|
|
11
|
+
{ name: "status", description: "Read the Kernel Assurance Projection for an exact task.", privileged: false },
|
|
12
|
+
{ name: "enroll", description: "Enroll a Git-tracked TaskIntent after native confirmation.", privileged: true },
|
|
13
|
+
{ name: "advance_assurance", description: "Advance frozen Assurance through deterministic QA and Review reservation.", privileged: false },
|
|
14
|
+
{ name: "submit_review", description: "Submit the Parent-mediated Review verdict bound to the correlated receipt.", privileged: false },
|
|
15
|
+
{ name: "request_authorization", description: "Apply exact literal-user authorization.", privileged: true },
|
|
16
|
+
{ name: "approve_breaking_intent_revision", description: "Approve a breaking TaskIntent revision.", privileged: true },
|
|
17
|
+
{ name: "stop", description: "Stop the active task with literal-user authority.", privileged: true },
|
|
18
|
+
{ name: "repair_authority_state", description: "Repair a recoverable stale backend claim.", privileged: true },
|
|
19
|
+
] as const;
|
|
20
|
+
|
|
21
|
+
export function listMcpTools() {
|
|
22
|
+
return TOOLS.map((tool) => ({
|
|
23
|
+
name: tool.name,
|
|
24
|
+
description: tool.description,
|
|
25
|
+
inputSchema: {
|
|
26
|
+
type: "object",
|
|
27
|
+
properties: {
|
|
28
|
+
task_id: { type: "string" },
|
|
29
|
+
...(tool.name === "approve_breaking_intent_revision" ? { next_intent: { type: "object" } } : {}),
|
|
30
|
+
...(tool.name === "stop" ? { reason: { type: "string" } } : {}),
|
|
31
|
+
...(tool.name === "submit_review" ? { verdict: { type: "object" } } : {}),
|
|
32
|
+
},
|
|
33
|
+
required: tool.name === "submit_review" ? ["task_id", "verdict"] : ["task_id"],
|
|
34
|
+
},
|
|
35
|
+
annotations: tool.privileged ? privilegedAnnotations() : { readOnlyHint: tool.name === "status" },
|
|
36
|
+
}));
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface McpRuntimeOptions {
|
|
40
|
+
cwd?: string;
|
|
41
|
+
env?: Record<string, string | undefined>;
|
|
42
|
+
ports?: AssuranceCoordinatorPorts;
|
|
43
|
+
host?: ClaudeReviewHost;
|
|
44
|
+
interactive?: boolean;
|
|
45
|
+
decisions?: Map<string, NativeDecision>;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function createMcpRuntime(options: McpRuntimeOptions = {}) {
|
|
49
|
+
const host = options.host ?? new ClaudeReviewHost(new FileHookEventLog());
|
|
50
|
+
const runtime = new ClaudeRuntime({
|
|
51
|
+
cwd: options.cwd ?? process.cwd(),
|
|
52
|
+
env: options.env ?? process.env,
|
|
53
|
+
host,
|
|
54
|
+
ports: options.ports,
|
|
55
|
+
interactive: options.interactive,
|
|
56
|
+
decisions: options.decisions,
|
|
57
|
+
});
|
|
58
|
+
// Trusted Host evidence negotiated on this JSON-RPC connection. Absent
|
|
59
|
+
// handshake evidence means unversioned and non-interactive: fail closed.
|
|
60
|
+
let negotiatedVersion: string | undefined;
|
|
61
|
+
let negotiatedInteractive = false;
|
|
62
|
+
return {
|
|
63
|
+
runtime,
|
|
64
|
+
host,
|
|
65
|
+
listTools: listMcpTools,
|
|
66
|
+
bindClientHandshake(identity: { version: string; interactive: boolean }) {
|
|
67
|
+
negotiatedVersion = identity.version || undefined;
|
|
68
|
+
negotiatedInteractive = identity.interactive;
|
|
69
|
+
runtime.bindHostVersion(negotiatedVersion);
|
|
70
|
+
},
|
|
71
|
+
sessionInteractive: () => negotiatedInteractive,
|
|
72
|
+
async callTool(name: string, args: Record<string, unknown>, meta: Partial<ToolMeta> = {}) {
|
|
73
|
+
const taskId = String(args.task_id ?? "");
|
|
74
|
+
if (!taskId) throw new Error("task_id is required");
|
|
75
|
+
if ("native_decision" in args) throw new Error("native_decision cannot be supplied in tool arguments");
|
|
76
|
+
// Read-only status must stay usable without trusted Host evidence, so it
|
|
77
|
+
// is dispatched before the capability probe rejects unversioned hosts.
|
|
78
|
+
if (name === "status") return runtime.status(taskId);
|
|
79
|
+
// The environment version fallback is disabled on this connection:
|
|
80
|
+
// only a version bound from the trusted initialize handshake may
|
|
81
|
+
// reach authority-mutating tools.
|
|
82
|
+
if (!negotiatedVersion) throw new Error("Claude Code version is unavailable");
|
|
83
|
+
// Every authority-mutating tool requires the handshake to have
|
|
84
|
+
// declared elicitation support: only read-only status works on
|
|
85
|
+
// non-interactive sessions, so QA/review/completion cannot mutate
|
|
86
|
+
// Kernel state without native interaction capability.
|
|
87
|
+
if (!negotiatedInteractive) throw new Error("non-interactive host session cannot execute authority tools");
|
|
88
|
+
const probe = probeHost(options.env ?? process.env, process.platform, negotiatedVersion);
|
|
89
|
+
if (!probe.ok) throw new Error(probe.reason);
|
|
90
|
+
const toolMeta: ToolMeta = {
|
|
91
|
+
sessionId: meta.sessionId ?? "session",
|
|
92
|
+
toolCallId: meta.toolCallId ?? `call-${name}`,
|
|
93
|
+
taskId,
|
|
94
|
+
requiresUserInteraction: meta.requiresUserInteraction ?? isPrivilegedOperation(name),
|
|
95
|
+
permissionMode: meta.permissionMode ?? probe.permissionMode,
|
|
96
|
+
interactive: meta.interactive ?? options.interactive ?? false,
|
|
97
|
+
decision: meta.decision,
|
|
98
|
+
};
|
|
99
|
+
if (name === "enroll") return runtime.enroll(taskId, toolMeta);
|
|
100
|
+
if (name === "advance_assurance") return runtime.advance(taskId, toolMeta.signal);
|
|
101
|
+
if (name === "submit_review") {
|
|
102
|
+
if (!Object.hasOwn(args, "verdict")) throw new Error("verdict is required");
|
|
103
|
+
return runtime.submitReview(taskId, args.verdict);
|
|
104
|
+
}
|
|
105
|
+
if (name === "request_authorization" || name === "approve_breaking_intent_revision" || name === "stop" || name === "repair_authority_state") {
|
|
106
|
+
return runtime.authorize(taskId, name, toolMeta, args);
|
|
107
|
+
}
|
|
108
|
+
throw new Error(`unknown tool ${name}`);
|
|
109
|
+
},
|
|
110
|
+
observe: (event: Parameters<ClaudeReviewHost["observe"]>[0]) => host.observe(event),
|
|
111
|
+
sessionOfElicitation: (toolCallId: string) => host.sessionOfElicitation(toolCallId),
|
|
112
|
+
shutdown: () => runtime.shutdown(),
|
|
113
|
+
aborts: new Map<string | number, AbortController>(),
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
interface JsonRpc {
|
|
118
|
+
jsonrpc: "2.0";
|
|
119
|
+
id?: string | number | null;
|
|
120
|
+
method?: string;
|
|
121
|
+
params?: unknown;
|
|
122
|
+
result?: unknown;
|
|
123
|
+
error?: { code: number; message: string };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function hostCallIdentity(
|
|
127
|
+
meta: Record<string, unknown> | undefined,
|
|
128
|
+
resolveSession?: (toolCallId: string) => string | undefined,
|
|
129
|
+
): { sessionId: string; toolCallId: string } | undefined {
|
|
130
|
+
if (!meta) return undefined;
|
|
131
|
+
// Real Claude Code wire correlation metadata carries ONLY the namespaced
|
|
132
|
+
// claudecode/toolUseId key; any other correlation key (tool_use_id,
|
|
133
|
+
// toolUseId, toolCallId) is noncanonical and fails closed. The Host session
|
|
134
|
+
// binding is always derived from the sole unconsumed ElicitationResult
|
|
135
|
+
// record for that exact call; no record, or an ambiguous record, means no
|
|
136
|
+
// native interaction: fail closed. A supplied session_id is never trusted
|
|
137
|
+
// and can never bypass ambiguity resolution.
|
|
138
|
+
const toolCallId = meta["claudecode/toolUseId"];
|
|
139
|
+
if (typeof toolCallId !== "string" || !toolCallId) return undefined;
|
|
140
|
+
const sessionId = resolveSession?.(toolCallId);
|
|
141
|
+
return sessionId ? { sessionId, toolCallId } : undefined;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function encodeMessage(message: JsonRpc): Buffer {
|
|
145
|
+
return Buffer.from(`${JSON.stringify(message)}\n`);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export async function handleJsonRpc(message: JsonRpc, mcp = createMcpRuntime()): Promise<JsonRpc | null> {
|
|
149
|
+
if (message.method === "initialize") {
|
|
150
|
+
const params = (message.params ?? {}) as {
|
|
151
|
+
clientInfo?: { name?: unknown; version?: unknown };
|
|
152
|
+
capabilities?: Record<string, unknown>;
|
|
153
|
+
};
|
|
154
|
+
// Trust binds to the exact Claude Code client identity: name, parseable
|
|
155
|
+
// version at the floor, and elicitation capability. Any other client
|
|
156
|
+
// name stays unversioned and non-interactive; read-only status remains usable.
|
|
157
|
+
const trustedClient = params.clientInfo?.name === HOST_ID;
|
|
158
|
+
const elicitation = params.capabilities?.elicitation;
|
|
159
|
+
mcp.bindClientHandshake({
|
|
160
|
+
version: trustedClient && typeof params.clientInfo?.version === "string" ? params.clientInfo.version : "",
|
|
161
|
+
// A client that does not declare elicitation support cannot surface
|
|
162
|
+
// the native interactions privileged authority requires; only a
|
|
163
|
+
// valid capability object counts, not any non-null value.
|
|
164
|
+
interactive: trustedClient && elicitation !== null && typeof elicitation === "object" && !Array.isArray(elicitation),
|
|
165
|
+
});
|
|
166
|
+
return {
|
|
167
|
+
jsonrpc: "2.0",
|
|
168
|
+
id: message.id ?? null,
|
|
169
|
+
result: {
|
|
170
|
+
protocolVersion: "2024-11-05",
|
|
171
|
+
serverInfo: { name: HOST_ID, version: MIN_CLAUDE_CODE_VERSION, contract: CORE_CONTRACT },
|
|
172
|
+
capabilities: { tools: {} },
|
|
173
|
+
},
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
if (message.method === "notifications/initialized") return null;
|
|
177
|
+
if (message.method === "notifications/cancelled") {
|
|
178
|
+
const requestId = (message.params as { requestId?: string | number } | undefined)?.requestId;
|
|
179
|
+
if (requestId !== undefined) mcp.aborts.get(requestId)?.abort(new Error("notifications/cancelled"));
|
|
180
|
+
return null;
|
|
181
|
+
}
|
|
182
|
+
if (message.method === "tools/list") {
|
|
183
|
+
return { jsonrpc: "2.0", id: message.id ?? null, result: { tools: mcp.listTools() } };
|
|
184
|
+
}
|
|
185
|
+
if (message.method === "tools/call") {
|
|
186
|
+
const params = (message.params ?? {}) as { name?: string; arguments?: Record<string, unknown>; _meta?: Record<string, unknown> };
|
|
187
|
+
const requestId = message.id ?? `anon-${Date.now()}`;
|
|
188
|
+
const ac = new AbortController();
|
|
189
|
+
mcp.aborts.set(requestId, ac);
|
|
190
|
+
try {
|
|
191
|
+
const name = String(params.name ?? "");
|
|
192
|
+
const interactive = mcp.sessionInteractive();
|
|
193
|
+
if (isPrivilegedOperation(name) && !interactive) {
|
|
194
|
+
throw new Error("non-interactive host session cannot mint authority");
|
|
195
|
+
}
|
|
196
|
+
const identity = hostCallIdentity(params._meta, (toolCallId) => mcp.sessionOfElicitation(toolCallId));
|
|
197
|
+
if (isPrivilegedOperation(name) && !identity) {
|
|
198
|
+
throw new Error("host correlation metadata missing");
|
|
199
|
+
}
|
|
200
|
+
const sessionId = identity?.sessionId ?? "stdio";
|
|
201
|
+
const toolCallId = identity?.toolCallId ?? String(message.id ?? "stdio");
|
|
202
|
+
const result = await mcp.callTool(name, params.arguments ?? {}, {
|
|
203
|
+
sessionId,
|
|
204
|
+
toolCallId,
|
|
205
|
+
interactive,
|
|
206
|
+
requiresUserInteraction: isPrivilegedOperation(name),
|
|
207
|
+
signal: ac.signal,
|
|
208
|
+
});
|
|
209
|
+
return {
|
|
210
|
+
jsonrpc: "2.0",
|
|
211
|
+
id: message.id ?? null,
|
|
212
|
+
result: { content: [{ type: "text", text: JSON.stringify(result) }] },
|
|
213
|
+
};
|
|
214
|
+
} catch (error) {
|
|
215
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
216
|
+
return {
|
|
217
|
+
jsonrpc: "2.0",
|
|
218
|
+
id: message.id ?? null,
|
|
219
|
+
error: { code: -32000, message: reason },
|
|
220
|
+
};
|
|
221
|
+
} finally {
|
|
222
|
+
mcp.aborts.delete(requestId);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
if (message.id === undefined) return null;
|
|
226
|
+
return { jsonrpc: "2.0", id: message.id, error: { code: -32601, message: `unknown method ${message.method}` } };
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
async function writeReply(output: Writable, reply: JsonRpc): Promise<void> {
|
|
230
|
+
const payload = encodeMessage(reply);
|
|
231
|
+
if (output.write(payload)) return;
|
|
232
|
+
await new Promise<void>((resolve) => output.once("drain", resolve));
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
export async function serveStdio(options: {
|
|
236
|
+
input?: Readable;
|
|
237
|
+
output?: Writable;
|
|
238
|
+
runtime?: ReturnType<typeof createMcpRuntime>;
|
|
239
|
+
exit?: (code: number) => void;
|
|
240
|
+
} = {}): Promise<void> {
|
|
241
|
+
const input = options.input ?? stdin;
|
|
242
|
+
const output = options.output ?? stdout;
|
|
243
|
+
const mcp = options.runtime ?? createMcpRuntime({ cwd: process.cwd() });
|
|
244
|
+
const exit = options.exit ?? ((code: number) => { process.exit(code); });
|
|
245
|
+
let buffer = Buffer.alloc(0);
|
|
246
|
+
let accepting = true;
|
|
247
|
+
const inFlight = new Set<Promise<void>>();
|
|
248
|
+
let chain = Promise.resolve();
|
|
249
|
+
|
|
250
|
+
const runCall = (parsed: JsonRpc): void => {
|
|
251
|
+
const task = handleJsonRpc(parsed, mcp).then(async (reply) => {
|
|
252
|
+
if (reply) await writeReply(output, reply);
|
|
253
|
+
});
|
|
254
|
+
inFlight.add(task);
|
|
255
|
+
void task.finally(() => inFlight.delete(task));
|
|
256
|
+
};
|
|
257
|
+
|
|
258
|
+
const drainStdio = async (): Promise<void> => {
|
|
259
|
+
while (true) {
|
|
260
|
+
const newline = buffer.indexOf("\n");
|
|
261
|
+
if (newline < 0) return;
|
|
262
|
+
const body = buffer.subarray(0, newline).toString("utf8").trim();
|
|
263
|
+
buffer = buffer.subarray(newline + 1);
|
|
264
|
+
if (!body) continue;
|
|
265
|
+
let parsed: unknown;
|
|
266
|
+
try {
|
|
267
|
+
parsed = JSON.parse(body);
|
|
268
|
+
} catch {
|
|
269
|
+
await writeReply(output, { jsonrpc: "2.0", id: null, error: { code: -32700, message: "Parse error" } });
|
|
270
|
+
continue;
|
|
271
|
+
}
|
|
272
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
273
|
+
await writeReply(output, { jsonrpc: "2.0", id: null, error: { code: -32600, message: "Invalid Request" } });
|
|
274
|
+
continue;
|
|
275
|
+
}
|
|
276
|
+
const obj = parsed as Record<string, unknown>;
|
|
277
|
+
if (obj.jsonrpc !== "2.0") {
|
|
278
|
+
await writeReply(output, { jsonrpc: "2.0", id: null, error: { code: -32600, message: "Invalid Request: jsonrpc must be '2.0'" } });
|
|
279
|
+
continue;
|
|
280
|
+
}
|
|
281
|
+
if (typeof obj.method !== "string" || !obj.method) {
|
|
282
|
+
const id = obj.id !== undefined && (typeof obj.id === "string" || typeof obj.id === "number") ? obj.id : null;
|
|
283
|
+
await writeReply(output, { jsonrpc: "2.0", id, error: { code: -32600, message: "Invalid Request: missing method" } });
|
|
284
|
+
continue;
|
|
285
|
+
}
|
|
286
|
+
if (obj.id !== undefined && typeof obj.id !== "string" && typeof obj.id !== "number" && obj.id !== null) {
|
|
287
|
+
await writeReply(output, { jsonrpc: "2.0", id: null, error: { code: -32600, message: "Invalid Request: invalid id type" } });
|
|
288
|
+
continue;
|
|
289
|
+
}
|
|
290
|
+
// tools/call is a request requiring a reply; notification-form tools/call (no id) is an invalid request
|
|
291
|
+
if (obj.method === "tools/call" && obj.id === undefined) {
|
|
292
|
+
await writeReply(output, { jsonrpc: "2.0", id: null, error: { code: -32600, message: "Invalid Request: tools/call requires an id" } });
|
|
293
|
+
continue;
|
|
294
|
+
}
|
|
295
|
+
const rpc = parsed as JsonRpc;
|
|
296
|
+
if (rpc.method === "notifications/cancelled") {
|
|
297
|
+
const requestId = (rpc.params as { requestId?: string | number } | undefined)?.requestId;
|
|
298
|
+
if (requestId !== undefined) mcp.aborts.get(requestId)?.abort(new Error("notifications/cancelled"));
|
|
299
|
+
continue;
|
|
300
|
+
}
|
|
301
|
+
if (parsed.method === "tools/call") {
|
|
302
|
+
if (!accepting) {
|
|
303
|
+
if (rpc.id !== undefined) await writeReply(output, { jsonrpc: "2.0", id: rpc.id, error: { code: -32000, message: "stdio closed" } });
|
|
304
|
+
continue;
|
|
305
|
+
}
|
|
306
|
+
runCall(rpc);
|
|
307
|
+
continue;
|
|
308
|
+
}
|
|
309
|
+
const reply = await handleJsonRpc(rpc, mcp);
|
|
310
|
+
if (reply) await writeReply(output, reply);
|
|
311
|
+
}
|
|
312
|
+
};
|
|
313
|
+
|
|
314
|
+
let shutdownPromise: Promise<void> | null = null;
|
|
315
|
+
const executeShutdown = (code = 0): Promise<void> => {
|
|
316
|
+
if (shutdownPromise) return shutdownPromise;
|
|
317
|
+
shutdownPromise = (async () => {
|
|
318
|
+
accepting = false;
|
|
319
|
+
for (const ac of mcp.aborts.values()) ac.abort(new Error("stdio closed"));
|
|
320
|
+
await Promise.allSettled([...inFlight]);
|
|
321
|
+
await mcp.shutdown();
|
|
322
|
+
if (output.writable && typeof output.end === "function") {
|
|
323
|
+
await new Promise<void>((cb) => output.end(() => cb()));
|
|
324
|
+
}
|
|
325
|
+
process.exitCode = code;
|
|
326
|
+
exit(code);
|
|
327
|
+
})();
|
|
328
|
+
return shutdownPromise;
|
|
329
|
+
};
|
|
330
|
+
|
|
331
|
+
await new Promise<void>((resolve) => {
|
|
332
|
+
input.on("data", (chunk: Buffer | string) => {
|
|
333
|
+
chain = chain.then(async () => {
|
|
334
|
+
buffer = Buffer.concat([buffer, Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)]);
|
|
335
|
+
await drainStdio();
|
|
336
|
+
});
|
|
337
|
+
});
|
|
338
|
+
input.on("end", () => {
|
|
339
|
+
void chain.then(() => executeShutdown(0)).finally(resolve);
|
|
340
|
+
});
|
|
341
|
+
input.on("close", () => {
|
|
342
|
+
void chain.then(() => executeShutdown(0)).finally(resolve);
|
|
343
|
+
});
|
|
344
|
+
input.on("error", () => {
|
|
345
|
+
void chain.then(() => executeShutdown(1)).finally(resolve);
|
|
346
|
+
});
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
async function runHook(): Promise<void> {
|
|
351
|
+
const lines: string[] = [];
|
|
352
|
+
const rl = createInterface({ input: stdin });
|
|
353
|
+
for await (const line of rl) lines.push(line);
|
|
354
|
+
const event = parseHookStdin(lines.join("\n"));
|
|
355
|
+
if (!event) return;
|
|
356
|
+
new FileHookEventLog().append(event);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
const entry = process.argv[1] ?? "";
|
|
360
|
+
if (entry.endsWith("mcp_server.ts") || entry.endsWith("mcp-server.mjs")) {
|
|
361
|
+
if (process.argv.includes("--hook")) void runHook();
|
|
362
|
+
else void serveStdio();
|
|
363
|
+
}
|