Build a Slack Notification MCP Server

Create an MCP server that sends Slack messages and manages channels through MCP tools. Learn webhook integration, message formatting, and notification patterns.


title: "Build a Slack Notification MCP Server" description: "Create an MCP server that sends Slack messages and manages channels through MCP tools. Learn webhook integration, message formatting, and notification patterns." order: 7 keywords:

  • slack mcp server
  • slack notification mcp
  • slack webhook mcp tool
  • mcp slack integration
  • send slack message ai date: "2026-04-01" level: "intermediate" duration: "25 min"

Quick Summary

Build an MCP server that enables AI assistants to send Slack messages, post to channels, and manage notifications. Uses Slack Incoming Webhooks for simple message delivery and the Slack Web API for richer interactions.

What You Will Build

A Slack notification server with:

  • send_message -- Send a message to a Slack channel via webhook
  • send_rich_message -- Send formatted messages with blocks
  • list_channels -- List available Slack channels (via Web API)
Slack Incoming Webhook

A unique URL that allows you to post messages to a specific Slack channel. Webhooks are the simplest way to send messages -- no OAuth flow required. Each webhook is tied to one channel.

Prerequisites

Project Setup

1

Create the project

npx mcp-framework create slack-mcp-server
cd slack-mcp-server
2

Create the Slack client helper

Create src/utils/slack.ts:

export interface SlackBlock {
  type: string;
  text?: { type: string; text: string };
  elements?: Array<{ type: string; text: string }>;
  fields?: Array<{ type: string; text: string }>;
}

export class SlackClient {
  private webhookUrl: string;
  private botToken?: string;

  constructor(webhookUrl: string, botToken?: string) {
    this.webhookUrl = webhookUrl;
    this.botToken = botToken;
  }

  async sendWebhookMessage(text: string, blocks?: SlackBlock[]): Promise<void> {
    const payload: Record<string, unknown> = { text };
    if (blocks) payload.blocks = blocks;

    const response = await fetch(this.webhookUrl, {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify(payload),
    });

    if (!response.ok) {
      const body = await response.text();
      throw new Error(`Slack webhook error: ${response.status} - ${body}`);
    }
  }

  async listChannels(): Promise<Array<{ id: string; name: string; topic: string }>> {
    if (!this.botToken) {
      throw new Error("Bot token required for listing channels");
    }

    const response = await fetch("https://slack.com/api/conversations.list", {
      headers: { Authorization: `Bearer ${this.botToken}` },
    });

    const data = await response.json() as {
      ok: boolean;
      channels: Array<{ id: string; name: string; topic: { value: string } }>;
      error?: string;
    };

    if (!data.ok) {
      throw new Error(`Slack API error: ${data.error}`);
    }

    return data.channels.map((ch) => ({
      id: ch.id,
      name: ch.name,
      topic: ch.topic.value,
    }));
  }
}

export function getSlackClient(): SlackClient {
  const webhookUrl = process.env.SLACK_WEBHOOK_URL;
  if (!webhookUrl) {
    throw new Error("SLACK_WEBHOOK_URL environment variable is required");
  }
  return new SlackClient(webhookUrl, process.env.SLACK_BOT_TOKEN);
}

Building the Tools

SendMessageTool

Create src/tools/SendMessageTool.ts:

import { MCPTool } from "mcp-framework";
import { z } from "zod";
import { getSlackClient } from "../utils/slack.js";

class SendMessageTool extends MCPTool<typeof inputSchema> {
  name = "send_message";
  description = "Send a plain text message to Slack via the configured webhook channel";

  schema = {
    text: {
      type: z.string().min(1).max(4000),
      description: "The message text to send",
    },
  };

  async execute(input: z.infer<typeof inputSchema>): Promise<string> {
    try {
      const client = getSlackClient();
      await client.sendWebhookMessage(input.text);
      return JSON.stringify({
        success: true,
        message: "Message sent to Slack successfully",
      });
    } catch (error) {
      const message = error instanceof Error ? error.message : "Unknown error";
      return JSON.stringify({ error: message });
    }
  }
}

const inputSchema = z.object({
  text: z.string().min(1).max(4000),
});

export default SendMessageTool;

SendRichMessageTool

Create src/tools/SendRichMessageTool.ts:

import { MCPTool } from "mcp-framework";
import { z } from "zod";
import { getSlackClient, SlackBlock } from "../utils/slack.js";

class SendRichMessageTool extends MCPTool<typeof inputSchema> {
  name = "send_rich_message";
  description = "Send a formatted message to Slack with a header, body text, and optional fields";

  schema = {
    header: {
      type: z.string().min(1),
      description: "Bold header text for the message",
    },
    body: {
      type: z.string().min(1),
      description: "Main body text (supports Slack markdown)",
    },
    fields: {
      type: z.array(z.object({
        label: z.string(),
        value: z.string(),
      })).optional(),
      description: "Key-value fields to display in columns",
    },
    color: {
      type: z.enum(["good", "warning", "danger"]).optional(),
      description: "Sidebar color indicator",
    },
  };

  async execute(input: z.infer<typeof inputSchema>): Promise<string> {
    try {
      const blocks: SlackBlock[] = [
        {
          type: "header",
          text: { type: "plain_text", text: input.header },
        },
        {
          type: "section",
          text: { type: "mrkdwn", text: input.body },
        },
      ];

      if (input.fields && input.fields.length > 0) {
        blocks.push({
          type: "section",
          fields: input.fields.map((f) => ({
            type: "mrkdwn",
            text: `*${f.label}:*\n${f.value}`,
          })),
        });
      }

      blocks.push({ type: "divider" } as SlackBlock);

      const client = getSlackClient();
      await client.sendWebhookMessage(input.header, blocks);

      return JSON.stringify({
        success: true,
        message: "Rich message sent to Slack",
        blockCount: blocks.length,
      });
    } catch (error) {
      const message = error instanceof Error ? error.message : "Unknown error";
      return JSON.stringify({ error: message });
    }
  }
}

const inputSchema = z.object({
  header: z.string().min(1),
  body: z.string().min(1),
  fields: z.array(z.object({
    label: z.string(),
    value: z.string(),
  })).optional(),
  color: z.enum(["good", "warning", "danger"]).optional(),
});

export default SendRichMessageTool;

ListChannelsTool

Create src/tools/ListChannelsTool.ts:

import { MCPTool } from "mcp-framework";
import { z } from "zod";
import { getSlackClient } from "../utils/slack.js";

class ListChannelsTool extends MCPTool<typeof inputSchema> {
  name = "list_channels";
  description = "List available Slack channels (requires SLACK_BOT_TOKEN)";

  schema = {};

  async execute(): Promise<string> {
    try {
      const client = getSlackClient();
      const channels = await client.listChannels();
      return JSON.stringify({
        count: channels.length,
        channels,
      }, null, 2);
    } catch (error) {
      const message = error instanceof Error ? error.message : "Unknown error";
      return JSON.stringify({ error: message });
    }
  }
}

const inputSchema = z.object({});

export default ListChannelsTool;
Message Length Limits

Slack has a 4,000 character limit for webhook messages and 40,000 for Web API messages. Always validate and truncate message content before sending. For large outputs, consider splitting into multiple messages.

Official SDK Version

import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";

const server = new McpServer({ name: "slack-server", version: "1.0.0" });

server.tool(
  "send_message",
  "Send a message to Slack",
  { text: z.string().min(1).max(4000) },
  async ({ text }) => {
    const response = await fetch(process.env.SLACK_WEBHOOK_URL!, {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ text }),
    });

    if (!response.ok) throw new Error("Slack webhook failed");

    return {
      content: [{ type: "text" as const, text: '{"success": true}' }],
    };
  }
);

const transport = new StdioServerTransport();
await server.connect(transport);

Claude Desktop Configuration

{
  "mcpServers": {
    "slack": {
      "command": "node",
      "args": ["/path/to/slack-mcp-server/dist/index.js"],
      "env": {
        "SLACK_WEBHOOK_URL": "https://hooks.slack.com/services/T.../B.../xxx",
        "SLACK_BOT_TOKEN": "xoxb-your-bot-token"
      }
    }
  }
}
Webhook vs Bot Token

Webhooks are simpler (one URL, one channel, no OAuth). Bot tokens are more powerful (multiple channels, reading messages, reactions). Start with webhooks for notification-only use cases.

Use Cases

Once connected, you can ask Claude things like:

  • "Send a Slack message summarizing today's git commits"
  • "Notify the team channel about the deployment status"
  • "Post a formatted report of the test results to Slack"

The AI combines your Slack tools with other tools (like git or database) to automate workflows.

Frequently Asked Questions