gitea-mcp was parked alongside cad on the stated reason that nothing puts its token into the session environment: GITEA_MCP_STATIC_TOKEN lives in the AgentSecrets vault, which the session identity deliberately cannot read (infra#224), so enabling it was blocked behind #32/#33. That reason never applied to gitea. gitea-mcp authenticates by PASS-THROUGH: the bearer is a Gitea PAT, which the server validates against Gitea and then acts as. No dedicated MCP token exists to be missing. DMABE_GITEA_API_TOKEN is op://HomeLab and already in the session environment. Verified on koala 2026-09-07 against tailscale serve 8446 -> NodePort 30340: initialize returns 200 with serverInfo gitea-mcp, tools/list returns 41 tools. Two DIFFERENT PATs both authenticate while GITEA_MCP_TOKEN 401s -- that is what proves pass-through validation rather than a static-token comparison, and it also shows GITEA_MCP_TOKEN is stale (it 401s against git-mcp.d-ma.be too). cad stays parked; its blocker is real and unchanged. Also adds bin/check-mcp-env and tests/mcp_config_test.sh, wired into task check. The fault it gates is silent: a header interpolating an undefined variable does not error, it sends an empty Authorization, and Claude Code reports "requires authentication" -- indistinguishable from a revoked credential. That cost a full investigation on koala today before the credential turned out to be fine. The check exits 2 rather than 0 when it cannot run (no jq, no env-file, as in CI), so an unverified config is never reported as a clean one. Servers under _parked are exempt: referencing an unprovided variable is what parked means. Bump-Type: minor Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
119 lines
4.5 KiB
Bash
Executable File
119 lines
4.5 KiB
Bash
Executable File
#!/bin/sh
|
|
# Tests for bin/check-mcp-env.
|
|
#
|
|
# The fault this gate exists for is silent, which is why it needs a gate at
|
|
# all. An `.mcp.json` server whose Authorization header interpolates a
|
|
# variable nothing in the session environment defines does not error: the
|
|
# header goes out empty, the server answers 401, and Claude Code reports
|
|
# "requires authentication". That is indistinguishable from a revoked or
|
|
# expired credential, and on 2026-09-07 it cost a full investigation on koala
|
|
# to establish that the credential was fine and the variable was simply never
|
|
# set.
|
|
#
|
|
# So the property is: every ${VAR} a LIVE server interpolates must be a
|
|
# variable the session env-file actually defines.
|
|
#
|
|
# The `_parked` block is deliberately exempt. Parked servers reference
|
|
# variables nothing provides on purpose -- that is what parked means -- and a
|
|
# gate that flagged them would be reporting the intended state as a fault.
|
|
#
|
|
# Fixtures drive the checker through argv, so no test touches the real host.
|
|
|
|
set -u
|
|
|
|
ROOT=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)
|
|
CHECK="$ROOT/bin/check-mcp-env"
|
|
|
|
pass=0
|
|
fail=0
|
|
WORK=${TMPDIR:-/tmp}/mcp_config_test.$$
|
|
|
|
if [ ! -f "$CHECK" ]; then
|
|
printf 'FAIL: %s does not exist\n' "$CHECK"
|
|
exit 1
|
|
fi
|
|
|
|
cleanup() { rm -rf "$WORK"; }
|
|
trap cleanup EXIT INT TERM
|
|
mkdir -p "$WORK"
|
|
|
|
if ! command -v jq >/dev/null 2>&1; then
|
|
printf 'SKIP: jq not installed -- bin/check-mcp-env cannot be exercised\n'
|
|
exit 0
|
|
fi
|
|
|
|
# ok <name> / bad <name> <detail>
|
|
ok() { pass=$((pass+1)); printf ' ok %s\n' "$1"; }
|
|
bad() { fail=$((fail+1)); printf ' FAIL %s %s\n' "$1" "${2:-}"; }
|
|
|
|
# run <mcp-json> <env-file> -> sets $rc and $out
|
|
run() {
|
|
out=$("$CHECK" "$1" "$2" 2>&1); rc=$?
|
|
}
|
|
|
|
ENVFILE="$WORK/op-env"
|
|
cat > "$ENVFILE" <<'ENVEOF'
|
|
BRAIN_MCP_TOKEN=op://HomeLab/brain/token
|
|
DMABE_GITEA_API_TOKEN=op://HomeLab/gitea/token
|
|
ENVEOF
|
|
|
|
# --- a live server whose variable IS defined ---------------------------------
|
|
cat > "$WORK/good.json" <<'JSONEOF'
|
|
{"mcpServers":{"brain":{"type":"http","url":"https://x/mcp",
|
|
"headers":{"Authorization":"Bearer ${BRAIN_MCP_TOKEN}"}}}}
|
|
JSONEOF
|
|
run "$WORK/good.json" "$ENVFILE"
|
|
[ "$rc" -eq 0 ] && ok "defined variable passes" || bad "defined variable passes" "rc=$rc $out"
|
|
|
|
# --- a live server whose variable is NOT defined ------------------------------
|
|
# The exact shape that broke: the server looks correct and fails at runtime.
|
|
cat > "$WORK/missing.json" <<'JSONEOF'
|
|
{"mcpServers":{"gitea":{"type":"http","url":"https://x/mcp",
|
|
"headers":{"Authorization":"Bearer ${GITEA_MCP_STATIC_TOKEN}"}}}}
|
|
JSONEOF
|
|
run "$WORK/missing.json" "$ENVFILE"
|
|
if [ "$rc" -ne 0 ]; then
|
|
case "$out" in
|
|
*gitea*GITEA_MCP_STATIC_TOKEN*) ok "undefined variable is flagged, naming server and variable" ;;
|
|
*) bad "undefined variable is flagged, naming server and variable" "report did not name both: $out" ;;
|
|
esac
|
|
else
|
|
bad "undefined variable is flagged, naming server and variable" "rc=0"
|
|
fi
|
|
|
|
# --- a server with no headers at all (the `infra` shape) ----------------------
|
|
# Unauthenticated by design. Nothing to resolve, so nothing to flag.
|
|
cat > "$WORK/noauth.json" <<'JSONEOF'
|
|
{"mcpServers":{"infra":{"type":"http","url":"https://x/mcp"}}}
|
|
JSONEOF
|
|
run "$WORK/noauth.json" "$ENVFILE"
|
|
[ "$rc" -eq 0 ] && ok "header-less server is not flagged" || bad "header-less server is not flagged" "rc=$rc $out"
|
|
|
|
# --- parked servers are exempt ------------------------------------------------
|
|
cat > "$WORK/parked.json" <<'JSONEOF'
|
|
{"mcpServers":{"brain":{"type":"http","url":"https://x/mcp",
|
|
"headers":{"Authorization":"Bearer ${BRAIN_MCP_TOKEN}"}}},
|
|
"_parked":{"_reason":"no identity provides these yet",
|
|
"cad":{"type":"http","url":"https://x/mcp",
|
|
"headers":{"Authorization":"Bearer ${CAD_MCP_TOKEN}"}}}}
|
|
JSONEOF
|
|
run "$WORK/parked.json" "$ENVFILE"
|
|
if [ "$rc" -eq 0 ]; then
|
|
ok "parked server is exempt"
|
|
else
|
|
bad "parked server is exempt" "rc=$rc $out"
|
|
fi
|
|
|
|
# --- malformed JSON must not pass silently ------------------------------------
|
|
# A config the checker cannot read is an unverified config, not a clean one.
|
|
printf '{"mcpServers":{' > "$WORK/broken.json"
|
|
run "$WORK/broken.json" "$ENVFILE"
|
|
[ "$rc" -ne 0 ] && ok "unparseable config does not report success" || bad "unparseable config does not report success" "rc=0"
|
|
|
|
# --- a missing env-file is unverifiable, not clean ----------------------------
|
|
run "$WORK/good.json" "$WORK/no-such-env"
|
|
[ "$rc" -ne 0 ] && ok "missing env-file does not report success" || bad "missing env-file does not report success" "rc=0"
|
|
|
|
printf '\n%s: %d passed, %d failed\n' "$(basename "$0")" "$pass" "$fail"
|
|
[ "$fail" -eq 0 ]
|