@jtalk22/slack-mcp 1.0.4 → 1.0.6
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/README.md +265 -194
- package/lib/handlers.js +182 -30
- package/lib/slack-client.js +168 -21
- package/lib/token-store.js +84 -10
- package/lib/tools.js +14 -2
- package/package.json +1 -1
- package/scripts/verify-v106.js +159 -0
- package/src/server.js +35 -2
- package/src/web-server.js +43 -4
package/lib/tools.js
CHANGED
|
@@ -5,6 +5,14 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
export const TOOLS = [
|
|
8
|
+
{
|
|
9
|
+
name: "slack_token_status",
|
|
10
|
+
description: "Check token health, age, auto-refresh status, and cache stats",
|
|
11
|
+
inputSchema: {
|
|
12
|
+
type: "object",
|
|
13
|
+
properties: {}
|
|
14
|
+
}
|
|
15
|
+
},
|
|
8
16
|
{
|
|
9
17
|
name: "slack_health_check",
|
|
10
18
|
description: "Check if Slack tokens are valid and show authentication status",
|
|
@@ -23,7 +31,7 @@ export const TOOLS = [
|
|
|
23
31
|
},
|
|
24
32
|
{
|
|
25
33
|
name: "slack_list_conversations",
|
|
26
|
-
description: "List all DMs and channels with user names resolved",
|
|
34
|
+
description: "List all DMs and channels with user names resolved. Uses cached DMs by default for speed.",
|
|
27
35
|
inputSchema: {
|
|
28
36
|
type: "object",
|
|
29
37
|
properties: {
|
|
@@ -35,6 +43,10 @@ export const TOOLS = [
|
|
|
35
43
|
limit: {
|
|
36
44
|
type: "number",
|
|
37
45
|
description: "Maximum results (default 100)"
|
|
46
|
+
},
|
|
47
|
+
discover_dms: {
|
|
48
|
+
type: "boolean",
|
|
49
|
+
description: "If true, actively discover all DMs (slower, may hit rate limits on large workspaces). Default false uses cached DMs."
|
|
38
50
|
}
|
|
39
51
|
}
|
|
40
52
|
}
|
|
@@ -183,7 +195,7 @@ export const TOOLS = [
|
|
|
183
195
|
properties: {
|
|
184
196
|
limit: {
|
|
185
197
|
type: "number",
|
|
186
|
-
description: "Maximum users to return (default
|
|
198
|
+
description: "Maximum users to return (default 500, supports pagination)"
|
|
187
199
|
}
|
|
188
200
|
}
|
|
189
201
|
}
|
package/package.json
CHANGED
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* v1.0.6 Verification Script
|
|
4
|
+
*
|
|
5
|
+
* Tests:
|
|
6
|
+
* 1. Atomic write - no .tmp artifacts remain after write
|
|
7
|
+
* 2. Server exits cleanly (unref timer doesn't cause zombie)
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { writeFileSync, readFileSync, existsSync, renameSync, unlinkSync, readdirSync } from "fs";
|
|
11
|
+
import { spawn } from "child_process";
|
|
12
|
+
import { homedir } from "os";
|
|
13
|
+
import { join, dirname } from "path";
|
|
14
|
+
import { fileURLToPath } from "url";
|
|
15
|
+
|
|
16
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
17
|
+
const TEST_DIR = join(homedir(), ".slack-mcp-test");
|
|
18
|
+
const TEST_FILE = join(TEST_DIR, "test-atomic.json");
|
|
19
|
+
|
|
20
|
+
// ============ Test 1: Atomic Write ============
|
|
21
|
+
|
|
22
|
+
function atomicWriteSync(filePath, content) {
|
|
23
|
+
const tempPath = `${filePath}.${process.pid}.tmp`;
|
|
24
|
+
try {
|
|
25
|
+
writeFileSync(tempPath, content);
|
|
26
|
+
renameSync(tempPath, filePath);
|
|
27
|
+
} catch (e) {
|
|
28
|
+
try { unlinkSync(tempPath); } catch {}
|
|
29
|
+
throw e;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async function testAtomicWrite() {
|
|
34
|
+
console.log("\n[TEST 1] Atomic Write");
|
|
35
|
+
console.log("─".repeat(40));
|
|
36
|
+
|
|
37
|
+
// Setup - ensure test directory exists
|
|
38
|
+
const { execSync } = await import("child_process");
|
|
39
|
+
try {
|
|
40
|
+
execSync(`mkdir -p "${TEST_DIR}"`);
|
|
41
|
+
} catch {}
|
|
42
|
+
|
|
43
|
+
// Test successful write
|
|
44
|
+
const testData = { test: "data", timestamp: Date.now() };
|
|
45
|
+
atomicWriteSync(TEST_FILE, JSON.stringify(testData, null, 2));
|
|
46
|
+
|
|
47
|
+
// Verify file exists
|
|
48
|
+
if (!existsSync(TEST_FILE)) {
|
|
49
|
+
console.log(" FAIL: File was not created");
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Verify content
|
|
54
|
+
const readBack = JSON.parse(readFileSync(TEST_FILE, "utf-8"));
|
|
55
|
+
if (readBack.test !== "data") {
|
|
56
|
+
console.log(" FAIL: Content mismatch");
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Check for .tmp artifacts in test dir
|
|
61
|
+
const files = readdirSync(TEST_DIR);
|
|
62
|
+
const tmpFiles = files.filter(f => f.endsWith(".tmp"));
|
|
63
|
+
if (tmpFiles.length > 0) {
|
|
64
|
+
console.log(` FAIL: Found .tmp artifacts: ${tmpFiles.join(", ")}`);
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Cleanup
|
|
69
|
+
try { unlinkSync(TEST_FILE); } catch {}
|
|
70
|
+
|
|
71
|
+
console.log(" PASS: Atomic write completed, no .tmp artifacts");
|
|
72
|
+
return true;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// ============ Test 2: Server Exit (No Zombie) ============
|
|
76
|
+
|
|
77
|
+
async function testServerExit() {
|
|
78
|
+
console.log("\n[TEST 2] Server Clean Exit (No Zombie)");
|
|
79
|
+
console.log("─".repeat(40));
|
|
80
|
+
|
|
81
|
+
const serverPath = join(__dirname, "../src/server.js");
|
|
82
|
+
|
|
83
|
+
return new Promise((resolve) => {
|
|
84
|
+
const timeout = 5000; // 5 second timeout
|
|
85
|
+
let exitCode = null;
|
|
86
|
+
let timedOut = false;
|
|
87
|
+
|
|
88
|
+
// Spawn server process
|
|
89
|
+
const proc = spawn("node", [serverPath], {
|
|
90
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
91
|
+
env: { ...process.env, SLACK_TOKEN: "test", SLACK_COOKIE: "test" }
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
// Set timeout - if process doesn't exit after stdin closes, it's a zombie
|
|
95
|
+
const timer = setTimeout(() => {
|
|
96
|
+
timedOut = true;
|
|
97
|
+
console.log(" FAIL: Server did not exit within 5s (zombie process detected)");
|
|
98
|
+
proc.kill("SIGKILL");
|
|
99
|
+
resolve(false);
|
|
100
|
+
}, timeout);
|
|
101
|
+
|
|
102
|
+
proc.on("exit", (code) => {
|
|
103
|
+
exitCode = code;
|
|
104
|
+
clearTimeout(timer);
|
|
105
|
+
if (!timedOut) {
|
|
106
|
+
console.log(` PASS: Server exited cleanly (code: ${code})`);
|
|
107
|
+
resolve(true);
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
proc.on("error", (err) => {
|
|
112
|
+
clearTimeout(timer);
|
|
113
|
+
console.log(` INFO: Server spawn error (expected if no SDK): ${err.message}`);
|
|
114
|
+
// This is OK - we're testing exit behavior, not full functionality
|
|
115
|
+
resolve(true);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
// Close stdin immediately to simulate MCP client disconnect
|
|
119
|
+
proc.stdin.end();
|
|
120
|
+
|
|
121
|
+
// Give it a moment then send SIGTERM
|
|
122
|
+
setTimeout(() => {
|
|
123
|
+
if (exitCode === null && !timedOut) {
|
|
124
|
+
proc.kill("SIGTERM");
|
|
125
|
+
}
|
|
126
|
+
}, 1000);
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// ============ Main ============
|
|
131
|
+
|
|
132
|
+
async function main() {
|
|
133
|
+
console.log("╔════════════════════════════════════════╗");
|
|
134
|
+
console.log("║ v1.0.6 Verification Tests ║");
|
|
135
|
+
console.log("╚════════════════════════════════════════╝");
|
|
136
|
+
|
|
137
|
+
const results = [];
|
|
138
|
+
|
|
139
|
+
results.push(await testAtomicWrite());
|
|
140
|
+
results.push(await testServerExit());
|
|
141
|
+
|
|
142
|
+
console.log("\n" + "═".repeat(40));
|
|
143
|
+
const passed = results.filter(r => r).length;
|
|
144
|
+
const total = results.length;
|
|
145
|
+
|
|
146
|
+
if (passed === total) {
|
|
147
|
+
console.log(`\n✓ ALL TESTS PASSED (${passed}/${total})`);
|
|
148
|
+
console.log("\nReady to deploy v1.0.6");
|
|
149
|
+
process.exit(0);
|
|
150
|
+
} else {
|
|
151
|
+
console.log(`\n✗ TESTS FAILED (${passed}/${total})`);
|
|
152
|
+
process.exit(1);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
main().catch(e => {
|
|
157
|
+
console.error("Test error:", e);
|
|
158
|
+
process.exit(1);
|
|
159
|
+
});
|
package/src/server.js
CHANGED
|
@@ -5,7 +5,13 @@
|
|
|
5
5
|
* A Model Context Protocol server for Slack integration.
|
|
6
6
|
* Provides read/write access to Slack messages, channels, and users.
|
|
7
7
|
*
|
|
8
|
-
*
|
|
8
|
+
* Features:
|
|
9
|
+
* - Automatic token refresh from Chrome
|
|
10
|
+
* - LRU user cache with TTL
|
|
11
|
+
* - Network error retry with exponential backoff
|
|
12
|
+
* - Background token health monitoring
|
|
13
|
+
*
|
|
14
|
+
* @version 1.0.6
|
|
9
15
|
*/
|
|
10
16
|
|
|
11
17
|
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
@@ -16,8 +22,10 @@ import {
|
|
|
16
22
|
} from "@modelcontextprotocol/sdk/types.js";
|
|
17
23
|
|
|
18
24
|
import { loadTokens } from "../lib/token-store.js";
|
|
25
|
+
import { checkTokenHealth } from "../lib/slack-client.js";
|
|
19
26
|
import { TOOLS } from "../lib/tools.js";
|
|
20
27
|
import {
|
|
28
|
+
handleTokenStatus,
|
|
21
29
|
handleHealthCheck,
|
|
22
30
|
handleRefreshTokens,
|
|
23
31
|
handleListConversations,
|
|
@@ -30,9 +38,12 @@ import {
|
|
|
30
38
|
handleListUsers,
|
|
31
39
|
} from "../lib/handlers.js";
|
|
32
40
|
|
|
41
|
+
// Background refresh interval (4 hours)
|
|
42
|
+
const BACKGROUND_REFRESH_INTERVAL = 4 * 60 * 60 * 1000;
|
|
43
|
+
|
|
33
44
|
// Package info
|
|
34
45
|
const SERVER_NAME = "slack-mcp-server";
|
|
35
|
-
const SERVER_VERSION = "1.0.
|
|
46
|
+
const SERVER_VERSION = "1.0.6";
|
|
36
47
|
|
|
37
48
|
// Initialize server
|
|
38
49
|
const server = new Server(
|
|
@@ -51,6 +62,9 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
51
62
|
|
|
52
63
|
try {
|
|
53
64
|
switch (name) {
|
|
65
|
+
case "slack_token_status":
|
|
66
|
+
return await handleTokenStatus();
|
|
67
|
+
|
|
54
68
|
case "slack_health_check":
|
|
55
69
|
return await handleHealthCheck();
|
|
56
70
|
|
|
@@ -104,8 +118,27 @@ async function main() {
|
|
|
104
118
|
console.error("Will attempt Chrome auto-extraction on first API call");
|
|
105
119
|
} else {
|
|
106
120
|
console.error(`Credentials loaded from: ${credentials.source}`);
|
|
121
|
+
|
|
122
|
+
// Check token health on startup
|
|
123
|
+
const health = await checkTokenHealth({ error: () => {} });
|
|
124
|
+
if (health.warning) {
|
|
125
|
+
console.error(`Token age: ${health.age_hours}h - ${health.message}`);
|
|
126
|
+
}
|
|
107
127
|
}
|
|
108
128
|
|
|
129
|
+
// Background token health check (every 4 hours)
|
|
130
|
+
// Use unref() so this timer doesn't prevent the process from exiting
|
|
131
|
+
// when the MCP transport closes (prevents zombie processes)
|
|
132
|
+
const backgroundTimer = setInterval(async () => {
|
|
133
|
+
const health = await checkTokenHealth(console);
|
|
134
|
+
if (health.refreshed) {
|
|
135
|
+
console.error("Background: tokens refreshed successfully");
|
|
136
|
+
} else if (health.critical) {
|
|
137
|
+
console.error("Background: tokens critical - open Slack in Chrome");
|
|
138
|
+
}
|
|
139
|
+
}, BACKGROUND_REFRESH_INTERVAL);
|
|
140
|
+
backgroundTimer.unref();
|
|
141
|
+
|
|
109
142
|
// Start server
|
|
110
143
|
const transport = new StdioServerTransport();
|
|
111
144
|
await server.connect(transport);
|
package/src/web-server.js
CHANGED
|
@@ -5,16 +5,21 @@
|
|
|
5
5
|
* Exposes Slack MCP tools as REST endpoints for browser access.
|
|
6
6
|
* Run alongside or instead of the MCP server for web-based access.
|
|
7
7
|
*
|
|
8
|
-
* @version 1.0.
|
|
8
|
+
* @version 1.0.5
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
import express from "express";
|
|
12
|
+
import { randomBytes } from "crypto";
|
|
12
13
|
import { fileURLToPath } from "url";
|
|
13
14
|
import { dirname, join } from "path";
|
|
15
|
+
import { existsSync, readFileSync, writeFileSync } from "fs";
|
|
16
|
+
import { execSync } from "child_process";
|
|
17
|
+
import { homedir } from "os";
|
|
14
18
|
import { loadTokens } from "../lib/token-store.js";
|
|
15
19
|
|
|
16
20
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
17
21
|
import {
|
|
22
|
+
handleTokenStatus,
|
|
18
23
|
handleHealthCheck,
|
|
19
24
|
handleRefreshTokens,
|
|
20
25
|
handleListConversations,
|
|
@@ -30,9 +35,33 @@ import {
|
|
|
30
35
|
const app = express();
|
|
31
36
|
const PORT = process.env.PORT || 3000;
|
|
32
37
|
|
|
33
|
-
//
|
|
34
|
-
const
|
|
35
|
-
|
|
38
|
+
// Secure API key management
|
|
39
|
+
const API_KEY_FILE = join(homedir(), ".slack-mcp-api-key");
|
|
40
|
+
|
|
41
|
+
function getOrCreateAPIKey() {
|
|
42
|
+
// Priority 1: Environment variable
|
|
43
|
+
if (process.env.SLACK_API_KEY) {
|
|
44
|
+
return process.env.SLACK_API_KEY;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Priority 2: Key file
|
|
48
|
+
if (existsSync(API_KEY_FILE)) {
|
|
49
|
+
try {
|
|
50
|
+
return readFileSync(API_KEY_FILE, "utf-8").trim();
|
|
51
|
+
} catch {}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Priority 3: Generate new secure key
|
|
55
|
+
const newKey = `smcp_${randomBytes(24).toString('base64url')}`;
|
|
56
|
+
try {
|
|
57
|
+
writeFileSync(API_KEY_FILE, newKey);
|
|
58
|
+
execSync(`chmod 600 "${API_KEY_FILE}"`);
|
|
59
|
+
} catch {}
|
|
60
|
+
|
|
61
|
+
return newKey;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const API_KEY = getOrCreateAPIKey();
|
|
36
65
|
|
|
37
66
|
// Middleware
|
|
38
67
|
app.use(express.json());
|
|
@@ -94,6 +123,16 @@ app.get("/", (req, res) => {
|
|
|
94
123
|
});
|
|
95
124
|
});
|
|
96
125
|
|
|
126
|
+
// Token status (detailed health + cache info)
|
|
127
|
+
app.get("/token-status", authenticate, async (req, res) => {
|
|
128
|
+
try {
|
|
129
|
+
const result = await handleTokenStatus();
|
|
130
|
+
res.json(extractContent(result));
|
|
131
|
+
} catch (e) {
|
|
132
|
+
res.status(500).json({ error: e.message });
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
|
|
97
136
|
// Health check
|
|
98
137
|
app.get("/health", authenticate, async (req, res) => {
|
|
99
138
|
try {
|