Production Slack MCP Server Starter Template (TypeScript)
Overview
This template provides a complete, production-ready foundation for building a Slack MCP (Model Context Protocol) server using TypeScript, Express, and the official @modelcontextprotocol/sdk. It implements the full Slack OAuth 2.0 Authorization Code flow so your server can be installed into any Slack workspace, manage bot tokens securely, and expose powerful Slack capabilities as MCP tools that Claude, Cursor, Windsurf, and other MCP-compatible clients can call.
The server ships with four Slack tool modules โ channels, messages, users, and search โ covering seven MCP tools in total, each backed by the official @slack/web-api client with proper error handling, input validation with Zod, and full TypeScript types throughout. It also includes a structured logging pipeline via Pino with request correlation IDs, per-IP rate limiting via express-rate-limit, and a /health endpoint that reports on every dependency.
Current state: this starter currently ships the server, its OAuth flow, and its test harness. A Dockerfile,
docker-compose.yml, GitHub Actions CI, and a checked-in.env.exampleare not yet included โ see Project Structure and Configuration below for what's actually here and how to supply your own environment variables in the meantime.
This template is aimed at intermediate-to-advanced TypeScript developers who want to skip the boilerplate and ship a secure, observable Slack MCP integration quickly. Whether you're building an internal productivity tool for your team or a public Slack app powered by AI, this starter gives you a solid foundation to build on.
What You'll Learn
- How to register and implement MCP tools using the official
@modelcontextprotocol/sdk(McpServer+StreamableHTTPServerTransport) with full input schemas - How to implement the Slack OAuth 2.0 Authorization Code flow including
stateCSRF protection, token exchange, and in-memory token storage - How to use the
@slack/web-apiWebClientto interact with Slack's REST API for channels, messages, users, and search - How to validate MCP tool inputs with Zod schemas and surface structured errors back to the MCP client
- How to set up Pino for structured JSON logging with request correlation IDs and a configurable
LOG_LEVEL - How to implement per-IP rate limiting with
express-rate-limitand return RFC 7807-style error responses - How to build a
/healthendpoint that checks each dependency (Slack API, token store) and returns machine-readable JSON - How to validate required environment variables at startup with a Zod-backed config schema, so misconfiguration fails fast instead of surfacing as a runtime error mid-request
Architecture
MCP Client (Claude / Cursor / Windsurf)
โ
โ HTTP POST /mcp (JSON-RPC 2.0)
โผ
Express Server โโโบ Rate Limiter (express-rate-limit)
โ
โผ
Bearer Token Middleware โโโบ validates Authorization: Bearer <MCP_API_KEY>
โ
โผ
MCP SDK (McpServer) โโโบ Tool Registry (channels, messages, users, search)
โ
โผ
Slack Web API Client (@slack/web-api)
โ
โผ
Slack Platform (api.slack.com)
Slack OAuth Flow (separate from /mcp, no bearer token required):
Browser โโโบ GET /auth/slack โโโบ Slack Authorization Page
โ
Browser โโโ GET /auth/slack/callback โโโ
โ
โผ
In-memory Token Store (swap for Redis/DB in multi-instance deployments)
Project Structure
slack-mcp-server/
โโโ src/
โ โโโ __tests__/
โ โ โโโ jest.setup.ts
โ โ โโโ config.test.ts
โ โโโ auth.ts
โ โโโ config.ts
โ โโโ health.ts
โ โโโ logger.ts
โ โโโ server.ts
โ โโโ tools/
โ โโโ channels.ts
โ โโโ messages.ts
โ โโโ search.ts
โ โโโ users.ts
โโโ eslint.config.js
โโโ jest.config.ts
โโโ package.json
โโโ package-lock.json
โโโ tsconfig.json
Not yet part of this template โ add them yourself if your deployment target needs them:
.github/workflows/ci.yml(GitHub Actions)Dockerfile/docker-compose.yml.env.example/.gitignore
Prerequisites
- Node.js 20+ and npm 10+
- A Slack App created at api.slack.com/apps with:
- OAuth & Permissions: Redirect URL set to
http://localhost:3000/auth/slack/callback - Bot Token Scopes:
channels:read,channels:history,users:read,search:read,chat:write - Enable OAuth & Permissions and copy your Client ID and Client Secret
- OAuth & Permissions: Redirect URL set to
- Git for version control
Quick Start
# 1. Clone or download this template
git clone https://github.com/your-org/slack-mcp-server.git
cd slack-mcp-server
# 2. Install dependencies
npm install
# 3. Configure environment
# There is no .env.example yet โ create .env yourself with the variables
# listed in the Configuration table below (or export them in your shell).
cat > .env << 'EOF'
SLACK_CLIENT_ID=your-client-id
SLACK_CLIENT_SECRET=your-client-secret
SLACK_SIGNING_SECRET=your-signing-secret
SLACK_REDIRECT_URI=http://localhost:3000/auth/slack/callback
MCP_API_KEY=$(node -e "console.log(require('crypto').randomBytes(32).toString('hex'))")
SESSION_SECRET=$(node -e "console.log(require('crypto').randomBytes(32).toString('hex'))")
EOF
# 4. Start in development mode (ts-node-dev with live reload)
npm run dev
# 5. Trigger the Slack OAuth installation flow
open http://localhost:3000/auth/slack
# Complete the OAuth flow in your browser
# Your bot token is now stored in the token store
# 6. Test the health endpoint
curl http://localhost:3000/health
# 7. Run tests
npm test
# 8. Build for production
npm run build
npm start
Features
- Full Slack OAuth 2.0 flow โ CSRF-protected state parameter, token exchange, workspace-scoped in-memory token storage
- 7 MCP tools across 4 modules โ
slack_list_channels,slack_get_channel_history(channels);slack_send_message,slack_get_thread_replies(messages);slack_get_user_info,slack_list_users(users);slack_search_messages(search) - Zod input validation โ every tool input is validated before hitting the Slack API
- Pino structured logging โ JSON output, request correlation IDs generated via
uuid(falls back to an incomingx-request-idheader when present), configurableLOG_LEVEL - Rate limiting โ per-IP on the
/mcpendpoint with configurable window and max requests - Health endpoint โ
/healthchecks Slack API reachability and token store state, returning200when healthy and503when down - Jest test harness โ
jest.setup.tsseeds the environment variables the Zod config schema requires, so tests don't need a real.envfile; ships with a config-validation test suite as a starting point (add integration tests for the/mcpand/healthroutes with the already-installedsupertestpackage) - Full TypeScript โ strict mode, ESLint flat config, complete type coverage
Known limitation:
SLACK_SIGNING_SECRETandSESSION_SECRETare required by the config schema but aren't wired into any logic yet โ there's no Slack request-signature verification middleware, and the OAuthstateparameter is tracked server-side in memory rather than in a signed cookie.cookie-parseris installed but unused for the same reason. Add signature verification before exposing an Events API endpoint, and treat both secrets as reserved for that future use rather than as already-enforced protections.
Works With
- Claude Code โ add as a remote HTTP MCP server:
claude mcp add --transport http slack http://localhost:3000/mcp \ --header "Authorization: Bearer YOUR_MCP_API_KEY" - Claude Desktop โ add to
claude_desktop_config.json - Cursor โ add as an MCP server in
.cursor/mcp.json - Windsurf โ configure in Windsurf MCP settings
- Continue โ add as an MCP provider in
config.json
Claude Desktop / generic HTTP MCP client configuration
{
"mcpServers": {
"slack": {
"type": "http",
"url": "http://localhost:3000/mcp",
"headers": {
"Authorization": "Bearer YOUR_MCP_API_KEY"
}
}
}
}
Swap the url for your deployed server's address once you're past local development. Any client that speaks MCP over Streamable HTTP with a static bearer token accepts this same shape.
Configuration
| Variable | Description | Required |
|---|---|---|
PORT | HTTP server port | Optional (default: 3000) |
NODE_ENV | Runtime environment (development/production/test) | Optional (default: development) |
LOG_LEVEL | Pino log level (trace/debug/info/warn/error/fatal/silent) | Optional (default: info) |
SLACK_CLIENT_ID | Slack App Client ID from api.slack.com/apps | Required |
SLACK_CLIENT_SECRET | Slack App Client Secret | Required |
SLACK_SIGNING_SECRET | Reserved for Slack request-signature verification โ validated at startup but not yet checked against incoming requests (see Known Limitation above) | Required |
SLACK_REDIRECT_URI | OAuth callback URL (must match Slack App settings); must be a valid URL | Required |
SLACK_BOT_TOKEN | Pre-existing bot token โ bootstraps the token store on startup, letting you skip the OAuth flow entirely | Optional |
MCP_API_KEY | Bearer token required on the /mcp endpoint; minimum 16 characters | Required |
SESSION_SECRET | Reserved for future cookie-signing use โ validated at startup but not currently used by the OAuth flow; minimum 16 characters | Required |
RATE_LIMIT_WINDOW_MS | Rate limit window in milliseconds | Optional (default: 60000) |
RATE_LIMIT_MAX | Max requests per window per IP | Optional (default: 100) |
BASE_URL | Public base URL of the server; must be a valid URL | Optional (default: http://localhost:3000) |
Deployment
There's no Dockerfile in this template yet, so deploy it like any standard Node.js HTTP server. Set the required environment variables from the table above on your platform, then run npm run build && npm start.
Railway
npm install -g @railway/cli
railway login
railway init
railway up
# Set environment variables in the Railway dashboard,
# and set the start command to: npm run build && npm start
Fly.io
Fly.io needs either a Dockerfile or Fly's Nixpacks-based buildpack detection for Node projects. Without a Dockerfile in this template, use fly launch and let it generate one, or add your own multi-stage Dockerfile before deploying:
npm install -g flyctl
fly auth login
fly launch --name slack-mcp-server
fly secrets set SLACK_CLIENT_ID=xxx SLACK_CLIENT_SECRET=xxx MCP_API_KEY=xxx SESSION_SECRET=xxx SLACK_SIGNING_SECRET=xxx SLACK_REDIRECT_URI=https://your-app.fly.dev/auth/slack/callback
fly deploy
Containerizing it yourself
If you want Docker, a minimal multi-stage setup for this project looks like: a build stage that runs npm ci && npm run build, and a runtime stage that copies dist/ plus production dependencies only (npm ci --omit=dev), runs as a non-root user, and starts with node dist/server.js. Wire /health into your container's HEALTHCHECK and your orchestrator's liveness probe.
Production Checklist
-
SLACK_CLIENT_ID,SLACK_CLIENT_SECRET, andSLACK_SIGNING_SECRETset from Slack App dashboard -
SLACK_REDIRECT_URImatches exactly what is registered in Slack App OAuth settings -
MCP_API_KEYis a cryptographically random string, at least 32 bytes (e.g.openssl rand -hex 32) -
SESSION_SECRETis a cryptographically random string separate fromMCP_API_KEY -
NODE_ENV=productionis set in the deployment environment -
LOG_LEVEL=infoorwarnin production (avoiddebug/trace) - HTTPS is terminated at the load balancer or reverse proxy โ never HTTP in production
- Rate limit values tuned for expected traffic (
RATE_LIMIT_MAX,RATE_LIMIT_WINDOW_MS) -
/healthendpoint is wired into your load balancer or orchestrator's health checks - Token store replaced with Redis or a database before running more than one instance โ the current store is an in-process
Mapand does not survive restarts or scale horizontally - Slack App has the minimum required scopes โ review and remove unused ones
- Rotate
MCP_API_KEYandSESSION_SECRETon a regular schedule - If you plan to receive Slack Events API callbacks, add signature verification against
SLACK_SIGNING_SECRETfirst โ it is not implemented in this starter
Testing
# Run all tests
npm test
# Run with coverage
npm run test:coverage
# Run tests in watch mode
npm run test:watch
# Manual curl test โ health endpoint
curl -s http://localhost:3000/health | jq .
# Manual curl test โ MCP tools/list (requires valid MCP_API_KEY)
curl -s -X POST http://localhost:3000/mcp \
-H 'Content-Type: application/json' \
-H 'Accept: application/json, text/event-stream' \
-H 'Authorization: Bearer YOUR_MCP_API_KEY' \
-d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"manual-test","version":"1.0.0"}}}'
# Manual curl test โ unauthenticated request should return 401
curl -s -o /dev/null -w "%{http_code}" -X POST http://localhost:3000/mcp \
-H 'Content-Type: application/json' \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}'
The test suite currently ships one file, config.test.ts, covering environment-variable validation. supertest is already a dev dependency โ use it to add integration tests against /health and /mcp (200/401/tool-call cases) as the next testing milestone.