@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/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 100)"
198
+ description: "Maximum users to return (default 500, supports pagination)"
187
199
  }
188
200
  }
189
201
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jtalk22/slack-mcp",
3
- "version": "1.0.4",
3
+ "version": "1.0.6",
4
4
  "description": "MCP server for Slack - Full access to DMs, channels, and messages from Claude. Browser token auth, no OAuth required.",
5
5
  "type": "module",
6
6
  "main": "src/server.js",
@@ -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
- * @version 1.0.0
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.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.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
- // 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;
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 {