Bound shared MCP container lifecycle
This commit is contained in:
384
bin/context-kit
384
bin/context-kit
@@ -48,20 +48,21 @@ fi
|
||||
|
||||
DEFAULT_DATA_DIR="${HOME:-}/.local/share/context-kit"
|
||||
PROJECT="${CONTEXT_KIT_COMPOSE_PROJECT:-context-kit}"
|
||||
[[ "${PROJECT}" =~ ^[a-z0-9][a-z0-9_-]*$ ]] || fail "invalid CONTEXT_KIT_COMPOSE_PROJECT: ${PROJECT}"
|
||||
HOST_UID="$(id -u)"
|
||||
COMPOSE_FILE="${ROOT}/compose.yml"
|
||||
DATA_DIR="${CONTEXT_KIT_DATA_DIR:-${DEFAULT_DATA_DIR}}"
|
||||
NETWORK="${PROJECT}_default"
|
||||
SEARXNG_PORT="${CONTEXT_KIT_SEARXNG_PORT:-8099}"
|
||||
WEB_SEARCH_PORT="${CONTEXT_KIT_WEB_SEARCH_PORT:-8777}"
|
||||
WEB_SEARCH_HTTP_URL="${CONTEXT_KIT_WEB_SEARCH_HTTP_URL:-http://127.0.0.1:${WEB_SEARCH_PORT}/mcp}"
|
||||
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:-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:-}"
|
||||
WEB_SEARCH_SERVICE_NAME="web-search-mcp"
|
||||
DOCS_SERVICE_NAME="docs-mcp"
|
||||
SHARED_SERVICES=(searxng "${WEB_SEARCH_SERVICE_NAME}" "${DOCS_SERVICE_NAME}")
|
||||
SNAPSHOT_IDS=()
|
||||
SNAPSHOT_RUNNING=()
|
||||
DOCS_SOURCES_FILE="${DATA_DIR}/docs-sources.txt"
|
||||
DOCS_DATA_DIR="${DATA_DIR}/docs"
|
||||
MODELS_DATA_DIR="${DATA_DIR}/models"
|
||||
@@ -77,16 +78,16 @@ usage() {
|
||||
context-kit: local context tools for coding agents
|
||||
|
||||
Usage:
|
||||
context-kit start Start SearXNG + the long-lived docs-mcp service
|
||||
context-kit stop Stop SearXNG + docs-mcp
|
||||
context-kit restart Restart SearXNG + docs-mcp
|
||||
context-kit start Start the shared SearXNG and HTTP MCP services
|
||||
context-kit stop Stop the shared services without removing them
|
||||
context-kit restart Restart the shared services
|
||||
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 shared 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:
|
||||
context-kit web-search Per-call SearXNG-backed web-search MCP (stdio)
|
||||
context-kit web-search Stdio bridge to the shared web-search service
|
||||
context-kit docs Stdio bridge to the long-lived docs-mcp service
|
||||
(clients that speak HTTP MCP should connect
|
||||
directly to the URL printed by `status`)
|
||||
@@ -102,7 +103,10 @@ USAGE
|
||||
|
||||
compose() {
|
||||
CONTEXT_KIT_DATA_DIR="${DATA_DIR}" \
|
||||
CONTEXT_KIT_COMPOSE_PROJECT="${PROJECT}" \
|
||||
CONTEXT_KIT_HOST_UID="${HOST_UID}" \
|
||||
CONTEXT_KIT_SEARXNG_PORT="${SEARXNG_PORT}" \
|
||||
CONTEXT_KIT_WEB_SEARCH_PORT="${WEB_SEARCH_PORT}" \
|
||||
CONTEXT_KIT_DOCS_PORT="${DOCS_PORT}" \
|
||||
CONTEXT_KIT_DOCS_UID="$(id -u)" \
|
||||
CONTEXT_KIT_DOCS_GID="$(id -g)" \
|
||||
@@ -128,7 +132,7 @@ write_docs_sources_file() {
|
||||
mkdir -p "$(dirname "${DOCS_SOURCES_FILE}")"
|
||||
local tmp="${DOCS_SOURCES_FILE}.tmp.$$"
|
||||
{
|
||||
printf '# generated by context-kit start; edit your CONTEXT_KIT_DOCS_SOURCES file(s) instead\n'
|
||||
printf '# generated by context-kit lifecycle commands; edit your CONTEXT_KIT_DOCS_SOURCES file(s) instead\n'
|
||||
resolved_sources
|
||||
} > "${tmp}"
|
||||
mv "${tmp}" "${DOCS_SOURCES_FILE}"
|
||||
@@ -204,6 +208,114 @@ require_network() {
|
||||
docker network inspect "${NETWORK}" >/dev/null 2>&1 || fail "missing Docker network ${NETWORK}; run: context-kit start"
|
||||
}
|
||||
|
||||
with_lifecycle_lock() {
|
||||
command -v flock >/dev/null 2>&1 || fail "flock is required for shared service lifecycle operations"
|
||||
command -v stat >/dev/null 2>&1 || fail "stat is required for shared service lifecycle operations"
|
||||
local lock_dir="/tmp/context-kit-${PROJECT}.lock" owner mode
|
||||
[[ -d /tmp && ! -L /tmp ]] || fail "canonical lifecycle lock parent /tmp is unsafe"
|
||||
if mkdir -m 700 "${lock_dir}" 2>/dev/null; then
|
||||
chmod 700 "${lock_dir}"
|
||||
fi
|
||||
[[ -d "${lock_dir}" && ! -L "${lock_dir}" ]] || fail "unsafe lifecycle lock path: ${lock_dir}"
|
||||
owner="$(stat -c %u "${lock_dir}")"
|
||||
mode="$(stat -c %a "${lock_dir}")"
|
||||
[[ "${owner}" == "${HOST_UID}" && "${mode}" == "700" ]] \
|
||||
|| fail "shared Compose project ${PROJECT} lock has uid ${owner} and mode ${mode}; expected uid ${HOST_UID} and mode 700"
|
||||
(
|
||||
flock -x 9
|
||||
assert_project_owner
|
||||
"$@"
|
||||
) 9>"${lock_dir}/lifecycle"
|
||||
}
|
||||
|
||||
service_container_id() {
|
||||
local service="$1" ids
|
||||
ids="$(compose ps --all --quiet "${service}")"
|
||||
[[ "${ids}" != *$'\n'* ]] || fail "multiple containers found for shared service ${service}"
|
||||
printf '%s' "${ids}"
|
||||
}
|
||||
|
||||
container_running() {
|
||||
docker inspect -f '{{.State.Running}}' "$1" 2>/dev/null | grep -qx true
|
||||
}
|
||||
|
||||
container_exists() {
|
||||
docker inspect "$1" >/dev/null 2>&1
|
||||
}
|
||||
|
||||
shared_service_running() {
|
||||
local container_id
|
||||
container_id="$(service_container_id "$1" 2>/dev/null || true)"
|
||||
[[ -n "${container_id}" ]] && container_running "${container_id}"
|
||||
}
|
||||
|
||||
container_matches_shared_service() {
|
||||
local container_id="$1" service="$2" labels
|
||||
labels="$(docker inspect -f '{{ index .Config.Labels "com.docker.compose.project" }}:{{ index .Config.Labels "com.docker.compose.service" }}' "${container_id}" 2>/dev/null || true)"
|
||||
[[ "${labels}" == "${PROJECT}:${service}" ]]
|
||||
}
|
||||
|
||||
assert_project_owner() {
|
||||
local container_id configured_uid
|
||||
container_id="$(service_container_id "${DOCS_SERVICE_NAME}" 2>/dev/null || true)"
|
||||
if [[ -n "${container_id}" ]]; then
|
||||
configured_uid="$(docker inspect -f '{{.Config.User}}' "${container_id}" 2>/dev/null || true)"
|
||||
configured_uid="${configured_uid%%:*}"
|
||||
[[ "${configured_uid}" == "${HOST_UID}" ]] \
|
||||
|| fail "shared Compose project ${PROJECT} docs service belongs to uid ${configured_uid:-unknown}; cross-user ownership is unsupported"
|
||||
fi
|
||||
|
||||
container_id="$(service_container_id "${WEB_SEARCH_SERVICE_NAME}" 2>/dev/null || true)"
|
||||
if [[ -n "${container_id}" ]]; then
|
||||
configured_uid="$(docker inspect -f '{{ index .Config.Labels "dev.context-kit.uid" }}' "${container_id}" 2>/dev/null || true)"
|
||||
[[ "${configured_uid}" == "${HOST_UID}" ]] \
|
||||
|| fail "shared Compose project ${PROJECT} web-search service belongs to uid ${configured_uid:-unknown}; cross-user ownership is unsupported"
|
||||
fi
|
||||
}
|
||||
|
||||
cleanup_owned_container() {
|
||||
local container_id="$1" owner="$2" actual_owner
|
||||
[[ -n "${container_id}" ]] || return 0
|
||||
actual_owner="$(docker inspect -f '{{ index .Config.Labels "dev.context-kit.owner" }}' "${container_id}" 2>/dev/null || true)"
|
||||
[[ "${actual_owner}" == "${owner}" ]] || return 0
|
||||
docker rm -f "${container_id}" >/dev/null 2>&1 || true
|
||||
}
|
||||
|
||||
run_owned_stdio_container() {
|
||||
local role="$1"
|
||||
shift
|
||||
local uid owner name container_id='' status=0
|
||||
uid="$(id -u)"
|
||||
owner="${PROJECT}:${role}:${uid}:$$"
|
||||
name="${PROJECT}-${role}-${uid}-$$"
|
||||
|
||||
trap 'cleanup_owned_container "${container_id}" "${owner}"' EXIT
|
||||
trap 'exit 129' HUP
|
||||
trap 'exit 130' INT
|
||||
trap 'exit 143' TERM
|
||||
container_id="$(docker create -i --rm \
|
||||
--name "${name}" \
|
||||
--label dev.context-kit=true \
|
||||
--label dev.context-kit.lifecycle=client \
|
||||
--label "dev.context-kit.owner=${owner}" \
|
||||
--label "dev.context-kit.role=${role}" \
|
||||
"$@")" || status=$?
|
||||
if [[ "${status}" -ne 0 ]]; then
|
||||
cleanup_owned_container "${container_id}" "${owner}"
|
||||
trap - EXIT HUP INT TERM
|
||||
return "${status}"
|
||||
fi
|
||||
|
||||
if [[ -n "${CONTEXT_KIT_DOCKER_CIDFILE:-}" ]]; then
|
||||
printf '%s\n' "${container_id}" > "${CONTEXT_KIT_DOCKER_CIDFILE}"
|
||||
fi
|
||||
|
||||
docker start -ai "${container_id}" <&0 || status=$?
|
||||
cleanup_owned_container "${container_id}" "${owner}"
|
||||
trap - EXIT HUP INT TERM
|
||||
return "${status}"
|
||||
}
|
||||
|
||||
wait_for_searxng() {
|
||||
command -v curl >/dev/null 2>&1 || return 0
|
||||
|
||||
@@ -219,11 +331,39 @@ wait_for_searxng() {
|
||||
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_web_search_mcp() {
|
||||
command -v curl >/dev/null 2>&1 || return 1
|
||||
|
||||
local attempt
|
||||
for attempt in {1..60}; do
|
||||
if probe_web_search_mcp; then
|
||||
return 0
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
|
||||
warn "web-search-mcp did not become ready on 127.0.0.1:${WEB_SEARCH_PORT} after 60s (check: docker compose logs ${WEB_SEARCH_SERVICE_NAME})"
|
||||
return 1
|
||||
}
|
||||
|
||||
probe_web_search_mcp() {
|
||||
local url="http://127.0.0.1:${WEB_SEARCH_PORT}/mcp" initialized tools
|
||||
initialized="$(curl -fsS --max-time 10 \
|
||||
-H 'Accept: application/json, text/event-stream' \
|
||||
-H 'Content-Type: application/json' \
|
||||
-H 'MCP-Protocol-Version: 2024-11-05' \
|
||||
--data '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"context-kit-doctor","version":"1"}}}' \
|
||||
"${url}" 2>/dev/null)" || return 1
|
||||
printf '%s' "${initialized}" | grep -Eq '"serverInfo"[[:space:]]*:' || return 1
|
||||
|
||||
tools="$(curl -fsS --max-time 10 \
|
||||
-H 'Accept: application/json, text/event-stream' \
|
||||
-H 'Content-Type: application/json' \
|
||||
-H 'MCP-Protocol-Version: 2024-11-05' \
|
||||
--data '{"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}}' \
|
||||
"${url}" 2>/dev/null)" || return 1
|
||||
printf '%s' "${tools}" | grep -Eq '"name"[[:space:]]*:[[:space:]]*"search_web"' || return 1
|
||||
printf '%s' "${tools}" | grep -Eq '"name"[[:space:]]*:[[:space:]]*"fetch_url"'
|
||||
}
|
||||
|
||||
wait_for_docs_mcp() {
|
||||
@@ -231,7 +371,7 @@ wait_for_docs_mcp() {
|
||||
|
||||
# First run can take a while: model download plus optional eager preindexing.
|
||||
local attempt http_ready=0
|
||||
for attempt in {1..180}; do
|
||||
for ((attempt=1; attempt <= 180; attempt++)); do
|
||||
if curl -fsS -o /dev/null "http://127.0.0.1:${DOCS_PORT}/status" 2>/dev/null; then
|
||||
http_ready=1
|
||||
break
|
||||
@@ -286,36 +426,130 @@ resolved_sources() {
|
||||
cmd_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.
|
||||
compose --profile mcp build web-search-mcp
|
||||
compose build docs-mcp
|
||||
compose build web-search-mcp docs-mcp
|
||||
docker pull "${REPOMIX_IMAGE}"
|
||||
}
|
||||
|
||||
cmd_start() {
|
||||
require_no_args "usage: context-kit start" "$@"
|
||||
require_docker
|
||||
snapshot_shared_services() {
|
||||
local service container_id
|
||||
SNAPSHOT_IDS=()
|
||||
SNAPSHOT_RUNNING=()
|
||||
for service in "${SHARED_SERVICES[@]}"; do
|
||||
container_id="$(service_container_id "${service}")"
|
||||
SNAPSHOT_IDS+=("${container_id}")
|
||||
if [[ -n "${container_id}" ]] && container_running "${container_id}"; then
|
||||
SNAPSHOT_RUNNING+=(1)
|
||||
else
|
||||
SNAPSHOT_RUNNING+=(0)
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
restore_shared_service_states() {
|
||||
local remove_new="$1" index service current_id prior_id failed=0
|
||||
for ((index=0; index < ${#SHARED_SERVICES[@]}; index++)); do
|
||||
service="${SHARED_SERVICES[index]}"
|
||||
prior_id="${SNAPSHOT_IDS[index]}"
|
||||
current_id="$(service_container_id "${service}" 2>/dev/null || true)"
|
||||
|
||||
if [[ -z "${prior_id}" && "${remove_new}" == true && -n "${current_id}" ]]; then
|
||||
if container_matches_shared_service "${current_id}" "${service}"; then
|
||||
if ! docker rm -f "${current_id}" >/dev/null 2>&1; then
|
||||
warn "failed to remove newly-created ${service} container ${current_id} during rollback"
|
||||
failed=1
|
||||
fi
|
||||
else
|
||||
warn "new ${service} container ${current_id} lacks the expected Compose ownership; leaving it untouched"
|
||||
failed=1
|
||||
fi
|
||||
continue
|
||||
fi
|
||||
[[ -n "${prior_id}" ]] || continue
|
||||
|
||||
if [[ -n "${current_id}" && "${current_id}" != "${prior_id}" ]]; then
|
||||
warn "${service} unexpectedly changed from ${prior_id} to ${current_id}; leaving the replacement untouched"
|
||||
failed=1
|
||||
continue
|
||||
fi
|
||||
if ! container_exists "${prior_id}"; then
|
||||
warn "cannot restore missing pre-start ${service} container ${prior_id}"
|
||||
failed=1
|
||||
continue
|
||||
fi
|
||||
if [[ "${SNAPSHOT_RUNNING[index]}" -eq 1 ]] && ! container_running "${prior_id}"; then
|
||||
if ! docker start "${prior_id}" >/dev/null 2>&1; then
|
||||
warn "failed to restart prior ${service} container ${prior_id} during rollback"
|
||||
failed=1
|
||||
fi
|
||||
elif [[ "${SNAPSHOT_RUNNING[index]}" -eq 0 ]] && container_running "${prior_id}"; then
|
||||
if ! docker stop "${prior_id}" >/dev/null 2>&1; then
|
||||
warn "failed to stop prior ${service} container ${prior_id} during rollback"
|
||||
failed=1
|
||||
fi
|
||||
fi
|
||||
done
|
||||
return "${failed}"
|
||||
}
|
||||
|
||||
shared_services_ready() {
|
||||
wait_for_searxng && wait_for_web_search_mcp && wait_for_docs_mcp
|
||||
}
|
||||
|
||||
start_locked() {
|
||||
prepare_data_dirs
|
||||
if ! docker image inspect "${WEB_SEARCH_IMAGE}" >/dev/null 2>&1 || ! docker image inspect "${DOCS_IMAGE}" >/dev/null 2>&1; then
|
||||
cmd_build
|
||||
fi
|
||||
write_docs_sources_file
|
||||
compose up -d searxng docs-mcp
|
||||
wait_for_searxng
|
||||
wait_for_docs_mcp
|
||||
|
||||
local result=0
|
||||
snapshot_shared_services
|
||||
compose up -d --no-recreate "${SHARED_SERVICES[@]}" || result=$?
|
||||
if [[ "${result}" -eq 0 ]] && ! shared_services_ready; then
|
||||
result=1
|
||||
fi
|
||||
[[ "${result}" -ne 0 ]] || return 0
|
||||
|
||||
warn "shared service startup failed; restoring prior container states"
|
||||
restore_shared_service_states true || true
|
||||
return "${result}"
|
||||
}
|
||||
|
||||
cmd_start() {
|
||||
require_no_args "usage: context-kit start" "$@"
|
||||
require_docker
|
||||
with_lifecycle_lock start_locked
|
||||
}
|
||||
|
||||
stop_locked() {
|
||||
compose stop "${SHARED_SERVICES[@]}"
|
||||
}
|
||||
|
||||
cmd_stop() {
|
||||
require_no_args "usage: context-kit stop" "$@"
|
||||
require_docker
|
||||
compose stop searxng docs-mcp
|
||||
with_lifecycle_lock stop_locked
|
||||
}
|
||||
|
||||
cmd_restart() {
|
||||
require_no_args "usage: context-kit restart" "$@"
|
||||
cmd_stop
|
||||
cmd_start
|
||||
require_docker
|
||||
with_lifecycle_lock restart_locked
|
||||
}
|
||||
|
||||
restart_locked() {
|
||||
local result=0
|
||||
write_docs_sources_file
|
||||
snapshot_shared_services
|
||||
compose restart "${SHARED_SERVICES[@]}" || result=$?
|
||||
if [[ "${result}" -eq 0 ]] && ! shared_services_ready; then
|
||||
result=1
|
||||
fi
|
||||
[[ "${result}" -ne 0 ]] || return 0
|
||||
|
||||
warn "shared service restart failed; restoring prior container states"
|
||||
restore_shared_service_states false || true
|
||||
return "${result}"
|
||||
}
|
||||
|
||||
cmd_status() {
|
||||
@@ -326,10 +560,17 @@ cmd_status() {
|
||||
printf '\nImages\n'
|
||||
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}}\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 '\nClient-owned stdio MCP containers\n'
|
||||
docker ps -a --filter label=dev.context-kit.lifecycle=client --format 'table {{.Names}}\t{{.Status}}\t{{.Label "dev.context-kit.role"}}\t{{.Label "dev.context-kit.owner"}}'
|
||||
printf '\nLegacy unlabeled Context Kit containers (diagnostic only; never auto-removed)\n'
|
||||
docker ps -a --filter label=dev.context-kit=true \
|
||||
--format '{{.Names}}\t{{.Status}}\t{{.Label "dev.context-kit.lifecycle"}}\t{{.Label "com.docker.compose.service"}}' \
|
||||
| awk -F '\t' 'BEGIN { print "NAMES\tSTATUS" } $3 == "" && $4 == "" { print $1 "\t" $2 }'
|
||||
printf '\nShared MCP endpoints\n- %s (service: %s)\n- %s (service: %s)\n' \
|
||||
"${WEB_SEARCH_HTTP_URL}" "${WEB_SEARCH_SERVICE_NAME}" \
|
||||
"${DOCS_HTTP_URL}" "${DOCS_SERVICE_NAME}"
|
||||
printf '\nShared Docker ownership\n- project: %s\n- uid: %s\n- network: %s\n- cache volume: %s_searxng-cache\n' \
|
||||
"${PROJECT}" "${HOST_UID}" "${NETWORK}" "${PROJECT}"
|
||||
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}"
|
||||
@@ -384,6 +625,13 @@ cmd_doctor() {
|
||||
ok=1
|
||||
fi
|
||||
|
||||
if command -v curl >/dev/null 2>&1 && probe_web_search_mcp; then
|
||||
printf 'pass web-search-mcp initialize and tools/list on 127.0.0.1:%s\n' "${WEB_SEARCH_PORT}"
|
||||
else
|
||||
printf 'fail web-search-mcp MCP protocol probe failed on 127.0.0.1:%s (run context-kit start)\n' "${WEB_SEARCH_PORT}"
|
||||
ok=1
|
||||
fi
|
||||
|
||||
if command -v curl >/dev/null 2>&1 && curl -fsS -o /dev/null "http://127.0.0.1:${DOCS_PORT}/status" 2>/dev/null; then
|
||||
printf 'pass docs-mcp HTTP responds on 127.0.0.1:%s\n' "${DOCS_PORT}"
|
||||
else
|
||||
@@ -405,23 +653,16 @@ cmd_web_search() {
|
||||
require_docker
|
||||
require_network
|
||||
require_image "${WEB_SEARCH_IMAGE}" "context-kit build"
|
||||
local cidfile_args=()
|
||||
if [[ -n "${CONTEXT_KIT_DOCKER_CIDFILE:-}" ]]; then
|
||||
cidfile_args=(--cidfile "${CONTEXT_KIT_DOCKER_CIDFILE}")
|
||||
if ! shared_service_running "${WEB_SEARCH_SERVICE_NAME}"; then
|
||||
fail "long-lived web-search-mcp not running; start it with: context-kit start"
|
||||
fi
|
||||
exec docker run --rm -i \
|
||||
--label dev.context-kit=true \
|
||||
"${cidfile_args[@]}" \
|
||||
|
||||
run_owned_stdio_container web-search-bridge \
|
||||
--network "${NETWORK}" \
|
||||
-e DEFAULT_SEARCH_PROVIDER="${WEB_SEARCH_PROVIDER}" \
|
||||
-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}" \
|
||||
-e MAX_RESULTS="${WEB_SEARCH_MAX_RESULTS}" \
|
||||
-e BROWSER_SEARCH_USER_AGENT="${WEB_SEARCH_BROWSER_USER_AGENT}" \
|
||||
-e MCP_COMPAT_MODE="${WEB_SEARCH_MCP_COMPAT_MODE}" \
|
||||
"${WEB_SEARCH_IMAGE}"
|
||||
--entrypoint mcp-proxy \
|
||||
"${WEB_SEARCH_IMAGE}" \
|
||||
--transport streamablehttp \
|
||||
"http://${WEB_SEARCH_SERVICE_NAME}:8000/mcp"
|
||||
}
|
||||
|
||||
cmd_docs() {
|
||||
@@ -435,18 +676,12 @@ cmd_docs() {
|
||||
require_network
|
||||
require_image "${DOCS_IMAGE}" "context-kit build"
|
||||
|
||||
if ! docs_service_running; then
|
||||
if ! shared_service_running "${DOCS_SERVICE_NAME}"; then
|
||||
fail "long-lived docs-mcp not running; start it with: context-kit start"
|
||||
fi
|
||||
|
||||
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}")
|
||||
fi
|
||||
exec docker run --rm -i \
|
||||
--label dev.context-kit=true \
|
||||
"${cidfile_args[@]}" \
|
||||
run_owned_stdio_container docs-bridge \
|
||||
--network "${NETWORK}" \
|
||||
--entrypoint mcp-proxy \
|
||||
"${DOCS_IMAGE}" \
|
||||
@@ -462,13 +697,7 @@ cmd_repomix() {
|
||||
dir="$(project_dir)"
|
||||
mount_dir="${CONTEXT_KIT_REPOMIX_MOUNT_DIR:-${dir}}"
|
||||
mount_dir="$(cd "${mount_dir}" && pwd -P)"
|
||||
local cidfile_args=()
|
||||
if [[ -n "${CONTEXT_KIT_DOCKER_CIDFILE:-}" ]]; then
|
||||
cidfile_args=(--cidfile "${CONTEXT_KIT_DOCKER_CIDFILE}")
|
||||
fi
|
||||
exec docker run --rm -i \
|
||||
--label dev.context-kit=true \
|
||||
"${cidfile_args[@]}" \
|
||||
run_owned_stdio_container repomix \
|
||||
-v "${mount_dir}:${mount_dir}:ro" \
|
||||
--workdir "${dir}" \
|
||||
"${REPOMIX_IMAGE}" --mcp
|
||||
@@ -483,22 +712,23 @@ snippet_command() {
|
||||
}
|
||||
|
||||
print_opencode() {
|
||||
local bin url
|
||||
local bin docs_url web_search_url
|
||||
bin="$(json_escape "$(snippet_command "${1:-}")")"
|
||||
url="$(json_escape "${DOCS_HTTP_URL}")"
|
||||
web_search_url="$(json_escape "${WEB_SEARCH_HTTP_URL}")"
|
||||
docs_url="$(json_escape "${DOCS_HTTP_URL}")"
|
||||
cat <<JSON
|
||||
{
|
||||
"\$schema": "https://opencode.ai/config.json",
|
||||
"mcp": {
|
||||
"context-web-search": {
|
||||
"type": "local",
|
||||
"command": ["${bin}", "web-search"],
|
||||
"type": "remote",
|
||||
"url": "${web_search_url}",
|
||||
"enabled": true,
|
||||
"timeout": 150000
|
||||
},
|
||||
"context-docs": {
|
||||
"type": "remote",
|
||||
"url": "${url}",
|
||||
"url": "${docs_url}",
|
||||
"enabled": true,
|
||||
"timeout": 150000
|
||||
},
|
||||
@@ -514,19 +744,20 @@ JSON
|
||||
}
|
||||
|
||||
print_claude() {
|
||||
local bin url
|
||||
local bin docs_url web_search_url
|
||||
bin="$(json_escape "$(snippet_command "${1:-}")")"
|
||||
url="$(json_escape "${DOCS_HTTP_URL}")"
|
||||
web_search_url="$(json_escape "${WEB_SEARCH_HTTP_URL}")"
|
||||
docs_url="$(json_escape "${DOCS_HTTP_URL}")"
|
||||
cat <<JSON
|
||||
{
|
||||
"mcpServers": {
|
||||
"context-web-search": {
|
||||
"command": "${bin}",
|
||||
"args": ["web-search"]
|
||||
"type": "http",
|
||||
"url": "${web_search_url}"
|
||||
},
|
||||
"context-docs": {
|
||||
"type": "http",
|
||||
"url": "${url}"
|
||||
"url": "${docs_url}"
|
||||
},
|
||||
"context-repomix": {
|
||||
"command": "${bin}",
|
||||
@@ -564,6 +795,7 @@ cmd_redaction_check() {
|
||||
local grep_opts=(
|
||||
-RInE
|
||||
--exclude-dir=.git
|
||||
--exclude=.git
|
||||
--exclude-dir=.cache
|
||||
--exclude-dir=tmp
|
||||
--exclude=.env
|
||||
|
||||
Reference in New Issue
Block a user