MCP Server Design - ptweezy/cronstable GitHub Wiki
Design document: a Model Context Protocol (MCP) server for cronstable
Status: Implemented · Author: Principal Engineering · Date: 2026-07-08 · Target spec: MCP 2025-11-25 (the server also serves 2026-07-28)
This page summarizes the MCP server's original design and how the
implementation differs. The server is in cronstable/mcp.py; its stdio
bridge is in cronstable/mcpcli.py. The full design dated 2026-07-08 is
available in this page's history.
The authoritative sources are:
- MCP: the user-facing documentation (enabling the server, the tool/resource/prompt catalog, client wiring, the stdio bridge, security).
mcpconfiguration reference: every shipped configuration field.cronstable/mcp.pyandcronstable/mcpcli.pyin the source tree: the implementation itself.- HTTP Control API: the REST surface the tools project,
including the
POST /mcpendpoint.
What the design decided
The document's headline decisions, all of which shipped (details that moved are in the divergence list):
- Implement a small pure-Python MCP layer on the existing aiohttp server
to avoid adding the official
mcpSDK's dependencies to multi-architecture builds:pydantic-core,cryptography,rpds-py,starlette,uvicorn,anyio, andhttpx. - Two transports, one core: a stateless Streamable HTTP
POST /mcproute embedded in the existing web server (same listeners, auth, and reload lifecycle), plus thecronstable mcpstdio bridge, a urllib frame-proxy with no daemon imports, for local desktop clients. - Safe by default:
readOnly: truestrips every mutating tool. The default toolset isobserve. Mutating tools require an explicitconfirm(anddry_runwhere a preview exists). Annotations are treated as UX hints, never a security boundary. - Authentication reuses what cronstable already has:
web.authTokenbearer tokens, filesystem-gated Unix sockets, and mutual TLS (mTLS). It fails closed on tokenless routable listeners. No OAuth for the self-hosted case. - Target spec revision
2025-11-25with a stateless, session-free server.
Shipped divergences
Section references (§) point into the original design text, in this page's history.
--validateshipped as--check(§9): the stdio bridge's self-check flag iscronstable mcp --check.- No per-run job resource template (§5.3): the proposed
cronstable://jobs/{name}/runs/{run_id}shipped ascronstable://jobs/{name}/runs(the whole retained history, norun_id). The directed acyclic graph template kept{run_key}. - Three additional configuration keys (§7): the shipped
mcp:block also takesallowUnauthenticated,resources, andprompts. - Stricter fail-closed rule (§6/§7): no bind-safe-listeners-and-warn mode
exists for a mixed listen set. Startup raises a
ConfigErrorwhenever any routable listener lacks a token.mcp.allowUnauthenticated: trueis the explicit override. - Offset paging, not opaque cursors (§5.1):
cron_get_statusandcron_list_jobstakeoffset/limitand returnnextOffset; other list tools take a clampedlimit. Only the two log-tail tools take acursor, an integer position for polling newly appended lines. - Resources and prompts are toolset-scoped (§5.3/§5.4): the
dagsresource templates and thewhy_did_dag_run_fail/backfill_planprompts require thedagstoolset. - Both protocol eras (§4.3): the design's method set targets
2025-11-25. The server also serves the stateless2026-07-28revision on the same endpoint, withserver/discover, per-request_meta, header validation,resultType, and cache hints, and the stdio bridge copies a modern frame's_metainto the headers that revision requires. See protocol revisions. - Argument completion (§4.3): the server adds
completion/complete, which completes prompt arguments and resource template variables from job names, workflow names, and recent run keys. - TLS is served by the daemon, not only by a reverse proxy (§6): where the
design text says to terminate TLS/mTLS in a reverse proxy and cites the
HTTP control API page for it,
web.listenacceptshttps://addresses served from aweb.tlsblock, andweb.tls.clientCamakes those listeners require a client certificate. That mTLS listener satisfies the fail-closed token gate on its own, exactly as a proxy-terminated one does. Plainhttps://does not. See listener TLS.