project-librarian 0.2.0 → 0.3.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.
@@ -0,0 +1,449 @@
1
+ "use strict";
2
+ // Hand-rolled MCP stdio server for the code-evidence index.
3
+ //
4
+ // Why hand-rolled: the product holds a zero-runtime-dependency posture (only
5
+ // node:sqlite plus optional tree-sitter grammars). Adopting @modelcontextprotocol/sdk
6
+ // would add a hard runtime dependency, so we implement the small slice of MCP we
7
+ // need directly: JSON-RPC 2.0 over newline-delimited JSON on stdio.
8
+ //
9
+ // Transport: MCP stdio transport carries one JSON-RPC message per line on stdin
10
+ // and stdout (newline-delimited JSON, "ndjson"); stderr is free for logging. We
11
+ // frame on \n and parse each non-empty line as one JSON-RPC message.
12
+ //
13
+ // Methods implemented exactly: initialize, notifications/initialized (no-op),
14
+ // ping, tools/list, tools/call. Unknown method -> JSON-RPC -32601. Parse error ->
15
+ // JSON-RPC -32700 with id null. These protocol error responses are MCP/JSON-RPC
16
+ // spec compliance, NOT fallback coding: a malformed frame or unknown method has a
17
+ // single spec-defined reply.
18
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
19
+ if (k2 === undefined) k2 = k;
20
+ var desc = Object.getOwnPropertyDescriptor(m, k);
21
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
22
+ desc = { enumerable: true, get: function() { return m[k]; } };
23
+ }
24
+ Object.defineProperty(o, k2, desc);
25
+ }) : (function(o, m, k, k2) {
26
+ if (k2 === undefined) k2 = k;
27
+ o[k2] = m[k];
28
+ }));
29
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
30
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
31
+ }) : function(o, v) {
32
+ o["default"] = v;
33
+ });
34
+ var __importStar = (this && this.__importStar) || (function () {
35
+ var ownKeys = function(o) {
36
+ ownKeys = Object.getOwnPropertyNames || function (o) {
37
+ var ar = [];
38
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
39
+ return ar;
40
+ };
41
+ return ownKeys(o);
42
+ };
43
+ return function (mod) {
44
+ if (mod && mod.__esModule) return mod;
45
+ var result = {};
46
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
47
+ __setModuleDefault(result, mod);
48
+ return result;
49
+ };
50
+ })();
51
+ Object.defineProperty(exports, "__esModule", { value: true });
52
+ exports.TRUST_SENTENCE = exports.TRUNCATION_NOTICE = exports.MAX_RESPONSE_CHARS = exports.SUPPORTED_PROTOCOL_VERSION = void 0;
53
+ exports.scaleGuidanceLines = scaleGuidanceLines;
54
+ exports.runMcpServerMode = runMcpServerMode;
55
+ exports.handleLine = handleLine;
56
+ const fs = __importStar(require("node:fs"));
57
+ const path = __importStar(require("node:path"));
58
+ const code_index_1 = require("./code-index");
59
+ const code_index_file_policy_1 = require("./code-index-file-policy");
60
+ const workspace_1 = require("./workspace");
61
+ // Pinned MCP protocol version. This is the spec revision this server is written
62
+ // against; one constant so the supported version is auditable in a single place.
63
+ const SUPPORTED_PROTOCOL_VERSION = "2025-06-18";
64
+ exports.SUPPORTED_PROTOCOL_VERSION = SUPPORTED_PROTOCOL_VERSION;
65
+ // JSON-RPC 2.0 standard error codes (subset we emit). Spec-defined, not tunable.
66
+ const JSONRPC_PARSE_ERROR = -32700;
67
+ const JSONRPC_METHOD_NOT_FOUND = -32601;
68
+ const JSONRPC_INVALID_PARAMS = -32602;
69
+ // Hard cap on a single tool-result text payload. The benchmark finding is that
70
+ // answer-shaped, bounded responses are the hypothesis under test; an unbounded
71
+ // dump would reintroduce the tool-output cost the boundary measured. When a body
72
+ // exceeds this, it is cut and an explicit notice is appended (never silent).
73
+ const MAX_RESPONSE_CHARS = 4000;
74
+ exports.MAX_RESPONSE_CHARS = MAX_RESPONSE_CHARS;
75
+ const TRUNCATION_NOTICE = "[truncated — refine the query]";
76
+ exports.TRUNCATION_NOTICE = TRUNCATION_NOTICE;
77
+ // The trust sentence appended to every tool description (B4 analogue). It tells
78
+ // the agent the index is authoritative for structure questions so it does not
79
+ // re-run repo-wide greps, gated on the staleness signal that code_status reports.
80
+ const TRUST_SENTENCE = "Results derive from the indexed code and are authoritative for structure questions — do not re-verify with repo-wide greps unless `code_status` reports staleness.";
81
+ exports.TRUST_SENTENCE = TRUST_SENTENCE;
82
+ // ---------------------------------------------------------------------------
83
+ // serverInfo from package.json (read at runtime; no version constant duplicated)
84
+ // ---------------------------------------------------------------------------
85
+ function serverInfo() {
86
+ try {
87
+ const raw = fs.readFileSync(path.resolve(__dirname, "..", "package.json"), "utf8");
88
+ const parsed = JSON.parse(raw);
89
+ return {
90
+ name: typeof parsed.name === "string" ? parsed.name : "project-librarian",
91
+ version: typeof parsed.version === "string" ? parsed.version : "0.0.0",
92
+ };
93
+ }
94
+ catch {
95
+ return { name: "project-librarian", version: "0.0.0" };
96
+ }
97
+ }
98
+ // ---------------------------------------------------------------------------
99
+ // Answer-shape helpers
100
+ // ---------------------------------------------------------------------------
101
+ function requireStringArg(args, key) {
102
+ const value = args[key];
103
+ if (typeof value !== "string" || value.trim() === "") {
104
+ throw new ToolArgumentError(`missing required string argument: ${key}`);
105
+ }
106
+ return value.trim();
107
+ }
108
+ // Thrown for bad tool arguments; surfaces as a JSON-RPC -32602 invalid params.
109
+ class ToolArgumentError extends Error {
110
+ }
111
+ // Prepend a single staleness warning line when the index is stale, then enforce
112
+ // the hard char cap with an explicit truncation notice. The warning is counted
113
+ // against the cap so the cap is a true ceiling on the returned text.
114
+ function finalizeAnswer(body, staleness) {
115
+ const warning = staleness.stale
116
+ ? `[stale index: ${staleness.changed} changed, ${staleness.added} added, ${staleness.deleted} deleted — rerun \`project-librarian --code-index\`]\n`
117
+ : "";
118
+ const combined = warning + body;
119
+ if (combined.length <= MAX_RESPONSE_CHARS)
120
+ return combined;
121
+ const budget = MAX_RESPONSE_CHARS - TRUNCATION_NOTICE.length - 1;
122
+ const sliceEnd = budget > 0 ? budget : 0;
123
+ return `${combined.slice(0, sliceEnd).trimEnd()}\n${TRUNCATION_NOTICE}`;
124
+ }
125
+ // Collapse a list to a bounded sample plus a "+N more" counter so repetitive
126
+ // items become counts instead of file bodies (paths/symbols/signatures only).
127
+ function sampleWithCount(items, limit, render) {
128
+ const lines = items.slice(0, limit).map(render);
129
+ if (items.length > limit)
130
+ lines.push(` …+${items.length - limit} more`);
131
+ return lines;
132
+ }
133
+ function asRows(value) {
134
+ return Array.isArray(value) ? value : [];
135
+ }
136
+ // ---------------------------------------------------------------------------
137
+ // Tools (answer-first text, grouped compact evidence)
138
+ // ---------------------------------------------------------------------------
139
+ function ownershipAnswer(filePath) {
140
+ const rules = (0, code_index_1.codeownerRules)();
141
+ const matched = (0, code_index_1.matchedCodeownerRules)(filePath, rules);
142
+ const context = (0, code_index_1.ownershipContext)();
143
+ const info = (0, code_index_1.ownershipInfo)(filePath, context);
144
+ const effective = matched[matched.length - 1];
145
+ const overridden = matched.slice(0, -1);
146
+ const ownerStatement = effective
147
+ ? `Owner of ${filePath} is ${effective.owners.join(" ")} (CODEOWNERS ${effective.file_path}:${effective.line} \`${effective.pattern}\`, last match); ${overridden.length} overridden rule${overridden.length === 1 ? "" : "s"}.`
148
+ : `No CODEOWNERS rule matches ${filePath}; path-derived owner is ${info.owner} (${info.owner_source}).`;
149
+ const lines = [ownerStatement];
150
+ if (overridden.length > 0) {
151
+ lines.push("Overridden rules (earlier matches, lower precedence):");
152
+ lines.push(...sampleWithCount(overridden, 8, (rule) => ` ${rule.file_path}:${rule.line} \`${rule.pattern}\` -> ${rule.owners.join(" ")}`));
153
+ }
154
+ lines.push(`Workspace owner: ${info.owner} (source: ${info.owner_source})${info.codeowners ? `; codeowners ${info.codeowners}` : ""}.`);
155
+ return lines.join("\n");
156
+ }
157
+ function impactAnswer(database, term) {
158
+ const impact = (0, code_index_1.codeImpact)(database, term);
159
+ const matches = (impact.matches ?? {});
160
+ const edges = (impact.edges ?? {});
161
+ const owners = asRows(impact.impacted_owners);
162
+ const files = asRows(matches.files);
163
+ const symbols = asRows(matches.symbols);
164
+ const routes = asRows(matches.routes);
165
+ const imports = asRows(matches.imports);
166
+ const incoming = asRows(edges.incoming);
167
+ const outgoing = asRows(edges.outgoing);
168
+ const ownerLabel = owners.length === 0
169
+ ? "no owners"
170
+ : owners.slice(0, 3).map((o) => `${String(o.owner)} (${Number(o.files)} files)`).join(", ") + (owners.length > 3 ? `, +${owners.length - 3} more` : "");
171
+ const lines = [
172
+ `Impact of "${term}": ${files.length} files, ${symbols.length} symbols, ${routes.length} routes, ${imports.length} imports; ${incoming.length} incoming / ${outgoing.length} outgoing edges; owners: ${ownerLabel}.`,
173
+ ];
174
+ if (symbols.length > 0) {
175
+ lines.push("Symbols:");
176
+ lines.push(...sampleWithCount(symbols, 12, (row) => ` ${String(row.file_path)}:${String(row.line)} ${String(row.kind)} ${String(row.name)} — ${String(row.signature)}`));
177
+ }
178
+ if (routes.length > 0) {
179
+ lines.push("Routes:");
180
+ lines.push(...sampleWithCount(routes, 8, (row) => ` ${String(row.method)} ${String(row.route)} -> ${String(row.handler)} (${String(row.file_path)}:${String(row.line)})`));
181
+ }
182
+ if (imports.length > 0) {
183
+ lines.push("Imports:");
184
+ lines.push(...sampleWithCount(imports, 8, (row) => ` ${String(row.from_file)} -> ${String(row.to_ref)}`));
185
+ }
186
+ if (incoming.length > 0) {
187
+ lines.push("Incoming edges (dependents):");
188
+ lines.push(...sampleWithCount(incoming, 10, (row) => ` ${String(row.kind)}: ${String(row.source)} -> ${String(row.target)} (${String(row.file_path)}:${String(row.line)})`));
189
+ }
190
+ if (owners.length > 0) {
191
+ lines.push("Owners:");
192
+ lines.push(...sampleWithCount(owners, 8, (row) => ` ${String(row.owner)} (${String(row.owner_source)}, ${Number(row.files)} files)${row.codeowners ? ` ${String(row.codeowners)}` : ""}`));
193
+ }
194
+ return lines.join("\n");
195
+ }
196
+ function workspaceGraphAnswer(filter) {
197
+ const graph = (0, code_index_1.workspaceDependencyGraph)();
198
+ const workspaces = asRows(graph.workspaces);
199
+ const internal = asRows(graph.internal_dependencies);
200
+ const external = asRows(graph.external_dependency_hotspots);
201
+ const lockfiles = asRows(graph.lockfiles);
202
+ const packageManagers = Array.isArray(graph.package_managers) ? graph.package_managers : [];
203
+ const scoped = filter
204
+ ? internal.filter((edge) => String(edge.from_workspace).includes(filter) || String(edge.to_workspace).includes(filter) || String(edge.from_package).includes(filter) || String(edge.to_package).includes(filter))
205
+ : internal;
206
+ const lines = [
207
+ `${filter ? `Workspace graph for "${filter}": ` : "Workspace graph: "}${workspaces.length} packages, ${scoped.length} internal dependency edge${scoped.length === 1 ? "" : "s"}, ${external.length} external hotspots; package managers: ${packageManagers.length > 0 ? packageManagers.join(", ") : "none"} (${lockfiles.length} lockfiles).`,
208
+ ];
209
+ if (workspaces.length > 0 && !filter) {
210
+ lines.push("Packages:");
211
+ lines.push(...sampleWithCount(workspaces, 12, (row) => ` ${String(row.name)} (${String(row.root)})`));
212
+ }
213
+ if (scoped.length > 0) {
214
+ lines.push("Internal dependencies:");
215
+ lines.push(...sampleWithCount(scoped, 15, (edge) => ` ${String(edge.from_package)} -> ${String(edge.to_package)} (${String(edge.dependency_type)} ${String(edge.version)})`));
216
+ }
217
+ if (external.length > 0) {
218
+ lines.push("External hotspots:");
219
+ lines.push(...sampleWithCount(external, 8, (row) => ` ${String(row.dependency)} (${String(row.dependency_type)}, ${Number(row.workspace_count)} workspaces)`));
220
+ }
221
+ return lines.join("\n");
222
+ }
223
+ function searchAnswer(database, term) {
224
+ const rows = (0, code_index_1.searchSymbols)(database, term);
225
+ const byKind = new Map();
226
+ for (const row of rows)
227
+ byKind.set(String(row.kind), (byKind.get(String(row.kind)) ?? 0) + 1);
228
+ const kindSummary = Array.from(byKind.entries()).sort((a, b) => b[1] - a[1]).map(([kind, count]) => `${count} ${kind}`).join(", ");
229
+ const lines = [
230
+ `Search "${term}": ${rows.length} matching symbol${rows.length === 1 ? "" : "s"}${rows.length >= 50 ? " (capped at 50)" : ""}${kindSummary ? ` — ${kindSummary}` : ""}.`,
231
+ ];
232
+ if (rows.length > 0) {
233
+ lines.push("Symbols:");
234
+ lines.push(...sampleWithCount(rows, 25, (row) => ` ${String(row.file_path)}:${String(row.line)} ${String(row.kind)} ${String(row.name)} — ${String(row.signature)}`));
235
+ }
236
+ return lines.join("\n");
237
+ }
238
+ // Scale-aware routing guidance for code_status (2026-06-12 decision, stageR1 /
239
+ // stage2d evidence): the indexed file count places the repo in a scale bracket,
240
+ // and the bracket tells the agent which question shapes the tools measured
241
+ // cheaper for. Exported as a pure function so both brackets are unit-testable.
242
+ function scaleGuidanceLines(indexedFileCount) {
243
+ const bracket = indexedFileCount < code_index_file_policy_1.SMALL_REPO_FILE_THRESHOLD
244
+ ? `Scale: small (${indexedFileCount} indexed files < ${code_index_file_policy_1.SMALL_REPO_FILE_THRESHOLD}) — at this scale direct reads measured cheaper for every benchmarked question; prefer direct reads for simple lookups and reserve these tools for expensive traversal questions.`
245
+ : `Scale: large (${indexedFileCount} indexed files >= ${code_index_file_policy_1.SMALL_REPO_FILE_THRESHOLD}) — expensive traversal questions (impact tracing) measured cheaper through the index at this scale.`;
246
+ return [
247
+ bracket,
248
+ "Ownership-style simple lookups measured cheaper via direct reads at every scale; prefer direct reads for those.",
249
+ ];
250
+ }
251
+ function statusAnswer(database, relativePath, staleness) {
252
+ const coverage = (0, code_index_1.evidenceCoverage)(database);
253
+ const staleLabel = staleness.stale
254
+ ? `STALE (${staleness.changed} changed, ${staleness.added} added, ${staleness.deleted} deleted)`
255
+ : "fresh";
256
+ const lines = [
257
+ `Index ${relativePath} is ${staleLabel}; ${coverage.files ?? 0} files, ${coverage.symbols ?? 0} symbols, ${coverage.imports ?? 0} imports, ${coverage.routes ?? 0} routes, ${coverage.edges ?? 0} edges, ${coverage.configs ?? 0} configs.`,
258
+ ...scaleGuidanceLines(Number(coverage.files ?? 0)),
259
+ ];
260
+ if (staleness.stale) {
261
+ lines.push("Action: rerun `project-librarian --code-index` (or `--code-index --incremental`) before trusting structure answers.");
262
+ }
263
+ return lines.join("\n");
264
+ }
265
+ const TOOLS = [
266
+ {
267
+ name: "code_impact",
268
+ description: `Impact of a file, symbol, route, or module term across the indexed code: matching symbols/signatures, routes, imports, dependent edges, and impacted owners. ${TRUST_SENTENCE}`,
269
+ inputSchema: {
270
+ type: "object",
271
+ properties: { term: { type: "string", description: "File path, symbol name, route, or module to trace." } },
272
+ required: ["term"],
273
+ },
274
+ run: (database, args) => impactAnswer(database, requireStringArg(args, "term")),
275
+ },
276
+ {
277
+ name: "code_ownership",
278
+ description: `Effective CODEOWNERS owner for a path under last-match-wins precedence, the rules it overrode, and the workspace owner. ${TRUST_SENTENCE}`,
279
+ inputSchema: {
280
+ type: "object",
281
+ properties: { path: { type: "string", description: "Repo-relative file or directory path." } },
282
+ required: ["path"],
283
+ },
284
+ run: (_database, args) => ownershipAnswer((0, workspace_1.normalizePath)(requireStringArg(args, "path"))),
285
+ },
286
+ {
287
+ name: "code_workspace_graph",
288
+ description: `Monorepo workspace dependency graph: packages, internal dependency edges, external hotspots, and package managers, optionally filtered to one workspace. ${TRUST_SENTENCE}`,
289
+ inputSchema: {
290
+ type: "object",
291
+ properties: { workspace: { type: "string", description: "Optional workspace name or path substring to focus the graph." } },
292
+ },
293
+ run: (_database, args) => {
294
+ const filter = typeof args.workspace === "string" ? args.workspace.trim() : "";
295
+ return workspaceGraphAnswer(filter);
296
+ },
297
+ },
298
+ {
299
+ name: "code_search",
300
+ description: `Search indexed symbols by name or signature substring; returns matching symbols with file, line, kind, and signature. ${TRUST_SENTENCE}`,
301
+ inputSchema: {
302
+ type: "object",
303
+ properties: { term: { type: "string", description: "Symbol name or signature substring." } },
304
+ required: ["term"],
305
+ },
306
+ run: (database, args) => searchAnswer(database, requireStringArg(args, "term")),
307
+ },
308
+ {
309
+ name: "code_status",
310
+ description: `Index freshness and coverage counts; reports whether the index is stale so callers know when re-verification is warranted. ${TRUST_SENTENCE}`,
311
+ inputSchema: { type: "object", properties: {} },
312
+ run: () => {
313
+ throw new Error("code_status is handled with index metadata in callTool");
314
+ },
315
+ },
316
+ ];
317
+ const TOOLS_BY_NAME = new Map(TOOLS.map((tool) => [tool.name, tool]));
318
+ // ---------------------------------------------------------------------------
319
+ // tools/call dispatch
320
+ // ---------------------------------------------------------------------------
321
+ function toolResultContent(text, isError = false) {
322
+ return { content: [{ type: "text", text }], isError };
323
+ }
324
+ function callTool(name, rawArgs) {
325
+ if (typeof name !== "string" || !TOOLS_BY_NAME.has(name)) {
326
+ return toolResultContent(`unknown tool: ${String(name)}; available tools: ${TOOLS.map((tool) => tool.name).join(", ")}`, true);
327
+ }
328
+ const args = rawArgs && typeof rawArgs === "object" && !Array.isArray(rawArgs) ? rawArgs : {};
329
+ let opened;
330
+ try {
331
+ opened = (0, code_index_1.openCodeEvidenceDatabaseForServing)();
332
+ }
333
+ catch (error) {
334
+ if (error instanceof code_index_1.CodeEvidenceIndexUnavailableError) {
335
+ return toolResultContent(error.message, true);
336
+ }
337
+ throw error;
338
+ }
339
+ try {
340
+ const staleness = (0, code_index_1.codeIndexStaleness)(opened.database);
341
+ const body = name === "code_status"
342
+ ? statusAnswer(opened.database, opened.relativePath, staleness)
343
+ : TOOLS_BY_NAME.get(name).run(opened.database, args);
344
+ return toolResultContent(finalizeAnswer(body, staleness));
345
+ }
346
+ catch (error) {
347
+ if (error instanceof ToolArgumentError)
348
+ throw error;
349
+ const message = error instanceof Error ? error.message : String(error);
350
+ return toolResultContent(`code evidence tool error: ${message}`, true);
351
+ }
352
+ finally {
353
+ opened.database.close();
354
+ }
355
+ }
356
+ // ---------------------------------------------------------------------------
357
+ // JSON-RPC wiring
358
+ // ---------------------------------------------------------------------------
359
+ function successResponse(id, result) {
360
+ return JSON.stringify({ jsonrpc: "2.0", id, result });
361
+ }
362
+ function errorResponse(id, error) {
363
+ return JSON.stringify({ jsonrpc: "2.0", id, error });
364
+ }
365
+ function negotiatedProtocolVersion(params) {
366
+ const requested = params && typeof params === "object" ? params.protocolVersion : undefined;
367
+ // Echo the client's version when it is the one we support; otherwise return our
368
+ // pinned version so the client can decide whether to proceed (MCP negotiation).
369
+ return requested === SUPPORTED_PROTOCOL_VERSION && typeof requested === "string" ? requested : SUPPORTED_PROTOCOL_VERSION;
370
+ }
371
+ function handleRequest(request) {
372
+ const id = request.id ?? null;
373
+ const method = request.method;
374
+ // Notifications have no id and expect no response. notifications/initialized is
375
+ // the only one we receive; treat any id-less message as a notification no-op.
376
+ if (request.id === undefined) {
377
+ return null;
378
+ }
379
+ switch (method) {
380
+ case "initialize":
381
+ return successResponse(id, {
382
+ protocolVersion: negotiatedProtocolVersion(request.params),
383
+ capabilities: { tools: {} },
384
+ serverInfo: serverInfo(),
385
+ });
386
+ case "ping":
387
+ return successResponse(id, {});
388
+ case "tools/list":
389
+ return successResponse(id, {
390
+ tools: TOOLS.map((tool) => ({ name: tool.name, description: tool.description, inputSchema: tool.inputSchema })),
391
+ });
392
+ case "tools/call": {
393
+ const params = request.params && typeof request.params === "object" ? request.params : {};
394
+ try {
395
+ return successResponse(id, callTool(params.name, params.arguments));
396
+ }
397
+ catch (error) {
398
+ if (error instanceof ToolArgumentError) {
399
+ return errorResponse(id, { code: JSONRPC_INVALID_PARAMS, message: error.message });
400
+ }
401
+ throw error;
402
+ }
403
+ }
404
+ default:
405
+ return errorResponse(id, { code: JSONRPC_METHOD_NOT_FOUND, message: `method not found: ${String(method)}` });
406
+ }
407
+ }
408
+ // Parse and route one ndjson line. A parse failure is answered with -32700 and a
409
+ // null id per JSON-RPC; this is the spec's defined reply for an invalid frame.
410
+ function handleLine(line) {
411
+ let parsed;
412
+ try {
413
+ parsed = JSON.parse(line);
414
+ }
415
+ catch {
416
+ return errorResponse(null, { code: JSONRPC_PARSE_ERROR, message: "parse error" });
417
+ }
418
+ if (!parsed || typeof parsed !== "object") {
419
+ return errorResponse(null, { code: JSONRPC_PARSE_ERROR, message: "parse error" });
420
+ }
421
+ return handleRequest(parsed);
422
+ }
423
+ function runMcpServerMode() {
424
+ let buffer = "";
425
+ process.stdin.setEncoding("utf8");
426
+ process.stdin.on("data", (chunk) => {
427
+ buffer += chunk;
428
+ let newlineIndex = buffer.indexOf("\n");
429
+ while (newlineIndex >= 0) {
430
+ const line = buffer.slice(0, newlineIndex).replace(/\r$/, "");
431
+ buffer = buffer.slice(newlineIndex + 1);
432
+ if (line.trim() !== "") {
433
+ const response = handleLine(line);
434
+ if (response !== null)
435
+ process.stdout.write(`${response}\n`);
436
+ }
437
+ newlineIndex = buffer.indexOf("\n");
438
+ }
439
+ });
440
+ process.stdin.on("end", () => {
441
+ const remainder = buffer.trim();
442
+ if (remainder !== "") {
443
+ const response = handleLine(remainder);
444
+ if (response !== null)
445
+ process.stdout.write(`${response}\n`);
446
+ }
447
+ process.exit(0);
448
+ });
449
+ }