@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.
- package/README.md +52 -7
- package/docs/DEPLOYMENT-MODES.md +56 -0
- package/docs/TROUBLESHOOTING.md +3 -1
- package/lib/handlers.js +92 -0
- package/lib/public-metadata.js +1 -1
- package/lib/public-pages.js +4 -4
- package/lib/slack-client.js +70 -17
- package/lib/token-store.js +279 -93
- package/lib/tools.js +160 -0
- package/lib/workflow-store.js +188 -0
- package/package.json +6 -3
- package/public/index.html +5 -4
- package/public/share.html +5 -5
- package/scripts/apply-template.js +117 -0
- package/scripts/setup-wizard.js +19 -0
- package/server.json +3 -3
- package/smithery.yaml +2 -0
- package/src/cli.js +3 -0
- package/src/server.js +58 -2
- package/templates/workflow-profiles/customer-feedback.json +10 -0
- package/templates/workflow-profiles/exec-monday.json +10 -0
- package/templates/workflow-profiles/incident-room.json +10 -0
- package/templates/workflow-profiles/oncall-handoff.json +10 -0
- package/templates/workflow-profiles/sprint-tracker.json +10 -0
- package/templates/workflow-profiles/support-triage.json +10 -0
|
@@ -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.
|
|
5
|
-
"description": "Slack MCP without OAuth
|
|
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": "^
|
|
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
|
|
259
|
-
<a href="https://mcp.revasserlabs.com" style="color:#f0c246;font-weight:600;text-decoration:none;white-space:nowrap" target="_blank">
|
|
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 →</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.
|
|
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.
|
|
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="
|
|
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
|
|
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
|
|
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
|
+
}
|
package/scripts/setup-wizard.js
CHANGED
|
@@ -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
|
|
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.
|
|
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.
|
|
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
|
-
//
|
|
327
|
-
//
|
|
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
|
+
}
|