How to Connect Cursor to Slack (with OAuth)
๐ค AI Agent? You can point your AI agent at this article and it can follow the steps to connect automatically. Everything it needs is here: the API endpoints, request/response formats, scopes, and working code samples. The TapAuth API is two calls โ
POST https://tapauth.ai/api/v1/grantsto create a grant, thenGET https://tapauth.ai/api/v1/token/{id}(with Bearer auth) to retrieve the access token. No API key needed โ your agent can follow the code examples below end-to-end.
Cursor is the AI-native code editor that's reshaping how developers write software. It can autocomplete functions, refactor entire files, and reason through complex codebases. But when you want Cursor to pull context from a Slack thread โ say, the discussion where your team debated the API design โ it needs a Slack OAuth token. And Slack's OAuth implementation is notorious for being non-standard in ways that break every assumption your code makes about how OAuth should work.
This is the kind of plumbing problem TapAuth eliminates. TapAuth is the token layer for AI agents โ your agent makes one API call and gets a valid, scoped, user-approved Slack token. No redirect servers. No parsing Slack's nested token responses. No managing workspace-scoped credentials. Just a working token, ready to use.
This guide covers the full manual Slack OAuth flow so you understand what's involved, then shows how TapAuth reduces it to two API calls.
Why connecting Cursor to Slack is harder than it looks
Cursor operates as a local application with AI capabilities layered on top. Getting it to authenticate with Slack introduces several compounding problems:
- Slack's non-standard token response. Most OAuth providers return
access_tokenat the top level of their JSON response. Slack nests user tokens insideauthed_user.access_tokenand puts bot tokens at the root level. If your parsing code assumes the standard format, you'll extract the wrong token โ or no token at all. - Workspace-scoped tokens. Unlike GitHub or Google where a single token works across an account, Slack tokens are tied to a specific workspace. If a developer is in multiple Slack workspaces, each one requires a separate OAuth grant. Cursor needs to track which workspace context it's operating in for every request.
- Dual scope namespaces. Slack separates
scope(bot permissions) fromuser_scope(user permissions) in the authorization URL. They use the same scope names but grant different access levels. Passing scopes in the wrong parameter produces silent failures โ no error, just missing permissions. - No token expiration for user tokens. Slack user tokens never expire, which means a compromised token works until someone manually revokes it. There's no built-in rotation, no refresh flow. For an AI agent that's constantly accessing workspace data, this is a security liability that requires external management.
- Localhost redirect complexity. Cursor runs locally, so your OAuth callback needs to hit a local server. Setting up an Express server just to catch one redirect, exchange the code, and parse Slack's weird response format is a lot of infrastructure for what should be a simple auth step.
Each of these is manageable in isolation. Together, they turn "let Cursor read my Slack threads" into a multi-hour project that has nothing to do with the actual feature you're building.
The manual Slack OAuth flow for Cursor
Here's what a manual implementation looks like. This handles the happy path โ production use needs workspace tracking, token storage, and Slack-specific error handling on top of this.
import express from 'express';
import crypto from 'crypto';
const app = express();
const SLACK_CLIENT_ID = process.env.SLACK_CLIENT_ID!;
const SLACK_CLIENT_SECRET = process.env.SLACK_CLIENT_SECRET!;
const REDIRECT_URI = 'http://localhost:3456/slack/callback';
// Step 1: Generate the authorization URL
// Cursor or an extension opens this in the user's browser
app.get('/slack/auth', (req, res) => {
const state = crypto.randomBytes(16).toString('hex');
// Store state for CSRF validation...
const params = new URLSearchParams({
client_id: SLACK_CLIENT_ID,
redirect_uri: REDIRECT_URI,
scope: 'chat:write,channels:read', // Bot scopes
user_scope: 'channels:history,search:read', // User scopes โ separate param!
state,
});
res.redirect(`https://slack.com/oauth/v2/authorize?${params}`);
});
// Step 2: Handle the OAuth callback
app.get('/slack/callback', async (req, res) => {
const { code, state } = req.query;
if (!code) {
res.status(400).send('Authorization denied or failed');
return;
}
// Validate state parameter against stored value...
// Step 3: Exchange authorization code for tokens
const tokenResponse = await fetch('https://slack.com/api/oauth.v2.access', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
client_id: SLACK_CLIENT_ID,
client_secret: SLACK_CLIENT_SECRET,
code: code as string,
redirect_uri: REDIRECT_URI,
}),
});
const data = await tokenResponse.json();
// Slack returns 200 even on errors โ check the ok field
if (!data.ok) {
console.error('Slack OAuth failed:', data.error);
res.status(500).send(`Authentication failed: ${data.error}`);
return;
}
// Slack's non-standard response structure:
// Bot token: data.access_token
// User token: data.authed_user.access_token (nested!)
// Team/workspace: data.team.id
const botToken = data.access_token;
const userToken = data.authed_user?.access_token;
const workspaceId = data.team?.id;
const workspaceName = data.team?.name;
console.log(`Connected to workspace: ${workspaceName} (${workspaceId})`);
// Store tokens keyed by workspace โ you'll need this for multi-workspace
await storeWorkspaceTokens(workspaceId, {
botToken,
userToken,
workspaceName,
});
res.send('Cursor is now connected to Slack!');
});
// Step 4: Use the token to fetch Slack data for Cursor
async function getChannelHistory(channelId: string, workspaceId: string) {
const tokens = await getWorkspaceTokens(workspaceId);
const response = await fetch(
`https://slack.com/api/conversations.history?channel=${channelId}&limit=50`,
{
headers: { Authorization: `Bearer ${tokens.userToken}` },
}
);
const result = await response.json();
// Again: 200 OK doesn't mean success with Slack
if (!result.ok) {
throw new Error(`Slack API error: ${result.error}`);
}
return result.messages;
}
app.listen(3456);
That's a lot of code for "read my Slack messages." And this doesn't even cover token storage, workspace selection UI, error retry logic, or the fact that you need a Slack app configured with the right redirect URL, scopes, and OAuth settings in Slack's developer portal. The yak-shaving is real.
The TapAuth approach: connect Cursor to Slack in two API calls
TapAuth is an access gateway purpose-built for AI agents. It handles every provider's OAuth quirks โ Slack's nested responses, Google's one-time refresh tokens, GitHub's device flow โ and gives your agent the same two-call interface for all of them.
// Step 1: Create a grant โ tell TapAuth what access you need
const grantResponse = await fetch('https://tapauth.ai/api/v1/grants', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.TAPAUTH_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
provider: 'slack',
scopes: ['channels:read', 'channels:history', 'search:read', 'chat:write'],
}),
});
const { grant_id, grant_secret, approve_url } = await grantResponse.json();
// Step 2: User approves access (show this URL once)
console.log(`Approve Slack access: ${approve_url}`);
// Step 3: Retrieve the token after approval
const tokenResponse = await fetch(
`https://tapauth.ai/api/v1/token/${grant_id}`,
{
headers: { 'Authorization': `Bearer ${grant_secret}` },
}
);
const { access_token } = await tokenResponse.json();
// Use the token with Slack's API โ TapAuth normalizes the response
const history = await fetch(
'https://slack.com/api/conversations.history?channel=C0123456789&limit=20',
{
headers: { Authorization: `Bearer ${access_token}` },
}
);
const messages = await history.json();
No Express server. No Slack app configuration. No parsing nested token responses or managing workspace-scoped credentials. TapAuth handles the OAuth dance, normalizes Slack's non-standard behavior, and gives you back a standard access token. The same two API calls work for connecting Cursor to Gmail, GitHub, Notion, or any other provider TapAuth supports.
What you can build with Cursor + Slack
Once Cursor has authenticated Slack access, the integration possibilities are immediately practical:
- Context-aware coding: "Read the #api-design thread from yesterday and generate the TypeScript interfaces they agreed on." Cursor can pull the actual discussion and turn decisions into code.
- PR context from threads: "Find the Slack discussion about the auth refactor and add it as context for this PR review." Link design decisions to the code that implements them.
- Bug report triage: "Search Slack for reports about the 500 errors on /api/users and summarize what users are experiencing." Give Cursor the context it needs to fix the right problem.
- Deployment notifications: "Post to #deployments when the build passes with a summary of what changed." Close the loop between code and communication.
These workflows are straightforward once authentication is handled. The API calls are simple โ it's always the OAuth setup that blocks progress.
Scopes and permissions: what to request
Slack's scope model is more granular than most providers. For Cursor integrations, start minimal and expand as needed:
| Scope | What it allows | Best for |
|---|---|---|
channels:read | List public channels | Channel discovery, workspace navigation |
channels:history | Read messages in public channels | Thread context, discussion summaries |
search:read | Search messages and files | Finding relevant discussions across channels |
chat:write | Post messages | Deployment notifications, status updates |
users:read | View user profiles | Identifying who said what in discussions |
For a read-only Cursor integration that pulls context from Slack, channels:read, channels:history, and search:read are sufficient. Add chat:write only when Cursor needs to post back to channels. Principle of least privilege matters here โ especially since Slack user tokens don't expire.
TapAuth lets you specify different scopes for different grants, so you can give one Cursor workflow read-only access and another workflow write permissions. Different tasks, different trust levels, same simple API.
Skip the Slack OAuth maze
Slack's OAuth implementation is uniquely frustrating โ non-standard responses, workspace-scoped tokens, dual scope namespaces, and error formats that break every assumption your HTTP client makes. Building this from scratch to let Cursor read a few Slack threads is a poor use of engineering time.
TapAuth handles all of it in two API calls. Connect Cursor to Slack the same way you'd connect it to any other service โ create a grant, get approval, retrieve the token. No Slack-specific code. No redirect servers. No workspace management.
Get started with TapAuth โ connect Cursor to Slack in under five minutes and get back to building what actually matters.