← Back to Blog
tutorialslackintegrationproject

Building a Slack Integration MCP Tool

Create an MCP tool that sends messages, creates channels, and manages Slack workspaces from AI agents.

·4 min read·xapable

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

  1. Go to https://api.slack.com/apps
  2. Create a new app → "From scratch"
  3. Name it (e.g., "MCP Slack Bot")
  4. 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
  5. Install to workspace → copy the Bot User OAuth Token (starts with xoxb-)

Step 2: Initialize

<span class="hljs-built_in">mkdir</span> slack-mcp &amp;&amp; <span class="hljs-built_in">cd</span> slack-mcp
npm init -y
npm install @modelcontextprotocol/sdk @slack/web-api

Step 3: Build the Server

<span class="hljs-keyword">import</span> { <span class="hljs-title class_">Server</span> } <span class="hljs-keyword">from</span> <span class="hljs-string">&quot;@modelcontextprotocol/sdk/server/index.js&quot;</span>;
<span class="hljs-keyword">import</span> { <span class="hljs-title class_">StdioServerTransport</span> } <span class="hljs-keyword">from</span> <span class="hljs-string">&quot;@modelcontextprotocol/sdk/server/stdio.js&quot;</span>;
<span class="hljs-keyword">import</span> { <span class="hljs-title class_">WebClient</span> } <span class="hljs-keyword">from</span> <span class="hljs-string">&quot;@slack/web-api&quot;</span>;

<span class="hljs-keyword">const</span> <span class="hljs-variable constant_">TOKEN</span> = process.<span class="hljs-property">env</span>.<span class="hljs-property">SLACK_BOT_TOKEN</span>;
<span class="hljs-keyword">if</span> (!<span class="hljs-variable constant_">TOKEN</span>) {
  <span class="hljs-variable language_">console</span>.<span class="hljs-title function_">error</span>(<span class="hljs-string">&quot;SLACK_BOT_TOKEN is required&quot;</span>);
  process.<span class="hljs-title function_">exit</span>(<span class="hljs-number">1</span>);
}

<span class="hljs-keyword">const</span> slack = <span class="hljs-keyword">new</span> <span class="hljs-title class_">WebClient</span>(<span class="hljs-variable constant_">TOKEN</span>);

<span class="hljs-keyword">const</span> server = <span class="hljs-keyword">new</span> <span class="hljs-title class_">Server</span>({
  <span class="hljs-attr">name</span>: <span class="hljs-string">&quot;slack-mcp&quot;</span>,
  <span class="hljs-attr">version</span>: <span class="hljs-string">&quot;1.0.0&quot;</span>,
}, { <span class="hljs-attr">capabilities</span>: { <span class="hljs-attr">tools</span>: {} } });

server.<span class="hljs-title function_">setRequestHandler</span>(<span class="hljs-string">&quot;tools/list&quot;</span>, <span class="hljs-title function_">async</span> () =&gt; ({
  <span class="hljs-attr">tools</span>: [
    {
      <span class="hljs-attr">name</span>: <span class="hljs-string">&quot;send_message&quot;</span>,
      <span class="hljs-attr">description</span>: <span class="hljs-string">&quot;Send a message to a Slack channel or user&quot;</span>,
      <span class="hljs-attr">inputSchema</span>: {
        <span class="hljs-attr">type</span>: <span class="hljs-string">&quot;object&quot;</span>,
        <span class="hljs-attr">properties</span>: {
          <span class="hljs-attr">channel</span>: {
            <span class="hljs-attr">type</span>: <span class="hljs-string">&quot;string&quot;</span>,
            <span class="hljs-attr">description</span>: <span class="hljs-string">&quot;Channel name (e.g., &#x27;#general&#x27;) or channel ID&quot;</span>
          },
          <span class="hljs-attr">text</span>: {
            <span class="hljs-attr">type</span>: <span class="hljs-string">&quot;string&quot;</span>,
            <span class="hljs-attr">description</span>: <span class="hljs-string">&quot;Message text (supports Slack mrkdwn)&quot;</span>
          }
        },
        <span class="hljs-attr">required</span>: [<span class="hljs-string">&quot;channel&quot;</span>, <span class="hljs-string">&quot;text&quot;</span>]
      }
    },
    {
      <span class="hljs-attr">name</span>: <span class="hljs-string">&quot;list_channels&quot;</span>,
      <span class="hljs-attr">description</span>: <span class="hljs-string">&quot;List all public channels in the workspace&quot;</span>,
      <span class="hljs-attr">inputSchema</span>: {
        <span class="hljs-attr">type</span>: <span class="hljs-string">&quot;object&quot;</span>,
        <span class="hljs-attr">properties</span>: {
          <span class="hljs-attr">include_archived</span>: {
            <span class="hljs-attr">type</span>: <span class="hljs-string">&quot;boolean&quot;</span>,
            <span class="hljs-attr">description</span>: <span class="hljs-string">&quot;Include archived channels (default: false)&quot;</span>
          }
        }
      }
    },
    {
      <span class="hljs-attr">name</span>: <span class="hljs-string">&quot;search_messages&quot;</span>,
      <span class="hljs-attr">description</span>: <span class="hljs-string">&quot;Search for messages matching a query&quot;</span>,
      <span class="hljs-attr">inputSchema</span>: {
        <span class="hljs-attr">type</span>: <span class="hljs-string">&quot;object&quot;</span>,
        <span class="hljs-attr">properties</span>: {
          <span class="hljs-attr">query</span>: {
            <span class="hljs-attr">type</span>: <span class="hljs-string">&quot;string&quot;</span>,
            <span class="hljs-attr">description</span>: <span class="hljs-string">&quot;Search query&quot;</span>
          },
          <span class="hljs-attr">channel</span>: {
            <span class="hljs-attr">type</span>: <span class="hljs-string">&quot;string&quot;</span>,
            <span class="hljs-attr">description</span>: <span class="hljs-string">&quot;Limit search to a specific channel&quot;</span>
          },
          <span class="hljs-attr">count</span>: {
            <span class="hljs-attr">type</span>: <span class="hljs-string">&quot;number&quot;</span>,
            <span class="hljs-attr">description</span>: <span class="hljs-string">&quot;Number of results (default: 10, max: 50)&quot;</span>
          }
        },
        <span class="hljs-attr">required</span>: [<span class="hljs-string">&quot;query&quot;</span>]
      }
    },
    {
      <span class="hljs-attr">name</span>: <span class="hljs-string">&quot;create_channel&quot;</span>,
      <span class="hljs-attr">description</span>: <span class="hljs-string">&quot;Create a new public channel&quot;</span>,
      <span class="hljs-attr">inputSchema</span>: {
        <span class="hljs-attr">type</span>: <span class="hljs-string">&quot;object&quot;</span>,
        <span class="hljs-attr">properties</span>: {
          <span class="hljs-attr">name</span>: {
            <span class="hljs-attr">type</span>: <span class="hljs-string">&quot;string&quot;</span>,
            <span class="hljs-attr">description</span>: <span class="hljs-string">&quot;Channel name (lowercase, no spaces, max 80 chars)&quot;</span>
          },
          <span class="hljs-attr">is_private</span>: {
            <span class="hljs-attr">type</span>: <span class="hljs-string">&quot;boolean&quot;</span>,
            <span class="hljs-attr">description</span>: <span class="hljs-string">&quot;Create a private channel (default: false)&quot;</span>
          }
        },
        <span class="hljs-attr">required</span>: [<span class="hljs-string">&quot;name&quot;</span>]
      }
    }
  ]
}));

server.<span class="hljs-title function_">setRequestHandler</span>(<span class="hljs-string">&quot;tools/call&quot;</span>, <span class="hljs-title function_">async</span> (request) =&gt; {
  <span class="hljs-keyword">const</span> { name, <span class="hljs-attr">arguments</span>: args } = request.<span class="hljs-property">params</span>;

  <span class="hljs-keyword">try</span> {
    <span class="hljs-keyword">switch</span> (name) {
      <span class="hljs-keyword">case</span> <span class="hljs-string">&quot;send_message&quot;</span>: {
        <span class="hljs-keyword">const</span> result = <span class="hljs-keyword">await</span> slack.<span class="hljs-property">chat</span>.<span class="hljs-title function_">postMessage</span>({
          <span class="hljs-attr">channel</span>: args.<span class="hljs-property">channel</span>,
          <span class="hljs-attr">text</span>: args.<span class="hljs-property">text</span>
        });

        <span class="hljs-keyword">return</span> {
          <span class="hljs-attr">content</span>: [{
            <span class="hljs-attr">type</span>: <span class="hljs-string">&quot;text&quot;</span>,
            <span class="hljs-attr">text</span>: <span class="hljs-string">`✅ Message sent to <span class="hljs-subst">${args.channel}</span> (<span class="hljs-subst">${result.ts}</span>)`</span>
          }]
        };
      }

      <span class="hljs-keyword">case</span> <span class="hljs-string">&quot;list_channels&quot;</span>: {
        <span class="hljs-keyword">const</span> result = <span class="hljs-keyword">await</span> slack.<span class="hljs-property">conversations</span>.<span class="hljs-title function_">list</span>({
          <span class="hljs-attr">exclude_archived</span>: !args.<span class="hljs-property">include_archived</span>,
          <span class="hljs-attr">types</span>: <span class="hljs-string">&quot;public_channel&quot;</span>
        });

        <span class="hljs-keyword">const</span> channels = result.<span class="hljs-property">channels</span>
          .<span class="hljs-title function_">map</span>(<span class="hljs-function"><span class="hljs-params">c</span> =&gt;</span> <span class="hljs-string">`   #<span class="hljs-subst">${c.name}</span> <span class="hljs-subst">${c.is_archived ? <span class="hljs-string">&quot;(archived)&quot;</span> : <span class="hljs-string">&quot;&quot;</span>}</span>`</span>)
          .<span class="hljs-title function_">join</span>(<span class="hljs-string">&quot;\n&quot;</span>);

        <span class="hljs-keyword">return</span> {
          <span class="hljs-attr">content</span>: [{
            <span class="hljs-attr">type</span>: <span class="hljs-string">&quot;text&quot;</span>,
            <span class="hljs-attr">text</span>: <span class="hljs-string">`📋 Channels (<span class="hljs-subst">${result.channels.length}</span>):
<span class="hljs-subst">${channels}</span>`</span>
          }]
        };
      }

      <span class="hljs-keyword">case</span> <span class="hljs-string">&quot;search_messages&quot;</span>: {
        <span class="hljs-keyword">const</span> result = <span class="hljs-keyword">await</span> slack.<span class="hljs-property">search</span>.<span class="hljs-title function_">messages</span>({
          <span class="hljs-attr">query</span>: args.<span class="hljs-property">query</span>,
          <span class="hljs-attr">count</span>: <span class="hljs-title class_">Math</span>.<span class="hljs-title function_">min</span>(args.<span class="hljs-property">count</span> || <span class="hljs-number">10</span>, <span class="hljs-number">50</span>)
        });

        <span class="hljs-keyword">const</span> messages = result.<span class="hljs-property">messages</span>.<span class="hljs-property">matches</span>
          .<span class="hljs-title function_">slice</span>(<span class="hljs-number">0</span>, args.<span class="hljs-property">count</span> || <span class="hljs-number">10</span>)
          .<span class="hljs-title function_">map</span>(<span class="hljs-function">(<span class="hljs-params">m, i</span>) =&gt;</span>
            <span class="hljs-string">`<span class="hljs-subst">${i + <span class="hljs-number">1</span>}</span>. [<span class="hljs-subst">${m.channel.name}</span>] <span class="hljs-subst">${m.username}</span>: <span class="hljs-subst">${m.text.slice(<span class="hljs-number">0</span>, <span class="hljs-number">100</span>)}</span>`</span>
          ).<span class="hljs-title function_">join</span>(<span class="hljs-string">&quot;\n&quot;</span>);

        <span class="hljs-keyword">return</span> {
          <span class="hljs-attr">content</span>: [{
            <span class="hljs-attr">type</span>: <span class="hljs-string">&quot;text&quot;</span>,
            <span class="hljs-attr">text</span>: <span class="hljs-string">`🔍 Results for &quot;<span class="hljs-subst">${args.query}</span>&quot;:
<span class="hljs-subst">${messages || <span class="hljs-string">&quot;No messages found.&quot;</span>}</span>`</span>
          }]
        };
      }

      <span class="hljs-keyword">case</span> <span class="hljs-string">&quot;create_channel&quot;</span>: {
        <span class="hljs-keyword">const</span> result = <span class="hljs-keyword">await</span> slack.<span class="hljs-property">conversations</span>.<span class="hljs-title function_">create</span>({
          <span class="hljs-attr">name</span>: args.<span class="hljs-property">name</span>.<span class="hljs-title function_">toLowerCase</span>().<span class="hljs-title function_">replace</span>(<span class="hljs-regexp">/[^a-z0-9-_]/g</span>, <span class="hljs-string">&quot;&quot;</span>).<span class="hljs-title function_">slice</span>(<span class="hljs-number">0</span>, <span class="hljs-number">80</span>),
          <span class="hljs-attr">is_private</span>: args.<span class="hljs-property">is_private</span> || <span class="hljs-literal">false</span>
        });

        <span class="hljs-keyword">return</span> {
          <span class="hljs-attr">content</span>: [{
            <span class="hljs-attr">type</span>: <span class="hljs-string">&quot;text&quot;</span>,
            <span class="hljs-attr">text</span>: <span class="hljs-string">`Channel #<span class="hljs-subst">${result.channel.name}</span> created!`</span>
          }]
        };
      }

      <span class="hljs-attr">default</span>:
        <span class="hljs-keyword">return</span> {
          <span class="hljs-attr">content</span>: [{ <span class="hljs-attr">type</span>: <span class="hljs-string">&quot;text&quot;</span>, <span class="hljs-attr">text</span>: <span class="hljs-string">`❌ Unknown tool: <span class="hljs-subst">${name}</span>`</span> }],
          <span class="hljs-attr">isError</span>: <span class="hljs-literal">true</span>
        };
    }
  } <span class="hljs-keyword">catch</span> (err) {
    <span class="hljs-keyword">return</span> {
      <span class="hljs-attr">content</span>: [{
        <span class="hljs-attr">type</span>: <span class="hljs-string">&quot;text&quot;</span>,
        <span class="hljs-attr">text</span>: <span class="hljs-string">`❌ Slack error: <span class="hljs-subst">${err.message}</span>`</span>
      }],
      <span class="hljs-attr">isError</span>: <span class="hljs-literal">true</span>
    };
  }
});

<span class="hljs-keyword">const</span> transport = <span class="hljs-keyword">new</span> <span class="hljs-title class_">StdioServerTransport</span>();
<span class="hljs-keyword">await</span> server.<span class="hljs-title function_">connect</span>(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