Harden web search and docs defaults
This commit is contained in:
49
scripts/release-check
Executable file
49
scripts/release-check
Executable file
@@ -0,0 +1,49 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
cd "${ROOT}"
|
||||
|
||||
tmp_dir="$(mktemp -d)"
|
||||
cleanup() {
|
||||
rm -rf "${tmp_dir}"
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
check_node() {
|
||||
local file
|
||||
for file in "$@"; do
|
||||
node --check "${file}"
|
||||
done
|
||||
}
|
||||
|
||||
git diff --check
|
||||
git ls-files --cached --error-unmatch \
|
||||
docker/web-search/patch-mcp-web-search.mjs \
|
||||
docker/web-search/overrides/bing.js \
|
||||
docker/docs/constraints.txt \
|
||||
scripts/smoke-web-search.mjs \
|
||||
scripts/release-check >/dev/null
|
||||
bash -n bin/context-kit
|
||||
bash -n scripts/release-check
|
||||
sh -n docker/docs/entrypoint.sh
|
||||
check_node docker/web-search/patch-mcp-web-search.mjs docker/web-search/overrides/bing.js scripts/smoke-web-search.mjs
|
||||
|
||||
node -e 'const fs=require("node:fs"); JSON.parse(fs.readFileSync("snippets/opencode.json", "utf8")); JSON.parse(fs.readFileSync("snippets/claude.mcp.json", "utf8"));'
|
||||
bin/context-kit install opencode > "${tmp_dir}/opencode.json"
|
||||
bin/context-kit install opencode --absolute > "${tmp_dir}/opencode-absolute.json"
|
||||
bin/context-kit install claude > "${tmp_dir}/claude.json"
|
||||
bin/context-kit install claude --absolute > "${tmp_dir}/claude-absolute.json"
|
||||
node -e 'const fs=require("node:fs"); for (const file of process.argv.slice(1)) JSON.parse(fs.readFileSync(file, "utf8"));' \
|
||||
"${tmp_dir}/opencode.json" \
|
||||
"${tmp_dir}/opencode-absolute.json" \
|
||||
"${tmp_dir}/claude.json" \
|
||||
"${tmp_dir}/claude-absolute.json"
|
||||
|
||||
bin/context-kit redaction-check
|
||||
docker compose -p context-kit -f compose.yml config >/dev/null
|
||||
bin/context-kit build
|
||||
bin/context-kit doctor
|
||||
node scripts/smoke-web-search.mjs bin/context-kit web-search
|
||||
|
||||
printf 'pass release-check\n'
|
||||
152
scripts/smoke-web-search.mjs
Normal file
152
scripts/smoke-web-search.mjs
Normal file
@@ -0,0 +1,152 @@
|
||||
import { spawn } from "node:child_process";
|
||||
|
||||
const command = process.argv[2];
|
||||
const args = process.argv.slice(3);
|
||||
|
||||
if (!command) {
|
||||
throw new Error("usage: node scripts/smoke-web-search.mjs <command> [args...]");
|
||||
}
|
||||
|
||||
const child = spawn(command, args, {
|
||||
cwd: new URL("..", import.meta.url).pathname,
|
||||
env: process.env,
|
||||
stdio: ["pipe", "pipe", "pipe"]
|
||||
});
|
||||
|
||||
let nextId = 1;
|
||||
const pending = new Map();
|
||||
let stdoutBuffer = "";
|
||||
let stderrBuffer = "";
|
||||
|
||||
function stopChild() {
|
||||
child.stdin.end();
|
||||
child.kill("SIGTERM");
|
||||
const killTimer = setTimeout(() => child.kill("SIGKILL"), 3000);
|
||||
return new Promise(resolve => {
|
||||
child.once("exit", () => {
|
||||
clearTimeout(killTimer);
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
const timeout = setTimeout(async () => {
|
||||
await stopChild();
|
||||
console.error(`MCP smoke timed out. stderr: ${stderrBuffer.slice(-2000)}`);
|
||||
process.exit(1);
|
||||
}, 120000);
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
function request(method, params = {}) {
|
||||
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`);
|
||||
}
|
||||
|
||||
function textFrom(result) {
|
||||
return (result.content || [])
|
||||
.filter(part => part.type === "text")
|
||||
.map(part => part.text)
|
||||
.join("\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-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 ["search_web", "fetch_url"]) {
|
||||
if (!toolNames.has(name)) throw new Error(`missing tool: ${name}`);
|
||||
}
|
||||
|
||||
const searxng = textFrom(await callTool("search_web", {
|
||||
q: "Model Context Protocol",
|
||||
limit: 2,
|
||||
provider: "searxng"
|
||||
}));
|
||||
if (!searxng.includes("Model")) throw new Error(`SearXNG smoke returned unexpected text: ${searxng.slice(0, 500)}`);
|
||||
|
||||
const bing = textFrom(await callTool("search_web", {
|
||||
q: "Model Context Protocol",
|
||||
limit: 2,
|
||||
provider: "bing"
|
||||
}));
|
||||
if (!bing.includes("Model")) throw new Error(`Bing smoke returned unexpected text: ${bing.slice(0, 500)}`);
|
||||
|
||||
const fetch = textFrom(await callTool("fetch_url", {
|
||||
url: "https://example.com/",
|
||||
format: "markdown",
|
||||
max_download_bytes: 52428800
|
||||
}));
|
||||
if (!fetch.includes("Example Domain")) throw new Error(`fetch smoke returned unexpected text: ${fetch.slice(0, 500)}`);
|
||||
|
||||
const browserFetch = textFrom(await callTool("fetch_url", {
|
||||
url: "https://example.com/",
|
||||
format: "markdown",
|
||||
engine: "browser",
|
||||
max_download_bytes: 52428800
|
||||
}));
|
||||
if (!browserFetch.includes("Example Domain")) throw new Error(`browser fetch smoke returned unexpected text: ${browserFetch.slice(0, 500)}`);
|
||||
|
||||
const localResult = await callTool("fetch_url", {
|
||||
url: "http://127.0.0.1:1/",
|
||||
max_download_bytes: 52428800
|
||||
});
|
||||
const localBlocked = Boolean(localResult.isError) && textFrom(localResult).includes("Blocked localhost/private URL");
|
||||
if (!localBlocked) throw new Error("localhost/private URL was not blocked as expected");
|
||||
|
||||
clearTimeout(timeout);
|
||||
await stopChild();
|
||||
console.log(JSON.stringify({
|
||||
tools: Array.from(toolNames).sort(),
|
||||
searxng: "pass",
|
||||
bing: "pass",
|
||||
fetch_url: "pass",
|
||||
fetch_url_browser_engine_currently_http: "pass",
|
||||
localhost_guard: "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