#
tokens: 3781/50000 10/10 files
lines: off (toggle) GitHub
raw markdown copy
# Directory Structure

```
├── .gitignore
├── Dockerfile
├── package-lock.json
├── package.json
├── README.md
├── smithery.yaml
├── src
│   ├── index.ts
│   └── slack
│       ├── client.ts
│       ├── messages.ts
│       └── users.ts
└── tsconfig.json
```

# Files

--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------

```
dist
node_modules
```

--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------

```markdown
# Slack Model Context Protocol Server

This is a connector to allow Claude Desktop (or any MCP client) to interact with your Slack workspace to post messages and query a list of all users.

## Local Development

In order to run this client locally, add the following configuration to your Claude Desktop MCP Server config file: 

```
 {
  "mcpServers": {
    "mcp-slack-local": {
      "command": "node",
      "args": ["/path/to/project/dist/index.js"], <---- replace this with your project path
      "env": {
        "SLACK_BOT_USER_OAUTH_TOKEN": "test-bot-token", 
        "SLACK_TEAM_ID": "test-team-id"
      }
    },
  }
}
```

After this, you should be able to test this implementation in your Claude Desktop App using example prompts like: 

- "Can you list all users of my Slack team?"
- "Can you send a welcome message to my Slack Channel with the ID `<channel id>`?"

Running the server locally: 

```
node dist/index.js
```

With the build in another terminal 

```
npm run watch
```

## Slack Permission Scopes 

The following permissions are already implemented: 

| Permission | Description | Implemented |
|------------|-------------|-------------|
| app_configurations:read | Read app configuration info via App Manifest APIs | ❌ |
| app_configurations:write | Write app configuration info and create apps via App Manifest APIs | ❌ |
| app_mentions:read | View messages that directly mention @your_slack_app in conversations that the app is in | ❌ |
| assistant:write | Allow your slack app to act as an AI Assistant | ❌ |
| bookmarks:read | List bookmarks | ❌ |
| bookmarks:write | Create, edit, and remove bookmarks | ❌ |
| calls:read | View information about ongoing and past calls | ❌ |
| calls:write | Start and manage calls in a workspace | ❌ |
| canvases:read | your slack app will be able to access contents of canvases created inside Slack. | ❌ |
| canvases:write | your slack app will be able to create, edit and remove canvases. | ❌ |
| channels:history | View messages and other content in public channels that your slack app has been added to | ❌ |
| channels:join | Join public channels in a workspace | ❌ |
| channels:manage | Manage public channels that your slack app has been added to and create new ones | ❌ |
| channels:read | View basic information about public channels in a workspace | ❌ |
| channels:write.invites | Invite members to public channels | ❌ |
| channels:write.topic | Set the description of public channels | ❌ |
| chat:write | Post messages in approved channels & conversations | ✅ |
| chat:write.customize | Send messages as @your_slack_app with a customized username and avatar | ❌ |
| chat:write.public | Send messages to channels @your_slack_app isn't a member of | ❌ |
| commands | Add shortcuts and/or slash commands that people can use | ❌ |
| conversations.connect:manage | Allows your slack app to manage Slack Connect channels | ❌ |
| conversations.connect:read | Receive Slack Connect invite events sent to the channels your slack app is in | ❌ |
| conversations.connect:write | Create Slack Connect invitations for channels that your slack app has been added to, and accept invitations sent to your slack app | ❌ |
| datastore:read | View and see data from Slack App Datastore | ❌ |
| datastore:write | Write data to Slack App Datastore | ❌ |
| dnd:read | View Do Not Disturb settings for people in a workspace | ❌ |
| emoji:read | View custom emoji in a workspace | ❌ |
| files:read | View files shared in channels and conversations that your slack app has been added to | ❌ |
| files:write | Upload, edit, and delete files as your slack app | ❌ |
| groups:history | View messages and other content in private channels that your slack app has been added to | ❌ |
| groups:read | View basic information about private channels that your slack app has been added to | ❌ |
| groups:write | Manage private channels that your slack app has been added to and create new ones | ❌ |
| groups:write.invites | Invite members to private channels | ❌ |
| groups:write.topic | Set the description of private channels | ❌ |
| im:history | View messages and other content in direct messages that your slack app has been added to | ❌ |
| im:read | View basic information about direct messages that your slack app has been added to | ❌ |
| im:write | Start direct messages with people | ❌ |
| im:write.topic | Set the description in direct messages | ❌ |
| incoming-webhook | Create one-way webhooks to post messages to a specific channel | ❌ |
| links.embed:write | Embed video player URLs in messages and app surfaces | ❌ |
| links:read | View URLs in messages | ❌ |
| links:write | Show previews of URLs in messages | ❌ |
| metadata.message:read | Allows your slack app to read message metadata in channels that your slack app has been added to | ❌ |
| mpim:history | View messages and other content in group direct messages that your slack app has been added to | ❌ |
| mpim:read | View basic information about group direct messages that your slack app has been added to | ❌ |
| mpim:write | Start group direct messages with people | ❌ |
| mpim:write.topic | Set the description in group direct messages | ❌ |
| none | Execute methods without needing a scope | ❌ |
| pins:read | View pinned content in channels and conversations that your slack app has been added to | ❌ |
| pins:write | Add and remove pinned messages and files | ❌ |
| reactions:read | View emoji reactions and their associated content in channels and conversations that your slack app has been added to | ❌ |
| reactions:write | Add and edit emoji reactions | ❌ |
| reminders:read | View reminders created by your slack app | ❌ |
| reminders:write | Add, remove, or mark reminders as complete | ❌ |
| remote_files:read | View remote files added by the app in a workspace | ❌ |
| remote_files:share | Share remote files on a user's behalf | ❌ |
| remote_files:write | Add, edit, and delete remote files on a user's behalf | ❌ |
| search:read.files | Search a workspace's content in files | ❌ |
| search:read.im | Search a workspace's content in direct messages | ❌ |
| search:read.mpim | Search a workspace's content in group direct messages | ❌ |
| search:read.private | Search a workspace's content in private channels | ❌ |
| search:read.public | Search a workspace's content in public channels | ❌ |
| team.billing:read | Allows your slack app to read the billing plan for workspaces your slack app has been installed to | ❌ |
| team.preferences:read | Allows your slack app to read the preferences for workspaces your slack app has been installed to | ❌ |
| team:read | View the name, email domain, and icon for workspaces your slack app is connected to | ❌ |
| tokens.basic | Execute methods without needing a scope | ❌ |
| triggers:read | Read new Platform triggers | ❌ |
| triggers:write | Create new Platform triggers | ❌ |
| usergroups:read | View user groups in a workspace | ❌ |
| usergroups:write | Create and manage user groups | ❌ |
| users.profile:read | View profile details about people in a workspace | ❌ |
| users:read | View people in a workspace | ✅ |
| users:read.email | View email addresses of people in a workspace | ❌ |
| users:write | Set presence for your slack app | ❌ |
| workflow.steps:execute | Add steps that people can use in Workflow Builder | ❌ |
| workflows.templates:read | Read a workflow template | ❌ |
| workflows.templates:write | Write a workflow template | ❌ |

## Contact 

If you have questions, feel free to contact us via [AVIMBU](https://avimbu.com).
```

--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------

```dockerfile
FROM node:23-alpine

WORKDIR /app

COPY . ./

# Install dependencies
RUN npm install

# Build the application
RUN npm run build

# Command will be provided by smithery.yaml
CMD ["node", "dist/index.js"]

```

--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------

```json
{
  "compilerOptions": {
    "outDir": "./dist",
    "rootDir": "./src",
    "target": "ES2020",
    "module": "ESNext",
    "moduleResolution": "node",
    "esModuleInterop": true,
    "lib": ["ES2020", "DOM"],
    "strict": true,
    "skipLibCheck": true
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules"]
}
```

--------------------------------------------------------------------------------
/smithery.yaml:
--------------------------------------------------------------------------------

```yaml
startCommand:
  type: stdio
  configSchema:
    type: object
    required:
      - slackBotUserOauthToken
      - slackTeamId
    properties:
      slackBotUserOauthToken:
        type: string
        description: The OAuth Token of your Slack's App User.
      slackTeamId:
        type: string
        description: The Slack Team ID of the workspace.
  commandFunction:
    |-
    (config) => ({ command: 'node', args: ['dist/index.js'], env: { SLACK_BOT_USER_OAUTH_TOKEN: config.slackBotToken, SLACK_TEAM_ID: config.slackTeamId } })
```

--------------------------------------------------------------------------------
/src/slack/messages.ts:
--------------------------------------------------------------------------------

```typescript
import { Tool } from "@modelcontextprotocol/sdk/types.js";

export interface PostMessageArgs {
  channel_id: string;
  text: string;
}

export const postMessageTool: Tool = {
  name: "slack_post_message",
  description: "Post a new message to a Slack channel",
  inputSchema: {
    type: "object",
    properties: {
      channel_id: {
        type: "string",
        description: "The Channel ID to post the message to",
      },
      text: {
        type: "string",
        description: "The message text to post",
      },
    },
    required: ["channel_id", "text"],
  },
};

```

--------------------------------------------------------------------------------
/src/slack/users.ts:
--------------------------------------------------------------------------------

```typescript
import { Tool } from "@modelcontextprotocol/sdk/types.js";

export interface GetUsersArgs {
  cursor?: string;
  limit?: number;
}

export const getUsersTool: Tool = {
  name: "slack_get_users",
  description:
    "Get a list of all users in the workspace with basic information",
  inputSchema: {
    type: "object",
    properties: {
      cursor: {
        type: "string",
        description: "Pagination cursor for next page of results",
      },
      limit: {
        type: "number",
        description: "Maximum number of users to return (default 100, max 200)",
        default: 100,
      },
    },
  },
};

```

--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------

```json
{
    "name": "slack-model-context-protocol-server",
    "version": "0.0.1",
    "description": "MCP Interaction Server for Slack Workspaces",
    "license": "MIT",
    "author": "AVIMBU FlexCo, Austria avimbu.com",
    "homepage": "https://avimbu.com",
    "type": "module",
    "bin": {
        "mcp-slack-server": "dist/index.js"
    },
    "files": [
        "dist"
    ],
    "scripts": {
        "build": "tsc && shx chmod +x dist/*.js",
        "prepare": "npm run build",
        "watch": "tsc --watch"
    },
    "dependencies": {
        "@modelcontextprotocol/sdk": "1.7.0",
        "@types/node": "^22"
    },
    "devDependencies": {
        "@types/node": "^22",
        "shx": "^0.3.4",
        "typescript": "^5.8.2"
    }
}

```

--------------------------------------------------------------------------------
/src/slack/client.ts:
--------------------------------------------------------------------------------

```typescript
class SlackClient {
  public static readonly BOT_USER_OAUTH_TOKEN =
    process.env.SLACK_BOT_USER_OAUTH_TOKEN || "test-token";
  public static readonly TEAM_ID = process.env.SLACK_TEAM_ID || "team-id";

  private readonly botHeaders: {
    Authorization: string;
    "Content-Type": string;
  };

  constructor() {
    // Check if token is properly set
    if (
      !SlackClient.BOT_USER_OAUTH_TOKEN ||
      SlackClient.BOT_USER_OAUTH_TOKEN === "test-token"
    ) {
      console.warn(
        "Warning: Using default Slack token. Set SLACK_BOT_TOKEN environment variable for production use."
      );
    }

    if (!SlackClient.TEAM_ID || SlackClient.TEAM_ID === "team-id") {
      console.warn(
        "Warning: Using default Team ID. Set SLACK_TEAM_ID environment variable for production use."
      );
    }

    this.botHeaders = {
      Authorization: `Bearer ${SlackClient.BOT_USER_OAUTH_TOKEN}`,
      "Content-Type": "application/json",
    };
  }

  async postMessage(channel_id: string, text: string): Promise<any> {
    const response = await fetch("https://slack.com/api/chat.postMessage", {
      method: "POST",
      headers: this.botHeaders,
      body: JSON.stringify({
        channel: channel_id,
        text: text,
      }),
    });

    return response.json();
  }

  async getUsers(limit: number = 100, cursor?: string): Promise<any> {
    const params = new URLSearchParams({
      limit: Math.min(limit, 200).toString(),
      team_id: SlackClient.TEAM_ID,
    });

    if (cursor) {
      params.append("cursor", cursor);
    }

    const response = await fetch(`https://slack.com/api/users.list?${params}`, {
      headers: this.botHeaders,
    });

    return response.json();
  }
}

export const slackClient = new SlackClient();

```

--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------

```typescript
#!/usr/bin/env node
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import {
  CallToolRequest,
  CallToolRequestSchema,
  ListToolsRequestSchema,
} from "@modelcontextprotocol/sdk/types.js";
import { GetUsersArgs, getUsersTool } from "./slack/users.js";
import { PostMessageArgs, postMessageTool } from "./slack/messages.js";
import { slackClient } from "./slack/client.js";

const server = new Server(
  {
    name: "slack-model-context-protocol-server",
    version: "0.0.1",
  },
  {
    capabilities: {
      tools: {},
    },
  }
);

server.setRequestHandler(ListToolsRequestSchema, async () => {
  return {
    tools: [getUsersTool, postMessageTool],
  };
});

server.setRequestHandler(
  CallToolRequestSchema,
  async (request: CallToolRequest) => {
    try {
      if (!request.params.arguments) {
        throw new Error("Arguments are required");
      }

      switch (request.params.name) {
        case "slack_post_message": {
          const args = request.params.arguments as unknown as PostMessageArgs;
          if (!args.channel_id || !args.text) {
            throw new Error("Missing required arguments: channel_id and text");
          }
          const response = await slackClient.postMessage(
            args.channel_id,
            args.text
          );
          return {
            content: [{ type: "text", text: JSON.stringify(response) }],
          };
        }

        case "slack_get_users": {
          const args = request.params.arguments as unknown as GetUsersArgs;
          const response = await slackClient.getUsers(args.limit, args.cursor);
          return {
            content: [{ type: "text", text: JSON.stringify(response) }],
          };
        }

        default:
          throw new Error(`Unknown tool: ${request.params.name}`);
      }
    } catch (error) {
      console.error("Error executing tool:", error);
      return {
        content: [
          {
            type: "text",
            text: JSON.stringify({
              error: error instanceof Error ? error.message : String(error),
            }),
          },
        ],
      };
    }
  }
);

async function runServer() {
  const transport = new StdioServerTransport();
  await server.connect(transport);
  console.error("Slack MCP Server running on stdio");
}

runServer().catch((error) => {
  console.error("Fatal error in main():", error);
  process.exit(1);
});

```