Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
da76a6b3d2 |
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "skills",
|
||||
"description": "Mathias's engineering skills library, plus the hooks and MCP servers that make it enforceable: TDD, clean code, debugging, specs, discovery, CI, and secret hygiene.",
|
||||
"version": "0.18.1",
|
||||
"version": "0.19.0",
|
||||
"author": {
|
||||
"name": "Mathias",
|
||||
"url": "https://git.d-ma.be/mathias"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"_comment": "Tailscale Serve on koala, not the public d-ma.be hostnames. The public path exits to Cloudflare and NPM to reach a k3s service on the same machine most sessions run on, which imposes rate limits and message-size caps on tool results. Serve terminates TLS with a real Tailscale cert and proxies to the NodePort over loopback, so traffic reaches the service from koala itself and satisfies the default-deny-ingress NetworkPolicy with no cluster change. Clients need 'tailscale set --accept-dns=true' for the MagicDNS name to resolve; see mathias/skills#24.",
|
||||
"_comment": "Tailscale Serve on koala, not the public d-ma.be hostnames. The public path exits to Cloudflare and NPM to reach a k3s service on the same machine most sessions run on, which imposes rate limits and message-size caps on tool results. Serve terminates TLS with a real Tailscale cert and proxies to the NodePort over loopback, so traffic reaches the service from koala itself and satisfies the default-deny-ingress NetworkPolicy with no cluster change. Clients need 'tailscale set --accept-dns=true' for the MagicDNS name to resolve; see mathias/skills#24. Every ${VAR} below must be one ~/.op-env defines -- bin/check-mcp-env enforces that, because an undefined one sends an empty Authorization header and reports as 'requires authentication', which is indistinguishable from a revoked credential.",
|
||||
"mcpServers": {
|
||||
"brain": {
|
||||
"type": "http",
|
||||
@@ -11,23 +11,24 @@
|
||||
"infra": {
|
||||
"type": "http",
|
||||
"url": "https://koala.tail30472e.ts.net:8444/mcp"
|
||||
},
|
||||
"gitea": {
|
||||
"_comment": "gitea-mcp authenticates by PASS-THROUGH: the bearer is a Gitea PAT, which the server validates against Gitea and then acts as. So no dedicated MCP token is needed, and the AgentSecrets blocker that parked this entry (skills#32, #33) never applied to it -- DMABE_GITEA_API_TOKEN is op://HomeLab and already in the session environment. Verified 2026-09-07: initialize 200 (serverInfo gitea-mcp) and tools/list returns 41 tools. Two DIFFERENT PATs both authenticate while GITEA_MCP_TOKEN 401s, which is what proves this is pass-through validation and not a static-token comparison.",
|
||||
"type": "http",
|
||||
"url": "https://koala.tail30472e.ts.net:8446/mcp",
|
||||
"headers": {
|
||||
"Authorization": "Bearer ${DMABE_GITEA_API_TOKEN}"
|
||||
}
|
||||
}
|
||||
},
|
||||
"_parked": {
|
||||
"_reason": "cad and gitea-mcp are proxied, reachable and AUTHENTICATING: tailscale serve 8445 -> 127.0.0.1:31877 and 8446 -> 127.0.0.1:30340; an MCP initialize with the correct bearer returns 200 from both (serverInfo gitea-mcp 0.1.0, cad aed2aaf). They stay parked only because nothing puts those two tokens into the session environment. Both live in the 1Password AgentSecrets vault, and by deliberate design (infra#224, closed) no single identity on koala reads both vaults: koala-homelab is HomeLab-only and resolves ~/.op-env, koala-agentsecrets-write is AgentSecrets-only and lives in ~/.op-agent-write.env. Adding an AgentSecrets reference to ~/.op-env does NOT degrade gracefully -- `op run` fails wholesale and every other token, BRAIN_MCP_TOKEN included, goes unresolved with it. Enabling needs a read-scoped AgentSecrets identity in the session-env path; see mathias/skills#32, #33.",
|
||||
"_reason": "cad is proxied, reachable and AUTHENTICATING: tailscale serve 8445 -> 127.0.0.1:31877, and an MCP initialize with the correct bearer returns 200 (serverInfo cad aed2aaf). It stays parked only because nothing puts CAD_MCP_TOKEN into the session environment. It lives in the 1Password AgentSecrets vault, and by deliberate design (infra#224, closed) no single identity on koala reads both vaults: koala-homelab is HomeLab-only and resolves ~/.op-env, koala-agentsecrets-write is AgentSecrets-only and lives in ~/.op-agent-write.env. Adding an AgentSecrets reference to ~/.op-env does NOT degrade gracefully -- `op run` fails wholesale and every other token, BRAIN_MCP_TOKEN included, goes unresolved with it. Enabling needs a read-scoped AgentSecrets identity in the session-env path; see mathias/skills#32, #33. gitea was parked here for the same stated reason and has been unparked: it needs no AgentSecrets token at all, because gitea-mcp is pass-through-authenticated.",
|
||||
"cad": {
|
||||
"type": "http",
|
||||
"url": "https://koala.tail30472e.ts.net:8445/mcp",
|
||||
"headers": {
|
||||
"Authorization": "Bearer ${CAD_MCP_TOKEN}"
|
||||
}
|
||||
},
|
||||
"gitea": {
|
||||
"type": "http",
|
||||
"url": "https://koala.tail30472e.ts.net:8446/mcp",
|
||||
"headers": {
|
||||
"Authorization": "Bearer ${GITEA_MCP_STATIC_TOKEN}"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -138,6 +138,21 @@ tasks:
|
||||
echo "ERROR: missing .claude-plugin/plugin.json" >&2; fail=1
|
||||
fi
|
||||
|
||||
# Every ${VAR} a live .mcp.json server interpolates must be one the
|
||||
# session env-file defines. An undefined one is silent: the header
|
||||
# goes out empty and the server reports "requires authentication",
|
||||
# which reads as a revoked credential. Exit 2 means the check could
|
||||
# not run (no jq, or no env-file — CI has neither); that is reported
|
||||
# and not fatal, but it is never treated as a pass.
|
||||
if [ -x "{{.TASKFILE_DIR}}/bin/check-mcp-env" ]; then
|
||||
"{{.TASKFILE_DIR}}/bin/check-mcp-env" "{{.TASKFILE_DIR}}/.mcp.json" "$HOME/.op-env"
|
||||
case $? in
|
||||
0) ;;
|
||||
2) echo "NOTE: MCP env wiring unverified on this host" >&2 ;;
|
||||
*) fail=1 ;;
|
||||
esac
|
||||
fi
|
||||
|
||||
if [ $fail -ne 0 ]; then
|
||||
echo "check FAILED" >&2; exit 1
|
||||
fi
|
||||
|
||||
Executable
+89
@@ -0,0 +1,89 @@
|
||||
#!/bin/sh
|
||||
# check-mcp-env — assert every variable a live MCP server interpolates is one
|
||||
# the session env-file actually defines.
|
||||
#
|
||||
# Why this is a gate and not a comment: the failure is silent. A header of
|
||||
# "Bearer ${FOO}" where FOO is undefined does not error -- it goes out empty,
|
||||
# the server answers 401, and Claude Code reports "requires authentication",
|
||||
# which reads exactly like a revoked credential. On 2026-09-07 that cost a
|
||||
# full investigation on koala before the credential turned out to be fine.
|
||||
#
|
||||
# Servers under `_parked` are exempt by design. Parked means "reachable, but
|
||||
# nothing puts its token in the environment yet" -- flagging those would
|
||||
# report the intended state as a fault.
|
||||
#
|
||||
# Usage: check-mcp-env <mcp.json> <env-file>
|
||||
#
|
||||
# Exit 0 every live server's variables are defined
|
||||
# 1 at least one is not, named on stderr
|
||||
# 2 could not verify (bad usage, unreadable input, jq absent).
|
||||
# Deliberately distinct from 0: an unverified config is not a clean
|
||||
# one, and a gate that cannot run should say so rather than pass.
|
||||
|
||||
set -u
|
||||
|
||||
CONFIG=${1:-}
|
||||
ENVFILE=${2:-}
|
||||
|
||||
if [ -z "$CONFIG" ] || [ -z "$ENVFILE" ]; then
|
||||
echo "usage: check-mcp-env <mcp.json> <env-file>" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
if ! command -v jq >/dev/null 2>&1; then
|
||||
echo "SKIP: jq not installed -- MCP env wiring unverified" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
if [ ! -f "$CONFIG" ]; then
|
||||
echo "ERROR: no such config: $CONFIG" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
if [ ! -f "$ENVFILE" ]; then
|
||||
echo "ERROR: no such env-file: $ENVFILE -- cannot verify MCP variables" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
# Every "<server>\t<VAR>" pair a live server interpolates. `..` walks the whole
|
||||
# value, so a variable is found wherever it is placed, not only under headers.
|
||||
refs=$(jq -r '
|
||||
.mcpServers // {}
|
||||
| to_entries[]
|
||||
| .key as $server
|
||||
| (.value | .. | strings)
|
||||
| select(test("\\$\\{"))
|
||||
| [ scan("\\$\\{([A-Za-z_][A-Za-z0-9_]*)\\}")[] ]
|
||||
| .[]
|
||||
| "\($server)\t\(.)"
|
||||
' "$CONFIG" 2>/dev/null) || {
|
||||
echo "ERROR: $CONFIG is not valid JSON -- MCP env wiring unverified" >&2
|
||||
exit 2
|
||||
}
|
||||
|
||||
if [ -z "$refs" ]; then
|
||||
echo "check-mcp-env: no interpolated variables in live servers"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
fail=0
|
||||
checked=0
|
||||
for pair in $(printf '%s\n' "$refs" | tr '\t' ':'); do
|
||||
server=${pair%%:*}
|
||||
var=${pair#*:}
|
||||
checked=$((checked + 1))
|
||||
if grep -qE "^[[:space:]]*(export[[:space:]]+)?$var=" "$ENVFILE"; then
|
||||
continue
|
||||
fi
|
||||
printf 'ERROR: MCP server %s interpolates %s, which %s does not define.\n' \
|
||||
"$server" "$var" "$ENVFILE" >&2
|
||||
printf ' It will send an empty Authorization header and report as "requires authentication".\n' >&2
|
||||
fail=1
|
||||
done
|
||||
|
||||
if [ "$fail" -ne 0 ]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
printf 'check-mcp-env: %d interpolated variable(s) across live MCP servers, all defined in %s\n' \
|
||||
"$checked" "$ENVFILE"
|
||||
Executable
+118
@@ -0,0 +1,118 @@
|
||||
#!/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 ]
|
||||
Reference in New Issue
Block a user