slack-personal-mcp
An MCP server that acts as you in Slack through your own user token, exposing 30 workspace-scoped tools for reading, sending, searching, and managing Slack content with a built-in style gate.
List configured workspaces and dry-run/lint drafts against the style gate.
Send, edit, delete, schedule, and cancel messages; get permalinks; read channel and thread history; check unread messages and mark conversations read.
Search messages and use Slack Real-time Search across messages, files, channels, and users; find users/channels and fetch user, channel, usergroup, and custom emoji info.
Add, remove, and inspect reactions; set or clear your Slack status.
Upload, download, list, and inspect files; read canvases as Markdown or HTML.
Enforce user-token identity, explicit workspace aliases, optional read-only mode, write-channel restrictions, style blocking for AI typography/phrases/promises, and file-root safety.
Provides tools for interacting with Slack using a personal user token, including sending, editing, deleting, and scheduling messages; managing reactions and status; reading channels, threads, and canvases; searching messages and files; uploading and downloading files; and looking up users, channels, usergroups, and custom emoji across configured workspaces.
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@slack-personal-mcp post in #team-updates that I'll be out sick today"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
slack-personal-mcp
An MCP server that works in Slack with your own user token (xoxp-). Messages, files,
reactions and status changes come from you, without the "via app" badge a bot token adds.
Every tool that touches Slack takes an explicit
workspacealias, so a read or a send never lands in the wrong workspace by default.A style gate checks outgoing text for AI typography (em dash, curly quotes, ellipsis character), stock AI phrases and unapproved promises before anything is sent.
SLACK_READ_ONLY=1hides every tool that changes Slack or local files;SLACK_WRITE_CHANNELSlimits writes to listed conversations.
It runs over stdio on your machine. The official Slack MCP server is a hosted service with OAuth and admin approval; this one is for a person who wants their own token on their own computer.
Tools
30 tools. In the table, R is read-only, W writes to Slack, D deletes or overwrites
something. SLACK_READ_ONLY=1 leaves the 19 R tools.
Tool | What it does | User token scopes | |
| R | Configured aliases and whose token each one is ( | none |
| R | Run the style gate on a draft without sending | none |
| W | Post as you |
|
| W | Edit one of your messages |
|
| D | Delete one of your messages |
|
| W | Schedule a message up to 120 days ahead |
|
| R | Your scheduled messages, soonest first | none |
| D | Cancel a scheduled message |
|
| R | Link to a message | none |
| W | React as you ( |
|
| D | Remove your reaction |
|
| R | Reactions on a message, with who reacted |
|
| W | Set or clear your status |
|
| W | Move your read marker |
|
| R | Messages after your read marker in up to 20 named conversations |
|
| R | One page of channel or DM history |
|
| R | A thread, one page or all of it | same as above |
| R | Message search with Slack query syntax |
|
| R | Real-time Search over messages, files, channels and users |
|
| R | People by name, exact email or |
|
| R | Conversations by name, including DMs with |
|
| R | Name, title, email, timezone, status |
|
| R | Name, topic, purpose, member count, your read marker |
|
| R | User groups and member counts |
|
| R | Custom emoji |
|
| R | Canvas as Markdown or HTML |
|
| W | Upload a local file as you |
|
| R | File metadata and every message that shares it |
|
| R | Files in a channel or by a person, newest first |
|
| D | Save a file locally (default |
|
*:read and *:history stand for the channels:, groups:, im: or mpim: scope that
matches the conversation type. Message authors get names from a directory cache, which needs users:read. In every
writing tool channel_id may be a user id U...: the message, file or reaction goes to
your DM with that person, which needs im:write.
If a scope is missing, the error names it. Add it under User Token Scopes, reinstall
the app and put the new token in .env.
Related MCP server: Slack MCP Server
How the tools behave
Sending. format="markdown" (the default) sends standard Markdown as markdown_text,
up to 12,000 characters; format="mrkdwn" sends Slack's own markup as text, up to
40,000. Link previews are off unless unfurl_links=true. To reply in a thread, pass the
ts of the thread's first message as thread_ts, not the ts of a reply.
reply_broadcast=true also shows the reply in the channel.
Your messages only. slack_edit_message and slack_delete_message check the author
first and refuse messages by anyone else, even when an admin token could change them.
Approval. The MCP client, not this server, asks you before it calls a tool. Tools carry MCP annotations (read-only, destructive, idempotent), so a client can ask only for writes. Keep approval on for writes and uploads and read the text or path before you allow it.
Files. slack_upload_file reads and slack_download_file writes only inside
SLACK_FILE_ROOTS (default: home and the temp folders). Hidden files and folders such as
~/.ssh or .env, and ~/Library and ~/AppData under the default roots, are refused,
symlinks included. Downloads go to a temporary file and replace an existing copy only once
the size matches. Files stored outside Slack return their external_url instead. The file
tools work on macOS, Linux and Windows.
Pages. Reads return one page at a time and say whether more exist.
slack_read_channel returns {messages, next_cursor, has_more}: pass next_cursor back
as cursor with the same other arguments. slack_read_thread adds complete, and with
all_pages=true follows the cursor up to max_pages (default 10).
slack_search_messages returns {messages, total, page, pages}; slack_search and
slack_list_scheduled return next_cursor. oldest and latest take Slack timestamps
such as 1729612345.123456.
Messages. Each message keeps Slack's ts and text and adds time (ISO 8601, UTC),
user_id and user_name, thread fields, reactions and files. Text from legacy
attachments is in attachments_text, text from blocks in blocks_text; a rich_text
block that only repeats text is left out. Empty fields are dropped.
Names. slack_find_user and slack_find_channel search full users.list and
conversations.list directories, cached in memory for an hour per workspace;
refresh=true reloads. An exact email goes through users.lookupByEmail and a U...
id through users.info, without the directory.
Unread. Slack's API has no unread inbox like the app has. slack_unread takes the
conversations you name and counts messages from others after your read marker.
Search. slack_search_messages uses the classic search.messages. slack_search
uses the Real-time Search API,
which works for internal apps; if your workspace does not offer it, the error says so.
Results. Tools return structured content with an output schema. Errors come back as MCP tool errors.
Style gate
src/slack_personal_mcp/style.py checks every message, edit, scheduled message, status,
upload comment and file title before it leaves:
Errors block the send: AI typography (em and en dash, ellipsis character, curly quotes, non-breaking space, bullet character) and stock AI phrases.
Warnings block the send unless
allow_promise=true: concrete promises and deadlines.
Code, URLs, Slack mentions (<@U...>, <#C...>, <!here>) and quoted lines are not
checked; link labels are. Each finding gives a zero-based character range. Phrase lists
cover English and Russian. SLACK_STYLE_GATE=off turns the gate off for sends;
slack_lint works either way.
Setup
Requires Python 3.10+ and uv.
git clone https://github.com/oviron/slack-personal-mcp
cd slack-personal-mcp
uv syncuv run pytest runs the offline tests.
Slack app and token
Each workspace needs its own Slack app.
At https://api.slack.com/apps choose Create New App, then From an app manifest, pick the workspace and paste
slack-app-manifest.yaml.Install to Workspace. Workspaces that need admin approval for apps ask an admin first.
Copy the User OAuth Token (
xoxp-...) from OAuth & Permissions.
The manifest asks for every scope in the table above. Remove the ones you don't need
before installing; the matching tools then fail with missing_scope. channels:write
and groups:write are only for slack_mark_read, but they also allow managing channels,
so leave them out if you don't need it.
Keep token rotation off: the manifest sets token_rotation_enabled: false. With rotation
on, Slack issues xoxe.xoxp- tokens that expire after 12 hours, this server does not
refresh them, and Slack does not let you turn rotation off again. An app with rotation on
has to be replaced by a new one.
The app is internal to your workspace, so Slack's
May 2025 rate limits
for commercially distributed apps (one conversations.history call a minute) do not
apply. Don't distribute one app to other workspaces: create one per workspace.
Configuration
Tokens come from the environment, or from .env next to server.py (ignored by git;
keep it chmod 600). Copy .env.example to start:
SLACK_USER_TOKEN_ACME=xoxp-...
SLACK_USER_TOKEN_SIDE_PROJECT=xoxp-...Each SLACK_USER_TOKEN_<ALIAS> adds a workspace; SIDE_PROJECT becomes the alias
side-project. slack_list_workspaces shows whose token each alias holds:
{"workspaces": [{"workspace": "acme", "configured": true, "team": "Acme", "team_id": "T0123",
"url": "https://acme.slack.com/", "user": "jane", "user_id": "U0456"}]}Variable | Meaning |
| User token for one workspace |
| Read this file instead of |
|
|
|
|
| Folders, separated by |
| Comma-separated ids ( |
Registering with a client
The server speaks MCP over stdio. Point the client at the clone's Python and
server.py; the examples use /path/to/slack-personal-mcp.
Claude Code:
claude mcp add slack-personal -- /path/to/slack-personal-mcp/.venv/bin/python /path/to/slack-personal-mcp/server.pyCodex, in ~/.codex/config.toml:
[mcp_servers.slack-personal]
command = "/path/to/slack-personal-mcp/.venv/bin/python"
args = ["/path/to/slack-personal-mcp/server.py"]Claude Desktop (claude_desktop_config.json) and Cursor (.cursor/mcp.json):
{
"mcpServers": {
"slack-personal": {
"command": "/path/to/slack-personal-mcp/.venv/bin/python",
"args": ["/path/to/slack-personal-mcp/server.py"]
}
}
}Without a clone, uv runs a tagged release straight from GitHub. There is no server.py
next to it, so name the env file:
SLACK_ENV_FILE=~/.config/slack-personal-mcp/.env uvx --from git+https://github.com/oviron/slack-personal-mcp@v0.2.3 slack-personal-mcpTroubleshooting
Error | What to do |
| Add the scope the error names, reinstall the app, replace the token |
| Join the conversation in Slack first |
| Check the id; private conversations and DMs need you to be a member |
| Create a new user token and replace it in |
| Token rotation is on and cannot be turned off: create a new app from the manifest and use its token |
| The server already waited for |
| A send failed after it left your machine and was not retried; check the channel before sending again |
| Real-time Search is not available in this workspace; use |
Changes to the code or .env take effect when the client restarts the server.
Releasing
The package is not published to PyPI or the MCP Registry; releases are git tags.
Bump the version in
pyproject.tomlandsrc/slack_personal_mcp/__init__.py, runuv lock, then merge.Tag
vX.Y.Zonmainand create a GitHub release.
License
MIT
Available Tools
30 toolsslack_add_reactionAIdempotent
Add an emoji reaction to a message AS THE USER (reactions.add, needs reactions:write). channel_id: as in slack_send_message. ts: the message timestamp. name: emoji name without colons, e.g. eyes.
Inputs: ts (required), name (required), workspace (required), channel_id (required). Returns: object
slack_cancel_scheduledCDestructiveIdempotent
Cancel a scheduled message (chat.deleteScheduledMessage, chat:write).
Inputs: workspace (required), channel_id (required), scheduled_message_id (required). Returns: object
slack_channel_infoBRead-only
Name, kind, topic, purpose, member count and your read marker via conversations.info; needs channels:read/groups:read/im:read/mpim:read for the conversation type.
Inputs: workspace (required), channel_id (required). Returns: object
slack_delete_messageADestructive
Delete one of YOUR messages (chat.delete, chat:write). Messages by others are refused even with an admin token. Cannot be undone.
Inputs: ts (required), workspace (required), channel_id (required). Returns: object
slack_download_fileADestructive
Download a Slack file by id to a local folder (needs files:read). Returns the saved path. Saved as <file_id>-, replacing an earlier copy only once the new one is complete. dest_dir: inside SLACK_FILE_ROOTS, like slack_upload_file. External files (Google Drive and the like) are not downloaded: the reply gives their external_url.
Inputs: file_id (required), workspace (required), dest_dir. Returns: object
slack_edit_messageAIdempotent
Replace the text of one of YOUR messages (chat.update, chat:write). Messages by others are refused even with an admin token. Same style gate and format as slack_send_message.
Inputs: ts (required), text (required), workspace (required), channel_id (required), +2 more. Returns: object
slack_file_infoARead-only
File metadata by id (files.info, needs files:read): name, type, size, owner, mode, and every message that shares it (channel, ts, thread_ts). External files carry external_url.
Inputs: file_id (required), workspace (required). Returns: object
slack_find_channelARead-only
Find non-archived conversations by name; types: public_channel,private_channel,im,mpim (comma-separated). Needs channels:read/groups:read/im:read/mpim:read by type; users:read for DM names. Refresh reloads the cache.
Inputs: query (required), workspace (required), limit, types, +1 more. Returns: object
slack_find_userARead-only
Find people by name via cached users.list (users:read); refresh bypasses the one-hour cache. Exact email uses users.lookupByEmail (users:read.email); U.../W... uses users.info (users:read).
Inputs: query (required), workspace (required), limit, refresh. Returns: object
slack_get_permalinkBRead-only
Link to a message (chat.getPermalink): opens it in Slack, inside its thread for replies.
Inputs: ts (required), workspace (required), channel_id (required). Returns: object
slack_get_reactionsARead-only
Get message reaction names, counts and users; users_complete flags partial lists. Needs reactions:read and users:read for cached user names.
Inputs: ts (required), workspace (required), channel_id (required). Returns: object
slack_lintARead-only
Dry-run the style gate on a draft without sending. clean=true means it would pass.
Inputs: text (required). Returns: object
slack_list_emojiARead-only
List custom emoji by case-insensitive name substring, with URLs or alias targets. Needs emoji:read.
Inputs: workspace (required), query. Returns: object
slack_list_filesARead-only
List files, newest first (files.list, needs files:read). Filter by channel_id and/or user_id (uploader). types: comma list of all, spaces, snippets, images, gdocs, zips, pdfs. page: 1-based; the reply says how many pages there are.
Inputs: workspace (required), page, count, types, +2 more. Returns: object
slack_list_scheduledBRead-only
Your messages scheduled but not yet sent (chat.scheduledMessages.list), soonest first within a page. Pass next_cursor as cursor for the next page.
Inputs: workspace (required), limit, cursor, channel_id. Returns: object
slack_list_usergroupsARead-only
List user groups and counts; include_users adds member IDs. Needs usergroups:read.
Inputs: workspace (required), include_users. Returns: object
slack_list_workspacesARead-only
List configured workspace aliases and verify token identities (auth.test; no extra scopes).
Inputs: no named parameters. Returns: object
slack_mark_readAIdempotent
Move your read marker in a channel/DM to ts (conversations.mark; needs channels:write, groups:write, im:write or mpim:write for that kind of conversation).
Inputs: ts (required), workspace (required), channel_id (required). Returns: object
slack_read_canvasARead-only
Read full canvas content as Markdown (default) or HTML via canvases.getContent. Needs canvases:read.
Inputs: canvas_id (required), workspace (required), content_type. Returns: object
slack_read_channelARead-only
Read one history page with cursor and Slack timestamp bounds; returns messages/next_cursor/has_more. Needs the conversation's channels:history, groups:history, im:history or mpim:history; users:read for names.
Inputs: workspace (required), channel_id (required), limit, cursor, +3 more. Returns: object
slack_read_threadARead-only
Read thread pages from cursor within timestamp bounds; all_pages stops at max_pages (default 10). Returns complete=false while more remain. Needs channels:history/groups:history/im:history/mpim:history; users:read.
Inputs: thread_ts (required), workspace (required), channel_id (required), limit, +6 more. Returns: object
slack_remove_reactionADestructiveIdempotent
Remove the user's emoji reaction from a message (reactions.remove, needs reactions:write).
Inputs: ts (required), name (required), workspace (required), channel_id (required). Returns: object
slack_schedule_messageA
Schedule a message AS THE USER (chat.scheduleMessage, chat:write). post_at: ISO 8601 with a timezone (2026-10-23T09:00:00+03:00) or unix seconds, up to 120 days ahead. Other arguments and the style gate as in slack_send_message.
Inputs: text (required), post_at (required), workspace (required), channel_id (required), +4 more. Returns: object
slack_searchBRead-only
Search with RTS and cursor; type lists default to messages in public_channel. Needs search:read.public; optionally search:read.private, search:read.im, search:read.mpim, search:read.files, search:read.users. User tokens need no action_token.
Inputs: query (required), workspace (required), limit, cursor, +2 more. Returns: object
slack_search_messagesBRead-only
Search visible messages with raw Slack query and optional filters; returns total/page/pages. Needs search:read and users:read for cached author names; count is 1-100.
Inputs: query (required), workspace (required), has, page, +7 more. Returns: object
slack_send_messageA
Post a Slack message AS THE USER (no app badge), via personal user token. Style gate (enforced before send unless SLACK_STYLE_GATE=off): no AI typography (em/en dash, ellipsis char, curly quotes, nbsp, bullet) - use plain ASCII; no AI-slop phrases; no concrete promises/deadlines unless allow_promise=true. Write short and plain, in the user's own voice and language. workspace: explicit alias from slack_list_workspaces. channel_id: C.../G.../D..., or a user id U... to write into the DM with that person (needs im:write). thread_ts: reply in a thread. reply_broadcast: with thread_ts, also show the reply in the channel. format: markdown (default; standard Markdown, up to 12,000 chars) or mrkdwn (Slack's own: bold, <url|label>). unfurl_links: show link previews (off by default).
Inputs: text (required), workspace (required), channel_id (required), format, +4 more. Returns: object
slack_set_statusAIdempotent
Set the user's Slack status (users.profile.set, needs users.profile:write). Empty text and emoji clear it. emoji: name like palm_tree. expires_in_minutes: 0 keeps it until changed. The style gate applies to text: everyone in the workspace sees it.
Inputs: workspace (required), text, emoji, allow_promise, +1 more. Returns: object
slack_unreadARead-only
How many messages arrived after your read marker in each of up to 20 channels/DMs (conversations.info + conversations.history). Slack's API has no full unread inbox like the app, so name the channels. count stops at limit; has_more tells there are more.
Inputs: workspace (required), channel_ids (required), limit. Returns: object
slack_upload_fileA
Upload a local file to a Slack channel/DM AS THE USER, with an optional comment. Same style gate as slack_send_message applies to initial_comment and title. channel_id: C.../G.../D..., or a user id U... to upload into the DM with that person (your own id = your self-DM; needs im:write). thread_ts: upload as a thread reply. file_path: absolute path inside SLACK_FILE_ROOTS (default: home and temp folders; hidden files and ~/Library refused). title: file title, defaults to the filename.
Inputs: file_path (required), workspace (required), channel_id (required), title, +3 more. Returns: object
slack_user_infoBRead-only
A user's name, title, email, timezone, status and role via users.info. Needs users:read, and users:read.email for the email.
Inputs: user_id (required), workspace (required). Returns: object
Tool Schema Changelog
Recent tool additions, removals, and schema changes observed during successful MCP inspections.
Loading tool schema changelog…
TDQS
Scored across 30 tools
Most tools target a distinct resource+action, but slack_search and slack_search_messages are genuinely confusable: both search messages, one via RTS with cursors and one via raw query with filters, and an agent can easily pick the wrong one. A few others (slack_unread vs slack_read_channel, slack_lint vs the style gate inside slack_send_message) require reading descriptions carefully to separate.
The dominant pattern is slack_verb_noun (send_message, edit_message, list_files, add_reaction), which is predictable once learned. Minor deviations exist: bare-verb slack_search, and noun-only names like slack_user_info, slack_channel_info, slack_file_info, and slack_unread break the verb-first convention, while verbs vary between get/read/find for similar lookups.
30 tools is on the heavy side for a personal Slack client, and the surface spans many sub-domains (messages, files, channels, users, reactions, scheduling, status, canvases, emoji, usergroups). Each tool covers real functionality, but the count is borderline and a few could be folded together.
Message lifecycle is well covered (send, edit, delete, schedule, cancel, reactions, thread reading, permalink), along with files, users, and info lookups. Gaps remain: there is no tool to list the channels/conversations you belong to (slack_find_channel requires a name, and slack_unread requires naming channels), and no pin/bookmark or channel create/join operations.
Maintenance
Related MCP Connectors
Hosted Slack MCP server for Claude, ChatGPT and Cursor, with each action switched per account.
A personal RAG database you build from chat, so AI creates work that sounds like you.
Chat with your AI agents from Slack: each gets a thread in your DM, and your replies there wake it.
Mac & Windows: let ChatGPT, Claude & Cursor use your email, calendar, iMessage, Teams, files.
Related MCP Servers
- AlicenseAqualityDmaintenanceEnables interaction with Slack workspaces as a user, supporting channel listing, message posting, threading, reactions, and user management via the Slack API.811MIT
- AlicenseAqualityBmaintenanceGives AI agents full Slack access via session tokens, requiring no app registration, admin approval, or OAuth.20713 npm33MIT
- FlicenseAqualityDmaintenanceEnables AI agents to interact with Slack using a user token, providing full access to message search, channel history, DMs, and posting as the user.8-
- AlicenseNot gradedqualityDmaintenanceEnables AI assistants like Cursor and Claude Desktop to interact with Slack workspaces, allowing message sending, channel reading, user management, and file operations through natural language.MIT