# 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:
--------------------------------------------------------------------------------
```
1 | dist
2 | node_modules
```
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
```markdown
1 | # Slack Model Context Protocol Server
2 |
3 | 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.
4 |
5 | ## Local Development
6 |
7 | In order to run this client locally, add the following configuration to your Claude Desktop MCP Server config file:
8 |
9 | ```
10 | {
11 | "mcpServers": {
12 | "mcp-slack-local": {
13 | "command": "node",
14 | "args": ["/path/to/project/dist/index.js"], <---- replace this with your project path
15 | "env": {
16 | "SLACK_BOT_USER_OAUTH_TOKEN": "test-bot-token",
17 | "SLACK_TEAM_ID": "test-team-id"
18 | }
19 | },
20 | }
21 | }
22 | ```
23 |
24 | After this, you should be able to test this implementation in your Claude Desktop App using example prompts like:
25 |
26 | - "Can you list all users of my Slack team?"
27 | - "Can you send a welcome message to my Slack Channel with the ID `<channel id>`?"
28 |
29 | Running the server locally:
30 |
31 | ```
32 | node dist/index.js
33 | ```
34 |
35 | With the build in another terminal
36 |
37 | ```
38 | npm run watch
39 | ```
40 |
41 | ## Slack Permission Scopes
42 |
43 | The following permissions are already implemented:
44 |
45 | | Permission | Description | Implemented |
46 | |------------|-------------|-------------|
47 | | app_configurations:read | Read app configuration info via App Manifest APIs | ❌ |
48 | | app_configurations:write | Write app configuration info and create apps via App Manifest APIs | ❌ |
49 | | app_mentions:read | View messages that directly mention @your_slack_app in conversations that the app is in | ❌ |
50 | | assistant:write | Allow your slack app to act as an AI Assistant | ❌ |
51 | | bookmarks:read | List bookmarks | ❌ |
52 | | bookmarks:write | Create, edit, and remove bookmarks | ❌ |
53 | | calls:read | View information about ongoing and past calls | ❌ |
54 | | calls:write | Start and manage calls in a workspace | ❌ |
55 | | canvases:read | your slack app will be able to access contents of canvases created inside Slack. | ❌ |
56 | | canvases:write | your slack app will be able to create, edit and remove canvases. | ❌ |
57 | | channels:history | View messages and other content in public channels that your slack app has been added to | ❌ |
58 | | channels:join | Join public channels in a workspace | ❌ |
59 | | channels:manage | Manage public channels that your slack app has been added to and create new ones | ❌ |
60 | | channels:read | View basic information about public channels in a workspace | ❌ |
61 | | channels:write.invites | Invite members to public channels | ❌ |
62 | | channels:write.topic | Set the description of public channels | ❌ |
63 | | chat:write | Post messages in approved channels & conversations | ✅ |
64 | | chat:write.customize | Send messages as @your_slack_app with a customized username and avatar | ❌ |
65 | | chat:write.public | Send messages to channels @your_slack_app isn't a member of | ❌ |
66 | | commands | Add shortcuts and/or slash commands that people can use | ❌ |
67 | | conversations.connect:manage | Allows your slack app to manage Slack Connect channels | ❌ |
68 | | conversations.connect:read | Receive Slack Connect invite events sent to the channels your slack app is in | ❌ |
69 | | 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 | ❌ |
70 | | datastore:read | View and see data from Slack App Datastore | ❌ |
71 | | datastore:write | Write data to Slack App Datastore | ❌ |
72 | | dnd:read | View Do Not Disturb settings for people in a workspace | ❌ |
73 | | emoji:read | View custom emoji in a workspace | ❌ |
74 | | files:read | View files shared in channels and conversations that your slack app has been added to | ❌ |
75 | | files:write | Upload, edit, and delete files as your slack app | ❌ |
76 | | groups:history | View messages and other content in private channels that your slack app has been added to | ❌ |
77 | | groups:read | View basic information about private channels that your slack app has been added to | ❌ |
78 | | groups:write | Manage private channels that your slack app has been added to and create new ones | ❌ |
79 | | groups:write.invites | Invite members to private channels | ❌ |
80 | | groups:write.topic | Set the description of private channels | ❌ |
81 | | im:history | View messages and other content in direct messages that your slack app has been added to | ❌ |
82 | | im:read | View basic information about direct messages that your slack app has been added to | ❌ |
83 | | im:write | Start direct messages with people | ❌ |
84 | | im:write.topic | Set the description in direct messages | ❌ |
85 | | incoming-webhook | Create one-way webhooks to post messages to a specific channel | ❌ |
86 | | links.embed:write | Embed video player URLs in messages and app surfaces | ❌ |
87 | | links:read | View URLs in messages | ❌ |
88 | | links:write | Show previews of URLs in messages | ❌ |
89 | | metadata.message:read | Allows your slack app to read message metadata in channels that your slack app has been added to | ❌ |
90 | | mpim:history | View messages and other content in group direct messages that your slack app has been added to | ❌ |
91 | | mpim:read | View basic information about group direct messages that your slack app has been added to | ❌ |
92 | | mpim:write | Start group direct messages with people | ❌ |
93 | | mpim:write.topic | Set the description in group direct messages | ❌ |
94 | | none | Execute methods without needing a scope | ❌ |
95 | | pins:read | View pinned content in channels and conversations that your slack app has been added to | ❌ |
96 | | pins:write | Add and remove pinned messages and files | ❌ |
97 | | reactions:read | View emoji reactions and their associated content in channels and conversations that your slack app has been added to | ❌ |
98 | | reactions:write | Add and edit emoji reactions | ❌ |
99 | | reminders:read | View reminders created by your slack app | ❌ |
100 | | reminders:write | Add, remove, or mark reminders as complete | ❌ |
101 | | remote_files:read | View remote files added by the app in a workspace | ❌ |
102 | | remote_files:share | Share remote files on a user's behalf | ❌ |
103 | | remote_files:write | Add, edit, and delete remote files on a user's behalf | ❌ |
104 | | search:read.files | Search a workspace's content in files | ❌ |
105 | | search:read.im | Search a workspace's content in direct messages | ❌ |
106 | | search:read.mpim | Search a workspace's content in group direct messages | ❌ |
107 | | search:read.private | Search a workspace's content in private channels | ❌ |
108 | | search:read.public | Search a workspace's content in public channels | ❌ |
109 | | team.billing:read | Allows your slack app to read the billing plan for workspaces your slack app has been installed to | ❌ |
110 | | team.preferences:read | Allows your slack app to read the preferences for workspaces your slack app has been installed to | ❌ |
111 | | team:read | View the name, email domain, and icon for workspaces your slack app is connected to | ❌ |
112 | | tokens.basic | Execute methods without needing a scope | ❌ |
113 | | triggers:read | Read new Platform triggers | ❌ |
114 | | triggers:write | Create new Platform triggers | ❌ |
115 | | usergroups:read | View user groups in a workspace | ❌ |
116 | | usergroups:write | Create and manage user groups | ❌ |
117 | | users.profile:read | View profile details about people in a workspace | ❌ |
118 | | users:read | View people in a workspace | ✅ |
119 | | users:read.email | View email addresses of people in a workspace | ❌ |
120 | | users:write | Set presence for your slack app | ❌ |
121 | | workflow.steps:execute | Add steps that people can use in Workflow Builder | ❌ |
122 | | workflows.templates:read | Read a workflow template | ❌ |
123 | | workflows.templates:write | Write a workflow template | ❌ |
124 |
125 | ## Contact
126 |
127 | If you have questions, feel free to contact us via [AVIMBU](https://avimbu.com).
```
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
```dockerfile
1 | FROM node:23-alpine
2 |
3 | WORKDIR /app
4 |
5 | COPY . ./
6 |
7 | # Install dependencies
8 | RUN npm install
9 |
10 | # Build the application
11 | RUN npm run build
12 |
13 | # Command will be provided by smithery.yaml
14 | CMD ["node", "dist/index.js"]
15 |
```
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
```json
1 | {
2 | "compilerOptions": {
3 | "outDir": "./dist",
4 | "rootDir": "./src",
5 | "target": "ES2020",
6 | "module": "ESNext",
7 | "moduleResolution": "node",
8 | "esModuleInterop": true,
9 | "lib": ["ES2020", "DOM"],
10 | "strict": true,
11 | "skipLibCheck": true
12 | },
13 | "include": ["src/**/*"],
14 | "exclude": ["node_modules"]
15 | }
```
--------------------------------------------------------------------------------
/smithery.yaml:
--------------------------------------------------------------------------------
```yaml
1 | startCommand:
2 | type: stdio
3 | configSchema:
4 | type: object
5 | required:
6 | - slackBotUserOauthToken
7 | - slackTeamId
8 | properties:
9 | slackBotUserOauthToken:
10 | type: string
11 | description: The OAuth Token of your Slack's App User.
12 | slackTeamId:
13 | type: string
14 | description: The Slack Team ID of the workspace.
15 | commandFunction:
16 | |-
17 | (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
1 | import { Tool } from "@modelcontextprotocol/sdk/types.js";
2 |
3 | export interface PostMessageArgs {
4 | channel_id: string;
5 | text: string;
6 | }
7 |
8 | export const postMessageTool: Tool = {
9 | name: "slack_post_message",
10 | description: "Post a new message to a Slack channel",
11 | inputSchema: {
12 | type: "object",
13 | properties: {
14 | channel_id: {
15 | type: "string",
16 | description: "The Channel ID to post the message to",
17 | },
18 | text: {
19 | type: "string",
20 | description: "The message text to post",
21 | },
22 | },
23 | required: ["channel_id", "text"],
24 | },
25 | };
26 |
```
--------------------------------------------------------------------------------
/src/slack/users.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { Tool } from "@modelcontextprotocol/sdk/types.js";
2 |
3 | export interface GetUsersArgs {
4 | cursor?: string;
5 | limit?: number;
6 | }
7 |
8 | export const getUsersTool: Tool = {
9 | name: "slack_get_users",
10 | description:
11 | "Get a list of all users in the workspace with basic information",
12 | inputSchema: {
13 | type: "object",
14 | properties: {
15 | cursor: {
16 | type: "string",
17 | description: "Pagination cursor for next page of results",
18 | },
19 | limit: {
20 | type: "number",
21 | description: "Maximum number of users to return (default 100, max 200)",
22 | default: 100,
23 | },
24 | },
25 | },
26 | };
27 |
```
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
```json
1 | {
2 | "name": "slack-model-context-protocol-server",
3 | "version": "0.0.1",
4 | "description": "MCP Interaction Server for Slack Workspaces",
5 | "license": "MIT",
6 | "author": "AVIMBU FlexCo, Austria avimbu.com",
7 | "homepage": "https://avimbu.com",
8 | "type": "module",
9 | "bin": {
10 | "mcp-slack-server": "dist/index.js"
11 | },
12 | "files": [
13 | "dist"
14 | ],
15 | "scripts": {
16 | "build": "tsc && shx chmod +x dist/*.js",
17 | "prepare": "npm run build",
18 | "watch": "tsc --watch"
19 | },
20 | "dependencies": {
21 | "@modelcontextprotocol/sdk": "1.7.0",
22 | "@types/node": "^22"
23 | },
24 | "devDependencies": {
25 | "@types/node": "^22",
26 | "shx": "^0.3.4",
27 | "typescript": "^5.8.2"
28 | }
29 | }
30 |
```
--------------------------------------------------------------------------------
/src/slack/client.ts:
--------------------------------------------------------------------------------
```typescript
1 | class SlackClient {
2 | public static readonly BOT_USER_OAUTH_TOKEN =
3 | process.env.SLACK_BOT_USER_OAUTH_TOKEN || "test-token";
4 | public static readonly TEAM_ID = process.env.SLACK_TEAM_ID || "team-id";
5 |
6 | private readonly botHeaders: {
7 | Authorization: string;
8 | "Content-Type": string;
9 | };
10 |
11 | constructor() {
12 | // Check if token is properly set
13 | if (
14 | !SlackClient.BOT_USER_OAUTH_TOKEN ||
15 | SlackClient.BOT_USER_OAUTH_TOKEN === "test-token"
16 | ) {
17 | console.warn(
18 | "Warning: Using default Slack token. Set SLACK_BOT_TOKEN environment variable for production use."
19 | );
20 | }
21 |
22 | if (!SlackClient.TEAM_ID || SlackClient.TEAM_ID === "team-id") {
23 | console.warn(
24 | "Warning: Using default Team ID. Set SLACK_TEAM_ID environment variable for production use."
25 | );
26 | }
27 |
28 | this.botHeaders = {
29 | Authorization: `Bearer ${SlackClient.BOT_USER_OAUTH_TOKEN}`,
30 | "Content-Type": "application/json",
31 | };
32 | }
33 |
34 | async postMessage(channel_id: string, text: string): Promise<any> {
35 | const response = await fetch("https://slack.com/api/chat.postMessage", {
36 | method: "POST",
37 | headers: this.botHeaders,
38 | body: JSON.stringify({
39 | channel: channel_id,
40 | text: text,
41 | }),
42 | });
43 |
44 | return response.json();
45 | }
46 |
47 | async getUsers(limit: number = 100, cursor?: string): Promise<any> {
48 | const params = new URLSearchParams({
49 | limit: Math.min(limit, 200).toString(),
50 | team_id: SlackClient.TEAM_ID,
51 | });
52 |
53 | if (cursor) {
54 | params.append("cursor", cursor);
55 | }
56 |
57 | const response = await fetch(`https://slack.com/api/users.list?${params}`, {
58 | headers: this.botHeaders,
59 | });
60 |
61 | return response.json();
62 | }
63 | }
64 |
65 | export const slackClient = new SlackClient();
66 |
```
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
```typescript
1 | #!/usr/bin/env node
2 | import { Server } from "@modelcontextprotocol/sdk/server/index.js";
3 | import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4 | import {
5 | CallToolRequest,
6 | CallToolRequestSchema,
7 | ListToolsRequestSchema,
8 | } from "@modelcontextprotocol/sdk/types.js";
9 | import { GetUsersArgs, getUsersTool } from "./slack/users.js";
10 | import { PostMessageArgs, postMessageTool } from "./slack/messages.js";
11 | import { slackClient } from "./slack/client.js";
12 |
13 | const server = new Server(
14 | {
15 | name: "slack-model-context-protocol-server",
16 | version: "0.0.1",
17 | },
18 | {
19 | capabilities: {
20 | tools: {},
21 | },
22 | }
23 | );
24 |
25 | server.setRequestHandler(ListToolsRequestSchema, async () => {
26 | return {
27 | tools: [getUsersTool, postMessageTool],
28 | };
29 | });
30 |
31 | server.setRequestHandler(
32 | CallToolRequestSchema,
33 | async (request: CallToolRequest) => {
34 | try {
35 | if (!request.params.arguments) {
36 | throw new Error("Arguments are required");
37 | }
38 |
39 | switch (request.params.name) {
40 | case "slack_post_message": {
41 | const args = request.params.arguments as unknown as PostMessageArgs;
42 | if (!args.channel_id || !args.text) {
43 | throw new Error("Missing required arguments: channel_id and text");
44 | }
45 | const response = await slackClient.postMessage(
46 | args.channel_id,
47 | args.text
48 | );
49 | return {
50 | content: [{ type: "text", text: JSON.stringify(response) }],
51 | };
52 | }
53 |
54 | case "slack_get_users": {
55 | const args = request.params.arguments as unknown as GetUsersArgs;
56 | const response = await slackClient.getUsers(args.limit, args.cursor);
57 | return {
58 | content: [{ type: "text", text: JSON.stringify(response) }],
59 | };
60 | }
61 |
62 | default:
63 | throw new Error(`Unknown tool: ${request.params.name}`);
64 | }
65 | } catch (error) {
66 | console.error("Error executing tool:", error);
67 | return {
68 | content: [
69 | {
70 | type: "text",
71 | text: JSON.stringify({
72 | error: error instanceof Error ? error.message : String(error),
73 | }),
74 | },
75 | ],
76 | };
77 | }
78 | }
79 | );
80 |
81 | async function runServer() {
82 | const transport = new StdioServerTransport();
83 | await server.connect(transport);
84 | console.error("Slack MCP Server running on stdio");
85 | }
86 |
87 | runServer().catch((error) => {
88 | console.error("Fatal error in main():", error);
89 | process.exit(1);
90 | });
91 |
```