Skip to content

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:

npx -y @gaby/mcp-slack

Until v0.3 tag: build locally and point Gaby at dist/server.js.

pnpm install
pnpm build

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

  1. Start via node dist/server.js
  2. Server binds Fastify on GABY_WEBHOOK_PORT before answering initialize (bind error → process exit 2, which Gaby surfaces as HandshakeError)
  3. Slack events POST to /slack/events:
  4. HMAC SHA256 verified against SLACK_SIGNING_SECRET
  5. Timestamp skew ≤ 5 min enforced
  6. URL-verification challenge echoed in-band
  7. Only message events from channels in SLACK_CHANNEL_IDS are enqueued
  8. Acked with 200 regardless (respects Slack's 3 s ack SLA)
  9. Overflow: oldest event dropped, droppedSinceBoot counter 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 for get_ticket
  • chat:writepost_reply
  • reactions:writemark_resolved (adds :white_check_mark:)
  • users:read — optional, populates customer display 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/events endpoint without rate-limiting is a DoS target. Front the port with a reverse proxy that caps POST /slack/events at ~10 req/s per source IP. NGINX limit_req_zone or 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_reply has 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.