Fix MCP stdio native log pollution
This commit is contained in:
parent
3f055e705d
commit
3653f6015c
@ -10,6 +10,7 @@
|
||||
hashes referenced by sibling collections, and drops `vectors_vec` only
|
||||
when the scoped clear empties all vectors.
|
||||
- Hybrid search: weight RRF lists by query type so original FTS and original vector evidence get the intended 2x boost, instead of accidentally boosting the first lexical expansion. #591
|
||||
- MCP: seed llama.cpp/GGML quiet env vars before launching `qmd mcp` so native logs cannot pollute stdio JSON-RPC framing. #593
|
||||
- GPU: respect explicit `QMD_LLAMA_GPU=metal|vulkan|cuda` backend overrides instead of always using auto GPU selection. #529
|
||||
- Fix: preserve original filename case in `handelize()`. The previous
|
||||
`.toLowerCase()` call made indexed paths unreachable on case-sensitive
|
||||
|
||||
10
bin/qmd
10
bin/qmd
@ -15,6 +15,16 @@ done
|
||||
# to avoid native module ABI mismatches (e.g., better-sqlite3 compiled for bun vs node)
|
||||
DIR="$(cd -P "$(dirname "$SOURCE")/.." && pwd)"
|
||||
|
||||
# MCP stdio reserves stdout exclusively for JSON-RPC frames. node-llama-cpp
|
||||
# / llama.cpp / ggml can write native logs directly to stdout before JS-level
|
||||
# log handlers are attached, so seed the native quiet env before Node/Bun imports
|
||||
# the CLI and its LLM modules. Preserve explicit user values when provided.
|
||||
if [ "$1" = "mcp" ]; then
|
||||
export LLAMA_LOG_LEVEL="${LLAMA_LOG_LEVEL:-error}"
|
||||
export GGML_LOG_LEVEL="${GGML_LOG_LEVEL:-error}"
|
||||
export GGML_BACKEND_SILENT="${GGML_BACKEND_SILENT:-1}"
|
||||
fi
|
||||
|
||||
# Detect the package manager that installed dependencies by checking lockfiles.
|
||||
# $BUN_INSTALL is intentionally NOT checked — it only indicates that bun exists
|
||||
# on the system, not that it was used to install this package (see #361).
|
||||
|
||||
@ -6,7 +6,7 @@
|
||||
*/
|
||||
|
||||
import { describe, test, expect, beforeAll, afterAll, beforeEach } from "vitest";
|
||||
import { mkdtemp, rm, writeFile, mkdir } from "fs/promises";
|
||||
import { chmod, copyFile, mkdtemp, rm, writeFile, mkdir } from "fs/promises";
|
||||
import { existsSync, lstatSync, readFileSync, symlinkSync, writeFileSync, unlinkSync } from "fs";
|
||||
import { tmpdir } from "os";
|
||||
import { join, dirname } from "path";
|
||||
@ -1601,3 +1601,67 @@ describe("mcp http daemon", () => {
|
||||
try { unlinkSync(pidPath()); } catch {}
|
||||
});
|
||||
});
|
||||
|
||||
// =============================================================================
|
||||
// MCP stdio stdout hygiene
|
||||
// =============================================================================
|
||||
|
||||
describe("mcp stdio launcher", () => {
|
||||
test("sets native llama/ggml quiet env before Node starts so stdout stays JSON-RPC only", async () => {
|
||||
const tempPackage = await mkdtemp(join(tmpdir(), "qmd-bin-mcp-"));
|
||||
try {
|
||||
await mkdir(join(tempPackage, "bin"), { recursive: true });
|
||||
await mkdir(join(tempPackage, "dist", "cli"), { recursive: true });
|
||||
await mkdir(join(tempPackage, "fake-bin"), { recursive: true });
|
||||
|
||||
const qmdBin = join(tempPackage, "bin", "qmd");
|
||||
await copyFile(join(projectRoot, "bin", "qmd"), qmdBin);
|
||||
await chmod(qmdBin, 0o755);
|
||||
|
||||
// Force the wrapper down the Node branch, then put our fake `node` first
|
||||
// in PATH. The fake node behaves like the native llama/ggml layer: it
|
||||
// writes a non-JSON stdout line unless qmd pre-seeded the documented
|
||||
// quiet env vars before launching JS.
|
||||
await writeFile(join(tempPackage, "package-lock.json"), "{}\n");
|
||||
const fakeNode = join(tempPackage, "fake-bin", "node");
|
||||
await writeFile(fakeNode, `#!/bin/sh
|
||||
if [ "\${GGML_BACKEND_SILENT:-}" != "1" ]; then
|
||||
printf 'llama.cpp native log on stdout\\n'
|
||||
fi
|
||||
printf '{"jsonrpc":"2.0","id":1,"result":{"ok":true}}\\n'
|
||||
`);
|
||||
await chmod(fakeNode, 0o755);
|
||||
|
||||
const proc = spawn(qmdBin, ["mcp"], {
|
||||
cwd: tempPackage,
|
||||
env: {
|
||||
...process.env,
|
||||
PATH: `${join(tempPackage, "fake-bin")}:${process.env.PATH}`,
|
||||
LLAMA_LOG_LEVEL: "",
|
||||
GGML_LOG_LEVEL: "",
|
||||
GGML_BACKEND_SILENT: "",
|
||||
},
|
||||
stdio: ["ignore", "pipe", "pipe"],
|
||||
});
|
||||
|
||||
let stdout = "";
|
||||
let stderr = "";
|
||||
proc.stdout?.on("data", (chunk: Buffer) => { stdout += chunk.toString(); });
|
||||
proc.stderr?.on("data", (chunk: Buffer) => { stderr += chunk.toString(); });
|
||||
const exitCode = await new Promise<number>((resolve, reject) => {
|
||||
proc.once("error", reject);
|
||||
proc.on("close", (code) => resolve(code ?? 1));
|
||||
});
|
||||
|
||||
expect(exitCode).toBe(0);
|
||||
expect(stderr).toBe("");
|
||||
const lines = stdout.trim().split("\n").filter(Boolean);
|
||||
expect(lines.length).toBeGreaterThan(0);
|
||||
for (const line of lines) {
|
||||
expect(() => JSON.parse(line)).not.toThrow();
|
||||
}
|
||||
} finally {
|
||||
await rm(tempPackage, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
Loading…
Reference in New Issue
Block a user