Both were caught by running the check against a state whose answer was already
known from the screen, which is the only reason they were caught at all. A
check that reports PENDING for a thing that has plainly happened is worse than
no check: it invites the reader to distrust the instrument or, worse, to
believe it.
1. It looked for a directory named after the plugin. Cowork installs into
rpm/plugin_<opaque id>/, so the name appears in no path -- only in
rpm/manifest.json, which is now what gets read. The old code also used an
unquoted $(find ...) in a for-loop over a path containing "Application
Support", so the space split every path in half. That is the exact
word-splitting failure this machine's shell rules warn about, and it made
the check report "not installed" while the server was running.
2. It expected ~/Library/Logs/Claude/mcp-server-<name>.log. That naming is for
Claude Desktop's own connectors; a plugin's MCP server is logged by
LocalMcpServerManager into main.log. An absent file at a guessed path is a
statement about the guess, not about the world.
What the corrected check now reads, and what it establishes:
rpm/manifest.json -> jobbsok-probe, plugin_01NxHfqM495jtJzjVjRvgWDm,
marketplace "My Uploads", installed 2026-09-04T16:48:20Z
installed copy -> 4 files, manifest version 0.0.1 (no stale cache)
main.log 18:48:28 -> Connecting to plugin:jobbsok-probe:probe-tools
negotiated protocol version: 2025-11-25
Connected ... (1 tools)
process -> pid 12997, /usr/local/bin/python3, a child of Claude.app
So a plugin-declared stdio MCP server does spawn and connect on this Intel Mac,
on the host interpreter rather than in the sandbox. The negotiated version also
vindicates one design choice: probe_tools.py echoes the client's protocolVersion
instead of asserting its own default, and the client asked for 2025-11-25, which
is newer than the default the server would otherwise have claimed.
Still open, and deliberately not inferred from the above: whether a tools/call
to probe_ping actually returns. main.log records the connection lifecycle, not
individual calls, so that one is confirmed in the chat or not at all -- the
script says so rather than treating a missing line as an answer.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
155 lines
6.6 KiB
Bash
Executable file
155 lines
6.6 KiB
Bash
Executable file
#!/bin/bash
|
|
# Report, from the host, how far the Cowork probe has actually got.
|
|
#
|
|
# The probe asks four questions that only the operator can answer in a Cowork
|
|
# session. But the host-MCP one leaves hard traces on this Mac, and a trace is
|
|
# a measurement where "what did you see in the UI" is a recollection. This
|
|
# script reads those traces so each step is verified before the next begins.
|
|
#
|
|
# Two earlier queries in this script were WRONG and the measurement caught
|
|
# both; the corrections are written down here so they are not re-derived:
|
|
#
|
|
# 1. It searched for a directory named after the plugin. Cowork installs into
|
|
# rpm/plugin_<opaque id>/, so the name never appears in a path. The
|
|
# manifest at rpm/manifest.json is the index; read that instead.
|
|
# 2. It expected ~/Library/Logs/Claude/mcp-server-<name>.log. That naming is
|
|
# for Claude Desktop's own connectors. A plugin's MCP server is logged by
|
|
# LocalMcpServerManager into main.log instead. No file at the guessed path
|
|
# meant the guess was wrong, not that nothing had happened.
|
|
#
|
|
# It reports; it does not gate. Exit 0 when every check has landed, 1 while any
|
|
# is pending. bash 3.2-clean, ASCII-only, read-only.
|
|
|
|
set -u
|
|
|
|
SCRIPT_DIR=$(cd "$(dirname "$0")" && pwd)
|
|
REPO_ROOT=$(cd "$SCRIPT_DIR/.." && pwd)
|
|
PROBE_DOC="$REPO_ROOT/docs/cowork-probe.md"
|
|
FIXTURE="$REPO_ROOT/tests/fixtures/cowork-probe"
|
|
ARCHIVE="${JOBBSOK_PROBE_ARCHIVE:-/tmp/jobbsok-probe.plugin}"
|
|
MAIN_LOG="$HOME/Library/Logs/Claude/main.log"
|
|
SESSIONS="$HOME/Library/Application Support/Claude/local-agent-mode-sessions"
|
|
PLUGIN_NAME="jobbsok-probe"
|
|
SERVER_KEY="plugin:jobbsok-probe:probe-tools"
|
|
|
|
pending=0
|
|
done_() { printf ' [DONE] %s\n' "$1"; }
|
|
pend() { printf ' [PENDING] %s\n' "$1"; pending=$((pending+1)); }
|
|
info() { printf ' %s\n' "$1"; }
|
|
|
|
printf '\n=== 1. Probe vehicle and archive ============================================\n'
|
|
n=$(find "$FIXTURE" -type f 2>/dev/null | wc -l | tr -d ' ')
|
|
if [ "$n" = "4" ]; then
|
|
done_ "probe vehicle: 4 files under tests/fixtures/cowork-probe"
|
|
else
|
|
pend "probe vehicle: expected 4 files, found $n"
|
|
fi
|
|
if [ -f "$ARCHIVE" ]; then
|
|
a=$(unzip -Z1 "$ARCHIVE" 2>/dev/null | wc -l | tr -d ' ')
|
|
leak=$(unzip -Z1 "$ARCHIVE" 2>/dev/null | grep -cE '^\.git|STATE\.md|\.venv|^\.claude/')
|
|
if [ "$a" = "4" ] && [ "$leak" = "0" ]; then
|
|
done_ "archive: $ARCHIVE ($a files, no leaked entries)"
|
|
else
|
|
pend "archive: $ARCHIVE has $a entries and $leak leaked ones - rebuild it"
|
|
fi
|
|
else
|
|
pend "archive not built: $ARCHIVE"
|
|
fi
|
|
|
|
printf '\n=== 2. Is jobbsok-probe installed in Cowork? =================================\n'
|
|
# python3 does the walking: the path contains "Application Support", and an
|
|
# unquoted $(find ...) in a for-loop splits on that space. That exact bug is
|
|
# what made this check report "not installed" while the plugin was running.
|
|
INSTALL_DIR=$(python3 - "$SESSIONS" "$PLUGIN_NAME" <<'PYEOF'
|
|
import json, os, sys
|
|
sessions, name = sys.argv[1], sys.argv[2]
|
|
for dirpath, dirnames, filenames in os.walk(sessions):
|
|
if os.path.basename(dirpath) != "rpm" or "manifest.json" not in filenames:
|
|
continue
|
|
try:
|
|
data = json.load(open(os.path.join(dirpath, "manifest.json")))
|
|
except Exception:
|
|
continue
|
|
for p in data.get("plugins", []):
|
|
if p.get("name") == name:
|
|
print("%s\t%s\t%s\t%s" % (
|
|
os.path.join(dirpath, p["id"]), p["id"],
|
|
p.get("marketplaceName", "?"), p.get("updatedAt", "?")))
|
|
sys.exit(0)
|
|
PYEOF
|
|
)
|
|
if [ -n "$INSTALL_DIR" ]; then
|
|
D=$(printf '%s' "$INSTALL_DIR" | cut -f1)
|
|
ID=$(printf '%s' "$INSTALL_DIR" | cut -f2)
|
|
MP=$(printf '%s' "$INSTALL_DIR" | cut -f3)
|
|
AT=$(printf '%s' "$INSTALL_DIR" | cut -f4)
|
|
done_ "listed in rpm/manifest.json as $ID (marketplace: $MP, updated $AT)"
|
|
f=$(find "$D" -type f 2>/dev/null | wc -l | tr -d ' ')
|
|
if [ "$f" = "4" ]; then
|
|
done_ "installed copy holds 4 files, matching the archive"
|
|
else
|
|
pend "installed copy holds $f files, expected 4"
|
|
fi
|
|
v=$(python3 -c "import json,sys;print(json.load(open(sys.argv[1]))['version'])" "$D/.claude-plugin/plugin.json" 2>/dev/null)
|
|
if [ "$v" = "0.0.1" ]; then
|
|
done_ "installed manifest version is 0.0.1 - not a stale cached build"
|
|
else
|
|
pend "installed manifest version is '$v', expected 0.0.1 - Cowork served a cached build"
|
|
fi
|
|
else
|
|
pend "no plugin named $PLUGIN_NAME in any rpm/manifest.json"
|
|
fi
|
|
|
|
printf '\n=== 3. Did Cowork spawn probe-tools on THIS Mac? =============================\n'
|
|
printf ' (the -Host-MCP: question, measured rather than recalled)\n'
|
|
if [ -f "$MAIN_LOG" ]; then
|
|
conn=$(grep -a "Connected to $SERVER_KEY" "$MAIN_LOG" 2>/dev/null | tail -1)
|
|
neg=$(grep -a "$SERVER_KEY negotiated protocol version" "$MAIN_LOG" 2>/dev/null | tail -1)
|
|
if [ -n "$conn" ]; then
|
|
done_ "connected: $(printf '%s' "$conn" | sed 's/.*\[LocalMcpServerManager\] //')"
|
|
else
|
|
pend "main.log has no 'Connected to $SERVER_KEY' line"
|
|
fi
|
|
if [ -n "$neg" ]; then
|
|
done_ "handshake: $(printf '%s' "$neg" | sed 's/.*negotiated/negotiated/')"
|
|
else
|
|
pend "no protocol negotiation recorded"
|
|
fi
|
|
call=$(grep -a 'probe_ping' "$MAIN_LOG" 2>/dev/null | tail -1)
|
|
if [ -n "$call" ]; then
|
|
done_ "probe_ping seen in the log"
|
|
else
|
|
info "no probe_ping line in main.log yet - the connection lifecycle is"
|
|
info "logged there but an individual tool call may not be. Confirm the"
|
|
info "call by its answer in the Cowork chat, not by this line's absence."
|
|
fi
|
|
else
|
|
pend "no main.log at $MAIN_LOG"
|
|
fi
|
|
proc=$(pgrep -fl probe_tools.py 2>/dev/null | head -1)
|
|
if [ -n "$proc" ]; then
|
|
done_ "process alive: pid $(printf '%s' "$proc" | awk '{print $1}')"
|
|
interp=$(printf '%s' "$proc" | grep -oE '/[^ ]*python3?' | head -1)
|
|
[ -n "$interp" ] && info "interpreter: $interp (host, not sandbox)"
|
|
else
|
|
info "no probe_tools.py process at this instant - servers may be started on"
|
|
info "demand, so absence here is not a nei. The log above is the record."
|
|
fi
|
|
|
|
printf '\n=== 4. Are the four answers written down? ===================================\n'
|
|
RE='^- (Sesjonsmodus|Host-MCP|python3|CLAUDE_PLUGIN_ROOT): +(lokal VM|sky|ja|nei)\b'
|
|
c=$(grep -cE "$RE" "$PROBE_DOC" 2>/dev/null)
|
|
if [ "$c" = "4" ]; then
|
|
done_ "all four answered - plan Step 1 Verify passes"
|
|
else
|
|
pend "$c of 4 answered in docs/cowork-probe.md"
|
|
fi
|
|
grep -E '^- (Sesjonsmodus|Host-MCP|python3|CLAUDE_PLUGIN_ROOT):' "$PROBE_DOC" 2>/dev/null | sed 's|^| |'
|
|
|
|
printf '\n============================================================================\n'
|
|
if [ "$pending" -eq 0 ]; then
|
|
printf 'ALL CHECKS LANDED.\n'
|
|
exit 0
|
|
fi
|
|
printf '%s check(s) still pending.\n' "$pending"
|
|
exit 1
|