Skip to content
API Reference
Guides
Build Agents

Build a Slack agent

Build a Slack bot that answers questions from each user's Google Calendar through a Keycard-protected MCP server, with per-user authorization instead of a shared service-account token.

In this tutorial, you’ll build a Slack bot that answers questions like “what’s on my calendar today?” from each user’s own Google Calendar. The agent connects to a Keycard-protected MCP server; each Slack user authorizes once, and every downstream call runs as that user with a short-lived, scoped credential.

Most Slack agents authenticate to their downstream tools with a single shared service-account token, then try to re-implement each user’s permissions in the application layer. That mirror is expensive to build, easy to get wrong, and drifts the moment the upstream tool’s permissions change. Here, Keycard removes the mirror: the upstream system enforces each user’s real permissions, there are no long-lived downstream secrets, and every authorization is logged.

Slack UserMentions and DMs
Slack AgentBolt + LLM loop + MCP client
KeycardPer-user auth + token exchange
Google MCPCalendar tools via Google API

There are two authorization hand-offs, and Keycard sits under both:

  • User to MCP server: the agent is an MCP client. On first use, each Slack user authorizes through the standard MCP authorization flow (OAuth 2.0 with PKCE) against Keycard, and the agent holds a per-user MCP session. The agent needs no Keycard secret for this; it registers as a public client.
  • MCP server to Google: the MCP server authenticates to Keycard with its own application credential (client ID + secret to start) and exchanges each incoming user token for a Google API token scoped to that user. The agent never sees a Google credential.

By the end of this tutorial, you’ll have:

  • Google connected to Keycard as a credential provider
  • A Keycard-protected Google MCP server running locally with Calendar tools
  • A Slack bot (in Python or TypeScript, your pick) that answers calendar questions as the requesting user
  • An audit trail in Keycard Console tracing every access to the specific Slack user

Before starting, you’ll need:

  • A Keycard account. Your Keycard URL and Redirect URL are under ConfigurationConnection (you’ll need them throughout)
  • A Slack workspace where you can create and install apps
  • A Google Cloud account with permission to create OAuth clients
  • An Anthropic API key (the agent’s LLM loop uses Claude)
  • uv installed (or Docker, both repos ship a Dockerfile)
  • Clone the tutorial repository (it contains both services):
    Terminal window
    git clone https://github.com/keycardai/tutorial-slack-agent.git
    cd tutorial-slack-agent

First, create a Google OAuth client and register it with Keycard so Keycard can broker Google credentials for your users.

  1. Copy your Keycard redirect URL

    In Keycard Console, copy the Redirect URL from ConfigurationConnection tab → OAuth.

  2. Create a Google OAuth client

    In Google Cloud Console, open APIs & ServicesCredentials (the APIs & Services section is under the ☰ menu in the top-left). Create an OAuth 2.0 Client ID (application type: Web application). Add the Keycard redirect URL from step 1 to Authorized redirect URIs, then note the Client ID and Client Secret.

    Also enable the Calendar API: APIs & ServicesEnabled APIs & services+ Enable APIs and services, search Google Calendar API, open it, and click Enable.

  3. Add Google Calendar from the Keycard catalog

    In Keycard Console, open Resources in the sidebar, click the dropdown arrow on the Add Resource button, and choose Explore Resources. Search for Google Calendar, select it, and fill in the Google Client ID and Client Secret from step 2. This creates the Google OAuth provider and the Google Calendar API resource (identifier https://www.googleapis.com/calendar/v3).

  4. Confirm the Calendar scope on the resource

    Open the Google Calendar resource and check its Scopes. Reading a calendar needs https://www.googleapis.com/auth/calendar.readonly; if you want the create/update/delete tools too, use https://www.googleapis.com/auth/calendar. Without a scope the token exchange returns a credential Google rejects, so add it here if the catalog did not.

Part 2: Register the MCP server in Keycard

Section titled “Part 2: Register the MCP server in Keycard”

Now register the MCP server itself. It shows up in Keycard as two linked things, and it helps to know why:

  • a resource, the thing users authorize and get a token for (the MCP endpoint), and
  • an application, the identity the server uses to call other resources on a user’s behalf (here, Google).

You register both and link them on one application: the application provides the MCP resource and depends on the Google resource. That pairing is what lets the server exchange a user’s MCP token for a Google token, the two hand-offs in the diagram below.

flowchart LR
    user([Slack user])
    subgraph app [Application: Google MCP Server]
        mcpres[Provides: Google MCP resource<br/>http://localhost:8000/mcp]
        dep[Depends on: Google Calendar<br/>https://www.googleapis.com/calendar/v3]
    end
    google([Google Calendar API])
    user -->|1. authorizes and gets a token for| mcpres
    mcpres -.->|2. token exchange| dep
    dep -->|3. scoped Google token| google
  1. Create the Google MCP resource

    Navigate to Resources. Click the dropdown arrow on the Add Resource button and choose Add Manually.

    FieldValue
    Resource NameGoogle MCP Server
    Resource Identifierhttp://localhost:8000/mcp
    Credential ProviderZone Provider
  2. Create the Google MCP application

    Navigate to Applications and click Add Application (this button goes straight to the form, no dropdown).

    FieldValue
    NameGoogle MCP Server
    Identifierhttp://localhost:8000/mcp

    Click Create Application. You land on the application’s details page, on the Dependencies tab.

    • On the Dependencies tab, click Add dependency and select the Google Calendar API resource.
    • On the Provides tab, click Add provided resource and select Google MCP Server.
  3. Generate client credentials

    On the application details page, go to Application Credentials and click Add Credential, selecting Client ID & Secret.

  4. Confirm access policy

    Navigate to Policies and make sure an active policy set includes default-app-delegation (the preconfigured policy that lets applications exchange tokens on behalf of users) and permits your users to access the two resources. If you have no active policy set yet, create one following the pattern in Access Policies.

In the google-mcp-server/ directory of the tutorial repository:

  1. Configure the environment

    Terminal window
    cd google-mcp-server
    cp .env.example .env

    Fill in:

    Terminal window
    KEYCARD_ISSUER=https://<keycard-id>.keycard.cloud
    KEYCARD_CLIENT_ID=<client-id-from-part-2>
    KEYCARD_CLIENT_SECRET=<client-secret-from-part-2>
    MCP_SERVER_URL=http://localhost:8000/
    GOOGLE_API_RESOURCE=https://www.googleapis.com/calendar/v3

    Leave MCP_SERVER_URL as the base URL http://localhost:8000/: the server appends mcp itself, so it advertises the http://localhost:8000/mcp resource you registered in Part 2. Trailing slashes are significant in these identifiers; copy them exactly as written.

  2. Start the server

    Terminal window
    uv sync
    uv run python -m google_mcp_server

    The MCP endpoint is now at http://localhost:8000/mcp.

Part 4: Create the Slack app and run the agent

Section titled “Part 4: Create the Slack app and run the agent”

The repository ships the same agent twice: agent-python/ builds on the Keycard MCP client (keycardai-mcp), and agent-typescript/ builds on the official MCP SDK wired through @keycardai/mcp’s OAuth client provider. Pick the language you’d build in; everything else in the tutorial is identical. In a second terminal:

  1. Create the Slack app from the manifest

    Go to api.slack.com/appsCreate New AppFrom a manifest, pick your workspace, and paste the contents of manifest.json from the agent directory you chose (agent-python/manifest.json or agent-typescript/manifest.json; the two are identical). The manifest enables Socket Mode (no public URL needed) with the bot scopes and events the agent uses.

    The manifest names the app Keycard Tutorial Agent. Rename it under display_information.name (and features.bot_user.display_name) before creating the app if you want a different name in your workspace, or if that name is already taken.

    After creating the app:

    • Under Install App, install it to your workspace and copy the Bot User OAuth Token (xoxb-...)
    • Under Basic InformationApp-Level Tokens, generate a token with the connections:write scope and copy it (xapp-...)
  2. Configure the environment

    Terminal window
    cd agent-python
    cp .env.example .env

    Fill in:

    Terminal window
    SLACK_BOT_TOKEN=<xoxb-token>
    SLACK_APP_TOKEN=<xapp-token>
    ANTHROPIC_API_KEY=<anthropic-api-key>
    MCP_SERVERS=[{"key":"google","url":"http://localhost:8000/mcp"}]

    The agent needs no Keycard credential: it registers with Keycard as a public MCP client via OAuth Dynamic Client Registration, and each user completes their own PKCE authorization.

  3. Start the agent

    Terminal window
    uv sync
    uv run slack-agent

    The agent connects to Slack over Socket Mode and serves the OAuth callback on http://localhost:3000 for the authorization flow. Run only one of the two variants (agent-python or agent-typescript) at a time: they connect to the same Slack app, so running both would answer every message twice.

  1. Ask for your calendar

    In Slack, DM the bot (or mention it in a channel it’s in):

    What's on my calendar today?

    Since you haven’t authorized yet, the bot replies with an authorization link.

  2. Authorize once

    Click the link. You’ll sign in to Keycard, then consent to the Google Calendar access (Google’s consent screen appears the first time, establishing trust between Keycard and Google). When the flow completes, the bot confirms the connection.

  3. Ask again

    What's on my calendar today?

    This time the agent lists your events. Each tool call carries a token scoped to you: another Slack user who asks gets their calendar after authorizing, and a user who never authorizes gets nothing.

  4. Verify in Keycard Console

    Open Audit Logs in Console. You should see entries for:

    users:authenticateThe Slack user authenticated via Keycard
    users:authorizeAccess was authorized to the resource
    credentials:issueA scoped Google token was issued via exchange

    Every entry traces to the specific user, not the agent’s service identity. That trace is the accountability artifact: proof that each action was authorized as the right person.

The MCP server pattern above is one integration mode. If your agent calls a provider API directly (no MCP server in between), exchange the requesting user’s Keycard access token (RFC 8693 token exchange) for a credential scoped to the target resource, and inject it into the call:

# pip install keycardai-oauth
import os
from keycardai.oauth import AsyncClient, BasicAuth
async def token_for(user_access_token: str, resource: str) -> str:
"""Exchange the requesting user's Keycard token for a downstream token."""
async with AsyncClient(
os.environ["KEYCARD_ISSUER"],
auth=BasicAuth(
os.environ["KEYCARD_CLIENT_ID"],
os.environ["KEYCARD_CLIENT_SECRET"],
),
) as client:
resp = await client.exchange_token(
subject_token=user_access_token,
subject_token_type="urn:ietf:params:oauth:token-type:access_token",
resource=resource,
)
return resp.access_token

Sometimes you’ve already mapped the Slack user to a Keycard user (for example via a verified email link) and want to act as them without a live user token in hand. The substitute-user exchange covers this: you assert the user identifier and Keycard issues a scoped credential for that user.

# AsyncClient.impersonate wraps the substitute-user exchange:
resp = await client.impersonate(
user_identifier=keycard_user_id,
resource="https://www.googleapis.com/calendar/v3",
)

Keycard does not prescribe how you build the agent. The tutorial repo uses Bolt in Socket Mode with a small Claude tool loop, but any reasonable choice works:

  • The LLM loop: Claude Agent SDK, OpenAI Agents, or a hand-rolled tool-use loop. Keycard sits underneath, at the point each tool call needs a credential.
  • Slack ingress: Socket Mode is the quickest start; HTTP event delivery scales better behind a load balancer.
  • Approval / human-in-the-loop: gating risky writes behind a confirmation is an application concern. Keycard’s policies are the hard enforcement floor; an approval gate is a softer layer you add on top.

You now have a Slack agent whose every downstream call is authorized as the requesting user. From here, you can:

  • Switch the MCP server to a workload identity to eliminate its client secret
  • Add more Keycard-protected MCP servers to MCP_SERVERS (each gets its own per-user authorization)
  • Broaden the Google resource (Drive, Gmail, Docs, Sheets tools ship in the server) by registering the matching API resources
  • Deploy both services anywhere that runs a container; update MCP_SERVER_URL, the resource identifiers, and the agent’s BASE_URL to the public URLs
  • Set up audit log export to stream the authorization trail into your SIEM