@jtalk22/slack-mcp 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +295 -0
- package/docs/API.md +286 -0
- package/docs/SETUP.md +134 -0
- package/docs/TROUBLESHOOTING.md +216 -0
- package/docs/WEB-API.md +277 -0
- package/docs/images/demo-channel-messages.png +0 -0
- package/docs/images/demo-channels.png +0 -0
- package/docs/images/demo-main.png +0 -0
- package/docs/images/demo-messages.png +0 -0
- package/docs/images/demo-sidebar.png +0 -0
- package/lib/handlers.js +421 -0
- package/lib/slack-client.js +119 -0
- package/lib/token-store.js +184 -0
- package/lib/tools.js +191 -0
- package/package.json +70 -0
- package/public/demo.html +920 -0
- package/public/index.html +258 -0
- package/scripts/capture-screenshots.js +96 -0
- package/scripts/publish-public.sh +37 -0
- package/scripts/sync-from-onedrive.sh +33 -0
- package/scripts/sync-to-onedrive.sh +31 -0
- package/scripts/token-cli.js +157 -0
- package/src/server.js +118 -0
- package/src/web-server.js +256 -0
package/src/server.js
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Slack MCP Server
|
|
4
|
+
*
|
|
5
|
+
* A Model Context Protocol server for Slack integration.
|
|
6
|
+
* Provides read/write access to Slack messages, channels, and users.
|
|
7
|
+
*
|
|
8
|
+
* @version 1.0.0
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
12
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
13
|
+
import {
|
|
14
|
+
CallToolRequestSchema,
|
|
15
|
+
ListToolsRequestSchema,
|
|
16
|
+
} from "@modelcontextprotocol/sdk/types.js";
|
|
17
|
+
|
|
18
|
+
import { loadTokens } from "../lib/token-store.js";
|
|
19
|
+
import { TOOLS } from "../lib/tools.js";
|
|
20
|
+
import {
|
|
21
|
+
handleHealthCheck,
|
|
22
|
+
handleRefreshTokens,
|
|
23
|
+
handleListConversations,
|
|
24
|
+
handleConversationsHistory,
|
|
25
|
+
handleGetFullConversation,
|
|
26
|
+
handleSearchMessages,
|
|
27
|
+
handleUsersInfo,
|
|
28
|
+
handleSendMessage,
|
|
29
|
+
handleGetThread,
|
|
30
|
+
handleListUsers,
|
|
31
|
+
} from "../lib/handlers.js";
|
|
32
|
+
|
|
33
|
+
// Package info
|
|
34
|
+
const SERVER_NAME = "slack-mcp-server";
|
|
35
|
+
const SERVER_VERSION = "1.0.0";
|
|
36
|
+
|
|
37
|
+
// Initialize server
|
|
38
|
+
const server = new Server(
|
|
39
|
+
{ name: SERVER_NAME, version: SERVER_VERSION },
|
|
40
|
+
{ capabilities: { tools: {} } }
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
// Register tool list handler
|
|
44
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
45
|
+
tools: TOOLS
|
|
46
|
+
}));
|
|
47
|
+
|
|
48
|
+
// Register tool call handler
|
|
49
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
50
|
+
const { name, arguments: args } = request.params;
|
|
51
|
+
|
|
52
|
+
try {
|
|
53
|
+
switch (name) {
|
|
54
|
+
case "slack_health_check":
|
|
55
|
+
return await handleHealthCheck();
|
|
56
|
+
|
|
57
|
+
case "slack_refresh_tokens":
|
|
58
|
+
return await handleRefreshTokens();
|
|
59
|
+
|
|
60
|
+
case "slack_list_conversations":
|
|
61
|
+
return await handleListConversations(args);
|
|
62
|
+
|
|
63
|
+
case "slack_conversations_history":
|
|
64
|
+
return await handleConversationsHistory(args);
|
|
65
|
+
|
|
66
|
+
case "slack_get_full_conversation":
|
|
67
|
+
return await handleGetFullConversation(args);
|
|
68
|
+
|
|
69
|
+
case "slack_search_messages":
|
|
70
|
+
return await handleSearchMessages(args);
|
|
71
|
+
|
|
72
|
+
case "slack_users_info":
|
|
73
|
+
return await handleUsersInfo(args);
|
|
74
|
+
|
|
75
|
+
case "slack_send_message":
|
|
76
|
+
return await handleSendMessage(args);
|
|
77
|
+
|
|
78
|
+
case "slack_get_thread":
|
|
79
|
+
return await handleGetThread(args);
|
|
80
|
+
|
|
81
|
+
case "slack_list_users":
|
|
82
|
+
return await handleListUsers(args);
|
|
83
|
+
|
|
84
|
+
default:
|
|
85
|
+
return {
|
|
86
|
+
content: [{ type: "text", text: `Unknown tool: ${name}` }],
|
|
87
|
+
isError: true
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
} catch (error) {
|
|
91
|
+
return {
|
|
92
|
+
content: [{ type: "text", text: `Error: ${error.message}` }],
|
|
93
|
+
isError: true
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
// Main entry point
|
|
99
|
+
async function main() {
|
|
100
|
+
// Check for credentials at startup
|
|
101
|
+
const credentials = loadTokens();
|
|
102
|
+
if (!credentials) {
|
|
103
|
+
console.error("WARNING: No Slack credentials found at startup");
|
|
104
|
+
console.error("Will attempt Chrome auto-extraction on first API call");
|
|
105
|
+
} else {
|
|
106
|
+
console.error(`Credentials loaded from: ${credentials.source}`);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Start server
|
|
110
|
+
const transport = new StdioServerTransport();
|
|
111
|
+
await server.connect(transport);
|
|
112
|
+
console.error(`${SERVER_NAME} v${SERVER_VERSION} running`);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
main().catch(error => {
|
|
116
|
+
console.error("Fatal error:", error);
|
|
117
|
+
process.exit(1);
|
|
118
|
+
});
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Slack Web API Server
|
|
4
|
+
*
|
|
5
|
+
* Exposes Slack MCP tools as REST endpoints for browser access.
|
|
6
|
+
* Run alongside or instead of the MCP server for web-based access.
|
|
7
|
+
*
|
|
8
|
+
* @version 1.0.0
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import express from "express";
|
|
12
|
+
import { fileURLToPath } from "url";
|
|
13
|
+
import { dirname, join } from "path";
|
|
14
|
+
import { loadTokens } from "../lib/token-store.js";
|
|
15
|
+
|
|
16
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
17
|
+
import {
|
|
18
|
+
handleHealthCheck,
|
|
19
|
+
handleRefreshTokens,
|
|
20
|
+
handleListConversations,
|
|
21
|
+
handleConversationsHistory,
|
|
22
|
+
handleGetFullConversation,
|
|
23
|
+
handleSearchMessages,
|
|
24
|
+
handleUsersInfo,
|
|
25
|
+
handleSendMessage,
|
|
26
|
+
handleGetThread,
|
|
27
|
+
handleListUsers,
|
|
28
|
+
} from "../lib/handlers.js";
|
|
29
|
+
|
|
30
|
+
const app = express();
|
|
31
|
+
const PORT = process.env.PORT || 3000;
|
|
32
|
+
|
|
33
|
+
// Default API key for convenience - override with SLACK_API_KEY env var for production
|
|
34
|
+
const DEFAULT_API_KEY = "slack-mcp-local";
|
|
35
|
+
const API_KEY = process.env.SLACK_API_KEY || DEFAULT_API_KEY;
|
|
36
|
+
|
|
37
|
+
// Middleware
|
|
38
|
+
app.use(express.json());
|
|
39
|
+
app.use(express.static(join(__dirname, "../public")));
|
|
40
|
+
|
|
41
|
+
// CORS for local development
|
|
42
|
+
app.use((req, res, next) => {
|
|
43
|
+
res.header("Access-Control-Allow-Origin", "*");
|
|
44
|
+
res.header("Access-Control-Allow-Headers", "Content-Type, Authorization");
|
|
45
|
+
res.header("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
|
|
46
|
+
if (req.method === "OPTIONS") {
|
|
47
|
+
return res.sendStatus(200);
|
|
48
|
+
}
|
|
49
|
+
next();
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
// API Key authentication
|
|
53
|
+
function authenticate(req, res, next) {
|
|
54
|
+
const authHeader = req.headers.authorization;
|
|
55
|
+
const providedKey = authHeader?.replace("Bearer ", "");
|
|
56
|
+
|
|
57
|
+
if (providedKey !== API_KEY) {
|
|
58
|
+
return res.status(401).json({ error: "Invalid API key" });
|
|
59
|
+
}
|
|
60
|
+
next();
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Helper to extract response content
|
|
64
|
+
function extractContent(result) {
|
|
65
|
+
if (result.content && result.content[0] && result.content[0].text) {
|
|
66
|
+
try {
|
|
67
|
+
return JSON.parse(result.content[0].text);
|
|
68
|
+
} catch {
|
|
69
|
+
return { text: result.content[0].text };
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return result;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Root endpoint (no auth required)
|
|
76
|
+
app.get("/", (req, res) => {
|
|
77
|
+
res.json({
|
|
78
|
+
name: "Slack Web API Server",
|
|
79
|
+
version: "1.0.0",
|
|
80
|
+
status: "running",
|
|
81
|
+
endpoints: [
|
|
82
|
+
"GET /health",
|
|
83
|
+
"POST /refresh",
|
|
84
|
+
"GET /conversations",
|
|
85
|
+
"GET /conversations/:id/history",
|
|
86
|
+
"GET /conversations/:id/full",
|
|
87
|
+
"GET /conversations/:id/thread/:ts",
|
|
88
|
+
"GET /search",
|
|
89
|
+
"POST /messages",
|
|
90
|
+
"GET /users",
|
|
91
|
+
"GET /users/:id",
|
|
92
|
+
],
|
|
93
|
+
docs: "Add Authorization: Bearer <api-key> header to all requests"
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
// Health check
|
|
98
|
+
app.get("/health", authenticate, async (req, res) => {
|
|
99
|
+
try {
|
|
100
|
+
const result = await handleHealthCheck();
|
|
101
|
+
res.json(extractContent(result));
|
|
102
|
+
} catch (e) {
|
|
103
|
+
res.status(500).json({ error: e.message });
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
// Refresh tokens
|
|
108
|
+
app.post("/refresh", authenticate, async (req, res) => {
|
|
109
|
+
try {
|
|
110
|
+
const result = await handleRefreshTokens();
|
|
111
|
+
res.json(extractContent(result));
|
|
112
|
+
} catch (e) {
|
|
113
|
+
res.status(500).json({ error: e.message });
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
// List conversations
|
|
118
|
+
app.get("/conversations", authenticate, async (req, res) => {
|
|
119
|
+
try {
|
|
120
|
+
const result = await handleListConversations({
|
|
121
|
+
types: req.query.types || "im,mpim",
|
|
122
|
+
limit: parseInt(req.query.limit) || 100
|
|
123
|
+
});
|
|
124
|
+
res.json(extractContent(result));
|
|
125
|
+
} catch (e) {
|
|
126
|
+
res.status(500).json({ error: e.message });
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
// Get conversation history
|
|
131
|
+
app.get("/conversations/:id/history", authenticate, async (req, res) => {
|
|
132
|
+
try {
|
|
133
|
+
const result = await handleConversationsHistory({
|
|
134
|
+
channel_id: req.params.id,
|
|
135
|
+
limit: parseInt(req.query.limit) || 50,
|
|
136
|
+
oldest: req.query.oldest,
|
|
137
|
+
latest: req.query.latest,
|
|
138
|
+
resolve_users: req.query.resolve_users !== "false"
|
|
139
|
+
});
|
|
140
|
+
res.json(extractContent(result));
|
|
141
|
+
} catch (e) {
|
|
142
|
+
res.status(500).json({ error: e.message });
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
// Get full conversation with threads
|
|
147
|
+
app.get("/conversations/:id/full", authenticate, async (req, res) => {
|
|
148
|
+
try {
|
|
149
|
+
const result = await handleGetFullConversation({
|
|
150
|
+
channel_id: req.params.id,
|
|
151
|
+
oldest: req.query.oldest,
|
|
152
|
+
latest: req.query.latest,
|
|
153
|
+
max_messages: parseInt(req.query.max_messages) || 2000,
|
|
154
|
+
include_threads: req.query.include_threads !== "false",
|
|
155
|
+
output_file: req.query.output_file
|
|
156
|
+
});
|
|
157
|
+
res.json(extractContent(result));
|
|
158
|
+
} catch (e) {
|
|
159
|
+
res.status(500).json({ error: e.message });
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
// Get thread
|
|
164
|
+
app.get("/conversations/:id/thread/:ts", authenticate, async (req, res) => {
|
|
165
|
+
try {
|
|
166
|
+
const result = await handleGetThread({
|
|
167
|
+
channel_id: req.params.id,
|
|
168
|
+
thread_ts: req.params.ts
|
|
169
|
+
});
|
|
170
|
+
res.json(extractContent(result));
|
|
171
|
+
} catch (e) {
|
|
172
|
+
res.status(500).json({ error: e.message });
|
|
173
|
+
}
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
// Search messages
|
|
177
|
+
app.get("/search", authenticate, async (req, res) => {
|
|
178
|
+
try {
|
|
179
|
+
if (!req.query.q) {
|
|
180
|
+
return res.status(400).json({ error: "Query parameter 'q' is required" });
|
|
181
|
+
}
|
|
182
|
+
const result = await handleSearchMessages({
|
|
183
|
+
query: req.query.q,
|
|
184
|
+
count: parseInt(req.query.count) || 20
|
|
185
|
+
});
|
|
186
|
+
res.json(extractContent(result));
|
|
187
|
+
} catch (e) {
|
|
188
|
+
res.status(500).json({ error: e.message });
|
|
189
|
+
}
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
// Send message
|
|
193
|
+
app.post("/messages", authenticate, async (req, res) => {
|
|
194
|
+
try {
|
|
195
|
+
if (!req.body.channel_id || !req.body.text) {
|
|
196
|
+
return res.status(400).json({ error: "channel_id and text are required" });
|
|
197
|
+
}
|
|
198
|
+
const result = await handleSendMessage({
|
|
199
|
+
channel_id: req.body.channel_id,
|
|
200
|
+
text: req.body.text,
|
|
201
|
+
thread_ts: req.body.thread_ts
|
|
202
|
+
});
|
|
203
|
+
res.json(extractContent(result));
|
|
204
|
+
} catch (e) {
|
|
205
|
+
res.status(500).json({ error: e.message });
|
|
206
|
+
}
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
// List users
|
|
210
|
+
app.get("/users", authenticate, async (req, res) => {
|
|
211
|
+
try {
|
|
212
|
+
const result = await handleListUsers({
|
|
213
|
+
limit: parseInt(req.query.limit) || 100
|
|
214
|
+
});
|
|
215
|
+
res.json(extractContent(result));
|
|
216
|
+
} catch (e) {
|
|
217
|
+
res.status(500).json({ error: e.message });
|
|
218
|
+
}
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
// Get user info
|
|
222
|
+
app.get("/users/:id", authenticate, async (req, res) => {
|
|
223
|
+
try {
|
|
224
|
+
const result = await handleUsersInfo({
|
|
225
|
+
user_id: req.params.id
|
|
226
|
+
});
|
|
227
|
+
res.json(extractContent(result));
|
|
228
|
+
} catch (e) {
|
|
229
|
+
res.status(500).json({ error: e.message });
|
|
230
|
+
}
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
// Start server
|
|
234
|
+
async function main() {
|
|
235
|
+
// Check for credentials
|
|
236
|
+
const credentials = loadTokens();
|
|
237
|
+
if (!credentials) {
|
|
238
|
+
console.error("WARNING: No Slack credentials found");
|
|
239
|
+
console.error("Run: npm run tokens:auto");
|
|
240
|
+
} else {
|
|
241
|
+
console.log(`Credentials loaded from: ${credentials.source}`);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
app.listen(PORT, () => {
|
|
245
|
+
console.log(`\nš Slack Web API Server running at http://localhost:${PORT}`);
|
|
246
|
+
console.log(`\nš API Key: ${API_KEY}`);
|
|
247
|
+
console.log(`\nExample usage:`);
|
|
248
|
+
console.log(` curl -H "Authorization: Bearer ${API_KEY}" http://localhost:${PORT}/health`);
|
|
249
|
+
console.log(` curl -H "Authorization: Bearer ${API_KEY}" http://localhost:${PORT}/conversations`);
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
main().catch(e => {
|
|
254
|
+
console.error("Fatal error:", e);
|
|
255
|
+
process.exit(1);
|
|
256
|
+
});
|