Skip to content

feat(archon): MCP acquisition tools (search/enqueue/list/cancel) over a serve-hosted socket#617

Merged
forkwright merged 1 commit into
mainfrom
feat/mcp-acquisition
Jul 15, 2026
Merged

feat(archon): MCP acquisition tools (search/enqueue/list/cancel) over a serve-hosted socket#617
forkwright merged 1 commit into
mainfrom
feat/mcp-acquisition

Conversation

@forkwright

Copy link
Copy Markdown
Owner

Closes #609. An agent can now drive acquisition natively — search → enqueue → observe → cancel — over the harmonia mcp surface, no hand-rolled HTTP loop or banner-scraped credentials.

harmonia serve hosts a Unix-domain-socket listener holding the live search/queue services; harmonia mcp forwards 4 acquisition tools over it (the 4 offline maintenance tools keep running in-process). Both processes derive the same socket path deterministically from database.db_path. Redaction stays ON for all tool output (LLM transcripts leave the box); enqueue_download takes a release_id (resolved via #608) or a magnet (http rejected), SSRF-guarded on both arms.

Hardened per a 3-lens adversarial review (4 findings, all fixed): the wire read is capped (no unbounded-line OOM); the socket lives in a dedicated harmonia-mcp/ 0700 subdir at 0600 (no chmod of a shared parent, and the pre-chmod perms window is closed); a second instance is refused via a liveness check rather than clobbering a live socket; and shutdown drains in-flight tool calls (TaskTracker + grace) instead of dropping them. Gate green (full workspace, incl. the MCP-only acceptance test driving search→enqueue→observe over the bridge).

… a serve-hosted socket

`harmonia mcp` gained the acquisition surface #609 asks for: an agent with
only the MCP connection can search -> enqueue -> observe completion without
touching HTTP or the startup banner.

The stdio MCP process is bare (no config, DB, or services); the live
DynSearchService/DynQueueManager exist only inside `harmonia serve`, and an
enqueue/cancel MUST reach the running syntaxis service or the row is invisible
until restart (the #499/#469 defect class). So the 4 acquisition tools are not
in-process adapters — `harmonia serve` hosts a bridge over a Unix domain
socket, and the stdio process forwards `tools/call` to it.

Serve side (crates/archon/src/mcp_bridge.rs): right after AppState
construction, spawn a UDS listener holding Arc clones of the same
search/queue/db AppState got. Framing is newline-delimited JSON-RPC, one
request line -> one response line per connection request. Bind is FATAL at
startup (mirrors the HTTP bind posture): unlink any stale socket, create the
parent dir 0700, chmod the socket 0600 after bind. Shutdown via a
shutdown_token child; the socket is unlinked on exit. `#[cfg(unix)]` with a
stub error elsewhere.

Stdio side (crates/archon/src/mcp.rs): tools/list returns all 8 tools (the 4
offline maintenance tools keep running in-process — db_migrate et al. must
work with no server). For the 4 acquisition tools, call_tool connects to the
UDS per call under a config timeout and forwards the request. Socket absent /
refused -> a clean tool_error ("harmonia server is not running (socket <path>
unavailable); start 'harmonia serve'"), never a protocol failure. The
tool_success envelope now carries a JSON payload in structuredContent plus a
compact text serialization.

The 4 tools are thin adapters over the SAME service methods the HTTP routes
call, no HTTP re-implementation:
- harmonia_search_releases -> DynSearchService::search (redacted results)
- harmonia_enqueue_download -> DynSearchService::resolve_release (the #608
  by-reference seam) or a magnet arm, then validate_download_url +
  DynQueueManager::enqueue; exactly one of release_id|magnet
- harmonia_list_downloads -> a shared paroche download-row query helper
- harmonia_cancel_download -> DynQueueManager::cancel

Auth is filesystem: the surface acts as local operator (socket 0600, owner =
serve's user). Redaction STAYS ON for MCP output — the client is an LLM whose
transcripts leave the box, so paroche::redact strips every outbound
download_url; the SSRF guard is retained on both enqueue arms.

Config: horismos gains `[mcp]` (socket_path, call_timeout_secs >=
zetesis.search_timeout_seconds). The default socket path is derived
deterministically as a sibling of database.db_path so BOTH processes agree.
`mcp.` is RESTART_REQUIRED (no live-rebind for a local socket). `harmonia mcp`
gained --config to resolve the socket path + timeout at startup.

Tests: per-tool dispatch tests with local stub services (redaction asserts no
SECRET reaches output; both/neither-source, http-in-magnet, private-tracker
rejections; release_id resolves to an unredacted queue url + a redacted
response); stdio degradation (socket-absent actionable error, offline tools
work with no socket, timeout path); and an acceptance test driving search ->
enqueue -> poll-list-until-completed over a real UDS bridge + real syntaxis
DownloadQueue, entirely via the MCP surface (no HTTP, no exousia, no banner).

Closes #609

Gate-Passed: kanon 0.1.6 +stages:fmt,check,clippy,nextest,lint sha:e86adf2cc46b0ec6188b1beb1bd31f809357bd47
@forkwright forkwright force-pushed the feat/mcp-acquisition branch from 938183a to 0b1d459 Compare July 15, 2026 15:37
@forkwright forkwright merged commit 25347eb into main Jul 15, 2026
12 checks passed
@forkwright forkwright deleted the feat/mcp-acquisition branch July 15, 2026 15:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

archon mcp: expose acquisition tools (search / enqueue / queue status) so agents can drive music acquisition natively

1 participant