Building a Production-Grade AI Slack Bot with Streaming Responses and MCP Tools
When we set out to build an AI-powered Slack bot for our e-commerce business, we knew it needed to be more than just a basic chatbot. It had to handle complex queries, process multiple file types, integrate with our business tools, and provide real-time streaming responses that felt natural in Slack's interface. Here's how we built it.
Architecture Overview
Our Slack bot is built on TypeScript with the Slack Bolt framework, integrating OpenAI's GPT-4.1 with the Model Context Protocol (MCP) for extensible tool access. The architecture follows a clean three-layer design:
graph TB
subgraph "Presentation Layer"
Slack[Slack Platform]
Bolt[Slack Bolt Framework]
end
subgraph "Business Logic Layer"
SlackTS[slack.ts<br/>Main Orchestrator]
Formatter[slack-formatter.ts<br/>Message & History]
FileProc[file-processor.ts<br/>Multi-modal Files]
end
subgraph "Integration Layer"
OpenAI[openai.ts<br/>GPT-4.1 + Streaming]
MCPClient[mcp-client.ts<br/>Tool Discovery]
S3[s3.ts<br/>File Storage]
end
Slack <--> Bolt
Bolt --> SlackTS
SlackTS --> Formatter
SlackTS --> FileProc
SlackTS --> OpenAI
OpenAI --> MCPClient
FileProc --> S3
style SlackTS fill:#4A90E2,stroke:#2E5C8A,color:#fff
style OpenAI fill:#10A37F,stroke:#0D8267,color:#fff
style MCPClient fill:#FF6B6B,stroke:#C44545,color:#fff
The Message Flow
sequenceDiagram
participant User
participant Slack
participant Bot
participant AI as OpenAI
participant MCP as MCP Tools
User->>Slack: Send message + file
Slack->>Bot: Event received
Bot->>Slack: Post "Thinking..."
Bot->>Bot: Process files & history
Bot->>AI: Start streaming
loop Streaming
AI-->>Bot: Content chunks
Bot->>Slack: Update message
end
alt Tool Needed
AI-->>Bot: Tool call
Bot->>MCP: Execute tool
MCP-->>Bot: Result
Bot->>AI: Continue
end
AI-->>Bot: Complete
Bot->>Slack: Final update
Key Technical Decisions
1. Streaming with Token Batching
Users expect ChatGPT-like progressive responses, but Slack has rate limits. Our solution: token batching.
const STREAMING_CONFIG = {
tokenThreshold: 10, // Update every 10 tokens
timeThreshold: 1000, // OR every 1 second
minChunkLength: 15 // Minimum characters
};
Result: First content appears in 0.5-2 seconds, then smooth updates every second.
2. Block Kit: No "(edited)" Labels
Slack shows "(edited)" when updating the text field. Solution: Use Block Kit blocks instead.
await slackClient.chat.update({
channel: channelId,
ts: messageTs,
blocks: [{
type: "section",
text: { type: "mrkdwn", text: content }
}]
});
Block Kit updates appear seamless and natural.
3. ASCII Art for Tables
Slack doesn't support HTML or markdown tables. We instruct the AI to use ASCII art:
┌─────────────────┬──────────┬────────────┐
│ Product │ Price │ Stock │
├─────────────────┼──────────┼────────────┤
│ Velvet Fabric A │ $25.99 │ In Stock │
└─────────────────┴──────────┴────────────┘
4. Multi-Modal File Processing
| File Type | Processor | Output |
|---|---|---|
unpdf |
Paginated text | |
| DOCX | mammoth |
Extracted text |
| Audio | Replicate Whisper | Transcript |
| Images | None | Vision API format |
Files are uploaded to S3 first, then processed:
const s3Url = await s3Service.uploadFromUrl(file.url_private, "slack");
if (file.filetype === "pdf") {
const text = await extractTextFromPdf(s3Url);
result.promptAddition = `\nCONTEXT:\n${text}`;
}
else if (IMAGE_FILE_TYPES.includes(file.filetype)) {
result.visionMessage = {
role: "user",
content: [{ type: "input_image", image_url: s3Url }]
};
}
5. MCP Tools Integration
The Model Context Protocol (MCP) gives the AI access to 150+ business tools across three servers:
- Business Tools: Product search, order lookup, MySQL queries
- Platform Tools: Blog management, image creation, web scraping
- Code Tools: Repository analysis, code generation
The MCP client discovers and caches tools on startup:
// Connect and fetch tools
for (const serverConfig of mcpServerTools) {
await mcpClient.connect(serverConfig);
}
const mcpTools = await mcpClient.getAllTools();
this.mcpToolsCache.set('default', mcpTools);
We support up to 30 rounds of tool calling for complex multi-step operations.
6. Tool Call Transparency
Users see when tools are being called:
yield {
type: "content",
content: `\n[MCP Tool: ${toolName}]\nInput: ${JSON.stringify(args, null, 2)}\n\n`
};
Appears in Slack as:
[MCP Tool: Product_Search]
Input: {
"prompt": "blue velvet fabric"
}
Error Handling Philosophy
Never let a tool failure break the conversation. Pass errors to the AI to explain naturally:
try {
const result = await this.callTool(serverLabel, toolName, args);
return { type: "function_call_output", call_id, output: result };
} catch (error) {
return {
type: "function_call_output",
call_id,
output: `Error executing tool: ${error.message}`
};
}
The Class-Based Service Pattern
All shared libraries use explicit configuration injection:
// ✅ Good: Explicit config
export class OpenAIService {
constructor(env: OpenAIEnv, logger?: any) {
this.client = new OpenAI({ apiKey: env.OPENAI_API_KEY });
}
}
// Usage
import dotenv from "dotenv";
dotenv.config(); // MUST be first!
const openaiService = new OpenAIService({
OPENAI_API_KEY: process.env.OPENAI_API_KEY!
}, logger);
Benefits: Testable, no hidden dependencies, type-safe, clear initialization.
Logging and Observability
We implemented proper error logging by extracting error messages:
// ✅ Logs useful information
logger.error({
error: error instanceof Error ? error.message : String(error),
stack: error instanceof Error ? error.stack : undefined
}, "Tool failed");
This fixed empty error objects ({}) that appeared when logging raw Error objects.
Performance Optimizations
- MCP Tool Caching: Tools fetched once and cached per agent
- Parallel Tool Execution: Multiple tools execute concurrently
- Conversation History Limits: Cap at 20 messages to prevent overflow
- Token Batching: Reduces Slack API calls while maintaining responsiveness
Lessons Learned
- Stream with batching - Balances responsiveness with API efficiency
- Use Block Kit - Avoid edit indicators on streaming updates
- Embrace platform constraints - ASCII art tables work great in Slack
- Make tools transparent - Show users what the AI is doing
- Never break conversations - Let AI explain errors naturally
- Use explicit config injection - For testable, maintainable services
- MCP makes AI extensible - 150+ tools without custom integration code
Conclusion
Building a production-grade AI Slack bot requires thoughtful architecture, platform-specific adaptations, robust error handling, and a focus on user experience.
The result is a bot that feels natural, handles complex queries across multiple modalities, integrates deeply with business tools, and provides transparency into its operations.
Related Articles
Interested in more AI automation and MCP topics? Check out these related posts:
- Building Automated Claude Code Workers with Cron and MCP Servers - Learn how to create autonomous AI agents that execute tasks automatically using MCP servers
- Using N8N as your automation platform deployed on Fly.io - Deploy the n8n automation platform that powers our MCP servers
- Building an AI-Powered Product Image Generation and Validation Workflow - Another AI automation workflow for generating and validating thousands of product images
Tech Stack:
- Runtime: Node.js with TypeScript
- Framework: Slack Bolt (Socket Mode)
- AI: OpenAI GPT-4.1 with streaming
- Tools: Model Context Protocol (MCP)
- Storage: AWS S3
- Audio: Replicate Whisper
- Document Processing: unpdf, mammoth