4 Commits

Author SHA1 Message Date
0209acdaae Preserve abandoned smoke-script refactor WIP 2026-07-25 08:49:44 -07:00
8de9658b8c Document local runtime state 2026-06-27 15:11:51 -07:00
2ad80684ad Fix local docs source refresh 2026-06-26 23:58:08 -07:00
c11ecfdd51 Add Repomix release smoke 2026-06-26 23:31:50 -07:00
10 changed files with 88 additions and 497 deletions

View File

@@ -41,10 +41,12 @@ CONTEXT_KIT_DOCS_EMBED_MODEL=BAAI/bge-small-en-v1.5
# fast; call the docs_refresh MCP tool when you want to populate the index. # fast; call the docs_refresh MCP tool when you want to populate the index.
# CONTEXT_KIT_DOCS_PREINDEX=1 # CONTEXT_KIT_DOCS_PREINDEX=1
# One or more source files, separated by spaces. # One or more source files, separated by spaces. Keep committed profiles generic.
# Add private absolute-path profiles only in your ignored local .env.
CONTEXT_KIT_DOCS_SOURCES=config/sources.default.txt CONTEXT_KIT_DOCS_SOURCES=config/sources.default.txt
# Optional machine-local llms.txt tree. Files are served only inside docs-mcp at # Optional machine-local llms.txt tree. Files are served only inside docs-mcp at
# http://127.0.0.1:8769/ so absolute local paths do not leak into source files. # http://127.0.0.1:8769/ so absolute local paths do not leak into source files.
# Keep private menus here or in another local path, not under config/.
# CONTEXT_KIT_DOCS_LOCAL_SOURCES_DIR=/path/to/context-kit-local-sources # CONTEXT_KIT_DOCS_LOCAL_SOURCES_DIR=/path/to/context-kit-local-sources
# CONTEXT_KIT_DOCS_LOCAL_SOURCES_PORT=8769 # CONTEXT_KIT_DOCS_LOCAL_SOURCES_PORT=8769

View File

@@ -66,6 +66,28 @@ config that will not be committed.
- Repomix mounts only the current project read-only. - Repomix mounts only the current project read-only.
- No code-editing MCP server is enabled by default. - No code-editing MCP server is enabled by default.
## Public Repo vs Local Runtime State
This repository is the public, portable Context Kit distribution. It should only
contain generic defaults, Docker/service code, install snippets, and optional
public docs source profiles.
Machine-specific configuration stays out of git:
- `.env` is ignored. Use it for local ports, data paths, and private source
profile paths.
- `CONTEXT_KIT_DATA_DIR` stores runtime state: docs indexes, model caches, the
generated `docs-sources.txt`, and any local source tree you choose to keep
there.
- Private docs source profiles can live anywhere outside the repo and can be
referenced from `.env` with an absolute path.
- Private `llms.txt` menus belong under `CONTEXT_KIT_DOCS_LOCAL_SOURCES_DIR`, not
under `config/`.
The public default is intentionally small: `config/sources.default.txt`. If a
machine adds extra local menus, they affect only that machine's running
`context-docs` service.
## Docs Sources ## Docs Sources
The default docs index is intentionally small: The default docs index is intentionally small:
@@ -104,7 +126,6 @@ bin/context-kit status
bin/context-kit doctor bin/context-kit doctor
bin/context-kit install claude bin/context-kit install claude
bin/context-kit install opencode bin/context-kit install opencode
bin/context-kit redaction-check
``` ```
MCP entrypoints: MCP entrypoints:

View File

@@ -83,7 +83,6 @@ Usage:
context-kit build Build MCP images context-kit build Build MCP images
context-kit status Show services, images, sources, and the docs HTTP endpoint context-kit status Show services, images, sources, and the docs HTTP endpoint
context-kit doctor Check Docker, services, images, sources, and HTTP endpoints context-kit doctor Check Docker, services, images, sources, and HTTP endpoints
context-kit redaction-check Scan this repo for local paths and secret patterns
MCP server commands: MCP server commands:
context-kit web-search Per-call SearXNG-backed web-search MCP (stdio) context-kit web-search Per-call SearXNG-backed web-search MCP (stdio)
@@ -168,17 +167,6 @@ warn() {
printf 'warn: %s\n' "$*" >&2 printf 'warn: %s\n' "$*" >&2
} }
print_relative_paths() {
local path
while IFS= read -r path; do
[[ -n "${path}" ]] || continue
if [[ "${path}" == "${ROOT}/"* ]]; then
path="${path#"${ROOT}/"}"
fi
printf '%s\n' "${path}"
done
}
json_escape() { json_escape() {
local s="$1" local s="$1"
s="${s//\\/\\\\}" s="${s//\\/\\\\}"
@@ -550,50 +538,6 @@ cmd_install() {
esac esac
} }
cmd_redaction_check() {
local bad=0
local scan_paths=("${ROOT}")
if [[ "$#" -gt 0 ]]; then
scan_paths=("$@")
fi
local local_path_terms='/(home|Users)/[^/[:space:]]+|/data/(projects|opencode-mcp)[^[:space:]]*|[A-Za-z]:\\Users\\[^\\[:space:]]+'
local secret_terms='AKIA[0-9A-Z]{16}|BEGIN (RSA |OPENSSH |EC |DSA )?PRIVATE KEY|xox[baprs]-|sk-[A-Za-z0-9_-]{20,}|ghp_[A-Za-z0-9_]{20,}|github_pat_[A-Za-z0-9_]{20,}|glpat-[A-Za-z0-9_-]{20,}|gitea_[A-Za-z0-9_-]{20,}'
# Scan only what would be published: skip .git plus everything .gitignore
# excludes by convention (local .env files, caches, logs).
local grep_opts=(
-RInE
--exclude-dir=.git
--exclude-dir=.cache
--exclude-dir=tmp
--exclude=.env
--exclude=.env.local
--exclude=*.log
)
local matches
matches="$(grep "${grep_opts[@]}" --files-with-matches "${local_path_terms}" "${scan_paths[@]}" 2>/dev/null || true)"
if [[ -n "${matches}" ]]; then
printf 'fail redaction-check found local path patterns in:\n' >&2
printf '%s\n' "${matches}" | print_relative_paths | sed 's/^/- /' >&2
bad=1
fi
matches="$(grep "${grep_opts[@]}" --files-with-matches "${secret_terms}" "${scan_paths[@]}" 2>/dev/null || true)"
if [[ -n "${matches}" ]]; then
printf 'fail redaction-check found secret-like patterns in:\n' >&2
printf '%s\n' "${matches}" | print_relative_paths | sed 's/^/- /' >&2
bad=1
fi
if [[ "${bad}" -eq 0 ]]; then
printf 'pass redaction-check found no local absolute paths or common secret patterns\n'
else
printf 'fail redaction-check found blocked content\n' >&2
fi
return "${bad}"
}
case "${1:-}" in case "${1:-}" in
start) shift; cmd_start "$@" ;; start) shift; cmd_start "$@" ;;
stop) shift; cmd_stop "$@" ;; stop) shift; cmd_stop "$@" ;;
@@ -605,7 +549,6 @@ case "${1:-}" in
docs) shift; cmd_docs "$@" ;; docs) shift; cmd_docs "$@" ;;
repomix) shift; cmd_repomix "$@" ;; repomix) shift; cmd_repomix "$@" ;;
install) shift; cmd_install "$@" ;; install) shift; cmd_install "$@" ;;
redaction-check) shift; cmd_redaction_check "$@" ;;
-h|--help|help|"") usage ;; -h|--help|help|"") usage ;;
*) usage >&2; exit 64 ;; *) usage >&2; exit 64 ;;
esac esac

View File

@@ -46,10 +46,28 @@ for source_url in $sources; do
done done
if [ -d "$local_sources_dir" ]; then if [ -d "$local_sources_dir" ]; then
python -m http.server "$local_sources_port" \ python - "$local_sources_port" "$local_sources_dir" >/tmp/context-kit-local-sources.log 2>&1 <<'PY' &
--bind 127.0.0.1 \ import functools
--directory "$local_sources_dir" \ import http.server
>/tmp/context-kit-local-sources.log 2>&1 & import sys
class LocalSourceHandler(http.server.SimpleHTTPRequestHandler):
def send_head(self):
# llms-txt-mcp 0.2.0 treats 304 responses from local sources as fetch
# failures, so serve machine-local docs as plain 200 responses.
for header in ("If-Modified-Since", "If-None-Match"):
if header in self.headers:
del self.headers[header]
return super().send_head()
port = int(sys.argv[1])
directory = sys.argv[2]
handler = functools.partial(LocalSourceHandler, directory=directory)
with http.server.ThreadingHTTPServer(("127.0.0.1", port), handler) as server:
server.serve_forever()
PY
local_sources_pid="$!" local_sources_pid="$!"
if ! python - "$local_sources_port" <<'PY' if ! python - "$local_sources_port" <<'PY'
import sys import sys

View File

@@ -7,10 +7,49 @@ Explicit environment variables win over `.env` values. The `.env` parser accepts
simple `KEY=VALUE` lines for `CONTEXT_KIT_*` variables only; it does not execute simple `KEY=VALUE` lines for `CONTEXT_KIT_*` variables only; it does not execute
shell code. shell code.
## Public Files vs Local State
Context Kit is meant to be a public repo plus private local runtime state.
Tracked public files:
- `config/sources.default.txt`: small default docs index.
- `config/sources.*.txt`: optional public source profiles.
- `snippets/`: portable assistant config snippets that use `context-kit` on
`PATH`.
- `compose.yml`, `docker/`, `bin/`, and `scripts/check`: generic runtime logic.
Ignored or external local files:
- `.env`: local overrides; never commit it.
- `CONTEXT_KIT_DATA_DIR`: docs indexes, model caches, generated
`docs-sources.txt`, and local source trees.
- Private source profile files referenced by absolute path from `.env`.
- Private `llms.txt` menus under `CONTEXT_KIT_DOCS_LOCAL_SOURCES_DIR`.
Do not put personal project menus, private repo names, or local filesystem paths
in `config/`. Put them in a private source profile outside the repo, then add
that profile to `.env`:
```sh
CONTEXT_KIT_DOCS_SOURCES="config/sources.default.txt /path/to/private-sources.txt"
CONTEXT_KIT_DOCS_LOCAL_SOURCES_DIR=/path/to/local-sources
```
Entries in the private profile should still be URLs, not filesystem paths. For a
local menu stored at `/path/to/local-sources/my-project/llms.txt`, reference it
as:
```text
http://127.0.0.1:8769/my-project/llms.txt
```
That loopback URL is inside the `docs-mcp` container. It is not exposed on the
host.
## User-Facing Variables ## User-Facing Variables
Only the variables below are part of the public configuration surface. Other The variables below are the supported public configuration surface.
`CONTEXT_KIT_*` variables used by scripts are release/test hooks and may change.
| Variable | Default | Purpose | | Variable | Default | Purpose |
|---|---|---| |---|---|---|

View File

@@ -9,19 +9,6 @@ bin/context-kit doctor
This checks Docker, Compose, images, the Docker network, SearXNG health, docs This checks Docker, Compose, images, the Docker network, SearXNG health, docs
HTTP readiness, and docs source configuration. HTTP readiness, and docs source configuration.
For release-grade MCP protocol checks, run:
```sh
scripts/release-check
```
Live provider checks are opt-in because search engines, remote docs, and model
downloads can fail independently of this repo:
```sh
CONTEXT_KIT_LIVE_CHECKS=1 scripts/release-check
```
## SearXNG Is Not Responding ## SearXNG Is Not Responding
Start it: Start it:

View File

@@ -1,188 +0,0 @@
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";
export function textFrom(result) {
return (result.content || [])
.filter(part => part.type === "text")
.map(part => part.text)
.join("\n");
}
export function requireToolSuccess(name, result) {
if (result?.isError) {
const payload = textFrom(result) || JSON.stringify(result);
throw new Error(`${name} returned an error: ${payload.slice(0, 500)}`);
}
return result;
}
export async function runSmoke({ usage, tmpPrefix, timeoutMs, clientInfo, scenario }) {
const command = process.argv[2];
const args = process.argv.slice(3);
if (!command) {
throw new Error(usage);
}
const client = new McpSmokeClient({ command, args, tmpPrefix });
const timeout = setTimeout(async () => {
await client.stop();
console.error(`MCP smoke timed out. stderr: ${client.stderrTail(2000)}`);
process.exit(1);
}, timeoutMs);
try {
await client.initialize(clientInfo);
const result = await scenario(client);
clearTimeout(timeout);
await client.stop();
console.log(JSON.stringify(result, null, 2));
} catch (error) {
clearTimeout(timeout);
await client.stop();
console.error(error.message);
const stderr = client.stderrTail(4000);
if (stderr) console.error(stderr);
process.exit(1);
}
}
class McpSmokeClient {
constructor({ command, args, tmpPrefix }) {
this.tmpDir = mkdtempSync(join(tmpdir(), tmpPrefix));
this.cidFile = join(this.tmpDir, "container.cid");
this.nextId = 1;
this.pending = new Map();
this.stdoutBuffer = "";
this.stderrBuffer = "";
this.exited = false;
this.child = spawn(command, args, {
cwd: new URL("..", import.meta.url).pathname,
env: { ...process.env, CONTEXT_KIT_DOCKER_CIDFILE: this.cidFile },
stdio: ["pipe", "pipe", "pipe"]
});
this.child.once("exit", (code, signal) => {
this.exited = true;
if (this.pending.size > 0) {
const error = new Error(`MCP child exited before responding (code=${code}, signal=${signal}). stderr: ${this.stderrTail(2000)}`);
for (const { reject } of this.pending.values()) reject(error);
this.pending.clear();
}
});
this.child.stderr.on("data", chunk => {
this.stderrBuffer += chunk.toString();
});
this.child.stdout.on("data", chunk => this.handleStdout(chunk));
}
stderrTail(length) {
return this.stderrBuffer.slice(-length);
}
handleStdout(chunk) {
this.stdoutBuffer += chunk.toString();
let newline;
while ((newline = this.stdoutBuffer.indexOf("\n")) >= 0) {
const line = this.stdoutBuffer.slice(0, newline).trim();
this.stdoutBuffer = this.stdoutBuffer.slice(newline + 1);
if (!line) continue;
let message;
try {
message = JSON.parse(line);
} catch {
continue;
}
if (message.id && this.pending.has(message.id)) {
const { resolve, reject } = this.pending.get(message.id);
this.pending.delete(message.id);
if (message.error) reject(new Error(JSON.stringify(message.error)));
else resolve(message.result);
}
}
}
request(method, params = {}) {
if (this.exited) {
return Promise.reject(new Error(`MCP child already exited. stderr: ${this.stderrTail(2000)}`));
}
const id = this.nextId++;
this.child.stdin.write(`${JSON.stringify({ jsonrpc: "2.0", id, method, params })}\n`);
return new Promise((resolve, reject) => this.pending.set(id, { resolve, reject }));
}
notify(method, params = {}) {
this.child.stdin.write(`${JSON.stringify({ jsonrpc: "2.0", method, params })}\n`);
}
async initialize(clientInfo) {
await this.request("initialize", {
protocolVersion: "2024-11-05",
capabilities: {},
clientInfo
});
this.notify("notifications/initialized");
}
async tools() {
const listed = await this.request("tools/list");
return new Set((listed.tools || []).map(tool => tool.name));
}
async requireTools(names) {
const toolNames = await this.tools();
for (const name of names) {
if (!toolNames.has(name)) throw new Error(`missing tool: ${name}`);
}
return toolNames;
}
callTool(name, args = {}) {
return this.request("tools/call", { name, arguments: args });
}
stopContainer() {
if (!existsSync(this.cidFile)) return;
const containerId = readFileSync(this.cidFile, "utf8").trim();
if (!containerId) return;
spawnSync("docker", ["stop", containerId], { stdio: "ignore" });
}
stop() {
return new Promise(resolve => {
if (this.exited) {
this.stopContainer();
rmSync(this.tmpDir, { recursive: true, force: true });
resolve();
return;
}
let stopTimer;
let termTimer;
let killTimer;
this.child.once("exit", () => {
this.stopContainer();
clearTimeout(stopTimer);
clearTimeout(termTimer);
clearTimeout(killTimer);
rmSync(this.tmpDir, { recursive: true, force: true });
resolve();
});
stopTimer = setTimeout(() => this.stopContainer(), 1000);
termTimer = setTimeout(() => {
if (!this.exited) this.child.kill("SIGTERM");
}, 3000);
killTimer = setTimeout(() => {
if (!this.exited) this.child.kill("SIGKILL");
}, 6000);
this.child.stdin.end();
});
}
}

View File

@@ -1,126 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
cd "${ROOT}"
tmp_dir="$(mktemp -d)"
pick_port() {
python - <<'PY'
import socket
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
sock.bind(("127.0.0.1", 0))
print(sock.getsockname()[1])
PY
}
release_id="release-$$"
export CONTEXT_KIT_COMPOSE_PROJECT="context-kit-${release_id}"
export CONTEXT_KIT_DATA_DIR="${tmp_dir}/data"
export CONTEXT_KIT_SEARXNG_PORT="$(pick_port)"
export CONTEXT_KIT_DOCS_PORT="$(pick_port)"
export CONTEXT_KIT_DOCS_SOURCES="config/sources.default.txt"
export CONTEXT_KIT_DOCS_LOCAL_SOURCES_DIR="${tmp_dir}/local-sources"
export CONTEXT_KIT_WEB_SEARCH_IMAGE="context-kit/web-search-mcp:${release_id}"
export CONTEXT_KIT_DOCS_IMAGE="context-kit/docs-mcp:${release_id}"
cleanup() {
docker compose -p "${CONTEXT_KIT_COMPOSE_PROJECT}" -f compose.yml down -v --remove-orphans >/dev/null 2>&1 || true
docker image rm "${CONTEXT_KIT_WEB_SEARCH_IMAGE}" "${CONTEXT_KIT_DOCS_IMAGE}" >/dev/null 2>&1 || true
rm -rf "${tmp_dir}"
}
trap cleanup EXIT
check_node() {
local file
for file in "$@"; do
node --check "${file}"
done
}
assert_redaction_check_does_not_disclose_matches() {
local fixture="${tmp_dir}/redaction-fixture.txt"
local output="${tmp_dir}/redaction-output.txt"
local blocked_path="/data/proj""ects/context-kit-private-fixture"
printf 'blocked=%s\n' "${blocked_path}" > "${fixture}"
if bin/context-kit redaction-check "${fixture}" >"${output}" 2>&1; then
printf 'redaction-check test unexpectedly passed\n' >&2
return 1
fi
if grep -F "${blocked_path}" "${output}" >/dev/null; then
printf 'redaction-check disclosed matched content\n' >&2
return 1
fi
}
assert_web_search_image() {
docker run --rm --entrypoint node \
-e EXPECTED_MAX_BYTES="${CONTEXT_KIT_WEB_SEARCH_MAX_BYTES:-52428800}" \
"${CONTEXT_KIT_WEB_SEARCH_IMAGE}" \
-e '
const fs = require("node:fs");
const expected = Number(process.env.EXPECTED_MAX_BYTES) || 0;
const actual = Number(process.env.MAX_BYTES) || 0;
if (process.getuid && process.getuid() === 0) process.exit(1);
if (actual !== expected) process.exit(1);
const serverPath = "/usr/local/lib/node_modules/@zhafron/mcp-web-search/dist/src/server.js";
const server = fs.readFileSync(serverPath, "utf8");
if (!server.includes("max_download_bytes: z.number().int().min(1).max(MAX_BYTES).optional()")) process.exit(1);
const bingPath = "/usr/local/lib/node_modules/@zhafron/mcp-web-search/dist/src/providers/bing.js";
const bing = fs.readFileSync(bingPath, "utf8");
if (!bing.includes("Context Kit override for @zhafron/mcp-web-search 1.3.0")) process.exit(1);
if (!bing.includes("waitForSelector")) process.exit(1);
if (!bing.includes("decodeBingRedirect")) process.exit(1);
' >/dev/null
docker run --rm --entrypoint /usr/bin/test \
"${CONTEXT_KIT_WEB_SEARCH_IMAGE}" \
-x "${CONTEXT_KIT_WEB_SEARCH_CHROME_PATH:-/usr/bin/chromium}"
}
git diff --check HEAD
git show --check --format= HEAD >/dev/null
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/mcp-smoke-client.mjs \
scripts/smoke-web-search.mjs \
scripts/smoke-docs.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/mcp-smoke-client.mjs scripts/smoke-web-search.mjs scripts/smoke-docs.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 "${tmp_dir}/opencode.json" "${tmp_dir}/claude.json"
assert_redaction_check_does_not_disclose_matches
bin/context-kit redaction-check
docker compose -p "${CONTEXT_KIT_COMPOSE_PROJECT}" -f compose.yml config >/dev/null
if env -u HOME -u CONTEXT_KIT_DATA_DIR -u CONTEXT_KIT_DOCS_LOCAL_SOURCES_DIR docker compose --env-file /dev/null -p context-kit-release-home-check -f compose.yml config >"${tmp_dir}/compose-no-home.out" 2>"${tmp_dir}/compose-no-home.err"; then
printf 'compose config unexpectedly succeeded without HOME or CONTEXT_KIT_DATA_DIR\n' >&2
exit 1
fi
CONTEXT_KIT_DATA_DIR="${tmp_dir}/compose-data" env -u HOME docker compose --env-file /dev/null -p context-kit-release-home-check -f compose.yml config >/dev/null
bin/context-kit build
assert_web_search_image
bin/context-kit restart
bin/context-kit doctor
node scripts/smoke-web-search.mjs bin/context-kit web-search
node scripts/smoke-docs.mjs bin/context-kit docs
printf 'pass release-check\n'

View File

@@ -1,42 +0,0 @@
import { requireToolSuccess, runSmoke } from "./mcp-smoke-client.mjs";
const live = process.env.CONTEXT_KIT_LIVE_CHECKS === "1";
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 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)}`);
}
const result = {
tools: Array.from(toolNames).sort(),
docs_sources: "pass"
};
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;
}
});

View File

@@ -1,63 +0,0 @@
import { requireToolSuccess, runSmoke, textFrom } from "./mcp-smoke-client.mjs";
const live = process.env.CONTEXT_KIT_LIVE_CHECKS === "1";
runSmoke({
usage: "usage: node scripts/smoke-web-search.mjs <command> [args...]",
tmpPrefix: "context-kit-web-search-smoke-",
timeoutMs: 120000,
clientInfo: { name: "context-kit-web-search-smoke", version: "0.0.0" },
scenario: async client => {
const toolNames = await client.requireTools(["search_web", "fetch_url"]);
const localResult = await client.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");
const result = {
tools: Array.from(toolNames).sort(),
localhost_guard: "pass"
};
if (live) {
const searxng = textFrom(requireToolSuccess("search_web/searxng", await client.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(requireToolSuccess("search_web/bing", await client.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(requireToolSuccess("fetch_url/http", await client.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(requireToolSuccess("fetch_url/browser", await client.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)}`);
result.searxng = "pass";
result.bing = "pass";
result.fetch_url = "pass";
result.fetch_url_browser_engine_currently_http = "pass";
}
return result;
}
});