Build a Slack Agent
Build an AI-powered Slack bot that uses MCP servers authenticated through Keycard
A Slack bot that just echoes commands is a toy. A Slack bot that can pull your GitHub PRs, check your calendar, and search your Drive — as you — is an agent. The difference is authentication: every API call must be scoped to the user who asked the question, not a shared service account.
Keycard handles that authentication. You connect MCP servers to Keycard, and each Slack user authorizes their own accounts through OAuth. When the agent needs to call an API, Keycard exchanges the user’s token for an API-specific credential — automatically, per-user, with full audit logging.
This guide builds an AI-powered Slack bot using the Keycard Python SDK, OpenAI Agents SDK, and Slack Bolt. The bot uses natural language — no slash commands — and requests authorization inline when it encounters a service the user hasn’t connected yet.
What You Will Build
Section titled “What You Will Build”- A Slack bot that responds to mentions and DMs with natural language
- OpenAI agent that calls MCP tools on behalf of each user
- Per-user OAuth via Keycard — auth links appear directly in the Slack thread
- Persistent token storage so users only authorize once
Prerequisites
Section titled “Prerequisites”- Python 3.10+
- Keycard account with a configured zone
- Slack workspace where you can create apps
- OpenAI API key
- At least one deployed MCP server protected by Keycard (see Build an MCP Server and Third-Party Tools)
Architecture
Section titled “Architecture”flowchart TD
A[Slack User] --> B[Slack Bot<br/>Bolt + Starlette]
B --> C[OpenAI Agent]
C --> D[GitHub MCP]
C --> E[Google MCP]
C --> F[... MCP]
D --> G[Keycard<br/>OAuth + Token Exchange]
E --> G
F --> GThe Slack bot receives a message, passes it to the OpenAI agent, and the agent decides which MCP tools to call. Each MCP server is authenticated through Keycard — the agent sees only the tools the user has authorized. If a service needs authorization, the agent sends an auth link directly in the Slack thread.
Create Slack App
Section titled “Create Slack App”-
Create a new Slack app
- Go to api.slack.com/apps
- Click Create New App → From scratch
- Name it (e.g.,
Keycard Agent) and select your workspace - Click Create App
-
Add bot scopes
Navigate to OAuth & Permissions → Bot Token Scopes:
app_mentions:readRead messages mentioning the bot chat:writeSend messages im:historyRead DM history im:writeSend direct messages -
Enable Socket Mode
Navigate to Socket Mode in the sidebar and toggle Enable Socket Mode to On. Generate an App-Level Token with the
connections:writescope. Copy this token (starts withxapp-). -
Enable events
Navigate to Event Subscriptions → toggle Enable Events to On:
Subscribe to bot events app_mention,message.im -
Install to workspace
- Navigate to Install App → Install to Workspace
- Review permissions and click Allow
- Copy the Bot User OAuth Token (starts with
xoxb-)
-
Copy credentials
Navigate to Basic Information and copy:
- Signing Secret
Configure Keycard
Section titled “Configure Keycard”-
Ensure your MCP servers are registered
Your MCP servers should already be registered as resources in Keycard. If not, follow the Build an MCP Server and Third-Party Tools guides.
-
Create an Application for the Slack bot
In Keycard Console, navigate to Applications → Create Application:
Application Name Slack AgentDependencies Select your MCP server resources (e.g., GitHub MCP, Google MCP) -
Generate client credentials
On the application detail page, generate Client Credentials (Client ID + Client Secret). Save these — you’ll need them in the next step.
-
Add the OAuth redirect URL
On the application detail page, go to Redirect URLs and add your bot’s callback URL:
- Local development:
http://localhost:8080/oauth/callback - Production:
https://your-bot.example.com/oauth/callback
This must match the
redirect_uriinagent.py. Keycard validates the redirect URI during OAuth — if it doesn’t match, the flow will fail. - Local development:
Set Up the Project
Section titled “Set Up the Project”-
Create project directory
Terminal window mkdir slack-agent && cd slack-agentpython -m venv .venv && source .venv/bin/activate -
Install dependencies
Terminal window pip install keycardai-mcp slack-bolt openai-agents python-dotenv uvicorn -
Create
.envTerminal window # SlackSLACK_BOT_TOKEN=xoxb-your-bot-tokenSLACK_SIGNING_SECRET=your-signing-secretSLACK_APP_TOKEN=xapp-your-app-token# OpenAIOPENAI_API_KEY=sk-your-openai-key# KeycardKEYCARD_ZONE_URL=https://your-zone.keycard.cloudKEYCARD_CLIENT_ID=your-agent-client-idKEYCARD_CLIENT_SECRET=your-agent-client-secret# Keycard MCP ServersGITHUB_MCP_URL=https://your-github-mcp.example.com/mcpGOOGLE_MCP_URL=https://your-google-mcp.example.com/mcp# App (HTTP server for OAuth callback)PORT=8080 -
Create project structure
slack-agent/├── .env└── src/└── slack_agent/├── __init__.py├── config.py├── agent.py└── bot.py
Key SDK Classes
Section titled “Key SDK Classes”Before looking at the code, here are the Keycard SDK classes that power the integration:
ClientManager— manages per-user MCP connections, keyed byslack:{user_id}StarletteAuthCoordinator— handles OAuth callbacks at/oauth/callbackSQLiteBackend— persists tokens across restarts so users only authorize onceSlackAuthToolHandler— sends auth links directly in the user’s Slack threadOpenAIAgentsClient— wraps everything for OpenAI Agents:get_system_prompt()adds auth awareness,get_mcp_servers()returns only authenticated servers,get_auth_tools()provides arequest_authenticationtool
Write the Code
Section titled “Write the Code”config.py — Configuration
Section titled “config.py — Configuration”import os
from dotenv import load_dotenv
load_dotenv()
SLACK_BOT_TOKEN = os.environ["SLACK_BOT_TOKEN"]SLACK_SIGNING_SECRET = os.environ["SLACK_SIGNING_SECRET"]SLACK_APP_TOKEN = os.environ["SLACK_APP_TOKEN"]OPENAI_API_KEY = os.environ["OPENAI_API_KEY"]KEYCARD_ZONE_URL = os.environ["KEYCARD_ZONE_URL"]KEYCARD_CLIENT_ID = os.environ["KEYCARD_CLIENT_ID"]KEYCARD_CLIENT_SECRET = os.environ["KEYCARD_CLIENT_SECRET"]PORT = int(os.getenv("PORT", "3000"))
MCP_SERVERS = {}
if os.getenv("GITHUB_MCP_URL"): MCP_SERVERS["github"] = { "url": os.environ["GITHUB_MCP_URL"], "transport": "streamable-http", "auth": {"type": "oauth"}, }
if os.getenv("GOOGLE_MCP_URL"): MCP_SERVERS["google"] = { "url": os.environ["GOOGLE_MCP_URL"], "transport": "streamable-http", "auth": {"type": "oauth"}, }agent.py — AI Agent with MCP Tools
Section titled “agent.py — AI Agent with MCP Tools”This is the core of the bot. It uses the Keycard SDK to manage per-user MCP connections and the OpenAI Agents SDK to run the agent.
from agents import Agent, Runnerfrom slack_sdk.web.async_client import AsyncWebClient
from keycardai.mcp.client import ClientManager, SQLiteBackend, StarletteAuthCoordinatorfrom keycardai.mcp.client.integrations.auth_tools import SlackAuthToolHandlerfrom keycardai.mcp.client.integrations.openai_agents import OpenAIAgentsClient
from .config import PORT, MCP_SERVERS
storage = SQLiteBackend("tokens.db")coordinator = StarletteAuthCoordinator( backend=storage, redirect_uri=f"http://localhost:{PORT}/oauth/callback",)client_manager = ClientManager( servers=MCP_SERVERS, storage_backend=storage, auth_coordinator=coordinator,)
AGENT_INSTRUCTIONS = ( "You are a helpful Slack assistant. Use your MCP tools to answer questions " "about the user's GitHub repos, calendar, Drive files, and other connected services. " "Be concise. Format responses for Slack using *bold*, _italic_, and bullet lists.")
async def handle_message( text: str, user_id: str, channel_id: str, thread_ts: str, slack_client: AsyncWebClient,) -> str: """Process a Slack message through the AI agent with MCP tools.""" mcp_client = await client_manager.get_client(context_id=f"slack:{user_id}")
auth_handler = SlackAuthToolHandler( slack_client=slack_client, channel_id=channel_id, thread_ts=thread_ts, )
async with OpenAIAgentsClient(mcp_client, auth_tool_handler=auth_handler) as openai_client: agent = Agent( name="slack_agent", instructions=openai_client.get_system_prompt(AGENT_INSTRUCTIONS), mcp_servers=openai_client.get_mcp_servers(), tools=openai_client.get_auth_tools(), )
result = await Runner.run(agent, text) return result.final_outputbot.py — Slack Bot Server
Section titled “bot.py — Slack Bot Server”import asyncio
import uvicornfrom slack_bolt.adapter.socket_mode.async_handler import AsyncSocketModeHandlerfrom slack_bolt.async_app import AsyncAppfrom starlette.applications import Starlettefrom starlette.responses import JSONResponsefrom starlette.routing import Route
from .agent import coordinator, handle_messagefrom .config import PORT, SLACK_APP_TOKEN, SLACK_BOT_TOKEN, SLACK_SIGNING_SECRET
app = AsyncApp(token=SLACK_BOT_TOKEN, signing_secret=SLACK_SIGNING_SECRET)
@app.event("app_mention")async def handle_mention(event, say, client): """Respond when mentioned in a channel.""" thread_ts = event.get("thread_ts") or event["ts"] await say(text="Looking into that...", thread_ts=thread_ts) response = await handle_message( text=event["text"], user_id=event["user"], channel_id=event["channel"], thread_ts=thread_ts, slack_client=client, ) await say(text=response, thread_ts=thread_ts)
@app.event("message")async def handle_dm(event, say, client): """Respond to direct messages.""" if event.get("subtype") or event.get("bot_id"): return response = await handle_message( text=event["text"], user_id=event["user"], channel_id=event["channel"], thread_ts=event["ts"], slack_client=client, ) await say(text=response, thread_ts=event["ts"])
starlette_app = Starlette(routes=[ Route("/oauth/callback", coordinator.get_completion_endpoint()), Route("/health", lambda req: JSONResponse({"status": "ok"})),])
async def main(): socket_handler = AsyncSocketModeHandler(app, SLACK_APP_TOKEN) config = uvicorn.Config(starlette_app, host="0.0.0.0", port=PORT) server = uvicorn.Server(config) await asyncio.gather(socket_handler.start_async(), server.serve())
if __name__ == "__main__": asyncio.run(main())The bot uses Socket Mode for Slack events (no public URL needed) and runs a lightweight Starlette server for the Keycard OAuth callback.
Run and Test
Section titled “Run and Test”-
Start the bot
Terminal window python -m slack_agent.bot -
Test in Slack
- Invite the bot to a channel:
/invite @Keycard Agent - Mention the bot:
@Keycard Agent what are my open PRs on GitHub? - The bot detects GitHub needs authorization and posts an auth link in the thread
- Click the link and complete the OAuth flow
- Ask again:
@Keycard Agent what are my open PRs on GitHub? - The bot calls the GitHub MCP server and returns your PRs
- Invite the bot to a channel:
-
Verify in Keycard Console
Check Audit Logs for the authentication and token exchange events:
users:authenticateThe Slack user authenticated via Keycard users:authorizeAccess was authorized to the MCP server credentials:issueAPI-specific token was issued via exchange Every tool call traces back to the specific Slack user — not the bot’s service account.
How It Works
Section titled “How It Works”- User sends
@Keycard Agent show my calendar for todayin Slack bot.pyreceives the event and callshandle_message()agent.pygets (or creates) an MCP client for this user viaClientManagerOpenAIAgentsClientconnects to all configured MCP servers and detects auth status- If Google MCP needs auth: the agent calls
request_authentication(service="google") SlackAuthToolHandlerposts an auth link directly in the Slack thread- The user clicks the link and completes OAuth with Keycard
- The coordinator stores the token — on the next message, the agent has access
Deploy to Production
Section titled “Deploy to Production”When deploying:
- Update the
redirect_uriinagent.pyto your production URL (must be HTTPS) - Set environment variables securely in your hosting platform
- Update Keycard resource identifiers if your MCP server URLs changed
- Consider switching to HTTP mode for production — Socket Mode is ideal for development, but HTTP event delivery scales better with load balancers
Troubleshooting
Section titled “Troubleshooting”Bot Not Responding
Section titled “Bot Not Responding”Cause: Event subscriptions not configured or bot not in channel.
Solution:
- Verify the Request URL is set and verified in Slack
- Invite the bot to the channel:
/invite @Keycard Agent - Confirm the bot has
app_mentions:readscope
Auth Link Not Working
Section titled “Auth Link Not Working”Cause: The OAuth callback endpoint is not reachable from the browser.
Solution:
- Verify the bot is running and the
/oauth/callbackendpoint is reachable athttp://localhost:{PORT}/oauth/callback - For production, update the
redirect_uriinagent.pyto your production URL
MCP Tools Not Appearing After Auth
Section titled “MCP Tools Not Appearing After Auth”Cause: MCP server resource not listed as a dependency on the Application.
Solution:
- In Keycard Console, check that your Application has the MCP server resources as Dependencies
- Verify the MCP server is running and reachable
Token Exchange Fails
Section titled “Token Exchange Fails”Cause: Application credentials misconfigured or missing dependency.
Solution:
- Verify Client ID and Client Secret are correct
- Check that the MCP server’s Application has the external API as a dependency
- Review Audit Logs in Keycard Console for error details