slack-max-api-mcp 1.0.5 → 1.0.7

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.
@@ -1,38 +1,89 @@
1
- #!/usr/bin/env node
2
-
1
+ #!/usr/bin/env node
2
+
3
3
  const fs = require("node:fs");
4
+ const os = require("node:os");
5
+ const http = require("node:http");
4
6
  const path = require("node:path");
7
+ const crypto = require("node:crypto");
8
+ const { spawn } = require("node:child_process");
5
9
  const { McpServer } = require("@modelcontextprotocol/sdk/server/mcp.js");
6
10
  const { StdioServerTransport } = require("@modelcontextprotocol/sdk/server/stdio.js");
7
11
  const { z } = require("zod");
8
-
9
- const SERVER_NAME = "slack-max-api-mcp";
10
- const SERVER_VERSION = "2.0.0";
11
-
12
- const SLACK_API_BASE_URL = process.env.SLACK_API_BASE_URL || "https://slack.com/api";
13
-
12
+
13
+ const SERVER_NAME = "slack-max-api-mcp";
14
+ const SERVER_VERSION = "2.0.0";
15
+
16
+ const SLACK_API_BASE_URL = process.env.SLACK_API_BASE_URL || "https://slack.com/api";
17
+
14
18
  const CATALOG_PATH =
15
19
  process.env.SLACK_CATALOG_PATH || path.join(process.cwd(), "data", "slack-catalog.json");
16
20
  const METHOD_TOOL_PREFIX = process.env.SLACK_METHOD_TOOL_PREFIX || "slack_method";
17
21
  const ENABLE_METHOD_TOOLS = process.env.SLACK_ENABLE_METHOD_TOOLS !== "false";
18
22
  const MAX_METHOD_TOOLS = Number(process.env.SLACK_MAX_METHOD_TOOLS || 0);
19
23
  const ENV_EXAMPLE_PATH = path.join(process.cwd(), ".env.example");
20
-
24
+ const TOKEN_STORE_PATH =
25
+ process.env.SLACK_TOKEN_STORE_PATH ||
26
+ path.join(os.homedir(), ".slack-max-api-mcp", "tokens.json");
27
+ const CLIENT_CONFIG_PATH =
28
+ process.env.SLACK_CLIENT_CONFIG_PATH ||
29
+ path.join(os.homedir(), ".slack-max-api-mcp", "client.json");
30
+ const ALLOW_ENV_EXAMPLE_FALLBACK = process.env.SLACK_ALLOW_ENV_EXAMPLE_FALLBACK === "true";
31
+ const OAUTH_CALLBACK_HOST = process.env.SLACK_OAUTH_CALLBACK_HOST || "127.0.0.1";
32
+ const OAUTH_CALLBACK_PORT = Number(process.env.SLACK_OAUTH_CALLBACK_PORT || 8787);
33
+ const OAUTH_CALLBACK_PATH = process.env.SLACK_OAUTH_CALLBACK_PATH || "/slack/oauth/callback";
34
+ const OAUTH_TIMEOUT_MS = Number(process.env.SLACK_OAUTH_TIMEOUT_MS || 5 * 60 * 1000);
35
+ const DEFAULT_OAUTH_BOT_SCOPES =
36
+ process.env.SLACK_OAUTH_BOT_SCOPES || "chat:write,channels:read,groups:read,users:read";
37
+ const DEFAULT_OAUTH_USER_SCOPES =
38
+ process.env.SLACK_OAUTH_USER_SCOPES ||
39
+ "search:read,channels:read,groups:read,channels:history,groups:history";
40
+ const RETRYABLE_TOKEN_ERRORS = new Set(["not_allowed_token_type", "missing_scope"]);
41
+ const GATEWAY_API_KEY = process.env.SLACK_GATEWAY_API_KEY || "";
42
+ const GATEWAY_PROFILE = process.env.SLACK_GATEWAY_PROFILE || "";
43
+ const GATEWAY_HOST = process.env.SLACK_GATEWAY_HOST || "127.0.0.1";
44
+ const GATEWAY_PORT = Number(process.env.SLACK_GATEWAY_PORT || 8790);
45
+ const GATEWAY_PUBLIC_BASE_URL =
46
+ process.env.SLACK_GATEWAY_PUBLIC_BASE_URL || `http://${GATEWAY_HOST}:${GATEWAY_PORT}`;
47
+ const GATEWAY_ALLOW_PUBLIC = process.env.SLACK_GATEWAY_ALLOW_PUBLIC === "true";
48
+ const GATEWAY_SHARED_SECRET = process.env.SLACK_GATEWAY_SHARED_SECRET || GATEWAY_API_KEY;
49
+ const GATEWAY_CLIENT_API_KEY =
50
+ process.env.SLACK_GATEWAY_CLIENT_API_KEY || GATEWAY_API_KEY || GATEWAY_SHARED_SECRET;
51
+ const GATEWAY_PUBLIC_ONBOARD_ENABLED = process.env.SLACK_GATEWAY_PUBLIC_ONBOARD === "true";
52
+ const GATEWAY_PUBLIC_ONBOARD_EXPOSE_API_KEY =
53
+ process.env.SLACK_GATEWAY_PUBLIC_ONBOARD_EXPOSE_API_KEY === "true";
54
+ const GATEWAY_PUBLIC_ONBOARD_API_KEY = process.env.SLACK_GATEWAY_PUBLIC_ONBOARD_API_KEY || "";
55
+ const GATEWAY_PUBLIC_ONBOARD_PROFILE_PREFIX =
56
+ process.env.SLACK_GATEWAY_PUBLIC_ONBOARD_PROFILE_PREFIX || "auto";
57
+ const GATEWAY_STATE_TTL_MS = Number(process.env.SLACK_GATEWAY_STATE_TTL_MS || 15 * 60 * 1000);
58
+ const INVITE_TOKEN_DEFAULT_DAYS = Number(process.env.SLACK_INVITE_TOKEN_DEFAULT_DAYS || 7);
59
+ const AUTO_ONBOARD_ENABLED = process.env.SLACK_AUTO_ONBOARD !== "false";
60
+ const AUTO_ONBOARD_GATEWAY =
61
+ process.env.SLACK_AUTO_ONBOARD_GATEWAY || process.env.SLACK_ONBOARD_GATEWAY_URL || "";
62
+ const AUTO_ONBOARD_PROFILE = process.env.SLACK_AUTO_ONBOARD_PROFILE || "";
63
+ const AUTO_ONBOARD_TOKEN = process.env.SLACK_AUTO_ONBOARD_TOKEN || process.env.SLACK_ONBOARD_TOKEN || "";
64
+ const AUTO_ONBOARD_URL = process.env.SLACK_AUTO_ONBOARD_URL || process.env.SLACK_ONBOARD_URL || "";
65
+ const AUTO_ONBOARD_PROFILE_PREFIX = process.env.SLACK_AUTO_ONBOARD_PROFILE_PREFIX || "auto";
66
+ const ONBOARD_PACKAGE_SPEC =
67
+ process.env.SLACK_ONBOARD_PACKAGE_SPEC ||
68
+ process.env.SLACK_ONBOARD_INSTALL_SPEC ||
69
+ "slack-max-api-mcp@latest";
70
+ const ONBOARD_SKIP_TLS_VERIFY = process.env.SLACK_ONBOARD_SKIP_TLS_VERIFY === "true";
71
+
21
72
  function parseSimpleEnvFile(filePath) {
22
73
  if (!fs.existsSync(filePath)) return {};
23
-
24
- const out = {};
25
- const lines = fs.readFileSync(filePath, "utf8").split(/\r?\n/);
26
- for (const line of lines) {
27
- const trimmed = line.trim();
28
- if (!trimmed || trimmed.startsWith("#")) continue;
29
- const eqIndex = trimmed.indexOf("=");
30
- if (eqIndex <= 0) continue;
31
- const key = trimmed.slice(0, eqIndex).trim();
32
- const value = trimmed.slice(eqIndex + 1).trim();
33
- if (!key) continue;
34
- out[key] = value;
35
- }
74
+
75
+ const out = {};
76
+ const lines = fs.readFileSync(filePath, "utf8").split(/\r?\n/);
77
+ for (const line of lines) {
78
+ const trimmed = line.trim();
79
+ if (!trimmed || trimmed.startsWith("#")) continue;
80
+ const eqIndex = trimmed.indexOf("=");
81
+ if (eqIndex <= 0) continue;
82
+ const key = trimmed.slice(0, eqIndex).trim();
83
+ const value = trimmed.slice(eqIndex + 1).trim();
84
+ if (!key) continue;
85
+ out[key] = value;
86
+ }
36
87
  return out;
37
88
  }
38
89
 
@@ -40,68 +91,360 @@ const ENV_EXAMPLE_VALUES = parseSimpleEnvFile(ENV_EXAMPLE_PATH);
40
91
  const FIXED_BOT_TOKEN = ENV_EXAMPLE_VALUES.SLACK_BOT_TOKEN || "";
41
92
  const FIXED_USER_TOKEN = ENV_EXAMPLE_VALUES.SLACK_USER_TOKEN || "";
42
93
  const FIXED_GENERIC_TOKEN = ENV_EXAMPLE_VALUES.SLACK_TOKEN || "";
43
- const DEFAULT_SLACK_TOKEN =
44
- process.env.SLACK_BOT_TOKEN ||
45
- process.env.SLACK_USER_TOKEN ||
46
- process.env.SLACK_TOKEN ||
47
- FIXED_BOT_TOKEN ||
48
- FIXED_USER_TOKEN ||
49
- FIXED_GENERIC_TOKEN;
50
-
51
- function requireSlackToken(tokenOverride) {
52
- const token = tokenOverride || DEFAULT_SLACK_TOKEN;
53
- if (!token) {
54
- throw new Error(
55
- "Slack token is missing. Set SLACK_BOT_TOKEN/SLACK_USER_TOKEN/SLACK_TOKEN or fill .env.example."
56
- );
94
+
95
+ function parseScopeList(raw) {
96
+ if (!raw) return [];
97
+ return [...new Set(String(raw).split(",").map((part) => part.trim()).filter(Boolean))];
98
+ }
99
+
100
+ function normalizeOnboardNamePart(value, fallback) {
101
+ const normalized = String(value || "")
102
+ .trim()
103
+ .toLowerCase()
104
+ .replace(/[^a-z0-9_-]+/g, "-")
105
+ .replace(/^-+|-+$/g, "");
106
+ if (!normalized) return fallback;
107
+ return normalized;
108
+ }
109
+
110
+ function createAutoOnboardProfileName(prefix = "auto") {
111
+ let username = "user";
112
+ try {
113
+ username = os.userInfo().username || process.env.USERNAME || process.env.USER || "user";
114
+ } catch {
115
+ username = process.env.USERNAME || process.env.USER || "user";
57
116
  }
58
- return token;
117
+ const host = os.hostname() || "host";
118
+ const profilePrefix = normalizeOnboardNamePart(prefix, "auto");
119
+ const userPart = normalizeOnboardNamePart(username, "user");
120
+ const hostPart = normalizeOnboardNamePart(host, "host");
121
+ const rand = crypto.randomBytes(3).toString("hex");
122
+ return `${profilePrefix}-${userPart}-${hostPart}-${rand}`.slice(0, 80);
59
123
  }
60
124
 
61
- function toUrlEncodedBody(params = {}) {
62
- const search = new URLSearchParams();
63
- for (const [key, value] of Object.entries(params)) {
64
- if (value === undefined || value === null) continue;
125
+ function ensureParentDirectory(filePath) {
126
+ const dirPath = path.dirname(filePath);
127
+ fs.mkdirSync(dirPath, { recursive: true });
128
+ }
65
129
 
66
- if (Array.isArray(value) || (typeof value === "object" && !(value instanceof Date))) {
67
- search.append(key, JSON.stringify(value));
68
- continue;
69
- }
130
+ function emptyTokenStore() {
131
+ return { version: 1, default_profile: null, profiles: {} };
132
+ }
133
+
134
+ function normalizeTokenStore(value) {
135
+ if (!value || typeof value !== "object") return emptyTokenStore();
136
+ const out = { ...emptyTokenStore(), ...value };
137
+ if (!out.profiles || typeof out.profiles !== "object" || Array.isArray(out.profiles)) {
138
+ out.profiles = {};
139
+ }
140
+ return out;
141
+ }
142
+
143
+ function loadTokenStore() {
144
+ if (!fs.existsSync(TOKEN_STORE_PATH)) return emptyTokenStore();
70
145
 
71
- search.append(key, String(value));
146
+ try {
147
+ const parsed = JSON.parse(fs.readFileSync(TOKEN_STORE_PATH, "utf8"));
148
+ return normalizeTokenStore(parsed);
149
+ } catch {
150
+ return emptyTokenStore();
72
151
  }
73
- return search.toString();
74
152
  }
75
153
 
76
- function parseJsonMaybe(value) {
77
- if (typeof value !== "string") return value;
154
+ function saveTokenStore(store) {
155
+ ensureParentDirectory(TOKEN_STORE_PATH);
156
+ fs.writeFileSync(TOKEN_STORE_PATH, JSON.stringify(normalizeTokenStore(store), null, 2), "utf8");
157
+ }
158
+
159
+ function emptyClientConfig() {
160
+ return {
161
+ version: 1,
162
+ gateway_url: "",
163
+ gateway_api_key: "",
164
+ profile: "",
165
+ updated_at: "",
166
+ };
167
+ }
168
+
169
+ function normalizeClientConfig(value) {
170
+ if (!value || typeof value !== "object") return emptyClientConfig();
171
+ return { ...emptyClientConfig(), ...value };
172
+ }
173
+
174
+ function loadClientConfig() {
175
+ if (!fs.existsSync(CLIENT_CONFIG_PATH)) return emptyClientConfig();
78
176
  try {
79
- return JSON.parse(value);
177
+ const parsed = JSON.parse(fs.readFileSync(CLIENT_CONFIG_PATH, "utf8"));
178
+ return normalizeClientConfig(parsed);
80
179
  } catch {
81
- return value;
180
+ return emptyClientConfig();
181
+ }
182
+ }
183
+
184
+ function saveClientConfig(config) {
185
+ ensureParentDirectory(CLIENT_CONFIG_PATH);
186
+ fs.writeFileSync(CLIENT_CONFIG_PATH, JSON.stringify(normalizeClientConfig(config), null, 2), "utf8");
187
+ }
188
+
189
+ function getRuntimeGatewayConfig() {
190
+ const config = loadClientConfig();
191
+ return {
192
+ url: (process.env.SLACK_GATEWAY_URL || config.gateway_url || "").replace(/\/+$/, ""),
193
+ apiKey: process.env.SLACK_GATEWAY_API_KEY || config.gateway_api_key || "",
194
+ profile:
195
+ process.env.SLACK_PROFILE ||
196
+ process.env.SLACK_GATEWAY_PROFILE ||
197
+ config.profile ||
198
+ GATEWAY_PROFILE ||
199
+ "",
200
+ };
201
+ }
202
+
203
+ function resolveTokenStoreProfileBySelector(store, selector) {
204
+ const profiles = store?.profiles || {};
205
+ const keys = Object.keys(profiles);
206
+ if (keys.length === 0) return null;
207
+
208
+ const selected = selector || store.default_profile || keys[0];
209
+ if (selected && profiles[selected]) {
210
+ return { key: selected, profile: profiles[selected] };
211
+ }
212
+
213
+ const byName = keys.find((key) => profiles[key]?.profile_name === selected);
214
+ if (byName) return { key: byName, profile: profiles[byName] };
215
+
216
+ const byTeamId = keys.filter((key) => profiles[key]?.team_id === selected);
217
+ if (byTeamId.length === 1) {
218
+ const key = byTeamId[0];
219
+ return { key, profile: profiles[key] };
220
+ }
221
+
222
+ return null;
223
+ }
224
+
225
+ function getPreferredTokenKinds(preferredTokenType) {
226
+ const preferred = (preferredTokenType || process.env.SLACK_DEFAULT_TOKEN_TYPE || "bot").toLowerCase();
227
+ if (preferred === "user") return ["user", "bot", "generic"];
228
+ if (preferred === "generic") return ["generic", "bot", "user"];
229
+ if (preferred === "auto") return ["user", "bot", "generic"];
230
+ return ["bot", "user", "generic"];
231
+ }
232
+
233
+ function appendCandidateTokens(candidates, source, tokenMap, preferredKinds, seen) {
234
+ for (const kind of preferredKinds) {
235
+ const token = tokenMap[kind];
236
+ if (!token || seen.has(token)) continue;
237
+ seen.add(token);
238
+ candidates.push({ token, source, kind });
82
239
  }
83
240
  }
84
241
 
242
+ function getSlackTokenCandidates(tokenOverride, options = {}) {
243
+ if (tokenOverride) {
244
+ return [{ token: tokenOverride, source: "token_override", kind: "override" }];
245
+ }
246
+
247
+ const preferredKinds = getPreferredTokenKinds(options.preferredTokenType);
248
+ const candidates = [];
249
+ const seen = new Set();
250
+
251
+ if (options.includeEnvTokens !== false) {
252
+ appendCandidateTokens(
253
+ candidates,
254
+ "env",
255
+ {
256
+ bot: process.env.SLACK_BOT_TOKEN || "",
257
+ user: process.env.SLACK_USER_TOKEN || "",
258
+ generic: process.env.SLACK_TOKEN || "",
259
+ },
260
+ preferredKinds,
261
+ seen
262
+ );
263
+ }
264
+
265
+ if (options.includeTokenStore !== false) {
266
+ const tokenStore = loadTokenStore();
267
+ const activeProfile = resolveTokenStoreProfileBySelector(
268
+ tokenStore,
269
+ options.profileSelector || process.env.SLACK_PROFILE || GATEWAY_PROFILE
270
+ );
271
+ if (activeProfile) {
272
+ appendCandidateTokens(
273
+ candidates,
274
+ `token_store:${activeProfile.key}`,
275
+ {
276
+ bot: activeProfile.profile?.bot_token || "",
277
+ user: activeProfile.profile?.user_token || "",
278
+ generic: "",
279
+ },
280
+ preferredKinds,
281
+ seen
282
+ );
283
+ }
284
+ }
285
+
286
+ if (ALLOW_ENV_EXAMPLE_FALLBACK) {
287
+ appendCandidateTokens(
288
+ candidates,
289
+ "env_example",
290
+ {
291
+ bot: FIXED_BOT_TOKEN,
292
+ user: FIXED_USER_TOKEN,
293
+ generic: FIXED_GENERIC_TOKEN,
294
+ },
295
+ preferredKinds,
296
+ seen
297
+ );
298
+ }
299
+
300
+ return candidates;
301
+ }
302
+
303
+ function requireSlackTokenCandidate(tokenOverride, options = {}) {
304
+ const candidates = getSlackTokenCandidates(tokenOverride, options);
305
+ if (candidates.length === 0) {
306
+ throw new Error(
307
+ "Slack token is missing. Set SLACK_BOT_TOKEN/SLACK_USER_TOKEN/SLACK_TOKEN or run `slack-max-api-mcp oauth login`."
308
+ );
309
+ }
310
+ return candidates[0];
311
+ }
312
+
313
+ function toUrlEncodedBody(params = {}) {
314
+ const search = new URLSearchParams();
315
+ for (const [key, value] of Object.entries(params)) {
316
+ if (value === undefined || value === null) continue;
317
+
318
+ if (Array.isArray(value) || (typeof value === "object" && !(value instanceof Date))) {
319
+ search.append(key, JSON.stringify(value));
320
+ continue;
321
+ }
322
+
323
+ search.append(key, String(value));
324
+ }
325
+ return search.toString();
326
+ }
327
+
328
+ function parseJsonMaybe(value) {
329
+ if (typeof value !== "string") return value;
330
+ try {
331
+ return JSON.parse(value);
332
+ } catch {
333
+ return value;
334
+ }
335
+ }
336
+
85
337
  function toRecordObject(value) {
86
338
  if (!value || typeof value !== "object" || Array.isArray(value)) return {};
87
339
  return value;
88
340
  }
89
341
 
90
- async function callSlackApi(method, params = {}, tokenOverride) {
91
- const token = requireSlackToken(tokenOverride);
92
- const url = `${SLACK_API_BASE_URL.replace(/\/+$/, "")}/${method}`;
342
+ function buildGatewayAuthHeaders(apiKey) {
343
+ const headers = { "Content-Type": "application/json; charset=utf-8" };
344
+ if (apiKey) {
345
+ headers.Authorization = `Bearer ${apiKey}`;
346
+ headers["X-API-Key"] = apiKey;
347
+ }
348
+ return headers;
349
+ }
93
350
 
94
- const response = await fetch(url, {
351
+ async function callSlackApiViaGateway(method, params = {}, tokenOverride, options = {}) {
352
+ const runtimeGateway = getRuntimeGatewayConfig();
353
+ if (!runtimeGateway.url) {
354
+ throw new Error("Gateway URL is missing. Set SLACK_GATEWAY_URL to use gateway mode.");
355
+ }
356
+
357
+ const response = await fetch(`${runtimeGateway.url}/api/slack/call`, {
95
358
  method: "POST",
96
- headers: {
97
- Authorization: `Bearer ${token}`,
98
- "Content-Type": "application/x-www-form-urlencoded; charset=utf-8",
99
- },
100
- body: toUrlEncodedBody(params),
359
+ headers: buildGatewayAuthHeaders(runtimeGateway.apiKey),
360
+ body: JSON.stringify({
361
+ method,
362
+ params,
363
+ token_override: tokenOverride || undefined,
364
+ profile_selector: options.profileSelector || runtimeGateway.profile || undefined,
365
+ preferred_token_type: options.preferredTokenType || process.env.SLACK_DEFAULT_TOKEN_TYPE || undefined,
366
+ }),
101
367
  });
102
368
 
103
369
  const text = await response.text();
104
- let data;
370
+ let body;
371
+ try {
372
+ body = JSON.parse(text);
373
+ } catch {
374
+ throw new Error(`Gateway returned non-JSON for ${method} (HTTP ${response.status}).`);
375
+ }
376
+
377
+ if (!response.ok) {
378
+ const error = new Error(
379
+ `Gateway HTTP ${response.status} for ${method}: ${body?.error || body?.message || "unknown_error"}`
380
+ );
381
+ error.http_status = response.status;
382
+ error.slack_error = body?.slack_error || body?.error || "gateway_error";
383
+ error.needed = body?.needed;
384
+ error.provided = body?.provided;
385
+ error.token_source = body?.token_source || "gateway";
386
+ throw error;
387
+ }
388
+
389
+ if (!body?.ok) {
390
+ const error = new Error(`Gateway call failed for ${method}: ${body?.error || "unknown_error"}`);
391
+ error.slack_error = body?.slack_error || body?.error || "gateway_error";
392
+ error.needed = body?.needed;
393
+ error.provided = body?.provided;
394
+ error.token_source = body?.token_source || "gateway";
395
+ throw error;
396
+ }
397
+
398
+ return body.data;
399
+ }
400
+
401
+ async function slackHttpViaGateway(input) {
402
+ const runtimeGateway = getRuntimeGatewayConfig();
403
+ if (!runtimeGateway.url) {
404
+ throw new Error("Gateway URL is missing. Set SLACK_GATEWAY_URL to use gateway mode.");
405
+ }
406
+
407
+ const response = await fetch(`${runtimeGateway.url}/api/slack/http`, {
408
+ method: "POST",
409
+ headers: buildGatewayAuthHeaders(runtimeGateway.apiKey),
410
+ body: JSON.stringify({
411
+ ...input,
412
+ profile_selector: input.profile_selector || runtimeGateway.profile || undefined,
413
+ preferred_token_type: input.preferred_token_type || process.env.SLACK_DEFAULT_TOKEN_TYPE || undefined,
414
+ }),
415
+ });
416
+
417
+ const text = await response.text();
418
+ let body;
419
+ try {
420
+ body = JSON.parse(text);
421
+ } catch {
422
+ throw new Error(`Gateway returned non-JSON for HTTP proxy (HTTP ${response.status}).`);
423
+ }
424
+
425
+ if (!response.ok) {
426
+ throw new Error(`Gateway HTTP ${response.status}: ${body?.error || "gateway_error"}`);
427
+ }
428
+ if (!body?.ok) {
429
+ throw new Error(`Gateway HTTP proxy failed: ${body?.error || "gateway_error"}`);
430
+ }
431
+ return body.data;
432
+ }
433
+
434
+ async function callSlackApiWithToken(method, params = {}, token, tokenSource) {
435
+ const url = `${SLACK_API_BASE_URL.replace(/\/+$/, "")}/${method}`;
436
+
437
+ const response = await fetch(url, {
438
+ method: "POST",
439
+ headers: {
440
+ Authorization: `Bearer ${token}`,
441
+ "Content-Type": "application/x-www-form-urlencoded; charset=utf-8",
442
+ },
443
+ body: toUrlEncodedBody(params),
444
+ });
445
+
446
+ const text = await response.text();
447
+ let data;
105
448
  try {
106
449
  data = JSON.parse(text);
107
450
  } catch {
@@ -109,31 +452,85 @@ async function callSlackApi(method, params = {}, tokenOverride) {
109
452
  }
110
453
 
111
454
  if (!response.ok) {
112
- throw new Error(`Slack API HTTP ${response.status} for ${method}: ${data.error || "unknown_error"}`);
455
+ const error = new Error(
456
+ `Slack API HTTP ${response.status} for ${method}: ${data.error || "unknown_error"}`
457
+ );
458
+ error.http_status = response.status;
459
+ error.slack_error = data.error || "unknown_error";
460
+ error.needed = data.needed;
461
+ error.provided = data.provided;
462
+ error.token_source = tokenSource;
463
+ throw error;
113
464
  }
114
465
 
115
466
  if (!data.ok) {
116
- throw new Error(`Slack method ${method} failed: ${data.error || "unknown_error"}`);
467
+ const extraParts = [];
468
+ if (data.needed) extraParts.push(`needed=${data.needed}`);
469
+ if (data.provided) extraParts.push(`provided=${data.provided}`);
470
+ if (tokenSource) extraParts.push(`token_source=${tokenSource}`);
471
+
472
+ const extra = extraParts.length ? ` (${extraParts.join(", ")})` : "";
473
+ const error = new Error(`Slack method ${method} failed: ${data.error || "unknown_error"}${extra}`);
474
+ error.slack_error = data.error || "unknown_error";
475
+ error.needed = data.needed;
476
+ error.provided = data.provided;
477
+ error.token_source = tokenSource;
478
+ throw error;
117
479
  }
118
480
 
119
481
  return data;
120
482
  }
121
483
 
122
- function successResult(payload) {
123
- return {
124
- content: [{ type: "text", text: JSON.stringify(payload, null, 2) }],
125
- structuredContent: payload,
126
- };
127
- }
484
+ async function callSlackApiWithCandidates(method, params, candidates) {
485
+ let lastError = null;
486
+
487
+ for (let i = 0; i < candidates.length; i += 1) {
488
+ const candidate = candidates[i];
489
+ try {
490
+ return await callSlackApiWithToken(method, params, candidate.token, candidate.source);
491
+ } catch (error) {
492
+ lastError = error;
493
+ const canRetry = i < candidates.length - 1 && RETRYABLE_TOKEN_ERRORS.has(error.slack_error);
494
+ if (!canRetry) {
495
+ throw error;
496
+ }
497
+ }
498
+ }
128
499
 
129
- function errorResult(error) {
130
- const message = error instanceof Error ? error.message : String(error);
131
- return {
132
- isError: true,
133
- content: [{ type: "text", text: message }],
134
- };
500
+ throw lastError || new Error(`Slack method ${method} failed.`);
135
501
  }
136
502
 
503
+ async function callSlackApi(method, params = {}, tokenOverride, options = {}) {
504
+ const runtimeGateway = getRuntimeGatewayConfig();
505
+ if (runtimeGateway.url) {
506
+ return callSlackApiViaGateway(method, params, tokenOverride, options);
507
+ }
508
+
509
+ const candidates = getSlackTokenCandidates(tokenOverride, options);
510
+ if (candidates.length === 0) {
511
+ throw new Error(
512
+ "Slack token is missing. Set SLACK_BOT_TOKEN/SLACK_USER_TOKEN/SLACK_TOKEN or run `slack-max-api-mcp oauth login`."
513
+ );
514
+ }
515
+
516
+ return callSlackApiWithCandidates(method, params, candidates);
517
+ }
518
+
519
+ function successResult(payload) {
520
+ return {
521
+ content: [{ type: "text", text: JSON.stringify(payload, null, 2) }],
522
+ structuredContent: payload,
523
+ };
524
+ }
525
+
526
+ function errorResult(error) {
527
+ const message = error instanceof Error ? error.message : String(error);
528
+ return {
529
+ isError: true,
530
+ content: [{ type: "text", text: message }],
531
+ };
532
+ }
533
+
137
534
  async function safeToolRun(executor) {
138
535
  try {
139
536
  const result = await executor();
@@ -143,576 +540,1808 @@ async function safeToolRun(executor) {
143
540
  }
144
541
  }
145
542
 
146
- function loadCatalog() {
147
- if (!fs.existsSync(CATALOG_PATH)) {
148
- return { methods: [], scopes: [], totals: {} };
543
+ function parseCliArgs(argv) {
544
+ const options = {};
545
+ const positionals = [];
546
+
547
+ for (let i = 0; i < argv.length; i += 1) {
548
+ const arg = argv[i];
549
+ if (!arg.startsWith("--")) {
550
+ positionals.push(arg);
551
+ continue;
552
+ }
553
+
554
+ const withoutPrefix = arg.slice(2);
555
+ const [rawKey, rawInlineValue] = withoutPrefix.split("=", 2);
556
+ const key = rawKey.trim();
557
+ if (!key) continue;
558
+
559
+ if (rawInlineValue !== undefined) {
560
+ options[key] = rawInlineValue;
561
+ continue;
562
+ }
563
+
564
+ const maybeValue = argv[i + 1];
565
+ if (maybeValue && !maybeValue.startsWith("--")) {
566
+ options[key] = maybeValue;
567
+ i += 1;
568
+ continue;
569
+ }
570
+
571
+ options[key] = true;
149
572
  }
150
573
 
574
+ return { options, positionals };
575
+ }
576
+
577
+ function base64UrlEncodeString(value) {
578
+ return Buffer.from(value, "utf8")
579
+ .toString("base64")
580
+ .replace(/\+/g, "-")
581
+ .replace(/\//g, "_")
582
+ .replace(/=+$/g, "");
583
+ }
584
+
585
+ function base64UrlDecodeToString(value) {
586
+ const padded = value.replace(/-/g, "+").replace(/_/g, "/").padEnd(Math.ceil(value.length / 4) * 4, "=");
587
+ return Buffer.from(padded, "base64").toString("utf8");
588
+ }
589
+
590
+ function hmacSign(text, secret) {
591
+ return crypto
592
+ .createHmac("sha256", secret)
593
+ .update(text)
594
+ .digest("base64")
595
+ .replace(/\+/g, "-")
596
+ .replace(/\//g, "_")
597
+ .replace(/=+$/g, "");
598
+ }
599
+
600
+ function createSignedInviteToken(payload, secret) {
601
+ const encodedPayload = base64UrlEncodeString(JSON.stringify(payload));
602
+ const signature = hmacSign(encodedPayload, secret);
603
+ return `${encodedPayload}.${signature}`;
604
+ }
605
+
606
+ function parseAndVerifyInviteToken(token, secret) {
607
+ const [encodedPayload, signature] = String(token || "").split(".", 2);
608
+ if (!encodedPayload || !signature) {
609
+ throw new Error("Invalid invite token format.");
610
+ }
611
+ const expected = hmacSign(encodedPayload, secret);
612
+ const expectedBuf = Buffer.from(expected, "utf8");
613
+ const sigBuf = Buffer.from(signature, "utf8");
614
+ if (expectedBuf.length !== sigBuf.length || !crypto.timingSafeEqual(expectedBuf, sigBuf)) {
615
+ throw new Error("Invalid invite token signature.");
616
+ }
617
+ let payload;
151
618
  try {
152
- const raw = fs.readFileSync(CATALOG_PATH, "utf8");
153
- const parsed = JSON.parse(raw);
154
- return parsed;
155
- } catch (error) {
156
- throw new Error(`Failed to load catalog at ${CATALOG_PATH}: ${error}`);
619
+ payload = JSON.parse(base64UrlDecodeToString(encodedPayload));
620
+ } catch {
621
+ throw new Error("Invalid invite token payload.");
622
+ }
623
+ if (typeof payload !== "object" || !payload) {
624
+ throw new Error("Invalid invite token payload object.");
157
625
  }
626
+ if (!payload.exp || Number(payload.exp) < Date.now()) {
627
+ throw new Error("Invite token expired.");
628
+ }
629
+ return payload;
158
630
  }
159
631
 
160
- function toolNameFromMethod(method, usedNames) {
161
- const base = `${METHOD_TOOL_PREFIX}_${method.replace(/[^a-zA-Z0-9]/g, "_")}`;
162
- if (!usedNames.has(base)) {
163
- usedNames.add(base);
164
- return base;
632
+ function requireGatewayInviteSecret() {
633
+ if (!GATEWAY_SHARED_SECRET) {
634
+ throw new Error("Set SLACK_GATEWAY_SHARED_SECRET before using gateway invite/onboarding.");
165
635
  }
636
+ return GATEWAY_SHARED_SECRET;
637
+ }
166
638
 
167
- let idx = 2;
168
- while (usedNames.has(`${base}_${idx}`)) idx += 1;
169
- const name = `${base}_${idx}`;
170
- usedNames.add(name);
171
- return name;
639
+ function isInteractiveTerminal() {
640
+ return Boolean(process.stdin.isTTY && process.stdout.isTTY);
172
641
  }
173
642
 
174
- function registerCoreTools(server) {
175
- server.registerTool(
176
- "slack_api_call",
177
- {
178
- description: "Call any Slack Web API method directly.",
179
- inputSchema: {
180
- method: z
181
- .string()
182
- .min(3)
183
- .regex(/^[a-z][a-zA-Z0-9_.]+$/, "Method must look like chat.postMessage"),
184
- params: z.record(z.string(), z.any()).optional(),
185
- token_override: z.string().optional(),
186
- },
187
- },
188
- async ({ method, params, token_override }) =>
189
- safeToolRun(async () => {
190
- const data = await callSlackApi(method, params || {}, token_override);
191
- return { method, data };
192
- })
193
- );
643
+ function hasAnyLocalAuthMaterial() {
644
+ const runtimeGateway = getRuntimeGatewayConfig();
645
+ if (runtimeGateway.url) return true;
646
+ const tokenCandidates = getSlackTokenCandidates(undefined, {
647
+ includeEnvTokens: true,
648
+ includeTokenStore: true,
649
+ });
650
+ return tokenCandidates.length > 0;
651
+ }
194
652
 
195
- server.registerTool(
196
- "slack_http_api_call",
197
- {
198
- description:
199
- "Generic HTTP call for Slack APIs outside standard Web API methods (SCIM/Audit/Legal Holds).",
200
- inputSchema: {
201
- url: z.string().url(),
202
- http_method: z.enum(["GET", "POST", "PUT", "PATCH", "DELETE"]).optional(),
203
- query: z.record(z.string(), z.any()).optional(),
204
- json_body: z.record(z.string(), z.any()).optional(),
205
- form_body: z.record(z.string(), z.any()).optional(),
206
- headers: z.record(z.string(), z.string()).optional(),
207
- token_override: z.string().optional(),
208
- },
209
- },
210
- async ({ url, http_method, query, json_body, form_body, headers, token_override }) =>
211
- safeToolRun(async () => {
212
- const token = requireSlackToken(token_override);
213
- const method = http_method || "GET";
653
+ async function runAutoOnboardingIfPossible() {
654
+ if (!AUTO_ONBOARD_ENABLED) return false;
655
+ if (!isInteractiveTerminal()) return false;
656
+ if (hasAnyLocalAuthMaterial()) return false;
657
+
658
+ if (AUTO_ONBOARD_URL) {
659
+ const opened = openExternalUrl(AUTO_ONBOARD_URL);
660
+ if (!opened) {
661
+ console.log(`[auto-onboard] Open this URL in browser:\n${AUTO_ONBOARD_URL}`);
662
+ } else {
663
+ console.log("[auto-onboard] Browser opened for onboarding.");
664
+ }
665
+ return true;
666
+ }
214
667
 
215
- const endpoint = new URL(url);
216
- for (const [k, v] of Object.entries(toRecordObject(query))) {
217
- if (v === undefined || v === null) continue;
218
- endpoint.searchParams.set(k, String(v));
219
- }
668
+ if (AUTO_ONBOARD_GATEWAY && AUTO_ONBOARD_TOKEN) {
669
+ const args = ["--gateway", AUTO_ONBOARD_GATEWAY, "--token", AUTO_ONBOARD_TOKEN];
670
+ if (AUTO_ONBOARD_PROFILE) args.push("--profile", AUTO_ONBOARD_PROFILE);
671
+ await runOnboardStart(args);
672
+ return true;
673
+ }
220
674
 
221
- const reqHeaders = {
222
- Authorization: `Bearer ${token}`,
223
- ...(headers || {}),
224
- };
675
+ if (AUTO_ONBOARD_GATEWAY) {
676
+ const args = ["--gateway", AUTO_ONBOARD_GATEWAY];
677
+ if (AUTO_ONBOARD_PROFILE) {
678
+ args.push("--profile", AUTO_ONBOARD_PROFILE);
679
+ } else if (AUTO_ONBOARD_PROFILE_PREFIX) {
680
+ args.push("--profile", createAutoOnboardProfileName(AUTO_ONBOARD_PROFILE_PREFIX));
681
+ }
682
+ await runOnboardStart(args);
683
+ return true;
684
+ }
225
685
 
226
- let body;
227
- if (form_body && Object.keys(form_body).length > 0) {
228
- reqHeaders["Content-Type"] = "application/x-www-form-urlencoded; charset=utf-8";
229
- body = toUrlEncodedBody(form_body);
230
- } else if (json_body && Object.keys(json_body).length > 0) {
231
- reqHeaders["Content-Type"] = "application/json; charset=utf-8";
232
- body = JSON.stringify(json_body);
233
- }
686
+ return false;
687
+ }
234
688
 
235
- const res = await fetch(endpoint.toString(), {
236
- method,
237
- headers: reqHeaders,
238
- body,
239
- });
689
+ function printOauthHelp() {
690
+ const lines = [
691
+ "Slack Max OAuth helper",
692
+ "",
693
+ "Usage:",
694
+ " slack-max-api-mcp oauth login [--profile NAME] [--team T123] [--scope a,b] [--user-scope c,d]",
695
+ " slack-max-api-mcp oauth list",
696
+ " slack-max-api-mcp oauth use <profile_key_or_name>",
697
+ " slack-max-api-mcp oauth current",
698
+ "",
699
+ "Required env vars for login:",
700
+ " SLACK_CLIENT_ID",
701
+ " SLACK_CLIENT_SECRET",
702
+ "",
703
+ "Optional env vars:",
704
+ " SLACK_OAUTH_BOT_SCOPES, SLACK_OAUTH_USER_SCOPES",
705
+ " SLACK_OAUTH_CALLBACK_HOST, SLACK_OAUTH_CALLBACK_PORT, SLACK_OAUTH_CALLBACK_PATH",
706
+ " SLACK_PROFILE, SLACK_TOKEN_STORE_PATH",
707
+ ];
708
+ console.log(lines.join("\n"));
709
+ }
240
710
 
241
- const text = await res.text();
242
- let parsedBody = text;
243
- try {
244
- parsedBody = JSON.parse(text);
245
- } catch {
246
- // Keep plain text when response is not JSON.
247
- }
711
+ function openExternalUrl(url) {
712
+ try {
713
+ if (process.platform === "win32") {
714
+ const child = spawn("cmd", ["/c", "start", "", url], { detached: true, stdio: "ignore" });
715
+ child.unref();
716
+ return true;
717
+ }
718
+ if (process.platform === "darwin") {
719
+ const child = spawn("open", [url], { detached: true, stdio: "ignore" });
720
+ child.unref();
721
+ return true;
722
+ }
723
+ const child = spawn("xdg-open", [url], { detached: true, stdio: "ignore" });
724
+ child.unref();
725
+ return true;
726
+ } catch {
727
+ return false;
728
+ }
729
+ }
248
730
 
249
- return {
250
- url: endpoint.toString(),
251
- status: res.status,
252
- ok: res.ok,
253
- headers: Object.fromEntries(res.headers.entries()),
254
- body: parsedBody,
255
- };
256
- })
257
- );
731
+ function buildOauthAuthorizeUrl({
732
+ clientId,
733
+ state,
734
+ redirectUri,
735
+ botScopes,
736
+ userScopes,
737
+ teamId,
738
+ }) {
739
+ const endpoint = new URL("https://slack.com/oauth/v2/authorize");
740
+ endpoint.searchParams.set("client_id", clientId);
741
+ endpoint.searchParams.set("state", state);
742
+ endpoint.searchParams.set("redirect_uri", redirectUri);
743
+ if (botScopes.length > 0) endpoint.searchParams.set("scope", botScopes.join(","));
744
+ if (userScopes.length > 0) endpoint.searchParams.set("user_scope", userScopes.join(","));
745
+ if (teamId) endpoint.searchParams.set("team", teamId);
746
+ return endpoint.toString();
747
+ }
258
748
 
259
- server.registerTool(
260
- "search_messages_files",
261
- {
262
- description:
263
- "Search messages and files. Uses search.messages and search.files and returns both.",
264
- inputSchema: {
265
- query: z.string().min(1),
266
- count: z.number().int().min(1).max(100).optional(),
267
- page: z.number().int().min(1).optional(),
268
- sort: z.enum(["score", "timestamp"]).optional(),
269
- sort_dir: z.enum(["asc", "desc"]).optional(),
270
- include_messages: z.boolean().optional(),
271
- include_files: z.boolean().optional(),
272
- token_override: z.string().optional(),
273
- },
274
- },
275
- async ({
276
- query,
277
- count,
278
- page,
279
- sort,
280
- sort_dir,
281
- include_messages,
282
- include_files,
283
- token_override,
284
- }) =>
285
- safeToolRun(async () => {
286
- const shouldSearchMessages = include_messages !== false;
287
- const shouldSearchFiles = include_files !== false;
288
- const sharedParams = {
289
- query,
290
- count: count ?? 20,
291
- page: page ?? 1,
292
- sort,
293
- sort_dir,
294
- };
749
+ async function waitForOauthCode({ host, port, callbackPath, state, timeoutMs }) {
750
+ return new Promise((resolve, reject) => {
751
+ let settled = false;
752
+ let timeout = null;
753
+ const server = http.createServer((req, res) => {
754
+ const requestUrl = new URL(req.url || "/", `http://${host}:${port}`);
755
+ if (requestUrl.pathname !== callbackPath) {
756
+ res.writeHead(404, { "Content-Type": "text/plain; charset=utf-8" });
757
+ res.end("Not found");
758
+ return;
759
+ }
760
+
761
+ const receivedError = requestUrl.searchParams.get("error");
762
+ if (receivedError) {
763
+ res.writeHead(400, { "Content-Type": "text/plain; charset=utf-8" });
764
+ res.end(`Slack OAuth failed: ${receivedError}`);
765
+ settle(new Error(`Slack OAuth failed: ${receivedError}`));
766
+ return;
767
+ }
768
+
769
+ const receivedState = requestUrl.searchParams.get("state");
770
+ if (!receivedState || receivedState !== state) {
771
+ res.writeHead(400, { "Content-Type": "text/plain; charset=utf-8" });
772
+ res.end("Invalid OAuth state.");
773
+ settle(new Error("Slack OAuth state mismatch."));
774
+ return;
775
+ }
776
+
777
+ const code = requestUrl.searchParams.get("code");
778
+ if (!code) {
779
+ res.writeHead(400, { "Content-Type": "text/plain; charset=utf-8" });
780
+ res.end("Missing authorization code.");
781
+ settle(new Error("OAuth callback did not include `code`."));
782
+ return;
783
+ }
784
+
785
+ res.writeHead(200, { "Content-Type": "text/plain; charset=utf-8" });
786
+ res.end("Slack OAuth authorization completed. You can close this tab.");
787
+ settle(null, code);
788
+ });
789
+
790
+ function settle(error, code) {
791
+ if (settled) return;
792
+ settled = true;
793
+ if (timeout) clearTimeout(timeout);
794
+ server.close(() => {
795
+ if (error) reject(error);
796
+ else resolve(code);
797
+ });
798
+ }
295
799
 
296
- let messages = null;
297
- let files = null;
800
+ server.on("error", (error) => {
801
+ settle(new Error(`Failed to listen on ${host}:${port}: ${error.message}`));
802
+ });
298
803
 
299
- if (shouldSearchMessages) {
300
- messages = await callSlackApi("search.messages", sharedParams, token_override);
301
- }
302
- if (shouldSearchFiles) {
303
- files = await callSlackApi("search.files", sharedParams, token_override);
304
- }
804
+ server.listen(port, host, () => {
805
+ timeout = setTimeout(() => {
806
+ settle(new Error("Timed out waiting for OAuth callback."));
807
+ }, timeoutMs);
808
+ });
809
+ });
810
+ }
305
811
 
306
- return {
307
- query,
308
- messages: messages ? messages.messages : null,
309
- files: files ? files.files : null,
310
- };
311
- })
312
- );
812
+ async function exchangeOauthCode({ clientId, clientSecret, code, redirectUri }) {
813
+ const endpoint = `${SLACK_API_BASE_URL.replace(/\/+$/, "")}/oauth.v2.access`;
814
+ const response = await fetch(endpoint, {
815
+ method: "POST",
816
+ headers: { "Content-Type": "application/x-www-form-urlencoded; charset=utf-8" },
817
+ body: toUrlEncodedBody({
818
+ client_id: clientId,
819
+ client_secret: clientSecret,
820
+ code,
821
+ redirect_uri: redirectUri,
822
+ }),
823
+ });
313
824
 
314
- server.registerTool(
315
- "search_users",
316
- {
317
- description:
318
- "Find users by partial match on id/name/display_name/email using users.list and local filtering.",
319
- inputSchema: {
320
- query: z.string().optional(),
321
- limit: z.number().int().min(1).max(1000).optional(),
322
- include_locale: z.boolean().optional(),
323
- include_deleted: z.boolean().optional(),
324
- include_bots: z.boolean().optional(),
325
- cursor: z.string().optional(),
326
- token_override: z.string().optional(),
327
- },
328
- },
329
- async ({
330
- query,
331
- limit,
332
- include_locale,
333
- include_deleted,
334
- include_bots,
335
- cursor,
336
- token_override,
337
- }) =>
338
- safeToolRun(async () => {
339
- const listData = await callSlackApi(
340
- "users.list",
341
- {
342
- limit: limit ?? 200,
343
- include_locale: include_locale ?? true,
344
- cursor,
345
- },
346
- token_override
347
- );
825
+ const text = await response.text();
826
+ let data;
827
+ try {
828
+ data = JSON.parse(text);
829
+ } catch {
830
+ throw new Error(`Slack OAuth token exchange returned non-JSON (HTTP ${response.status}).`);
831
+ }
348
832
 
349
- const normalizedQuery = query ? query.toLowerCase() : null;
350
- let users = Array.isArray(listData.members) ? listData.members : [];
833
+ if (!response.ok) {
834
+ throw new Error(`Slack OAuth token exchange failed (HTTP ${response.status}): ${data.error || "unknown_error"}`);
835
+ }
351
836
 
352
- if (include_deleted !== true) {
353
- users = users.filter((u) => !u.deleted);
354
- }
355
- if (include_bots !== true) {
356
- users = users.filter((u) => !u.is_bot && !u.is_app_user);
357
- }
358
- if (normalizedQuery) {
359
- users = users.filter((u) => {
360
- const candidates = [
361
- u.id,
362
- u.name,
363
- u.real_name,
364
- u.profile?.display_name,
365
- u.profile?.real_name,
366
- u.profile?.email,
367
- ]
368
- .filter((v) => typeof v === "string")
369
- .map((v) => v.toLowerCase());
370
- return candidates.some((value) => value.includes(normalizedQuery));
371
- });
372
- }
837
+ if (!data.ok) {
838
+ throw new Error(`Slack OAuth token exchange failed: ${data.error || "unknown_error"}`);
839
+ }
373
840
 
374
- return {
375
- users,
376
- next_cursor: listData.response_metadata?.next_cursor || null,
377
- count: users.length,
378
- };
379
- })
841
+ return data;
842
+ }
843
+
844
+ function normalizeProfileName(rawName, fallback) {
845
+ const name = (rawName || "").trim();
846
+ if (name) return name;
847
+ return fallback;
848
+ }
849
+
850
+ function upsertOauthProfile(oauthResponse, preferredProfileName) {
851
+ const teamId = oauthResponse.team?.id || "unknown_team";
852
+ const teamName = oauthResponse.team?.name || teamId;
853
+ const authedUserId = oauthResponse.authed_user?.id || oauthResponse.bot_user_id || "unknown_user";
854
+ const profileKey = `${teamId}:${authedUserId}`;
855
+
856
+ const tokenStore = loadTokenStore();
857
+ const existing = tokenStore.profiles[profileKey] || {};
858
+ const now = new Date().toISOString();
859
+
860
+ tokenStore.profiles[profileKey] = {
861
+ ...existing,
862
+ profile_name: normalizeProfileName(preferredProfileName, `${teamName}-${authedUserId}`),
863
+ team_id: teamId,
864
+ team_name: teamName,
865
+ app_id: oauthResponse.app_id || existing.app_id || "",
866
+ token_type: oauthResponse.token_type || existing.token_type || "",
867
+ bot_user_id: oauthResponse.bot_user_id || existing.bot_user_id || "",
868
+ bot_scope: oauthResponse.scope || existing.bot_scope || "",
869
+ user_scope: oauthResponse.authed_user?.scope || existing.user_scope || "",
870
+ bot_token: oauthResponse.access_token || existing.bot_token || "",
871
+ user_token: oauthResponse.authed_user?.access_token || existing.user_token || "",
872
+ authed_user_id: oauthResponse.authed_user?.id || existing.authed_user_id || "",
873
+ incoming_webhook_url: oauthResponse.incoming_webhook?.url || existing.incoming_webhook_url || "",
874
+ created_at: existing.created_at || now,
875
+ updated_at: now,
876
+ };
877
+ tokenStore.default_profile = profileKey;
878
+
879
+ saveTokenStore(tokenStore);
880
+ return { key: profileKey, profile: tokenStore.profiles[profileKey] };
881
+ }
882
+
883
+ function formatTokenProfileSummary(key, profile, isDefault) {
884
+ const flags = [];
885
+ if (isDefault) flags.push("default");
886
+ if (profile.bot_token) flags.push("bot");
887
+ if (profile.user_token) flags.push("user");
888
+
889
+ return [
890
+ `${isDefault ? "*" : " "} ${key} (${profile.profile_name || "unnamed"})`,
891
+ ` team=${profile.team_name || profile.team_id || "unknown"} | user=${profile.authed_user_id || "unknown"} | tokens=${flags.join(", ") || "none"}`,
892
+ ].join("\n");
893
+ }
894
+
895
+ async function runOauthLogin(args) {
896
+ const { options } = parseCliArgs(args);
897
+ const clientId = options["client-id"] || process.env.SLACK_CLIENT_ID;
898
+ const clientSecret = options["client-secret"] || process.env.SLACK_CLIENT_SECRET;
899
+ if (!clientId || !clientSecret) {
900
+ throw new Error("Missing SLACK_CLIENT_ID or SLACK_CLIENT_SECRET. Set env vars or pass --client-id/--client-secret.");
901
+ }
902
+
903
+ const host = options.host || OAUTH_CALLBACK_HOST;
904
+ const port = Number(options.port || OAUTH_CALLBACK_PORT);
905
+ const callbackPath = options["callback-path"] || OAUTH_CALLBACK_PATH;
906
+ const redirectUri = options["redirect-uri"] || `http://${host}:${port}${callbackPath}`;
907
+ const timeoutMs = Number(options["timeout-ms"] || OAUTH_TIMEOUT_MS);
908
+ const botScopes = parseScopeList(options.scope || DEFAULT_OAUTH_BOT_SCOPES);
909
+ const userScopes = parseScopeList(options["user-scope"] || DEFAULT_OAUTH_USER_SCOPES);
910
+ const teamId = options.team || process.env.SLACK_OAUTH_TEAM_ID || "";
911
+
912
+ if (botScopes.length === 0 && userScopes.length === 0) {
913
+ throw new Error("At least one scope is required. Set --scope or --user-scope.");
914
+ }
915
+
916
+ const state = crypto.randomBytes(24).toString("hex");
917
+ const authorizeUrl = buildOauthAuthorizeUrl({
918
+ clientId,
919
+ state,
920
+ redirectUri,
921
+ botScopes,
922
+ userScopes,
923
+ teamId,
924
+ });
925
+
926
+ console.log(`[oauth] callback listening on http://${host}:${port}${callbackPath}`);
927
+ console.log(`[oauth] authorize URL:\n${authorizeUrl}`);
928
+
929
+ if (!options["no-open"]) {
930
+ const opened = openExternalUrl(authorizeUrl);
931
+ if (!opened) {
932
+ console.log("[oauth] Could not auto-open browser. Open the URL above manually.");
933
+ }
934
+ }
935
+
936
+ const code = await waitForOauthCode({ host, port, callbackPath, state, timeoutMs });
937
+ const oauthResponse = await exchangeOauthCode({ clientId, clientSecret, code, redirectUri });
938
+ const { key, profile } = upsertOauthProfile(oauthResponse, options.profile);
939
+
940
+ console.log(`[oauth] saved profile: ${profile.profile_name} (${key})`);
941
+ console.log(`[oauth] token store path: ${TOKEN_STORE_PATH}`);
942
+ console.log("[oauth] Next step for MCP clients:");
943
+ console.log(` setx SLACK_PROFILE \"${profile.profile_name}\"`);
944
+ }
945
+
946
+ function runOauthList() {
947
+ const tokenStore = loadTokenStore();
948
+ const keys = Object.keys(tokenStore.profiles);
949
+ if (keys.length === 0) {
950
+ console.log(`[oauth] no saved profiles in ${TOKEN_STORE_PATH}`);
951
+ return;
952
+ }
953
+
954
+ console.log(`[oauth] profiles in ${TOKEN_STORE_PATH}`);
955
+ for (const key of keys) {
956
+ console.log(formatTokenProfileSummary(key, tokenStore.profiles[key], tokenStore.default_profile === key));
957
+ }
958
+ }
959
+
960
+ function runOauthUse(args) {
961
+ const { positionals } = parseCliArgs(args);
962
+ const selector = positionals[0];
963
+ if (!selector) {
964
+ throw new Error("Usage: slack-max-api-mcp oauth use <profile_key_or_name>");
965
+ }
966
+
967
+ const tokenStore = loadTokenStore();
968
+ const resolved = resolveTokenStoreProfileBySelector(tokenStore, selector);
969
+ if (!resolved) {
970
+ throw new Error(`Profile not found: ${selector}`);
971
+ }
972
+
973
+ tokenStore.default_profile = resolved.key;
974
+ saveTokenStore(tokenStore);
975
+ console.log(`[oauth] default profile set to ${resolved.key} (${resolved.profile.profile_name || "unnamed"})`);
976
+ }
977
+
978
+ function runOauthCurrent() {
979
+ const tokenStore = loadTokenStore();
980
+ const resolved = resolveTokenStoreProfileBySelector(tokenStore, process.env.SLACK_PROFILE);
981
+ if (!resolved) {
982
+ console.log("[oauth] no active profile");
983
+ return;
984
+ }
985
+ console.log(formatTokenProfileSummary(resolved.key, resolved.profile, tokenStore.default_profile === resolved.key));
986
+ }
987
+
988
+ async function runOauthCli(args) {
989
+ const subcommand = (args[0] || "help").toLowerCase();
990
+ const rest = args.slice(1);
991
+
992
+ if (subcommand === "help" || subcommand === "--help" || subcommand === "-h") {
993
+ printOauthHelp();
994
+ return;
995
+ }
996
+ if (subcommand === "login") {
997
+ await runOauthLogin(rest);
998
+ return;
999
+ }
1000
+ if (subcommand === "list") {
1001
+ runOauthList();
1002
+ return;
1003
+ }
1004
+ if (subcommand === "use") {
1005
+ runOauthUse(rest);
1006
+ return;
1007
+ }
1008
+ if (subcommand === "current") {
1009
+ runOauthCurrent();
1010
+ return;
1011
+ }
1012
+
1013
+ throw new Error(`Unknown oauth command: ${subcommand}`);
1014
+ }
1015
+
1016
+ function printOnboardHelp() {
1017
+ const lines = [
1018
+ "Slack Max onboarding helper",
1019
+ "",
1020
+ "Usage:",
1021
+ " slack-max-api-mcp onboard run --gateway https://gateway.example.com [--token <invite_token>]",
1022
+ " [--profile NAME] [--team T123] [--scope a,b] [--user-scope c,d]",
1023
+ " slack-max-api-mcp onboard quick --gateway https://gateway.example.com",
1024
+ " slack-max-api-mcp onboard help",
1025
+ "",
1026
+ "If --token is omitted, it uses gateway public onboarding endpoint (/onboard/bootstrap).",
1027
+ "This command writes local client config and opens the Slack OAuth approval page automatically.",
1028
+ ];
1029
+ console.log(lines.join("\n"));
1030
+ }
1031
+
1032
+ async function runOnboardStart(args) {
1033
+ const { options } = parseCliArgs(args);
1034
+ const gateway = String(options.gateway || options.url || "").replace(/\/+$/, "");
1035
+ const token = String(options.token || "");
1036
+ if (!gateway) {
1037
+ throw new Error(
1038
+ "Usage: slack-max-api-mcp onboard run --gateway <url> [--token <invite_token>] [--profile <name>]"
1039
+ );
1040
+ }
1041
+
1042
+ const requestedProfile =
1043
+ String(options.profile || "").trim() || createAutoOnboardProfileName(AUTO_ONBOARD_PROFILE_PREFIX);
1044
+ const requestedTeam = String(options.team || "").trim();
1045
+ const requestedScope = parseScopeList(options.scope || "").join(",");
1046
+ const requestedUserScope = parseScopeList(options["user-scope"] || options.user_scope || "").join(",");
1047
+
1048
+ const onboardingUrl = token
1049
+ ? `${gateway}/onboard/resolve?token=${encodeURIComponent(token)}`
1050
+ : (() => {
1051
+ const params = new URLSearchParams();
1052
+ if (requestedProfile) params.set("profile", requestedProfile);
1053
+ if (requestedTeam) params.set("team", requestedTeam);
1054
+ if (requestedScope) params.set("scope", requestedScope);
1055
+ if (requestedUserScope) params.set("user_scope", requestedUserScope);
1056
+ const query = params.toString();
1057
+ return `${gateway}/onboard/bootstrap${query ? `?${query}` : ""}`;
1058
+ })();
1059
+
1060
+ const response = await fetch(onboardingUrl, {
1061
+ method: "GET",
1062
+ headers: { Accept: "application/json" },
1063
+ });
1064
+
1065
+ const text = await response.text();
1066
+ let data;
1067
+ try {
1068
+ data = JSON.parse(text);
1069
+ } catch {
1070
+ throw new Error(`Onboarding response was non-JSON (HTTP ${response.status}).`);
1071
+ }
1072
+
1073
+ if (!response.ok || !data?.ok) {
1074
+ if (!token && response.status === 404) {
1075
+ throw new Error("Onboarding failed: public onboarding is disabled on gateway (enable SLACK_GATEWAY_PUBLIC_ONBOARD=true).");
1076
+ }
1077
+ throw new Error(`Onboarding failed: ${data?.error || `http_${response.status}`}`);
1078
+ }
1079
+
1080
+ const resolvedGatewayUrl = String(data.gateway_url || gateway).replace(/\/+$/, "");
1081
+ const resolvedApiKey = String(data.gateway_api_key || "");
1082
+ const profile = String(data.profile || requestedProfile || "");
1083
+ const oauthStartUrl = String(data.oauth_start_url || "");
1084
+
1085
+ if (data.requires_gateway_api_key && !resolvedApiKey) {
1086
+ throw new Error(
1087
+ "Gateway requires API key but onboarding response did not provide one. Enable public gateway access or set SLACK_GATEWAY_PUBLIC_ONBOARD_API_KEY."
1088
+ );
1089
+ }
1090
+
1091
+ saveClientConfig({
1092
+ version: 1,
1093
+ gateway_url: resolvedGatewayUrl,
1094
+ gateway_api_key: resolvedApiKey,
1095
+ profile,
1096
+ updated_at: new Date().toISOString(),
1097
+ });
1098
+
1099
+ if (oauthStartUrl) {
1100
+ const opened = openExternalUrl(oauthStartUrl);
1101
+ if (!opened) {
1102
+ console.log(`[onboard] Open this URL in browser:\n${oauthStartUrl}`);
1103
+ }
1104
+ }
1105
+
1106
+ console.log(`[onboard] client config saved: ${CLIENT_CONFIG_PATH}`);
1107
+ console.log(`[onboard] gateway: ${resolvedGatewayUrl}`);
1108
+ if (profile) console.log(`[onboard] profile: ${profile}`);
1109
+ if (data.mode === "public_onboard") {
1110
+ console.log("[onboard] mode: public_onboard (tokenless)");
1111
+ }
1112
+ console.log("[onboard] Next: approve in browser, then use Codex MCP as usual.");
1113
+ }
1114
+
1115
+ async function runOnboardCli(args) {
1116
+ const subcommand = (args[0] || "help").toLowerCase();
1117
+ const rest = args.slice(1);
1118
+ if (subcommand === "help" || subcommand === "--help" || subcommand === "-h") {
1119
+ printOnboardHelp();
1120
+ return;
1121
+ }
1122
+ if (subcommand === "run" || subcommand === "start" || subcommand === "quick") {
1123
+ await runOnboardStart(rest);
1124
+ return;
1125
+ }
1126
+ throw new Error(`Unknown onboard command: ${subcommand}`);
1127
+ }
1128
+
1129
+ function sendText(res, statusCode, text) {
1130
+ res.writeHead(statusCode, { "Content-Type": "text/plain; charset=utf-8" });
1131
+ res.end(text);
1132
+ }
1133
+
1134
+ function sendJson(res, statusCode, payload) {
1135
+ res.writeHead(statusCode, { "Content-Type": "application/json; charset=utf-8" });
1136
+ res.end(JSON.stringify(payload, null, 2));
1137
+ }
1138
+
1139
+ async function readRequestText(req, maxBytes = 1024 * 1024) {
1140
+ return new Promise((resolve, reject) => {
1141
+ const chunks = [];
1142
+ let total = 0;
1143
+ req.on("data", (chunk) => {
1144
+ total += chunk.length;
1145
+ if (total > maxBytes) {
1146
+ reject(new Error("Request body too large."));
1147
+ req.destroy();
1148
+ return;
1149
+ }
1150
+ chunks.push(chunk);
1151
+ });
1152
+ req.on("end", () => resolve(Buffer.concat(chunks).toString("utf8")));
1153
+ req.on("error", (error) => reject(error));
1154
+ });
1155
+ }
1156
+
1157
+ async function readRequestJson(req, maxBytes) {
1158
+ const text = await readRequestText(req, maxBytes);
1159
+ if (!text.trim()) return {};
1160
+ try {
1161
+ return JSON.parse(text);
1162
+ } catch {
1163
+ throw new Error("Invalid JSON body.");
1164
+ }
1165
+ }
1166
+
1167
+ function getRequestApiKey(req) {
1168
+ const authHeader = req.headers.authorization || "";
1169
+ if (authHeader.toLowerCase().startsWith("bearer ")) {
1170
+ return authHeader.slice(7).trim();
1171
+ }
1172
+ const xApiKey = req.headers["x-api-key"];
1173
+ return typeof xApiKey === "string" ? xApiKey.trim() : "";
1174
+ }
1175
+
1176
+ function isGatewayAuthorized(req) {
1177
+ if (GATEWAY_ALLOW_PUBLIC) return true;
1178
+ const allowedKeys = [GATEWAY_SHARED_SECRET, GATEWAY_CLIENT_API_KEY].filter(Boolean);
1179
+ if (allowedKeys.length === 0) return false;
1180
+ const provided = getRequestApiKey(req);
1181
+ return Boolean(provided && allowedKeys.includes(provided));
1182
+ }
1183
+
1184
+ function requireGatewayClientCredentials() {
1185
+ const clientId = process.env.SLACK_CLIENT_ID || "";
1186
+ const clientSecret = process.env.SLACK_CLIENT_SECRET || "";
1187
+ if (!clientId || !clientSecret) {
1188
+ throw new Error("Gateway OAuth requires SLACK_CLIENT_ID and SLACK_CLIENT_SECRET on the gateway server.");
1189
+ }
1190
+ return { clientId, clientSecret };
1191
+ }
1192
+
1193
+ function profileSummariesFromStore(store) {
1194
+ const summaries = [];
1195
+ for (const [key, profile] of Object.entries(store.profiles || {})) {
1196
+ summaries.push({
1197
+ key,
1198
+ profile_name: profile.profile_name || "",
1199
+ team_id: profile.team_id || "",
1200
+ team_name: profile.team_name || "",
1201
+ authed_user_id: profile.authed_user_id || "",
1202
+ has_bot_token: Boolean(profile.bot_token),
1203
+ has_user_token: Boolean(profile.user_token),
1204
+ updated_at: profile.updated_at || null,
1205
+ is_default: store.default_profile === key,
1206
+ });
1207
+ }
1208
+ return summaries;
1209
+ }
1210
+
1211
+ function buildGatewayRedirectUri() {
1212
+ const url = new URL(OAUTH_CALLBACK_PATH, `${GATEWAY_PUBLIC_BASE_URL.replace(/\/+$/, "")}/`);
1213
+ return url.toString();
1214
+ }
1215
+
1216
+ function parseScopesFromQuery(searchParams, key, fallback) {
1217
+ const value = searchParams.get(key);
1218
+ return parseScopeList(value || fallback);
1219
+ }
1220
+
1221
+ function buildOauthStartUrlFromInvitePayload(gatewayBaseUrl, payload) {
1222
+ const params = new URLSearchParams();
1223
+ if (payload.profile) params.set("profile", payload.profile);
1224
+ if (payload.team) params.set("team", payload.team);
1225
+ if (payload.scope) params.set("scope", payload.scope);
1226
+ if (payload.user_scope) params.set("user_scope", payload.user_scope);
1227
+ return `${gatewayBaseUrl.replace(/\/+$/, "")}/oauth/start${params.toString() ? `?${params.toString()}` : ""}`;
1228
+ }
1229
+
1230
+ function buildPublicOnboardPayload(gatewayBaseUrl, params = {}) {
1231
+ const profile = String(params.profile || "").trim() || createAutoOnboardProfileName(GATEWAY_PUBLIC_ONBOARD_PROFILE_PREFIX);
1232
+ const team = String(params.team || process.env.SLACK_OAUTH_TEAM_ID || "").trim();
1233
+ const scope = parseScopeList(params.scope || DEFAULT_OAUTH_BOT_SCOPES).join(",");
1234
+ const userScope = parseScopeList(params.user_scope || DEFAULT_OAUTH_USER_SCOPES).join(",");
1235
+ const payload = {
1236
+ gateway_url: gatewayBaseUrl,
1237
+ gateway_api_key: "",
1238
+ profile,
1239
+ team,
1240
+ scope,
1241
+ user_scope: userScope,
1242
+ };
1243
+ if (GATEWAY_ALLOW_PUBLIC) {
1244
+ payload.gateway_api_key = "";
1245
+ } else if (GATEWAY_PUBLIC_ONBOARD_API_KEY) {
1246
+ payload.gateway_api_key = GATEWAY_PUBLIC_ONBOARD_API_KEY;
1247
+ } else if (GATEWAY_PUBLIC_ONBOARD_EXPOSE_API_KEY) {
1248
+ payload.gateway_api_key = GATEWAY_CLIENT_API_KEY || "";
1249
+ }
1250
+ const oauthStartUrl = buildOauthStartUrlFromInvitePayload(gatewayBaseUrl, payload);
1251
+ return {
1252
+ ok: true,
1253
+ mode: "public_onboard",
1254
+ gateway_url: payload.gateway_url,
1255
+ gateway_api_key: payload.gateway_api_key,
1256
+ profile: payload.profile,
1257
+ oauth_start_url: oauthStartUrl,
1258
+ requires_gateway_api_key: !GATEWAY_ALLOW_PUBLIC,
1259
+ };
1260
+ }
1261
+
1262
+ function buildOnboardPowerShellScript({ gatewayBaseUrl, token, profile, team, scope, userScope }) {
1263
+ const safeGateway = String(gatewayBaseUrl || "").replace(/'/g, "''");
1264
+ const safeToken = String(token || "").replace(/'/g, "''");
1265
+ const safeProfile = String(profile || "").replace(/'/g, "''");
1266
+ const safeTeam = String(team || "").replace(/'/g, "''");
1267
+ const safeScope = String(scope || "").replace(/'/g, "''");
1268
+ const safeUserScope = String(userScope || "").replace(/'/g, "''");
1269
+ const safePackageSpec = String(ONBOARD_PACKAGE_SPEC || "").replace(/'/g, "''");
1270
+ const onboardCommandParts = [`npx -y '${safePackageSpec}' onboard run --gateway '${safeGateway}'`];
1271
+ if (safeToken) onboardCommandParts.push(`--token '${safeToken}'`);
1272
+ if (safeProfile) onboardCommandParts.push(`--profile '${safeProfile}'`);
1273
+ if (safeTeam) onboardCommandParts.push(`--team '${safeTeam}'`);
1274
+ if (safeScope) onboardCommandParts.push(`--scope '${safeScope}'`);
1275
+ if (safeUserScope) onboardCommandParts.push(`--user-scope '${safeUserScope}'`);
1276
+
1277
+ const lines = [
1278
+ "$ErrorActionPreference = 'Stop'",
1279
+ "if (-not (Get-Command npx -ErrorAction SilentlyContinue)) { throw 'npx is required. Install Node.js first.' }",
1280
+ ];
1281
+ if (ONBOARD_SKIP_TLS_VERIFY) {
1282
+ lines.push("$env:NODE_TLS_REJECT_UNAUTHORIZED='0'");
1283
+ }
1284
+ lines.push(onboardCommandParts.join(" "));
1285
+ if (ONBOARD_SKIP_TLS_VERIFY) {
1286
+ lines.push("Remove-Item Env:NODE_TLS_REJECT_UNAUTHORIZED -ErrorAction SilentlyContinue");
1287
+ }
1288
+ return lines.join("\r\n");
1289
+ }
1290
+
1291
+ function createGatewayInviteTokenFromOptions(options = {}) {
1292
+ const secret = requireGatewayInviteSecret();
1293
+ const profile = String(options.profile || "").trim();
1294
+ const team = String(options.team || "").trim();
1295
+ const scope = parseScopeList(options.scope || DEFAULT_OAUTH_BOT_SCOPES).join(",");
1296
+ const userScope = parseScopeList(options["user-scope"] || options.user_scope || DEFAULT_OAUTH_USER_SCOPES).join(
1297
+ ","
380
1298
  );
1299
+ const ttlDays = Math.max(1, Number(options.days || INVITE_TOKEN_DEFAULT_DAYS));
1300
+ const gatewayUrl = String(options.gateway || options.gateway_url || GATEWAY_PUBLIC_BASE_URL).replace(/\/+$/, "");
1301
+
1302
+ const payload = {
1303
+ v: 1,
1304
+ exp: Date.now() + ttlDays * 24 * 60 * 60 * 1000,
1305
+ gateway_url: gatewayUrl,
1306
+ gateway_api_key: String(options["client-api-key"] || options.client_api_key || GATEWAY_CLIENT_API_KEY || ""),
1307
+ profile,
1308
+ team,
1309
+ scope,
1310
+ user_scope: userScope,
1311
+ };
1312
+ const token = createSignedInviteToken(payload, secret);
1313
+ return { token, payload };
1314
+ }
381
1315
 
382
- server.registerTool(
383
- "search_channels",
384
- {
385
- description:
386
- "Find channels by partial match on name/topic/purpose using conversations.list and local filtering.",
387
- inputSchema: {
388
- query: z.string().optional(),
389
- types: z.string().optional(),
390
- exclude_archived: z.boolean().optional(),
391
- limit: z.number().int().min(1).max(1000).optional(),
392
- cursor: z.string().optional(),
393
- token_override: z.string().optional(),
394
- },
395
- },
396
- async ({ query, types, exclude_archived, limit, cursor, token_override }) =>
397
- safeToolRun(async () => {
398
- const data = await callSlackApi(
399
- "conversations.list",
400
- {
401
- types: types || "public_channel,private_channel",
402
- exclude_archived: exclude_archived ?? true,
403
- limit: limit ?? 200,
404
- cursor,
405
- },
406
- token_override
407
- );
1316
+ async function proxySlackHttpRequest(payload) {
1317
+ const tokenCandidate = requireSlackTokenCandidate(payload.token_override, {
1318
+ profileSelector: payload.profile_selector || process.env.SLACK_PROFILE || GATEWAY_PROFILE || undefined,
1319
+ preferredTokenType: payload.preferred_token_type || process.env.SLACK_DEFAULT_TOKEN_TYPE || undefined,
1320
+ });
1321
+
1322
+ const method = payload.http_method || "GET";
1323
+ const endpoint = new URL(payload.url);
1324
+ for (const [k, v] of Object.entries(toRecordObject(payload.query))) {
1325
+ if (v === undefined || v === null) continue;
1326
+ endpoint.searchParams.set(k, String(v));
1327
+ }
408
1328
 
409
- let channels = Array.isArray(data.channels) ? data.channels : [];
410
- if (query) {
411
- const normalizedQuery = query.toLowerCase();
412
- channels = channels.filter((channel) => {
413
- const candidates = [channel.id, channel.name, channel.purpose?.value, channel.topic?.value]
414
- .filter((v) => typeof v === "string")
415
- .map((v) => v.toLowerCase());
416
- return candidates.some((value) => value.includes(normalizedQuery));
1329
+ const reqHeaders = {
1330
+ Authorization: `Bearer ${tokenCandidate.token}`,
1331
+ ...toRecordObject(payload.headers),
1332
+ };
1333
+
1334
+ let body;
1335
+ const formBody = toRecordObject(payload.form_body);
1336
+ const jsonBody = toRecordObject(payload.json_body);
1337
+ if (Object.keys(formBody).length > 0) {
1338
+ reqHeaders["Content-Type"] = "application/x-www-form-urlencoded; charset=utf-8";
1339
+ body = toUrlEncodedBody(formBody);
1340
+ } else if (Object.keys(jsonBody).length > 0) {
1341
+ reqHeaders["Content-Type"] = "application/json; charset=utf-8";
1342
+ body = JSON.stringify(jsonBody);
1343
+ }
1344
+
1345
+ const res = await fetch(endpoint.toString(), { method, headers: reqHeaders, body });
1346
+ const text = await res.text();
1347
+ let parsedBody = text;
1348
+ try {
1349
+ parsedBody = JSON.parse(text);
1350
+ } catch {
1351
+ // keep text
1352
+ }
1353
+
1354
+ return {
1355
+ url: endpoint.toString(),
1356
+ status: res.status,
1357
+ ok: res.ok,
1358
+ headers: Object.fromEntries(res.headers.entries()),
1359
+ body: parsedBody,
1360
+ token_source: tokenCandidate.source,
1361
+ };
1362
+ }
1363
+
1364
+ async function startGatewayServer() {
1365
+ const pendingStates = new Map();
1366
+ const callbackPath = OAUTH_CALLBACK_PATH;
1367
+ const redirectUri = buildGatewayRedirectUri();
1368
+ const gatewayBaseUrl = `${GATEWAY_PUBLIC_BASE_URL.replace(/\/+$/, "")}`;
1369
+
1370
+ const server = http.createServer(async (req, res) => {
1371
+ try {
1372
+ const method = req.method || "GET";
1373
+ const requestUrl = new URL(req.url || "/", `http://${GATEWAY_HOST}:${GATEWAY_PORT}`);
1374
+
1375
+ if (method === "GET" && requestUrl.pathname === "/health") {
1376
+ sendJson(res, 200, {
1377
+ ok: true,
1378
+ service: SERVER_NAME,
1379
+ mode: "gateway",
1380
+ token_store_path: TOKEN_STORE_PATH,
1381
+ client_config_path: CLIENT_CONFIG_PATH,
1382
+ callback_url: redirectUri,
1383
+ });
1384
+ return;
1385
+ }
1386
+
1387
+ if (method === "GET" && requestUrl.pathname === "/onboard.ps1") {
1388
+ const token = requestUrl.searchParams.get("token") || "";
1389
+ let script = "";
1390
+ if (token) {
1391
+ const secret = requireGatewayInviteSecret();
1392
+ const payload = parseAndVerifyInviteToken(token, secret);
1393
+ script = buildOnboardPowerShellScript({
1394
+ gatewayBaseUrl: payload.gateway_url || gatewayBaseUrl,
1395
+ token,
417
1396
  });
1397
+ } else {
1398
+ if (!GATEWAY_PUBLIC_ONBOARD_ENABLED) {
1399
+ sendJson(res, 404, { ok: false, error: "public_onboard_disabled" });
1400
+ return;
1401
+ }
1402
+ const profile =
1403
+ requestUrl.searchParams.get("profile") ||
1404
+ createAutoOnboardProfileName(GATEWAY_PUBLIC_ONBOARD_PROFILE_PREFIX);
1405
+ const team = requestUrl.searchParams.get("team") || "";
1406
+ const scope = requestUrl.searchParams.get("scope") || "";
1407
+ const userScope = requestUrl.searchParams.get("user_scope") || "";
1408
+ script = buildOnboardPowerShellScript({
1409
+ gatewayBaseUrl,
1410
+ profile,
1411
+ team,
1412
+ scope,
1413
+ userScope,
1414
+ });
1415
+ }
1416
+ res.writeHead(200, {
1417
+ "Content-Type": "text/plain; charset=utf-8",
1418
+ "Cache-Control": "no-store",
1419
+ });
1420
+ res.end(script);
1421
+ return;
1422
+ }
1423
+
1424
+ if (method === "GET" && requestUrl.pathname === "/onboard/bootstrap") {
1425
+ if (!GATEWAY_PUBLIC_ONBOARD_ENABLED) {
1426
+ sendJson(res, 404, { ok: false, error: "public_onboard_disabled" });
1427
+ return;
418
1428
  }
1429
+ const payload = buildPublicOnboardPayload(gatewayBaseUrl, {
1430
+ profile: requestUrl.searchParams.get("profile") || "",
1431
+ team: requestUrl.searchParams.get("team") || "",
1432
+ scope: requestUrl.searchParams.get("scope") || "",
1433
+ user_scope: requestUrl.searchParams.get("user_scope") || "",
1434
+ });
1435
+ sendJson(res, 200, payload);
1436
+ return;
1437
+ }
1438
+
1439
+ if (method === "GET" && requestUrl.pathname === "/onboard/resolve") {
1440
+ const token = requestUrl.searchParams.get("token") || "";
1441
+ const secret = requireGatewayInviteSecret();
1442
+ const payload = parseAndVerifyInviteToken(token, secret);
1443
+ const oauthStartUrl = buildOauthStartUrlFromInvitePayload(gatewayBaseUrl, payload);
1444
+ sendJson(res, 200, {
1445
+ ok: true,
1446
+ mode: "invite_token",
1447
+ gateway_url: payload.gateway_url || gatewayBaseUrl,
1448
+ gateway_api_key: payload.gateway_api_key || "",
1449
+ profile: payload.profile || "",
1450
+ oauth_start_url: oauthStartUrl,
1451
+ expires_at: new Date(Number(payload.exp)).toISOString(),
1452
+ });
1453
+ return;
1454
+ }
1455
+
1456
+ if (method === "GET" && requestUrl.pathname === "/oauth/start") {
1457
+ const { clientId } = requireGatewayClientCredentials();
1458
+ const profileName = requestUrl.searchParams.get("profile") || "";
1459
+ const teamId = requestUrl.searchParams.get("team") || process.env.SLACK_OAUTH_TEAM_ID || "";
1460
+ const botScopes = parseScopesFromQuery(requestUrl.searchParams, "scope", DEFAULT_OAUTH_BOT_SCOPES);
1461
+ const userScopes = parseScopesFromQuery(
1462
+ requestUrl.searchParams,
1463
+ "user_scope",
1464
+ DEFAULT_OAUTH_USER_SCOPES
1465
+ );
419
1466
 
420
- return {
421
- channels,
422
- next_cursor: data.response_metadata?.next_cursor || null,
423
- count: channels.length,
424
- };
425
- })
426
- );
1467
+ if (botScopes.length === 0 && userScopes.length === 0) {
1468
+ sendJson(res, 400, { ok: false, error: "missing_scope" });
1469
+ return;
1470
+ }
427
1471
 
428
- server.registerTool(
429
- "send_message",
430
- {
431
- description: "Send a message using chat.postMessage.",
432
- inputSchema: {
433
- channel: z.string().min(1),
434
- text: z.string().min(1),
435
- thread_ts: z.string().optional(),
436
- blocks: z.any().optional(),
437
- mrkdwn: z.boolean().optional(),
438
- unfurl_links: z.boolean().optional(),
439
- unfurl_media: z.boolean().optional(),
440
- token_override: z.string().optional(),
441
- },
442
- },
443
- async ({ channel, text, thread_ts, blocks, mrkdwn, unfurl_links, unfurl_media, token_override }) =>
444
- safeToolRun(async () => {
445
- const data = await callSlackApi(
446
- "chat.postMessage",
447
- {
448
- channel,
449
- text,
450
- thread_ts,
451
- blocks: parseJsonMaybe(blocks),
452
- mrkdwn,
453
- unfurl_links,
454
- unfurl_media,
455
- },
456
- token_override
457
- );
1472
+ const state = crypto.randomBytes(24).toString("hex");
1473
+ pendingStates.set(state, {
1474
+ created_at: Date.now(),
1475
+ profile_name: profileName,
1476
+ team_id: teamId,
1477
+ bot_scopes: botScopes,
1478
+ user_scopes: userScopes,
1479
+ });
458
1480
 
459
- return {
460
- channel: data.channel,
461
- ts: data.ts,
462
- message: data.message,
463
- };
464
- })
465
- );
1481
+ const authorizeUrl = buildOauthAuthorizeUrl({
1482
+ clientId,
1483
+ state,
1484
+ redirectUri,
1485
+ botScopes,
1486
+ userScopes,
1487
+ teamId,
1488
+ });
466
1489
 
467
- server.registerTool(
468
- "read_channel",
469
- {
470
- description: "Read channel history with conversations.history.",
471
- inputSchema: {
472
- channel: z.string().min(1),
473
- limit: z.number().int().min(1).max(1000).optional(),
474
- cursor: z.string().optional(),
475
- oldest: z.string().optional(),
476
- latest: z.string().optional(),
477
- inclusive: z.boolean().optional(),
478
- token_override: z.string().optional(),
479
- },
480
- },
481
- async ({ channel, limit, cursor, oldest, latest, inclusive, token_override }) =>
482
- safeToolRun(async () => {
483
- const data = await callSlackApi(
484
- "conversations.history",
485
- {
486
- channel,
487
- limit: limit ?? 50,
488
- cursor,
489
- oldest,
490
- latest,
491
- inclusive,
492
- },
493
- token_override
494
- );
1490
+ res.writeHead(302, { Location: authorizeUrl });
1491
+ res.end();
1492
+ return;
1493
+ }
1494
+
1495
+ if (method === "GET" && requestUrl.pathname === callbackPath) {
1496
+ const { clientId, clientSecret } = requireGatewayClientCredentials();
1497
+ const receivedError = requestUrl.searchParams.get("error");
1498
+ if (receivedError) {
1499
+ sendText(res, 400, `Slack OAuth failed: ${receivedError}`);
1500
+ return;
1501
+ }
495
1502
 
496
- return {
497
- channel,
498
- messages: data.messages || [],
499
- has_more: Boolean(data.has_more),
500
- next_cursor: data.response_metadata?.next_cursor || null,
501
- };
502
- })
503
- );
1503
+ const state = requestUrl.searchParams.get("state");
1504
+ const code = requestUrl.searchParams.get("code");
1505
+ if (!state || !code) {
1506
+ sendText(res, 400, "Missing state/code.");
1507
+ return;
1508
+ }
504
1509
 
505
- server.registerTool(
506
- "read_thread",
507
- {
508
- description: "Read a thread using conversations.replies.",
509
- inputSchema: {
510
- channel: z.string().min(1),
511
- thread_ts: z.string().min(1),
512
- limit: z.number().int().min(1).max(1000).optional(),
513
- cursor: z.string().optional(),
514
- oldest: z.string().optional(),
515
- latest: z.string().optional(),
516
- inclusive: z.boolean().optional(),
517
- token_override: z.string().optional(),
518
- },
519
- },
520
- async ({ channel, thread_ts, limit, cursor, oldest, latest, inclusive, token_override }) =>
521
- safeToolRun(async () => {
522
- const data = await callSlackApi(
523
- "conversations.replies",
524
- {
525
- channel,
526
- ts: thread_ts,
527
- limit: limit ?? 50,
528
- cursor,
529
- oldest,
530
- latest,
531
- inclusive,
532
- },
533
- token_override
1510
+ const pending = pendingStates.get(state);
1511
+ pendingStates.delete(state);
1512
+ if (!pending) {
1513
+ sendText(res, 400, "Invalid or expired OAuth state.");
1514
+ return;
1515
+ }
1516
+ if (Date.now() - pending.created_at > GATEWAY_STATE_TTL_MS) {
1517
+ sendText(res, 400, "Expired OAuth state.");
1518
+ return;
1519
+ }
1520
+
1521
+ const oauthResponse = await exchangeOauthCode({ clientId, clientSecret, code, redirectUri });
1522
+ const { key, profile } = upsertOauthProfile(oauthResponse, pending.profile_name);
1523
+ sendText(
1524
+ res,
1525
+ 200,
1526
+ [
1527
+ "Slack OAuth authorization completed.",
1528
+ `Saved profile: ${profile.profile_name || key}`,
1529
+ `Profile key: ${key}`,
1530
+ "You can close this tab.",
1531
+ ].join("\n")
534
1532
  );
1533
+ return;
1534
+ }
1535
+
1536
+ if (method === "GET" && requestUrl.pathname === "/oauth/link") {
1537
+ const params = new URLSearchParams();
1538
+ const profile = requestUrl.searchParams.get("profile") || "";
1539
+ const team = requestUrl.searchParams.get("team") || "";
1540
+ const scope = requestUrl.searchParams.get("scope") || "";
1541
+ const userScope = requestUrl.searchParams.get("user_scope") || "";
1542
+ if (profile) params.set("profile", profile);
1543
+ if (team) params.set("team", team);
1544
+ if (scope) params.set("scope", scope);
1545
+ if (userScope) params.set("user_scope", userScope);
1546
+ sendJson(res, 200, {
1547
+ ok: true,
1548
+ url: `${gatewayBaseUrl}/oauth/start${params.toString() ? `?${params.toString()}` : ""}`,
1549
+ });
1550
+ return;
1551
+ }
535
1552
 
536
- return {
537
- channel,
538
- thread_ts,
539
- messages: data.messages || [],
540
- has_more: Boolean(data.has_more),
541
- next_cursor: data.response_metadata?.next_cursor || null,
542
- };
543
- })
544
- );
1553
+ if (method === "GET" && requestUrl.pathname === "/profiles") {
1554
+ if (!isGatewayAuthorized(req)) {
1555
+ sendJson(res, 401, { ok: false, error: "unauthorized" });
1556
+ return;
1557
+ }
1558
+ const tokenStore = loadTokenStore();
1559
+ sendJson(res, 200, {
1560
+ ok: true,
1561
+ default_profile: tokenStore.default_profile,
1562
+ profiles: profileSummariesFromStore(tokenStore),
1563
+ });
1564
+ return;
1565
+ }
545
1566
 
546
- server.registerTool(
547
- "create_canvas",
548
- {
549
- description: "Create a canvas using canvases.create. Pass Slack params in `params`.",
550
- inputSchema: {
551
- params: z.record(z.string(), z.any()),
552
- token_override: z.string().optional(),
553
- },
554
- },
555
- async ({ params, token_override }) =>
556
- safeToolRun(async () => {
557
- const data = await callSlackApi("canvases.create", params, token_override);
558
- return data;
559
- })
560
- );
1567
+ if (method === "POST" && requestUrl.pathname === "/api/slack/call") {
1568
+ if (!isGatewayAuthorized(req)) {
1569
+ sendJson(res, 401, { ok: false, error: "unauthorized" });
1570
+ return;
1571
+ }
561
1572
 
562
- server.registerTool(
563
- "update_canvas",
564
- {
565
- description: "Update a canvas using canvases.edit. Pass Slack params in `params`.",
566
- inputSchema: {
567
- params: z.record(z.string(), z.any()),
568
- token_override: z.string().optional(),
569
- },
570
- },
571
- async ({ params, token_override }) =>
572
- safeToolRun(async () => {
573
- const data = await callSlackApi("canvases.edit", params, token_override);
574
- return data;
575
- })
576
- );
1573
+ const payload = await readRequestJson(req, 1024 * 1024);
1574
+ const methodName = payload.method;
1575
+ if (!methodName || typeof methodName !== "string") {
1576
+ sendJson(res, 400, { ok: false, error: "missing_method" });
1577
+ return;
1578
+ }
577
1579
 
578
- server.registerTool(
579
- "read_canvas",
580
- {
581
- description: "Read canvas content using canvases.sections.lookup. Pass Slack params in `params`.",
582
- inputSchema: {
583
- params: z.record(z.string(), z.any()),
584
- token_override: z.string().optional(),
585
- },
586
- },
587
- async ({ params, token_override }) =>
588
- safeToolRun(async () => {
589
- const data = await callSlackApi("canvases.sections.lookup", params, token_override);
590
- return data;
591
- })
592
- );
1580
+ const candidates = getSlackTokenCandidates(payload.token_override, {
1581
+ profileSelector:
1582
+ payload.profile_selector || process.env.SLACK_PROFILE || GATEWAY_PROFILE || undefined,
1583
+ preferredTokenType:
1584
+ payload.preferred_token_type || process.env.SLACK_DEFAULT_TOKEN_TYPE || undefined,
1585
+ });
1586
+ if (candidates.length === 0) {
1587
+ sendJson(res, 400, { ok: false, error: "missing_token" });
1588
+ return;
1589
+ }
593
1590
 
594
- server.registerTool(
595
- "read_user_profile",
596
- {
597
- description: "Read user info/profile using users.info plus users.profile.get (best effort).",
598
- inputSchema: {
599
- user: z.string().min(1),
600
- include_locale: z.boolean().optional(),
601
- include_labels: z.boolean().optional(),
602
- token_override: z.string().optional(),
603
- },
604
- },
605
- async ({ user, include_locale, include_labels, token_override }) =>
606
- safeToolRun(async () => {
607
- const info = await callSlackApi(
608
- "users.info",
609
- { user, include_locale: include_locale ?? true },
610
- token_override
611
- );
1591
+ const data = await callSlackApiWithCandidates(methodName, payload.params || {}, candidates);
1592
+ sendJson(res, 200, { ok: true, data });
1593
+ return;
1594
+ }
612
1595
 
613
- let profile = null;
614
- try {
615
- const profileData = await callSlackApi(
616
- "users.profile.get",
617
- { user, include_labels: include_labels ?? true },
618
- token_override
619
- );
620
- profile = profileData.profile || null;
621
- } catch {
622
- profile = info.user?.profile || null;
1596
+ if (method === "POST" && requestUrl.pathname === "/api/slack/http") {
1597
+ if (!isGatewayAuthorized(req)) {
1598
+ sendJson(res, 401, { ok: false, error: "unauthorized" });
1599
+ return;
623
1600
  }
624
1601
 
625
- return { user: info.user || null, profile };
626
- })
627
- );
628
- }
1602
+ const payload = await readRequestJson(req, 1024 * 1024);
1603
+ if (!payload.url || typeof payload.url !== "string") {
1604
+ sendJson(res, 400, { ok: false, error: "missing_url" });
1605
+ return;
1606
+ }
629
1607
 
630
- function registerCatalogMethodTools(server, catalog) {
631
- if (!ENABLE_METHOD_TOOLS) return { registered: 0 };
1608
+ const data = await proxySlackHttpRequest(payload);
1609
+ sendJson(res, 200, { ok: true, data });
1610
+ return;
1611
+ }
1612
+
1613
+ sendJson(res, 404, { ok: false, error: "not_found" });
1614
+ } catch (error) {
1615
+ sendJson(res, 500, {
1616
+ ok: false,
1617
+ error: error instanceof Error ? error.message : String(error),
1618
+ slack_error: error?.slack_error || null,
1619
+ needed: error?.needed || null,
1620
+ provided: error?.provided || null,
1621
+ token_source: error?.token_source || null,
1622
+ });
1623
+ } finally {
1624
+ for (const [state, value] of pendingStates.entries()) {
1625
+ if (Date.now() - value.created_at > GATEWAY_STATE_TTL_MS) {
1626
+ pendingStates.delete(state);
1627
+ }
1628
+ }
1629
+ }
1630
+ });
632
1631
 
633
- const methods = Array.isArray(catalog.methods) ? catalog.methods : [];
634
- const limited = MAX_METHOD_TOOLS > 0 ? methods.slice(0, MAX_METHOD_TOOLS) : methods;
1632
+ await new Promise((resolve, reject) => {
1633
+ server.once("error", reject);
1634
+ server.listen(GATEWAY_PORT, GATEWAY_HOST, resolve);
1635
+ });
635
1636
 
636
- const usedNames = new Set();
637
- let registered = 0;
1637
+ console.error(
1638
+ `[${SERVER_NAME}] gateway listening at http://${GATEWAY_HOST}:${GATEWAY_PORT} | public_base=${gatewayBaseUrl}`
1639
+ );
1640
+ console.error(`[${SERVER_NAME}] oauth start URL: ${gatewayBaseUrl}/oauth/start`);
1641
+ console.error(`[${SERVER_NAME}] profile list URL: ${gatewayBaseUrl}/profiles`);
1642
+ if (GATEWAY_PUBLIC_ONBOARD_ENABLED) {
1643
+ console.error(`[${SERVER_NAME}] public onboard URL: ${gatewayBaseUrl}/onboard/bootstrap`);
1644
+ }
1645
+ }
638
1646
 
639
- for (const methodInfo of limited) {
640
- const method = methodInfo?.method;
641
- if (!method || typeof method !== "string") continue;
1647
+ function printGatewayHelp() {
1648
+ const lines = [
1649
+ "Slack Max Gateway helper",
1650
+ "",
1651
+ "Usage:",
1652
+ " slack-max-api-mcp gateway start",
1653
+ " slack-max-api-mcp gateway invite --profile woobin --team T123",
1654
+ " # tokenless onboarding endpoint (when enabled):",
1655
+ " # https://gateway.example.com/onboard/bootstrap",
1656
+ " slack-max-api-mcp gateway help",
1657
+ "",
1658
+ "Gateway env vars (server-side):",
1659
+ " SLACK_CLIENT_ID, SLACK_CLIENT_SECRET",
1660
+ " SLACK_GATEWAY_HOST, SLACK_GATEWAY_PORT, SLACK_GATEWAY_PUBLIC_BASE_URL",
1661
+ " SLACK_GATEWAY_SHARED_SECRET (recommended)",
1662
+ " SLACK_GATEWAY_CLIENT_API_KEY (optional, defaults to shared secret)",
1663
+ " SLACK_GATEWAY_PUBLIC_ONBOARD=true # allow tokenless onboarding endpoint",
1664
+ " SLACK_GATEWAY_PUBLIC_ONBOARD_API_KEY=<client key> # optional, used when gateway is not fully public",
1665
+ " SLACK_GATEWAY_PUBLIC_ONBOARD_EXPOSE_API_KEY=true # fallback: expose client key as-is",
1666
+ " SLACK_OAUTH_BOT_SCOPES, SLACK_OAUTH_USER_SCOPES",
1667
+ "",
1668
+ "Client env vars (mcp caller-side):",
1669
+ " SLACK_GATEWAY_URL, SLACK_GATEWAY_API_KEY",
1670
+ " SLACK_PROFILE or SLACK_GATEWAY_PROFILE",
1671
+ ];
1672
+ console.log(lines.join("\n"));
1673
+ }
642
1674
 
643
- const toolName = toolNameFromMethod(method, usedNames);
644
- const descriptionParts = [
645
- `Slack Web API method wrapper for ${method}.`,
646
- methodInfo.description ? `Official: ${methodInfo.description}` : "",
647
- Array.isArray(methodInfo.scopes) && methodInfo.scopes.length
648
- ? `Scopes: ${methodInfo.scopes.join(", ")}`
649
- : "",
650
- ].filter(Boolean);
1675
+ function runGatewayInvite(args) {
1676
+ const { options } = parseCliArgs(args);
1677
+ const { token, payload } = createGatewayInviteTokenFromOptions(options);
1678
+ const gatewayBaseUrl = String(payload.gateway_url || GATEWAY_PUBLIC_BASE_URL).replace(/\/+$/, "");
1679
+ const onboardScriptUrl = `${gatewayBaseUrl}/onboard.ps1?token=${encodeURIComponent(token)}`;
1680
+ const oauthStartUrl = buildOauthStartUrlFromInvitePayload(gatewayBaseUrl, payload);
1681
+ const command = `powershell -ExecutionPolicy Bypass -Command "irm '${onboardScriptUrl}' | iex"`;
1682
+ const commandCurlFallback = [
1683
+ `$tmp = Join-Path $env:TEMP 'slack-onboard.ps1'`,
1684
+ `curl.exe -k -sS '${onboardScriptUrl}' -o $tmp`,
1685
+ `powershell -ExecutionPolicy Bypass -File $tmp`,
1686
+ `Remove-Item $tmp -Force`,
1687
+ ].join("; ");
1688
+
1689
+ console.log("[gateway] invite token created");
1690
+ console.log(`[gateway] expires_at: ${new Date(Number(payload.exp)).toISOString()}`);
1691
+ console.log(`[gateway] onboarding_script: ${onboardScriptUrl}`);
1692
+ console.log(`[gateway] oauth_start_url: ${oauthStartUrl}`);
1693
+ console.log("[gateway] one-click command for team member:");
1694
+ console.log(command);
1695
+ console.log("[gateway] fallback command (self-signed TLS):");
1696
+ console.log(commandCurlFallback);
1697
+ }
651
1698
 
652
- server.registerTool(
653
- toolName,
654
- {
655
- description: descriptionParts.join(" "),
656
- inputSchema: {
657
- params: z.record(z.string(), z.any()).optional(),
658
- token_override: z.string().optional(),
659
- },
660
- },
661
- async ({ params, token_override }) =>
662
- safeToolRun(async () => {
663
- const data = await callSlackApi(method, params || {}, token_override);
664
- return { method, data };
665
- })
666
- );
667
- registered += 1;
1699
+ async function runGatewayCli(args) {
1700
+ const subcommand = (args[0] || "help").toLowerCase();
1701
+ if (subcommand === "help" || subcommand === "--help" || subcommand === "-h") {
1702
+ printGatewayHelp();
1703
+ return;
1704
+ }
1705
+ if (subcommand === "start") {
1706
+ await startGatewayServer();
1707
+ return;
668
1708
  }
1709
+ if (subcommand === "invite") {
1710
+ runGatewayInvite(args.slice(1));
1711
+ return;
1712
+ }
1713
+ throw new Error(`Unknown gateway command: ${subcommand}`);
1714
+ }
1715
+
1716
+ function loadCatalog() {
1717
+ if (!fs.existsSync(CATALOG_PATH)) {
1718
+ return { methods: [], scopes: [], totals: {} };
1719
+ }
1720
+
1721
+ try {
1722
+ const raw = fs.readFileSync(CATALOG_PATH, "utf8");
1723
+ const parsed = JSON.parse(raw);
1724
+ return parsed;
1725
+ } catch (error) {
1726
+ throw new Error(`Failed to load catalog at ${CATALOG_PATH}: ${error}`);
1727
+ }
1728
+ }
1729
+
1730
+ function toolNameFromMethod(method, usedNames) {
1731
+ const base = `${METHOD_TOOL_PREFIX}_${method.replace(/[^a-zA-Z0-9]/g, "_")}`;
1732
+ if (!usedNames.has(base)) {
1733
+ usedNames.add(base);
1734
+ return base;
1735
+ }
1736
+
1737
+ let idx = 2;
1738
+ while (usedNames.has(`${base}_${idx}`)) idx += 1;
1739
+ const name = `${base}_${idx}`;
1740
+ usedNames.add(name);
1741
+ return name;
1742
+ }
1743
+
1744
+ function registerCoreTools(server) {
1745
+ server.registerTool(
1746
+ "slack_api_call",
1747
+ {
1748
+ description: "Call any Slack Web API method directly.",
1749
+ inputSchema: {
1750
+ method: z
1751
+ .string()
1752
+ .min(3)
1753
+ .regex(/^[a-z][a-zA-Z0-9_.]+$/, "Method must look like chat.postMessage"),
1754
+ params: z.record(z.string(), z.any()).optional(),
1755
+ token_override: z.string().optional(),
1756
+ },
1757
+ },
1758
+ async ({ method, params, token_override }) =>
1759
+ safeToolRun(async () => {
1760
+ const data = await callSlackApi(method, params || {}, token_override);
1761
+ return { method, data };
1762
+ })
1763
+ );
1764
+
1765
+ server.registerTool(
1766
+ "slack_http_api_call",
1767
+ {
1768
+ description:
1769
+ "Generic HTTP call for Slack APIs outside standard Web API methods (SCIM/Audit/Legal Holds).",
1770
+ inputSchema: {
1771
+ url: z.string().url(),
1772
+ http_method: z.enum(["GET", "POST", "PUT", "PATCH", "DELETE"]).optional(),
1773
+ query: z.record(z.string(), z.any()).optional(),
1774
+ json_body: z.record(z.string(), z.any()).optional(),
1775
+ form_body: z.record(z.string(), z.any()).optional(),
1776
+ headers: z.record(z.string(), z.string()).optional(),
1777
+ token_override: z.string().optional(),
1778
+ },
1779
+ },
1780
+ async ({ url, http_method, query, json_body, form_body, headers, token_override }) =>
1781
+ safeToolRun(async () => {
1782
+ const runtimeGateway = getRuntimeGatewayConfig();
1783
+ if (runtimeGateway.url) {
1784
+ return slackHttpViaGateway({
1785
+ url,
1786
+ http_method,
1787
+ query,
1788
+ json_body,
1789
+ form_body,
1790
+ headers,
1791
+ token_override,
1792
+ });
1793
+ }
669
1794
 
670
- server.registerTool(
671
- "slack_method_tools_info",
672
- {
673
- description: "Return summary for catalog-driven method tools currently loaded.",
674
- inputSchema: {},
1795
+ const tokenCandidate = requireSlackTokenCandidate(token_override);
1796
+ const method = http_method || "GET";
1797
+
1798
+ const endpoint = new URL(url);
1799
+ for (const [k, v] of Object.entries(toRecordObject(query))) {
1800
+ if (v === undefined || v === null) continue;
1801
+ endpoint.searchParams.set(k, String(v));
1802
+ }
1803
+
1804
+ const reqHeaders = {
1805
+ Authorization: `Bearer ${tokenCandidate.token}`,
1806
+ ...(headers || {}),
1807
+ };
1808
+
1809
+ let body;
1810
+ if (form_body && Object.keys(form_body).length > 0) {
1811
+ reqHeaders["Content-Type"] = "application/x-www-form-urlencoded; charset=utf-8";
1812
+ body = toUrlEncodedBody(form_body);
1813
+ } else if (json_body && Object.keys(json_body).length > 0) {
1814
+ reqHeaders["Content-Type"] = "application/json; charset=utf-8";
1815
+ body = JSON.stringify(json_body);
1816
+ }
1817
+
1818
+ const res = await fetch(endpoint.toString(), {
1819
+ method,
1820
+ headers: reqHeaders,
1821
+ body,
1822
+ });
1823
+
1824
+ const text = await res.text();
1825
+ let parsedBody = text;
1826
+ try {
1827
+ parsedBody = JSON.parse(text);
1828
+ } catch {
1829
+ // Keep plain text when response is not JSON.
1830
+ }
1831
+
1832
+ return {
1833
+ url: endpoint.toString(),
1834
+ status: res.status,
1835
+ ok: res.ok,
1836
+ headers: Object.fromEntries(res.headers.entries()),
1837
+ body: parsedBody,
1838
+ };
1839
+ })
1840
+ );
1841
+
1842
+ server.registerTool(
1843
+ "search_messages_files",
1844
+ {
1845
+ description:
1846
+ "Search messages and files. Uses search.messages and search.files and returns both.",
1847
+ inputSchema: {
1848
+ query: z.string().min(1),
1849
+ count: z.number().int().min(1).max(100).optional(),
1850
+ page: z.number().int().min(1).optional(),
1851
+ sort: z.enum(["score", "timestamp"]).optional(),
1852
+ sort_dir: z.enum(["asc", "desc"]).optional(),
1853
+ include_messages: z.boolean().optional(),
1854
+ include_files: z.boolean().optional(),
1855
+ token_override: z.string().optional(),
1856
+ },
1857
+ },
1858
+ async ({
1859
+ query,
1860
+ count,
1861
+ page,
1862
+ sort,
1863
+ sort_dir,
1864
+ include_messages,
1865
+ include_files,
1866
+ token_override,
1867
+ }) =>
1868
+ safeToolRun(async () => {
1869
+ const shouldSearchMessages = include_messages !== false;
1870
+ const shouldSearchFiles = include_files !== false;
1871
+ const sharedParams = {
1872
+ query,
1873
+ count: count ?? 20,
1874
+ page: page ?? 1,
1875
+ sort,
1876
+ sort_dir,
1877
+ };
1878
+
1879
+ let messages = null;
1880
+ let files = null;
1881
+
1882
+ if (shouldSearchMessages) {
1883
+ messages = await callSlackApi("search.messages", sharedParams, token_override);
1884
+ }
1885
+ if (shouldSearchFiles) {
1886
+ files = await callSlackApi("search.files", sharedParams, token_override);
1887
+ }
1888
+
1889
+ return {
1890
+ query,
1891
+ messages: messages ? messages.messages : null,
1892
+ files: files ? files.files : null,
1893
+ };
1894
+ })
1895
+ );
1896
+
1897
+ server.registerTool(
1898
+ "search_users",
1899
+ {
1900
+ description:
1901
+ "Find users by partial match on id/name/display_name/email using users.list and local filtering.",
1902
+ inputSchema: {
1903
+ query: z.string().optional(),
1904
+ limit: z.number().int().min(1).max(1000).optional(),
1905
+ include_locale: z.boolean().optional(),
1906
+ include_deleted: z.boolean().optional(),
1907
+ include_bots: z.boolean().optional(),
1908
+ cursor: z.string().optional(),
1909
+ token_override: z.string().optional(),
1910
+ },
1911
+ },
1912
+ async ({
1913
+ query,
1914
+ limit,
1915
+ include_locale,
1916
+ include_deleted,
1917
+ include_bots,
1918
+ cursor,
1919
+ token_override,
1920
+ }) =>
1921
+ safeToolRun(async () => {
1922
+ const listData = await callSlackApi(
1923
+ "users.list",
1924
+ {
1925
+ limit: limit ?? 200,
1926
+ include_locale: include_locale ?? true,
1927
+ cursor,
1928
+ },
1929
+ token_override
1930
+ );
1931
+
1932
+ const normalizedQuery = query ? query.toLowerCase() : null;
1933
+ let users = Array.isArray(listData.members) ? listData.members : [];
1934
+
1935
+ if (include_deleted !== true) {
1936
+ users = users.filter((u) => !u.deleted);
1937
+ }
1938
+ if (include_bots !== true) {
1939
+ users = users.filter((u) => !u.is_bot && !u.is_app_user);
1940
+ }
1941
+ if (normalizedQuery) {
1942
+ users = users.filter((u) => {
1943
+ const candidates = [
1944
+ u.id,
1945
+ u.name,
1946
+ u.real_name,
1947
+ u.profile?.display_name,
1948
+ u.profile?.real_name,
1949
+ u.profile?.email,
1950
+ ]
1951
+ .filter((v) => typeof v === "string")
1952
+ .map((v) => v.toLowerCase());
1953
+ return candidates.some((value) => value.includes(normalizedQuery));
1954
+ });
1955
+ }
1956
+
1957
+ return {
1958
+ users,
1959
+ next_cursor: listData.response_metadata?.next_cursor || null,
1960
+ count: users.length,
1961
+ };
1962
+ })
1963
+ );
1964
+
1965
+ server.registerTool(
1966
+ "search_channels",
1967
+ {
1968
+ description:
1969
+ "Find channels by partial match on name/topic/purpose using conversations.list and local filtering.",
1970
+ inputSchema: {
1971
+ query: z.string().optional(),
1972
+ types: z.string().optional(),
1973
+ exclude_archived: z.boolean().optional(),
1974
+ limit: z.number().int().min(1).max(1000).optional(),
1975
+ cursor: z.string().optional(),
1976
+ token_override: z.string().optional(),
1977
+ },
1978
+ },
1979
+ async ({ query, types, exclude_archived, limit, cursor, token_override }) =>
1980
+ safeToolRun(async () => {
1981
+ const data = await callSlackApi(
1982
+ "conversations.list",
1983
+ {
1984
+ types: types || "public_channel,private_channel",
1985
+ exclude_archived: exclude_archived ?? true,
1986
+ limit: limit ?? 200,
1987
+ cursor,
1988
+ },
1989
+ token_override
1990
+ );
1991
+
1992
+ let channels = Array.isArray(data.channels) ? data.channels : [];
1993
+ if (query) {
1994
+ const normalizedQuery = query.toLowerCase();
1995
+ channels = channels.filter((channel) => {
1996
+ const candidates = [channel.id, channel.name, channel.purpose?.value, channel.topic?.value]
1997
+ .filter((v) => typeof v === "string")
1998
+ .map((v) => v.toLowerCase());
1999
+ return candidates.some((value) => value.includes(normalizedQuery));
2000
+ });
2001
+ }
2002
+
2003
+ return {
2004
+ channels,
2005
+ next_cursor: data.response_metadata?.next_cursor || null,
2006
+ count: channels.length,
2007
+ };
2008
+ })
2009
+ );
2010
+
2011
+ server.registerTool(
2012
+ "send_message",
2013
+ {
2014
+ description: "Send a message using chat.postMessage.",
2015
+ inputSchema: {
2016
+ channel: z.string().min(1),
2017
+ text: z.string().min(1),
2018
+ thread_ts: z.string().optional(),
2019
+ blocks: z.any().optional(),
2020
+ mrkdwn: z.boolean().optional(),
2021
+ unfurl_links: z.boolean().optional(),
2022
+ unfurl_media: z.boolean().optional(),
2023
+ token_override: z.string().optional(),
2024
+ },
2025
+ },
2026
+ async ({ channel, text, thread_ts, blocks, mrkdwn, unfurl_links, unfurl_media, token_override }) =>
2027
+ safeToolRun(async () => {
2028
+ const data = await callSlackApi(
2029
+ "chat.postMessage",
2030
+ {
2031
+ channel,
2032
+ text,
2033
+ thread_ts,
2034
+ blocks: parseJsonMaybe(blocks),
2035
+ mrkdwn,
2036
+ unfurl_links,
2037
+ unfurl_media,
2038
+ },
2039
+ token_override
2040
+ );
2041
+
2042
+ return {
2043
+ channel: data.channel,
2044
+ ts: data.ts,
2045
+ message: data.message,
2046
+ };
2047
+ })
2048
+ );
2049
+
2050
+ server.registerTool(
2051
+ "read_channel",
2052
+ {
2053
+ description: "Read channel history with conversations.history.",
2054
+ inputSchema: {
2055
+ channel: z.string().min(1),
2056
+ limit: z.number().int().min(1).max(1000).optional(),
2057
+ cursor: z.string().optional(),
2058
+ oldest: z.string().optional(),
2059
+ latest: z.string().optional(),
2060
+ inclusive: z.boolean().optional(),
2061
+ token_override: z.string().optional(),
2062
+ },
2063
+ },
2064
+ async ({ channel, limit, cursor, oldest, latest, inclusive, token_override }) =>
2065
+ safeToolRun(async () => {
2066
+ const data = await callSlackApi(
2067
+ "conversations.history",
2068
+ {
2069
+ channel,
2070
+ limit: limit ?? 50,
2071
+ cursor,
2072
+ oldest,
2073
+ latest,
2074
+ inclusive,
2075
+ },
2076
+ token_override
2077
+ );
2078
+
2079
+ return {
2080
+ channel,
2081
+ messages: data.messages || [],
2082
+ has_more: Boolean(data.has_more),
2083
+ next_cursor: data.response_metadata?.next_cursor || null,
2084
+ };
2085
+ })
2086
+ );
2087
+
2088
+ server.registerTool(
2089
+ "read_thread",
2090
+ {
2091
+ description: "Read a thread using conversations.replies.",
2092
+ inputSchema: {
2093
+ channel: z.string().min(1),
2094
+ thread_ts: z.string().min(1),
2095
+ limit: z.number().int().min(1).max(1000).optional(),
2096
+ cursor: z.string().optional(),
2097
+ oldest: z.string().optional(),
2098
+ latest: z.string().optional(),
2099
+ inclusive: z.boolean().optional(),
2100
+ token_override: z.string().optional(),
2101
+ },
2102
+ },
2103
+ async ({ channel, thread_ts, limit, cursor, oldest, latest, inclusive, token_override }) =>
2104
+ safeToolRun(async () => {
2105
+ const data = await callSlackApi(
2106
+ "conversations.replies",
2107
+ {
2108
+ channel,
2109
+ ts: thread_ts,
2110
+ limit: limit ?? 50,
2111
+ cursor,
2112
+ oldest,
2113
+ latest,
2114
+ inclusive,
2115
+ },
2116
+ token_override
2117
+ );
2118
+
2119
+ return {
2120
+ channel,
2121
+ thread_ts,
2122
+ messages: data.messages || [],
2123
+ has_more: Boolean(data.has_more),
2124
+ next_cursor: data.response_metadata?.next_cursor || null,
2125
+ };
2126
+ })
2127
+ );
2128
+
2129
+ server.registerTool(
2130
+ "create_canvas",
2131
+ {
2132
+ description: "Create a canvas using canvases.create. Pass Slack params in `params`.",
2133
+ inputSchema: {
2134
+ params: z.record(z.string(), z.any()),
2135
+ token_override: z.string().optional(),
2136
+ },
2137
+ },
2138
+ async ({ params, token_override }) =>
2139
+ safeToolRun(async () => {
2140
+ const data = await callSlackApi("canvases.create", params, token_override);
2141
+ return data;
2142
+ })
2143
+ );
2144
+
2145
+ server.registerTool(
2146
+ "update_canvas",
2147
+ {
2148
+ description: "Update a canvas using canvases.edit. Pass Slack params in `params`.",
2149
+ inputSchema: {
2150
+ params: z.record(z.string(), z.any()),
2151
+ token_override: z.string().optional(),
2152
+ },
2153
+ },
2154
+ async ({ params, token_override }) =>
2155
+ safeToolRun(async () => {
2156
+ const data = await callSlackApi("canvases.edit", params, token_override);
2157
+ return data;
2158
+ })
2159
+ );
2160
+
2161
+ server.registerTool(
2162
+ "read_canvas",
2163
+ {
2164
+ description: "Read canvas content using canvases.sections.lookup. Pass Slack params in `params`.",
2165
+ inputSchema: {
2166
+ params: z.record(z.string(), z.any()),
2167
+ token_override: z.string().optional(),
2168
+ },
2169
+ },
2170
+ async ({ params, token_override }) =>
2171
+ safeToolRun(async () => {
2172
+ const data = await callSlackApi("canvases.sections.lookup", params, token_override);
2173
+ return data;
2174
+ })
2175
+ );
2176
+
2177
+ server.registerTool(
2178
+ "read_user_profile",
2179
+ {
2180
+ description: "Read user info/profile using users.info plus users.profile.get (best effort).",
2181
+ inputSchema: {
2182
+ user: z.string().min(1),
2183
+ include_locale: z.boolean().optional(),
2184
+ include_labels: z.boolean().optional(),
2185
+ token_override: z.string().optional(),
2186
+ },
2187
+ },
2188
+ async ({ user, include_locale, include_labels, token_override }) =>
2189
+ safeToolRun(async () => {
2190
+ const info = await callSlackApi(
2191
+ "users.info",
2192
+ { user, include_locale: include_locale ?? true },
2193
+ token_override
2194
+ );
2195
+
2196
+ let profile = null;
2197
+ try {
2198
+ const profileData = await callSlackApi(
2199
+ "users.profile.get",
2200
+ { user, include_labels: include_labels ?? true },
2201
+ token_override
2202
+ );
2203
+ profile = profileData.profile || null;
2204
+ } catch {
2205
+ profile = info.user?.profile || null;
2206
+ }
2207
+
2208
+ return { user: info.user || null, profile };
2209
+ })
2210
+ );
2211
+ }
2212
+
2213
+ function registerCatalogMethodTools(server, catalog) {
2214
+ if (!ENABLE_METHOD_TOOLS) return { registered: 0 };
2215
+
2216
+ const methods = Array.isArray(catalog.methods) ? catalog.methods : [];
2217
+ const limited = MAX_METHOD_TOOLS > 0 ? methods.slice(0, MAX_METHOD_TOOLS) : methods;
2218
+
2219
+ const usedNames = new Set();
2220
+ let registered = 0;
2221
+
2222
+ for (const methodInfo of limited) {
2223
+ const method = methodInfo?.method;
2224
+ if (!method || typeof method !== "string") continue;
2225
+
2226
+ const toolName = toolNameFromMethod(method, usedNames);
2227
+ const descriptionParts = [
2228
+ `Slack Web API method wrapper for ${method}.`,
2229
+ methodInfo.description ? `Official: ${methodInfo.description}` : "",
2230
+ Array.isArray(methodInfo.scopes) && methodInfo.scopes.length
2231
+ ? `Scopes: ${methodInfo.scopes.join(", ")}`
2232
+ : "",
2233
+ ].filter(Boolean);
2234
+
2235
+ server.registerTool(
2236
+ toolName,
2237
+ {
2238
+ description: descriptionParts.join(" "),
2239
+ inputSchema: {
2240
+ params: z.record(z.string(), z.any()).optional(),
2241
+ token_override: z.string().optional(),
2242
+ },
2243
+ },
2244
+ async ({ params, token_override }) =>
2245
+ safeToolRun(async () => {
2246
+ const data = await callSlackApi(method, params || {}, token_override);
2247
+ return { method, data };
2248
+ })
2249
+ );
2250
+ registered += 1;
2251
+ }
2252
+
2253
+ server.registerTool(
2254
+ "slack_method_tools_info",
2255
+ {
2256
+ description: "Return summary for catalog-driven method tools currently loaded.",
2257
+ inputSchema: {},
675
2258
  },
676
2259
  async () =>
677
- safeToolRun(async () => ({
678
- catalog_path: CATALOG_PATH,
679
- method_tools_enabled: ENABLE_METHOD_TOOLS,
680
- max_method_tools: MAX_METHOD_TOOLS,
681
- methods_in_catalog: methods.length,
682
- method_tools_registered: registered,
683
- method_tool_prefix: METHOD_TOOL_PREFIX,
684
- }))
2260
+ safeToolRun(async () => {
2261
+ const tokenStore = loadTokenStore();
2262
+ const activeProfile = resolveTokenStoreProfileBySelector(tokenStore, process.env.SLACK_PROFILE);
2263
+ const clientConfig = loadClientConfig();
2264
+ const runtimeGateway = getRuntimeGatewayConfig();
2265
+ return {
2266
+ catalog_path: CATALOG_PATH,
2267
+ method_tools_enabled: ENABLE_METHOD_TOOLS,
2268
+ max_method_tools: MAX_METHOD_TOOLS,
2269
+ methods_in_catalog: methods.length,
2270
+ method_tools_registered: registered,
2271
+ method_tool_prefix: METHOD_TOOL_PREFIX,
2272
+ token_store_path: TOKEN_STORE_PATH,
2273
+ client_config_path: CLIENT_CONFIG_PATH,
2274
+ active_profile: activeProfile
2275
+ ? {
2276
+ key: activeProfile.key,
2277
+ profile_name: activeProfile.profile?.profile_name || "",
2278
+ team_id: activeProfile.profile?.team_id || "",
2279
+ }
2280
+ : null,
2281
+ client_profile: clientConfig.profile || "",
2282
+ env_tokens_present: {
2283
+ bot: Boolean(process.env.SLACK_BOT_TOKEN),
2284
+ user: Boolean(process.env.SLACK_USER_TOKEN),
2285
+ generic: Boolean(process.env.SLACK_TOKEN),
2286
+ },
2287
+ gateway_mode: Boolean(runtimeGateway.url),
2288
+ gateway_url: runtimeGateway.url || null,
2289
+ env_example_fallback_enabled: ALLOW_ENV_EXAMPLE_FALLBACK,
2290
+ };
2291
+ })
685
2292
  );
686
-
687
- return { registered };
688
- }
689
-
690
- async function main() {
2293
+
2294
+ return { registered };
2295
+ }
2296
+
2297
+ async function startMcpServer() {
691
2298
  const server = new McpServer(
692
2299
  { name: SERVER_NAME, version: SERVER_VERSION },
693
2300
  { capabilities: { logging: {} } }
694
2301
  );
695
-
696
- registerCoreTools(server);
697
- const catalog = loadCatalog();
698
- const methodStats = registerCatalogMethodTools(server, catalog);
699
-
700
- const transport = new StdioServerTransport();
701
- await server.connect(transport);
702
-
703
- const catalogCount =
704
- catalog && catalog.totals && typeof catalog.totals.methods === "number"
705
- ? catalog.totals.methods
706
- : Array.isArray(catalog.methods)
707
- ? catalog.methods.length
708
- : 0;
709
-
2302
+
2303
+ registerCoreTools(server);
2304
+ const catalog = loadCatalog();
2305
+ const methodStats = registerCatalogMethodTools(server, catalog);
2306
+
2307
+ const transport = new StdioServerTransport();
2308
+ await server.connect(transport);
2309
+
2310
+ const catalogCount =
2311
+ catalog && catalog.totals && typeof catalog.totals.methods === "number"
2312
+ ? catalog.totals.methods
2313
+ : Array.isArray(catalog.methods)
2314
+ ? catalog.methods.length
2315
+ : 0;
2316
+
710
2317
  console.error(
711
2318
  `[${SERVER_NAME}] connected via stdio | catalog_methods=${catalogCount} | method_tools_registered=${methodStats.registered}`
712
2319
  );
713
2320
  }
714
2321
 
715
- main().catch((error) => {
2322
+ async function runEntryPoint() {
2323
+ const [firstArg, ...rest] = process.argv.slice(2);
2324
+ const command = (firstArg || "").toLowerCase();
2325
+ if (command === "oauth") {
2326
+ await runOauthCli(rest);
2327
+ return;
2328
+ }
2329
+ if (command === "gateway") {
2330
+ await runGatewayCli(rest);
2331
+ return;
2332
+ }
2333
+ if (command === "onboard") {
2334
+ await runOnboardCli(rest);
2335
+ return;
2336
+ }
2337
+ if (!command) {
2338
+ const onboarded = await runAutoOnboardingIfPossible();
2339
+ if (onboarded) return;
2340
+ }
2341
+ await startMcpServer();
2342
+ }
2343
+
2344
+ runEntryPoint().catch((error) => {
716
2345
  console.error(`[${SERVER_NAME}] fatal error:`, error);
717
2346
  process.exit(1);
718
2347
  });