@jtalk22/slack-mcp 4.1.0 → 4.2.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.
@@ -0,0 +1,188 @@
1
+ /**
2
+ * Workflow Profile Store (local JSON, OSS-only)
3
+ *
4
+ * Stores user-defined workflow profiles that bind a workflow_kind
5
+ * (support_inbox | incident_room | exec_brief | product_launch_watch | custom)
6
+ * to a set of channels, priority people, retention mode, and summary cadence.
7
+ *
8
+ * The hosted AI brain (smart_search, catch_me_up, triage) consumes these
9
+ * profiles and returns structured JSON per the workflow_kind. The OSS
10
+ * package ships the profile primitives but not the AI brain — it's
11
+ * hosted-only because it needs Vectorize + Workers AI.
12
+ *
13
+ * File: ~/.slack-mcp-workflows.json (chmod 600)
14
+ * Atomic write pattern (temp → chmod → rename) prevents corruption from
15
+ * concurrent writes by multiple slack-mcp-server instances.
16
+ */
17
+
18
+ import { writeFileSync, readFileSync, existsSync, renameSync, unlinkSync, chmodSync } from "fs";
19
+ import { homedir, platform } from "os";
20
+ import { join } from "path";
21
+
22
+ const STORE_FILE = join(homedir(), ".slack-mcp-workflows.json");
23
+ const STORE_VERSION = 1;
24
+
25
+ const ALLOWED_WORKFLOW_KINDS = new Set([
26
+ "support_inbox",
27
+ "incident_room",
28
+ "exec_brief",
29
+ "product_launch_watch",
30
+ "custom",
31
+ ]);
32
+
33
+ const ALLOWED_RETENTION_MODES = new Set(["ephemeral", "persistent"]);
34
+ const ALLOWED_SUMMARY_CADENCES = new Set(["on_demand", "daily_8am", "weekly_monday"]);
35
+
36
+ const STRUCTURED_KEYS_BY_KIND = {
37
+ support_inbox: ["open_threads", "ack_lag", "owner_gaps", "escalations", "next_actions"],
38
+ incident_room: ["incident_summary", "timeline", "open_risks", "owner_gaps", "next_actions"],
39
+ exec_brief: ["summary", "decisions", "risks", "asks", "action_items"],
40
+ product_launch_watch: ["launch_signals", "feedback_themes", "blockers", "metrics", "next_actions"],
41
+ custom: ["summary", "highlights", "open_questions", "next_actions"],
42
+ };
43
+
44
+ function emptyStore() {
45
+ return { version: STORE_VERSION, profiles: {} };
46
+ }
47
+
48
+ function atomicWriteSync(filePath, content) {
49
+ const tempPath = `${filePath}.${process.pid}.tmp`;
50
+ try {
51
+ writeFileSync(tempPath, content);
52
+ if (platform() === "darwin" || platform() === "linux") {
53
+ try { chmodSync(tempPath, 0o600); } catch {}
54
+ }
55
+ renameSync(tempPath, filePath);
56
+ } catch (e) {
57
+ try { unlinkSync(tempPath); } catch {}
58
+ throw e;
59
+ }
60
+ }
61
+
62
+ export function loadStore() {
63
+ if (!existsSync(STORE_FILE)) return emptyStore();
64
+ let raw;
65
+ try {
66
+ raw = readFileSync(STORE_FILE, "utf-8");
67
+ } catch {
68
+ return emptyStore();
69
+ }
70
+ try {
71
+ const data = JSON.parse(raw);
72
+ if (!data || typeof data !== "object" || !data.profiles) {
73
+ backupCorruptStore(raw, "shape-invalid");
74
+ return emptyStore();
75
+ }
76
+ if (data.version !== STORE_VERSION) {
77
+ // Future: migration logic. For now, back up the unrecognized version
78
+ // before falling back to empty so the old data is recoverable.
79
+ backupCorruptStore(raw, `version-${data.version}`);
80
+ return emptyStore();
81
+ }
82
+ return data;
83
+ } catch {
84
+ backupCorruptStore(raw, "json-parse-error");
85
+ return emptyStore();
86
+ }
87
+ }
88
+
89
+ function backupCorruptStore(raw, reasonTag) {
90
+ try {
91
+ const stamp = new Date().toISOString().replace(/[:.]/g, "-");
92
+ const backupPath = `${STORE_FILE}.bak.${reasonTag}.${stamp}`;
93
+ writeFileSync(backupPath, raw);
94
+ if (platform() === "darwin" || platform() === "linux") {
95
+ try { chmodSync(backupPath, 0o600); } catch {}
96
+ }
97
+ } catch {
98
+ // Backup is best-effort; do not throw on backup failure.
99
+ }
100
+ }
101
+
102
+ export function saveStore(store) {
103
+ atomicWriteSync(STORE_FILE, JSON.stringify(store, null, 2));
104
+ }
105
+
106
+ export function structuredKeysFor(workflowKind) {
107
+ return STRUCTURED_KEYS_BY_KIND[workflowKind] || STRUCTURED_KEYS_BY_KIND.custom;
108
+ }
109
+
110
+ export function validateProfile(input) {
111
+ const errors = [];
112
+ if (!input || typeof input !== "object") {
113
+ return { valid: false, errors: ["profile must be an object"] };
114
+ }
115
+ if (!input.profile_name || typeof input.profile_name !== "string" || !input.profile_name.trim()) {
116
+ errors.push("profile_name is required (non-empty string)");
117
+ }
118
+ if (!input.workflow_kind || !ALLOWED_WORKFLOW_KINDS.has(input.workflow_kind)) {
119
+ errors.push(`workflow_kind must be one of: ${Array.from(ALLOWED_WORKFLOW_KINDS).join(", ")}`);
120
+ }
121
+ if (input.channels && (!Array.isArray(input.channels) || input.channels.some((c) => typeof c !== "string"))) {
122
+ errors.push("channels must be an array of strings (Slack channel IDs)");
123
+ }
124
+ if (input.priority_people && (!Array.isArray(input.priority_people) || input.priority_people.some((p) => typeof p !== "string"))) {
125
+ errors.push("priority_people must be an array of strings (Slack user IDs)");
126
+ }
127
+ if (input.retention_mode && !ALLOWED_RETENTION_MODES.has(input.retention_mode)) {
128
+ errors.push(`retention_mode must be one of: ${Array.from(ALLOWED_RETENTION_MODES).join(", ")}`);
129
+ }
130
+ if (input.summary_cadence && !ALLOWED_SUMMARY_CADENCES.has(input.summary_cadence)) {
131
+ errors.push(`summary_cadence must be one of: ${Array.from(ALLOWED_SUMMARY_CADENCES).join(", ")}`);
132
+ }
133
+ return { valid: errors.length === 0, errors };
134
+ }
135
+
136
+ export function saveProfile(input) {
137
+ const { valid, errors } = validateProfile(input);
138
+ if (!valid) {
139
+ return { ok: false, errors };
140
+ }
141
+ const store = loadStore();
142
+ const now = new Date().toISOString();
143
+ const existing = store.profiles[input.profile_name];
144
+ const profile = {
145
+ workflow_kind: input.workflow_kind,
146
+ channels: Array.isArray(input.channels) ? [...input.channels] : [],
147
+ priority_people: Array.isArray(input.priority_people) ? [...input.priority_people] : [],
148
+ retention_mode: input.retention_mode || "ephemeral",
149
+ summary_cadence: input.summary_cadence || "on_demand",
150
+ structured_keys: structuredKeysFor(input.workflow_kind),
151
+ created_at: existing && existing.created_at ? existing.created_at : now,
152
+ updated_at: now,
153
+ };
154
+ store.profiles[input.profile_name] = profile;
155
+ saveStore(store);
156
+ return { ok: true, profile_name: input.profile_name, profile };
157
+ }
158
+
159
+ export function listProfiles({ workflow_kind } = {}) {
160
+ const store = loadStore();
161
+ const entries = Object.entries(store.profiles).map(([name, profile]) => ({ profile_name: name, ...profile }));
162
+ if (workflow_kind) {
163
+ if (!ALLOWED_WORKFLOW_KINDS.has(workflow_kind)) {
164
+ return { ok: false, errors: [`workflow_kind filter must be one of: ${Array.from(ALLOWED_WORKFLOW_KINDS).join(", ")}`] };
165
+ }
166
+ return { ok: true, profiles: entries.filter((p) => p.workflow_kind === workflow_kind) };
167
+ }
168
+ return { ok: true, profiles: entries };
169
+ }
170
+
171
+ export function deleteProfile(profile_name) {
172
+ const store = loadStore();
173
+ if (!store.profiles[profile_name]) {
174
+ return { ok: false, errors: [`profile_name "${profile_name}" not found`] };
175
+ }
176
+ delete store.profiles[profile_name];
177
+ saveStore(store);
178
+ return { ok: true, profile_name };
179
+ }
180
+
181
+ export function getProfile(profile_name) {
182
+ const store = loadStore();
183
+ const profile = store.profiles[profile_name];
184
+ if (!profile) return null;
185
+ return { profile_name, ...profile };
186
+ }
187
+
188
+ export const ALLOWED_WORKFLOW_KINDS_LIST = Array.from(ALLOWED_WORKFLOW_KINDS);
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "@jtalk22/slack-mcp",
3
3
  "mcpName": "io.github.jtalk22/slack-mcp-server",
4
- "version": "4.1.0",
5
- "description": "Slack MCP without OAuth no app registration, no admin approval. Works with Claude Code, Cursor, Copilot (where the official server doesn't). 16 tools, one command.",
4
+ "version": "4.2.0",
5
+ "description": "Slack MCP without OAuth. 21 tools: 16 read/write Slack + 2 workflow profile primitives + 3 discoverable upgrade stubs to hosted AI brain. Free OSS or hosted (free tier, no card; $9/mo Pro for unlimited AI tools scheduled morning catch-up DM rolling out Q2 2026).",
6
6
  "type": "module",
7
7
  "main": "src/server.js",
8
8
  "bin": {
@@ -90,7 +90,7 @@
90
90
  },
91
91
  "dependencies": {
92
92
  "@modelcontextprotocol/sdk": "^1.27.0",
93
- "express": "^4.18.2"
93
+ "express": "^5.2.1"
94
94
  },
95
95
  "files": [
96
96
  "src/",
@@ -99,9 +99,12 @@
99
99
  "public/share.html",
100
100
  "scripts/setup-wizard.js",
101
101
  "scripts/token-cli.js",
102
+ "scripts/apply-template.js",
103
+ "templates/workflow-profiles/",
102
104
  "docs/SETUP.md",
103
105
  "docs/API.md",
104
106
  "docs/TROUBLESHOOTING.md",
107
+ "docs/DEPLOYMENT-MODES.md",
105
108
  "docs/assets/icon.svg",
106
109
  "docs/assets/icon-512.png",
107
110
  "README.md",
package/public/index.html CHANGED
@@ -4,9 +4,10 @@
4
4
  <meta charset="UTF-8">
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
6
  <title>Slack MCP Server — Web Dashboard</title>
7
+ <link rel="preconnect" href="https://fonts.googleapis.com">
8
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
9
+ <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=IBM+Plex+Sans:wght@400;500;600&family=Space+Grotesk:wght@500;600;700&display=swap">
7
10
  <style>
8
- @import url('https://fonts.googleapis.com/css2?family=IBM+Plex+Sans:wght@400;500;600&family=Space+Grotesk:wght@500;600;700&display=swap');
9
-
10
11
  :root {
11
12
  --font-heading: "Space Grotesk", "Avenir Next", "Segoe UI", sans-serif;
12
13
  --font-body: "IBM Plex Sans", "Inter", "Segoe UI", sans-serif;
@@ -255,8 +256,8 @@
255
256
  <div class="container">
256
257
  <h1>Slack Web API <span id="status" class="status"></span></h1>
257
258
  <div style="background:rgba(240,194,70,0.08);border:1px solid rgba(240,194,70,0.2);border-radius:8px;padding:8px 14px;margin-bottom:16px;display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:8px;font-size:13px;color:#d4c48a">
258
- <span>Hosted version coming soon — <strong style="color:#f0c246">permanent OAuth, semantic search, AI summaries</strong></span>
259
- <a href="https://mcp.revasserlabs.com" style="color:#f0c246;font-weight:600;text-decoration:none;white-space:nowrap" target="_blank">Learn more &rarr;</a>
259
+ <span>Hosted tiers live — <strong style="color:#f0c246">managed MCP endpoint, OAuth bridge for Claude.ai, encrypted storage</strong></span>
260
+ <a href="https://mcp.revasserlabs.com" style="color:#f0c246;font-weight:600;text-decoration:none;white-space:nowrap" target="_blank">See tiers &rarr;</a>
260
261
  </div>
261
262
  <div class="grid">
262
263
  <div class="sidebar">
package/public/share.html CHANGED
@@ -4,17 +4,17 @@
4
4
  <meta charset="utf-8">
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1">
6
6
  <title>Slack MCP Server</title>
7
- <meta name="description" content="No OAuth. No admin. 16 Slack tools for Claude, Cursor, Copilot, Gemini, and any MCP client. One command: npx -y @jtalk22/slack-mcp --setup">
7
+ <meta name="description" content="No OAuth. No admin. 21 Slack tools for Claude, Cursor, Copilot, Gemini, and any MCP client. One command: npx -y @jtalk22/slack-mcp --setup">
8
8
  <meta property="og:type" content="website">
9
9
  <meta property="og:title" content="Slack MCP Server — No OAuth, no admin, just your browser session">
10
- <meta property="og:description" content="Slack's official MCP needs OAuth + admin. This one uses your browser session. 16 tools, works with Claude, Cursor, Copilot, Gemini.">
10
+ <meta property="og:description" content="Slack's official MCP needs OAuth + admin. This one uses your browser session. 21 tools, works with Claude, Cursor, Copilot, Gemini.">
11
11
  <meta property="og:url" content="https://jtalk22.github.io/slack-mcp-server/public/share.html">
12
12
  <meta property="og:image" content="https://jtalk22.github.io/slack-mcp-server/docs/images/social-preview-v3.png">
13
13
  <meta property="og:image:width" content="1280">
14
14
  <meta property="og:image:height" content="640">
15
15
  <meta name="twitter:card" content="summary_large_image">
16
16
  <meta name="twitter:title" content="Slack MCP Server — No OAuth, no admin, just your browser session">
17
- <meta name="twitter:description" content="16 tools for Claude, Cursor, Copilot, Gemini. npx -y @jtalk22/slack-mcp --setup">
17
+ <meta name="twitter:description" content="21 tools for Claude, Cursor, Copilot, Gemini. npx -y @jtalk22/slack-mcp --setup">
18
18
  <meta name="twitter:image" content="https://jtalk22.github.io/slack-mcp-server/docs/images/social-preview-v3.png">
19
19
  <link rel="icon" href="https://jtalk22.github.io/slack-mcp-server/docs/assets/icon-512.png" type="image/png">
20
20
  <style>
@@ -107,7 +107,7 @@
107
107
  <body>
108
108
  <main class="wrap">
109
109
  <h1>Slack MCP Server</h1>
110
- <p class="sub">Give Claude full access to your Slack. Self-host 16 tools for free. Hosted version with semantic search, AI summaries, and permanent OAuth coming soon.</p>
110
+ <p class="sub">Give Claude full access to your Slack. Self-host 21 tools for free (16 read/write + 2 workflow profile primitives + 3 paid stubs that point at hosted). Hosted free tier workflow continuity + AI catch-up. Sign up no card at <a href="https://mcp.revasserlabs.com">mcp.revasserlabs.com</a>.</p>
111
111
 
112
112
  <a class="preview" href="https://github.com/jtalk22/slack-mcp-server" rel="noopener">
113
113
  <img src="https://jtalk22.github.io/slack-mcp-server/docs/images/social-preview-v3.png" alt="Slack MCP Server social preview card">
@@ -123,7 +123,7 @@
123
123
  <a href="https://mcp.revasserlabs.com" rel="noopener" style="background:rgba(240,194,70,0.18);border-color:rgba(240,194,70,0.45);color:#f0c246">Hosted</a>
124
124
  </div>
125
125
 
126
- <p class="note"><strong>Verify in 30 seconds:</strong> <code>--version</code>, <code>--doctor</code>, <code>--status</code>. Self-host gives 16 tools with session-based auth. Works with any MCP client — Claude, ChatGPT, Cursor, Copilot, Gemini, Windsurf. Hosted version coming soon at <a href="https://mcp.revasserlabs.com">mcp.revasserlabs.com</a>.</p>
126
+ <p class="note"><strong>Verify in 30 seconds:</strong> <code>--version</code>, <code>--doctor</code>, <code>--status</code>. Self-host gives 21 tools with session-based auth. Works with any MCP client — Claude, ChatGPT, Cursor, Copilot, Gemini, Windsurf. Hosted free tier (no card) live at <a href="https://mcp.revasserlabs.com">mcp.revasserlabs.com</a> — Pro $9/mo unlocks unlimited AI tools (scheduled morning catch-up DM rolling out Q2 2026).</p>
127
127
  </main>
128
128
  </body>
129
129
  </html>
@@ -0,0 +1,117 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Apply a workflow profile template to ~/.slack-mcp-workflows.json
4
+ *
5
+ * Usage:
6
+ * slack-mcp --apply-template <template-name>
7
+ * slack-mcp --apply-template <template-name> --channels C012,C067 [--priority-people U0PRIME]
8
+ *
9
+ * Templates ship in templates/workflow-profiles/. Available templates:
10
+ * oncall-handoff, support-triage, exec-monday, sprint-tracker,
11
+ * customer-feedback, incident-room
12
+ *
13
+ * If --channels is not provided, the template is applied with empty
14
+ * channels — you can add them later via slack_workflow_save.
15
+ */
16
+
17
+ import { readFileSync, existsSync, readdirSync } from "fs";
18
+ import { dirname, join } from "path";
19
+ import { fileURLToPath } from "url";
20
+ import { saveProfile } from "../lib/workflow-store.js";
21
+
22
+ const __dirname = dirname(fileURLToPath(import.meta.url));
23
+ const TEMPLATES_DIR = join(__dirname, "..", "templates", "workflow-profiles");
24
+
25
+ const args = process.argv.slice(2);
26
+
27
+ function listTemplates() {
28
+ if (!existsSync(TEMPLATES_DIR)) return [];
29
+ return readdirSync(TEMPLATES_DIR)
30
+ .filter((f) => f.endsWith(".json"))
31
+ .map((f) => f.replace(/\.json$/, ""));
32
+ }
33
+
34
+ function printUsage() {
35
+ console.log("Usage: slack-mcp --apply-template <template-name> [--channels C012,C067] [--priority-people U0PRIME,U0SECONDARY] [--profile-name custom-name]");
36
+ console.log("");
37
+ console.log("Available templates:");
38
+ for (const t of listTemplates()) {
39
+ console.log(" " + t);
40
+ }
41
+ console.log("");
42
+ console.log("Example:");
43
+ console.log(" slack-mcp --apply-template support-triage --channels C012345,C067890");
44
+ console.log("");
45
+ console.log("Templates write to ~/.slack-mcp-workflows.json. The hosted AI brain at");
46
+ console.log("mcp.revasserlabs.com (free tier or Pro $9/mo) reads these profiles and");
47
+ console.log("returns structured JSON per the workflow_kind. The OSS package ships the");
48
+ console.log("profile primitives + 3 discoverable upgrade stubs (slack_smart_search,");
49
+ console.log("slack_catch_me_up, slack_triage). The brain is hosted-only.");
50
+ }
51
+
52
+ function parseFlag(flag) {
53
+ const idx = args.indexOf(flag);
54
+ if (idx === -1) return null;
55
+ return args[idx + 1];
56
+ }
57
+
58
+ const templateName = args[0];
59
+ if (!templateName || templateName === "--help" || templateName === "-h") {
60
+ printUsage();
61
+ process.exit(templateName ? 0 : 1);
62
+ }
63
+
64
+ if (templateName.startsWith("--") || templateName.startsWith("-")) {
65
+ console.error(`Missing template name. Got "${templateName}" as the first positional argument.`);
66
+ console.error("");
67
+ printUsage();
68
+ process.exit(1);
69
+ }
70
+
71
+ const templatePath = join(TEMPLATES_DIR, `${templateName}.json`);
72
+ if (!existsSync(templatePath)) {
73
+ console.error(`Template "${templateName}" not found at ${templatePath}`);
74
+ console.error("");
75
+ console.error("Available templates: " + listTemplates().join(", "));
76
+ process.exit(1);
77
+ }
78
+
79
+ let template;
80
+ try {
81
+ template = JSON.parse(readFileSync(templatePath, "utf-8"));
82
+ } catch (err) {
83
+ console.error(`Failed to parse template "${templateName}": ${err.message}`);
84
+ process.exit(1);
85
+ }
86
+
87
+ const channelsArg = parseFlag("--channels");
88
+ const priorityArg = parseFlag("--priority-people");
89
+ const profileNameOverride = parseFlag("--profile-name");
90
+
91
+ const profile = {
92
+ profile_name: profileNameOverride || template.profile_name,
93
+ workflow_kind: template.workflow_kind,
94
+ channels: channelsArg ? channelsArg.split(",").map((s) => s.trim()).filter(Boolean) : (template.channels || []),
95
+ priority_people: priorityArg ? priorityArg.split(",").map((s) => s.trim()).filter(Boolean) : (template.priority_people || []),
96
+ retention_mode: template.retention_mode || "ephemeral",
97
+ summary_cadence: template.summary_cadence || "on_demand",
98
+ };
99
+
100
+ const result = saveProfile(profile);
101
+ if (!result.ok) {
102
+ console.error("Failed to save profile:");
103
+ for (const err of result.errors) console.error(" " + err);
104
+ process.exit(1);
105
+ }
106
+
107
+ console.log(`Saved workflow profile "${result.profile_name}" to ~/.slack-mcp-workflows.json`);
108
+ console.log(JSON.stringify(result.profile, null, 2));
109
+ console.log("");
110
+ if (!profile.channels.length) {
111
+ console.log("Note: no channels set. Add channels with:");
112
+ console.log(` slack-mcp --apply-template ${templateName} --channels C012345,C067890`);
113
+ console.log("Or call slack_workflow_save from your MCP client to update.");
114
+ } else {
115
+ console.log("Profile is ready. Run slack_catch_me_up against it from your MCP client.");
116
+ console.log(`(Free tier: 3 catch_me_up calls/month. Pro $9/mo unlocks unlimited; scheduled morning DM rolling out Q2 2026.)`);
117
+ }
@@ -148,6 +148,9 @@ async function runMacOSSetup(rl) {
148
148
  print(" 1. Chrome is running");
149
149
  print(" 2. You have a Slack tab open (app.slack.com)");
150
150
  print(" 3. You're logged into that workspace");
151
+ print();
152
+ print(`${colors.dim}Chrome-free or non-macOS? Hosted tier bypasses Chrome entirely:${colors.reset}`);
153
+ print(`${colors.dim} https://mcp.revasserlabs.com${colors.reset}`);
151
154
  }
152
155
  print();
153
156
 
@@ -283,6 +286,9 @@ async function runManualSetup(rl) {
283
286
  print(" • Tokens expired - try refreshing Slack and copying again");
284
287
  print(" • Wrong workspace - make sure you copied from the right tab");
285
288
  print(" • Incomplete copy - ensure you got the full token/cookie");
289
+ print();
290
+ print(`${colors.dim}Tired of paste-the-token loops? Hosted tier uses OAuth:${colors.reset}`);
291
+ print(`${colors.dim} https://mcp.revasserlabs.com${colors.reset}`);
286
292
  return false;
287
293
  }
288
294
 
@@ -402,6 +408,9 @@ async function runDoctor() {
402
408
  print();
403
409
  print("Next action:");
404
410
  print(" npx -y @jtalk22/slack-mcp --setup");
411
+ print();
412
+ print(`${colors.dim}Prefer no local tokens? Hosted tier uses OAuth:${colors.reset}`);
413
+ print(`${colors.dim} https://mcp.revasserlabs.com${colors.reset}`);
405
414
  process.exit(1);
406
415
  }
407
416
 
@@ -424,6 +433,9 @@ async function runDoctor() {
424
433
  print("Next action:");
425
434
  if (exitCode === 2) {
426
435
  print(" npx -y @jtalk22/slack-mcp --setup");
436
+ print();
437
+ print(`${colors.dim}Tokens expire every 1-2 weeks. Hosted tier has permanent OAuth:${colors.reset}`);
438
+ print(`${colors.dim} https://mcp.revasserlabs.com${colors.reset}`);
427
439
  } else {
428
440
  print(" Check network connectivity and retry:");
429
441
  print(" npx -y @jtalk22/slack-mcp --doctor");
@@ -460,6 +472,10 @@ async function showHelp() {
460
472
  print();
461
473
  print(`${colors.bold}More info:${colors.reset}`);
462
474
  print(" https://github.com/jtalk22/slack-mcp-server");
475
+ print();
476
+ print(`${colors.bold}Hosted tier:${colors.reset}`);
477
+ print(" https://mcp.revasserlabs.com — $9/mo Pro, permanent OAuth,");
478
+ print(" semantic search, workflow continuity across channels.");
463
479
  }
464
480
 
465
481
  async function main() {
@@ -530,6 +546,9 @@ async function main() {
530
546
  print(" • Verify: npx -y @jtalk22/slack-mcp --status");
531
547
  print(" • Start server: npx -y @jtalk22/slack-mcp");
532
548
  print(" • Or add to Claude Desktop config");
549
+ print();
550
+ print(`${colors.dim}Want permanent tokens, semantic search, and workflow continuity?${colors.reset}`);
551
+ print(`${colors.dim}Hosted tier: https://mcp.revasserlabs.com — $9/mo Pro, 10 free paid calls.${colors.reset}`);
533
552
  } else {
534
553
  print(`${colors.red}Setup failed.${colors.reset} See errors above.`);
535
554
  process.exit(1);
package/server.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
3
3
  "name": "io.github.jtalk22/slack-mcp-server",
4
4
  "title": "Slack MCP Server",
5
- "description": "Slack MCP without OAuth no app registration, no admin approval. Works with Claude Code, Cursor, Copilot (where the official server doesn't). 16 tools, one command.",
5
+ "description": "Slack MCP without OAuth. 21 tools: 16 read/write Slack + 2 workflow profile primitives + 3 discoverable upgrade stubs to hosted AI brain. Free OSS or hosted (free tier, no card; $9/mo Pro for unlimited AI tools scheduled morning catch-up DM rolling out Q2 2026).",
6
6
  "websiteUrl": "https://mcp.revasserlabs.com",
7
7
  "icons": [
8
8
  {
@@ -17,7 +17,7 @@
17
17
  "url": "https://github.com/jtalk22/slack-mcp-server",
18
18
  "source": "github"
19
19
  },
20
- "version": "4.1.0",
20
+ "version": "4.2.0",
21
21
  "remotes": [
22
22
  {
23
23
  "type": "streamable-http",
@@ -28,7 +28,7 @@
28
28
  {
29
29
  "registryType": "npm",
30
30
  "identifier": "@jtalk22/slack-mcp",
31
- "version": "4.1.0",
31
+ "version": "4.2.0",
32
32
  "transport": {
33
33
  "type": "stdio"
34
34
  },
package/smithery.yaml CHANGED
@@ -1,5 +1,7 @@
1
1
  # Smithery configuration for slack-mcp-server
2
2
  # https://smithery.ai/docs/build/project-config/smithery-yaml
3
+ # Slack MCP — free OSS or hosted (free tier + $9/mo Pro). 21 tools:
4
+ # read/write Slack + workflow profile primitives + AI brain via hosted upgrade.
3
5
 
4
6
  startCommand:
5
7
  type: stdio
package/src/cli.js CHANGED
@@ -34,6 +34,9 @@ if (firstArg === "web") {
34
34
  } else if (firstArg === "http") {
35
35
  scriptPath = join(__dirname, "server-http.js");
36
36
  scriptArgs = args.slice(1);
37
+ } else if (firstArg === "--apply-template" || firstArg === "apply-template") {
38
+ scriptPath = join(__dirname, "../scripts/apply-template.js");
39
+ scriptArgs = args.slice(1);
37
40
  } else if (WIZARD_ARGS.has(firstArg)) {
38
41
  scriptPath = join(__dirname, "../scripts/setup-wizard.js");
39
42
  scriptArgs = args;
package/src/server.js CHANGED
@@ -46,6 +46,11 @@ import {
46
46
  handleConversationsMark,
47
47
  handleConversationsUnreads,
48
48
  handleUsersSearch,
49
+ handleWorkflowSave,
50
+ handleWorkflows,
51
+ handleSmartSearch,
52
+ handleCatchMeUp,
53
+ handleTriage,
49
54
  } from "../lib/handlers.js";
50
55
 
51
56
  // Background refresh interval (4 hours)
@@ -275,6 +280,23 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
275
280
  case "slack_users_search":
276
281
  return await handleUsersSearch(args);
277
282
 
283
+ // Workflow profile primitives (OSS local JSON store)
284
+ case "slack_workflow_save":
285
+ return await handleWorkflowSave(args);
286
+
287
+ case "slack_workflows":
288
+ return await handleWorkflows(args);
289
+
290
+ // Hosted-only AI tools (OSS = upgrade stubs)
291
+ case "slack_smart_search":
292
+ return await handleSmartSearch(args);
293
+
294
+ case "slack_catch_me_up":
295
+ return await handleCatchMeUp(args);
296
+
297
+ case "slack_triage":
298
+ return await handleTriage(args);
299
+
278
300
  default:
279
301
  return {
280
302
  content: [{
@@ -290,6 +312,22 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
290
312
  };
291
313
  }
292
314
  } catch (error) {
315
+ if (error?.code === "token_auth_failed") {
316
+ return {
317
+ content: [{
318
+ type: "text",
319
+ text: JSON.stringify({
320
+ status: "error",
321
+ code: "token_auth_failed",
322
+ message: String(error?.message || error),
323
+ slack_error: error.slack_error || null,
324
+ extraction_error: error.extraction_error || null,
325
+ next_action: error.next_action || "Open http://localhost:3000 and click Refresh, OR run `npm run tokens:auto` with Slack open in Chrome, OR check Chrome > View > Developer > Allow JavaScript from Apple Events."
326
+ }, null, 2)
327
+ }],
328
+ isError: true
329
+ };
330
+ }
293
331
  return {
294
332
  content: [{
295
333
  type: "text",
@@ -323,8 +361,9 @@ async function main() {
323
361
  }
324
362
 
325
363
  // Background token health check (every 4 hours)
326
- // Use unref() so this timer doesn't prevent the process from exiting
327
- // when the MCP transport closes (prevents zombie processes)
364
+ // unref() alone doesn't prevent StdioServerTransport from keeping the event
365
+ // loop alive after the MCP client disconnects we add explicit shutdown
366
+ // handlers below to kill zombie processes on stdin EOF and signals.
328
367
  const backgroundTimer = setInterval(async () => {
329
368
  try {
330
369
  const health = await checkTokenHealth(console);
@@ -339,6 +378,23 @@ async function main() {
339
378
  }, BACKGROUND_REFRESH_INTERVAL);
340
379
  backgroundTimer.unref();
341
380
 
381
+ // Explicit shutdown path prevents the zombie-process pileup we were seeing
382
+ // when Claude Code or another MCP client disconnected without signalling.
383
+ // StdioServerTransport doesn't exit the event loop on its own when stdin EOFs.
384
+ let shuttingDown = false;
385
+ const shutdown = (reason) => {
386
+ if (shuttingDown) return;
387
+ shuttingDown = true;
388
+ try { clearInterval(backgroundTimer); } catch {}
389
+ console.error(`slack-mcp-server exiting: ${reason}`);
390
+ process.exit(0);
391
+ };
392
+ process.on("SIGTERM", () => shutdown("SIGTERM"));
393
+ process.on("SIGINT", () => shutdown("SIGINT"));
394
+ process.on("SIGHUP", () => shutdown("SIGHUP"));
395
+ process.stdin.on("end", () => shutdown("stdin end (MCP client disconnected)"));
396
+ process.stdin.on("error", (err) => shutdown(`stdin error: ${err?.message || err}`));
397
+
342
398
  // Start server
343
399
  const transport = new StdioServerTransport();
344
400
  await server.connect(transport);
@@ -0,0 +1,10 @@
1
+ {
2
+ "profile_name": "customer-feedback",
3
+ "workflow_kind": "product_launch_watch",
4
+ "channels": [],
5
+ "priority_people": [],
6
+ "retention_mode": "persistent",
7
+ "summary_cadence": "daily_8am",
8
+ "structured_keys": ["launch_signals", "feedback_themes", "blockers", "metrics", "next_actions"],
9
+ "_template_notes": "Apply with: slack-mcp --apply-template customer-feedback --channels C0FEEDBACK,C0CUSTOMERS,C0SUPPORT. retention_mode=persistent so feedback themes accumulate across days. Weekly synthesis becomes more valuable as the index grows."
10
+ }
@@ -0,0 +1,10 @@
1
+ {
2
+ "profile_name": "exec-monday",
3
+ "workflow_kind": "exec_brief",
4
+ "channels": [],
5
+ "priority_people": [],
6
+ "retention_mode": "ephemeral",
7
+ "summary_cadence": "weekly_monday",
8
+ "structured_keys": ["summary", "decisions", "risks", "asks", "action_items"],
9
+ "_template_notes": "Apply with: slack-mcp --apply-template exec-monday --channels C0EXEC,C0PRODUCT,C0OPS --priority-people U0CEO,U0CTO. Weekly Monday cadence requires Pro or Team. Posts a structured brief to your DM at 8am workspace time on Mondays."
10
+ }
@@ -0,0 +1,10 @@
1
+ {
2
+ "profile_name": "incident-room",
3
+ "workflow_kind": "incident_room",
4
+ "channels": [],
5
+ "priority_people": [],
6
+ "retention_mode": "persistent",
7
+ "summary_cadence": "on_demand",
8
+ "structured_keys": ["incident_summary", "timeline", "open_risks", "owner_gaps", "next_actions"],
9
+ "_template_notes": "Apply with: slack-mcp --apply-template incident-room --channels C0INCIDENTS --priority-people U0ONCALL. on_demand cadence — call slack_catch_me_up at handoff or before stakeholder updates. retention_mode=persistent so post-incident review has the timeline."
10
+ }
@@ -0,0 +1,10 @@
1
+ {
2
+ "profile_name": "oncall-handoff",
3
+ "workflow_kind": "incident_room",
4
+ "channels": [],
5
+ "priority_people": [],
6
+ "retention_mode": "persistent",
7
+ "summary_cadence": "on_demand",
8
+ "structured_keys": ["incident_summary", "timeline", "open_risks", "owner_gaps", "next_actions"],
9
+ "_template_notes": "Apply with: slack-mcp --apply-template oncall-handoff --channels C012345,C067890 --priority-people U0PRIME,U0SECONDARY. retention_mode=persistent so the next responder can see prior incident context. Run slack_catch_me_up against this profile before each handoff."
10
+ }