---
title: "Connecting an MCP server to the Slackbot MCP Client"
summary: "The Slackbot MCP Client allows developers to connect remote MCP servers via HTTP to Slack. Once connected, Slackbot will automatically discover your MCP server's tools and invoke them based on user prompts in conversation."
url: "https://docs.slack.dev/ai/slackbot-mcp-client"
type: "guide"
---

Source: https://docs.slack.dev/ai/slackbot-mcp-client

# Connecting an MCP server to the Slackbot MCP Client

The Slackbot MCP Client allows developers to connect remote [MCP servers](https://modelcontextprotocol.io/) via HTTP to Slack. Once connected, Slackbot will automatically discover your MCP server's tools and invoke them based on user prompts in conversation.

You can add MCP server support via a Slack app, like any other Slack platform feature. Within this guide, you'll determine the proper authentication method for your MCP server, configure it within a Slack app, and then install said app to a workspace. By the end, you'll be able to invoke the tool from Slackbot.

* * *

## Choosing the proper authentication method {#choosing-the-proper-authentication-method}

Slack supports four authentication methods for MCP server requests. Choose the one that fits your use case and the MCP server you plan to use.

Authentication method

Use

Per-user identity

Requires separate OAuth flow

**No auth**

Use when your MCP server serves the same responses regardless of who is asking. Users can start using tools immediately with no OAuth flow.

No

No

**Slack identity auth**

Use when your MCP server maps Slack user IDs and team IDs to available features. No separate OAuth flow is required for end users; Slack identifies them via signed requests containing their user and team IDs.

Yes (Slack user/team ID)

No

**Dynamic Client Registration**

Use when connecting to a third-party MCP server that supports [DCR](https://blog.modelcontextprotocol.io/posts/client_registration/). You provide the URL and the server handles [OAuth](https://docs.slack.dev/authentication/installing-with-oauth.md) client registration automatically.

Yes (OAuth tokens)

Yes

**Manual OAuth**

Use when your MCP server doesn't support DCR. You define the authorization URL, token URL, scopes, and identity endpoint. When a user connects in Slackbot, Slack redirects them through the provider's OAuth flow using these credentials.

Yes (OAuth tokens)

Yes

* * *

## Adding the MCP server to your Slack app {#adding-the-mcp-server-to-your-slack-app}

You can configure your MCP server within App Settings or by editing the app manifest directly. If you add a server via App Settings, those settings are automatically added to the manifest.

*   App Settings
*   App manifest

1.  Go to your app at [api.slack.com/apps](https://api.slack.com/apps). If you don't already have one, create one by clicking **Create New App**.
    
2.  In the left sidebar under **Features**, click **MCP Servers**.
    
3.  Click the **Get Started** button to open the MCP Server form.
    
4.  Fill in the following fields:
    
    *   **Name**: A display name for your MCP server.
    *   **URL**: The HTTPS endpoint of your MCP server (e.g., `https://mcp.acme.com/mcp`).
    *   **Auth Type**: Select your desired authentication.
5.  Complete any additional fields required by your selected auth type, then click **Save**. The fields depend on the auth type you chose:
    

*   Slack identity
*   No auth
*   Dynamic Client Registration
*   Manual OAuth

There are no additional fields. Slack identifies the user from signed requests.

There are no additional fields.

The server handles OAuth client registration automatically; you can provide some optional identity fields:

*   **Identity URL** (optional): The endpoint Slack calls to fetch the user's identity after authentication.
*   **Account Identifier** (optional): A JSONPath to the user identifier in the identity response, such as `$.id`, `$.email`, or `$.user.id`.

Register with your provider first

If you plan to use Manual OAuth, [register an OAuth client with your provider](#manual-oauth-register) before you configure the server, so you have the `client_id`, `client_secret`, and endpoint URLs on hand.

The form collects the following OAuth provider values:

*   **Client ID** and **Client Secret**: Your OAuth credentials from the provider. If you enable PKCE, the client secret is optional.
*   **Authorization URL**: The endpoint that initiates the OAuth handshake and shows the consent screen.
*   **Token request URL**: The endpoint Slack calls to exchange the authorization code for an access token.
*   **Use HTTP Basic Authentication**: Enable to send client credentials via the HTTP Basic Auth header instead of the POST body.
*   **Use PKCE (Proof Key for Code Exchange)**: Enable if your provider requires PKCE.
*   **OAuth scopes** (optional): The scopes to request during authorization. Press Enter to add each scope.
*   **Identity URL** (optional): The endpoint Slack calls to fetch the user's identity after authentication.
*   **Account Identifier** (optional): A JSONPath to the user identifier in the identity response, such as `$.id`, `$.email`, or `$.user.id`.
*   **HTTP method** (optional): The HTTP method used to fetch the user identity. Defaults to `GET`.
*   **Custom headers** (optional): Extra headers for the identity request. Slack sets the `Authorization` header automatically.

Once saved, your MCP server appears in the **MCP Server Connection Details** table. You can add additional servers by clicking **\+ Add MCP Server**.

Adding an MCP server via App Settings automatically adds the [`mcp:connect`](https://docs.slack.dev/reference/scopes/mcp.connect.md) scope to your app.

1.  Go to your app at [api.slack.com/apps](https://api.slack.com/apps). If you don't already have one, create one by clicking **Create New App**.
    
2.  In the left sidebar under **Features**, click **App Manifest**.
    
3.  The `mcp:connect` bot scope is required for MCP server connectivity. Add it like so:
    

```
  "oauth_config": {    "scopes": {      "bot": ["mcp:connect"]    }  },
```

4.  You'll also need to add an `mcp_servers` block to your app manifest. The `mcp_servers` block tells Slack where to find your MCP endpoint.

Field

Required

Description

`url`

Required

The HTTPS URL of your MCP server endpoint

`auth_type`

Required

Authentication type. Can be `"no_auth"`, `"slack_identity_auth"`, `"manual_auth"`, or `"dynamic_client_registration"`

`auth_provider_key`

Conditional

Reference to an `external_auth_providers` entry for per-user OAuth. Required when `auth_type` is `manual_auth`.

5.  Choose the auth type that fits your use case. The following are example manifests:

*   Slack identity
*   No auth
*   Dynamic Client Registration
*   Manual OAuth

The app needs [`users:read`](https://docs.slack.dev/reference/scopes/users.read.md) and [`users:read.email`](https://docs.slack.dev/reference/scopes/users.read.email.md) scopes to fetch profile data, plus a `redirect_url` for the app's own [OAuth install flow](https://docs.slack.dev/authentication/installing-with-oauth.md).

```
{  "display_information": {    "name": "MCP Client - Slack ID",    "description": "Connects app MCP server to Slackbot MCP client using Slack identity auth"  },  "features": {    "bot_user": {      "display_name": "MCP Client - Slack ID",      "always_online": true    }  },  "oauth_config": {    "redirect_urls": [      "https://1234-56-78-90-0.ngrok-free.app/slack/oauth_redirect"    ],    "scopes": {      "bot": ["mcp:connect", "users:read", "users:read.email"]    }  },  "settings": {    "org_deploy_enabled": true,    "socket_mode_enabled": false,    "token_rotation_enabled": false  },  "mcp_servers": {    "profile": {      "url": "https://1234-56-78-90-0.ngrok-free.app/mcp",      "auth_type": "slack_identity_auth"    }  }}
```

```
{  "display_information": {    "name": "MCP Client - No Auth",    "description": "Connects app MCP server to Slackbot MCP client without authentication"  },  "features": {    "bot_user": {      "display_name": "MCP Client - No Auth",      "always_online": true    }  },  "oauth_config": {    "scopes": {      "bot": ["mcp:connect"]    }  },  "settings": {    "org_deploy_enabled": true,    "socket_mode_enabled": false,    "token_rotation_enabled": false  },  "mcp_servers": {    "dice": {      "url": "https://1234-56-78-90-0.ngrok-free.app/mcp",      "auth_type": "no_auth"    }  }}
```

You don't need an `external_auth_providers` block because the MCP server advertises its own OAuth configuration.

```
{  "display_information": {    "name": "MCP Client - DCR",    "description": "Connects Notion MCP server to Slackbot MCP client using Dynamic Client Registration"  },  "features": {    "bot_user": {      "display_name": "MCP Client - DCR",      "always_online": true    }  },  "oauth_config": {    "scopes": {      "bot": ["mcp:connect"]    }  },  "settings": {    "org_deploy_enabled": true,    "socket_mode_enabled": false,    "token_rotation_enabled": false  },  "mcp_servers": {    "notion": {      "url": "https://mcp.notion.com/mcp",      "auth_provider_key": "mcp_dcr_notion",      "auth_type": "dynamic_client_registration"    }  }}
```

Register with your provider first

If you plan to use Manual OAuth, [register an OAuth client with your provider](#manual-oauth-register) before you configure the server, so you have the `client_id`, `client_secret`, and endpoint URLs on hand.

Define your OAuth provider under `external_auth_providers.oauth2` and reference it from `mcp_servers.{server_name}.auth_provider_key`.

##### external_auth_providers fields {#external-auth-providers-fields}

Providers are nested under a fixed `oauth2` key, then under a provider key you choose (for example, `mcp_github`). That key must exactly match the server's `auth_provider_key`, cannot contain spaces or periods, and must be 100 characters or fewer. Renaming a key is treated as delete-and-recreate, which invalidates existing user tokens (changing an existing provider's `client_id` in place has the same effect).

The following fields sit at the provider level, relative to `external_auth_providers.oauth2.{provider_key}`.

Field

Required

Description

`provider_type`

Required

Set to `CUSTOM`. This is the only supported value.

`options`

Required

An object holding the provider's OAuth configuration. See the fields below.

The `options` object holds the OAuth configuration itself. Paths in the following table are relative to `external_auth_providers.oauth2.{provider_key}.options`.

Field

Required

Description

`client_id`

Required

Your OAuth client ID from the provider. When registering the client, set the redirect URI to `https://oauth2.slack.com/external/auth/callback`, where the provider sends users after they authorize.

`provider_name`

Required

A human-readable name for the provider, shown to users (2 to 255 characters).

`scope`

Required

An array of scopes to request from the provider (for example, `["repo"]`). Use `[]` if your server handles scoping internally.

`authorization_url`

Required

The provider's authorization endpoint (must be `https://`). Where Slack redirects users to authorize.

`token_url`

Required

The provider's token endpoint, which is where Slack exchanges the authorization code for a token. Must begin with `https://`.

`identity_config`

Conditional

An object that tells Slack how to identify the connected account, or `null`. Required for `CUSTOM` providers, but if your provider key starts with `mcp_` you may set it to `null` to skip account mapping.

`identity_config.url`

Conditional

Required when `identity_config` is set. The endpoint Slack calls to identify the connected account. Must begin with `https://`.

`identity_config.account_identifier`

Conditional

Required when `identity_config` is set. A JSONPath that extracts a unique account identifier from the identity response (for example, `$.login`).

`identity_config.headers`

Optional

Extra headers to send with the identity request.

`identity_config.body`

Optional

Body parameters for the identity request, used only when `http_method_type` is `POST`.

`identity_config.http_method_type`

Optional

Defaults to `GET`. The HTTP method for the identity request, which can be `GET` or `POST`.

`use_pkce`

Optional

Defaults to `false`. Set to `true` if your provider requires PKCE. For `mcp_`\-prefixed providers, enabling PKCE removes the need for a client secret.

`token_url_config.use_basic_auth_scheme`

Optional

Defaults to `false`, which sends credentials via `client_secret_post`. Set to `true` if your token endpoint expects `client_secret_basic`. If unsure, check the provider's `/.well-known/oauth-authorization-server` response for `token_endpoint_auth_methods_supported`.

Client secrets aren't stored in the manifest

`client_secret` is not a manifest field. Add it out of band with `slack external-auth add-secret` or through the **MCP Servers** section in App Settings. Providers with an `mcp_` prefix that use PKCE don't need a client secret.

See the [app manifest reference](https://docs.slack.dev/reference/app-manifest.md) for the complete schema.

This example connects to GitHub's MCP server.

```
{  "display_information": {    "name": "MCP Client - External Auth",    "description": "Connects GitHub MCP server to Slackbot MCP client using an external auth provider"  },  "features": {    "bot_user": {      "display_name": "MCP Client - External Auth",      "always_online": true    }  },  "oauth_config": {    "scopes": {      "bot": ["mcp:connect"]    }  },  "settings": {    "org_deploy_enabled": true,    "socket_mode_enabled": false,    "token_rotation_enabled": false  },  "external_auth_providers": {    "oauth2": {      "mcp_github": {        "provider_type": "CUSTOM",        "options": {          "client_id": "YOUR_GITHUB_CLIENT_ID",          "scope": ["repo"],          "provider_name": "GitHub",          "authorization_url": "https://github.com/login/oauth/authorize",          "token_url": "https://github.com/login/oauth/access_token",          "identity_config": {            "url": "https://api.github.com/user",            "account_identifier": "$.login"          },          "use_pkce": false,          "token_url_config": {            "use_basic_auth_scheme": false          }        }      }    }  },  "mcp_servers": {    "github": {      "url": "https://api.githubcopilot.com/mcp/",      "auth_type": "manual_auth",      "auth_provider_key": "mcp_github"    }  }}
```

Refer to the [app manifest reference](https://docs.slack.dev/reference/app-manifest.md) for the full schema.

### Register an OAuth client with your provider {#manual-oauth-register}

If you're using Manual OAuth, register an OAuth client with your provider before you configure the server above. When registering, set the redirect URI to Slack's external auth callback endpoint:

```
https://oauth2.slack.com/external/auth/callback
```

This is where the provider sends users after they authorize. The provider then issues a `client_id` and `client_secret` for the client, which you supply when you configure the server. If your provider needs to identify Slack as a client during registration, Slack publishes a Client ID Metadata Document (CIMD) at:

```
https://slack.com/.well-known/oauth-client-metadata
```

When editing the manifest directly, the `client_secret` is not stored in the manifest. Add it separately through the **MCP Servers** section in App Settings or with the Slack CLI:

```
slack external-auth add-secret
```

If your provider uses PKCE (`use_pkce: true`) and your provider key starts with `mcp_`, no client secret is required.

* * *

## Implementing your MCP server {#implementing-your-mcp-server}

MCP servers must use the Streamable HTTP transport.

The deprecated standalone HTTP+SSE transport and stdio are not supported. SSE-formatted responses within Streamable HTTP are supported and expected.

How you implement your MCP server will depend on the type of authentication your MCP server is using.

*   Slack identity
*   No auth
*   Dynamic Client Registration
*   Manual OAuth

### Using Slack identity auth {#slack-identity}

The snippets below use either [Bolt for JavaScript](https://docs.slack.dev/tools/bolt-js.md) or [Bolt for Python](https://docs.slack.dev/tools/bolt-python.md), alongside the [Slack CLI](https://docs.slack.dev/tools/slack-cli.md)

View the full examples: [JavaScript](https://github.com/slack-samples/bolt-js-examples/tree/main/ai/slackbot-mcp-client/slack-identity) | [Python](https://github.com/slack-samples/bolt-python-examples/tree/main/ai/slackbot-mcp-client/slack-identity)

In this example, three things happen on each request:

1.  The `/mcp` route [verifies the Slack request signature](https://docs.slack.dev/authentication/verifying-requests-from-slack.md).
2.  The tool reads the caller's identity from `_meta.slack` (provided automatically by Slack).
3.  The [bot token](https://docs.slack.dev/authentication/tokens.md) from the installation store calls the [`users.info`](https://docs.slack.dev/reference/methods/users.info.md) API method.

#### The _meta.slack identity fields {#meta-slack-fields}

When a tool is called through Slack identity auth, Slack attaches the caller's identity to the request under the `_meta.slack` object, nested inside the JSON-RPC request's `params`. These fields let your MCP server identify who is calling without requiring a separate OAuth flow. Read them from the `_meta` argument that Slack passes to your tool handler.

A `tools/call` request from a standalone workspace looks like this:

```
{  "jsonrpc": "2.0",  "id": 1,  "method": "tools/call",  "params": {    "name": "get_profile_card",    "arguments": {      "user_id": "U0101010101"    },    "_meta": {      "slack": {        "user_id": "U01234ABCDE",        "team_id": "T01234ABCDE",        "enterprise_id": null      }    }  }}
```

Field

Type

Description

`user_id`

`string`

The ID of the Slack user who invoked the tool. Pass it to the [`users.info`](https://docs.slack.dev/reference/methods/users.info.md) method to look up profile data.

`team_id`

`string` or `null`

The workspace ID for a standalone workspace. The value will be `null` for an [Enterprise](https://docs.slack.dev/enterprise.md) org user, unless the tool call resolves to a specific workspace where the app is installed.

`enterprise_id`

`string` or `null`

The org ID for an [Enterprise](https://docs.slack.dev/enterprise.md) org user. The value will be `null` for a standalone workspace.

All three keys are always present in the object, but `team_id` and `enterprise_id` can be `null`. The combination tells you where the request came from:

Scenario

`user_id`

`team_id`

`enterprise_id`

Standalone workspace

`U01234ABCDE`

`T01234ABCDE`

`null`

Enterprise org, resolved to a workspace

`U01234ABCDE`

`T01234ABCDE`

`E01234ABCDE`

Enterprise org, not resolved to a workspace

`U01234ABCDE`

`null`

`E01234ABCDE`

Verify the request signature before trusting anything in `_meta.slack`. The field is ordinary request JSON and can be forged by any caller that reaches your endpoint, so its contents mean nothing until you've [verified the `X-Slack-Signature` header](https://docs.slack.dev/authentication/verifying-requests-from-slack.md). Once verified, read `user_id` and handle a `null` `team_id` when building the installation lookup. Slack attaches `_meta.slack` for all auth types so its presence alone tells you nothing about how the request was authenticated.

*   JavaScript
*   Python

`src/app.js`

```javascript
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
import { App, isValidSlackRequest, LogLevel } from "@slack/bolt";
import { FileInstallationStore } from "@slack/oauth";
import { z } from "zod";

/**
 * @typedef {object} SlackIdentity
 * @property {string} [user_id]
 * @property {string} [team_id]
 * @property {string} [enterprise_id]
 */

/**
 * Creates an MCP server with a Slack profile card tool.
 * @param {import("@slack/web-api").WebClient} client - Slack Web API client
 * @param {import("@slack/oauth").InstallationStore} installationStore - OAuth installation store
 * @param {import("@slack/bolt").Logger} logger - Bolt logger instance
 * @see {@link https://github.com/modelcontextprotocol/typescript-sdk#getting-started}
 */
function createServer(client, installationStore, logger) {
  const server = new McpServer({ name: "Profile Card", version: "1.0.0" });

  server.registerTool(
    "get_profile_card",
    {
      title: "Get Profile Card",
      description: "Get a profile card for a Slack user by their user ID.",
      inputSchema: {
        user_id: z
          .string()
          .describe("The Slack user ID to look up (e.g. U01234ABCDE)"),
      },
      annotations: {
        readOnlyHint: true,
      },
    },
    async ({ user_id }, { _meta }) => {
      /** @type {SlackIdentity | undefined} */
      const slack = /** @type {any} */ (_meta?.slack);

      if (!slack?.user_id || !slack?.team_id) {
        return {
          content: [
            {
              type: "text",
              text: "Missing Slack identity context. This tool must be called from Slack.",
            },
          ],
        };
      }

      let botToken;
      try {
        const installation = await installationStore.fetchInstallation({
          teamId: slack.team_id,
          userId: slack.user_id,
          enterpriseId: slack.enterprise_id || undefined,
          isEnterpriseInstall: !!slack.enterprise_id,
        });
        if (!installation.bot?.token) {
          throw new Error("No bot token found for this installation");
        }
        botToken = installation.bot.token;
      } catch (err) {
        logger.error(err);
        return {
          content: [
            {
              type: "text",
              text: `App not installed to this workspace. Please install first: ${process.env.BASE_URL}/slack/install`,
            },
          ],
        };
      }

      let profile;
      try {
        const result = await client.users.info({
          token: botToken,
          user: user_id,
        });
        if (!result.user?.profile) {
          throw new Error(`User ${user_id} not found.`);
        }
        profile = result.user.profile;
      } catch (err) {
        logger.error(err);
        return {
          content: [
            {
              type: "text",
              text: `Failed to fetch profile for ${user_id}.`,
            },
          ],
        };
      }

      return {
        content: [
          {
            type: "text",
            text: [
              `Profile card for ${profile.real_name}`,
              `Title: ${profile.title}`,
              `Email: ${profile.email}`,
            ].join("\n"),
          },
        ],
      };
    },
  );

  return server;
}

/**
 * Bolt app with OAuth install flow and custom /mcp route.
 * @see {@link https://docs.slack.dev/tools/bolt-js/getting-started}
 */
const installationStore = new FileInstallationStore({
  baseDir: "./installations/",
});

export const app = new App({
  clientId: process.env.SLACK_CLIENT_ID,
  clientSecret: process.env.SLACK_CLIENT_SECRET,
  signingSecret: process.env.SLACK_SIGNING_SECRET,
  stateSecret: process.env.SLACK_STATE_SECRET,
  scopes: ["mcp:connect", "users:read", "users:read.email"],
  installationStore,
  logLevel: LogLevel.INFO,
  customRoutes: [
    {
      path: "/mcp",
      method: "POST",
      handler: async (req, res) => {
        const chunks = [];
        for await (const chunk of req) {
          chunks.push(chunk);
        }
        const rawBody = Buffer.concat(chunks).toString();

        if (
          !isValidSlackRequest({
            signingSecret: `${process.env.SLACK_SIGNING_SECRET}`,
            headers: /** @type {any} */ (req.headers),
            body: rawBody,
          })
        ) {
          res.writeHead(401, { "Content-Type": "application/json" });
          return res.end(
            JSON.stringify({
              jsonrpc: "2.0",
              error: { code: -32600, message: "Invalid request" },
              id: null,
            }),
          );
        }

        const body = JSON.parse(rawBody);
        const server = createServer(app.client, installationStore, app.logger);
        const transport = new StreamableHTTPServerTransport({
          sessionIdGenerator: undefined,
        });
        await server.connect(transport);
        await transport.handleRequest(req, res, body);
      },
    },
  ],
});

```

#### Entry point {#entry-point-slack-identity-js}

Start the server on the configured port. The Bolt app handles OAuth install and redirect routes alongside your MCP endpoint.

`app.js`

```javascript
import { app } from "./src/app.js";

const port = Number.parseInt(process.env.PORT || "3000", 10);

(async () => {
  try {
    await app.start(port);
    app.logger.info("⚡️ Bolt app is running!");
  } catch (error) {
    app.logger.error("Failed to start the app", error);
  }
})();

```

`src/app.py`

```python
import contextlib
import os
from typing import Any

from mcp.server import MCPServer
from mcp.server.mcpserver import Context
from mcp.types import CallToolResult, TextContent, ToolAnnotations
from slack_bolt import App
from slack_bolt.adapter.starlette import SlackRequestHandler
from slack_bolt.oauth.oauth_settings import OAuthSettings
from slack_sdk.errors import SlackApiError
from slack_sdk.oauth.installation_store import FileInstallationStore
from slack_sdk.oauth.state_store import FileOAuthStateStore
from slack_sdk.signature import SignatureVerifier
from slack_sdk.web import WebClient
from starlette.applications import Starlette
from starlette.requests import Request
from starlette.responses import JSONResponse
from starlette.routing import Route
from starlette.types import ASGIApp, Receive, Scope, Send

"""Creates an MCP server with a profile card tool using Slack identity.

https://github.com/modelcontextprotocol/python-sdk#quickstart
"""

mcp_server = MCPServer("Profile Card")


@mcp_server.tool(
    name="get_profile_card",
    title="Get Profile Card",
    description="Get a profile card for a Slack user by their user ID.",
    annotations=ToolAnnotations(read_only_hint=True),
)
async def get_profile_card(
    user_id: str,
    ctx: Context,
) -> CallToolResult:
    meta: dict[str, Any] = dict(ctx.request_context.meta or {})
    slack = meta.get("slack") or {}

    if not slack.get("user_id") or not slack.get("team_id"):
        return CallToolResult(
            content=[
                TextContent(
                    type="text",
                    text="Missing Slack identity context. This tool must be called from Slack.",
                )
            ],
        )

    installation = installation_store.find_installation(
        enterprise_id=slack.get("enterprise_id"),
        team_id=slack["team_id"],
        user_id=slack["user_id"],
        is_enterprise_install=bool(slack.get("enterprise_id")),
    )
    if not installation or not installation.bot_token:
        install_url = f"{os.environ.get('BASE_URL', '')}/slack/install"
        return CallToolResult(
            content=[
                TextContent(
                    type="text",
                    text=f"App not installed to this workspace. Please install first: {install_url}",
                )
            ],
        )

    try:
        client = WebClient(token=installation.bot_token)
        result = client.users_info(user=user_id)
        profile = (result["user"] or {}).get("profile")
        if not profile:
            raise ValueError("No profile found")
    except (SlackApiError, ValueError, KeyError):
        return CallToolResult(
            content=[
                TextContent(
                    type="text",
                    text=f"Failed to fetch profile for {user_id}.",
                )
            ],
        )

    return CallToolResult(
        content=[
            TextContent(
                type="text",
                text=f"Profile card for {profile['real_name']}\n"
                f"Title: {profile.get('title', '')}\n"
                f"Email: {profile.get('email', '')}",
            )
        ],
    )


"""Creates a Bolt app with OAuth and a custom /mcp route.

https://docs.slack.dev/tools/bolt-python/getting-started
"""

installation_store = FileInstallationStore(base_dir="./installations")

bolt_app = App(
    signing_secret=os.environ.get("SLACK_SIGNING_SECRET"),
    oauth_settings=OAuthSettings(
        client_id=os.environ.get("SLACK_CLIENT_ID"),
        client_secret=os.environ.get("SLACK_CLIENT_SECRET"),
        scopes=["mcp:connect", "users:read", "users:read.email"],
        installation_store=installation_store,
        state_store=FileOAuthStateStore(
            expiration_seconds=600,
            base_dir="./states",
        ),
    ),
)


class SlackSignatureMiddleware:
    def __init__(self, app: ASGIApp) -> None:
        self.app = app
        self.verifier = SignatureVerifier(
            signing_secret=os.environ["SLACK_SIGNING_SECRET"]
        )

    async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
        if scope["type"] != "http":
            await self.app(scope, receive, send)
            return

        request = Request(scope, receive, send)
        body = await request.body()

        if not self.verifier.is_valid_request(body, dict(request.headers)):
            response = JSONResponse(
                {
                    "jsonrpc": "2.0",
                    "error": {"code": -32600, "message": "Invalid request"},
                    "id": None,
                },
                status_code=401,
            )
            await response(scope, receive, send)
            return

        async def replay_receive():
            return {"type": "http.request", "body": body, "more_body": False}

        await self.app(scope, replay_receive, send)


@contextlib.asynccontextmanager
async def lifespan(a):
    async with mcp_server.session_manager.run():
        yield


mcp_app = mcp_server.streamable_http_app(stateless_http=True, json_response=True)


app = Starlette(
    routes=[
        Route(
            "/slack/events",
            endpoint=SlackRequestHandler(bolt_app).handle,
            methods=["POST"],
        ),
        Route(
            "/slack/install",
            endpoint=SlackRequestHandler(bolt_app).handle,
            methods=["GET"],
        ),
        Route(
            "/slack/oauth_redirect",
            endpoint=SlackRequestHandler(bolt_app).handle,
            methods=["GET"],
        ),
        Route(
            "/mcp",
            endpoint=SlackSignatureMiddleware(mcp_app),
            methods=["POST"],
        ),
    ],
    lifespan=lifespan,
)

```

#### Entry point {#entry-point-slack-identity-python}

Start the server on the configured port. The Bolt app handles OAuth install and redirect routes alongside your MCP endpoint.

`app.py`

```python
import os

import uvicorn

from src.app import app

if __name__ == "__main__":
    port = int(os.environ.get("PORT", "3000"))
    uvicorn.run(app, host="0.0.0.0", port=port)

```

#### Setup {#setup-slack-identity}

1.  Expose your local server with ngrok. Update the `url` in your manifest with the generated URL.
    
    ```
    ngrok http 3000
    ```
    
2.  Install the app:
    
    1.  Go to [App Settings](https://api.slack.com/apps).
    2.  Navigate to **OAuth & Permissions** > **Install to Workspace** (or **Install to Organization** for Enterprise).
    3.  Authorize the app.
3.  Update environment variables with values from App Settings.
    
    The [signing secret](https://docs.slack.dev/authentication/verifying-requests-from-slack.md) verifies that incoming requests originated from Slack. The state secret secures the OAuth install flow.
    
    ai/slackbot-mcp-client/slack-identity/.env.example
    
    ```
    loading...
    ```
    
    [View on GitHub](https://github.com/slack-samples/bolt-js-examples/blob/main/ai/slackbot-mcp-client/slack-identity/.env.example
    )
    
    ```
    slack env init
    ```
    
4.  Start the app:
    
    ```
    slack run
    ```
    

### Using no auth {#no-auth}

The snippets below use either [Bolt for JavaScript](https://docs.slack.dev/tools/bolt-js.md) or [Bolt for Python](https://docs.slack.dev/tools/bolt-python.md), alongside the [Slack CLI](https://docs.slack.dev/tools/slack-cli.md)

View the full examples: [JavaScript](https://github.com/slack-samples/bolt-js-examples/tree/main/ai/slackbot-mcp-client/no-auth) | [Python](https://github.com/slack-samples/bolt-python-examples/tree/main/ai/slackbot-mcp-client/no-auth)

Slack will send unauthenticated requests to your MCP server. Slack still signs every request so your server can [verify it originated from Slack](https://docs.slack.dev/authentication/verifying-requests-from-slack.md).

In this example, the `/mcp` route [verifies the Slack request signature](https://docs.slack.dev/authentication/verifying-requests-from-slack.md) before forwarding the request to the MCP transport.

*   JavaScript
*   Python

`src/app.js`

```javascript
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
import { App, isValidSlackRequest, LogLevel } from "@slack/bolt";
import { z } from "zod";

/**
 * Creates an MCP server with a dice roller tool.
 * @see {@link https://github.com/modelcontextprotocol/typescript-sdk#getting-started}
 */
function createServer() {
  const server = new McpServer({ name: "Dice Game", version: "1.0.0" });

  server.registerTool(
    "roll_dice",
    {
      title: "Roll Dice",
      description: "Roll one or more dice with a configurable number of sides.",
      inputSchema: {
        sides: z
          .number()
          .default(6)
          .describe("Number of sides on each die (e.g., 6, 20)"),
        count: z.number().default(1).describe("Number of dice to roll"),
      },
      annotations: {
        readOnlyHint: true,
      },
    },
    async ({ sides, count }) => {
      const rolls = Array.from(
        { length: count },
        () => Math.floor(Math.random() * sides) + 1,
      );
      const total = rolls.reduce((sum, r) => sum + r, 0);
      const label = `${count}d${sides}`;
      const rollsDisplay = count > 1 ? ` [${rolls.join(", ")}]` : "";

      return {
        content: [
          {
            type: "text",
            text: `Rolled ${label}:${rollsDisplay} = ${total}`,
          },
        ],
      };
    },
  );

  return server;
}

/**
 * Creates a Bolt app with custom /mcp route.
 * @see {@link https://docs.slack.dev/tools/bolt-js/getting-started}
 */
export const app = new App({
  token: process.env.SLACK_BOT_TOKEN,
  signingSecret: process.env.SLACK_SIGNING_SECRET,
  logLevel: LogLevel.INFO,
  customRoutes: [
    {
      path: "/mcp",
      method: "POST",
      handler: async (req, res) => {
        const chunks = [];
        for await (const chunk of req) {
          chunks.push(chunk);
        }
        const rawBody = Buffer.concat(chunks).toString();

        if (
          !isValidSlackRequest({
            signingSecret: `${process.env.SLACK_SIGNING_SECRET}`,
            headers: /** @type {any} */ (req.headers),
            body: rawBody,
          })
        ) {
          res.writeHead(401, { "Content-Type": "application/json" });
          return res.end(
            JSON.stringify({
              jsonrpc: "2.0",
              error: { code: -32600, message: "Invalid request" },
              id: null,
            }),
          );
        }

        const body = JSON.parse(rawBody);
        const server = createServer();
        const transport = new StreamableHTTPServerTransport({
          sessionIdGenerator: undefined,
        });
        await server.connect(transport);
        await transport.handleRequest(req, res, body);
      },
    },
  ],
});

```

#### Entry point {#entry-point-no-auth}

Bolt handles the Slack events route automatically, while the custom `/mcp` route serves your MCP server.

`app.js`

```javascript
import { app } from "./src/app.js";

const port = Number.parseInt(process.env.PORT || "3000", 10);

(async () => {
  try {
    await app.start(port);
    app.logger.info("⚡️ Bolt app is running!");
  } catch (error) {
    app.logger.error("Failed to start the app", error);
  }
})();

```

`src/app.py`

```python
import contextlib
import os
import random

from mcp.server import MCPServer
from mcp.types import CallToolResult, TextContent, ToolAnnotations
from slack_bolt import App
from slack_bolt.adapter.starlette import SlackRequestHandler
from slack_sdk.signature import SignatureVerifier
from starlette.applications import Starlette
from starlette.requests import Request
from starlette.responses import JSONResponse
from starlette.routing import Route
from starlette.types import ASGIApp, Receive, Scope, Send

"""Creates an MCP server with a dice roller tool.

https://github.com/modelcontextprotocol/python-sdk#quickstart
"""

mcp_server = MCPServer("Dice Game")


@mcp_server.tool(
    name="roll_dice",
    title="Roll Dice",
    description="Roll one or more dice with a configurable number of sides.",
    annotations=ToolAnnotations(read_only_hint=True),
)
def roll_dice(sides: int = 6, count: int = 1) -> CallToolResult:
    rolls = [random.randint(1, sides) for _ in range(count)]
    total = sum(rolls)
    label = f"{count}d{sides}"
    rolls_display = f" [{', '.join(str(r) for r in rolls)}]" if count > 1 else ""

    return CallToolResult(
        content=[
            TextContent(type="text", text=f"Rolled {label}:{rolls_display} = {total}")
        ],
    )


"""Creates a Bolt app with a custom /mcp route.

https://docs.slack.dev/tools/bolt-python/getting-started
"""

bolt_app = App(
    token=os.environ.get("SLACK_BOT_TOKEN"),
    signing_secret=os.environ.get("SLACK_SIGNING_SECRET"),
)


class SlackSignatureMiddleware:
    def __init__(self, app: ASGIApp) -> None:
        self.app = app
        self.verifier = SignatureVerifier(
            signing_secret=os.environ["SLACK_SIGNING_SECRET"]
        )

    async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
        if scope["type"] != "http":
            await self.app(scope, receive, send)
            return

        request = Request(scope, receive, send)
        body = await request.body()

        if not self.verifier.is_valid_request(body, dict(request.headers)):
            response = JSONResponse(
                {
                    "jsonrpc": "2.0",
                    "error": {"code": -32600, "message": "Invalid request"},
                    "id": None,
                },
                status_code=401,
            )
            await response(scope, receive, send)
            return

        async def replay_receive():
            return {"type": "http.request", "body": body, "more_body": False}

        await self.app(scope, replay_receive, send)


@contextlib.asynccontextmanager
async def lifespan(a):
    async with mcp_server.session_manager.run():
        yield


mcp_app = mcp_server.streamable_http_app(stateless_http=True, json_response=True)


app = Starlette(
    routes=[
        Route(
            "/slack/events",
            endpoint=SlackRequestHandler(bolt_app).handle,
            methods=["POST"],
        ),
        Route(
            "/mcp",
            endpoint=SlackSignatureMiddleware(mcp_app),
            methods=["POST"],
        ),
    ],
    lifespan=lifespan,
)

```

#### Entry point {#entry-point-no-auth}

Bolt handles the Slack events route automatically, while the custom `/mcp` route serves your MCP server.

`app.py`

```python
import os

import uvicorn

from src.app import app

if __name__ == "__main__":
    port = int(os.environ.get("PORT", "3000"))
    uvicorn.run(app, host="0.0.0.0", port=port)

```

#### Setup {#setup-no-auth}

1.  Expose your local server with ngrok. Update the `url` in your manifest with the generated URL.
    
    ```
    ngrok http 3000
    ```
    
2.  Install the app:
    
    1.  Go to [App Settings](https://api.slack.com/apps).
    2.  Navigate to **OAuth & Permissions** > **Install to Workspace** (or **Install to Organization** for Enterprise).
    3.  Authorize the app.
3.  From App Settings, get the **signing secret**.
    
4.  Set the signing secret as an environment variable:
    
    ```
    slack app settingsslack env set SLACK_SIGNING_SECRET placeholder
    ```
    
5.  Start the app:
    
    ```
    slack run
    ```
    

When [using Slackbot](#using-slackbot), you can ask for things like "Roll 2d20".

### Using Dynamic Client Registration {#dcr}

With DCR, the MCP server registers Slack as a client automatically, so there's no redirect URI to configure by hand.

#### Setup {#setup}

1.  Install the app:
    1.  Go to [App Settings](https://api.slack.com/apps).
    2.  Navigate to **OAuth & Permissions** > **Install to Workspace** (or **Install to Organization** for Enterprise).
    3.  Authorize the app.

When a user connects to this MCP server in Slackbot, they'll be redirected to the provider's OAuth consent screen to authorize access.

Editing DCR servers

If you created your MCP server via App Settings with Dynamic Client Registration, only the identity URL and account identifier fields are editable in the UI. To change any other configuration, edit the app manifest directly.

### Using manual OAuth {#manual-oauth}

Use manual OAuth when your MCP server doesn't support DCR. You supply the OAuth details yourself:

*   a client ID and secret,
*   the authorization and token endpoints,
*   and an identity endpoint.

Slack uses those details to run the OAuth flow on the user's behalf when they connect your server in Slackbot.

You should have already [registered an OAuth client with your provider](#manual-oauth-register) and configured it when you [added the MCP server](#external-auth-providers-fields). Now you can install and verify the app.

#### 1. Install the app {#manual-oauth-step-install}

1.  Go to [App Settings](https://api.slack.com/apps).
2.  Navigate to **OAuth & Permissions** > **Install to Workspace**.
3.  Authorize the app.

#### 2. Verify the authentication flow {#manual-oauth-step-verify}

Connect the server in Slackbot as described under [Using Slackbot](#using-slackbot). When you connect, Slack will redirect you to the provider's OAuth consent screen. After you authorize, Slackbot should be able to list and call the server's tools. If the connection fails, see [OAuth troubleshooting](#troubleshooting-manual-oauth) below.

#### OAuth troubleshooting {#troubleshooting-manual-oauth}

##### "Unexpected error" on authorize page {#unexpected-error-on-authorize-page}

This error is caused by a Redirect URI mismatch. Ensure the redirect URI registered with your OAuth provider matches `https://oauth2.slack.com/external/auth/callback`.

##### access_token_exchange_failed error {#access_token_exchange_failed-error}

This error is caused by a token endpoint auth method mismatch. Check that `use_basic_auth_scheme` matches your provider's expected method.

##### "Not able to connect this account" {#not-able-to-connect-this-account}

This error is caused by a missing or wrong client secret. Re-add the client secret via the **MCP Servers** section in App Settings or using `slack external-auth add-secret`.

Tools must respond within 60 seconds

If your MCP server doesn't return a result within 60 seconds, Slackbot aborts the call and surfaces an error to the user.

* * *

## Using Slackbot {#using-slackbot}

1.  Open a DM with **Slackbot**.
    
2.  Click the **Apps** button in the Slackbot toolbar.
    
    ![The Slackbot toolbar showing the apps button](/img/mcp_slackbot_toolbar.png)
3.  On first use, a list of suggested apps appears. Click the **+** button next to the app to add it. Users can have up to 5 active MCP servers at a time.
    
4.  If using OAuth, you will be redirected to authorize with the partner's platform.
    
5.  Once connected, the app appears under **Your apps** in the **Integrations** tab with a **Manage** button.
    

### Check if Slackbot detects your tool {#check-if-slackbot-detects-your-tool}

Ask Slackbot a question that maps to one of your tools:

```
What tools are available from Acme?
```

Slackbot will query your MCP server's tool definitions and list all available tools with descriptions.

You can also verify your tools by clicking **Manage** next to your app in the **Integrations** tab to view the list of available tools and their associated permissions.

### Have Slackbot use a tool {#have-slackbot-use-a-tool}

Try a natural language prompt:

```
Show me recent activity on my Acme account
```

Slackbot will select the appropriate tool. If the tool is from a third-party, the user must explicitly authorize each tool call, both read and write, before Slackbot executes it.

When Slackbot invokes a tool, users are presented with the following options:

*   **Allow once**: permits this tool call one time.
*   **Always allow**: permits all future calls to this tool without prompting.
*   **Deny**: blocks this tool call.

Users can view and update their tool permissions at any time in the MCP config dialog. After making a selection, Slackbot will then invoke the tool and return the results.

You can report responses with hallucinated information by clicking the thumbs down button. Then check the boxes next to the issues you want to report, and include the messages in your conversation if you’d like. We’ll always receive the last message you sent to Slackbot as part of a report. These reports help us identify patterns and improve Slackbot.

### Enrich your tool responses {#enrich-your-tool-responses}

By default, tool results appear as plain text. To create a more polished experience, your MCP server can return [rich responses](https://docs.slack.dev/ai/slackbot-mcp-client/returning-rich-responses.md) using Block Kit for native Slack layouts or interactive HTML/JS via MCP Apps.

### Troubleshooting {#troubleshooting-slackbot}

#### Viewing logs {#viewing-logs}

You can view debug logs for your MCP server connections and tool calls via App Settings. In the left sidebar under **Features**, click **MCP Servers**, then select the **Logs** tab. The logs only include activity from app collaborators.

#### MCP connector not appearing in Slackbot {#mcp-connector-not-appearing-in-slackbot}

Check the following:

*   the app has `mcp:connect` in its bot scopes.
*   the MCP server is configured via an app manifest or App Settings.

Then try reinstalling the app after making any changes. On an Enterprise org, an org-level installation isn't required, but it can be useful when you want the app available across all workspaces in the org rather than installed workspace by workspace.

#### Slackbot failed to load tools {#slackbot-failed-to-load-tools}

Your MCP server may be rejecting authentication. If your app is using OAuth, ensure the user has completed the authorization flow.

You can also test your MCP endpoint directly:

```
curl -X POST \  -H "Content-Type: application/json" \  -H "Authorization: Bearer TOKEN" \  -d '{    "jsonrpc": "2.0",    "id": 1,    "method": "initialize",    "params": {      "protocolVersion": "2024-11-05",      "capabilities": {},      "clientInfo": {"name": "test", "version": "1.0"}    }  }' \  https://mcp.acme.com/mcp
```

#### Slackbot discovers but does not invoke tools {#slackbot-discovers-but-does-not-invoke-tools}

The tools may be unclassified and default to write classification. Ensure your tools include proper descriptions and classification metadata so that read-only tools execute without additional unnecessary confirmation prompts.

#### The invalid_client error {#the-invalid_client-error}

After connecting an MCP server that supports Dynamic Client Registration (DCR), Slackbot may return an `invalid_client` error when calling the server. This can happen when the remote MCP server no longer recognizes the `client_id` that Slackbot registered with it. This typically happens when a self-hosted server stores OAuth client registrations and tokens only in memory. A server restart wipes that registration, so any `client_id` issued before the restart becomes invalid.

Disconnecting and reconnecting the MCP server in Slackbot does **not** force a new client registration. It retries the OAuth flow using the same `client_id` as before. If the server still doesn't recognize it, the error will persist, making the issue look intermittent. It may "resolve" on a retry only if the underlying token/session happens to still be valid.

To force re-registration, edit the MCP server connection and change the server URL (even temporarily), then save. Slackbot only performs a fresh client registration when the server URL changes; reusing the identical URL always reuses the existing `client_id`.

We recommend persisting OAuth client registrations and tokens to durable storage rather than memory only, so a server restart doesn't invalidate `client_id`s that Slack clients (including Slackbot) are still using.

#### OAuth errors {#oauth-errors}

If using manual OAuth, see the [manual OAuth troubleshooting section](#troubleshooting-manual-oauth) above.

* * *

## Onward {#onward}

By following along, you'll have an app that connects an MCP server to Slackbot for your users to interact with. But there's always more!

✨ **To have visually graceful, interactive experiences**, check out [returning rich responses](https://docs.slack.dev/ai/slackbot-mcp-client/returning-rich-responses.md).

✨ **To set up workspace controls**, check out [admin approval](https://docs.slack.dev/ai/slackbot-mcp-client/admin-approval.md) to manage how users install and access your app.

✨ **To publish your app**, check out [distributing the app](https://docs.slack.dev/ai/slackbot-mcp-client/distributing.md) on the Slack Marketplace.
