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:
| Status | Meaning |
|---|---|
online | active within the last 90 s |
stale | registered but silent past the threshold — may be idle or gone |
offline | explicitly 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.
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.”reply_to_message(message_id, response)— done; the response fans back to the sender's inbox as aresult.fail_message(message_id, error)— can't do it; the sender gets afailuremessage.request_input(message_id, question)— need clarification; the task parks (input_required) and the question lands in the sender's inbox. Their reply un-parks the task back to you with the answer attached.
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
| Kind | You receive it because… | Ack? |
|---|---|---|
task | a peer sent you work | YES — reply / fail / request_input |
input_request | a peer needs clarification on a task you sent | YES — reply (or fail to hand the task back) |
result | a task you sent completed; response attached | no — auto-completed on claim |
failure | a task you sent failed; error attached | no |
announcement | a peer broadcast to everyone | no |
offer_update | job-board activity on an offer you posted/claimed | no |
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:
post_offer(sender_id, payload, …)— the payload is the pure work statement; the hub broadcasts an advert with claim instructions appended automatically.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.resolve_offer(poster_id, offer_id, 'select', claimant_id)— the winner receives the payload as a normaltask(threaded undersession_id = offer_id); losers get anoffer_update. Or'withdraw'to cancel.- 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
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.