Slack MCP Server (amazing-slack-mcp)
A Model Context Protocol server for the
Slack Web API, built on the official
MCP Python SDK (FastMCP). It
speaks as you: the only credential it accepts is the User OAuth Token (xoxp-…) of
your own internal Slack app, so everything it posts shows your name and avatar, and
everything it reads is what you can see in Slack. Slack does not label these messages:
they look exactly like ones you typed (measured with an internal app, in a self-DM and in
a channel). To let teammates tell them apart, set SLACK_SIGNATURE — the server shows
it under every message it posts, replies with, schedules or edits, in a Block Kit context
block (the small grey font of Slack's own labels). The recommended value is a bold label
and a mention of your app's bot user (*Sent using* <@U0BOTID>). See
Signing your messages.
You create one personal, internal app per workspace (see Prerequisites).
The server runs over stdio only. There is no HTTP transport, so a token that acts as
you never leaves the machine that runs the server. Bot tokens (xoxb-…), app-level
tokens (xapp-…), browser session tokens (xoxc-… / xoxd-…) and refresh tokens are
refused before anything is sent. Bots belong to a bot gateway (such as Hermes), not here.
The PyPI distribution and the console script are both
amazing-slack-mcp. The bareslack-mcpandslack-mcp-servernames on PyPI belong to unrelated packages, so always runuvx amazing-slack-mcp, neveruvx slack-mcp.
Safety model
The write rails (kill-switch, allowlists, the closed method list, no write retry) live in the HTTP client, not in the tools, so no tool can forget them:
- Writes are off by default. Posting, replying, editing, scheduling, reacting and
opening DMs return
Error: … writes are disableduntilSLACK_ALLOW_WRITES=1. Reads need nothing. - Allowlists, by ID only. With writes on, a write still needs its target in
SLACK_WRITE_CHANNELS(conversation IDs), or, for a DM or group DM, every other member inSLACK_WRITE_USERS(user IDs). A write that names a#channel-nameor aU…user id instead of a conversation ID is refused, because Slack would resolve it after the allowlists were checked. To message a person, open the DM withslack_open_conversation(every id must be inSLACK_WRITE_USERS) and post to theD…/G…id it returns. - Your self-DM needs no allowlist entry. Once
SLACK_ALLOW_WRITES=1, the DM with yourself passes without any allowlist entry, which makes it a good place to try writes out. - Slack Connect only by explicit listing. A channel or group DM shared outside the
workspace (any of
is_shared,is_ext_shared,is_org_shared,is_pending_ext_shared) is writable only if you list its conversation id inSLACK_WRITE_CHANNELS. The same goes for a DM that Slack flags as shared. Any other DM or group DM with someone from another organisation needs that person's own user id inSLACK_WRITE_USERS. Slack does not reliably flag DMs with external people, so for DMs the member allowlist is the gate that holds: nobody gets a message unless you listed them, or the conversation, yourself. - Nothing marks a post as automated unless you sign it. Slack shows these messages
under your name with no app label, exactly like ones you typed. Set
SLACK_SIGNATUREso every message the server posts carries a line, under the text in Slack's grey label font, that says it was sent by your app (Signing your messages).slack_health_checksays whether it is on. - A write is never retried. A retried post is a duplicate message. A timeout or 5xx on a write is reported as outcome UNKNOWN: read the conversation back before trying again. (A read is retried once on HTTP 429, and only when Slack asks to wait 10 s or less.)
- No delete tool. There is no tool to delete a message or to cancel a scheduled one. The server never joins a channel on its own, and it can only call the Slack methods on its built-in list.
- No token in any output. Every result and error a tool returns is scrubbed of the configured token and of anything shaped like a Slack token (Slack echoes tokens in some error bodies). This includes what the MCP SDK itself produces on the server's behalf — an argument rejected before a tool runs (the rejected value is not echoed), an unknown tool name, and the SDK's own log lines on stderr. The HTTP library's one-line-per-request logs are silenced.
- Text from other people is data, not instructions. History, threads and search results contain messages anyone in the workspace could have written. Don't let your agent follow instructions found in them, and keep your MCP client's approval prompt on for the 🔒 tools: the guard is a backstop, not a replacement for that prompt.
The server stores nothing on disk. The user directory used by slack_resolve_user and
the guard's conversation lookups (reused for five minutes) are kept in memory only. Slack's
Developer Policy forbids using Slack data to train an LLM. This server only returns what
you ask for to your MCP client; what the client keeps is set by the client.
Found a way around one of these rails? Report it privately: see SECURITY.md.
Features
18 tools (17 Slack tools plus a health check). They are listed below by group, and every one is in the table.
- Conversations.
slack_list_conversationsturns a channel name into the conversation ID every other tool needs. It lists public and private channels, group DMs and DMs, and flags shared ones.slack_get_conversationshows one conversation, with an explicitSlack Connect: yes/noline.slack_list_membersreturns member ids.slack_get_historyandslack_get_threadrender messages oldest first as[time] author: text (ts …), so a reply or a reaction can target thets. A user token can read any public channel without joining it.slack_open_conversation🔒 opens (or finds) a DM or a group DM with 1–8 people. - Users.
slack_list_users,slack_get_userandslack_lookup_user_by_email(exact match).slack_resolve_useris the name → id resolver. It reads up to 20 pages of the directory (about 4,000 people) on first use and answers from that per-process cache afterwards (refresh=truere-reads it), then ranks people by similarity to a name, display name or @handle, ignoring case and accents. It calls out a unique match together with the<@U…>string that mentions that person. - Chat.
slack_post_message🔒,slack_reply_in_thread🔒,slack_update_message🔒 (your own messages only) andslack_schedule_message🔒 (in the future, at most 120 days ahead). Text goes out verbatim (only leading and trailing whitespace is trimmed), and each tool has aformatswitch:"mrkdwn"(default, Slack's own syntax) or"markdown"(real Markdown, sent asmarkdown_text). After a post or a thread reply, the confirmation includes the message's permalink. If the permalink lookup fails, the message is still reported as sent. Every confirmation repeats only what Slack returned, including any warning Slack attached, plus asignatureline:SLACK_SIGNATURE, when set, goes in a context block under every post, reply, scheduled message and edit, and ends the plain-text fallback exactly once. - Reactions.
slack_add_reaction🔒 andslack_remove_reaction🔒 (yours only). Colons around the emoji name are stripped. "Already there" and "not there" are reported as outcomes, not errors. - Search.
slack_search_messagespasses Slack's own query syntax through verbatim (in:#channel,from:@user,after:2026-10-01,is:thread, …). Search works only with a user token, because a bot token cannot search at all. That is one reason this server speaks as you. - Health.
slack_health_checkcallsauth.testand shows who you are, the token kind, the granted scopes compared with the 15 the tools need, the write-guard state, and whether a signature is on.
Read tools other than slack_health_check take response_format: "markdown"
(default) or "json" (the raw Slack objects). Markdown output is capped at 50 rows per call. Cursor-paginated tools print
next_cursor verbatim and never walk more than one page per call. Search pages by
number. The one exception is slack_resolve_user, which reads up to 20 pages of the directory
into its cache on first use and says so when the index is partial.
Available Tools
All 18 tools, grouped by module. 🔒 = refused unless SLACK_ALLOW_WRITES=1 and the target passes the allowlists (SLACK_WRITE_CHANNELS / SLACK_WRITE_USERS).
| Tool | Description |
|---|---|
| Health | |
slack_health_check |
Verify the token against Slack, show who you are, the granted scopes, and the write guard. |
| Conversations — channels, DMs, history, threads | |
slack_get_conversation |
Show one conversation's details, including whether it is shared outside the workspace. |
slack_get_history |
Read the messages of a channel, group DM or DM, rendered oldest first. |
slack_get_thread |
Read one thread: the parent message first, then its replies oldest first. |
slack_list_conversations |
List channels, private channels, group DMs and DMs, one page at a time. |
slack_list_members |
List the user ids of a conversation's members, one page at a time. |
slack_open_conversation 🔒 |
Open (or find) a DM or group DM with 1–8 people and return its conversation id. |
| Users — lookup and name → ID resolution | |
slack_get_user |
Show one person's profile by user id. |
slack_list_users |
List the people in the workspace, one cursor page at a time. |
slack_lookup_user_by_email |
Find the person behind an email address and show their profile. |
slack_resolve_user |
Turn a name, display name or @handle into Slack user ids, ranked by similarity. |
| Chat — post, reply, edit, schedule | |
slack_post_message 🔒 |
Post a message to a Slack channel, DM or group DM as you — Slack adds no label; SLACK_SIGNATURE signs it if set. |
slack_reply_in_thread 🔒 |
Reply inside a Slack thread as you — Slack adds no label; SLACK_SIGNATURE signs it if set. |
slack_schedule_message 🔒 |
Schedule a Slack message for later, posted as you — Slack adds no label; SLACK_SIGNATURE signs it if set. |
slack_update_message 🔒 |
Edit one of your own Slack messages as you — a SLACK_SIGNATURE line, if set, stays exactly once. |
| Reactions | |
slack_add_reaction 🔒 |
Add an emoji reaction to a message, as you. |
slack_remove_reaction 🔒 |
Remove one of YOUR emoji reactions from a message. |
| Search | |
slack_search_messages |
Search messages across every conversation you can see, with Slack's query syntax. |
The table is generated from the code (scripts/gen_tool_table.py). Each row is the first
line of that tool's docstring.
Prerequisites — create the Slack app
You need Python 3.13+ (or just uv, which brings its own), and one Slack app per workspace, created by you, for you:
-
Go to https://api.slack.com/apps → Create New App → From scratch. Give it any name (the author's is "Alejandro AI"). Slack does not show it on your messages; to name it there, put it in
SLACK_SIGNATURE(Signing your messages). Pick the workspace. -
OAuth & Permissions → Scopes → User Token Scopes (not Bot Token Scopes) → add exactly these 15:
channels:read channels:history groups:read groups:history im:read im:history im:write mpim:read mpim:history mpim:write chat:write reactions:write users:read users:read.email search:read
Scopes Used by channels:readgroups:readim:readmpim:readlisting and inspecting public channels, private channels, DMs and group DMs (also the write guard's lookups) channels:historygroups:historyim:historympim:historyslack_get_history,slack_get_threadim:writempim:writeslack_open_conversation(DM / group DM)chat:writepost, reply, edit, schedule reactions:writeadd / remove a reaction users:readusers:read.emailthe user tools (emails, and slack_lookup_user_by_email, needusers:read.email)search:readslack_search_messagesThis is the list
slack_health_checkcompares against. Add nothing else.channels:writeandreactions:readare deliberately absent: no tool joins a channel or reads a message's reactions, andchannels:writewould also allow archiving, creating, renaming and kicking. Leave Bot Token Scopes empty, because this server never uses a bot token — the one exception is the single bot scope (users:read) of the bot user the recommended signature mentions, if you add it (Signing your messages). -
Token rotation: leave it OFF. Slack says rotation "may not be turned off once it's turned on", and a rotated token (
xoxe.xoxp-…) expires every 12 hours. This server has no refresh flow: it accepts such a token, and the health check warns that it will expire. -
Manage Distribution: leave public distribution OFF. Turning it on makes the app an unlisted distributed app. Under Slack's 2025 rate-limit change, new installs of those apps drop to 1 request a minute on history and threads, while internal apps keep their limits. An undistributed app lives in a single workspace, which is why a second workspace needs a second app.
-
Install to Workspace → allow → copy the User OAuth Token (
xoxp-…). That value is yourSLACK_USER_TOKEN. If the app has a bot user, the page also shows a Bot User OAuth Token (xoxb-…): never use that one — the server refuses it.
Scopes only ever get added to an issued token: Slack says "it is not possible to downgrade an access token's scopes". To add a scope later, add it and reinstall the app, then copy the token again. To drop a scope, re-create the app. Slack's docs also say an app can be uninstalled automatically when the person who installed it leaves the workspace or becomes a guest.
Quickstart
-
Put the token where the server can read it: either a
.envfile in the directory you launch from, or real environment variables (they win over.env):# .env (never commit it) SLACK_USER_TOKEN=xoxp-...
-
Run it with no install:
uvx amazing-slack-mcpIt is a stdio server, so in a bare terminal it just waits for an MCP client on stdin. Stop it with Ctrl-C and point a client at it instead (below).
-
Call
slack_health_checkfirst. It needs no arguments and shows the workspace, your user name and id, whether the token is a user token, which of the 15 scopes are missing, and whether writes are enabled. Without a client, use the MCP Inspector.
From a clone of the repo (uv sync --group dev), the same check runs in one line:
uv run python -c "import asyncio; from slack_mcp.tools.health import slack_health_check; print(asyncio.run(slack_health_check()))"
Writes stay off until you add SLACK_ALLOW_WRITES=1 plus the allowlists
(Environment variables).
Client configuration
Every client starts the server as a subprocess and passes settings through env.
Two workspaces means two entries: same package, a different SLACK_USER_TOKEN (one
app per workspace), and usually a different SLACK_WRITE_USERS. The examples below use
slack-udocz and slack-masava. Leave out SLACK_ALLOW_WRITES for an entry that
should only read.
Keep the token out of config files where you can. Two ways to do that:
- Variable expansion. Cursor (
${env:NAME}) and a Claude Code project.mcp.json(${NAME}) expand variables from the environment the client was started in. - 1Password
op run. Launch the server asop run -- uvx amazing-slack-mcpand setSLACK_USER_TOKENto anop://vault/item/fieldreference.op runswaps in the secret only in the subprocess's environment. This works in any client that can launch a command, provided theopCLI is installed and signed in where the client runs (use absolute paths if the client has a minimal PATH), and it is the simplest way to give two entries two different tokens.
Claude Code
claude mcp add-json slack-udocz '{
"type": "stdio",
"command": "op",
"args": ["run", "--", "uvx", "amazing-slack-mcp"],
"env": {
"SLACK_USER_TOKEN": "op://Private/slack-udocz/credential",
"SLACK_ALLOW_WRITES": "1",
"SLACK_WRITE_USERS": "U0123ABCDEF"
}
}' --scope user
claude mcp add-json slack-masava '{
"type": "stdio",
"command": "op",
"args": ["run", "--", "uvx", "amazing-slack-mcp"],
"env": {
"SLACK_USER_TOKEN": "op://Private/slack-masava/credential",
"SLACK_ALLOW_WRITES": "1",
"SLACK_WRITE_USERS": "U0456GHIJKL"
}
}' --scope user
Without 1Password, use "command": "uvx", "args": ["amazing-slack-mcp"]. In a project
.mcp.json, write the token as "SLACK_USER_TOKEN": "${SLACK_UDOCZ_TOKEN}" and export
SLACK_UDOCZ_TOKEN in the shell that starts Claude Code. A literal xoxp-… value in
env also works, but it then sits in plain text in the config file.
Cursor
~/.cursor/mcp.json (global) or .cursor/mcp.json (per project):
{
"mcpServers": {
"slack-udocz": {
"command": "uvx",
"args": ["amazing-slack-mcp"],
"env": {
"SLACK_USER_TOKEN": "${env:SLACK_UDOCZ_TOKEN}",
"SLACK_ALLOW_WRITES": "1",
"SLACK_WRITE_USERS": "U0123ABCDEF"
}
},
"slack-masava": {
"command": "uvx",
"args": ["amazing-slack-mcp"],
"env": {
"SLACK_USER_TOKEN": "${env:SLACK_MASAVA_TOKEN}",
"SLACK_WRITE_USERS": "U0456GHIJKL"
}
}
}
}
(slack-masava here is read-only: no SLACK_ALLOW_WRITES.)
Codex
~/.codex/config.toml. With one workspace, env_vars forwards SLACK_USER_TOKEN from
the environment Codex runs in, so the token never goes into the file:
[mcp_servers.slack-udocz]
command = "uvx"
args = ["amazing-slack-mcp"]
env_vars = ["SLACK_USER_TOKEN"]
env = { SLACK_ALLOW_WRITES = "1", SLACK_WRITE_USERS = "U0123ABCDEF" }
env_vars forwards a variable under its own name, so two entries would both receive the
same SLACK_USER_TOKEN. For a second workspace, give each entry its own reference through
op run:
[mcp_servers.slack-masava]
command = "op"
args = ["run", "--", "uvx", "amazing-slack-mcp"]
env = { SLACK_USER_TOKEN = "op://Private/slack-masava/credential", SLACK_WRITE_USERS = "U0456GHIJKL" }
Claude Desktop
Settings → Developer → Edit Config opens claude_desktop_config.json (on macOS:
~/Library/Application Support/Claude/claude_desktop_config.json). Add the same
mcpServers block as in the Cursor example. Desktop's config file is not documented to
expand environment variables, so use a literal token or the op run form. Quit Desktop
before you edit the file: a running Desktop has been seen to write its in-memory copy
back over edits. Restart Desktop afterwards. If Desktop cannot find uvx or op, use
their absolute paths (which uvx, which op).
Mind the tool budget. Claude Desktop's cloud (Cowork) sessions publish your local MCP tools when they start, and a session has been seen to fail to start once the total tool count across all servers grew large. Each entry of this server adds 18 tools, so with many servers configured, consider leaving it out of Desktop and using it from Claude Code, Cursor or Codex.
MCP Inspector
From a directory with your .env:
npx @modelcontextprotocol/inspector uvx amazing-slack-mcp
Open the Tools tab and run slack_health_check. From a clone,
npx @modelcontextprotocol/inspector uv run amazing-slack-mcp does the same against your
working tree.
Environment variables
Settings are read once per server process, on first use, so restart the client after
changing one. String values are whitespace-stripped. Real environment variables win over
.env, which is read from the server's working directory.
| Variable | Default | Meaning |
|---|---|---|
SLACK_USER_TOKEN |
(empty) | Required. Your app's User OAuth Token (xoxp-…), sent as Authorization: Bearer …. Any other token is refused (known kinds such as xoxb-, xapp-, xoxc- by name), and the value is never echoed. |
SLACK_ALLOW_WRITES |
off | Kill-switch. 1 allows posting, replying, editing, scheduling, reacting and opening DMs, and even then every write must pass the allowlists. |
SLACK_WRITE_CHANNELS |
(empty) | Comma-separated conversation IDs (C…, G…, D…) a write may target. IDs only; a #name never matches. The only way to write to a Slack Connect channel or a shared group DM. |
SLACK_WRITE_USERS |
(empty) | Comma-separated user IDs (U…, W…). A DM or group DM is writable when every other member is listed, and slack_open_conversation only opens conversations with these people. Your own self-DM needs no entry. |
SLACK_SIGNATURE |
(empty = off) | A line the server shows under every message it posts, replies with, schedules or edits, in a Block Kit context block (Slack's small grey label font), and appends to the plain-text fallback after a blank line ("\n\n" + signature) — never twice. Slack does not label these messages, so this is how teammates can tell them apart. Recommended: *Sent using* <@U0BOTID>, a bold label and a mention of your app's bot user. Slack mrkdwn, one line (a multi-line value is joined into one), at most 3,000 characters; it counts toward the length limits. See Signing your messages. |
SLACK_API_URL |
https://slack.com/api |
Web API base URL, for proxies and tests. Must be https://; plain http:// is accepted only for 127.0.0.1 / localhost. |
SLACK_REQUEST_TIMEOUT_SECONDS |
30 |
Per-request timeout. A timed-out write is reported as outcome UNKNOWN, never retried. |
SLACK_TEST_ALLOW_WRITES |
(unset) | Test suite only, never read by the server. 1 opens the live_write tests, but only when it is in the environment pytest starts with (a value in .env is ignored for this variable) and only with exactly -m live_write. See Running the tests. |
Signing your messages
Slack does not label these messages: they look exactly like ones you typed. Measured on
2026-10-10 with an internal app and a user token, in a self-DM and in a channel:
no app label under the message. To let teammates tell them apart, set SLACK_SIGNATURE.
Every message the server posts, replies with, schedules or edits then carries it in a
Block Kit context block under the text, which Slack renders in the small grey font of
its own labels.
The recommended value is a bold label, like Slack's native one, and a real Slack mention of your app's bot user:
# .env — the bot user's id (U…), found as in step 3 below
SLACK_SIGNATURE=*Sent using* <@U0BOTID>
Slack renders the mention as the blue @Alejandro AI tag, and clicking it opens the
app's profile inside Slack — a link would open the web browser instead. The app gets a
bot user only so it can be mentioned. Its bot token is never used, and the server refuses
it anyway (xoxb-…). To add one:
-
https://api.slack.com/apps → your app → App Manifest → add these keys, keeping the user scopes as they are:
features: bot_user: display_name: Alejandro AI always_online: false oauth_config: scopes: bot: - users:read
One bot scope,
users:read, because Slack does not install a bot user without one. It is a bot scope: it does not go on your user token, whose 15 scopes stay as they are. -
Save, then Install App → Reinstall to Workspace.
SLACK_USER_TOKENstays the User OAuth Token (xoxp-…; copy it again if it changed), never the new Bot User OAuth Token (xoxb-…). -
Find the bot user's id with
slack_list_usersandinclude_bots: true(bots are hidden by default), or withslack_resolve_user,query: "Alejandro AI"andinclude_bots: true. It is aU…id: that is what goes in<@…>.
Without a bot user, the fallback is a link to your app's page. It renders the
@Alejandro AI part in link colour, like a tag, but opens the web:
# .env — your workspace's subdomain, and the App ID from your app's Basic Information page
SLACK_SIGNATURE=*Sent using* <https://<workspace>.slack.com/marketplace/<APP_ID>|@Alejandro AI>
For example, the author's app in the Masava workspace is
*Sent using* <https://masavaco.slack.com/marketplace/A0C835DFNSH|@Alejandro AI>.
In a shell, quote the value (*, <, > and | are shell metacharacters); in a
client's JSON env block it is an ordinary string. Either line only looks like Slack's
label: it is message content, a marker and not provenance — anyone can type the same
line, and any app can post the same block. How it behaves:
-
With a signature set,
slack_post_message,slack_reply_in_thread,slack_schedule_messageandslack_update_messagesend Block Kitblocksplus atextfallback, in both formats. Without one, nothing changes: plaintext(mrkdwn) ormarkdown_text(Markdown), no blocks. The blocks are:format: "mrkdwn": the text in one or moresectionblocks, cut at line boundaries into pieces of at most 3,000 characters (Slack's limit for a section's text). A single longer line is cut at a space, never inside a<@U…>mention or<url|label>link, and hard at 3,000 only when there is no such point;format: "markdown": the text in onemarkdownblock (at most 12,000 characters), and nomarkdown_text, which Slack does not accept next toblocks;- then a
contextblock with the signature as mrkdwn — so write it in mrkdwn (*bold*,<@U…>,<url|label>).
With the recommended value, what goes to Slack for
Deploy done :rocket:is:{ "text": "Deploy done :rocket:\n\n*Sent using* <@U0BOTID>", "blocks": [ {"type": "section", "text": {"type": "mrkdwn", "text": "Deploy done :rocket:"}}, {"type": "context", "elements": [{"type": "mrkdwn", "text": "*Sent using* <@U0BOTID>"}]} ] }
-
The
textfallback is what Slack shows in notifications and search: the text, a blank line ("\n\n"), then the signature. Forformat: "markdown"its<url|label>links are written as[label](url)there; a<@U…>mention stays as it is. The signature never goes inside a body block. -
Slack flattens the
textfallback of a message posted with blocks when it stores it: every newline becomes a space (measured 2026-10-10), so it reads back as one line. The read tools (slack_get_thread,slack_get_history) rebuild the text from the blocks, so forformat: "mrkdwn"what you read back is what was posted: the text with its line breaks, then the signature as its last line. This applies to every app-built message, not only the server's: text another app put only in itstextfallback is not shown in markdown mode (JSON mode returns the raw messages,textand blocks included). A message typed by a person (onlyrich_textblocks) still reads back fromtext.One v0.1 limit: Slack stores a
markdownblock asrich_text(measured 2026-10-10), so a message posted withformat: "markdown"reads back from its flattenedtext, as one line with only the signature on its own last line. Editing it from that read-back loses its line structure (headings, lists and paragraphs run together). Pass the original text when you edit such a message. -
slack_update_messageresends the blocks with every edit, replacing the old ones (an edit that sends onlytextwould drop them). The signature is appended unless the new text already ends with it, so an edit of a signed message stays signed exactly once: it counts whatever separates it from the text — blank lines, one newline, the spaces Slack's flattened fallback leaves of them, or nothing — in either form (<url|label>or[label](url), so switchingformaton an edit is safe) and as Slack stores it (&read back as&), so editing the text read back from Slack does not add a second one — the signature is cut from the body and goes back in the context block. A signature quoted in the middle of a text does not count; the text is signed again at the end. Slack does not show its "edited" mark on an edit made with blocks. -
It counts toward Slack's length limits, blank line included: the caps apply to the final
text. A text that only goes over because of the signature is refused before anything is sent, and the error says so. So is a signature longer than 3,000 characters (the limit for a context block's text) and a text that would need more than 49 sections (Slack allows 50 blocks per message, the context block included). -
Every confirmation has a
signatureline (appended (context block),already at the end … (context block), ornot configured), andslack_health_checkshowson (N chars)oroff.
Formatting cheatsheet
With the default format: "mrkdwn", the text is Slack's own syntax, which is not
Markdown:
| You want | Write |
|---|---|
| Mention a person | <@U0123ABCDEF>, which takes the user id, never a name (get it from slack_resolve_user). Slack notifies them only if they are a member of that conversation. |
| Link a channel | <#C0123ABCDEF>. Slack renders the channel's current name. Text read back may carry it as <#C0123ABCDEF|general>. |
| A link | <https://example.com|label>, or a bare URL |
| Bold / italic / strike | *bold* (single asterisks; **x** is not bold), _italic_, ~strike~ |
| Code | `inline` and ```block``` |
| Quote | > quoted line |
| Notify everyone | <!channel> notifies every member and <!here> only the active ones; both go out as you. |
| Headings, lists | mrkdwn has no headings and no list syntax. Write • or - lines as plain text, or use format: "markdown". |
A literal &, < or > |
escape only those three: &, <, > |
Set format: "markdown" to write ordinary Markdown instead (**bold**, # heading,
[label](url), lists). The same string is sent as Slack's markdown_text, which holds
at most 12,000 characters. Mentions still need the <@U…> form. The other limits:
slack_post_message, slack_reply_in_thread and slack_schedule_message accept up to
40,000 characters of mrkdwn, and slack_update_message up to 4,000. A SLACK_SIGNATURE
counts toward each of these limits, and with one set the text goes out in Block Kit
blocks (mrkdwn in sections of 3,000 characters, Markdown in one markdown block — see
Signing your messages).
Running the tests
uv sync --group dev
uv run pytest -m "not live and not live_write" # the gate — never touches Slack
uv run pytest -m live # read-only smoke; needs SLACK_USER_TOKEN (shell or .env)
SLACK_TEST_ALLOW_WRITES=1 uv run pytest -m live_write # the self-DM smoke (tests/test_live_write.py)
- The default tier mocks every HTTP call and strips your real
SLACK_*settings, so it runs anywhere. liveruns only the read-only smoke tests against your workspace, and only whenSLACK_USER_TOKENis set. Otherwise those tests are skipped.live_writeis the self-DM smoke (tests/test_live_write.py). It writes to Slack as you, in your own self-DM only, never a channel or another person. It runs only when all three hold: the token is set,SLACK_TEST_ALLOW_WRITES=1is in the environment pytest starts with (set it inline as shown; a value in.envis ignored on purpose), and the marker expression is exactly-m live_write. Nothing can delete what it posts, so the messages stay in your self-DM.
Troubleshooting
missing_scope. The error names the scope Slack wanted (and the ones the token has, when Slack says). Add it under User Token Scopes, reinstall the app, put the token inSLACK_USER_TOKENagain and restart the client.slack_health_checklists every missing scope at once.not_in_channel. You can read a public channel without joining it, but you cannot post there. Join it in Slack yourself: the server never auto-joins, and it does not requestchannels:write.invalid_auth/token_revokedafter a reinstall. The token was revoked or replaced. Copy the User OAuth Token from OAuth & Permissions again, update it wherever it is configured, and restart the client.- "SLACK_USER_TOKEN is a bot token (xoxb-…), which this server refuses". You pasted
the Bot User OAuth Token. This server speaks as you and accepts only a user token, so
copy the User OAuth Token (
xoxp-…) instead. If you need a bot, that belongs to a bot gateway, not here. App-level (xapp-…) and browser (xoxc-…/xoxd-…) tokens get the same treatment. - A
#nameis rejected, orchannel_not_found. Tools take conversation IDs only, and a#nameis refused before anything is sent. Find the ID withslack_list_conversations.channel_not_foundmeans the ID is wrong or it is a private conversation you are not in. - Writes refused. The error names the variable that would allow it:
SLACK_ALLOW_WRITES=1, the conversation id inSLACK_WRITE_CHANNELS, or the people inSLACK_WRITE_USERS. Restart the client after changing them. A shared (Slack Connect) conversation always needs its id inSLACK_WRITE_CHANNELS. msg_too_longon an edit.chat.updaterefuses more than 4,000 characters, soslack_update_messagecaps the text there. Shorten it, or post the rest as a reply. WithSLACK_SIGNATUREset, the signature counts too, and the error says how many characters it adds.- Teammates can't tell which messages the agent sent. Slack adds no app label to a
message posted with a user token. Set
SLACK_SIGNATURE(Signing your messages) and restart the client;slack_health_checkthen showssignature: on. - The health check says the token is rotating (
xoxe.xoxp-…). Token rotation was turned on, and Slack does not let you turn it off. The token expires every 12 hours, so create a new app with rotation off. uvx slack-mcpinstalls something else. That name (andslack-mcp-server) belongs to unrelated PyPI packages. This one isuvx amazing-slack-mcp.
Contributing
uv sync --group dev
uv run pytest -m "not live and not live_write"
uv run ruff check src/ tests/ && uv run ruff format --check src/ tests/
uv run mypy src/
uv run python scripts/gen_tool_table.py --check # --write after adding or renaming a tool
Commits follow Conventional Commits (feat(chat): …,
fix(guard): …). Releases and the changelog are cut by release-please. A new Slack method
is a deliberate change to the client's method list (src/slack_mcp/client.py), and a
new write method has to be added to WRITE_METHODS so the guard sees it. Security
issues go through SECURITY.md, never a public issue.
License
Apache-2.0 — see LICENSE.
Metadata
Release files for amazing-slack-mcp 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| amazing_slack_mcp-0.1.0.tar.gz | 253.6 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| amazing_slack_mcp-0.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 351.9 kB
Release files / amazing_slack_mcp-0.1.0.tar.gz
| Download URL | amazing_slack_mcp-0.1.0.tar.gz |
|---|---|
| Size | 253.6 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
10c928dd34c5388cb2fdf5a30b38e6c377a1a40e62710e46038caf39b421cd43
|
|
BLAKE2b-256 checksum How to use checksums |
bca3e99538a93d859f2b7a9d635c98ee226f6d66618df7c7f8c2a694f881372b
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.13.0 {"installer":{"name":"uv","version":"0.13.0","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|
Release files / amazing_slack_mcp-0.1.0-py3-none-any.whl
| Download URL | amazing_slack_mcp-0.1.0-py3-none-any.whl |
|---|---|
| Size | 98.3 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
e0b47322f159de464835db2d647109cd61808a7eabe7a46ab348f191ad162c85
|
|
BLAKE2b-256 checksum How to use checksums |
65549d96ee7209ac9e84f7ce79c0cf23c0ee6b57b7aac38d5d2b67a2a8c9353e
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.13.0 {"installer":{"name":"uv","version":"0.13.0","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|