gptqueue
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@gptqueuesend a task to agent Alice to process the report"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
gptqueue
Inter-agent message queue over MCP + Redis.
Start here: docs/QUICKSTART.md — install, connect two agents, and land your first acknowledged task in ten minutes. The delivery contract you operate under (at-least-once semantics, idempotency obligations, dead-letter and renewal policy) is specified in docs/OPERATING_RULES.md.
gptqueue lets AI agents (Claude Code, Codex, Gemini CLI, or any MCP-compatible client) discover each other and exchange messages through a shared Redis-backed queue. Each agent registers with a name and description, then sends and receives typed messages via MCP tool calls.
Architecture
MCP (stdio)
┌─────────────┐ ◄──────────────────────► ┌──────────────┐
│ AI Agent A │ │ │
└─────────────┘ │ gptqueue │
MCP (HTTP) │ MCP server │◄─────► Redis
┌─────────────┐ ◄──────────────────────► │ │
│ AI Agent B │ │ (sessions) │
└─────────────┘ └──────────────┘Each agent gets its own bounded inbox queue in Redis. Messages are delivered atomically via a Lua script that enforces queue size limits.
Agent identity is backed by Redis session records with TTL-based leases, so sessions survive process restarts and work across transport boundaries.
Related MCP server: agent-mesh
Components
Component | Description |
MCP server ( | MCP server exposing tools for agent communication |
HTTP transport ( | Streamable HTTP server -- no bridge needed for shared hosting |
Core ( | Transport-agnostic session store, mailbox store, and type definitions |
PTY wrapper ( | Wraps a CLI process in a PTY, watches Redis for incoming messages, and injects notifications when the process is idle |
Hook script ( | Claude Code hook for startup context injection and stop-gate (blocks exit if inbox has unread messages) |
Design Reports
MCP Tools
Tool | Description |
| Register with a name, role, and description. Returns a |
| Send a typed message ( |
| Blocking pop from your inbox (default timeout: 5s; accepted range: 0–60 whole seconds). Supports optional |
| Discover agents with readable labels, messaging names, directories, UUIDs, and online/offline status |
| Check queue depth and capacity for one or all agents |
| Close the current session but preserve the mailbox. Messages remain queued for later reconnection. Supports optional |
| Unregister and delete all queue data (destructive). Supports optional |
| Claim custody of a worktree for this session. Handles initial claim, graceful re-claim, and successor takeover (forfeited worktrees require an |
| Release a held worktree, recording a structured handoff for the next custodian. Only the current custodian session may release |
| Inspect a worktree's custody record, or list every stored record. Expired leases are forfeited lazily |
| Register a durable actor profile and launch contract in the shared actor directory. The durable actor identity is DERIVED from the calling session's registered agent name (there is no |
| Classify a durable actor's runtime presence ( |
| Atomically claim up to |
| Acknowledge a |
| Renew an outstanding |
| List the calling agent's dead-letter queue (DLQ) entries, newest first. A message is dead-lettered after it has been recovered (re-queued) more than |
| Move one dead-lettered message (by |
Registered shell (additional tools)
The registered shell (bin/gptqueue-session) registers four additive tools on top of the table above:
Tool | Description |
| Find exact agent candidates by declared purpose and identity. Ambiguous matches are never routed automatically; online does not imply activation readiness |
| Inspect an exact mailbox, runtime binding, published capabilities, declared role and activation readiness. Omit |
| Inspect one message's queue, claim, acknowledgement or dead-letter evidence without consuming it. Missing retained evidence means unknown, not delivered |
| Declare this connection's readable label, purpose and kind. A declaration is a discovery hint, never proof of controller authority or permission to take over another mailbox |
package.json maps only gptqueue-server, gptqueue-http, and gptqueue-pty as installed commands; the registered-shell and wrapper entry points are repo-local — invoke them as node bin/<entry> (for example node bin/gptqueue-session).
Dead-letter queue (provisional policy)
Lazy recovery (in claims-recover.lua) counts, per message, how many times a delivered-but-unacked task has been re-queued. Once that count exceeds a cap, the task is moved to the actor's dead-letter queue (gptq:dlq:<actor>) instead of the inbox, so a message that repeatedly fails after expiry cannot bounce forever. The competing constants below are provisional policy: they are named, documented placeholders pending principal calibration, and tuning them is policy, not code.
RECOVER_CAP = 5— max recoveries of one message before lazy recovery quarantines it to the DLQ.DLQ_MAX_LENGTH = 1000— per-actor DLQ bound; the newest entries are kept and older trimmed entries are dropped (they were already dead-lettered once).RECOVER_COUNTER_TTL_SECONDS = 604800— TTL on each per-message recovery counter, bounding orphans (7 days).
Acknowledging a claim clears the counters of its tasks, and dlq_requeue restores a fresh budget, so a message can be inspected and re-driven indefinitely. Only id-bearing envelopes are counted; a legacy envelope without a stable message id is re-queued without counter accounting.
Claim renewal (provisional budget policy)
renew_claim extends an outstanding claim's expiry so a runtime can keep a long-running batch alive without surrendering it to lazy recovery. The extension is applied from the renew instant but is capped so a claim can never be renewed more than CLAIM_LIFETIME_BUDGET_SECONDS past its original claimed_at. This budget is provisional policy: it is a named, documented placeholder pending principal calibration, and tuning it is policy, not code.
CLAIM_LIFETIME_BUDGET_SECONDS = 86400— max lifetime of any claim measured fromclaimed_at(1 day);renew_claimrefuses an extension that would push past it (budget_exceeded) and cannot resurrect an already-expired claim (claim_expired).
Prerequisites
Node.js >= 20
Redis running locally (default
redis://127.0.0.1:6379)
Install
From npm (ships prebuilt; provides gptqueue-server, gptqueue-http and gptqueue-pty):
npm install -g gptqueuegptqueue-pty needs the native node-pty module. If your npm holds back
dependency install scripts, approve node-pty (npm install-scripts approve node-pty)
so it can build.
From source:
git clone https://github.com/rahulrajaram/gptqueue.git
cd gptqueue
npm install # builds automatically via prepareTransports
Stdio (direct, single-client)
For local use with one MCP client:
node /path/to/gptqueue/dist/mcp-server/index.js [agent-name]Or set GPTQ_AGENT_NAME in the environment.
HTTP (shared, multi-client)
For shared hosting without bridges like supergateway:
node /path/to/gptqueue/dist/transports/http.js --port 3001Each connecting client gets its own MCP session backed by Redis. Sessions survive reconnects.
Security boundary
The HTTP server binds loopback (127.0.0.1) by default. For the local
single-host deployment the security boundary is this loopback bind: only
processes on the same machine can reach the MCP endpoint, so no token is
required by default.
Two environment variables control the surface:
Variable | Default | Description |
|
| Interface to bind. Set to a non-loopback address only when the server must be reachable beyond one host. |
| (none) | Shared |
Refusal rule: if GPTQUEUE_HOST is set to a non-loopback address and
GPTQUEUE_HTTP_TOKEN is unset or empty, the server refuses to start
(exit non-zero before listening, with a clear message) rather than expose an
unauthenticated MCP surface to every reachable host. If the host is loopback
(127.0.0.1 / localhost), the token is optional and the local default remains
loopback + tokenless. The startup log states the bind host and whether token
auth is active (it never prints the token value).
Example — token-authenticated server reachable from other machines:
GPTQUEUE_HOST=0.0.0.0 \
GPTQUEUE_HTTP_TOKEN=$(openssl rand -hex 32) \
nohup node /path/to/gptqueue/dist/transports/http.js --port 3001 > /tmp/gptqueue.log 2>&1 &GPTQueue is an external/shared coordination plane; use it in place of native in-session agent collaboration for a workflow, not concurrently with it.
Tool responses retain their legacy text JSON and also expose normalized
structuredContent. Failures use stable codes such as REDIS_UNAVAILABLE,
SESSION_UNAVAILABLE, AGENT_NOT_REGISTERED, and QUEUE_FULL.
Health check: GET /health returns {"status":"ok","sessions":N}.
Wake launch allowlist (PROVISIONAL)
When a durable wake_if_offline actor is woken, the server spawns the
actor's registered launch_command directly (shell:false, never
interpolated into a string). Historically that command was copied verbatim,
which let a caller register an arbitrary program (or a shell like
/bin/sh -c <payload>) and trigger it via send_message → wake. To close
that hole, GPTQueue now gates every runtime launch behind an operator
allowlist.
The allowlist is an operator-authored file at
$XDG_CONFIG_HOME/gptqueue/launch-allowlist.json (default
~/.config/gptqueue/launch-allowlist.json), deliberately outside any agent
workspace: an allowlist that agents can edit is one they can authorize
themselves with. Override the path with GPTQUEUE_LAUNCH_ALLOWLIST, and keep it
somewhere agents cannot write. Symlinks and group- or world-writable files are
refused. A legacy ./.gptqueue/launch-allowlist.json in the working directory
is no longer read; the refusal message names the new location. Format (version 2):
{
"version": 2,
"commands": [
{
"command": "/absolute/path/or/name",
"allowed_args": [["--agent", "alice"], []],
"comment": "optional human note"
}
]
}Matching rules (identical at admission and at dispatch):
A requested
launch_commandmust satisfy an allowlisted entry's identity exactly — never by basename aliasing. A bare-name entry matches only the byte-identical bare name; an absolute-path entry matches only an absolute request thatpath.resolves to the same path./attacker/work/nodedoes NOT match an allowlistednode.A requested arg vector must equal one
allowed_argstemplate exactly: same length, every element identical. There is no suffix freedom, and[]accepts only a request with no args.launch_cwd, when provided, must be an existing directory within the server workspace root (path.resolve+ prefix check; realpath-based symlink-escape handling is intentionally out of scope).
Fail-closed semantics:
A new
wake_if_offlineregistration is refused with a typed error (launch_not_allowlisted) if the allowlist file is absent, unparseable, or does not permit the requested command/args. Already-admitted actors keep functioning.Dangerous delegators — a command whose basename is a shell (
sh,bash,zsh,dash,fish,ksh,cmd,powershell,pwsh) or a shell carrying-c/-lc/-Command— are rejected regardless of the allowlist (launch_command_rejected).Interpreter inline-code flags are rejected regardless of the allowlist (
launch_command_rejected): a command whose basename is an interpreter (node,deno,bun,tsx,ts-node,python,python2,python3,ruby,perl,php,awk) carrying-e/--eval/-c/--commandis an arbitrary-code channel even under an exact-template grant. Point the interpreter at a fixed script file instead (node /path/to/runtime.mjs).Version-1 documents (basename matching + unbounded
allowed_args_prefixessuffixes) are rejected at parse time with a migration message; they cannot be soundly auto-converted to exact templates.Rejections apply again at dispatch (
dispatchLaunchre-reads the allowlist), so a stale actor-directory entry cannot spawn a command the operator has since disallowed. A refused dispatch surfaces aswake.status: "launch_failed"on the send.
PROVISIONAL: the global operator allowlist is a stop-gap. It is slated to be replaced by per-actor operator grants (each actor may only launch programs its own operator explicitly granted). Operators should treat the allowlist as the minimum permit set and audit it regularly.
Configuration
Claude Code (stdio)
Add to ~/.claude.json under mcpServers:
{
"gptqueue": {
"type": "stdio",
"command": "node",
"args": ["/path/to/gptqueue/dist/mcp-server/index.js"]
}
}Claude Code (HTTP)
Start the HTTP server, then configure the client to connect:
# Start the server
node /path/to/gptqueue/dist/transports/http.js --port 3001{
"gptqueue": {
"type": "streamable-http",
"url": "http://127.0.0.1:3001/mcp"
}
}Hook script
Optionally add the hook script to ~/.claude/settings.json for automatic startup context and exit gating:
{
"hooks": {
"SessionStart": [{
"matcher": "",
"hooks": [{
"type": "command",
"command": "/path/to/gptqueue/scripts/check-queue.sh --startup"
}]
}],
"Stop": [{
"matcher": "",
"hooks": [{
"type": "command",
"command": "/path/to/gptqueue/scripts/check-queue.sh --stop"
}]
}]
}
}Environment variables
Variable | Default | Description |
|
| Redis connection URL |
|
| HTTP server bind host; non-loopback requires |
|
| Path of the operator wake-launch allowlist; keep it outside agent workspaces (see the security section) |
| (none) | Bearer token required on every |
|
| Close an HTTP MCP session after this long with no open request (an open SSE stream counts as open); the client gets |
|
| Maximum concurrent HTTP MCP sessions; further |
| (none) | Pre-register with this agent name on startup (stdio only) |
|
| Max messages per agent inbox |
|
| Attribution for PTY-wrapped child CLI processes |
| Current directory name | Project attribution for PTY-wrapped child CLI processes |
| Agent name | Session attribution for PTY-wrapped child CLI processes |
|
| HTTP server port |
|
| Redis host (hook script only) |
|
| Redis port (hook script only) |
PTY wrapper
The PTY wrapper lets you run any CLI (e.g. claude, codex) inside a PTY that monitors Redis for incoming messages and injects prompts when the process goes idle:
node bin/gptqueue-pty --agent alice --cmd claudeWhen a woken agent has pending messages, the injected prompt instructs it to
consume via claim_tasks (optional max_batch, ttl_seconds) and confirm
with acknowledge_tasks (claim_id) rather than receive_message, so a
durable actor woken through the PTY consumes its batch at-least-once.
How it works
Registration -- An agent calls
register_agentwith a name, role, and description. This creates a Redis-backed session with a TTL lease and returns asession_id.Sessions -- Each registration creates a session record in Redis. Sessions have TTL-based leases that are automatically refreshed. An agent can have multiple concurrent sessions (e.g. from different transports).
Discovery -- Any agent (even unregistered) can call
list_agentsto see all registered agents and whether they're online. Online status is computed from active session leases.Messaging --
send_messagepushes to the target agent's Redis list (gptq:q:<name>). A Lua script enforces the queue bound atomically. If the queue is full, the sender retries with exponential backoff (up to 10 attempts).Resolve-then-push. Before any queue write,
send_messageresolves the recipient: it proceeds when the target has a durable actor-directory record or is a registered agent; otherwise it rejects the send with a structuredunknown_recipienterror and creates no queue (gptq:q:<name>) or metadata (gptq:meta:<name>) keys. A typo'd or unknown name can never silently create an orphan mailbox. Self-send (to the caller's own registered name) remains valid.Wake-on-send. When the recipient is a durable
wake_if_offlineactor (seeactor_register) with no live runtime and a runnable launch contract,send_messagepersists the message first, then attempts to wake the actor: it acquires a bounded (60s) per-actor wake lease, and if it owns the lease, spawns the actor's launch command (detached, no shell). The send always succeeds regardless of the wake outcome, and the additive result fieldwakereportswake_dispatched/wake_coalesced/launch_failed/wake_error.store_onlyactors and plain agents (no durable record) never wake, so their send behavior is unchanged.Pid-liveness reconciliation. When a wake lease records a
spawned_pidand the launched runtime never registers (no live session), the actor would otherwise stay pinned instartinguntil the lease TTL lapses. Presence assembly (bothactor_statusandsend_message's wake gate) reconciles this observationally: if the lease's spawned process is no longer alive, the lease is cleared as a failed activation, so the actor returns to offline and re-wake becomes possible. A live or un-probed (no pid) lease is retained and reported asstarting;actor_status'swake_leasepayload includes an additivepid_livenessofalive/dead/unknown.Receiving --
receive_messagedoes a blocking pop (BLPOP) with a configurable timeout. For plain agents (no durable actor-directory record) this is unchanged. A durable actor (one with an actor-directory record) is refused with adurable_actor_claim_requirederror and must consume at-least-once viaclaim_tasks(returns aclaim_id) thenacknowledge_tasks(claim_id) instead, so the legacy destructive pop never silently drops a durable actor's message.Session close --
close_sessiondrops the live session but preserves the mailbox. Queued messages remain available for a future session.Unregister --
unregister_agentcloses the session AND deletes the mailbox. This is a destructive operation.
Known Limitations
Bridge-based transport (supergateway)
When using the stdio transport behind a bridge like supergateway, tool calls may land in different worker processes. The recommended solution is to use the native HTTP transport instead, which eliminates the need for bridges entirely.
If you must use a stateless or bridge-based transport, capture the
session_id returned by register_agent and pass it to session-scoped
tools such as send_message, receive_message, close_session, and
unregister_agent.
Child-process accumulation
If still using bridges, they can accumulate orphaned gptqueue worker processes. To check and clean up:
# Count gptqueue child workers
pgrep -f 'gptqueue' | wc -l
# Review candidates first: -f matches any command line containing the text,
# including servers from other checkouts
pgrep -af 'dist/mcp-server/index.js'
# then stop only the PIDs you have confirmed are orphaned
kill <pid> ...Stdio servers now exit on their own when their client closes stdin, so orphans should only come from older builds.
License
MIT
This server cannot be deployed
Maintenance
Related MCP Connectors
AI agent registry — search, discover, register, and connect agents via MCP.
Discover and call AI agents via MCP. Supports A2A agents and platform agents with async tasks.
Agent communication platform for agent to agent messaging via MCP. Messages, channels, skills.
Agent-native collaboration network: orchestrate a team of long-running agents from any MCP client.
Related MCP Servers
- AlicenseNot gradedqualityCmaintenanceA Redis-backed MCP server that enables multiple AI agents to communicate, coordinate, and collaborate while working on parallel development tasks, preventing conflicts in shared codebases.20MIT
- AlicenseNot gradedqualityDmaintenanceProvides a messaging multiplexer for agents using Redis streams, allowing Claude Code to send, receive, and ping other agents through MCP tools.MIT
- AlicenseNot gradedqualityAmaintenanceEnables AI agents to collaborate in shared rooms with people, managing room presence, message delivery, and automatic agent registration via MCP tools.MIT
- AlicenseNot gradedqualityAmaintenanceThis MCP server acts as a message broker for AI agents, enabling them to register, discover each other, form persistent squads or ad-hoc teams, and communicate via point-to-point messaging, RPC, or pub/sub. It persists state in SQLite and offers 26 MCP tools for agent, squad, team, and message management.Apache 2.0