Slack (ticket source)¶
@gaby/mcp-slack¶
Slack ticket-source MCP server for Gaby.
Receives Slack Events API webhooks, drains them as RawTickets via the
fetch_new_tickets tool, and posts agent replies back on the thread.
Apache 2.0.
Install¶
Eventually:
Until v0.3 tag: build locally and point Gaby at dist/server.js.
Environment¶
| Var | Required | Default | Notes |
|---|---|---|---|
SLACK_BOT_TOKEN |
✓ | — | xoxb-…, bot scope includes channels:history, chat:write, reactions:write |
SLACK_SIGNING_SECRET |
✓ | — | From Slack App → Basic Information |
SLACK_CHANNEL_IDS |
✓ | — | Comma-separated channel IDs (e.g. C01234,C05678). Server refuses to start if empty to prevent ingesting every message in every bot-joined channel. |
GABY_WEBHOOK_PORT |
✓ | — | 1024–65535. Bound before initialize responds; bind failure fails the MCP handshake. |
GABY_WEBHOOK_HOST |
— | 0.0.0.0 |
Listener bind interface |
GABY_QUEUE_CAPACITY |
— | 10000 |
Ring buffer size; oldest dropped on overflow |
Webhook lifecycle¶
- Start via
node dist/server.js - Server binds Fastify on
GABY_WEBHOOK_PORTbefore answeringinitialize(bind error → process exit 2, which Gaby surfaces asHandshakeError) - Slack events POST to
/slack/events: - HMAC SHA256 verified against
SLACK_SIGNING_SECRET - Timestamp skew ≤ 5 min enforced
- URL-verification
challengeechoed in-band - Only
messageevents from channels inSLACK_CHANNEL_IDSare enqueued - Acked with 200 regardless (respects Slack's 3 s ack SLA)
- Overflow: oldest event dropped,
droppedSinceBootcounter surfaces in future healthchecks
Contract conformance¶
Passes connectors/_contract/test_ticket_source_contract.py v1 — see
the contract spec for the full 5-tool wire shape.
Queue state is in-memory only. On restart the buffer is empty and
the first fetch_new_tickets response sets cursor_reset: true so
Gaby's bridge re-bootstraps from null.
Slack app scopes¶
Bot scopes required:
channels:history— read thread replies forget_ticketchat:write—post_replyreactions:write—mark_resolved(adds:white_check_mark:)users:read— optional, populatescustomerdisplay name
Event subscriptions:
message.channels(if you use public channels)- Request URL:
https://<your-host>:<GABY_WEBHOOK_PORT>/slack/events
Public URL: you'll need a reverse proxy, ngrok, or Cloudflare Tunnel. Gaby does not proxy Slack webhooks.
Operations — hardening notes¶
- Rate-limit the webhook. Signature verification rejects bad HMACs
cheaply, but a public
/slack/eventsendpoint without rate-limiting is a DoS target. Front the port with a reverse proxy that capsPOST /slack/eventsat ~10 req/s per source IP. NGINXlimit_req_zoneor Cloudflare Rules work fine. - Queue is in-memory only. A crash drops all unreplied events. This is deliberate (no disk state, no config to back up) but means rapid restarts under load will lose messages. v0.4 adds optional SQLite- backed durable queue.
post_replyhas no dedupe. Retries on the Gaby side will post duplicate messages on the thread. v0.4 adds a dedup key. For now, the bridge's idempotent snapshot guarantee in 13a means retries are rare in practice.