Skip to main content
Glama

slack-cdp

Script Slack as yourself, through the desktop app's own session. No Slack app, no bot token, no OAuth: slack-cdp attaches to the Slack desktop app over the Chrome DevTools Protocol, makes Web API calls inside the app's page with the page's session, and listens to the events the page receives on its own websocket.

Comes as a library (with TypeScript types), a CLI, and an MCP server for AI agents. Node 22+, no dependencies.

WARNING

While Slack's DevTools port is open, any process on your machine can take over your Slack session. The port binds to 127.0.0.1, which only keeps out other machines. Open it only while you need it. With launch: true / --launch, slack-cdp restarts Slack without the port when it's done.

Unofficial: it relies on Slack internals that can change without notice (run slack-cdp doctor when something breaks), and everything it does, it does as you. Check your workspace's rules before automating your account.

Install

npm install github:kinduff/slack-cdp

Related MCP server: slack-user-mcp-server

Start Slack with the port

connect({ launch: true }) and --launch quit Slack and start it again with the port. Or start it yourself:

Install

Command

Linux flatpak

flatpak run com.slack.Slack --remote-debugging-port=9222 --remote-debugging-address=127.0.0.1

Linux deb/rpm/snap

slack --remote-debugging-port=9222 --remote-debugging-address=127.0.0.1

macOS

open -a Slack --args --remote-debugging-port=9222 --remote-debugging-address=127.0.0.1

Windows

%LOCALAPPDATA%\slack\slack.exe --remote-debugging-port=9222 --remote-debugging-address=127.0.0.1

Quit Slack fully first: if it's already running, the new launch just focuses it. Only the flatpak launcher has been tested; the Microsoft Store build of Slack isn't supported.

Then check the chain:

npx slack-cdp doctor
ok   app: found Slack flatpak
ok   port: 127.0.0.1:9222 answers (Chrome/138.0.7204.251)
ok   page: https://app.slack.com/client/T029JKK3Y/D08CLGLHL0J
ok   session: 2 workspaces: Acme, Side Project
ok   api params: every workspace has API params to copy
ok   auth Acme: signed in as kinduff (U08CHMKQ9NZ)

Library

import { connect } from "slack-cdp";

const slack = await connect({ launch: true });

for await (const m of slack.messages()) {
  if (m.self || m.bot) continue;
  if (m.text.startsWith("!ping")) await slack.reply(m, "pong");
}
await slack.postMessage("#general", "deploy done :rocket:", { team: "acme" });
await slack.postMessage("@max", "lunch?");
await slack.upload("#releases", { path: "./changelog.md", comment: "v2 notes" });
const hits = await slack.search("from:@max in:#general deploy");
const thread = await slack.history("#general", { threadTs: "1792699294.295379" });
const body = await slack.api("emoji.list"); // any Web API method

More in docs/recipes.md and examples/.

Workspaces

Every call takes { team }: a workspace ID or a name prefix ("acme"). Without it, calls go to the workspace active in the app, which changes when you click another workspace. Events arrive from every workspace, each with its team, so answer with slack.reply(m, …) or pass { team: m.team }.

Channels

Methods that take a channel accept an ID (C…, G…, D…), #name or name for a conversation you're in, or @person or a user ID for their DM, opened if needed.

API

Call

Returns

connect({ host, port, launch, reconnect, reconnectDelay })

A Slack. Defaults: 127.0.0.1, 9222, no launch, reconnect every 5 s

api(method, params, { team })

Any Web API method's response body. Objects in params go as JSON. Throws SlackError (code, retryAfter, body); waits out rate limits up to 3 times

paginate(method, params, { team, key })

Async iterator over every item of a cursor-paginated method

postMessage(channel, text, { threadTs, team, ...params })

The ts. Extra params (blocks, unfurl_links, …) pass through

reply(message, text, params)

The ts. In the message's thread and workspace

updateMessage(channel, ts, text, params) / deleteMessage(channel, ts)

Edits or deletes your message

react(channel, ts, name)

Adds a reaction

upload(channel, { data | path, filename, title, comment, threadTs })

{ id, name, permalink }

openDM(user | users)

DM or group DM channel ID

history(channel, { limit, oldest, latest, threadTs })

Messages, newest first; a thread's replies with threadTs

search(query, { count, sort })

{ channel, channelName, ts, user, username, text, permalink }[]

conversations({ types })

Every conversation you're in

conversationInfo(channel)

{ id, name, user, im, mpim, private }

channelByName(name) / userByName(name) / userByEmail(email)

Lookup or null; name lists cached 5 min

userName(id) / place(channel)

Display name / #general, @Max; cached

resolveChannel(ref)

A channel ID from any channel reference

workspaces() / selfIds() / isSelf(id)

Signed-in workspaces / your user IDs / whether an ID is you

getStatus() / setStatus({ text, emoji, expiresAt }) / clearStatus()

Your status

messages() / stream(kind)

Async iterators of new events until close()

evaluate(expression)

Runs JS in the Slack page

close({ restore })

Detaches; restore (default: whether connect() launched Slack) restarts Slack without the port

Events (slack.on(…)):

Event

Payload

message

{ team, channel, user, bot, self, text, ts, threadTs, files }

edit

{ team, channel, user, self, text, previousText, ts, threadTs }

delete

{ team, channel, user, self, text, ts, threadTs }

reaction

{ team, channel, ts, user, self, name, added, itemUser }

event

Every raw websocket event: typing, presence, channel joins, …

connect / disconnect / close

Connection state; reconnects follow a disconnect

You get events for the conversations you're in, as the app does. text is Slack markup; plainText(text, idToName) renders mentions, channel links and URLs readably, and mentions(text) lists the user IDs to look up first.

Also exported: doctor(), detectApp(), restartSlack(), serveMcp(), and the pure event parsers. Types are in index.d.ts.

CLI

slack-cdp doctor
slack-cdp tail --org acme --channel general --mentions
slack-cdp send '#general' 'hello'
echo 'from stdin' | slack-cdp send @max
slack-cdp history '#general' -n 50
slack-cdp search 'from:@max deploy'
slack-cdp api emoji.list --org acme
slack-cdp tail --json | jq .text

slack-cdp --help lists everything. Add --launch to open the port if it isn't open.

MCP server

slack-cdp mcp serves Slack to AI agents over stdio, with tools for workspaces, conversations, history, search, send, react, and an inbox of new messages. For Claude Code, in .mcp.json:

{ "mcpServers": { "slack": { "command": "npx", "args": ["slack-cdp", "mcp", "--launch"] } } }

The agent posts as you. Give it to agents you'd hand your Slack account to.

How it works

  • CDP Runtime.evaluate on the app.slack.com page target. The page's localStorage.localConfig_v2 holds each signed-in workspace's token and URL.

  • The API accepts the page's credentialed requests only with the app's own _x_* query params, so they're copied from the page's most recent /api/ call. Right after Slack starts there is none yet; calls wait up to 30 s for one (no_template_call).

  • The token never crosses CDP: it's read and sent inside the page. Responses do come back.

  • CDP Network.enable plus Network.webSocketFrameReceived taps the page's websocket, whose frames are Slack's RTM-style JSON events.

Development

npm test

Tests run against a fake DevTools port (test/fake-slack.js) that evaluates the real page expressions in a sandbox, so they need no Slack. See AGENTS.md before changing things.

License

MIT

Related MCP Connectors

Related MCP Servers