← All posts
How-ToSeptember 2, 2026 · 9 min

How to build mcp server for slack - a step-by-step guide

Result: By the end of this tutorial you will have a locally hosted Model Context Protocol (MCP) server that receives Slack slash commands, authenticates the request, forwards the intent to Claude via MCP, and writes the result into a Google Sheet - all without ever exposing your API keys to the public internet.

What you'll get: a reproducible Docker-based stack, a Slack app that triggers the workflow, and a secure webhook that ties the three pieces together.


What is MCP? Model Context Protocol (MCP) is an open-source standard that lets LLMs call external services (APIs, databases, files) as if they were native functions. Think of it as a USB-C port for AI: any model that understands MCP can plug into your custom workflow without custom code for each integration.

What is a Slack slash command? A slash command is a special message that starts with "/" in Slack. When a user types it, Slack sends an HTTP POST to a URL you configure, allowing you to run arbitrary logic on the back-end.

What is the Google Sheets API? A RESTful interface that lets you read, write, and format spreadsheets programmatically.


What you need

ToolPlan / PriceRole
DockerFree (self-hosted)Container runtime for n8n and the MCP server
n8nCommunity (free) or Cloud $20 / monthVisual workflow engine that hosts the webhook and MCP nodes
Node.js (v20)FreeRuntime for any custom MCP adapters you may write
Slack AppFreeProvides the slash command endpoint
Google Cloud projectFree tier (check Google's current pricing)Enables the Google Sheets API and OAuth2 credentials
Anthropic Claude APIPay-as-you-go (≈ $0.25 per 1 M input tokens)LLM that executes MCP calls
VS CodeFreeCode editor for editing JSON and scripts
GitFreeVersion control for your configuration files

Time to build: 4 - 6 hours, assuming you have a basic familiarity with Docker and OAuth2.


How to build mcp server for slack

Below is a concrete, numbered walk-through. Every step names the exact UI field, JSON key, or CLI flag you need to set.

1. Install the local toolchain

Open a terminal and run the following commands.

bash
# Install Docker (Linux example)
curl -fsSL https://get.docker.com -o get-docker.sh
sh get-docker.sh

# Verify Docker is running
docker version

# Install Node.js v20 via nvm (recommended)
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.5/install.sh | bash
source ~/.bashrc
nvm install 20
node -v

If Docker reports "permission denied", add your user to the docker group (sudo usermod -aG docker $USER).

Key insight: Using Docker isolates n8n and the MCP server from your host OS, making it easy to spin up the exact versions the docs reference.

2. Create a Slack app and enable a slash command

1. Go to <https://api.slack.com/apps> and click Create New AppFrom scratch. 2. Name the app "MCP-Sheets Bridge" and pick the workspace where you'll test. 3. In the left sidebar, select Slash CommandsCreate New Command. - Command: /sheetadd - Request URL: https://YOUR_PUBLIC_HOST/webhook/slack (you'll replace YOUR_PUBLIC_HOST later). - Short description: "Add a row to the finance sheet". - Usage hint: key=value ... 4. Save the command. Slack will generate a Signing Secret under Basic Information - copy it; you'll need it for request verification.

3. Set up a Google Cloud project and OAuth2 credentials

1. Open <https://console.cloud.google.com/> and create a new project called "MCP-Sheets". 2. In the navigation menu, go to APIs & Services → Library and enable Google Sheets API. 3. Still under APIs & Services, click OAuth consent screenExternalCreate. Fill in the required fields (app name, support email). 4. Under Credentials, click Create Credentials → OAuth client IDWeb application. - Authorized redirect URIs: https://YOUR_PUBLIC_HOST/oauth2callback (replace later). - Authorized JavaScript origins: https://YOUR_PUBLIC_HOST. 5. Click Create and download the JSON file. Rename it to google-credentials.json and keep it in a secure folder (e.g., ./secrets).

Why this matters: The OAuth client lets n8n obtain a short-lived access token that the Google Sheets node will use.

4. Deploy n8n with Docker

Create a docker-compose.yml in a new folder mcp-slack.

yaml
version: "3.8"
services:
 n8n:
 image: n8nio/n8n:latest
 restart: always
 ports:
 - "5678:5678"
 environment:
 - N8N_BASIC_AUTH_ACTIVE=true
 - N8N_BASIC_AUTH_USER=admin
 - N8N_BASIC_AUTH_PASSWORD=changeme123
 - N8N_HOST=0.0.0.0
 - N8N_PORT=5678
 - N8N_PROTOCOL=https
 - N8N_SSL_CERT=/home/node/.n8n/ssl/cert.pem
 - N8N_SSL_KEY=/home/node/.n8n/ssl/key.pem
 volumes:
 - ./n8n-data:/home/node/.n8n
 - ./secrets:/home/node/.n8n/secrets

Generate a self-signed certificate (or use Let's Encrypt via a reverse proxy).

bash
mkdir -p n8n-data/ssl
openssl req -newkey rsa:2048 -nodes -keyout n8n-data/ssl/key.pem -x509 -days 365 -out n8n-data/ssl/cert.pem -subj "/CN=localhost"

Start the stack:

bash
docker compose up -d

Visit https://localhost:5678 (accept the self-signed warning) and log in with the credentials you set (admin / changeme123).

5. Add a Webhook node to receive Slack commands

  1. In the n8n UI, click New WorkflowAdd Node → search "Webhook".
  2. Set HTTP Method to POST.
  3. Set Path to webhook/slack. This matches the URL you gave Slack in step 2.
  4. Enable Response ModeOn Received. This will let us acknowledge Slack quickly (within 3000 ms).

6. Verify Slack signatures (security)

Add a Function node after the webhook to validate the request.

javascript
// Verify Slack request signature
const crypto = require('crypto');
const signingSecret = process.env.SLACK_SIGNING_SECRET; // set in .env
const timestamp = $json.headers['x-slack-request-timestamp'];
const sig = $json.headers['x-slack-signature'];
const body = $json.body;

const basestring = `v0:${timestamp}:${body}`;
const mySig = `v0=${crypto.createHmac('sha256', signingSecret).update(basestring).digest('hex')}`;

if (crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(mySig))) {
 return [{ verified: true, text: $json.body.text }];
}
throw new Error('Invalid Slack signature');

Add the environment variable SLACK_SIGNING_SECRET in the n8n Docker compose file under environment: (or via the UI's Credentials tab).

7. Parse the slash command arguments

Slack sends the command payload as a URL-encoded string (text=key1=value1+key2=value2). Add another Function node to turn it into a JSON object.

javascript
const raw = $json.text; // e.g. "name=Bob amount=42"
const pairs = raw.split(' ');
const result = {};
pairs.forEach(p => {
 const [k, v] = p.split('=');
 result[k] = v;
});
return [{ payload: result }];

Now $json.payload holds { "name": "Bob", "amount": "42" }.

8. Call Claude via MCP to format the row

Claude does not need to know the Google Sheets details; it only needs to request an MCP operation. Use an HTTP Request node configured for the Anthropic API.

  • Method: POST
  • URL: https://api.anthropic.com/v1/complete
  • Authentication: Bearer token (set ANTHROPIC_API_KEY in n8n credentials).
  • Headers: Content-Type: application/json, anthropic-version: 2023-06-01
  • Body (JSON):
json
{
 "model": "claude-3-5-sonnet-20240620",
 "max_tokens": 1024,
 "temperature": 0,
 "prompt": "You are an MCP client. Use the Model Context Protocol to append a row to a Google Sheet with the following fields:\n{{ $json.payload }}\nReturn only the MCP JSON payload."
}

The response will contain an MCP-compatible JSON block that looks like:

json
{
 "mcp": {
 "action": "google_sheets.append",
 "params": {
 "spreadsheetId": "1AbCdefGhIjKlMnOpQrStUvWxYz",
 "range": "Sheet1!A:B",
 "values": [
 ["{{payload.name}}", "{{payload.amount}}"]
 ]
 }
 }
}

9. Execute the MCP call with the MCP node

n8n does not ship a native MCP node, but you can use a Function node to forward the JSON to a tiny MCP server you'll run locally. First, spin up the MCP server (see step 10). Then add an HTTP Request node:

  • Method: POST
  • URL: http://host.docker.internal:8000/mcp (Docker bridge to host).
  • Body: {{ $json.mcp }} (the MCP payload from Claude).

The MCP server will translate the request into a Google Sheets API call and return success/failure.

10. Run a minimal MCP server (Node.js)

Create a folder mcp-server and add package.json:

json
{
 "name": "mcp-server",
 "version": "1.0.0",
 "type": "module",
 "dependencies": {
 "express": "^4.18.2",
 "googleapis": "^124.0.0"
 }
}

Install dependencies:

bash
cd mcp-server
npm install

Create server.js:

javascript
import express from 'express';
import { google } from 'googleapis';
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';

const __dirname = path.dirname(fileURLToPath(import.meta.url));
const app = express();
app.use(express.json());

// Load Google OAuth2 client from secret file
const credentials = JSON.parse(fs.readFileSync(path.join(__dirname, '../secrets/google-credentials.json')));
const { client_id, client_secret, redirect_uris } = credentials.installed;
const oAuth2Client = new google.auth.OAuth2(client_id, client_secret, redirect_uris[0]);

// In a real deployment you would store and refresh tokens.
// For this demo we assume a pre-generated refresh token is saved.
const tokenPath = path.join(__dirname, '../secrets/google-token.json');
if (fs.existsSync(tokenPath)) {
 oAuth2Client.setCredentials(JSON.parse(fs.readFileSync(tokenPath)));
} else {
 console.error('Google token not found - run the OAuth flow first.');
 process.exit(1);
}
const sheets = google.sheets({ version: 'v4', auth: oAuth2Client });

app.post('/mcp', async (req, res) => {
 const { action, params } = req.body;
 if (action !== 'google_sheets.append') {
 return res.status(400).json({ error: 'Unsupported action' });
 }
 try {
 await sheets.spreadsheets.values.append({
 spreadsheetId: params.spreadsheetId,
 range: params.range,
 valueInputOption: 'RAW',
 requestBody: { values: params.values }
 });
 res.json({ status: 'ok' });
 } catch (e) {
 console.error(e);
 res.status(500).json({ error: e.message });
 }
});

app.listen(8000, () => console.log('MCP server listening on :8000'));

Run the server:

bash
node server.js

What this does: It exposes a single /mcp endpoint that accepts the MCP JSON from Claude, validates the action, and calls the Google Sheets API to append the row.

11. Wire everything together in n8n

Connect the nodes in this order:

  1. Webhook → 2. Function (verify Slack) → 3. Function (parse args) → 4. HTTP Request (Claude) → 5. Function (extract MCP payload) → 6. HTTP Request (MCP server) → 7. Set (optional: format a success message) → 8. Respond to Slack.

In the final Respond to Slack node, set Response Body to:

json
{
 "response_type": "in_channel",
 "text": "✅ Row added for {{ $json.payload.name }}."
}

Save the workflow and activate it (toggle the switch in the top-right).

12. Expose the webhook publicly (development only)

For local testing you can use ngrok:

bash
npm install -g ngrok
ngrok http 5678 --host-header=localhost

Copy the generated HTTPS URL (e.g., https://abcd1234.ngrok.io) and replace YOUR_PUBLIC_HOST in the Slack app's Request URL and the Google OAuth redirect URI.

⚠️ Important: Never leave a development tunnel open in production. Deploy the same Docker compose stack behind a proper reverse proxy (Traefik, Caddy) with a real TLS certificate.

13. Test the end-to-end flow

In Slack, type:

/sheetadd name=Alice amount=123

You should see the confirmation message, and the row appear in the target Google Sheet. Check the n8n execution log for any errors.


Where this breaks

Failure modeSymptomFix
Slack signature mismatch400 error from n8n, "Invalid Slack signature"Ensure the SLACK_SIGNING_SECRET env var matches the secret shown in Slack's Basic Information page.
Google OAuth token expiredMCP server logs "Request failed with status code 401"Run the OAuth refresh flow: open https://accounts.google.com/o/oauth2/v2/auth?... with the client ID, grant access, and save the returned refresh_token to google-token.json.
Rate limit on Google Sheets API429 response, "User rate limit exceeded"Batch multiple rows into a single append call, or request a higher quota in the Google Cloud console.
Anthropic rate limit429 from api.anthropic.comBack-off 1 second and retry; consider purchasing a higher-tier plan if you exceed the free quota.
MCP server not reachablen8n HTTP Request returns "ECONNREFUSED"Verify Docker networking (host.docker.internal works on your OS) or expose the MCP server on the same Docker network.
Cost blow-upUnexpected charge on Anthropic billingSet a hard limit in the Anthropic dashboard, and add a n8n IF node that aborts if token usage exceeds a threshold.
HTTPS misconfigurationSlack rejects the webhook with "Invalid URL"Use a valid TLS certificate; Slack requires HTTPS with a trusted CA.

> Never expose raw API keys in the webhook URL or in Slack's command text. All secrets must live in environment variables or Docker secrets.


For a deeper technical reference, see n8n's documentation.

Frequently asked questions

How do I rotate the Slack signing secret without breaking existing commands?

Create a new secret in Slack's Basic Information page, update the `SLACK_SIGNING_SECRET` env var, and restart the n8n container (`docker compose restart n8n`). Old requests signed with the previous secret will be rejected, so inform your team to re-install the app if needed.

Can I host the MCP server on a cloud VM instead of locally?

Yes. Deploy the same `server.js` on any Linux VM, expose port 8000, and secure it with a firewall rule that only allows traffic from your n8n container's IP. Remember to use a managed TLS termination point (e.g., Cloudflare) because the MCP spec expects HTTPS for production.

What if I need to write to multiple sheets from the same command?

Modify the Claude prompt to return an array of MCP actions, then add a SplitInBatches node in n8n to iterate over each action and call the MCP server for each.

How do I debug a failing Google Sheets append?

Enable the Google API client's debug mode by adding `google.options({ logger: console })` before the `sheets.spreadsheets.values.append` call. The server will log the full request payload and any error message returned by Google.

Is there a way to avoid paying for Anthropic if I only need simple formatting?

You can replace Claude with an open-source LLM (e.g., Llama 3) running locally and expose it via an HTTP endpoint that returns the same MCP JSON. The rest of the workflow stays unchanged. --- Ready to start building? Grab the free guide that walks you through the first three automations you can sell and the exact Docker commands you need: https://getaab.com/free If you want inspiration for monetiz

Get the full toolkit

Grab the free guide with the node-by-node build for all 10 automations.

No spam. Unsubscribe anytime. Just the good stuff.