Skip to main content

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
PDF 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:

  1. Business Tools: Product search, order lookup, MySQL queries
  2. Platform Tools: Blog management, image creation, web scraping
  3. 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

  1. Stream with batching - Balances responsiveness with API efficiency
  2. Use Block Kit - Avoid edit indicators on streaming updates
  3. Embrace platform constraints - ASCII art tables work great in Slack
  4. Make tools transparent - Show users what the AI is doing
  5. Never break conversations - Let AI explain errors naturally
  6. Use explicit config injection - For testable, maintainable services
  7. 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:


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

Need help with your project or have questions?

We specialize in AI automation, custom integrations, and intelligent workflows tailored to your business needs.

Whether you need help deploying, building, implementing, or creating a solution - or just want expert guidance on your project - we're here to help.

Contact us today to discuss your project.