Slack (ticket source)¶
@gaby/mcp-slack¶
Slack ticket-source MCP server for Gaby.
Connects to Slack over Socket Mode (an outbound WebSocket — no public
URL, no inbound webhook, no signing secret), drains messages as
RawTickets via the fetch_new_tickets tool, and posts agent replies back
on the thread.
Apache 2.0.
Install¶
Eventually:
Until published: build locally and point Gaby at dist/server.js.
Create the Slack app (~2 minutes)¶
Use the bundled slack-app-manifest.yaml:
- https://api.slack.com/apps → Create New App → From a manifest → pick your workspace → paste the YAML.
- Basic Information → App-Level Tokens → Generate Token and Scopes → add
connections:write→ copy thexapp-…token → that'sSLACK_APP_TOKEN. - OAuth & Permissions → Install to Workspace → copy the Bot User OAuth Token (
xoxb-…) → that'sSLACK_BOT_TOKEN. - In Gaby, add a Slack (MCP) ticket source and paste the two tokens.
- Invite the bot to your support channel:
/invite @Gaby.
Every top-level message in a channel the bot is in becomes a ticket; thread replies are pulled in when Gaby investigates, and Gaby replies in-thread.
Environment¶
| Var | Required | Default | Notes |
|---|---|---|---|
SLACK_APP_TOKEN |
✓ | — | xapp-… app-level token with connections:write (Basic Information → App-Level Tokens). Authenticates the Socket Mode WebSocket. |
SLACK_BOT_TOKEN |
✓ | — | xoxb-… bot token (OAuth & Permissions). |
SLACK_CHANNEL_IDS |
— | (all) | Optional comma-separated channel IDs (C01234,C05678) to restrict to. Empty = ingest every channel the bot is invited to — the intended "invite Gaby to #support" model. |
GABY_QUEUE_CAPACITY |
— | 10000 |
Ring buffer size; oldest dropped on overflow. |
Socket Mode lifecycle¶
- Start via
node dist/server.js. - The server opens the Socket Mode WebSocket before answering
initialize(connect error → process exit, which Gaby surfaces asHandshakeError). - Slack pushes events over the socket:
- only top-level human
messageevents are enqueued (no subtypes, no bot posts, no thread replies), - filtered to
SLACK_CHANNEL_IDSwhen set (otherwise all member channels), - each event is
ack()-ed promptly (Slack's 3 s SLA). - Overflow: oldest event dropped,
droppedSinceBootcounter surfaces in future healthchecks. healthcheck.webhook_boundreports the socket connection state (the contract field name is kept for compatibility).
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 (encoded in the manifest):
channels:history/groups:history— read messages + thread replies forget_ticketchannels:read/groups:read— resolve channel infochat:write—post_replyreactions:write—mark_resolved(adds:white_check_mark:)users:read— populates thecustomerdisplay nameusers:read.email— populatesrequester_email. Slack gates email behind this separate scope; with onlyusers:readyou get a name and no address, and Gaby cannot resolve which customer is asking
App-level scope: connections:write (Socket Mode). Event subscriptions:
message.channels, message.groups.
Troubleshooting¶
Nothing is ingested, and nothing errors. This is the common failure, and it is almost always the Slack app rather than Gaby: the app was created from scratch or from a Slack template instead of the manifest, so the bot token has no read scopes. Slack returns the granted scopes in a response header, so you can check a token directly:
curl -sS -D- -o /dev/null https://slack.com/api/auth.test \
-H "Authorization: Bearer xoxb-…" | grep -i x-oauth-scopes
Expect channels:history, channels:read, groups:history, groups:read,
chat:write, reactions:write, users:read, users:read.email. A token showing only
assistant:write, chat:write came from the AI-assistant template and cannot
read a single message. A token with users:read but not users:read.email
reads messages fine but yields tickets with a name and no address, which is
enough for Gaby to greet someone and not enough to find their account.
| Symptom | Cause | Fix |
|---|---|---|
| No tickets, no errors anywhere | bot token missing read scopes | add the scopes, reinstall, paste the new xoxb-… |
missing_scope from any Slack call |
same | same |
| Works in public channels, silent in a private one | groups:history / groups:read missing, or the bot was never invited |
add the scopes and /invite @Gaby |
| Still silent after a reinstall | SLACK_CHANNEL_IDS names a different channel |
clear it (ingest every invited channel) or correct the id |
| Only messages sent after a restart arrive | queue is in-memory | expected: the first fetch sets cursor_reset: true |
| Gaby escalates with "customer cannot be identified" | users:read.email not granted, so tickets carry a name but no email |
add the scope, reinstall, update the token |
Changing scopes never upgrades an existing token. You must reinstall the app and update the token stored in the Gaby ticket source.
Operations — notes¶
- No public surface. Socket Mode dials out to Slack, so there's no inbound endpoint to rate-limit or protect — the previous Events API webhook (and its reverse-proxy/ngrok requirement) is gone.
- 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.
post_replyhas no dedupe. Retries on the Gaby side will post duplicate messages on the thread. The bridge's idempotent snapshot guarantee makes retries rare in practice.
App manifest¶
Create the app with From a manifest and paste this verbatim. Do not use
"From scratch" or a Slack template: the AI-assistant template installs with only
assistant:write + chat:write, which cannot read a message, and Gaby then
ingests nothing without reporting an error.
# Gaby — Slack app manifest (Socket Mode)
#
# Create your Slack app from this in ~2 minutes:
# 1. https://api.slack.com/apps → "Create New App" → "From a manifest"
# Use "From a manifest" — NOT "From scratch" and not one of Slack's
# templates. The AI-assistant template installs with only
# `assistant:write` + `chat:write`, which cannot read a single message,
# and Gaby then ingests nothing with no error anywhere.
# 2. Pick your workspace, paste this YAML, Create.
# 3. Basic Information → App-Level Tokens → "Generate Token and Scopes"
# → add scope `connections:write` → copy the `xapp-…` token.
# 4. OAuth & Permissions → "Install to Workspace" → copy the
# Bot User OAuth Token (`xoxb-…`).
# 5. In Gaby: add a Slack ticket source, paste the two tokens.
# 6. Invite the bot to your support channel: /invite @Gaby
# A PRIVATE channel needs the bot invited AND the groups:* scopes below.
#
# Changing scopes later does NOT update an existing token: you must
# reinstall the app (OAuth & Permissions → Reinstall) and paste the new
# `xoxb-…` into Gaby, otherwise the old token keeps the old scopes.
#
# Socket Mode means Slack pushes events to Gaby over an outbound WebSocket —
# no public URL, no inbound webhook, no signing secret.
#
# The operator-approval scopes (im:write, commands) and interactivity are
# consumed by the v0.6 operator bot: Gaby DMs your ops channel when a draft
# needs review, with Send / Discard / Open-in-Gaby buttons riding the same
# Socket Mode connection. Installing with them now avoids a reinstall later;
# they are inert until that bot ships. (/gaby before then answers nothing.)
display_information:
name: Gaby Support
description: AI support agent that turns Slack messages into investigated, grounded replies.
background_color: "#4f46e5"
features:
bot_user:
display_name: Gaby
always_online: true
slash_commands:
- command: /gaby
description: Ask Gaby (read-only) or manage a pending draft
usage_hint: "ask <question> · pending"
should_escape: false
oauth_config:
scopes:
bot:
- channels:history # read messages + thread replies in public channels
- channels:read # resolve channel info
- groups:history # same, for private channels the bot is invited to
- groups:read
- chat:write # post replies in-thread + operator notifications
- reactions:write # mark_resolved adds :white_check_mark:
- users:read # resolve the requester's display name
# Slack gates email behind its own scope. Without it users.info returns
# a name and no email, the ticket carries no identifying address, and an
# investigation cannot work out which customer is asking: it escalates
# with "cannot be identified in the system".
- users:read.email # resolve the requester's email (tenant lookup key)
- im:write # v0.6 operator bot — open DMs for draft approvals
- commands # v0.6 operator bot — the /gaby slash command
settings:
event_subscriptions:
bot_events:
- message.channels # new messages in public channels the bot is in
- message.groups # new messages in private channels the bot is in
# NOTE: message.im is deliberately NOT subscribed yet — the current
# ticket-source server doesn't handle DM events. The v0.6 operator
# bot adds it (Settings → Event Subscriptions → reinstall).
interactivity:
is_enabled: true # block actions (Send/Discard buttons) over Socket Mode
org_deploy_enabled: false
socket_mode_enabled: true
token_rotation_enabled: false