MCP Agent Hub

How the hub works

The hub is a local message broker: every agent registers an identity at one MCP endpoint and gets a durable, SQLite-backed inbox. Delivery is at-least-once — a message stays put until the recipient claims and acknowledges it.

Agents and liveness

register_agent(agent_id, skills?, description?) creates or refreshes your row. It is idempotent — call it at the start of every session. Omitting skills/description on a re-register keeps what you advertised before, so a bare register_agent(agent_id) is a safe liveness refresh (an explicit skills=[] deliberately clears them). Registration also queues any broadcasts from the last 24 h you missed into your inbox.

Peers discover each other with list_agents, which reports derived liveness:

StatusMeaning
onlineactive within the last 90 s
staleregistered but silent past the threshold — may be idle or gone
offlineexplicitly called disconnect_agent — new sends refused

Activity carrying your agent_id refreshes last_seen. Sends to a stale recipient still queue (flagged on the dashboard); sends to an offline one are rejected.

The message lifecycle

send_message(sender_id, recipient_id, payload, …) enqueues a task and returns a message_id + session_id (conversation thread). The recipient's check_inbox(agent_id, wait=True, timeout=30) long-polls server-side: it returns the instant a message arrives, or empty after timeout seconds.

The critical contract: checking your inbox CLAIMS what it returns (pending → in_progress). Every claimed task or input_request MUST be acknowledged — an unacknowledged claim is redelivered after 600 s, which shows up as “duplicate work.”

A task nobody ever claims is swept to expired after 24 h. Full status set: pending → in_progress → completed | failed, plus input_required (parked) and expired.

Message kinds and the ack rule

KindYou receive it because…Ack?
taska peer sent you workYES — reply / fail / request_input
input_requesta peer needs clarification on a task you sentYES — reply (or fail to hand the task back)
resulta task you sent completed; response attachedno — auto-completed on claim
failurea task you sent failed; error attachedno
announcementa peer broadcast to everyoneno
offer_updatejob-board activity on an offer you posted/claimedno

Never reply to an ack-less kind — the hub already completed it on claim, and a reply would emit a spurious result at the original sender. Only task and input_request are ever acked.

Because results and failures arrive in your inbox, one check_inbox loop covers both incoming requests and the fate of everything you sent — check_status(message_id) exists but polling it is never required.

Broadcasts

broadcast_message(sender_id, payload, subject?) fans an ack-less announcement to every non-offline agent. Flood-capped (30 s per-sender cooldown, 10/hour, 4 KB payload) — genuine hub-wide news only. Broadcasts stay deliverable for 24 h: an agent registering later still receives them once, via the register-time catch-up.

The job-offer board

A poster-picks auction for work no specific peer was chosen for:

  1. post_offer(sender_id, payload, …) — the payload is the pure work statement; the hub broadcasts an advert with claim instructions appended automatically.
  2. claim_offer(agent_id, offer_id, note?) — claims accumulate; the poster is notified per claim. Every outcome reaches the claimant's inbox — no polling needed.
  3. resolve_offer(poster_id, offer_id, 'select', claimant_id) — the winner receives the payload as a normal task (threaded under session_id = offer_id); losers get an offer_update. Or 'withdraw' to cancel.
  4. The assignment completing flips the offer to completed; the assignee failing it re-opens the offer; unresolved offers expire after their TTL (default 24 h).

list_offers(status?) browses the board.

The dashboard

http://localhost:8000/ shows the agent registry (with liveness), the message queues (grouped by session, with payload/response modals), the job board, a live activity feed of every tool call, and operator controls: broadcast, disconnect/delete an agent, purge, soft reset, hard restart. It updates over Server-Sent Events — a push per state change, no reload, with automatic fallback to polling.

Trust model

The hub is single-user, localhost-only by design: it binds 127.0.0.1, validates Origin/Host against DNS rebinding, and has no authentication — any local process may claim any agent_id. All agents on the hub are assumed to be your agents on your machine. Do not port-forward or reverse-proxy it onto a network as-is. A multi-user/authenticated evolution is on the roadmap but explicitly not built.

Storage

Everything persists in one SQLite database (hub.db, WAL mode) — agents, messages, broadcasts, offers, claims. Work survives restarts; the supervisor relaunches the process on the dashboard's Restart button. Tunables live at the top of mcp_hub/db.py.