Simplify runtime checks and MCP smokes

This commit is contained in:
2026-06-25 09:19:26 -07:00
parent 99881b608b
commit 8da552bea1
13 changed files with 476 additions and 465 deletions

View File

@@ -42,22 +42,26 @@ fail() {
load_env_file
DEFAULT_DATA_DIR="${HOME:-${PWD}}/.local/share/context-kit"
if [[ -z "${CONTEXT_KIT_DATA_DIR:-}" && -z "${HOME:-}" ]]; then
fail "HOME or CONTEXT_KIT_DATA_DIR must be set"
fi
DEFAULT_DATA_DIR="${HOME:-}/.local/share/context-kit"
PROJECT="${CONTEXT_KIT_COMPOSE_PROJECT:-context-kit}"
COMPOSE_FILE="${ROOT}/compose.yml"
DATA_DIR="${CONTEXT_KIT_DATA_DIR:-${DEFAULT_DATA_DIR}}"
NETWORK="${CONTEXT_KIT_DOCKER_NETWORK:-${PROJECT}_default}"
NETWORK="${PROJECT}_default"
SEARXNG_PORT="${CONTEXT_KIT_SEARXNG_PORT:-8099}"
DOCS_PORT="${CONTEXT_KIT_DOCS_PORT:-8776}"
DOCS_HTTP_URL="${CONTEXT_KIT_DOCS_HTTP_URL:-http://127.0.0.1:${DOCS_PORT}/mcp}"
WEB_SEARCH_MAX_BYTES="${CONTEXT_KIT_WEB_SEARCH_MAX_BYTES:-52428800}"
WEB_SEARCH_PROVIDER="${CONTEXT_KIT_WEB_SEARCH_PROVIDER:-${DEFAULT_SEARCH_PROVIDER:-searxng}}"
WEB_SEARCH_HTTP_TIMEOUT="${CONTEXT_KIT_WEB_SEARCH_HTTP_TIMEOUT:-${HTTP_TIMEOUT:-15000}}"
WEB_SEARCH_MAX_RESULTS="${CONTEXT_KIT_WEB_SEARCH_MAX_RESULTS:-${MAX_RESULTS:-10}}"
WEB_SEARCH_CHROME_PATH="${CONTEXT_KIT_WEB_SEARCH_CHROME_PATH:-${CHROME_PATH:-/usr/bin/chromium}}"
WEB_SEARCH_BROWSER_USER_AGENT="${CONTEXT_KIT_WEB_SEARCH_BROWSER_USER_AGENT:-${BROWSER_SEARCH_USER_AGENT:-}}"
WEB_SEARCH_MCP_COMPAT_MODE="${CONTEXT_KIT_WEB_SEARCH_MCP_COMPAT_MODE:-${MCP_COMPAT_MODE:-}}"
DOCS_CONTAINER_NAME="context-kit-docs-mcp"
WEB_SEARCH_PROVIDER="${CONTEXT_KIT_WEB_SEARCH_PROVIDER:-searxng}"
WEB_SEARCH_HTTP_TIMEOUT="${CONTEXT_KIT_WEB_SEARCH_HTTP_TIMEOUT:-15000}"
WEB_SEARCH_MAX_RESULTS="${CONTEXT_KIT_WEB_SEARCH_MAX_RESULTS:-10}"
WEB_SEARCH_CHROME_PATH="${CONTEXT_KIT_WEB_SEARCH_CHROME_PATH:-/usr/bin/chromium}"
WEB_SEARCH_BROWSER_USER_AGENT="${CONTEXT_KIT_WEB_SEARCH_BROWSER_USER_AGENT:-}"
WEB_SEARCH_MCP_COMPAT_MODE="${CONTEXT_KIT_WEB_SEARCH_MCP_COMPAT_MODE:-}"
DOCS_SERVICE_NAME="docs-mcp"
DOCS_SOURCES_FILE="${DATA_DIR}/docs-sources.txt"
DOCS_DATA_DIR="${DATA_DIR}/docs"
MODELS_DATA_DIR="${DATA_DIR}/models"
@@ -108,10 +112,18 @@ compose() {
CONTEXT_KIT_DOCS_PREINDEX="${CONTEXT_KIT_DOCS_PREINDEX:-0}" \
CONTEXT_KIT_DOCS_LOCAL_SOURCES_DIR="${DOCS_LOCAL_SOURCES_DIR}" \
CONTEXT_KIT_DOCS_LOCAL_SOURCES_PORT="${DOCS_LOCAL_SOURCES_PORT}" \
BUILDX_BUILDER="${CONTEXT_KIT_BUILDX_BUILDER:-${BUILDX_BUILDER:-default}}" \
CONTEXT_KIT_WEB_SEARCH_IMAGE="${WEB_SEARCH_IMAGE}" \
CONTEXT_KIT_DOCS_IMAGE="${DOCS_IMAGE}" \
BUILDX_BUILDER="${CONTEXT_KIT_BUILDX_BUILDER:-default}" \
docker compose -p "${PROJECT}" -f "${COMPOSE_FILE}" "$@"
}
require_no_args() {
local usage_text="$1"
shift
[[ "$#" -eq 0 ]] || fail "${usage_text}"
}
write_docs_sources_file() {
mkdir -p "$(dirname "${DOCS_SOURCES_FILE}")"
local tmp="${DOCS_SOURCES_FILE}.tmp.$$"
@@ -152,41 +164,6 @@ check_data_dirs() {
return "${ok}"
}
check_web_search_schema_patch() {
docker run --rm --entrypoint node \
-e MAX_BYTES="${WEB_SEARCH_MAX_BYTES}" \
-e EXPECTED_MAX_BYTES="${WEB_SEARCH_MAX_BYTES}" \
"${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;
const serverPath = "/usr/local/lib/node_modules/@zhafron/mcp-web-search/dist/src/server.js";
const source = fs.readFileSync(serverPath, "utf8");
if (actual !== expected) process.exit(1);
if (!source.includes("max_download_bytes: z.number().int().min(1).max(MAX_BYTES).optional()")) process.exit(1);
' >/dev/null 2>&1
}
check_web_search_bing_override() {
docker run --rm --entrypoint node \
"${WEB_SEARCH_IMAGE}" \
-e '
const fs = require("node:fs");
const bingPath = "/usr/local/lib/node_modules/@zhafron/mcp-web-search/dist/src/providers/bing.js";
const source = fs.readFileSync(bingPath, "utf8");
if (!source.includes("Context Kit override for @zhafron/mcp-web-search 1.3.0")) process.exit(1);
if (!source.includes("waitForSelector")) process.exit(1);
if (!source.includes("decodeBingRedirect")) process.exit(1);
' >/dev/null 2>&1
}
check_web_search_chrome() {
docker run --rm --entrypoint /usr/bin/test \
"${WEB_SEARCH_IMAGE}" \
-x "${WEB_SEARCH_CHROME_PATH}" >/dev/null 2>&1
}
warn() {
printf 'warn: %s\n' "$*" >&2
}
@@ -239,21 +216,35 @@ wait_for_searxng() {
done
warn "SearXNG did not become ready on 127.0.0.1:${SEARXNG_PORT} after 30s"
return 1
}
docs_service_running() {
local container_id
container_id="$(compose ps -q "${DOCS_SERVICE_NAME}" 2>/dev/null || true)"
[[ -n "${container_id}" ]] || return 1
docker inspect -f '{{.State.Running}}' "${container_id}" 2>/dev/null | grep -qx true
}
wait_for_docs_mcp() {
command -v curl >/dev/null 2>&1 || return 0
# First-run can take a while: model download + full preindex of every source.
local attempt
# First run can take a while: model download plus optional eager preindexing.
local attempt http_ready=0
for attempt in {1..180}; do
if curl -fsS -o /dev/null "http://127.0.0.1:${DOCS_PORT}/status" 2>/dev/null; then
return 0
http_ready=1
break
fi
sleep 1
done
warn "docs-mcp did not become ready on 127.0.0.1:${DOCS_PORT} after 180s (check: docker logs ${DOCS_CONTAINER_NAME})"
if [[ "${http_ready}" -ne 1 ]]; then
warn "docs-mcp did not become ready on 127.0.0.1:${DOCS_PORT} after 180s (check: docker compose logs ${DOCS_SERVICE_NAME})"
return 1
fi
return 0
}
abs_dir() {
@@ -293,7 +284,7 @@ resolved_sources() {
}
cmd_build() {
[[ "$#" -eq 0 ]] || fail "usage: context-kit build"
require_no_args "usage: context-kit build" "$@"
require_docker
# web-search-mcp is still profile-gated (built but not auto-started);
# docs-mcp is a regular long-lived service so it builds without a profile.
@@ -303,6 +294,7 @@ cmd_build() {
}
cmd_start() {
require_no_args "usage: context-kit start" "$@"
require_docker
prepare_data_dirs
if ! docker image inspect "${WEB_SEARCH_IMAGE}" >/dev/null 2>&1 || ! docker image inspect "${DOCS_IMAGE}" >/dev/null 2>&1; then
@@ -315,11 +307,19 @@ cmd_start() {
}
cmd_stop() {
require_no_args "usage: context-kit stop" "$@"
require_docker
compose stop searxng docs-mcp
}
cmd_restart() {
require_no_args "usage: context-kit restart" "$@"
cmd_stop
cmd_start
}
cmd_status() {
require_no_args "usage: context-kit status" "$@"
require_docker
printf 'Services\n'
compose ps
@@ -327,9 +327,9 @@ cmd_status() {
docker image ls --format '{{.Repository}}:{{.Tag}}\t{{.Size}}' \
| grep -E '^(context-kit/|ghcr.io/yamadashy/repomix:)' || true
printf '\nActive per-call MCP containers\n'
docker ps -a --filter label=dev.context-kit=true --format '{{.Names}}\t{{.Status}}\t{{.Image}}\t{{.Command}}' \
| awk 'BEGIN { print "NAMES\tSTATUS\tIMAGE\tCOMMAND" } $1 !~ /^context-kit-(docs-mcp|searxng-1)$/ { print }'
printf '\nDocs MCP endpoint\n- %s (container: %s)\n' "${DOCS_HTTP_URL}" "${DOCS_CONTAINER_NAME}"
docker ps -a --filter label=dev.context-kit=true --format '{{.Names}}\t{{.Status}}\t{{.Image}}\t{{.Command}}\t{{.Label "com.docker.compose.service"}}' \
| awk -F '\t' 'BEGIN { print "NAMES\tSTATUS\tIMAGE\tCOMMAND" } $5 !~ /^(searxng|docs-mcp)$/ { print $1 "\t" $2 "\t" $3 "\t" $4 }'
printf '\nDocs MCP endpoint\n- %s (service: %s)\n' "${DOCS_HTTP_URL}" "${DOCS_SERVICE_NAME}"
printf '\nDocs sources\n'
resolved_sources | sed 's/^/- /'
printf '\nLocal docs source directory\n- %s (served inside docs-mcp at http://127.0.0.1:%s/)\n' "${DOCS_LOCAL_SOURCES_DIR}" "${DOCS_LOCAL_SOURCES_PORT}"
@@ -337,6 +337,7 @@ cmd_status() {
}
cmd_doctor() {
require_no_args "usage: context-kit doctor" "$@"
local ok=0
printf 'Context Kit doctor\n'
@@ -376,27 +377,6 @@ cmd_doctor() {
fi
done
if docker image inspect "${WEB_SEARCH_IMAGE}" >/dev/null 2>&1; then
if check_web_search_schema_patch; then
printf 'pass web-search fetch_url max-bytes schema patch: %s\n' "${WEB_SEARCH_MAX_BYTES}"
else
printf 'fail web-search max-bytes schema patch missing; run: context-kit build\n'
ok=1
fi
if check_web_search_bing_override; then
printf 'pass web-search Bing provider override installed\n'
else
printf 'fail web-search Bing provider override missing; run: context-kit build\n'
ok=1
fi
if check_web_search_chrome; then
printf 'pass web-search Chromium path: %s\n' "${WEB_SEARCH_CHROME_PATH}"
else
printf 'fail web-search Chromium path unavailable: %s\n' "${WEB_SEARCH_CHROME_PATH}"
ok=1
fi
fi
if command -v curl >/dev/null 2>&1 && curl -fsS "http://127.0.0.1:${SEARXNG_PORT}/healthz" >/dev/null 2>&1; then
printf 'pass SearXNG responds on 127.0.0.1:%s\n' "${SEARXNG_PORT}"
else
@@ -421,6 +401,7 @@ cmd_doctor() {
}
cmd_web_search() {
require_no_args "usage: context-kit web-search" "$@"
require_docker
require_network
require_image "${WEB_SEARCH_IMAGE}" "context-kit build"
@@ -433,7 +414,7 @@ cmd_web_search() {
"${cidfile_args[@]}" \
--network "${NETWORK}" \
-e DEFAULT_SEARCH_PROVIDER="${WEB_SEARCH_PROVIDER}" \
-e SEARXNG_URL="${SEARXNG_URL:-http://searxng:8080}" \
-e SEARXNG_URL="http://searxng:8080" \
-e CHROME_PATH="${WEB_SEARCH_CHROME_PATH}" \
-e HTTP_TIMEOUT="${WEB_SEARCH_HTTP_TIMEOUT}" \
-e MAX_BYTES="${WEB_SEARCH_MAX_BYTES}" \
@@ -444,6 +425,7 @@ cmd_web_search() {
}
cmd_docs() {
require_no_args "usage: context-kit docs" "$@"
# Prefer the `type: remote` MCP config pointing at ${DOCS_HTTP_URL}.
# This stdio entrypoint is kept for clients that cannot speak HTTP MCP:
# it spawns a thin mcp-proxy bridge per call but all calls multiplex onto
@@ -453,11 +435,11 @@ cmd_docs() {
require_network
require_image "${DOCS_IMAGE}" "context-kit build"
if ! docker ps --filter "name=^${DOCS_CONTAINER_NAME}$" --filter "status=running" --format '{{.Names}}' | grep -qx "${DOCS_CONTAINER_NAME}"; then
if ! docs_service_running; then
fail "long-lived docs-mcp not running; start it with: context-kit start"
fi
local bridge_url="http://${DOCS_CONTAINER_NAME}:8000/mcp"
local bridge_url="http://${DOCS_SERVICE_NAME}:8000/mcp"
local cidfile_args=()
if [[ -n "${CONTEXT_KIT_DOCKER_CIDFILE:-}" ]]; then
cidfile_args=(--cidfile "${CONTEXT_KIT_DOCKER_CIDFILE}")
@@ -473,6 +455,7 @@ cmd_docs() {
}
cmd_repomix() {
require_no_args "usage: context-kit repomix" "$@"
require_docker
require_image "${REPOMIX_IMAGE}" "docker pull ${REPOMIX_IMAGE}"
local dir mount_dir
@@ -557,9 +540,12 @@ JSON
cmd_install() {
local target="${1:-}"
shift || true
local option="${1:-}"
shift || true
[[ "$#" -eq 0 ]] || fail "usage: context-kit install claude|opencode [--absolute]"
case "${target}" in
opencode) print_opencode "${1:-}" ;;
claude) print_claude "${1:-}" ;;
opencode) print_opencode "${option}" ;;
claude) print_claude "${option}" ;;
*) fail "usage: context-kit install claude|opencode [--absolute]" ;;
esac
}
@@ -611,7 +597,7 @@ cmd_redaction_check() {
case "${1:-}" in
start) shift; cmd_start "$@" ;;
stop) shift; cmd_stop "$@" ;;
restart) shift; cmd_stop; cmd_start "$@" ;;
restart) shift; cmd_restart "$@" ;;
build) shift; cmd_build "$@" ;;
status) shift; cmd_status "$@" ;;
doctor) shift; cmd_doctor "$@" ;;