OpenCode's MCP support follows the same JSON-config shape as most agent CLIs, but its tool-permission model and TUI-first workflow change how you'd realistically use Slack from inside it. This topic walks through the actual config file, day-to-day read/post usage, a release-note broadcast example end to end, and the real limitations you'll hit if you push OpenCode's Slack integration past simple notification posting.
Installing and Connecting Slack MCP to OpenCode
OpenCode reads MCP server definitions from opencode.json (project-level, checked into the repo) or ~/.config/opencode/opencode.json (global). Add the Slack server under the mcp key:
{
"$schema": "https://opencode.ai/config.json",
"mcp": {
"slack": {
"type": "local",
"command": ["npx", "-y", "@modelcontextprotocol/server-slack"],
"environment": {
"SLACK_BOT_TOKEN": "{env:SLACK_BOT_TOKEN}",
"SLACK_TEAM_ID": "{env:SLACK_TEAM_ID}",
"SLACK_CHANNEL_IDS": "{env:SLACK_CHANNEL_IDS}"
},
"enabled": true
}
}
}
The {env:VAR_NAME} interpolation syntax is OpenCode-specific — it reads from your shell environment at launch time rather than requiring the token to be written into the JSON file, which is what makes this config safe to commit. Export the underlying vars in your shell profile or .envrc:
export SLACK_BOT_TOKEN="xoxb-0000000000-0000000000-XXXXXXXXXXXXXXXXXXXXXXXX"
export SLACK_TEAM_ID="T0123456"
export SLACK_CHANNEL_IDS="C0123ABCXYZ"
Launch OpenCode and check the server connected:
opencode
/mcp
If it shows disconnected or error, the most common cause is npx not being resolvable in the environment OpenCode's local MCP type spawns — this is a local transport (subprocess), not remote, so the npx binary needs to be on the same PATH OpenCode itself runs with, which isn't always your interactive shell's PATH if OpenCode was launched from a different context (a systemd service, a different shell init).
Tool permissions in OpenCode are configured per-tool, not just per-server. If you want slack_get_channel_history to run without prompting but slack_post_message to always ask first, set that explicitly:
{
"permission": {
"mcp__slack__slack_get_channel_history": "allow",
"mcp__slack__slack_get_thread_replies": "allow",
"mcp__slack__slack_post_message": "ask"
}
}
This split — read tools auto-approved, write tools always confirmed — is the right default for almost every Slack workflow and worth setting up before your first real session rather than after the first accidental post.
Tips
- Use{env:VAR}interpolation inopencode.jsonso the committed config never contains a literal token.
- If/mcpshows the Slack server as disconnected, check thatnpxis on the PATH of the process that launched OpenCode, not just your interactive shell.
- Split tool permissions so read tools auto-approve andslack_post_messagealways asks — set this before your first session, not after an accidental send.
Reading Channels and Posting Messages from OpenCode
Basic read usage inside an OpenCode session:
List the channels the bot has access to, then fetch the last 20 messages
from #eng-standup and tell me what's been discussed today.
OpenCode resolves this into slack_list_channels followed by slack_get_channel_history, filtering client-side for the channel matching "eng-standup" by name (the tool itself takes a channel ID, so name resolution happens as an intermediate reasoning step — worth double-checking in the transcript if you're on a workspace with similarly-named channels across teams, since a fuzzy match can grab the wrong one).
Posting follows the same pattern as any MCP tool call, gated by the ask permission set above:
Post to #eng-standup: "Migration script for the orders table finished
running in staging, no errors. Will run against prod tonight at 22:00 UTC."
OpenCode will show you the exact tool call it's about to make before executing, since slack_post_message is set to ask:
{
"tool": "mcp__slack__slack_post_message",
"input": {
"channel": "C0789GHIJKL",
"text": "Migration script for the orders table finished running in staging, no errors. Will run against prod tonight at 22:00 UTC."
}
}
Confirm, and it posts; deny, and you can redirect it — "no, post that to #db-migrations instead" — without OpenCode losing the drafted text. This confirm-then-redirect loop is where OpenCode's TUI actually shines over a pure chat interface: the tool-call preview is a first-class, editable step, not just a log line you scroll past.
For anything beyond simple channel reads and single-message posts, OpenCode's Slack handling gets noticeably less smooth — multi-step workflows involving pagination through slack_get_thread_replies cursors require you to be explicit about continuing, since the model doesn't always proactively ask "should I fetch the next page?" on its own.
Tips
- Double-check channel-name-to-ID resolution when your workspace has similarly named channels across teams — fuzzy matching can silently grab the wrong one.
- Use the deny-and-redirect flow when a drafted post is close but wrong; OpenCode keeps the draft context rather than starting over.
- For paginated thread fetches, tell the model explicitly to keep fetching until it has everything — don't assume it will ask on its own.
Practical Example: Broadcasting a Release Note to Stakeholders
A realistic end-to-end task: your team just shipped v3.2.0, and you want a stakeholder-facing summary posted to #product-updates, distinct from the terse #deploys bot notification that already fired from CI.
Read CHANGELOG.md for the v3.2.0 section. Write a stakeholder-facing
release note — non-technical audience, focus on what changed for users,
not implementation detail. Post it to #product-updates using Block Kit:
a header, a bulleted section per change, and a context footer with the
release date and a link to the full changelog on GitHub.
Show me the draft before posting.
A reasonable resulting payload:
{
"channel": "C0789PRODUPD",
"blocks": [
{
"type": "header",
"text": { "type": "plain_text", "text": "🚀 v3.2.0 is live" }
},
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": "• Faster search results (avg. 400ms → 120ms)\n• New CSV export on the reports page\n• Fixed an issue where invoices could show the wrong currency symbol"
}
},
{
"type": "context",
"elements": [
{ "type": "mrkdwn", "text": "Released Aug 21, 2026 · <https://github.com/org/repo/releases/tag/v3.2.0|Full changelog>" }
]
}
]
}
Note the header emoji here is a deliberate exception to the "no emoji unless asked" rule you'd set for internal ops channels — stakeholder-facing announcement channels tolerate, and often expect, a slightly warmer tone than an incident or deploy feed. Keep these as separate, explicit style profiles rather than one global tone rule; a single "always/never use emoji" instruction will be wrong for one of your two audiences.
Once the draft looks right, confirm the post. If your CHANGELOG.md mixes internal implementation notes with user-facing changes, be explicit about filtering — OpenCode will otherwise happily include "refactored the auth middleware" in a note meant for a product manager, which is technically true and completely useless to that reader.
Tips
- Maintain separate tone profiles for internal ops channels versus stakeholder-facing announcement channels — one global style rule won't fit both.
- Explicitly instruct filtering of internal-only changelog entries when generating stakeholder-facing content; the model won't infer audience-appropriateness on its own.
- Always review the draft for a broadcast-style post before confirming — the audience size makes a wrong send costlier than an internal channel mistake.
Known Limitations for Slack MCP in OpenCode
Be honest with yourself about where this breaks down before you build a critical workflow on it:
- No proactive pagination. As noted above, long thread or channel history fetches stop at whatever the first page returns unless you explicitly ask for more. This silently truncates summaries of very active channels if you don't notice.
- Fuzzy channel-name matching can misfire. With
SLACK_CHANNEL_IDSunset and a large workspace, asking to post to "the deploys channel" when there's both#deploysand#deploys-legacyis a real failure mode — always confirm the resolved channel ID in the tool-call preview before approving. - No built-in search tool in the reference server that most OpenCode configs use — if your workflow needs full-text search across history rather than sequential reads, you need the community
slack-mcp-servervariant withsearch_messages, which requires re-configuring auth (it typically expects user-session tokens, not a bot token — revisit the topic on Slack MCP authentication before switching). - No native scheduling. OpenCode doesn't have a built-in cron/scheduler for Slack posts; recurring notifications (daily digest, weekly summary) need an external trigger — a real cron job invoking
opencode runwith a fixed prompt file, not something OpenCode manages internally. - Session-local tool-approval memory. Approving
slack_post_messageonce in a session doesn't persist as a standing "always allow" unless you've set it inopencode.json— every new session starts back at whatever the config file says, which is usually the safer outcome but surprises people expecting it to "remember."
Tips
- Treat channel resolution as untrusted until you've read the tool-call preview — a name-based post request can silently target the wrong similarly-named channel.
- Don't rely on OpenCode for scheduled/recurring Slack posts; wire an external cron job that invokesopencode runwith a fixed prompt instead.
- If your workflow needs Slack search, plan for the community server and its different (user-token) auth model rather than assuming the reference server covers it.
Tips
Tips
- Configureopencode.jsonwith{env:VAR}interpolation and split tool permissions (read: allow, write: ask) before your first real session.
- Use the deny-and-redirect flow in the TUI to correct a near-right draft instead of restarting the prompt from scratch.
- Know the gaps going in — no proactive pagination, no built-in search in the default server, no native scheduling — and build workarounds rather than assuming OpenCode covers them.