About
Slack-backed coordination rooms for multi-agent sessions
README
Operator-staged, Slack-backed coordination rooms for multi-agent sessions. You create a huddle, declare seats, and hand out the per-seat keys to your agents. Each key is an identity: when an agent calls huddle.post, the wrapper auto-attributes the message by seat ([displayName] body), and huddle.read returns history with the speaker decoded. You — the operator — are the implicit orchestrator: a distinct identity, visible to every agent in the room, posting without a key. The room is a real Slack channel, so you can watch and chime in from your phone.
Two sides, one room
Every verb belongs to one of two roles, and the split is the whole model:
- Operator opens, closes, lists, and adds humans to huddles. The operator holds the
huddleIdand orchestrates — distinct identity, no key needed. - Seat is an agent given exactly one key. A seat posts, reads, and asks who else is in the room. The key is the identity; an agent can't post as anyone but itself.
Why it exists
huddle is one swappable layer in a portfolio dev-workbench: the multi-agent coordination plane. It owns one thing — a durable, attributed, operator-visible shared channel for a set of agents working the same problem — and nothing else.
Everything else stays out of scope. Project memory (what's planned / in-flight / shipped) lives in dossier. Driving an agent against a task doc lives in ship. Git worktrees live in the /worktree-* skills. huddle does not run agents, schedule work, or persist long-term project state. Huddles are ephemeral by design — they exist for the span of a coordination task and then get archived. For a one-off run that doesn't need cross-agent chatter, skip huddle entirely and read the run's events log.
The Slack backing is deliberate, not incidental: it means a human can sit in the room from any Slack client, and huddle.who_else surfaces those humans alongside the agent seats. The Slack façade is a swap seam — handlers depend on a slack.Adapter interface, never on slack-go directly, so a Discord- or in-memory-backed room is a matter of a new adapter.
Verb surface (7 verbs)
The stdio MCP server registers seven tools. Tool names are dotted (huddle.create, …).
| Verb | Side | What it does |
|---|---|---|
huddle.create |
operator | Open a huddle: Slack channel + persisted row + N per-seat keys. Optional humans (Slack user IDs or emails) invited best-effort. Returns the seat keys + channel id. |
huddle.close |
operator | Archive the Slack channel and mark the huddle done. |
huddle.list |
operator | List huddles (filter by active). |
huddle.post |
seat | Post a message; the key is the identity, the post is auto-attributed. |
huddle.read |
seat | Read channel history with each speaker's identity decoded. |
huddle.who_else |
seat | Return the huddle purpose, orchestrator, peer seats, and channel humans for a key. |
huddle.invite_human |
operator | Invite humans (Slack user IDs or emails) to an existing huddle, best-effort. |
Slack-touching verbs (create / close / post / read) need HUDDLE_SLACK_BOT_TOKEN. huddle.who_else works tokenless — it lists the channel's humans when a token is present and returns humans: [] when not. huddle.invite_human still returns success with refs under skipped (reason invite_failed) when Slack is unavailable.
Not built yet: add_seat, revoke_key, watch (the planned v1 surface); broadcast, ping_orchestrator, search come later.
Install
go install github.com/itsHabib/huddle/cmd/huddle@main
(No semver tags published yet; switch to @latest once a release is cut.) Or clone and build locally:
git clone https://github.com/itsHabib/huddle.git
cd huddle
make install # builds cmd/huddle → $GOBIN/huddle
Quickstart
Set the Slack token (plus any optional overrides), then start the stdio MCP server:
export HUDDLE_SLACK_BOT_TOKEN=xoxb-... # required for create/close/post/read
export HUDDLE_STATE_DIR=~/.huddle # optional; default .huddle (in cwd)
export HUDDLE_ORCHESTRATOR_SLACK_USER_ID=U... # optional; auto-invite operator to new channels
make run # same as go run ./cmd/huddle
The server boots without HUDDLE_SLACK_BOT_TOKEN — huddle.who_else works either way. Slack-touching verbs return HUDDLE_SLACK_BOT_TOKEN is not set; Slack-touching verbs (create, close, post, read) are unavailable — set the env to enable them at call time until the token is set.
Register the server with Claude Code so sessions can call the verbs:
claude mcp add huddle -- huddle
Claude Desktop uses an mcpServers block in its config file — same binary and env vars (see the MCP user quickstart for the file location):
{
"mcpServers": {
"huddle": {
"command": "huddle",
"env": {
"HUDDLE_SLACK_BOT_TOKEN": "xoxb-..."
}
}
}
}
CLI binaries
Four binaries live under cmd/<name>/. make install ships huddle only; run the others with go run ./cmd/<name>.
| Binary | Role |
|---|---|
huddle |
The MCP server (stdio transport). Reads env-only config at startup. |
seat |
Seat-side CLI wrapper for post / read / who-else — act as a seat from outside an MCP harness. Spawns a fresh go run ./cmd/huddle subprocess per call (so run it from the repo root) and always sees current state. |
smoke |
End-to-end harness driving cmd/huddle as a subprocess against real Slack. Manual only — not part of go test or CI. |
seed-huddle |
One-shot generator for a long-lived test huddle; surfaces invite-skip warnings inline. |
go run ./cmd/seat who-else --key K_... # tokenless
go run ./cmd/seat post --key K_... --body "ack" # needs HUDDLE_SLACK_BOT_TOKEN
go run ./cmd/seat read --key K_... --limit 20
Configuration
The MCP server reads these environment variables: HUDDLE_SLACK_BOT_TOKEN (gates Slack-touching verbs; not required at startup), HUDDLE_STATE_DIR (default .huddle in cwd), HUDDLE_LOG_LEVEL (debug/info/warn/error, default info), HUDDLE_CHANNEL_PREFIX (default huddle-), and HUDDLE_ORCHESTRATOR_SLACK_USER_ID (best-effort auto-invite of the operator to every channel huddle.create opens). Defaults, validation, and semantics live in docs/design.md#configuration.
Slack OAuth scopes: channels:read and users:read cover channel membership and user-ID human refs; add users:read.email to resolve email refs in huddle.create / huddle.invite_human (without it, email refs land in skipped with missing_email_scope).
Architecture
Layered top-down — entry → server → handlers → adapter / store. Each layer has a typed Deps struct and depends only on the layers below; the Slack façade and the SQLite store are the swap seams.
cmd/huddle ──▶ internal/server ──▶ internal/handlers ──┬──▶ internal/slack (Adapter seam)
env→config MCP lifecycle, one file per verb, │ real slackGoAdapter | FakeAdapter
→store→slack RegisterVerbStubs resolve + Deps └──▶ internal/store (SQLite, modernc)
→MCP server huddles + keys tables
| Package | Role |
|---|---|
cmd/huddle |
Bootstrap: env → config → store → slack adapter → MCP server → signal-aware run loop. |
internal/server |
MCP lifecycle. RegisterVerbStubs wires every handler (despite the name, no stubs remain). |
internal/handlers |
One file per verb plus resolve.go (key-vs-huddleId speaker resolution) and deps.go. |
internal/slack |
Slack façade. Adapter interface is the seam; slackGoAdapter is the real impl, FakeAdapter backs handler tests. Identity-on-the-wire encoding lives in encoding.go. |
internal/store |
SQLite via modernc.org/sqlite (pure Go, no CGO). Two tables: huddles and keys (FK with ON DELETE CASCADE). |
internal/config |
Env-only Load(); no env required at startup. |
internal/errors |
Wraps business errors into JSON-RPC codes (InvalidParams / InternalError). |
internal/types |
Shared structs + per-verb arg/result types; IdentityKind is seat | orchestrator | human. |
Develop
make check # lint + test + build (the composite "is this shippable?" gate)
make check is the local "is this shippable?" gate. CI runs the same lint + test surface as separate jobs (go test -race, golangci-lint, plus a govulncheck scan). While iterating:
make test # go test ./...
make test-cover # with -cover
make lint # go vet + go tool golangci-lint run
make lint-fix # golangci-lint --fix
make test-e2e # cmd/smoke against real Slack — needs a token, NOT in CI
golangci-lint is a go tool dependency declared in go.mod, so there's no separate install step. The handler suite is heavily parallelized; single test idiom: go test ./internal/handlers/ -run TestCreateHappyPathTwoSeats -v.
Docs map
- docs/design.md — the "how" reference: stack rationale, repo layout, verb-level pseudocode, Slack message encoding, storage schema, env table, error handling.
- docs/features/ — per-feature design + driver docs (e.g.
human-participants/, thepolish-2026-05-18phase).
License
MIT.
Installing Huddle
This server has no published package — it is built from source. Open the repository and follow its README.
▸ github.com/itsHabib/huddleFAQ
Is Huddle MCP free?
Yes, Huddle MCP is free — one-click install via Unyly at no cost.
Does Huddle need an API key?
No, Huddle runs without API keys or environment variables.
Is Huddle hosted or self-hosted?
Self-hosted: the server runs locally on your machine via the install command above.
How do I install Huddle in Claude Desktop, Claude Code or Cursor?
Open Huddle on unyly.org, pick your client tab (Claude Desktop, Claude Code, Cursor) and press Install — the config is generated automatically, no JSON editing.
Related MCPs
Gmail
Read, send and search emails from Claude
by GoogleSlack
Send, search and summarize Slack messages
by SlackRunbear
No-code MCP client for team chat platforms, such as Slack, Microsoft Teams, and Discord.
Discord Server
A community discord server dedicated to MCP by [Frank Fiegel](https://github.com/punkpeye)
Compare Huddle with
Not sure what to pick?
Find your stack in 60 seconds
Author?
Embed badge for your README
Browse similar
All communication MCPs
