MCP: jonquil --mcp
Part of the architecture notes.
The full design record, with the reasoning and what was ruled out, is kept in docs/history/architecture/mcp.md.
Three namespaces serve the headless server. None requires a jonquil.gtk one, so the headless entry never opens a window:
jonquil.mcp.jsonis the JSON codec (decode,encode, string keys both ways) overorg.clojure/data.json, plus a small pre-pass that reads surrogate-pair escapes.jonquil.mcpis the protocol.handle-messageturns one decoded request into a response map or nil.serve!loops over a line reader and a line writer it is handed, skips blank lines and guards every message, so one bad request never ends the loop or mislabels its error.jonquil.mcp.toolsholds the six tool definitions andmake-tools. It keeps one evaluator per project selection (the same.jonquil.ednchoice the window makes) and registers each withjonquil.runtime-registry, soregistry/stop-all!stops them. An evaluation that passes its deadline is interrupted, and a dead one can't wedge its project.
Window mode. jonquil.mcp.window builds the twenty tools for jonquil --mcp --window. It never requires jonquil.gtk.window. It receives the window's ui map (the :on-ready map) and touches it only through gui-call, a thunk handed to the GTK thread with a bounded deref, so the serve thread never calls GTK itself.
The window supplies three notifying hooks: do-run! (with an optional notify), mcp-eval! and mcp-run-selection!. Each calls its notify once with {:refused msg}, {:busy warning}, {:problem text}, {:none s} or, from the eval worker, {:done {:result r :output s}}, and returns a reader (fn [] output-so-far). A notify only delivers a promise, so a slow or abandoned call can't block the worker. The window keeps its one evaluator; the tools never stop or swap it. jonquil_change_set_propose saves through the ui's :change-set-propose!* hook (the change sets part, with the client's name from make-window-tools' :client-fn) and applies nothing.
Listener and bridge. jonquil.mcp.listen lets a window with :mcp-listen? true serve those twenty tools to a socket client. It binds 127.0.0.1 on an OS-picked port, accepts one connection at a time and runs serve! over it. It records the port in a per-window registry file (<settings-dir>/mcp/<pid>.edn), which stop! removes and startup housekeeping reaps when the pid is dead.
jonquil --mcp-attach [DIR] is the other half: discover picks the live entry whose folder matches DIR (or the only live one), and bridge! copies lines between stdio and the socket without re-encoding them. Neither side touches jonquil.gtk.
The stdout rule. JSON-RPC owns the real stdout, and nothing else may write to it, in window mode too. jonquil.main/-main branches on mcp-args before start!, saves the real *out*, then points the root *out* at *err* with alter-var-root, so a println from any runtime thread lands on stderr. Only the :write-line function passed to serve! rebinds *out* to the saved writer, prints one line and flushes. The shutdown hook that calls registry/stop-all! covers this path as well as the window's.
This page is docs/guide/architecture/mcp.md in the Jonquil repository.