Building a Slack Integration MCP Tool
Let AI agents interact with Slack — send messages, create channels, search conversations. Full walkthrough.
What You'll Build
send_message — send a message to a channel or user
list_channels — list all channels the bot can see
search_messages — search conversation history
create_channel — create a new channel
Step 1: Setup Slack App
- Go to https://api.slack.com/apps
- Create a new app → "From scratch"
- Name it (e.g., "MCP Slack Bot")
- Under OAuth & Permissions, add scopes:
chat:write — send messages
channels:read — list channels
channels:history — search messages
channels:manage — create channels
search:read — search messages
- Install to workspace → copy the Bot User OAuth Token (starts with
xoxb-)
Step 2: Initialize
mkdir slack-mcp && cd slack-mcp
npm init -y
npm install @modelcontextprotocol/sdk @slack/web-api
Step 3: Build the Server
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { WebClient } from "@slack/web-api";
const TOKEN = process.env.SLACK_BOT_TOKEN;
if (!TOKEN) {
console.error("SLACK_BOT_TOKEN is required");
process.exit(1);
}
const slack = new WebClient(TOKEN);
const server = new Server({
name: "slack-mcp",
version: "1.0.0",
}, { capabilities: { tools: {} } });
server.setRequestHandler("tools/list", async () => ({
tools: [
{
name: "send_message",
description: "Send a message to a Slack channel or user",
inputSchema: {
type: "object",
properties: {
channel: {
type: "string",
description: "Channel name (e.g., '#general') or channel ID"
},
text: {
type: "string",
description: "Message text (supports Slack mrkdwn)"
}
},
required: ["channel", "text"]
}
},
{
name: "list_channels",
description: "List all public channels in the workspace",
inputSchema: {
type: "object",
properties: {
include_archived: {
type: "boolean",
description: "Include archived channels (default: false)"
}
}
}
},
{
name: "search_messages",
description: "Search for messages matching a query",
inputSchema: {
type: "object",
properties: {
query: {
type: "string",
description: "Search query"
},
channel: {
type: "string",
description: "Limit search to a specific channel"
},
count: {
type: "number",
description: "Number of results (default: 10, max: 50)"
}
},
required: ["query"]
}
},
{
name: "create_channel",
description: "Create a new public channel",
inputSchema: {
type: "object",
properties: {
name: {
type: "string",
description: "Channel name (lowercase, no spaces, max 80 chars)"
},
is_private: {
type: "boolean",
description: "Create a private channel (default: false)"
}
},
required: ["name"]
}
}
]
}));
server.setRequestHandler("tools/call", async (request) => {
const { name, arguments: args } = request.params;
try {
switch (name) {
case "send_message": {
const result = await slack.chat.postMessage({
channel: args.channel,
text: args.text
});
return {
content: [{
type: "text",
text: `✅ Message sent to ${args.channel} (${result.ts})`
}]
};
}
case "list_channels": {
const result = await slack.conversations.list({
exclude_archived: !args.include_archived,
types: "public_channel"
});
const channels = result.channels
.map(c => ` #${c.name} ${c.is_archived ? "(archived)" : ""}`)
.join("\n");
return {
content: [{
type: "text",
text: `📋 Channels (${result.channels.length}):
${channels}`
}]
};
}
case "search_messages": {
const result = await slack.search.messages({
query: args.query,
count: Math.min(args.count || 10, 50)
});
const messages = result.messages.matches
.slice(0, args.count || 10)
.map((m, i) =>
`${i + 1}. [${m.channel.name}] ${m.username}: ${m.text.slice(0, 100)}`
).join("\n");
return {
content: [{
type: "text",
text: `🔍 Results for "${args.query}":
${messages || "No messages found."}`
}]
};
}
case "create_channel": {
const result = await slack.conversations.create({
name: args.name.toLowerCase().replace(/[^a-z0-9-_]/g, "").slice(0, 80),
is_private: args.is_private || false
});
return {
content: [{
type: "text",
text: `✅ Channel #${result.channel.name} created!`
}]
};
}
default:
return {
content: [{ type: "text", text: `❌ Unknown tool: ${name}` }],
isError: true
};
}
} catch (err) {
return {
content: [{
type: "text",
text: `❌ Slack error: ${err.message}`
}],
isError: true
};
}
});
const transport = new StdioServerTransport();
await server.connect(transport);
Step 4: Test and Publish
SLACK_BOT_TOKEN=xoxb-your-token node index.js
mcpm-dev publish
Security Notes
- Bot tokens have limited scope — configure only what you need
- Never expose tokens — use environment variables
- Be careful with
create_channel — can create channel spam
- Add approval for sensitive actions if needed
Your AI agent now has full Slack superpowers. 🎉
#MCP #Tutorial #Slack #Integration #mcpm
Building a Slack Integration MCP Tool
Let AI agents interact with Slack — send messages, create channels, search conversations. Full walkthrough.
What You'll Build
send_message— send a message to a channel or userlist_channels— list all channels the bot can seesearch_messages— search conversation historycreate_channel— create a new channelStep 1: Setup Slack App
chat:write— send messageschannels:read— list channelschannels:history— search messageschannels:manage— create channelssearch:read— search messagesxoxb-)Step 2: Initialize
mkdir slack-mcp && cd slack-mcp npm init -y npm install @modelcontextprotocol/sdk @slack/web-apiStep 3: Build the Server
import { Server } from "@modelcontextprotocol/sdk/server/index.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { WebClient } from "@slack/web-api"; const TOKEN = process.env.SLACK_BOT_TOKEN; if (!TOKEN) { console.error("SLACK_BOT_TOKEN is required"); process.exit(1); } const slack = new WebClient(TOKEN); const server = new Server({ name: "slack-mcp", version: "1.0.0", }, { capabilities: { tools: {} } }); server.setRequestHandler("tools/list", async () => ({ tools: [ { name: "send_message", description: "Send a message to a Slack channel or user", inputSchema: { type: "object", properties: { channel: { type: "string", description: "Channel name (e.g., '#general') or channel ID" }, text: { type: "string", description: "Message text (supports Slack mrkdwn)" } }, required: ["channel", "text"] } }, { name: "list_channels", description: "List all public channels in the workspace", inputSchema: { type: "object", properties: { include_archived: { type: "boolean", description: "Include archived channels (default: false)" } } } }, { name: "search_messages", description: "Search for messages matching a query", inputSchema: { type: "object", properties: { query: { type: "string", description: "Search query" }, channel: { type: "string", description: "Limit search to a specific channel" }, count: { type: "number", description: "Number of results (default: 10, max: 50)" } }, required: ["query"] } }, { name: "create_channel", description: "Create a new public channel", inputSchema: { type: "object", properties: { name: { type: "string", description: "Channel name (lowercase, no spaces, max 80 chars)" }, is_private: { type: "boolean", description: "Create a private channel (default: false)" } }, required: ["name"] } } ] })); server.setRequestHandler("tools/call", async (request) => { const { name, arguments: args } = request.params; try { switch (name) { case "send_message": { const result = await slack.chat.postMessage({ channel: args.channel, text: args.text }); return { content: [{ type: "text", text: `✅ Message sent to ${args.channel} (${result.ts})` }] }; } case "list_channels": { const result = await slack.conversations.list({ exclude_archived: !args.include_archived, types: "public_channel" }); const channels = result.channels .map(c => ` #${c.name} ${c.is_archived ? "(archived)" : ""}`) .join("\n"); return { content: [{ type: "text", text: `📋 Channels (${result.channels.length}): ${channels}` }] }; } case "search_messages": { const result = await slack.search.messages({ query: args.query, count: Math.min(args.count || 10, 50) }); const messages = result.messages.matches .slice(0, args.count || 10) .map((m, i) => `${i + 1}. [${m.channel.name}] ${m.username}: ${m.text.slice(0, 100)}` ).join("\n"); return { content: [{ type: "text", text: `🔍 Results for "${args.query}": ${messages || "No messages found."}` }] }; } case "create_channel": { const result = await slack.conversations.create({ name: args.name.toLowerCase().replace(/[^a-z0-9-_]/g, "").slice(0, 80), is_private: args.is_private || false }); return { content: [{ type: "text", text: `✅ Channel #${result.channel.name} created!` }] }; } default: return { content: [{ type: "text", text: `❌ Unknown tool: ${name}` }], isError: true }; } } catch (err) { return { content: [{ type: "text", text: `❌ Slack error: ${err.message}` }], isError: true }; } }); const transport = new StdioServerTransport(); await server.connect(transport);Step 4: Test and Publish
Security Notes
create_channel— can create channel spamYour AI agent now has full Slack superpowers. 🎉
#MCP #Tutorial #Slack #Integration #mcpm