Claude Code treats MCP servers as first-class citizens of both the CLI session and the VS Code extension's chat panel, which makes it a natural home for Slack automation: you're already in the terminal running builds and tests, so posting the result to #deploys is a one-line ask instead of a context switch. This topic covers the actual claude mcp add invocation, real Block Kit payloads for build/deploy/test notifications, thread summarization prompts that hold up on long threads, and the prompting discipline that keeps tone and mentions under control.
Installing and Connecting Slack MCP to Claude Code
Claude Code reads MCP server definitions from .mcp.json at the repo root (project scope), or from user-level config for servers you want available everywhere. Add the Slack server with the CLI rather than hand-editing JSON — it validates the entry and writes it for you:
claude mcp add slack \
--scope project \
-- npx -y @modelcontextprotocol/server-slack
export SLACK_BOT_TOKEN="xoxb-0000000000-0000000000-XXXXXXXXXXXXXXXXXXXXXXXX"
export SLACK_TEAM_ID="T0123456"
export SLACK_CHANNEL_IDS="C0123ABCXYZ,C0456DEFUVW"
If you commit .mcp.json, do not hardcode the bot token in it — reference an environment variable instead so the file is safe to check in:
{
"mcpServers": {
"slack": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-slack"],
"env": {
"SLACK_BOT_TOKEN": "${SLACK_BOT_TOKEN}",
"SLACK_TEAM_ID": "${SLACK_TEAM_ID}",
"SLACK_CHANNEL_IDS": "${SLACK_CHANNEL_IDS}"
}
}
}
}
Verify the connection before trusting it with anything important:
claude mcp list
claude mcp get slack
The VS Code extension picks up the same .mcp.json automatically once you open the repo — there's no separate configuration step. Open the Claude Code panel, and the Slack tools appear in the tool-approval list the first time the agent tries to call one. Approve slack_post_message explicitly rather than blanket-approving all Slack tools; you want a prompt on every send until you trust the workflow.
A common early mistake: running claude mcp add from inside a subdirectory of a monorepo, which writes .mcp.json to that subdirectory instead of the repo root, and then the server silently doesn't load when you cd back up. Always run claude mcp add --scope project from the repo root.
Tips
- Reference env vars (${SLACK_BOT_TOKEN}) in a committed.mcp.jsonrather than hardcoding the token, so the config file stays safe to check in.
- Runclaude mcp get slackafter setup and actually read the resolved env — a blank token connects "successfully" as a process but fails on the first real API call.
- Leaveslack_post_messageon manual approval in VS Code until you've watched the agent draft a dozen messages you'd actually send unedited.
Posting Build, Deploy, and Test Failure Notifications from the Terminal
The highest-value, lowest-risk starting workflow is posting pipeline outcomes to a dedicated channel — low risk because the content is factual and structured, not the agent's free-form judgment call.
Inside a claude session, after a build or test run:
The build just finished. Exit code was 1 and the failing test is
TestUserAuthFlow in auth_test.go, line 214. Post a failure notification
to channel C0123ABCXYZ using Block Kit: a section with the test name,
file:line, and a one-line excerpt of the assertion failure, plus a
context block with the branch name and commit SHA from git.
Do not include the full stack trace — link to the CI run instead.
Claude Code will typically produce (and, if approved, send) a payload close to:
{
"channel": "C0123ABCXYZ",
"blocks": [
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": "*Test failure:* `TestUserAuthFlow`\n*Location:* `auth_test.go:214`\n*Assertion:* expected status 200, got 401"
}
},
{
"type": "context",
"elements": [
{ "type": "mrkdwn", "text": "Branch `feature/oauth-refresh` · Commit `9f3a1c2` · <https://ci.example.com/runs/48213|View full log>" }
]
}
]
}
For deploy success notifications, a slightly richer layout with an action button linking back to the deploy dashboard reads better in a busy channel:
{
"channel": "C0456DEFUVW",
"blocks": [
{
"type": "section",
"text": { "type": "mrkdwn", "text": "*Deployed:* `api-service` `v2.14.3` to production" }
},
{
"type": "context",
"elements": [
{ "type": "mrkdwn", "text": "Duration 3m48s · Triggered by <@U0456DEF>" }
]
},
{
"type": "actions",
"elements": [
{
"type": "button",
"text": { "type": "plain_text", "text": "View deploy log" },
"url": "https://ci.example.com/deploys/9821"
}
]
}
]
}
Wire this into your actual pipeline, not just ad-hoc terminal prompts, by having your CI step invoke claude -p (headless mode) with a fixed prompt template that references the just-finished build's exit code and log path. This keeps the format stable across runs instead of varying with however you happened to phrase the request that day:
claude -p "$(cat .claude/prompts/deploy-notify.md)" \
--allowedTools mcp__slack__slack_post_message \
--append-system-prompt "Build exit code: $BUILD_EXIT. Commit: $GIT_SHA. Branch: $GIT_BRANCH."
Restricting --allowedTools to exactly the tool you need is worth doing in CI specifically — a headless run has no human watching the tool-approval prompts, so an overly broad allowlist is the difference between "posts one message" and "the agent decided to also read three private channels while it was at it."
Tips
- Template your CI notification prompt as a committed file (.claude/prompts/deploy-notify.md) so the message format doesn't drift run to run.
- In headless (-p) CI invocations, restrict--allowedToolsto the exact Slack tool needed — there's no human approving prompts mid-run to catch scope creep.
- Link to the full CI log instead of pasting a full stack trace into the message body; long text blocks in Slack read as noise and get skipped.
Summarizing Long Channel Threads into Actionable Decisions
Thread summarization is where Slack MCP earns its keep on a busy team — a 70-reply thread about an API contract change is exactly the kind of thing engineers scroll past and then ask "wait, what did we decide?" three days later.
Pull the thread with slack_get_thread_replies, giving the parent message's channel and ts:
Fetch all replies in the thread at channel C0123ABCXYZ, ts 1699999000.000100,
using slack_get_thread_replies. Then summarize it as:
1. One-sentence statement of what was being discussed
2. Final decision (quote the exact message if one person's reply settled it)
3. Anyone who disagreed and whether their concern was resolved
4. Open questions still unanswered
5. Named owners for any follow-up action, resolved to real names via slack_get_users
Keep it under 150 words. Do not editorialize beyond what's in the thread.
That last constraint — "do not editorialize beyond what's in the thread" — matters more than it looks. LLMs summarizing ambiguous discussions will confidently assert a decision was made when the thread actually trailed off unresolved. Explicitly instructing the model to flag ambiguity as "unresolved" rather than inventing a clean resolution is the single biggest quality lever for this task.
For threads long enough to blow past context in one fetch (100+ replies), chunk the summarization instead of trying it in one shot:
Fetch replies for this thread in batches of 30 using slack_get_thread_replies
with cursor pagination. Summarize each batch into 2-3 bullets first, then
produce one final synthesis from the batch summaries. Show me the
intermediate batch summaries so I can catch anything that got compressed
away.
Posting the summary back into the thread (rather than DMing it to you) is usually the better default — it becomes a durable artifact anyone landing on the thread later can read, and it signals to the thread participants that the discussion has a documented outcome:
{
"channel": "C0123ABCXYZ",
"thread_ts": "1699999000.000100",
"text": "Summary of this thread: decided to version the API as /v2 rather than a header flag. @alice raised a caching concern, resolved by @bob's note about cache-control headers. Open: who updates the client SDK — no owner assigned yet."
}
Tips
- Explicitly instruct the model to mark ambiguous outcomes as "unresolved" rather than let it invent a clean decision that was never actually made.
- Chunk summarization for threads over ~60 messages and review the intermediate batch summaries — single-shot summarization silently drops details on long threads.
- Post the summary back into the thread itself so it becomes a durable, shared artifact instead of a private note only you saw.
Prompting Patterns for Message Tone, Format, and Mentions
Tone drift is the most common quality complaint on Slack automation: a bot that's too formal reads as corporate noise, one that's too casual reads as unprofessional in an incident channel. Fix this by giving Claude Code a standing tone instruction in CLAUDE.md or the system prompt, not by re-specifying it every time:
## Slack posting style
- Short sentences. No exclamation marks. No emoji unless explicitly asked.
- Lead with the outcome, not the process ("Deploy failed" not "I tried to deploy and it failed").
- Never use @here or @channel without explicit human confirmation in the same turn.
- Resolve all @mentions to real Slack user IDs before posting — never post a raw name as plain text if the person has a Slack account.
For mentions specifically, the failure mode to guard against is the model guessing at a Slack user ID format instead of resolving it:
Bad: "cc @John Smith" (plain text, doesn't notify anyone)
Bad: "cc <@U000000>" (fabricated ID, will render as "unknown user" or worse, ping the wrong person)
Good: call slack_get_users first, match by display name or email,
then use the real ID: "cc <@U04X7QK2M8Y>"
Make the two-step resolve-then-mention pattern explicit in your prompt when mentions matter:
I need to loop in whoever owns the payments service. Look them up with
slack_get_users (match display name containing "payments" or check
slack_get_user_profile for a team field), confirm the match with me,
then draft the message with their real mention.
For format, Block Kit over plain text is the right default for anything posted repeatedly, but resist over-formatting one-off human-facing messages — a quick "hey, FYI the staging DB is being restored, expect flakiness for 20 min" reads better as plain mrkdwn text than as a three-block card. Reserve Block Kit structure for recurring, templated notifications where the visual consistency actually helps scanning.
Tips
- Put standing tone and mention rules inCLAUDE.mdonce, rather than re-explaining them in every prompt — consistency across sessions matters more than any single message's wording.
- Always resolve mentions viaslack_get_usersbefore posting; a fabricated or guessed user ID either fails silently or pings the wrong person.
- Reserve Block Kit for recurring, templated notifications — a one-off human update often reads better as a short plain-text message.
Tips
Tips
- Set up.mcp.jsonat the repo root with env-var references, and verify withclaude mcp get slackbefore trusting any workflow.
- Restrict--allowedToolstightly for headless CI invocations — no human is there to catch an overly broad tool grant mid-run.
- Standardize tone, mention resolution, and format rules inCLAUDE.mdso Slack output stays consistent across sessions instead of drifting with each prompt's phrasing.