How to Connect LangGraph to Slack with Arcade (MCP)

How to Connect LangGraph to Slack with Arcade (MCP)

Arcade.dev Team's avatar
Arcade.dev Team
OCTOBER 16, 2025
10 MIN READ
THOUGHT LEADERSHIP
Rays decoration image
Ghost Icon

Building production-ready AI agents that interact with Slack requires solving multiple technical challenges: secure authentication, token management, user context isolation, and seamless integration with orchestration frameworks. This guide demonstrates how to connect LangGraph agents to Slack using Arcade's Model Context Protocol (MCP) implementation, enabling your agents to send messages, create channels, and interact with workspaces on behalf of multiple users.

Prerequisites and Setup Requirements

Before implementing the LangGraph-Slack integration, ensure you have these components configured:

  • An active Arcade.dev account with API key
  • Slack workspace with admin access to create OAuth apps
  • Python 3.8+ with LangGraph installed
  • Basic familiarity with async Python patterns
  • Access to a development environment for testing

Initial Environment Configuration

Start by installing the required packages and setting up your development environment. The Arcade Python client and Tool Development Kit provide the foundation for MCP server communication and Slack authentication.

# Install Arcade Python client and dependencies
pip install arcadepy arcade_tdk langraph langgraph

# Configure environment variables
export ARCADE_API_KEY="your_arcade_api_key"
export SLACK_CLIENT_ID="your_slack_client_id"
export SLACK_CLIENT_SECRET="your_slack_client_secret"

MCP Architecture for Slack Integration

The Authentication Challenge in Agent Frameworks

LangGraph excels at orchestrating multi-stage agent workflows through its graph-based architecture, but it lacks native authentication mechanisms for external services. When agents need to post to Slack channels or read messages, they typically require hardcoded bot tokens or manual credential management—patterns that fail security audits and limit multi-user scalability.

Arcade's MCP implementation bridges this gap by:

  • Providing OAuth 2.0 flows for each user
  • Managing token rotation automatically
  • Isolating user contexts for security
  • Exposing Slack capabilities through standardized tool definitions

MCP Server Configuration for LangGraph

Configure the MCP server to expose Slack tools to your LangGraph agents. This setup enables the framework to access Slack through Arcade's authentication layer without managing credentials directly.

# mcp_config.py
from arcade_tdk import MCPServer
from arcadepy import Arcade

class SlackMCPServer:
    def __init__(self):
        self.arcade = Arcade(api_key=os.getenv("ARCADE_API_KEY"))
        self.mcp_server = MCPServer(
            name="slack-langgraph",
            url="https://api.arcade.dev/v1/mcps/slack/mcp"
        )

    async def initialize(self):
        """Initialize MCP server with Slack toolkit"""
        toolkit = await self.arcade.tools.list(toolkit="slack")
        self.mcp_server.register_tools(toolkit.items)
        return self.mcp_server

Building the LangGraph-Slack Integration

Creating the Slack Authentication Node

LangGraph agents operate through nodes and edges. Create a specialized authentication node that handles Slack OAuth flows before executing workspace operations.

from langraph.graph import Graph, Node
from langraph.checkpoint import MemorySaver
from arcadepy import Arcade
from typing import Dict, Any

class SlackAuthNode(Node):
    def __init__(self):
        super().__init__(name="slack_auth")
        self.arcade = Arcade(api_key=os.getenv("ARCADE_API_KEY"))
        self.authenticated_users = {}

    async def execute(self, state: Dict[str, Any]) -> Dict[str, Any]:
        user_id = state.get("user_id")

        # Check if Slack.PostMessage requires authorization
        auth_response = await self.arcade.tools.authorize(
            tool_name="Slack.PostMessage",
            user_id=user_id
        )

        if auth_response.status != "completed":
            # User needs OAuth flow
            state["requires_auth"] = True
            state["auth_url"] = auth_response.url
            return state

        # Store authenticated status
        await self.arcade.auth.wait_for_completion(auth_response)
        self.authenticated_users[user_id] = True
        state["slack_authenticated"] = True

        return state

Implementing Slack Action Nodes

With authentication handled, create nodes for specific Slack operations. These nodes leverage Arcade's pre-built Slack toolkit to execute workspace actions.

class SlackMessageNode(Node):
    def __init__(self):
        super().__init__(name="send_slack_message")
        self.arcade = Arcade(api_key=os.getenv("ARCADE_API_KEY"))

    async def execute(self, state: Dict[str, Any]) -> Dict[str, Any]:
        if not state.get("slack_authenticated"):
            state["error"] = "Authentication required"
            return state

        # Execute Slack action with user context
        response = await self.arcade.tools.execute(
            tool_name="Slack.PostMessage",
            input={
                "channel": state.get("channel"),
                "text": state.get("message"),
                "thread_ts": state.get("thread_ts")  # Optional threading
            },
            user_id=state.get("user_id")
        )

        state["message_sent"] = True
        state["message_ts"] = response.output.get("ts")

        return state

class SlackChannelNode(Node):
    def __init__(self):
        super().__init__(name="manage_channels")
        self.arcade = Arcade(api_key=os.getenv("ARCADE_API_KEY"))

    async def execute(self, state: Dict[str, Any]) -> Dict[str, Any]:
        action = state.get("channel_action")

        tool_map = {
            "create": "Slack.CreateChannel",
            "archive": "Slack.ArchiveChannel",
            "list": "Slack.ListChannels",
            "invite": "Slack.InviteToChannel"
        }

        tool_name = tool_map.get(action)
        if not tool_name:
            state["error"] = f"Unknown action: {action}"
            return state

        response = await self.arcade.tools.execute(
            tool_name=tool_name,
            input=state.get("channel_params", {}),
            user_id=state.get("user_id")
        )

        state["channel_result"] = response.output
        return state

Constructing the LangGraph Workflow

Combine authentication and action nodes into a complete LangGraph workflow that handles the full lifecycle of Slack interactions.

from langraph.graph import Graph, END
from langraph.prebuilt import ToolExecutor

class SlackLangGraph:
    def __init__(self):
        self.graph = Graph()

        # Initialize nodes
        self.auth_node = SlackAuthNode()
        self.message_node = SlackMessageNode()
        self.channel_node = SlackChannelNode()

        # Build graph structure
        self._build_graph()

    def _build_graph(self):
        # Add nodes to graph
        self.graph.add_node("authenticate", self.auth_node.execute)
        self.graph.add_node("send_message", self.message_node.execute)
        self.graph.add_node("manage_channel", self.channel_node.execute)

        # Define edges with conditional routing
        self.graph.add_edge("authenticate", self._route_after_auth)
        self.graph.add_edge("send_message", END)
        self.graph.add_edge("manage_channel", "send_message")

        # Set entry point
        self.graph.set_entry_point("authenticate")

    def _route_after_auth(self, state: Dict[str, Any]) -> str:
        """Route based on authentication status and requested action"""
        if state.get("requires_auth"):
            return END  # Exit for user to complete OAuth

        if state.get("channel_action"):
            return "manage_channel"
        return "send_message"

    async def run(self, input_state: Dict[str, Any]) -> Dict[str, Any]:
        """Execute the graph with given input"""
        checkpointer = MemorySaver()
        result = await self.graph.astream(
            input_state,
            {"configurable": {"thread_id": input_state.get("user_id")}}
        )

        return result

Handling Multi-User Authentication Flows

OAuth Flow Management

Implement proper OAuth callback handling to support multiple users authenticating simultaneously with their Slack workspaces.

from fastapi import FastAPI, Request
from fastapi.responses import JSONResponse
import asyncio

app = FastAPI()

# Store authorization states
AUTH_STATES = {}

@app.post("/api/slack/oauth/initiate")
async def initiate_slack_oauth(req: Request):
    body = await req.json()
    user_id = body.get("userId")

    arcade = Arcade(api_key=os.getenv("ARCADE_API_KEY"))

    # Initiate Slack OAuth
    auth_response = await arcade.tools.authorize(
        tool_name="Slack.PostMessage",
        user_id=user_id
    )

    # Store auth state for callback
    AUTH_STATES[user_id] = auth_response

    return JSONResponse({
        "authUrl": auth_response.url,
        "authId": auth_response.id
    })

@app.post("/api/slack/oauth/callback")
async def slack_oauth_callback(req: Request):
    body = await req.json()
    user_id = body.get("userId")

    # Retrieve stored auth response
    auth_response = AUTH_STATES.get(user_id)
    if not auth_response:
        return JSONResponse({"error": "Invalid auth state"}, status_code=400)

    arcade = Arcade(api_key=os.getenv("ARCADE_API_KEY"))

    # Wait for OAuth completion
    auth_result = await arcade.auth.wait_for_completion(auth_response)

    if auth_result.status == "completed":
        # Load Slack tools for authenticated user
        slack_tools = await arcade.tools.list(
            toolkit="slack",
            user_id=user_id
        )

        # Clean up auth state
        del AUTH_STATES[user_id]

        return JSONResponse({
            "success": True,
            "availableTools": [tool.name for tool in slack_tools.items]
        })

    return JSONResponse({"success": False, "error": "Authorization incomplete"})

User Context Isolation

Maintain strict isolation between different users' Slack contexts to prevent cross-contamination of workspace access.

class MultiUserSlackManager:
    def __init__(self):
        self.arcade = Arcade(api_key=os.getenv("ARCADE_API_KEY"))
        self.user_graphs: Dict[str, SlackLangGraph] = {}
        self.user_contexts: Dict[str, Dict] = {}

    async def get_user_graph(self, user_id: str) -> SlackLangGraph:
        """Get or create user-specific LangGraph instance"""
        if user_id not in self.user_graphs:
            graph = SlackLangGraph()
            self.user_graphs[user_id] = graph

            # Initialize user context
            self.user_contexts[user_id] = {
                "authenticated": False,
                "workspace_id": None,
                "permissions": []
            }

        return self.user_graphs[user_id]

    async def execute_slack_workflow(
        self,
        user_id: str,
        workflow_params: Dict[str, Any]
    ) -> Dict[str, Any]:
        """Execute workflow with proper user isolation"""

        # Get user-specific graph
        graph = await self.get_user_graph(user_id)

        # Inject user context
        workflow_params["user_id"] = user_id
        workflow_params["context"] = self.user_contexts.get(user_id)

        try:
            result = await graph.run(workflow_params)

            # Update user context based on results
            if result.get("slack_authenticated"):
                self.user_contexts[user_id]["authenticated"] = True

            return result

        except Exception as e:
            # Handle auth errors specifically
            if "authorization_required" in str(e):
                return {
                    "requiresAuth": True,
                    "message": "Please authenticate with Slack"
                }
            raise

Advanced Slack Operations with LangGraph

Thread Management and Conversations

Implement conversation threading to maintain context across multiple Slack messages within LangGraph workflows.

class SlackThreadManager:
    def __init__(self):
        self.arcade = Arcade(api_key=os.getenv("ARCADE_API_KEY"))
        self.thread_cache: Dict[str, str] = {}

    async def send_threaded_message(
        self,
        user_id: str,
        channel: str,
        message: str,
        thread_key: str = None
    ) -> Dict[str, Any]:
        """Send message with automatic thread management"""

        params = {
            "channel": channel,
            "text": message
        }

        # Add thread timestamp if continuing conversation
        if thread_key and thread_key in self.thread_cache:
            params["thread_ts"] = self.thread_cache[thread_key]

        response = await self.arcade.tools.execute(
            tool_name="Slack.PostMessage",
            input=params,
            user_id=user_id
        )

        # Cache thread timestamp for future messages
        if thread_key and not thread_key in self.thread_cache:
            self.thread_cache[thread_key] = response.output.get("ts")

        return response.output

    async def get_thread_history(
        self,
        user_id: str,
        channel: str,
        thread_ts: str
    ) -> list:
        """Retrieve conversation thread history"""

        response = await self.arcade.tools.execute(
            tool_name="Slack.GetConversationReplies",
            input={
                "channel": channel,
                "ts": thread_ts
            },
            user_id=user_id
        )

        return response.output.get("messages", [])

File Operations and Attachments

Enable your LangGraph agents to share files and handle attachments in Slack conversations.

class SlackFileHandler:
    def __init__(self):
        self.arcade = Arcade(api_key=os.getenv("ARCADE_API_KEY"))

    async def upload_file(
        self,
        user_id: str,
        channels: list,
        file_content: bytes,
        filename: str,
        title: str = None,
        initial_comment: str = None
    ) -> Dict[str, Any]:
        """Upload file to Slack channels"""

        response = await self.arcade.tools.execute(
            tool_name="Slack.UploadFile",
            input={
                "channels": ",".join(channels),
                "file": file_content,
                "filename": filename,
                "title": title or filename,
                "initial_comment": initial_comment
            },
            user_id=user_id
        )

        return response.output

    async def share_file_in_thread(
        self,
        user_id: str,
        channel: str,
        thread_ts: str,
        file_id: str,
        comment: str = None
    ) -> Dict[str, Any]:
        """Share existing file in thread"""

        response = await self.arcade.tools.execute(
            tool_name="Slack.ShareFile",
            input={
                "channel": channel,
                "thread_ts": thread_ts,
                "file": file_id,
                "comment": comment
            },
            user_id=user_id
        )

        return response.output

Production Deployment Patterns

Scaling Considerations

Deploy the LangGraph-Slack integration with proper resource management and scaling configurations for production workloads.

# kubernetes/langgraph-slack-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: langgraph-slack-mcp
spec:
  replicas: 3
  selector:
    matchLabels:
      app: langgraph-slack
  template:
    metadata:
      labels:
        app: langgraph-slack
    spec:
      containers:
        - name: arcade-mcp-server
          image: ghcr.io/arcadeai/engine:latest
          env:
            - name: ARCADE_API_KEY
              valueFrom:
                secretKeyRef:
                  name: arcade-secrets
                  key: api-key
            - name: SLACK_CLIENT_ID
              valueFrom:
                secretKeyRef:
                  name: slack-oauth
                  key: client-id
            - name: SLACK_CLIENT_SECRET
              valueFrom:
                secretKeyRef:
                  name: slack-oauth
                  key: client-secret
          ports:
            - containerPort: 9099
          resources:
            requests:
              memory: "1Gi"
              cpu: "1000m"
            limits:
              memory: "2Gi"
              cpu: "2000m"
          livenessProbe:
            httpGet:
              path: /health
              port: 9099
            periodSeconds: 30
          readinessProbe:
            httpGet:
              path: /ready
              port: 9099
            periodSeconds: 10

Error Handling and Recovery

Implement comprehensive error handling for Slack API rate limits and connection issues within LangGraph workflows.

import asyncio
from typing import Optional

class SlackErrorHandler:
    def __init__(self):
        self.retry_delays = {
            "rate_limit": 60,
            "connection_error": 5,
            "auth_error": 0
        }

    async def handle_slack_error(
        self,
        error: Exception,
        user_id: str,
        operation: str
    ) -> Optional[Dict[str, Any]]:
        """Handle Slack-specific errors with retry logic"""

        error_type = self._classify_error(error)

        if error_type == "rate_limit":
            # Implement exponential backoff
            delay = self.retry_delays["rate_limit"]
            await asyncio.sleep(delay)
            return {"retry": True, "delay": delay}

        elif error_type == "auth_error":
            # Trigger re-authentication
            arcade = Arcade(api_key=os.getenv("ARCADE_API_KEY"))
            auth_response = await arcade.tools.authorize(
                tool_name=f"Slack.{operation}",
                user_id=user_id
            )
            return {
                "requiresAuth": True,
                "authUrl": auth_response.url
            }

        elif error_type == "connection_error":
            # Retry with shorter delay
            delay = self.retry_delays["connection_error"]
            await asyncio.sleep(delay)
            return {"retry": True, "delay": delay}

        # Unknown error, propagate
        raise error

    def _classify_error(self, error: Exception) -> str:
        error_str = str(error).lower()

        if "rate" in error_str or "limit" in error_str:
            return "rate_limit"
        elif "auth" in error_str or "token" in error_str:
            return "auth_error"
        elif "connection" in error_str or "timeout" in error_str:
            return "connection_error"

        return "unknown"

Monitoring and Observability

Track LangGraph-Slack integration health with comprehensive metrics and logging.

import logging
from datetime import datetime

class SlackIntegrationMonitor:
    def __init__(self):
        self.logger = logging.getLogger("langgraph.slack")
        self.metrics = {
            "messages_sent": 0,
            "auth_flows_completed": 0,
            "graph_executions": 0,
            "errors": 0
        }

    async def track_operation(
        self,
        operation: str,
        user_id: str,
        success: bool,
        latency_ms: int = None
    ):
        """Track Slack operation metrics"""

        self.metrics[f"{operation}_total"] = \
            self.metrics.get(f"{operation}_total", 0) + 1

        if not success:
            self.metrics["errors"] += 1

        log_entry = {
            "timestamp": datetime.utcnow().isoformat(),
            "operation": operation,
            "user_id": self._hash_user_id(user_id),
            "success": success,
            "latency_ms": latency_ms
        }

        self.logger.info(f"Slack operation: {log_entry}")

    def get_health_status(self) -> Dict[str, Any]:
        """Generate health report for monitoring"""

        error_rate = (
            self.metrics["errors"] /
            max(self.metrics.get("graph_executions", 1), 1)
        )

        return {
            "status": "healthy" if error_rate < 0.05 else "degraded",
            "metrics": self.metrics,
            "error_rate": error_rate,
            "timestamp": datetime.utcnow().isoformat()
        }

    def _hash_user_id(self, user_id: str) -> str:
        """Hash user ID for privacy"""
        import hashlib
        return hashlib.sha256(user_id.encode()).hexdigest()[:8]

Security Best Practices

Token Security and Management

Protect Slack tokens and credentials throughout the LangGraph workflow lifecycle.

class SecureTokenManager:
    def __init__(self, encryption_key: bytes):
        from cryptography.fernet import Fernet
        self.cipher = Fernet(encryption_key)
        self.token_store: Dict[str, bytes] = {}

    def store_slack_token(self, user_id: str, token: str):
        """Store encrypted Slack token"""
        encrypted = self.cipher.encrypt(token.encode())
        self.token_store[user_id] = encrypted

        # Never log tokens
        logging.info(f"Token stored for user: {user_id[:4]}***")

    def get_slack_token(self, user_id: str) -> Optional[str]:
        """Retrieve and decrypt Slack token"""
        encrypted = self.token_store.get(user_id)
        if not encrypted:
            return None

        return self.cipher.decrypt(encrypted).decode()

    def revoke_token(self, user_id: str):
        """Remove token from storage"""
        if user_id in self.token_store:
            del self.token_store[user_id]
            logging.info(f"Token revoked for user: {user_id[:4]}***")

Workspace Isolation

Ensure complete isolation between different Slack workspaces when handling multi-tenant scenarios.

class WorkspaceIsolationManager:
    def __init__(self):
        self.workspace_contexts: Dict[str, Dict] = {}
        self.arcade = Arcade(api_key=os.getenv("ARCADE_API_KEY"))

    async def validate_workspace_access(
        self,
        user_id: str,
        workspace_id: str,
        requested_channel: str
    ) -> bool:
        """Validate user has access to workspace and channel"""

        # Get user's available workspaces
        response = await self.arcade.tools.execute(
            tool_name="Slack.GetWorkspaces",
            input={},
            user_id=user_id
        )

        user_workspaces = response.output.get("workspaces", [])

        # Verify workspace access
        if workspace_id not in [w["id"] for w in user_workspaces]:
            logging.warning(
                f"Unauthorized workspace access attempt: {user_id[:4]}***"
            )
            return False

        # Verify channel membership
        channels_response = await self.arcade.tools.execute(
            tool_name="Slack.ListChannels",
            input={"workspace": workspace_id},
            user_id=user_id
        )

        user_channels = channels_response.output.get("channels", [])

        if requested_channel not in [c["id"] for c in user_channels]:
            logging.warning(
                f"Unauthorized channel access attempt: {user_id[:4]}***"
            )
            return False

        return True

Testing and Validation

Integration Testing Patterns

Create comprehensive tests for your LangGraph-Slack integration to ensure reliability across different scenarios.

import pytest
from unittest.mock import AsyncMock, patch

class TestSlackLangGraphIntegration:
    @pytest.fixture
    async def mock_arcade(self):
        with patch('arcadepy.Arcade') as mock:
            arcade_instance = AsyncMock()
            mock.return_value = arcade_instance
            yield arcade_instance

    @pytest.mark.asyncio
    async def test_authentication_flow(self, mock_arcade):
        """Test Slack OAuth authentication flow"""

        # Setup mock responses
        mock_arcade.tools.authorize.return_value = AsyncMock(
            status="pending",
            url="https://slack.com/oauth/authorize",
            id="auth_123"
        )

        # Initialize graph
        graph = SlackLangGraph()

        # Execute authentication
        result = await graph.run({
            "user_id": "test_user",
            "channel": "#general",
            "message": "Test message"
        })

        # Verify authentication was triggered
        assert result.get("requires_auth") == True
        assert "slack.com/oauth" in result.get("auth_url", "")

        mock_arcade.tools.authorize.assert_called_once_with(
            tool_name="Slack.PostMessage",
            user_id="test_user"
        )

    @pytest.mark.asyncio
    async def test_message_sending(self, mock_arcade):
        """Test successful message sending flow"""

        # Mock authenticated state
        mock_arcade.tools.execute.return_value = AsyncMock(
            output={
                "ok": True,
                "ts": "1234567890.123456",
                "channel": "C1234567890"
            }
        )

        manager = MultiUserSlackManager()

        # Simulate authenticated user
        manager.user_contexts["test_user"] = {"authenticated": True}

        result = await manager.execute_slack_workflow(
            "test_user",
            {
                "channel": "#general",
                "message": "Hello from LangGraph"
            }
        )

        assert result.get("message_sent") == True
        assert "ts" in result.get("message_ts", "")

Conclusion

Connecting LangGraph to Slack through Arcade's MCP implementation provides a production-ready solution for building AI agents that interact with Slack workspaces. The combination of LangGraph's orchestration capabilities with Arcade's secure authentication layer enables advanced workflows while maintaining enterprise-grade security and multi-user support.

The patterns demonstrated here—from OAuth flow management to workspace isolation and error handling—form the foundation for scalable Slack integrations. By leveraging Arcade's pre-built Slack toolkit and MCP server architecture, development teams can focus on agent logic rather than authentication overhead.

For additional implementation details and advanced patterns, explore the Arcade documentation and API reference. The GitHub repository contains example implementations and starter templates for accelerating your LangGraph-Slack integration development.

SHARE THIS POST

RECENT ARTICLES

Rays decoration image
THOUGHT LEADERSHIP

How to Use MCP with LangGraph through Arcade

Model Context Protocol (MCP) standardizes how AI models interact with tools and external systems. LangGraph enables building stateful, graph-based AI workflows. When combined through Arcade's authentication-first platform, developers can build production-ready AI agents that actually take actions—not just suggest them. This guide shows you exactly how to integrate MCP with LangGraph using Arcade's infrastructure, solving the critical authentication challenges that prevent most AI projects from

Rays decoration image
THOUGHT LEADERSHIP

How to Set Up Multi-User Authentication with MCP for Gmail

Multi-user authentication represents one of the most challenging aspects of deploying AI agents in production. This guide demonstrates how to implement secure, scalable multi-user Gmail authentication using Arcade.dev’s Model Context Protocol (MCP) support, enabling AI agents to access Gmail on behalf of multiple users simultaneously. The authentication gap in MCP servers Model Context Protocol emerged as a standard for AI-tool interaction, but most open-source MCP servers default to single

Rays decoration image
THOUGHT LEADERSHIP

How to Query Postgres from LangGraph via Arcade (MCP)

Building AI agents that interact with databases presents significant technical challenges. Authentication, connection management, and secure query execution often become roadblocks that prevent agents from reaching production. This guide shows you how to leverage Arcade's Model Context Protocol (MCP) support to connect LangGraph agents with Postgres databases, enabling secure database operations without managing advanced infrastructure. The Database Integration Challenge for AI Agents Tradi

Blog CTA Icon

Get early access to Arcade, and start building now.