Agent Dispatch
About
MCP server that lets Claude Code agents delegate tasks to agents in other project directories, with parallel dispatch, sessions, and async jobs.
Details
- Author
- ginkida
- Downloads
- 276
- Categories
- Developer Tools, Other, AI
Jump to
- Parallel dispatch across multiple agents
- Multi-turn sessions and async jobs
- Agent-to-agent dialogue
- Self-hosted, no cloud vendor required
- Each agent uses its own CLAUDE.md and MCP tools
Setting up with Highlight
This MCP is not yet compatible with Highlight’s one-click setup. However, you can still use it with Highlight by following these steps:
- Download and install Highlight from highlightai.com/download
- Navigate to the plugins tab and select "Add Custom Plugin"
-
Configure the plugin with the settings below
Plugin Name
Agent DispatchCommand (node, npx, python, etc.)Please refer to the README for specific instructions on how to obtain API keys or other required environment variables.
- Enable "Start Automatically" if you want the plugin to start when Highlight launches
From the repository
Agent Dispatch is run as both an MCP server and a CLI tool. Installation and configuration instructions are available in the GitHub repository at https://github.com/ginkida/agent-dispatch.
Claude Desktop / Cursor
Paste into your MCP client config file to install this server.
{
"mcpServers": {
"agent dispatch": {
"agent-dispatch": {
"command": "agent-dispatch",
"args": [
"serve"
]
}
}
}
}
McpServers
{
"agent-dispatch": {
"command": "agent-dispatch",
"args": [
"serve"
]
}
}
MCP server that lets Claude Code agents delegate tasks to agents in other project directories.
Each agent runs as a separateclaude -psession in its own project directory — inheriting that project's MCP servers, CLAUDE.md, and tools. The calling agent just gets the result back.
Related projects can be bundled into agroup— a shared brief plus a member list — so one session can coordinate work across them (e.g. code repos + aninfra/Portainer gateway + ananalyticsgateway).
Works with OAuth, API key, and Claude subscription authentication.
AI agents:this README is the canonical doc forusingthe tool — setup:Quick Start(every step has a deterministic verify), first call:dispatch, tool selection:Which Tool to Use, failure handling:Error Recovery. Workingonthis repo instead? SeeAGENTS.md.
Prerequisite:theClaude Code CLImust be installed and authenticated. Check first:
claude --version # must print a version — if it fails, install Claude Code before continuing
pip install agent-dispatch # or: pipx install agent-dispatch # 1. Create config + register the MCP server with Claude Code (user scope) agent-dispatch init # 2. Register project directories as agents — REPLACE the example paths with # real directories on your machine; they must exist (~ is expanded, relative # paths are resolved). Descriptions are auto-generated from project files. # No second project handy? Use the zero-setup block below instead. agent-dispatch add infra ~/projects/infra agent-dispatch add backend ~/projects/backend # 3. Smoke test — dispatches a real task to the agent added in step 2 and prints # the answer; exit 0 on success. Default task when none given: # "What project is this? Describe in one sentence." agent-dispatch test infra # 4. Verify the whole install — prints "All checks passed." and exits 0 on success agent-dispatch doctor
Zero-setup alternativefor steps 2–3 (no second project needed — registers the current directory):
agent-dispatch add self . && agent-dispatch test self "Say hello"
Every Claude Code session now has the dispatch tools. Independent check:claude mcp listmust print a line starting withagent-dispatch:. From inside a Claude Code session, the first MCP calls arelist_agents(), thendispatch(...).
Ifinitfails to register the MCP server(prints a warning instead ofRegistered MCP server), register manually:
claude mcp add-json agent-dispatch "{\"type\":\"stdio\",\"command\":\"$(which agent-dispatch)\",\"args\":[\"serve\"]}" --scope user
Iftestfails with a permission error(error_type: "permission"), grant tool access and re-test:
agent-dispatch update infra --allowed-tools "Bash,Read,Grep" # least privilege # or, if the agent needs everything (see SECURITY.md for the trade-off): agent-dispatch update infra --permission-mode bypassPermissions
Do dispatchwhen a task needs tools, files, or context from another project:
- Check container logs via infra agent's Portainer MCP
- Query a database via db agent's postgres MCP
- Read code or run tests in another repository
Don't dispatchwhen you can do it yourself — dispatching spawns a full Claude session.
Lists all configured agents.Call this firstto see what's available.
// Response (capability + permission fields shown only when populated) [ { "name": "infra", "directory": "/home/user/projects/infra", "description": "Infrastructure agent. MCP: portainer. Stack: Python, Docker", "healthy": true, "has_claude_md": true, "has_mcp_config": true, "mcp_servers": ["portainer", "postgres"], "stacks": ["Python", "Docker"], "dbs": ["Alembic"], "capabilities": ["docker_logs", "deploy_debug"], "risky_capabilities": ["restart_services"], "permission_mode": "bypassPermissions", "allowed_tools": ["Bash", "Read", "Grep"] } ]
mcp_servers,stacks, anddbsare detected from the agent's project files (.mcp.json,Dockerfile,pyproject.toml,Cargo.toml,prisma/,alembic.ini, etc.) so callers can pick the right agent without dispatching a probe.
Cheap detailed lookup — reads the agent's files without spawning aclaudesession. Returns the full config (timeout, model, budget, permission mode, allowed/disallowed tools), detected MCP/stacks/DBs, plus short previews ofCLAUDE.mdandREADME.mdwhen present.
Use thisbeforedispatch_async/dispatchto confirm an agent has the tools and context for your task — much cheaper than a probe dispatch.
Agroupbundles related agents into a cross-project working set — typically a few code repos plus capability gateways (aninfraagent with a Portainer MCP, ananalyticsagent with a browser + Yandex Metrica). It lets one orchestrating session coordinate work that spans code, deploy, and verification.
A group is adescriptive layer, not an execution engine— there is no router and no state machine. You pick members by reading their hints and coordinate with the normal dispatch tools. Two text fields target two audiences:
- description—orchestrator-facing: how to coordinate the group (the order of steps, who to call for what). Surfaced bylist_groups/inspect_group,neverinjected into a member's prompt.
- shared_context—member-facing facts(stack names, counter ids, conventions) that hold regardless of which member reads them. Auto-prepended to a member'scontextwhen you passgroup=.
Members reference agents by name; membership is many-to-many (a shared gateway can belong to several groups). Manage groups with theagent-dispatch groupCLI (add/list/inspect/update/remove) or by editingagents.yaml.
list_groups()— cheap, no-subprocess readout of every group: description, member count, and each member'suse_forhint + health. A member whose agent was removed is flagged"unknown": truerather than crashing.
inspect_group(name)— one group's full brief:description, the completeshared_context, and the member list. For a deep dive on a specific member, callinspect_agent(member)—inspect_groupdeliberately stays a cheap membership readout.
Using a group— passgroup=todispatch(or per-item indispatch_parallel). The agent must be a member; its group'sshared_contextrides along automatically:
# From the shop-web codebase, hand the deploy to the infra gateway. # The "shop" group's facts (stack name, counter id) are auto-attached. dispatch( agent="infra", task="Redeploy the shop-web container", caller="shop-web", goal="ship the checkout fix", group="shop", )
group=""(the default) is byte-for-byte identical to a plain dispatch — the shared facts are folded into thecontextstring, so the result cache disambiguates groups automatically and group-less calls are unaffected.
One-shot task delegation. Results are cached — identical requests within TTL return instantly.
# Call — recommended form (always include caller and goal) dispatch( agent="infra", # must exist in list_agents() task="Check container logs for errors related to the scheduler service", context="Error: TypeError at scheduler.py:42", caller="backend", # your project/role goal="debug production crash" # the broader objective )
// Response (success) { "agent": "infra", "success": true, "result": "Found 3 errors in container logs: TypeError in scheduler.py:42...", "session_id": "sess-abc-123", "cost_usd": 0.02, "duration_ms": 5000, "num_turns": 2 } // Response (failure — error_type helps you handle programmatically) { "agent": "infra", "success": false, "result": "", "error": "Tool_use is not allowed in this permission mode\n\nHint: ...", "error_type": "permission" }
error_typevalues:permission(tool/action denied),timeout,recursion(dispatch depth exceeded),not_found(missing directory or CLI),budget(theclaudeCLI stopped the session atmax_budget_usd),cli_error(other failures). Permission and budget errors include an actionable hint.
Resumable timeouts:every fresh dispatch pre-assigns a session UUID (--session-id), so a timed-out dispatch still returns asession_id— the partial transcript survives the kill. The timeout error spells out the recovery: resume withdispatch_session(agent, "Continue where you left off", session_id=...), retry with a biggertimeout_seconds, or usedispatch_async.
Denied-tools visibility:in non-interactive mode the claude CLI auto-denies tools the agent isn't allowed to use — the agent then often "succeeds" with an answer like"I need your permission for one read-only query". When that happens the response carries the deterministic signal:denied_tools(parsed from the CLI'spermission_denials) plus ahintexplaining the result may be incomplete and how to grant access.successstaystrue— it's a soft signal, not a failure.
// Response (success, but a tool was blocked) { "agent": "analysis", "success": true, "result": "Here is the offline mapping. To finish I'd need to run one read-only query...", "denied_tools": ["Bash"], "hint": "1 tool call(s) were denied by permissions: Bash. The result may be incomplete..." }
Structured JSON output:passresponse_format="json"to ask the agent for a single JSON value. The runner appends an instruction footer ("respond with a single valid JSON value, no fences, no prose") and on success parses the response — the parsed value lands inparsed_result. The raw text is always inresult. Parse failures leaveparsed_result=Nonebut don't fail the dispatch (soft mode).
// Response with response_format="json" { "agent": "infra", "success": true, "result": "{\"errors\": 3, \"first_at\": \"14:02\"}", "parsed_result": {"errors": 3, "first_at": "14:02"} }
Always passcallerandgoal— the dispatched agent sees a structured prompt:
## Goal debug production crash ## Dispatched by backend ## Context Error: TypeError at scheduler.py:42 ## Task Check container logs for recent errors related to the scheduler service
Multi-turn: continue a conversation with an agent. First call starts a session, passsession_idback to continue. Never cached.
dispatch_sessionis also thetimeout recovery path: a timed-outdispatchreturns asession_id— pass it here withtask="Continue where you left off"to salvage the partial work instead of restarting.
Turn 1: dispatch_session("infra", "List running containers") → session_id: "sess-abc" Turn 2: dispatch_session("infra", "Restart the nginx one", session_id="sess-abc") → agent remembers previous context
Run multiple tasks concurrently. Much faster than sequentialdispatchcalls.
Important:dispatchesis a JSON string, not a list.
// Input [ {"agent": "infra", "task": "check pod logs for errors", "caller": "backend", "goal": "debug crash"}, {"agent": "db", "task": "are all migrations applied?", "caller": "backend", "goal": "debug crash"} ]
// Response (without aggregate) [ {"agent": "infra", "success": true, "result": "No errors in pod logs", ...}, {"agent": "db", "success": true, "result": "All migrations applied", ...} ]
// Response (with aggregate="backend") { "individual_results": [ {"agent": "infra", "success": true, "result": "No errors in pod logs", ...}, {"agent": "db", "success": true, "result": "All migrations applied", ...} ], "aggregated": { "agent": "backend", "success": true, "result": "Summary: all systems nominal. No pod errors, all migrations applied." } }
Same asdispatchbut shows live progress while the agent works. Use for long-running tasks. Not cached.
Parameters are the same asdispatchexceptreturn_ref/summary_chars(streaming is incompatible with ref-mode) andgroup(group context injection is supported only ondispatchand per-item indispatch_parallel).
Two agents collaborate through multi-turn conversation. Never cached.
Each round costs up to 2 dispatches. Agents signal completion with[RESOLVED].
// Response { "resolved": true, "rounds": 2, "total_cost_usd": 0.04, "total_duration_ms": 12000, "final_answer": "Staging had 1 pending migration. Applied successfully.", "conversation": [ {"agent": "db", "role": "responder", "round": 1, "message": "Which environment?", "cost_usd": 0.01}, {"agent": "backend", "role": "requester", "round": 1, "message": "Staging", "cost_usd": 0.01}, {"agent": "db", "role": "responder", "round": 2, "message": "Applied. [RESOLVED]", "cost_usd": 0.01} ] }
Register a new project directory as an agent. Description is auto-generated from project files if omitted.
Update an existing agent's configuration. Only non-empty fields are changed. Pass"none"to clear a field.
Changing an agent's config drops that agent's cached results — the cache key holds the agentname, so a re-pointed or re-permissioned agent would otherwise keep answering from the previous config for the rest of the TTL. The same applies toadd_agentandremove_agent.
View cache hit rate and size, or clear all cached results.
Result references —return_ref+fetch_result
For dispatches whose result text is large (audits, log dumps, code searches), passing the full text back inflates the calling agent's context. Usereturn_ref=Trueto get just a small reference instead:
dispatch(agent="infra", task="audit every container", return_ref=True, summary_chars=200) -> {"ref": "8f3a...e1", "agent": "infra", "success": true, "size": 14823, "summary_chars": 200, "summary": "Inspected 32 containers. Found 3 OOM kills in the last hour:\n- worker-3...", "cost_usd": 0.08, "duration_ms": 9200} // Later, when you actually need to read the result: fetch_result(ref="8f3a...e1") -> full DispatchResult JSON fetch_result(ref="8f3a...e1", max_chars=2000) -> truncated, plus {"truncated": true, "full_size": 14823}
Refs reuse the same storage asdispatch_asyncjobs (under~/.config/agent-dispatch/jobs/), so anyjob_idreturned bydispatch_asyncis also a validrefforfetch_result.parsed_result(whenresponse_format="json"is set) is small and is always inlined directly in the ref response — no second fetch needed.
Async dispatch —dispatch_async,dispatch_status,dispatch_wait,dispatch_cancel,dispatch_jobs,dispatch_gc
When a dispatched task is going to take a while, you don't want to block your own tool slot for minutes. Async dispatch returns ajob_idimmediately and lets you check back when you're ready.
// 1. fire and forget (timeout_seconds= works here too for known-long tasks) dispatch_async(agent="infra", task="audit every container log for OOM kills today") -> {"job_id": "8f3a...e1", "status": "pending", "agent": "infra"} // 2. do other work, then check progress (non-blocking) // progress is a rolling tail of what the agent is doing right now dispatch_status(job_id="8f3a...e1") -> {"id": "8f3a...e1", "status": "running", "started_at": 1730000123.4, "progress": ["Using tool: Bash", "Scanning container logs for OOM events..."], ...} // 3. or block until done (timeout_seconds default: 60, capped at 3600) dispatch_wait(job_id="8f3a...e1", timeout_seconds=120) -> {"id": "8f3a...e1", "status": "done", "result": {"agent": "infra", "success": true, ...}} // If the timeout fires, the job keeps running: -> {"id": "...", "status": "running", "timed_out_waiting": true}
dispatch_cancel(job_id)cancels apendingjob, and also kills arunningjob'sclaudesubprocess when the job was started by the same server instance (the job is markedcancelledfirst, so the worker's trailing write can't undo it; partial work is lost but the progress tail is preserved). A running job started by apreviousserver run can't be killed safely and is left to finish. The response carries anoutcomeofcancelled,cancelled_running,running(not owned by this server),already_terminal, ornot_found.
Async workers run with streaming under the hood: the job file keeps a rolling tail (last 20 lines, ~1 write/sec) of assistant text and tool-use events.dispatch_statusshows it asprogresswhile the job runs and keeps it afterwards as a post-mortem trace;dispatch_jobsshowslast_progressfor running jobs.
dispatch_jobs(status?)lists recent jobs as summaries (filter bypending/running/done/failed/cancelled).dispatch_gc(max_age_days=7)purges terminal jobs older than the threshold — pending and running jobs are never deleted.
Job state persists to disk at~/.config/agent-dispatch/jobs/(override withAGENT_DISPATCH_JOBS_DIR). One JSON file per job, written owner-only (0o600) with atomic writes — safe to read orlswhile jobs are in flight. Caller-suppliedjob_ids are validated as 32-char hex before any file access (no path traversal). On startup the server recovers jobs a crashed instance abandoned:runningones stuck over an hour, andpendingones over 24 hours, are markedfailedso they stop being polled forever and become collectable bydispatch_gc. (Thependingthreshold is deliberately long — the jobs directory is shared by every running server, so a job queued behind another server's concurrency limit must not be swept.)
Failures are deterministic: checksuccess, then branch onerror_type.
Three soft signals that arrive withsuccess: true:
- denied_tools+hint— the agent finished but some tool calls were blocked; the result may be incomplete. Grant access (see thepermissionrow) and re-dispatch.
- parsed_result: nullwithresponse_format="json"— the reply wasn't valid JSON; the raw text is still inresult. Caveat: an agent thatcan'tcomply returns{"error": "<reason>"}— which parses successfully — so also checkparsed_resultfor an"error"key.
- budget_exceeded: true—cost_usdcame in over the agent'smax_budget_usd(or the settings default) without the CLI stopping the run (the final turn can overshoot the cap). The dispatch is not failed — the money is already spent — but a runaway agent is now visible. Tighten the task, pick a cheaper model, or raise the budget. A run the CLIdidstop fails witherror_type: "budget"instead.
Tool-level errors (unknown agent, malformed input) return a plain envelope instead of aDispatchResult:
{"error": "Unknown agent: 'foo'. Available: infra, db, monitoring"}
Config at~/.config/agent-dispatch/agents.yaml(override:AGENT_DISPATCH_CONFIGenv var):
agents: infra: directory: ~/projects/infra description: "Infrastructure agent. MCP: portainer." timeout: 300 # seconds, default: 300 capabilities: # capability labels, shown in list_agents - docker_logs - deploy_debug risky_capabilities: # high-risk labels, surfaced for visibility - restart_services # model: sonnet # optional model override # max_budget_usd: 1.0 # cost limit per dispatch # permission_mode: bypassPermissions # one of: default | plan | bypassPermissions # allowed_tools: # restrict which tools the agent can use # - Read # - Grep # disallowed_tools: # block specific tools # - Write # Optional: bundle related agents into a cross-project working set. # A descriptive layer — no router; the orchestrating session coordinates # with the normal dispatch tools. See the Groups section above. groups: shop: # ORCHESTRATOR-facing: how to coordinate the group. Surfaced by # list_groups/inspect_group, NEVER injected into a member's prompt. description: "After a code change: deploy via infra, then verify via analytics." # MEMBER-facing facts, auto-prepended to dispatch(..., group="shop"). shared_context: | Prod runs in Portainer stack "shop". Metrica counter 12345. members: # reference agents above (many-to-many) - agent: infra use_for: deploy, restart, container logs # - agent: backend # use_for: orders/payments endpoints settings: default_timeout: 300 # default_permission_mode: bypassPermissions # inherited by all agents # default_allowed_tools: # inherited when agent has none # - Bash # - Read # - Edit max_dispatch_depth: 3 # recursion protection max_concurrency: 5 # max parallel claude -p processes (per dispatch path) # job_retention_days: 30 # 0 (default) = never prune. See "Job retention" below. cache: enabled: true ttl: 300 # seconds max_size: 1000 # max cached entries; oldest evicted first (FIFO)
Config is reloaded on every tool call — add agents without restarting.
Sign in to leave a review
Use Google, GitHub, or an email account so ratings stay tied to real people.
No reviews posted yet.





