Skip to main content
Glama
Hackerprod

Agent Orchestrator

by Hackerprod

Agent Orchestrator

A shared MCP (Model Context Protocol) hub that lets multiple coding-agent CLIs register themselves and talk to each other — bidirectional, non-blocking, no fixed ports to remember.

Ships with adapters for OpenCode and for Claude Code itself (via a companion claude-channel plugin, since Claude Code has no server of its own); the adapter interface is designed so support for other agent CLIs (Codex, Pi, Antigravity, ...) can be added without touching the core.

Why This Exists

Bridges that spawn a subprocess, wait for it to finish, and collect the result don't let two agents actually converse — one side always blocks. Agent Orchestrator instead runs as a small standalone daemon that both agents connect to as MCP clients:

  • Either side sends a message at any time with agent_send. Delivery is live for every adapter — the target starts processing it in its active turn immediately, the same way a human's message would. Nothing is queued.

  • The reply comes back the same way: as the other agent's own agent_send call targeting you. There is no polling step in the middle — agent_read exists separately, for pulling a target's own past history (e.g. what it did during a long async turn), not for noticing new inbound messages.

  • Registration is explicit and per-instance: you register only the specific agent window you want exposed, so a "working" window you never register stays completely invisible to the orchestrator.

  • Every registration also names a session — an orchestrator-level group, agreed out of band with whoever you want to reach. Agents in different sessions can't see or address each other even if both are registered, which is what stops two unrelated conversations from crossing wires (a real failure mode this project hit and fixed — see Troubleshooting).

Related MCP server: central-mcp

Architecture

Claude Code ── stdio ──► claude-channel plugin ──┐
(this session)         (local :dynamic, REST+SSE)│
                                                    ├── HTTP (streamable MCP) ──► Agent Orchestrator daemon
opencode window A (registered) ────────────────────┘        :8765/mcp              (registry + per-agent adapter)
                                                                       │
opencode window B ─────────────────────────────────────────────  REST + SSE (per adapter)
(unregistered, never touched)                                         ▼
                                                              opencode A's own
                                                              HTTP API (:port), or
                                                              claude-channel's (:dynamic)
  • The daemon (agent-orchestrator) is a long-running process exposing one MCP endpoint over Streamable HTTP. Claude Code and every opencode window connect to it as independent clients (stateful MCP sessions, mcp-session-id header).

  • The registry tracks every registered agent ({agentId, agentType, session, host, port, label}) and, per agent, holds one persistent event stream plus a per-backend-session ring buffer (last 500 events, monotonic cursor — reading history never silently stalls once one passes the cap). Every lookup is scoped by session: an agentId that exists but belongs to a different session is treated as not found.

  • Adapters translate the registry's generic calls into a specific backend's protocol. The opencode adapter talks to opencode's own REST + SSE API (/session, /session/:id/prompt_async, /event). The claude adapter talks the same REST + SSE shape, but to a local claude-channel companion process (see below), not to Claude Code directly.

Reaching Claude Code

Claude Code, unlike opencode, has no server of its own to adapt to — so it can't be registered directly. claude-channel (src/channel-plugin/claude-channel.ts) closes that gap: it's a Claude Code "channel" plugin (research preview), spawned by Claude Code itself as a stdio MCP subprocess when the session starts with --channels/--dangerously-load-development-channels. It exposes a small local REST+SSE surface that mirrors the opencode adapter's shape, so the claude adapter can register and talk to it exactly like any other agent — inbound text gets pushed straight into the live session via the claude/channel notification (real push, not polling), and Claude's replies (via the plugin's reply tool) come back out over SSE.

The port is dynamic (OS-assigned, 0 by default) specifically so multiple Claude Code windows can each run their own instance without colliding — there's no fixed address to hardcode. Since there's nothing external to detect a not-yet-running process's port from, the instance reports its own to the model that owns it: baked into that session's connect-time system prompt, and re-checkable anytime via the plugin's whoami tool. Every live instance's {pid, port, host, startedAt} is also on disk at runtime/claude-channel-instances/<pid>.json, garbage-collected on each new startup (a file whose pid is no longer alive is a previous instance that crashed or was force-killed without cleanup, not a conflict — a dynamic port can't collide by construction). See skills/agent-orchestrator/references/claude-channel.md for setup.

With more than one claude-type agent registered in the same session (one per window), list_agents won't tell them apart beyond the label each was given at registration — always check it before assuming an agentId is yours. Windows on genuinely unrelated tasks should use different session names instead, so they can't see each other at all.

Prerequisites

  • Node.js >= 18.0.0

  • OpenCode CLI installed and authenticated (npm install -g opencode-ai, then opencode login)

Installation

npm install
npm run build

Running the daemon

npm start
# Agent Orchestrator listening on http://127.0.0.1:8765/mcp

It needs to be running before either Claude Code or opencode connects to it. Run it in its own terminal, or set it up as a background service — there's no auto-start built in on purpose, since it's a shared resource independent of any one agent's lifecycle.

npm run start:supervised runs the daemon under scripts/supervisor.js: stdout/stderr are captured to runtime/daemon.out.log / runtime/daemon.err.log, every start/exit is recorded in runtime/daemon.supervisor.log (exit code/signal), and the daemon is respawned with backoff after a crash. The daemon also installs crash guards that log uncaughtException / unhandledRejection stacks synchronously before exiting — a silent death becomes a diagnosable log line instead of a vanished hidden console. On Windows, pair it with a Task Scheduler entry at logon so it survives reboots.

Environment variables

Variable

Default

Description

AGENT_ORCHESTRATOR_HOST

127.0.0.1

Host the MCP endpoint binds to

AGENT_ORCHESTRATOR_PORT

8765

Port the MCP endpoint listens on

Persistence

Registrations and event history survive daemon restarts. Every change to the registry (an agent registering or unregistering, an event landing in a buffer) is written to runtime/state.json, debounced to at most one write every 500ms, with a final flush on shutdown. On boot the state is loaded back: each persisted agent is re-validated with a live health check and only re-registered when it's still reachable (a window that died while the daemon was down is skipped), and its per-session event buffers are restored so agent_read history survives. MCP client connections themselves are transient — each client must reconnect and re-initialize after a restart — but the registrations they rely on persist.

Connecting Claude Code

claude mcp add --transport http agent-orchestrator -s user http://127.0.0.1:8765/mcp

Connecting opencode

opencode mcp add
# follow the prompt: transport = http, url = http://127.0.0.1:8765/mcp

Registering an agent instance

This is the deliberate, per-instance step that keeps unregistered windows invisible to the orchestrator — there's no automatic/background registration, no plugin hook, no polling for new instances. You decide, per window, when it joins.

The full operational guide (how to detect your own port safely, the complete tool reference, a worked example) lives in skills/agent-orchestrator/SKILL.md — written to be followed by any agent CLI, not just Claude Code. AGENTS.md at the repo root points opencode (and other AGENTS.md-aware CLIs) at it automatically.

MCP Tools

Every tool below takes a mandatory session — an orchestrator-level group name, not a backend chat id (that's backendSessionId, a separate thing). Only agents sharing the same session can see or address each other; agree on a name with whoever you want to reach before registering.

register_agent

Register a running agent instance. Input: { agentType, session, port, host?, label? }. Output: the stored AgentRecord. The first registration with a new session name creates it.

unregister_agent

Input: { agentId, session }. Stops its event stream and drops its buffers. Returns false if that agentId isn't registered in that session — even if it exists in a different one.

list_agents

Input: { session }. Returns every agent registered in that session — nothing from any other.

agent_create_backend_session

Creates a chat thread on the target's own backend (e.g. a fresh opencode session) — not related to the orchestrator session beyond needing it to find the agent. Input: { agentId, session, title?, agent? }. Output: { backendSessionId, title }.

agent_list_backend_sessions

Input: { agentId, session }. Returns every chat thread known to that agent's backend (not just ones created through the orchestrator — opencode's session store is shared across all its own instances).

agent_send

Input: { agentId, session, text, backendSessionId?, agent?, system? }. Delivers live into the target's active turn for every adapter — this call itself returns as soon as the backend accepts it ({ accepted: true }), not once the target replies, but the reply itself also arrives live, as the target's own agent_send call back to you. Don't follow it with an agent_read loop; there's nothing there to wait for. Fails if agentId isn't registered in session. backendSessionId is optional: omit it to use the session's single canonical conversation — the daemon resolves (or creates) one backend session per (session, agent) automatically and persists it (claude → 'default'; opencode → a dedicated session titled with the orchestrator session name).

agent_read

Input: { agentId, session, backendSessionId, sinceCursor? }. Output: { events, cursor, hasMore, droppedEvents? }. Retrieves a target's past event history — useful for checking what an agent did during a long async turn, not for receiving new messages (those arrive live via agent_send, see above). droppedEvents appears if your cursor pointed at history already evicted from the 500-event ring buffer.

agent_status

Input: { agentId, session }. Returns the agent's record plus a live reachable health check.

user_send

Send a message to the human user. Input: { session, text }. Output: { accepted }. The Telegram bot delivers it to the user's chat — it only ever reaches the human, never another agent.

user_send_file

Send one or more local files (+ optional message) to the human user. Input: { session, paths, text? }. Output: { accepted, files }. The bot uploads each file to Telegram (sendDocument; text becomes the caption on the first file only). All paths must exist on the daemon's machine. Delivery is best-effort — if the bot isn't running, the event is not queued.

reply (claude-channel only, not the orchestrator daemon)

A separate MCP server — the local claude-channel process, not agent-orchestrator itself. Input: { text }. Writes to that channel's own local transcript, for a human directly watching it (e.g. via /events). It does not reach another orchestrator agent; to answer whoever messaged Claude Code, call agent_send targeting their agentId, same as any other agent.

Adding a new agent type

Implement the AgentAdapter interface (src/adapters/types.ts) for the new backend and register it in src/adapters/index.ts. The registry, event buffering, cursor logic, and every MCP tool are already generic — they don't know or care which adapter they're talking to.

Telegram bridge

A parallel, human-facing channel that runs completely outside the agent graph: a separate telegram-bot/ process (grammY) talks to an internal /bridge REST + SSE surface on the daemon. It mirrors inter-agent messages live to a Telegram chat and lets a human send messages to any agent — without agents ever learning the channel exists. No new agent type, no registration, no changes to adapters or to the registry's event buffers; the daemon just emits a mirrored message onto an in-memory bus whenever an agent_send is delivered.

Endpoint

Description

GET /bridge/sessions

List all orchestrator session names

GET /bridge/agents?session=X

List agents registered in session X

GET /bridge/threads?session=X&agentId=A

List A's backend chat threads

GET /bridge/read?session=X&agentId=A&backendSessionId=B&sinceCursor=N

Read A's past events for thread B since cursor N

POST /bridge/send

Send a message to an agent; body {session, agentId, text, backendSessionId?, agent?, system?} (backendSessionId omitted → the daemon resolves the agent's canonical conversation automatically)

GET /bridge/events?session=X

SSE stream of inter-agent messages in session X

Every /bridge route requires the shared BRIDGE_TOKEN, sent either as Authorization: Bearer <token> or ?token=<token>. Leave BRIDGE_TOKEN unset to disable the surface entirely (all routes return 403).

Variable

Default

Description

BRIDGE_TOKEN

Shared secret for /bridge; unset disables the surface

For a noob-friendly setup, run cd telegram-bot && npm run setup — it asks for the bot token interactively (create the bot with @BotFather, message it once so your chat id can be detected), generates the shared BRIDGE_TOKEN, and writes both telegram-bot/.env and the root .env automatically. Then restart the daemon (it loads .env at startup) and run npm run dev in telegram-bot/.

Run and configure the Telegram side from telegram-bot/ — see telegram-bot/README.md for its env vars and start instructions. The daemon must be restarted (with BRIDGE_TOKEN set) to pick up the /bridge surface.

Troubleshooting

Port already in use — set AGENT_ORCHESTRATOR_PORT to something else and re-register both clients against the new URL.

register_agent fails with "Could not verify a reachable agent" — the port doesn't respond, or it's not actually opencode. Re-run the port-detection step from inside the target window; ports change every time opencode restarts.

Sent a message but the other agent never seemed to receive it — check you called agent_send, not agent_read (there's nothing to receive by polling) or, on the Claude Code side, the channel's own reply tool (that only writes to its local transcript, never to another agent).

Task finished but no completion report came back — reporting back isn't automatic. The receiving agent has no guarantee it consulted the skill before acting, so its default is to answer wherever it normally would (its own console/human), not through the channel. Ask for the report explicitly in the message: "reply via agent_send to <your agentId> when done."

A reply landed on the wrong agent, or agent_send/list_agents claims an agentId doesn't exist even though it was just registered — almost always a session mismatch: either the two sides agreed on different names, or one side is reusing a memorized agentId from an earlier, unrelated session instead of re-checking list_agents for the current one. This is exactly the isolation working as intended (see Why This Exists) — re-confirm both sides are using the same session string, not just guessing.

Logsruntime/orchestrator.log (created next to wherever you run the daemon from).

Development

npm run typecheck
npm run lint
npm test
npm run build

Tests are network-free: adapter SSE parsing and the registry's cursor/ring-buffer logic are both tested against fakes, no real opencode instance required.

License

MIT. See LICENSE for details.

A
license - permissive license
-
quality - not tested
B
maintenance

Maintenance

Maintainers
Response time
Release cycle
Releases (12mo)
Commit activity

Resources

Unclaimed servers have limited discoverability.

Looking for Admin?

If you are the server author, to access and configure the admin panel.

Related MCP Servers

View all related MCP servers

Related MCP Connectors

  • Real-time chat hub for AI agents — Claude Code, Cursor, Cline, Codex over MCP or REST.

  • User-owned memory for AI agents, Copilot, Claude, IDEs, CLIs, and chat apps over remote MCP.

  • Agent-native collaboration network: orchestrate a team of long-running agents from any MCP client.

View all MCP Connectors

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/Hackerprod/Agent-Orchestrator'

If you have feedback or need assistance with the MCP directory API, please join our Discord server