jobbsok/tests/fixtures/cowork-probe/probe_tools.py
Kjell Tore Guttormsen 0f92b2a1c3 docs(m1): record Cowork probe results and degradation branch
Plan Step 1. Builds the probe vehicle and the checklist; does NOT answer it.

The four questions all require the operator in a live Cowork session on this
Mac, and that is a fact about the measurement rather than a caveat: measured
2026-09-04, the session mode is not readable from disk (cowork_settings.json
carries only extraKnownMarketplaces; the desktop config carries
coworkNetworkMode and a trusted-folder list, neither states where a session
runs), and the other three each need a plugin installed in a live session.

The step's Verify is therefore RED on purpose and stays red until the operator
answers:
  test "$(grep -cE '...' docs/cowork-probe.md)" = 4   -> exit 1 (0 of 4 matched)
Validated in both directions: a fully answered copy exits 0, a copy with three
of four answered exits 1. The gate can pass, so its failure means something.

Probe vehicle: jobbsok-probe, exactly four files under
tests/fixtures/cowork-probe/ -- manifest, skills/probe-versjon/SKILL.md,
.mcp.json declaring the stdio server probe-tools, and probe_tools.py (standard
library only). Smoke-tested from Claude Code: initialize handshake completes,
tools/list returns probe_ping, tools/call returns an interpreter version, an
unknown tool name is rejected. What is NOT verified is whether Cowork ever
starts it -- that is the question.

Archive is built from an explicit four-file include list, never zip -r over the
repo root; verified to contain no .git, STATE.md, .venv or .claude/ entry.

The degradation branch for each possible nei is written down now, before any
answer is known, so it is not decided under pressure later.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-04 17:22:59 +02:00

114 lines
3.6 KiB
Python

#!/usr/bin/env python3
"""Minimal stdio MCP server for the Cowork probe (plan Step 1).
Standard library only, and deliberately so: the point of the probe is to learn
whether a plugin-declared stdio server appears in a Cowork session at all. A
server that failed because a dependency was missing would answer a different
question than the one being asked.
Exposes one tool, ``probe_ping``, which reports the interpreter that is actually
running it. Transport is newline-delimited JSON-RPC 2.0 over stdin/stdout, which
is what MCP stdio means; nothing is written to stdout except protocol messages.
"""
import json
import sys
SERVER_NAME = "probe-tools"
SERVER_VERSION = "0.0.1"
DEFAULT_PROTOCOL_VERSION = "2024-11-05"
TOOLS = [
{
"name": "probe_ping",
"description": (
"Return the interpreter version and executable path of the process "
"serving this tool. Used to establish whether a plugin-declared "
"stdio MCP server runs at all in a Cowork session."
),
"inputSchema": {"type": "object", "properties": {}, "additionalProperties": False},
}
]
def log(message):
"""Diagnostics go to stderr; stdout carries protocol traffic only."""
print("[probe-tools] %s" % message, file=sys.stderr, flush=True)
def ping_payload():
return {
"server": SERVER_NAME,
"server_version": SERVER_VERSION,
"python_version": sys.version,
"python_executable": sys.executable,
}
def handle(request):
"""Return a response dict, or None for a notification (no reply expected)."""
method = request.get("method")
req_id = request.get("id")
if req_id is None:
# A notification. `notifications/initialized` is the one we expect.
return None
if method == "initialize":
params = request.get("params") or {}
# Echo the client's protocol version when it offers one; guessing a
# newer version than the client speaks is how handshakes fail silently.
protocol_version = params.get("protocolVersion") or DEFAULT_PROTOCOL_VERSION
result = {
"protocolVersion": protocol_version,
"capabilities": {"tools": {}},
"serverInfo": {"name": SERVER_NAME, "version": SERVER_VERSION},
}
elif method == "tools/list":
result = {"tools": TOOLS}
elif method == "tools/call":
params = request.get("params") or {}
if params.get("name") != "probe_ping":
return {
"jsonrpc": "2.0",
"id": req_id,
"error": {"code": -32602, "message": "unknown tool: %r" % params.get("name")},
}
result = {
"content": [
{"type": "text", "text": json.dumps(ping_payload(), indent=2, sort_keys=True)}
],
"isError": False,
}
elif method == "ping":
result = {}
else:
return {
"jsonrpc": "2.0",
"id": req_id,
"error": {"code": -32601, "message": "method not found: %r" % method},
}
return {"jsonrpc": "2.0", "id": req_id, "result": result}
def main():
log("started on %s" % sys.version.replace("\n", " "))
for line in sys.stdin:
line = line.strip()
if not line:
continue
try:
request = json.loads(line)
except ValueError as exc:
log("undecodable line: %s" % exc)
continue
response = handle(request)
if response is not None:
sys.stdout.write(json.dumps(response) + "\n")
sys.stdout.flush()
return 0
if __name__ == "__main__":
sys.exit(main())