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>
114 lines
3.6 KiB
Python
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())
|