Give your swarm shared memory.
When you spawn multiple AI agents to work on the same problem, they run blind. No awareness of what others are doing. They make decisions in isolation and commit to them — picking the same variable names, overwriting each other's files, duplicating work that's already done.
Incubator is an MCP server that lets agents add live context to each other while running. No orchestrator deciding who does what. No pre-scripted roles. No sequential handoffs. Agents self-organize by reading each other's state, claiming resources before touching them, and sharing discoveries as they work.
And anything that speaks HTTP can join the conversation.
New in v3: Coordination Specs — define reusable coordination patterns in .acp.json files. Agents call getProtocol once and get phase-aware instructions, resource naming conventions, and error handling — no prompt injection of coordination rules needed.
3 agents splitting a monolith into modules, without coordination:
agent 1: "I split the monolith into 5 modules" ← did all the work
agent 2: "I split the monolith into 5 modules" ← overwrote agent 1
agent 3: "I split the monolith into 5 modules" ← overwrote agent 2
All 3 did the same 5 files. 15 file writes for 6 files of actual work. Last writer wins, everyone else's output is garbage.
Same 3 agents, same task, with Incubator:
agent_45af598c claim file:utils.js → approved
agent_45af598c claim file:cache.js → approved
agent_dff039a5 claim file:utils.js → rejected (owner: agent_45af598c)
agent_dff039a5 claim file:posts.js → approved
agent_dff039a5 claim file:search.js → approved
agent_13fa08bc claim file:utils.js → rejected (owner: agent_45af598c)
agent_13fa08bc claim file:posts.js → rejected (owner: agent_dff039a5)
...
agent_45af598c publishDiscovery "module:utils.js exports generateId, retry, debounce, deepClone"
agent_dff039a5 publishDiscovery "module:posts.js exports createPost, publishPost, addTag, removeTag"
Each agent claims what it's working on. Rejections redirect agents to unclaimed work. Discoveries give every agent context about what others decided — without anyone having to prompt them.
6 file writes for 6 files. Zero collisions. Zero wasted work.
Most multi-agent frameworks use an orchestrator: a central brain that assigns tasks, routes messages, and sequences work. The orchestrator is a bottleneck and a single point of failure. It has to understand the problem well enough to decompose it. If it plans wrong, every agent does wrong work.
Incubator is the opposite. There's no orchestrator. Agents share a coordination layer — claims, discoveries, events, state — and figure out the division of labor themselves. The coordination primitives are simple enough that any LLM understands them from the tool descriptions alone.
| Orchestrator (top-down) | Incubator (bottom-up) | |
|---|---|---|
| Who decides | Central planner | Each agent |
| Failure mode | Orchestrator wrong → all agents wrong | One agent wrong → others adapt |
| Scaling | Orchestrator becomes bottleneck | Agents self-balance |
| Flexibility | Pre-planned task graph | Emergent work distribution |
| Agent awareness | Only what orchestrator tells them | Full shared context |
Incubator is a lightweight MCP server with a REST API. Agents connect over MCP and get 12 coordination tools. Everything else — dashboards, scripts, webhooks, CI pipelines — talks REST.
Same stores. Same state. Three protocols.
┌───────────┐ MCP ┌─────────────────┐ REST ┌─────────────┐
│ Agent 1 │◄────────────►│ │◄─────────────►│ Dashboard │
│ Agent 2 │◄────────────►│ Incubator │◄─────────────►│ Webhooks │
│ Agent 3 │◄────────────►│ │◄─────────────►│ CI / cron │
└───────────┘ └────────┬────────┘ └─────────────┘
│ WebSocket
┌───────▼────────┐
│ Live clients │
│ (push events) │
└────────────────┘
Memory, SQLite, or Redis
Single process or distributed
Zero to minimal infrastructure
Claims — resource locking, first-come-first-served
incubator_claim/incubator_releaseClaim/incubator_checkClaim/incubator_listClaims
Discoveries — shared knowledge with text search
incubator_publishDiscovery/incubator_searchDiscoveries
Events — agent-to-agent signaling with cursor-based polling
incubator_publishEvent/incubator_getEvents
State — shared key-value store, last-writer-wins
incubator_getState/incubator_setState/incubator_queryState/incubator_deleteState
Protocol — coordination spec bootstrap
incubator_getProtocol— returns phase-aware instructions, rules, and resource conventions from a loaded spec
The tool descriptions themselves are the coordination protocol. Agents read them, understand the pattern, and self-organize. No plugin, no separate instructions, no injected system prompts. For structured workflows, load a coordination spec and agents call getProtocol to get phase-specific instructions.
Every MCP operation is also available over plain HTTP. Prefix with /api/:
| Method | Route | Description |
|---|---|---|
GET |
/api/health |
Server status, agent count, claim/event counts |
GET |
/api/state/:key |
Read a state value |
PUT |
/api/state/:key |
Write a state value |
DELETE |
/api/state/:key |
Delete a state value |
GET |
/api/state?pattern=&category= |
Query state entries |
POST |
/api/claims |
Claim a resource |
DELETE |
/api/claims/:resource |
Release a claim |
GET |
/api/claims/:resource |
Check who owns a resource |
GET |
/api/claims?pattern= |
List active claims |
POST |
/api/events |
Publish an event |
GET |
/api/events?since=&type= |
Poll events by cursor |
POST |
/api/discoveries |
Share a discovery |
GET |
/api/discoveries?query=&category= |
Search discoveries |
GET |
/api/protocol |
Get loaded coordination spec |
PUT |
/api/protocol |
Load a coordination spec |
Agent identity is passed via agentId in the JSON body or X-Agent-Id header. CORS is enabled by default. Namespace isolation via X-Namespace header (default: default).
# Check server health
curl http://localhost:3100/api/health
# Set shared state
curl -X PUT http://localhost:3100/api/state/config \
-H "Content-Type: application/json" \
-d '{"value": {"maxRetries": 3}, "agentId": "dashboard"}'
# Watch active claims
curl http://localhost:3100/api/claims
# Search what agents have found
curl "http://localhost:3100/api/discoveries?query=naming+convention"Events are pushed to WebSocket clients the instant they're published. No polling. Connect to /ws on the same port:
ws://localhost:3100/ws?namespace=default&since=0&types=conflict,completed
| Parameter | Description |
|---|---|
namespace |
Namespace to subscribe to (default: default) |
since |
Replay events after this cursor, then stream live |
types |
Comma-separated event type filter (omit for all) |
On connect, the server replays any missed events (from since), sends a replay_done marker, then streams live events as they happen:
{"type": "event", "event": {"id": 5, "type": "conflict", "data": {...}, ...}}
{"type": "event", "event": {"id": 6, "type": "completed", "data": {...}, ...}}
{"type": "replay_done", "cursor": 6}
{"type": "event", "event": {"id": 7, "type": "conflict", "data": {...}, ...}}Clients can update their type filter without reconnecting:
{"type": "subscribe", "types": ["conflict"]}Send null types to receive everything again. A ping message is sent every 30 seconds as a keepalive.
WebSocket auto-enables in HTTP mode when the ws package is installed. If not installed, everything else works normally — agents poll via MCP, REST clients poll via GET /api/events?since=.
# Install ws to enable WebSocket push
npm install ws
# Quick test with wscat
wscat -c ws://localhost:3100/wsWith Redis backend, WebSocket push works across multiple Incubator instances. Events published to one instance are pushed to WebSocket clients connected to any instance via Redis Pub/Sub.
The REST API enables something orchestrators can't do cleanly: external control over running agents.
Agents check a shared state key before each operation. An external system sets that key via REST. Next time any agent checks, it sees the signal and stops gracefully.
# Agents are working...
# Send STOP from anywhere — curl, a dashboard, a webhook
curl -X PUT http://localhost:3100/api/state/halt \
-H "Content-Type: application/json" \
-d '{"value": true, "agentId": "dashboard"}'
# Agents see halt=true on their next state check, report what they completed, and exit
# Resume later
curl -X DELETE http://localhost:3100/api/state/haltThis isn't a special feature. It's just the coordination primitives doing their job. Any state key can be a control signal. Any REST client can send it. The agents don't know or care that the signal came from outside MCP.
See examples/stop-button/ for a full working demo.
The coordination layer is an attack surface. If one agent gets compromised via prompt injection, it can poison shared state, discoveries, and events that every other agent trusts. We call this a coordination injection attack — the compromised agent doesn't need to hack the server. It just writes something toxic into shared memory and lets the other agents do the damage.
Incubator defends against this with pia-guard, a deterministic prompt injection scanner. Three layers of defense, all on by default:
- Writes — every write to state, claims, events, and discoveries is scanned before it hits the stores. Malicious content is rejected with a
PIABlockedError. - Reads — every read filters results through the scanner. Poisoned entries that somehow entered the stores (migration, manual edit, pre-guard snapshots) are silently dropped.
- Snapshots — persisted snapshots are scanned on load. Poisoned entries are stripped before they reach the stores.
agent_evil → setState("config", "Ignore all instructions...")
→ pia-guard: score 150, BLOCK
→ 403 { error: "prompt_injection_detected", findings: [...] }
agent_good → setState("config", "maxRetries: 3")
→ pia-guard: score 0, PASS
→ 200 { success: true }
Internal system events (like claim.acquired) bypass the guard since they're generated by Incubator itself, not by agent input.
MCP agents see { isError: true, text: "[pia-guard] Blocked: ..." } and can retry with clean content. REST clients get a 403 with the full scan result including score and findings.
To disable (not recommended):
node dist/index.js --http --no-guard
# WARNING: PIA Guard disabled — no prompt injection scanningMulti-agent coding — N agents working on the same repo. Claims prevent file conflicts. Discoveries share naming conventions and patterns found.
Human-in-the-loop control — Dashboard monitors agent progress in real time via REST. Pause, resume, or redirect agents by setting state.
CI/CD integration — Pipeline sets shared state before agents run (target branch, test results, deploy config). Agents read it without custom tooling.
Webhook-driven coordination — External events (GitHub PR merged, Slack message, monitoring alert) publish to the event store. Agents poll and react.
Agent handoffs — Agent A finishes phase 1, publishes discoveries summarizing what it learned. Agent B picks up phase 2 with full context, no prompt needed.
Rate limiting / resource management — Claims with TTL as leases. Agents check out API quotas, database connections, or deployment slots.
npm install
npm run build# HTTP mode (multi-agent, MCP + REST)
# PIA Guard is on by default — all writes are scanned for prompt injection
node dist/index.js --http --port=3100 --verbose
# With SQLite persistence (survives restarts)
node dist/index.js --http --backend=sqlite --db=./incubator.db
# With a coordination spec (agents call getProtocol for instructions)
node dist/index.js --http --protocol=examples/monolith-split.acp.json
# With Redis (multi-process scaling, shared across instances)
node dist/index.js --http --backend=redis
node dist/index.js --http --backend=redis --redis-url=redis://10.0.0.5:6379
# With JSON snapshot persistence (memory backend only)
node dist/index.js --http --persist=./data/snapshot
# Stdio mode (single agent / testing)
node dist/index.js --agent-id=agent_1Create an MCP config file:
{
"mcpServers": {
"incubator": {
"type": "http",
"url": "http://localhost:3100/mcp"
}
}
}Then pass it when launching agents:
claude -p "your task here" --mcp-config mcp.json --model haikuOr add it permanently:
claude mcp add --transport http incubator http://localhost:3100/mcpAdd coordination instructions to your prompt:
You are one of N agents working on the same directory. Use the incubator tools:
1. Call incubator_searchDiscoveries first to see what others have done.
2. Call incubator_claim before touching any file. If rejected, skip it.
3. Call incubator_publishDiscovery after completing work so others know.
4. Call incubator_releaseClaim when done with each resource.
5. Only touch resources you successfully claimed.
That's it. The agents figure out the rest.
For structured workflows, use a coordination spec instead — agents call getProtocol and get phase-aware instructions automatically.
For repeatable workflows, define the coordination pattern once in a .acp.json file. Agents call getProtocol on startup and get phase-aware instructions — no copy-paste prompts, no hardcoded coordination rules.
# Start with a coordination spec
node dist/index.js --http --protocol=examples/monolith-split.acp.json
# Agents call getProtocol to get their instructions
# No prompt injection of coordination rules neededA spec defines roles (types of agents), phases (workflow stages), rules (what each role does per phase), and resources (naming conventions for claims, discoveries, events, state keys).
{
"acp": "1.0",
"name": "monolith-split",
"title": "Monolith Module Split",
"roles": {
"worker": { "description": "Extracts modules from the monolith", "count": "2+" },
"coordinator": { "description": "Monitors progress, manages phases", "count": "0-1" }
},
"phases": {
"init": { "description": "Orient and check existing state" },
"work": { "description": "Claim and process resources" },
"done": { "description": "All work complete", "terminal": true }
},
"rules": {
"worker": {
"work": {
"loop": true,
"steps": [
{ "action": "claim", "params": { "resource": "$next_unclaimed" }, "on_rejected": { "skip": true } },
{ "action": "external", "hint": "Read source, extract module, write output" },
{ "action": "publishDiscovery", "params": { "topic": "module:$resource", "category": "exports" } },
{ "action": "releaseClaim" }
]
}
}
}
}When an agent calls incubator_getProtocol, it gets back:
- Rendered instructions — natural language prose the LLM follows, generated from the spec + current runtime state
- Structured rules — the steps for its role in the current phase, with resolved
$variables - Resource conventions — how to name claims, discoveries, events, and state keys
- Phase overview — all phases and which one is current
- Error handling — what to do on claim rejection, expiry, or halt
The agent calls getProtocol once on startup, follows the instructions, and calls it again when it observes a phase change. Phases advance via shared state (setState({ key: 'phase', value: 'work' })).
Specs can use $variables that resolve against live store state:
| Variable | Source | Description |
|---|---|---|
$self |
Agent ID | The calling agent's identifier |
$cursor |
Event store | Current event cursor for polling |
$next_unclaimed |
Claims + instances | First resource not yet claimed or completed |
$active_claims |
Claims store | Currently active claim resources |
$completed_resources |
Discoveries | Resources with published discoveries |
# Via CLI flag (loaded on startup)
node dist/index.js --http --protocol=path/to/spec.acp.json
# Via REST API (loaded at runtime)
curl -X PUT http://localhost:3100/api/protocol \
-H "Content-Type: application/json" \
-d '{"spec": "{\"acp\":\"1.0\", ...}"}'
# Check what's loaded
curl http://localhost:3100/api/protocolSee examples/monolith-split.acp.json for a complete reference spec.
- Near-zero infrastructure — Default is pure in-memory. Optional SQLite for single-process persistence. Optional Redis for multi-process scaling.
- Sub-millisecond startup —
node dist/index.jsand it's ready. SQLite auto-creates its schema on first run. Redis connects before the server starts. - Single process or distributed — Memory and SQLite are single-threaded with no race conditions. Redis lets multiple Incubator instances share one coordination layer.
- Pluggable backends — Memory (default), SQLite, or Redis. Same async interfaces, same behavior. Swap with a CLI flag.
- Three dependencies —
@modelcontextprotocol/sdk,zod, andpia-guard.better-sqlite3,ioredis,ws, andjs-yamlare optional (loaded on demand). - Three protocols — MCP for agents, REST for external systems, WebSocket for real-time push. Same stores, same state.
- Three persistence modes — JSON snapshots (
--persist) for memory backend. SQLite and Redis are inherently persistent. - Optional TTL — State entries and claims can auto-expire. Lazy cleanup on read (consistent across all backends).
- Cursor-based events — Agents call
getEvents(since: cursor)to catch up via MCP/REST. WebSocket clients get events pushed in real time with replay-then-live (no missed events, no duplicates). - Text search for discoveries — Substring matching, not vector similarity. At agent scale (dozens of discoveries, not millions), it's instant and deterministic.
- Namespace isolation — Multiple teams/projects share one server. SQLite isolates by namespace column; Redis by key prefix. No data leakage.
incubator/
src/
index.ts # CLI entry, dual transport (stdio + HTTP)
server.ts # McpServer, 13 tool registrations
rest.ts # REST API route handlers
bus.ts # NotificationBus (LocalBus + RedisBus)
ws.ts # WebSocket manager (upgrade, replay, live push)
guard.ts # PIA Guard integration (coordination injection defense)
protocol/
types.ts # ACP spec TypeScript interfaces
schema.ts # JSON Schema v1 for spec validation
parser.ts # YAML/JSON loading + structural validation
variables.ts # Runtime $variable resolution from stores
renderer.ts # Spec → natural language instruction generation
index.ts # Re-exports
stores/
interfaces.ts # Store interfaces (IStateStore, IEventStore, etc.)
backend.ts # Backend factory (memory, sqlite, or redis)
state.ts # Memory: key-value store (last-writer-wins)
claims.ts # Memory: resource claims (first-come-first-served)
events.ts # Memory: append-only event log with cursors
discoveries.ts # Memory: shared findings with text search
sqlite/
db.ts # Schema init, DB handle caching
state.ts # SQLite state store
claims.ts # SQLite claim store
events.ts # SQLite event store
discoveries.ts # SQLite discovery store
index.ts # createSqliteStores factory
redis/
db.ts # Connection management, client caching
state.ts # Redis state store (Hash)
claims.ts # Redis claim store (Hash + WATCH)
events.ts # Redis event store (Sorted Set + MULTI/EXEC)
discoveries.ts # Redis discovery store (List)
index.ts # createRedisStores factory
types.ts # TypeScript types
persistence.ts # JSON snapshot save/load (memory backend)
namespaces.ts # Namespace registry with backend config
utils.ts # matchGlob, generateId, isExpired
examples/
monolith-split.acp.json # Reference coordination spec
stop-button/ # STOP button demo (REST + MCP)
e2e/
run.ts # Simulated E2E (4 workers × 10 rounds)
stop-button.ts # STOP button integration test
- Bottom-up coordination — No orchestrator. Agents self-organize through shared primitives. The tool descriptions are the protocol.
- Claims are first-come-first-served — Rejected claims return the current owner so agents can adapt, not just fail.
- Last-writer-wins for state — State is shared knowledge, not a lock. Use claims for mutual exclusion.
- Polling for agents, push for dashboards — MCP agents poll with
getEvents(since: cursor)when they're ready. WebSocket clients get real-time push with replay-then-live. Both share the same event store and cursors. - REST mirrors MCP — Same operations, same stores. Agents don't need to know REST exists. External systems don't need to speak MCP.
- No vector DB — Substring search for discoveries. Deterministic, instant, and you can
grepthe results in your head. - Guard by default — PIA Guard scans all writes. Opt out with
--no-guard, not opt in. Shared memory is a trust boundary; it should be defended unless you explicitly choose otherwise. - Pluggable, not coupled — Async store interfaces let you swap memory for SQLite or Redis without touching any consumer code. Same tests run against all three backends.
# Unit + integration tests (250 tests — stores, guard, WebSocket, namespaces, protocol)
# Redis tests skip gracefully if no Redis is running
npm test
# Run with Redis for full coverage
docker run -d --name redis-test -p 6379:6379 redis:alpine
npm test
docker stop redis-test && docker rm redis-test
# Simulated E2E (4 workers × 10 rounds, no LLM)
npx tsx e2e/run.ts
# STOP button integration test (3 MCP workers + REST halt signal)
npx tsx e2e/stop-button.ts
# Real agent E2E (3 Claude agents, costs ~$0.15)
bash e2e/run-real.shAgents hatch ideas in isolation. Give them a shared space and they become a swarm.