Simplify runtime checks and MCP smokes
This commit is contained in:
@@ -1,170 +1,42 @@
|
||||
import { spawn, spawnSync } from "node:child_process";
|
||||
import { existsSync, mkdtempSync, readFileSync, rmSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { requireToolSuccess, runSmoke } from "./mcp-smoke-client.mjs";
|
||||
|
||||
const command = process.argv[2];
|
||||
const args = process.argv.slice(3);
|
||||
const live = process.env.CONTEXT_KIT_LIVE_CHECKS === "1";
|
||||
|
||||
if (!command) {
|
||||
throw new Error("usage: node scripts/smoke-docs.mjs <command> [args...]");
|
||||
}
|
||||
runSmoke({
|
||||
usage: "usage: node scripts/smoke-docs.mjs <command> [args...]",
|
||||
tmpPrefix: "context-kit-docs-smoke-",
|
||||
timeoutMs: 300000,
|
||||
clientInfo: { name: "context-kit-docs-smoke", version: "0.0.0" },
|
||||
scenario: async client => {
|
||||
const toolNames = await client.requireTools(["docs_query", "docs_sources"]);
|
||||
|
||||
const tmpDir = mkdtempSync(join(tmpdir(), "context-kit-docs-smoke-"));
|
||||
const cidFile = join(tmpDir, "container.cid");
|
||||
|
||||
const child = spawn(command, args, {
|
||||
cwd: new URL("..", import.meta.url).pathname,
|
||||
env: { ...process.env, CONTEXT_KIT_DOCKER_CIDFILE: cidFile },
|
||||
stdio: ["pipe", "pipe", "pipe"]
|
||||
});
|
||||
|
||||
let nextId = 1;
|
||||
const pending = new Map();
|
||||
let stdoutBuffer = "";
|
||||
let stderrBuffer = "";
|
||||
let childExited = false;
|
||||
|
||||
child.once("exit", (code, signal) => {
|
||||
childExited = true;
|
||||
if (pending.size > 0) {
|
||||
const error = new Error(`MCP child exited before responding (code=${code}, signal=${signal}). stderr: ${stderrBuffer.slice(-2000)}`);
|
||||
for (const { reject } of pending.values()) reject(error);
|
||||
pending.clear();
|
||||
}
|
||||
});
|
||||
|
||||
function stopChild() {
|
||||
return new Promise(resolve => {
|
||||
if (childExited) {
|
||||
stopContainer();
|
||||
rmSync(tmpDir, { recursive: true, force: true });
|
||||
resolve();
|
||||
return;
|
||||
const sources = requireToolSuccess("docs_sources", await client.callTool("docs_sources"));
|
||||
if (!Array.isArray(sources?.structuredContent?.result)) {
|
||||
const sourcesText = JSON.stringify(sources);
|
||||
throw new Error(`docs_sources returned unexpected payload: ${sourcesText.slice(0, 500)}`);
|
||||
}
|
||||
|
||||
child.stdin.end();
|
||||
const stopTimer = setTimeout(() => {
|
||||
stopContainer();
|
||||
}, 1000);
|
||||
const termTimer = setTimeout(() => {
|
||||
if (!childExited) child.kill("SIGTERM");
|
||||
}, 3000);
|
||||
const killTimer = setTimeout(() => {
|
||||
if (!childExited) child.kill("SIGKILL");
|
||||
}, 6000);
|
||||
const result = {
|
||||
tools: Array.from(toolNames).sort(),
|
||||
docs_sources: "pass"
|
||||
};
|
||||
|
||||
child.once("exit", () => {
|
||||
stopContainer();
|
||||
clearTimeout(stopTimer);
|
||||
clearTimeout(termTimer);
|
||||
clearTimeout(killTimer);
|
||||
rmSync(tmpDir, { recursive: true, force: true });
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function stopContainer() {
|
||||
if (!existsSync(cidFile)) return;
|
||||
const containerId = readFileSync(cidFile, "utf8").trim();
|
||||
if (!containerId) return;
|
||||
spawnSync("docker", ["stop", containerId], { stdio: "ignore" });
|
||||
}
|
||||
|
||||
const timeout = setTimeout(async () => {
|
||||
await stopChild();
|
||||
console.error(`Docs MCP smoke timed out. stderr: ${stderrBuffer.slice(-2000)}`);
|
||||
process.exit(1);
|
||||
}, 300000);
|
||||
|
||||
child.stderr.on("data", chunk => {
|
||||
stderrBuffer += chunk.toString();
|
||||
});
|
||||
|
||||
child.stdout.on("data", chunk => {
|
||||
stdoutBuffer += chunk.toString();
|
||||
let newline;
|
||||
while ((newline = stdoutBuffer.indexOf("\n")) >= 0) {
|
||||
const line = stdoutBuffer.slice(0, newline).trim();
|
||||
stdoutBuffer = stdoutBuffer.slice(newline + 1);
|
||||
if (!line) continue;
|
||||
let message;
|
||||
try {
|
||||
message = JSON.parse(line);
|
||||
} catch {
|
||||
continue;
|
||||
}
|
||||
if (message.id && pending.has(message.id)) {
|
||||
const { resolve, reject } = pending.get(message.id);
|
||||
pending.delete(message.id);
|
||||
if (message.error) reject(new Error(JSON.stringify(message.error)));
|
||||
else resolve(message.result);
|
||||
if (live) {
|
||||
const query = requireToolSuccess("docs_query", await client.callTool("docs_query", {
|
||||
query: "Model Context Protocol documentation",
|
||||
limit: 3,
|
||||
auto_retrieve: true,
|
||||
auto_retrieve_threshold: 0.1,
|
||||
auto_retrieve_limit: 1,
|
||||
max_bytes: 12000
|
||||
}));
|
||||
const queryText = JSON.stringify(query);
|
||||
if (!queryText.includes("search_results") && !queryText.includes("Model Context Protocol")) {
|
||||
throw new Error(`docs_query returned unexpected payload: ${queryText.slice(0, 500)}`);
|
||||
}
|
||||
result.docs_query = "pass";
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
});
|
||||
|
||||
function request(method, params = {}) {
|
||||
if (childExited) {
|
||||
return Promise.reject(new Error(`MCP child already exited. stderr: ${stderrBuffer.slice(-2000)}`));
|
||||
}
|
||||
const id = nextId++;
|
||||
child.stdin.write(`${JSON.stringify({ jsonrpc: "2.0", id, method, params })}\n`);
|
||||
return new Promise((resolve, reject) => pending.set(id, { resolve, reject }));
|
||||
}
|
||||
|
||||
function notify(method, params = {}) {
|
||||
child.stdin.write(`${JSON.stringify({ jsonrpc: "2.0", method, params })}\n`);
|
||||
}
|
||||
|
||||
async function callTool(name, args = {}) {
|
||||
return request("tools/call", { name, arguments: args });
|
||||
}
|
||||
|
||||
try {
|
||||
await request("initialize", {
|
||||
protocolVersion: "2024-11-05",
|
||||
capabilities: {},
|
||||
clientInfo: { name: "context-kit-docs-smoke", version: "0.0.0" }
|
||||
});
|
||||
notify("notifications/initialized");
|
||||
|
||||
const listed = await request("tools/list");
|
||||
const toolNames = new Set((listed.tools || []).map(tool => tool.name));
|
||||
for (const name of ["docs_query", "docs_sources"]) {
|
||||
if (!toolNames.has(name)) throw new Error(`missing tool: ${name}`);
|
||||
}
|
||||
|
||||
const sources = await callTool("docs_sources");
|
||||
const sourcesText = JSON.stringify(sources);
|
||||
if (sources.isError) {
|
||||
throw new Error(`docs_sources returned an error: ${sourcesText.slice(0, 500)}`);
|
||||
}
|
||||
|
||||
const query = await callTool("docs_query", {
|
||||
query: "Model Context Protocol documentation",
|
||||
limit: 3,
|
||||
auto_retrieve: true,
|
||||
auto_retrieve_threshold: 0.1,
|
||||
auto_retrieve_limit: 1,
|
||||
max_bytes: 12000
|
||||
});
|
||||
const queryText = JSON.stringify(query);
|
||||
if (!queryText.includes("search_results") && !queryText.includes("Model Context Protocol")) {
|
||||
throw new Error(`docs_query returned unexpected payload: ${queryText.slice(0, 500)}`);
|
||||
}
|
||||
|
||||
clearTimeout(timeout);
|
||||
await stopChild();
|
||||
console.log(JSON.stringify({
|
||||
tools: Array.from(toolNames).sort(),
|
||||
docs_sources: "pass",
|
||||
docs_query: "pass"
|
||||
}, null, 2));
|
||||
} catch (error) {
|
||||
clearTimeout(timeout);
|
||||
await stopChild();
|
||||
console.error(error.message);
|
||||
if (stderrBuffer) console.error(stderrBuffer.slice(-4000));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user