slack-max-api-mcp 1.0.7 → 1.0.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +20 -37
- package/README.md +62 -209
- package/package.json +3 -3
- package/src/slack-mcp-server.js +616 -972
package/src/slack-mcp-server.js
CHANGED
|
@@ -13,20 +13,28 @@ const { z } = require("zod");
|
|
|
13
13
|
const SERVER_NAME = "slack-max-api-mcp";
|
|
14
14
|
const SERVER_VERSION = "2.0.0";
|
|
15
15
|
|
|
16
|
-
const SLACK_API_BASE_URL = process.env.SLACK_API_BASE_URL || "https://slack.com/api";
|
|
17
|
-
|
|
16
|
+
const SLACK_API_BASE_URL = process.env.SLACK_API_BASE_URL || "https://slack.com/api";
|
|
17
|
+
|
|
18
18
|
const CATALOG_PATH =
|
|
19
19
|
process.env.SLACK_CATALOG_PATH || path.join(process.cwd(), "data", "slack-catalog.json");
|
|
20
20
|
const METHOD_TOOL_PREFIX = process.env.SLACK_METHOD_TOOL_PREFIX || "slack_method";
|
|
21
|
-
const
|
|
22
|
-
const
|
|
21
|
+
const TOOL_EXPOSURE_MODE = normalizeToolExposureMode(process.env.SLACK_TOOL_EXPOSURE_MODE);
|
|
22
|
+
const ENABLE_METHOD_TOOLS = parseBooleanEnv(
|
|
23
|
+
process.env.SLACK_ENABLE_METHOD_TOOLS,
|
|
24
|
+
TOOL_EXPOSURE_MODE === "legacy"
|
|
25
|
+
);
|
|
26
|
+
const MAX_METHOD_TOOLS = parseNumberEnv(
|
|
27
|
+
process.env.SLACK_MAX_METHOD_TOOLS,
|
|
28
|
+
TOOL_EXPOSURE_MODE === "legacy" ? 0 : 50
|
|
29
|
+
);
|
|
30
|
+
const SMART_COMPAT_CORE_TOOLS = parseBooleanEnv(
|
|
31
|
+
process.env.SLACK_SMART_COMPAT_CORE_TOOLS,
|
|
32
|
+
true
|
|
33
|
+
);
|
|
23
34
|
const ENV_EXAMPLE_PATH = path.join(process.cwd(), ".env.example");
|
|
24
35
|
const TOKEN_STORE_PATH =
|
|
25
36
|
process.env.SLACK_TOKEN_STORE_PATH ||
|
|
26
37
|
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
38
|
const ALLOW_ENV_EXAMPLE_FALLBACK = process.env.SLACK_ALLOW_ENV_EXAMPLE_FALLBACK === "true";
|
|
31
39
|
const OAUTH_CALLBACK_HOST = process.env.SLACK_OAUTH_CALLBACK_HOST || "127.0.0.1";
|
|
32
40
|
const OAUTH_CALLBACK_PORT = Number(process.env.SLACK_OAUTH_CALLBACK_PORT || 8787);
|
|
@@ -38,37 +46,37 @@ const DEFAULT_OAUTH_USER_SCOPES =
|
|
|
38
46
|
process.env.SLACK_OAUTH_USER_SCOPES ||
|
|
39
47
|
"search:read,channels:read,groups:read,channels:history,groups:history";
|
|
40
48
|
const RETRYABLE_TOKEN_ERRORS = new Set(["not_allowed_token_type", "missing_scope"]);
|
|
41
|
-
const
|
|
42
|
-
const
|
|
43
|
-
const
|
|
44
|
-
const
|
|
45
|
-
const
|
|
46
|
-
process.env.
|
|
47
|
-
const
|
|
48
|
-
const
|
|
49
|
-
const
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
"
|
|
70
|
-
|
|
71
|
-
|
|
49
|
+
const DEFAULT_ONBOARD_SERVER_URL = "https://43.202.54.65.sslip.io";
|
|
50
|
+
const ONBOARD_SERVER_URL = process.env.SLACK_ONBOARD_SERVER_URL || DEFAULT_ONBOARD_SERVER_URL;
|
|
51
|
+
const ONBOARD_SERVER_HOST = process.env.SLACK_ONBOARD_SERVER_HOST || "127.0.0.1";
|
|
52
|
+
const ONBOARD_SERVER_PORT = Number(process.env.SLACK_ONBOARD_SERVER_PORT || 8790);
|
|
53
|
+
const ONBOARD_PUBLIC_BASE_URL =
|
|
54
|
+
process.env.SLACK_ONBOARD_PUBLIC_BASE_URL || `http://${ONBOARD_SERVER_HOST}:${ONBOARD_SERVER_PORT}`;
|
|
55
|
+
const ONBOARD_CALLBACK_PATH = process.env.SLACK_ONBOARD_SERVER_CALLBACK_PATH || OAUTH_CALLBACK_PATH;
|
|
56
|
+
const ONBOARD_CLAIM_TTL_MS = Number(process.env.SLACK_ONBOARD_CLAIM_TTL_MS || 10 * 60 * 1000);
|
|
57
|
+
const ONBOARD_POLL_INTERVAL_MS = Number(process.env.SLACK_ONBOARD_POLL_INTERVAL_MS || 2000);
|
|
58
|
+
const ONBOARD_TIMEOUT_MS = Number(process.env.SLACK_ONBOARD_TIMEOUT_MS || 5 * 60 * 1000);
|
|
59
|
+
function parseBooleanEnv(rawValue, defaultValue) {
|
|
60
|
+
if (rawValue === undefined || rawValue === null || rawValue === "") return defaultValue;
|
|
61
|
+
const normalized = String(rawValue).trim().toLowerCase();
|
|
62
|
+
if (["1", "true", "yes", "on"].includes(normalized)) return true;
|
|
63
|
+
if (["0", "false", "no", "off"].includes(normalized)) return false;
|
|
64
|
+
return defaultValue;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function parseNumberEnv(rawValue, defaultValue) {
|
|
68
|
+
if (rawValue === undefined || rawValue === null || rawValue === "") return defaultValue;
|
|
69
|
+
const parsed = Number(rawValue);
|
|
70
|
+
if (!Number.isFinite(parsed)) return defaultValue;
|
|
71
|
+
return parsed;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function normalizeToolExposureMode(rawValue) {
|
|
75
|
+
const normalized = String(rawValue || "smart").trim().toLowerCase();
|
|
76
|
+
if (normalized === "legacy") return "legacy";
|
|
77
|
+
return "smart";
|
|
78
|
+
}
|
|
79
|
+
|
|
72
80
|
function parseSimpleEnvFile(filePath) {
|
|
73
81
|
if (!fs.existsSync(filePath)) return {};
|
|
74
82
|
|
|
@@ -97,31 +105,6 @@ function parseScopeList(raw) {
|
|
|
97
105
|
return [...new Set(String(raw).split(",").map((part) => part.trim()).filter(Boolean))];
|
|
98
106
|
}
|
|
99
107
|
|
|
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";
|
|
116
|
-
}
|
|
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);
|
|
123
|
-
}
|
|
124
|
-
|
|
125
108
|
function ensureParentDirectory(filePath) {
|
|
126
109
|
const dirPath = path.dirname(filePath);
|
|
127
110
|
fs.mkdirSync(dirPath, { recursive: true });
|
|
@@ -156,50 +139,6 @@ function saveTokenStore(store) {
|
|
|
156
139
|
fs.writeFileSync(TOKEN_STORE_PATH, JSON.stringify(normalizeTokenStore(store), null, 2), "utf8");
|
|
157
140
|
}
|
|
158
141
|
|
|
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();
|
|
176
|
-
try {
|
|
177
|
-
const parsed = JSON.parse(fs.readFileSync(CLIENT_CONFIG_PATH, "utf8"));
|
|
178
|
-
return normalizeClientConfig(parsed);
|
|
179
|
-
} catch {
|
|
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
142
|
function resolveTokenStoreProfileBySelector(store, selector) {
|
|
204
143
|
const profiles = store?.profiles || {};
|
|
205
144
|
const keys = Object.keys(profiles);
|
|
@@ -266,7 +205,7 @@ function getSlackTokenCandidates(tokenOverride, options = {}) {
|
|
|
266
205
|
const tokenStore = loadTokenStore();
|
|
267
206
|
const activeProfile = resolveTokenStoreProfileBySelector(
|
|
268
207
|
tokenStore,
|
|
269
|
-
options.profileSelector || process.env.SLACK_PROFILE
|
|
208
|
+
options.profileSelector || process.env.SLACK_PROFILE
|
|
270
209
|
);
|
|
271
210
|
if (activeProfile) {
|
|
272
211
|
appendCandidateTokens(
|
|
@@ -339,98 +278,6 @@ function toRecordObject(value) {
|
|
|
339
278
|
return value;
|
|
340
279
|
}
|
|
341
280
|
|
|
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
|
-
}
|
|
350
|
-
|
|
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`, {
|
|
358
|
-
method: "POST",
|
|
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
|
-
}),
|
|
367
|
-
});
|
|
368
|
-
|
|
369
|
-
const text = await response.text();
|
|
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
281
|
async function callSlackApiWithToken(method, params = {}, token, tokenSource) {
|
|
435
282
|
const url = `${SLACK_API_BASE_URL.replace(/\/+$/, "")}/${method}`;
|
|
436
283
|
|
|
@@ -501,21 +348,16 @@ async function callSlackApiWithCandidates(method, params, candidates) {
|
|
|
501
348
|
}
|
|
502
349
|
|
|
503
350
|
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
351
|
const candidates = getSlackTokenCandidates(tokenOverride, options);
|
|
510
352
|
if (candidates.length === 0) {
|
|
511
353
|
throw new Error(
|
|
512
|
-
"Slack token is missing. Set SLACK_BOT_TOKEN/SLACK_USER_TOKEN/SLACK_TOKEN or run
|
|
354
|
+
"Slack token is missing. Set SLACK_BOT_TOKEN/SLACK_USER_TOKEN/SLACK_TOKEN or run slack-max-api-mcp oauth login."
|
|
513
355
|
);
|
|
514
356
|
}
|
|
515
357
|
|
|
516
358
|
return callSlackApiWithCandidates(method, params, candidates);
|
|
517
359
|
}
|
|
518
|
-
|
|
360
|
+
|
|
519
361
|
function successResult(payload) {
|
|
520
362
|
return {
|
|
521
363
|
content: [{ type: "text", text: JSON.stringify(payload, null, 2) }],
|
|
@@ -574,118 +416,6 @@ function parseCliArgs(argv) {
|
|
|
574
416
|
return { options, positionals };
|
|
575
417
|
}
|
|
576
418
|
|
|
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;
|
|
618
|
-
try {
|
|
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.");
|
|
625
|
-
}
|
|
626
|
-
if (!payload.exp || Number(payload.exp) < Date.now()) {
|
|
627
|
-
throw new Error("Invite token expired.");
|
|
628
|
-
}
|
|
629
|
-
return payload;
|
|
630
|
-
}
|
|
631
|
-
|
|
632
|
-
function requireGatewayInviteSecret() {
|
|
633
|
-
if (!GATEWAY_SHARED_SECRET) {
|
|
634
|
-
throw new Error("Set SLACK_GATEWAY_SHARED_SECRET before using gateway invite/onboarding.");
|
|
635
|
-
}
|
|
636
|
-
return GATEWAY_SHARED_SECRET;
|
|
637
|
-
}
|
|
638
|
-
|
|
639
|
-
function isInteractiveTerminal() {
|
|
640
|
-
return Boolean(process.stdin.isTTY && process.stdout.isTTY);
|
|
641
|
-
}
|
|
642
|
-
|
|
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
|
-
}
|
|
652
|
-
|
|
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
|
-
}
|
|
667
|
-
|
|
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
|
-
}
|
|
674
|
-
|
|
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
|
-
}
|
|
685
|
-
|
|
686
|
-
return false;
|
|
687
|
-
}
|
|
688
|
-
|
|
689
419
|
function printOauthHelp() {
|
|
690
420
|
const lines = [
|
|
691
421
|
"Slack Max OAuth helper",
|
|
@@ -1013,117 +743,8 @@ async function runOauthCli(args) {
|
|
|
1013
743
|
throw new Error(`Unknown oauth command: ${subcommand}`);
|
|
1014
744
|
}
|
|
1015
745
|
|
|
1016
|
-
function
|
|
1017
|
-
|
|
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}`);
|
|
746
|
+
function sleep(ms) {
|
|
747
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
1127
748
|
}
|
|
1128
749
|
|
|
1129
750
|
function sendText(res, statusCode, text) {
|
|
@@ -1136,355 +757,242 @@ function sendJson(res, statusCode, payload) {
|
|
|
1136
757
|
res.end(JSON.stringify(payload, null, 2));
|
|
1137
758
|
}
|
|
1138
759
|
|
|
1139
|
-
|
|
1140
|
-
return
|
|
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
|
-
}
|
|
760
|
+
function isClaimSessionExpired(session) {
|
|
761
|
+
return !session || Date.now() > Number(session.expires_at || 0);
|
|
1165
762
|
}
|
|
1166
763
|
|
|
1167
|
-
function
|
|
1168
|
-
const
|
|
1169
|
-
|
|
1170
|
-
|
|
764
|
+
function cleanupExpiredClaimSessions(claimSessions, stateToClaim) {
|
|
765
|
+
for (const [claimToken, session] of claimSessions.entries()) {
|
|
766
|
+
if (!isClaimSessionExpired(session)) continue;
|
|
767
|
+
claimSessions.delete(claimToken);
|
|
768
|
+
if (session.state) stateToClaim.delete(session.state);
|
|
1171
769
|
}
|
|
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
770
|
}
|
|
1183
771
|
|
|
1184
|
-
function
|
|
1185
|
-
const
|
|
1186
|
-
const
|
|
1187
|
-
|
|
1188
|
-
|
|
772
|
+
async function fetchJsonResponse(url, options, label) {
|
|
773
|
+
const response = await fetch(url, options);
|
|
774
|
+
const rawText = await response.text();
|
|
775
|
+
let data;
|
|
776
|
+
try {
|
|
777
|
+
data = JSON.parse(rawText);
|
|
778
|
+
} catch {
|
|
779
|
+
throw new Error(`${label} returned non-JSON (HTTP ${response.status}).`);
|
|
1189
780
|
}
|
|
1190
|
-
|
|
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
|
-
});
|
|
781
|
+
if (!response.ok || !data?.ok) {
|
|
782
|
+
throw new Error(`${label} failed: ${data?.error || `http_${response.status}`}`);
|
|
1207
783
|
}
|
|
1208
|
-
return
|
|
1209
|
-
}
|
|
1210
|
-
|
|
1211
|
-
function buildGatewayRedirectUri() {
|
|
1212
|
-
const url = new URL(OAUTH_CALLBACK_PATH, `${GATEWAY_PUBLIC_BASE_URL.replace(/\/+$/, "")}/`);
|
|
1213
|
-
return url.toString();
|
|
784
|
+
return data;
|
|
1214
785
|
}
|
|
1215
786
|
|
|
1216
|
-
function
|
|
1217
|
-
const
|
|
1218
|
-
|
|
787
|
+
function printOnboardHelp() {
|
|
788
|
+
const lines = [
|
|
789
|
+
"Slack Max onboard helper (client-side)",
|
|
790
|
+
"",
|
|
791
|
+
"Usage:",
|
|
792
|
+
" slack-max-api-mcp onboard run",
|
|
793
|
+
" slack-max-api-mcp onboard run --server https://onboard.example.com",
|
|
794
|
+
" [--profile NAME] [--team T123] [--scope a,b] [--user-scope c,d]",
|
|
795
|
+
" slack-max-api-mcp onboard help",
|
|
796
|
+
"",
|
|
797
|
+
"Notes:",
|
|
798
|
+
` - Default onboard server: ${ONBOARD_SERVER_URL}`,
|
|
799
|
+
" - This command does not require SLACK_CLIENT_SECRET on team PCs.",
|
|
800
|
+
" - It opens browser OAuth via central onboarding server and saves tokens locally.",
|
|
801
|
+
];
|
|
802
|
+
console.log(lines.join("\n"));
|
|
1219
803
|
}
|
|
1220
804
|
|
|
1221
|
-
function
|
|
1222
|
-
const
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
805
|
+
function printOnboardServerHelp() {
|
|
806
|
+
const lines = [
|
|
807
|
+
"Slack Max onboard server (central)",
|
|
808
|
+
"",
|
|
809
|
+
"Usage:",
|
|
810
|
+
" slack-max-api-mcp onboard-server start",
|
|
811
|
+
" [--host 0.0.0.0] [--port 8790] [--public-base-url https://onboard.example.com]",
|
|
812
|
+
" [--callback-path /slack/oauth/callback]",
|
|
813
|
+
" slack-max-api-mcp onboard-server help",
|
|
814
|
+
"",
|
|
815
|
+
"Required env vars (server-side only):",
|
|
816
|
+
" SLACK_CLIENT_ID",
|
|
817
|
+
" SLACK_CLIENT_SECRET",
|
|
818
|
+
"",
|
|
819
|
+
"Optional env vars:",
|
|
820
|
+
" SLACK_ONBOARD_SERVER_HOST, SLACK_ONBOARD_SERVER_PORT",
|
|
821
|
+
" SLACK_ONBOARD_PUBLIC_BASE_URL, SLACK_ONBOARD_SERVER_CALLBACK_PATH",
|
|
822
|
+
" SLACK_ONBOARD_CLAIM_TTL_MS",
|
|
823
|
+
];
|
|
824
|
+
console.log(lines.join("\n"));
|
|
1228
825
|
}
|
|
1229
826
|
|
|
1230
|
-
function
|
|
1231
|
-
const
|
|
1232
|
-
const
|
|
1233
|
-
|
|
1234
|
-
|
|
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 || "";
|
|
827
|
+
async function runOnboardClient(args) {
|
|
828
|
+
const { options } = parseCliArgs(args);
|
|
829
|
+
const serverBase = String(options.server || ONBOARD_SERVER_URL).trim().replace(/\/+$/, "");
|
|
830
|
+
if (!serverBase) {
|
|
831
|
+
throw new Error("Missing onboard server URL. Use --server or set SLACK_ONBOARD_SERVER_URL.");
|
|
1249
832
|
}
|
|
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
833
|
|
|
1262
|
-
|
|
1263
|
-
const
|
|
1264
|
-
const
|
|
1265
|
-
const
|
|
1266
|
-
|
|
1267
|
-
const
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
if (
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
834
|
+
const requestedProfile = String(options.profile || "").trim();
|
|
835
|
+
const requestedTeam = String(options.team || "").trim();
|
|
836
|
+
const requestedScope = parseScopeList(options.scope || "").join(",");
|
|
837
|
+
const requestedUserScope = parseScopeList(options["user-scope"] || "").join(",");
|
|
838
|
+
|
|
839
|
+
const bootstrapParams = new URLSearchParams();
|
|
840
|
+
if (requestedProfile) bootstrapParams.set("profile", requestedProfile);
|
|
841
|
+
if (requestedTeam) bootstrapParams.set("team", requestedTeam);
|
|
842
|
+
if (requestedScope) bootstrapParams.set("scope", requestedScope);
|
|
843
|
+
if (requestedUserScope) bootstrapParams.set("user_scope", requestedUserScope);
|
|
844
|
+
|
|
845
|
+
const bootstrapUrl = `${serverBase}/onboard/bootstrap${bootstrapParams.toString() ? `?${bootstrapParams.toString()}` : ""}`;
|
|
846
|
+
const bootstrap = await fetchJsonResponse(
|
|
847
|
+
bootstrapUrl,
|
|
848
|
+
{ method: "GET", headers: { Accept: "application/json" } },
|
|
849
|
+
"Onboard bootstrap"
|
|
850
|
+
);
|
|
1276
851
|
|
|
1277
|
-
const
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
if (ONBOARD_SKIP_TLS_VERIFY) {
|
|
1282
|
-
lines.push("$env:NODE_TLS_REJECT_UNAUTHORIZED='0'");
|
|
852
|
+
const startUrl = String(bootstrap.onboard_start_url || "");
|
|
853
|
+
const claimToken = String(bootstrap.claim_token || "");
|
|
854
|
+
if (!startUrl || !claimToken) {
|
|
855
|
+
throw new Error("Onboard bootstrap response is missing onboard_start_url or claim_token.");
|
|
1283
856
|
}
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
857
|
+
|
|
858
|
+
const opened = openExternalUrl(startUrl);
|
|
859
|
+
if (!opened) {
|
|
860
|
+
console.log(`[onboard] Open this URL in browser:\n${startUrl}`);
|
|
861
|
+
} else {
|
|
862
|
+
console.log("[onboard] Browser opened for OAuth approval.");
|
|
1287
863
|
}
|
|
1288
|
-
return lines.join("\r\n");
|
|
1289
|
-
}
|
|
1290
864
|
|
|
1291
|
-
|
|
1292
|
-
const
|
|
1293
|
-
const
|
|
1294
|
-
const
|
|
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
|
-
","
|
|
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
|
-
}
|
|
865
|
+
const pollIntervalMs = Math.max(500, Number(options["poll-ms"] || ONBOARD_POLL_INTERVAL_MS));
|
|
866
|
+
const timeoutMs = Math.max(30_000, Number(options["timeout-ms"] || ONBOARD_TIMEOUT_MS));
|
|
867
|
+
const deadline = Date.now() + timeoutMs;
|
|
868
|
+
const claimUrl = `${serverBase}/onboard/claim?claim=${encodeURIComponent(claimToken)}`;
|
|
1315
869
|
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
870
|
+
while (Date.now() < deadline) {
|
|
871
|
+
await sleep(pollIntervalMs);
|
|
872
|
+
const claimData = await fetchJsonResponse(
|
|
873
|
+
claimUrl,
|
|
874
|
+
{ method: "GET", headers: { Accept: "application/json" } },
|
|
875
|
+
"Onboard claim"
|
|
876
|
+
);
|
|
1321
877
|
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
if (v === undefined || v === null) continue;
|
|
1326
|
-
endpoint.searchParams.set(k, String(v));
|
|
1327
|
-
}
|
|
878
|
+
if (claimData.status === "pending") {
|
|
879
|
+
continue;
|
|
880
|
+
}
|
|
1328
881
|
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
};
|
|
882
|
+
if (claimData.status !== "ready") {
|
|
883
|
+
throw new Error(`Unexpected onboard claim status: ${claimData.status || "unknown"}`);
|
|
884
|
+
}
|
|
1333
885
|
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
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
|
-
}
|
|
886
|
+
const oauthResponse = claimData.oauth_response;
|
|
887
|
+
if (!oauthResponse || typeof oauthResponse !== "object") {
|
|
888
|
+
throw new Error("Onboard claim result is missing oauth_response.");
|
|
889
|
+
}
|
|
1344
890
|
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
// keep text
|
|
891
|
+
const { key, profile } = upsertOauthProfile(oauthResponse, claimData.profile || requestedProfile || "");
|
|
892
|
+
console.log(`[onboard] saved profile: ${profile.profile_name} (${key})`);
|
|
893
|
+
console.log(`[onboard] token store path: ${TOKEN_STORE_PATH}`);
|
|
894
|
+
console.log("[onboard] Next step for MCP clients:");
|
|
895
|
+
console.log(` setx SLACK_PROFILE \"${profile.profile_name}\"`);
|
|
896
|
+
return;
|
|
1352
897
|
}
|
|
1353
898
|
|
|
1354
|
-
|
|
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
|
-
};
|
|
899
|
+
throw new Error("Timed out waiting for central onboarding completion.");
|
|
1362
900
|
}
|
|
1363
901
|
|
|
1364
|
-
async function
|
|
1365
|
-
const
|
|
1366
|
-
const
|
|
1367
|
-
const
|
|
1368
|
-
|
|
902
|
+
async function runOnboardServerStart(args) {
|
|
903
|
+
const { options } = parseCliArgs(args);
|
|
904
|
+
const clientId = options["client-id"] || process.env.SLACK_CLIENT_ID;
|
|
905
|
+
const clientSecret = options["client-secret"] || process.env.SLACK_CLIENT_SECRET;
|
|
906
|
+
if (!clientId || !clientSecret) {
|
|
907
|
+
throw new Error("Missing SLACK_CLIENT_ID or SLACK_CLIENT_SECRET on onboarding server.");
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
const host = String(options.host || ONBOARD_SERVER_HOST);
|
|
911
|
+
const port = Number(options.port || ONBOARD_SERVER_PORT);
|
|
912
|
+
const callbackPath = String(options["callback-path"] || ONBOARD_CALLBACK_PATH);
|
|
913
|
+
const publicBaseUrl = String(options["public-base-url"] || ONBOARD_PUBLIC_BASE_URL).replace(/\/+$/, "");
|
|
914
|
+
const claimTtlMs = Math.max(60_000, Number(options["claim-ttl-ms"] || ONBOARD_CLAIM_TTL_MS));
|
|
915
|
+
const redirectUri = new URL(callbackPath, `${publicBaseUrl}/`).toString();
|
|
916
|
+
|
|
917
|
+
const claimSessions = new Map();
|
|
918
|
+
const stateToClaim = new Map();
|
|
1369
919
|
|
|
1370
920
|
const server = http.createServer(async (req, res) => {
|
|
1371
921
|
try {
|
|
922
|
+
cleanupExpiredClaimSessions(claimSessions, stateToClaim);
|
|
1372
923
|
const method = req.method || "GET";
|
|
1373
|
-
const requestUrl = new URL(req.url || "/", `http://${
|
|
924
|
+
const requestUrl = new URL(req.url || "/", `http://${host}:${port}`);
|
|
1374
925
|
|
|
1375
926
|
if (method === "GET" && requestUrl.pathname === "/health") {
|
|
1376
927
|
sendJson(res, 200, {
|
|
1377
928
|
ok: true,
|
|
1378
929
|
service: SERVER_NAME,
|
|
1379
|
-
mode: "
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
callback_url: redirectUri,
|
|
930
|
+
mode: "onboard_server",
|
|
931
|
+
public_base_url: publicBaseUrl,
|
|
932
|
+
callback_path: callbackPath,
|
|
1383
933
|
});
|
|
1384
934
|
return;
|
|
1385
935
|
}
|
|
1386
936
|
|
|
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,
|
|
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
937
|
if (method === "GET" && requestUrl.pathname === "/onboard/bootstrap") {
|
|
1425
|
-
|
|
1426
|
-
|
|
938
|
+
const profile = String(requestUrl.searchParams.get("profile") || "").trim();
|
|
939
|
+
const team = String(requestUrl.searchParams.get("team") || "").trim();
|
|
940
|
+
const botScopes = parseScopeList(requestUrl.searchParams.get("scope") || DEFAULT_OAUTH_BOT_SCOPES);
|
|
941
|
+
const userScopes = parseScopeList(
|
|
942
|
+
requestUrl.searchParams.get("user_scope") || DEFAULT_OAUTH_USER_SCOPES
|
|
943
|
+
);
|
|
944
|
+
|
|
945
|
+
if (botScopes.length === 0 && userScopes.length === 0) {
|
|
946
|
+
sendJson(res, 400, { ok: false, error: "missing_scope" });
|
|
1427
947
|
return;
|
|
1428
948
|
}
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
949
|
+
|
|
950
|
+
const claimToken = crypto.randomBytes(32).toString("hex");
|
|
951
|
+
const expiresAt = Date.now() + claimTtlMs;
|
|
952
|
+
claimSessions.set(claimToken, {
|
|
953
|
+
claim_token: claimToken,
|
|
954
|
+
profile,
|
|
955
|
+
team,
|
|
956
|
+
bot_scopes: botScopes,
|
|
957
|
+
user_scopes: userScopes,
|
|
958
|
+
oauth_response: null,
|
|
959
|
+
state: "",
|
|
960
|
+
expires_at: expiresAt,
|
|
1434
961
|
});
|
|
1435
|
-
sendJson(res, 200, payload);
|
|
1436
|
-
return;
|
|
1437
|
-
}
|
|
1438
962
|
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
const
|
|
1442
|
-
|
|
1443
|
-
const oauthStartUrl = buildOauthStartUrlFromInvitePayload(gatewayBaseUrl, payload);
|
|
963
|
+
const startParams = new URLSearchParams();
|
|
964
|
+
startParams.set("claim", claimToken);
|
|
965
|
+
const startUrl = `${publicBaseUrl}/onboard/start?${startParams.toString()}`;
|
|
966
|
+
|
|
1444
967
|
sendJson(res, 200, {
|
|
1445
968
|
ok: true,
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
oauth_start_url: oauthStartUrl,
|
|
1451
|
-
expires_at: new Date(Number(payload.exp)).toISOString(),
|
|
969
|
+
onboard_start_url: startUrl,
|
|
970
|
+
claim_token: claimToken,
|
|
971
|
+
profile,
|
|
972
|
+
expires_at: new Date(expiresAt).toISOString(),
|
|
1452
973
|
});
|
|
1453
974
|
return;
|
|
1454
975
|
}
|
|
1455
976
|
|
|
1456
|
-
if (method === "GET" && requestUrl.pathname === "/
|
|
1457
|
-
const
|
|
1458
|
-
const
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
const userScopes = parseScopesFromQuery(
|
|
1462
|
-
requestUrl.searchParams,
|
|
1463
|
-
"user_scope",
|
|
1464
|
-
DEFAULT_OAUTH_USER_SCOPES
|
|
1465
|
-
);
|
|
1466
|
-
|
|
1467
|
-
if (botScopes.length === 0 && userScopes.length === 0) {
|
|
1468
|
-
sendJson(res, 400, { ok: false, error: "missing_scope" });
|
|
977
|
+
if (method === "GET" && requestUrl.pathname === "/onboard/start") {
|
|
978
|
+
const claimToken = String(requestUrl.searchParams.get("claim") || "");
|
|
979
|
+
const session = claimSessions.get(claimToken);
|
|
980
|
+
if (!session || isClaimSessionExpired(session)) {
|
|
981
|
+
sendText(res, 400, "Invalid or expired onboarding claim.");
|
|
1469
982
|
return;
|
|
1470
983
|
}
|
|
1471
984
|
|
|
1472
985
|
const state = crypto.randomBytes(24).toString("hex");
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
profile_name: profileName,
|
|
1476
|
-
team_id: teamId,
|
|
1477
|
-
bot_scopes: botScopes,
|
|
1478
|
-
user_scopes: userScopes,
|
|
1479
|
-
});
|
|
986
|
+
session.state = state;
|
|
987
|
+
stateToClaim.set(state, claimToken);
|
|
1480
988
|
|
|
1481
989
|
const authorizeUrl = buildOauthAuthorizeUrl({
|
|
1482
990
|
clientId,
|
|
1483
991
|
state,
|
|
1484
992
|
redirectUri,
|
|
1485
|
-
botScopes,
|
|
1486
|
-
userScopes,
|
|
1487
|
-
teamId,
|
|
993
|
+
botScopes: session.bot_scopes,
|
|
994
|
+
userScopes: session.user_scopes,
|
|
995
|
+
teamId: session.team,
|
|
1488
996
|
});
|
|
1489
997
|
|
|
1490
998
|
res.writeHead(302, { Location: authorizeUrl });
|
|
@@ -1493,7 +1001,6 @@ async function startGatewayServer() {
|
|
|
1493
1001
|
}
|
|
1494
1002
|
|
|
1495
1003
|
if (method === "GET" && requestUrl.pathname === callbackPath) {
|
|
1496
|
-
const { clientId, clientSecret } = requireGatewayClientCredentials();
|
|
1497
1004
|
const receivedError = requestUrl.searchParams.get("error");
|
|
1498
1005
|
if (receivedError) {
|
|
1499
1006
|
sendText(res, 400, `Slack OAuth failed: ${receivedError}`);
|
|
@@ -1507,106 +1014,45 @@ async function startGatewayServer() {
|
|
|
1507
1014
|
return;
|
|
1508
1015
|
}
|
|
1509
1016
|
|
|
1510
|
-
const
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
sendText(res, 400, "Invalid or expired OAuth state.");
|
|
1017
|
+
const claimToken = stateToClaim.get(state);
|
|
1018
|
+
if (!claimToken) {
|
|
1019
|
+
sendText(res, 400, "Invalid OAuth state.");
|
|
1514
1020
|
return;
|
|
1515
1021
|
}
|
|
1516
|
-
|
|
1517
|
-
|
|
1022
|
+
|
|
1023
|
+
const session = claimSessions.get(claimToken);
|
|
1024
|
+
if (!session || isClaimSessionExpired(session)) {
|
|
1025
|
+
sendText(res, 400, "Expired onboarding claim.");
|
|
1518
1026
|
return;
|
|
1519
1027
|
}
|
|
1520
1028
|
|
|
1521
1029
|
const oauthResponse = await exchangeOauthCode({ clientId, clientSecret, code, redirectUri });
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
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")
|
|
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
|
-
}
|
|
1552
|
-
|
|
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
|
-
});
|
|
1030
|
+
session.oauth_response = oauthResponse;
|
|
1031
|
+
stateToClaim.delete(state);
|
|
1032
|
+
sendText(res, 200, "Slack OAuth completed. Return to your CLI and wait for onboarding to finish.");
|
|
1564
1033
|
return;
|
|
1565
1034
|
}
|
|
1566
1035
|
|
|
1567
|
-
if (method === "
|
|
1568
|
-
|
|
1569
|
-
|
|
1036
|
+
if (method === "GET" && requestUrl.pathname === "/onboard/claim") {
|
|
1037
|
+
const claimToken = String(requestUrl.searchParams.get("claim") || "");
|
|
1038
|
+
const session = claimSessions.get(claimToken);
|
|
1039
|
+
if (!session || isClaimSessionExpired(session)) {
|
|
1040
|
+
sendJson(res, 400, { ok: false, error: "invalid_or_expired_claim" });
|
|
1570
1041
|
return;
|
|
1571
1042
|
}
|
|
1572
1043
|
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
if (!methodName || typeof methodName !== "string") {
|
|
1576
|
-
sendJson(res, 400, { ok: false, error: "missing_method" });
|
|
1044
|
+
if (!session.oauth_response) {
|
|
1045
|
+
sendJson(res, 200, { ok: true, status: "pending" });
|
|
1577
1046
|
return;
|
|
1578
1047
|
}
|
|
1579
1048
|
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1049
|
+
sendJson(res, 200, {
|
|
1050
|
+
ok: true,
|
|
1051
|
+
status: "ready",
|
|
1052
|
+
profile: session.profile || "",
|
|
1053
|
+
oauth_response: session.oauth_response,
|
|
1585
1054
|
});
|
|
1586
|
-
|
|
1587
|
-
sendJson(res, 400, { ok: false, error: "missing_token" });
|
|
1588
|
-
return;
|
|
1589
|
-
}
|
|
1590
|
-
|
|
1591
|
-
const data = await callSlackApiWithCandidates(methodName, payload.params || {}, candidates);
|
|
1592
|
-
sendJson(res, 200, { ok: true, data });
|
|
1593
|
-
return;
|
|
1594
|
-
}
|
|
1595
|
-
|
|
1596
|
-
if (method === "POST" && requestUrl.pathname === "/api/slack/http") {
|
|
1597
|
-
if (!isGatewayAuthorized(req)) {
|
|
1598
|
-
sendJson(res, 401, { ok: false, error: "unauthorized" });
|
|
1599
|
-
return;
|
|
1600
|
-
}
|
|
1601
|
-
|
|
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
|
-
}
|
|
1607
|
-
|
|
1608
|
-
const data = await proxySlackHttpRequest(payload);
|
|
1609
|
-
sendJson(res, 200, { ok: true, data });
|
|
1055
|
+
claimSessions.delete(claimToken);
|
|
1610
1056
|
return;
|
|
1611
1057
|
}
|
|
1612
1058
|
|
|
@@ -1615,102 +1061,50 @@ async function startGatewayServer() {
|
|
|
1615
1061
|
sendJson(res, 500, {
|
|
1616
1062
|
ok: false,
|
|
1617
1063
|
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
1064
|
});
|
|
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
1065
|
}
|
|
1630
1066
|
});
|
|
1631
1067
|
|
|
1632
1068
|
await new Promise((resolve, reject) => {
|
|
1633
1069
|
server.once("error", reject);
|
|
1634
|
-
server.listen(
|
|
1070
|
+
server.listen(port, host, resolve);
|
|
1635
1071
|
});
|
|
1636
1072
|
|
|
1637
|
-
console.error(
|
|
1638
|
-
|
|
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
|
-
}
|
|
1073
|
+
console.error(`[${SERVER_NAME}] onboard server listening at http://${host}:${port}`);
|
|
1074
|
+
console.error(`[${SERVER_NAME}] public base: ${publicBaseUrl}`);
|
|
1075
|
+
console.error(`[${SERVER_NAME}] bootstrap URL: ${publicBaseUrl}/onboard/bootstrap`);
|
|
1645
1076
|
}
|
|
1646
1077
|
|
|
1647
|
-
function
|
|
1648
|
-
const
|
|
1649
|
-
|
|
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
|
-
}
|
|
1078
|
+
async function runOnboardCli(args) {
|
|
1079
|
+
const subcommand = (args[0] || "help").toLowerCase();
|
|
1080
|
+
const rest = args.slice(1);
|
|
1674
1081
|
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
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);
|
|
1082
|
+
if (subcommand === "help" || subcommand === "--help" || subcommand === "-h") {
|
|
1083
|
+
printOnboardHelp();
|
|
1084
|
+
return;
|
|
1085
|
+
}
|
|
1086
|
+
if (subcommand === "run") {
|
|
1087
|
+
await runOnboardClient(rest);
|
|
1088
|
+
return;
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
throw new Error(`Unknown onboard command: ${subcommand}`);
|
|
1697
1092
|
}
|
|
1698
1093
|
|
|
1699
|
-
async function
|
|
1094
|
+
async function runOnboardServerCli(args) {
|
|
1700
1095
|
const subcommand = (args[0] || "help").toLowerCase();
|
|
1096
|
+
const rest = args.slice(1);
|
|
1097
|
+
|
|
1701
1098
|
if (subcommand === "help" || subcommand === "--help" || subcommand === "-h") {
|
|
1702
|
-
|
|
1099
|
+
printOnboardServerHelp();
|
|
1703
1100
|
return;
|
|
1704
1101
|
}
|
|
1705
1102
|
if (subcommand === "start") {
|
|
1706
|
-
await
|
|
1707
|
-
return;
|
|
1708
|
-
}
|
|
1709
|
-
if (subcommand === "invite") {
|
|
1710
|
-
runGatewayInvite(args.slice(1));
|
|
1103
|
+
await runOnboardServerStart(rest);
|
|
1711
1104
|
return;
|
|
1712
1105
|
}
|
|
1713
|
-
|
|
1106
|
+
|
|
1107
|
+
throw new Error(`Unknown onboard-server command: ${subcommand}`);
|
|
1714
1108
|
}
|
|
1715
1109
|
|
|
1716
1110
|
function loadCatalog() {
|
|
@@ -1727,21 +1121,301 @@ function loadCatalog() {
|
|
|
1727
1121
|
}
|
|
1728
1122
|
}
|
|
1729
1123
|
|
|
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);
|
|
1124
|
+
function toolNameFromMethod(method, usedNames) {
|
|
1125
|
+
const base = `${METHOD_TOOL_PREFIX}_${method.replace(/[^a-zA-Z0-9]/g, "_")}`;
|
|
1126
|
+
if (!usedNames.has(base)) {
|
|
1127
|
+
usedNames.add(base);
|
|
1734
1128
|
return base;
|
|
1735
1129
|
}
|
|
1736
1130
|
|
|
1737
1131
|
let idx = 2;
|
|
1738
1132
|
while (usedNames.has(`${base}_${idx}`)) idx += 1;
|
|
1739
1133
|
const name = `${base}_${idx}`;
|
|
1740
|
-
usedNames.add(name);
|
|
1741
|
-
return name;
|
|
1742
|
-
}
|
|
1743
|
-
|
|
1744
|
-
function
|
|
1134
|
+
usedNames.add(name);
|
|
1135
|
+
return name;
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1138
|
+
function normalizeSearchTokens(value) {
|
|
1139
|
+
return String(value || "")
|
|
1140
|
+
.toLowerCase()
|
|
1141
|
+
.split(/[^a-z0-9_.-]+/)
|
|
1142
|
+
.map((part) => part.trim())
|
|
1143
|
+
.filter(Boolean);
|
|
1144
|
+
}
|
|
1145
|
+
|
|
1146
|
+
function compactCatalogMethodInfo(methodInfo, options = {}) {
|
|
1147
|
+
const includeScopes = options.includeScopes !== false;
|
|
1148
|
+
const includeUrl = options.includeUrl === true;
|
|
1149
|
+
const out = {
|
|
1150
|
+
method: methodInfo?.method || "",
|
|
1151
|
+
family: methodInfo?.family || "",
|
|
1152
|
+
description: methodInfo?.description || "",
|
|
1153
|
+
};
|
|
1154
|
+
if (includeScopes) {
|
|
1155
|
+
out.scopes = Array.isArray(methodInfo?.scopes) ? methodInfo.scopes : [];
|
|
1156
|
+
}
|
|
1157
|
+
if (includeUrl) {
|
|
1158
|
+
out.url = methodInfo?.url || "";
|
|
1159
|
+
}
|
|
1160
|
+
return out;
|
|
1161
|
+
}
|
|
1162
|
+
|
|
1163
|
+
function scoreCatalogMethod(methodInfo, tokens) {
|
|
1164
|
+
if (!Array.isArray(tokens) || tokens.length === 0) return 0;
|
|
1165
|
+
const method = String(methodInfo?.method || "").toLowerCase();
|
|
1166
|
+
const description = String(methodInfo?.description || "").toLowerCase();
|
|
1167
|
+
const family = String(methodInfo?.family || "").toLowerCase();
|
|
1168
|
+
const scopes = Array.isArray(methodInfo?.scopes) ? methodInfo.scopes.join(" ").toLowerCase() : "";
|
|
1169
|
+
|
|
1170
|
+
let score = 0;
|
|
1171
|
+
for (const token of tokens) {
|
|
1172
|
+
if (!token) continue;
|
|
1173
|
+
if (method.includes(token)) score += 8;
|
|
1174
|
+
if (description.includes(token)) score += 4;
|
|
1175
|
+
if (family.includes(token)) score += 3;
|
|
1176
|
+
if (scopes.includes(token)) score += 2;
|
|
1177
|
+
}
|
|
1178
|
+
return score;
|
|
1179
|
+
}
|
|
1180
|
+
|
|
1181
|
+
function findCatalogMethods(catalog, query, maxItems = 10) {
|
|
1182
|
+
const methods = Array.isArray(catalog?.methods) ? catalog.methods : [];
|
|
1183
|
+
const size = Math.max(1, Math.min(50, Number(maxItems) || 10));
|
|
1184
|
+
const tokens = normalizeSearchTokens(query);
|
|
1185
|
+
|
|
1186
|
+
if (tokens.length === 0) {
|
|
1187
|
+
return methods.slice(0, size);
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1190
|
+
return methods
|
|
1191
|
+
.map((methodInfo) => ({
|
|
1192
|
+
methodInfo,
|
|
1193
|
+
score: scoreCatalogMethod(methodInfo, tokens),
|
|
1194
|
+
}))
|
|
1195
|
+
.filter((item) => item.score > 0)
|
|
1196
|
+
.sort((a, b) => {
|
|
1197
|
+
if (b.score !== a.score) return b.score - a.score;
|
|
1198
|
+
return String(a.methodInfo?.method || "").localeCompare(String(b.methodInfo?.method || ""));
|
|
1199
|
+
})
|
|
1200
|
+
.slice(0, size)
|
|
1201
|
+
.map((item) => item.methodInfo);
|
|
1202
|
+
}
|
|
1203
|
+
|
|
1204
|
+
function findCatalogMethodByExactName(catalog, methodName) {
|
|
1205
|
+
const target = String(methodName || "").trim();
|
|
1206
|
+
if (!target) return null;
|
|
1207
|
+
const methods = Array.isArray(catalog?.methods) ? catalog.methods : [];
|
|
1208
|
+
return methods.find((methodInfo) => methodInfo?.method === target) || null;
|
|
1209
|
+
}
|
|
1210
|
+
|
|
1211
|
+
async function executeSlackHttpRequest({
|
|
1212
|
+
url,
|
|
1213
|
+
http_method,
|
|
1214
|
+
query,
|
|
1215
|
+
json_body,
|
|
1216
|
+
form_body,
|
|
1217
|
+
headers,
|
|
1218
|
+
token_override,
|
|
1219
|
+
}) {
|
|
1220
|
+
const tokenCandidate = requireSlackTokenCandidate(token_override);
|
|
1221
|
+
const method = http_method || "GET";
|
|
1222
|
+
|
|
1223
|
+
const endpoint = new URL(url);
|
|
1224
|
+
for (const [k, v] of Object.entries(toRecordObject(query))) {
|
|
1225
|
+
if (v === undefined || v === null) continue;
|
|
1226
|
+
endpoint.searchParams.set(k, String(v));
|
|
1227
|
+
}
|
|
1228
|
+
|
|
1229
|
+
const reqHeaders = {
|
|
1230
|
+
Authorization:
|
|
1231
|
+
"Bearer " + tokenCandidate.token,
|
|
1232
|
+
...(headers || {}),
|
|
1233
|
+
};
|
|
1234
|
+
|
|
1235
|
+
let body;
|
|
1236
|
+
if (form_body && Object.keys(form_body).length > 0) {
|
|
1237
|
+
reqHeaders["Content-Type"] = "application/x-www-form-urlencoded; charset=utf-8";
|
|
1238
|
+
body = toUrlEncodedBody(form_body);
|
|
1239
|
+
} else if (json_body && Object.keys(json_body).length > 0) {
|
|
1240
|
+
reqHeaders["Content-Type"] = "application/json; charset=utf-8";
|
|
1241
|
+
body = JSON.stringify(json_body);
|
|
1242
|
+
}
|
|
1243
|
+
|
|
1244
|
+
const res = await fetch(endpoint.toString(), {
|
|
1245
|
+
method,
|
|
1246
|
+
headers: reqHeaders,
|
|
1247
|
+
body,
|
|
1248
|
+
});
|
|
1249
|
+
|
|
1250
|
+
const text = await res.text();
|
|
1251
|
+
let parsedBody = text;
|
|
1252
|
+
try {
|
|
1253
|
+
parsedBody = JSON.parse(text);
|
|
1254
|
+
} catch {
|
|
1255
|
+
// Keep plain text when response is not JSON.
|
|
1256
|
+
}
|
|
1257
|
+
|
|
1258
|
+
return {
|
|
1259
|
+
url: endpoint.toString(),
|
|
1260
|
+
status: res.status,
|
|
1261
|
+
ok: res.ok,
|
|
1262
|
+
headers: Object.fromEntries(res.headers.entries()),
|
|
1263
|
+
body: parsedBody,
|
|
1264
|
+
};
|
|
1265
|
+
}
|
|
1266
|
+
|
|
1267
|
+
function registerSmartGatewayTools(server, catalog) {
|
|
1268
|
+
server.registerTool(
|
|
1269
|
+
"gateway_plan",
|
|
1270
|
+
{
|
|
1271
|
+
description:
|
|
1272
|
+
"Plan a Slack task with minimal context. Returns ranked candidate methods and the next actions.",
|
|
1273
|
+
inputSchema: {
|
|
1274
|
+
goal: z.string().min(3),
|
|
1275
|
+
max_methods: z.number().int().min(1).max(20).optional(),
|
|
1276
|
+
},
|
|
1277
|
+
},
|
|
1278
|
+
async ({ goal, max_methods }) =>
|
|
1279
|
+
safeToolRun(async () => {
|
|
1280
|
+
const candidates = findCatalogMethods(catalog, goal, max_methods ?? 8).map((methodInfo) =>
|
|
1281
|
+
compactCatalogMethodInfo(methodInfo)
|
|
1282
|
+
);
|
|
1283
|
+
return {
|
|
1284
|
+
mode: TOOL_EXPOSURE_MODE,
|
|
1285
|
+
goal,
|
|
1286
|
+
candidate_methods: candidates,
|
|
1287
|
+
next_steps: [
|
|
1288
|
+
"1) Use gateway_load to inspect candidate methods in detail.",
|
|
1289
|
+
"2) Execute the chosen method with gateway_run(action=method).",
|
|
1290
|
+
"3) Summarize output and repeat only when additional actions are needed.",
|
|
1291
|
+
],
|
|
1292
|
+
note:
|
|
1293
|
+
"This planner keeps tool exposure small by routing through a minimal tool surface.",
|
|
1294
|
+
};
|
|
1295
|
+
})
|
|
1296
|
+
);
|
|
1297
|
+
|
|
1298
|
+
server.registerTool(
|
|
1299
|
+
"gateway_load",
|
|
1300
|
+
{
|
|
1301
|
+
description:
|
|
1302
|
+
"Load only the method references you need. Supports exact method lookup or query search.",
|
|
1303
|
+
inputSchema: {
|
|
1304
|
+
method: z.string().optional(),
|
|
1305
|
+
query: z.string().optional(),
|
|
1306
|
+
max_items: z.number().int().min(1).max(30).optional(),
|
|
1307
|
+
include_scopes: z.boolean().optional(),
|
|
1308
|
+
include_url: z.boolean().optional(),
|
|
1309
|
+
},
|
|
1310
|
+
},
|
|
1311
|
+
async ({ method, query, max_items, include_scopes, include_url }) =>
|
|
1312
|
+
safeToolRun(async () => {
|
|
1313
|
+
if (!method && !query) {
|
|
1314
|
+
throw new Error("Either `method` or `query` is required.");
|
|
1315
|
+
}
|
|
1316
|
+
|
|
1317
|
+
const includeScopes = include_scopes !== false;
|
|
1318
|
+
const includeUrl = include_url === true;
|
|
1319
|
+
let methods = [];
|
|
1320
|
+
|
|
1321
|
+
if (method) {
|
|
1322
|
+
const exact = findCatalogMethodByExactName(catalog, method);
|
|
1323
|
+
methods = exact ? [exact] : [];
|
|
1324
|
+
} else {
|
|
1325
|
+
methods = findCatalogMethods(catalog, query, max_items ?? 10);
|
|
1326
|
+
}
|
|
1327
|
+
|
|
1328
|
+
return {
|
|
1329
|
+
mode: TOOL_EXPOSURE_MODE,
|
|
1330
|
+
count: methods.length,
|
|
1331
|
+
methods: methods.map((methodInfo) =>
|
|
1332
|
+
compactCatalogMethodInfo(methodInfo, { includeScopes, includeUrl })
|
|
1333
|
+
),
|
|
1334
|
+
};
|
|
1335
|
+
})
|
|
1336
|
+
);
|
|
1337
|
+
|
|
1338
|
+
server.registerTool(
|
|
1339
|
+
"gateway_run",
|
|
1340
|
+
{
|
|
1341
|
+
description:
|
|
1342
|
+
"Run a Slack action through a single gateway tool. Supports Slack Web API method call and generic HTTP call.",
|
|
1343
|
+
inputSchema: {
|
|
1344
|
+
action: z.enum(["method", "http"]),
|
|
1345
|
+
method: z.string().optional(),
|
|
1346
|
+
params: z.record(z.string(), z.any()).optional(),
|
|
1347
|
+
preferred_token_type: z.enum(["bot", "user", "generic", "auto"]).optional(),
|
|
1348
|
+
url: z.string().url().optional(),
|
|
1349
|
+
http_method: z.enum(["GET", "POST", "PUT", "PATCH", "DELETE"]).optional(),
|
|
1350
|
+
query: z.record(z.string(), z.any()).optional(),
|
|
1351
|
+
json_body: z.record(z.string(), z.any()).optional(),
|
|
1352
|
+
form_body: z.record(z.string(), z.any()).optional(),
|
|
1353
|
+
headers: z.record(z.string(), z.string()).optional(),
|
|
1354
|
+
token_override: z.string().optional(),
|
|
1355
|
+
},
|
|
1356
|
+
},
|
|
1357
|
+
async ({
|
|
1358
|
+
action,
|
|
1359
|
+
method,
|
|
1360
|
+
params,
|
|
1361
|
+
preferred_token_type,
|
|
1362
|
+
url,
|
|
1363
|
+
http_method,
|
|
1364
|
+
query,
|
|
1365
|
+
json_body,
|
|
1366
|
+
form_body,
|
|
1367
|
+
headers,
|
|
1368
|
+
token_override,
|
|
1369
|
+
}) =>
|
|
1370
|
+
safeToolRun(async () => {
|
|
1371
|
+
if (action === "method") {
|
|
1372
|
+
if (!method) throw new Error("`method` is required when action=method.");
|
|
1373
|
+
const data = await callSlackApi(
|
|
1374
|
+
method,
|
|
1375
|
+
params || {},
|
|
1376
|
+
token_override,
|
|
1377
|
+
preferred_token_type ? { preferredTokenType: preferred_token_type } : {}
|
|
1378
|
+
);
|
|
1379
|
+
return { action, method, data };
|
|
1380
|
+
}
|
|
1381
|
+
|
|
1382
|
+
if (!url) throw new Error("`url` is required when action=http.");
|
|
1383
|
+
const data = await executeSlackHttpRequest({
|
|
1384
|
+
url,
|
|
1385
|
+
http_method,
|
|
1386
|
+
query,
|
|
1387
|
+
json_body,
|
|
1388
|
+
form_body,
|
|
1389
|
+
headers,
|
|
1390
|
+
token_override,
|
|
1391
|
+
});
|
|
1392
|
+
return { action, data };
|
|
1393
|
+
})
|
|
1394
|
+
);
|
|
1395
|
+
|
|
1396
|
+
server.registerTool(
|
|
1397
|
+
"gateway_info",
|
|
1398
|
+
{
|
|
1399
|
+
description: "Return gateway exposure mode and lightweight tool registration summary.",
|
|
1400
|
+
inputSchema: {},
|
|
1401
|
+
},
|
|
1402
|
+
async () =>
|
|
1403
|
+
safeToolRun(async () => {
|
|
1404
|
+
return {
|
|
1405
|
+
mode: TOOL_EXPOSURE_MODE,
|
|
1406
|
+
execution_mode: "local",
|
|
1407
|
+
method_tools_enabled: ENABLE_METHOD_TOOLS,
|
|
1408
|
+
max_method_tools: MAX_METHOD_TOOLS,
|
|
1409
|
+
methods_in_catalog: Array.isArray(catalog?.methods) ? catalog.methods.length : 0,
|
|
1410
|
+
exposed_tools_hint: "smart mode keeps the tool surface compact via gateway_plan/load/run/info.",
|
|
1411
|
+
};
|
|
1412
|
+
})
|
|
1413
|
+
);
|
|
1414
|
+
|
|
1415
|
+
return { registered: 4 };
|
|
1416
|
+
}
|
|
1417
|
+
|
|
1418
|
+
function registerCoreTools(server) {
|
|
1745
1419
|
server.registerTool(
|
|
1746
1420
|
"slack_api_call",
|
|
1747
1421
|
{
|
|
@@ -1779,65 +1453,17 @@ function registerCoreTools(server) {
|
|
|
1779
1453
|
},
|
|
1780
1454
|
async ({ url, http_method, query, json_body, form_body, headers, token_override }) =>
|
|
1781
1455
|
safeToolRun(async () => {
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
}
|
|
1794
|
-
|
|
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
|
-
);
|
|
1456
|
+
return executeSlackHttpRequest({
|
|
1457
|
+
url,
|
|
1458
|
+
http_method,
|
|
1459
|
+
query,
|
|
1460
|
+
json_body,
|
|
1461
|
+
form_body,
|
|
1462
|
+
headers,
|
|
1463
|
+
token_override,
|
|
1464
|
+
});
|
|
1465
|
+
})
|
|
1466
|
+
);
|
|
1841
1467
|
|
|
1842
1468
|
server.registerTool(
|
|
1843
1469
|
"search_messages_files",
|
|
@@ -2205,10 +1831,12 @@ function registerCoreTools(server) {
|
|
|
2205
1831
|
profile = info.user?.profile || null;
|
|
2206
1832
|
}
|
|
2207
1833
|
|
|
2208
|
-
return { user: info.user || null, profile };
|
|
2209
|
-
})
|
|
2210
|
-
);
|
|
2211
|
-
|
|
1834
|
+
return { user: info.user || null, profile };
|
|
1835
|
+
})
|
|
1836
|
+
);
|
|
1837
|
+
|
|
1838
|
+
return { registered: 12 };
|
|
1839
|
+
}
|
|
2212
1840
|
|
|
2213
1841
|
function registerCatalogMethodTools(server, catalog) {
|
|
2214
1842
|
if (!ENABLE_METHOD_TOOLS) return { registered: 0 };
|
|
@@ -2260,17 +1888,15 @@ function registerCatalogMethodTools(server, catalog) {
|
|
|
2260
1888
|
safeToolRun(async () => {
|
|
2261
1889
|
const tokenStore = loadTokenStore();
|
|
2262
1890
|
const activeProfile = resolveTokenStoreProfileBySelector(tokenStore, process.env.SLACK_PROFILE);
|
|
2263
|
-
const clientConfig = loadClientConfig();
|
|
2264
|
-
const runtimeGateway = getRuntimeGatewayConfig();
|
|
2265
1891
|
return {
|
|
2266
1892
|
catalog_path: CATALOG_PATH,
|
|
1893
|
+
execution_mode: "local",
|
|
2267
1894
|
method_tools_enabled: ENABLE_METHOD_TOOLS,
|
|
2268
1895
|
max_method_tools: MAX_METHOD_TOOLS,
|
|
2269
1896
|
methods_in_catalog: methods.length,
|
|
2270
1897
|
method_tools_registered: registered,
|
|
2271
1898
|
method_tool_prefix: METHOD_TOOL_PREFIX,
|
|
2272
1899
|
token_store_path: TOKEN_STORE_PATH,
|
|
2273
|
-
client_config_path: CLIENT_CONFIG_PATH,
|
|
2274
1900
|
active_profile: activeProfile
|
|
2275
1901
|
? {
|
|
2276
1902
|
key: activeProfile.key,
|
|
@@ -2278,14 +1904,11 @@ function registerCatalogMethodTools(server, catalog) {
|
|
|
2278
1904
|
team_id: activeProfile.profile?.team_id || "",
|
|
2279
1905
|
}
|
|
2280
1906
|
: null,
|
|
2281
|
-
client_profile: clientConfig.profile || "",
|
|
2282
1907
|
env_tokens_present: {
|
|
2283
1908
|
bot: Boolean(process.env.SLACK_BOT_TOKEN),
|
|
2284
1909
|
user: Boolean(process.env.SLACK_USER_TOKEN),
|
|
2285
1910
|
generic: Boolean(process.env.SLACK_TOKEN),
|
|
2286
1911
|
},
|
|
2287
|
-
gateway_mode: Boolean(runtimeGateway.url),
|
|
2288
|
-
gateway_url: runtimeGateway.url || null,
|
|
2289
1912
|
env_example_fallback_enabled: ALLOW_ENV_EXAMPLE_FALLBACK,
|
|
2290
1913
|
};
|
|
2291
1914
|
})
|
|
@@ -2299,13 +1922,25 @@ async function startMcpServer() {
|
|
|
2299
1922
|
{ name: SERVER_NAME, version: SERVER_VERSION },
|
|
2300
1923
|
{ capabilities: { logging: {} } }
|
|
2301
1924
|
);
|
|
2302
|
-
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
|
|
2307
|
-
|
|
2308
|
-
|
|
1925
|
+
|
|
1926
|
+
const catalog = loadCatalog();
|
|
1927
|
+
let coreStats = { registered: 0 };
|
|
1928
|
+
let smartStats = { registered: 0 };
|
|
1929
|
+
let compatCoreStats = { registered: 0 };
|
|
1930
|
+
|
|
1931
|
+
if (TOOL_EXPOSURE_MODE === "legacy") {
|
|
1932
|
+
coreStats = registerCoreTools(server);
|
|
1933
|
+
} else {
|
|
1934
|
+
smartStats = registerSmartGatewayTools(server, catalog);
|
|
1935
|
+
if (SMART_COMPAT_CORE_TOOLS) {
|
|
1936
|
+
compatCoreStats = registerCoreTools(server);
|
|
1937
|
+
}
|
|
1938
|
+
coreStats = { registered: smartStats.registered + compatCoreStats.registered };
|
|
1939
|
+
}
|
|
1940
|
+
const methodStats = registerCatalogMethodTools(server, catalog);
|
|
1941
|
+
|
|
1942
|
+
const transport = new StdioServerTransport();
|
|
1943
|
+
await server.connect(transport);
|
|
2309
1944
|
|
|
2310
1945
|
const catalogCount =
|
|
2311
1946
|
catalog && catalog.totals && typeof catalog.totals.methods === "number"
|
|
@@ -2315,7 +1950,7 @@ async function startMcpServer() {
|
|
|
2315
1950
|
: 0;
|
|
2316
1951
|
|
|
2317
1952
|
console.error(
|
|
2318
|
-
`[${SERVER_NAME}] connected via stdio | catalog_methods=${catalogCount} | method_tools_registered=${methodStats.registered}`
|
|
1953
|
+
`[${SERVER_NAME}] connected via stdio | mode=${TOOL_EXPOSURE_MODE} | core_tools_registered=${coreStats?.registered ?? 0} | smart_tools_registered=${smartStats.registered} | compat_core_tools_registered=${compatCoreStats.registered} | catalog_methods=${catalogCount} | method_tools_registered=${methodStats.registered}`
|
|
2319
1954
|
);
|
|
2320
1955
|
}
|
|
2321
1956
|
|
|
@@ -2326,17 +1961,26 @@ async function runEntryPoint() {
|
|
|
2326
1961
|
await runOauthCli(rest);
|
|
2327
1962
|
return;
|
|
2328
1963
|
}
|
|
2329
|
-
if (command === "gateway") {
|
|
2330
|
-
await runGatewayCli(rest);
|
|
2331
|
-
return;
|
|
2332
|
-
}
|
|
2333
1964
|
if (command === "onboard") {
|
|
2334
1965
|
await runOnboardCli(rest);
|
|
2335
1966
|
return;
|
|
2336
1967
|
}
|
|
2337
|
-
if (
|
|
2338
|
-
|
|
2339
|
-
|
|
1968
|
+
if (command === "onboard-server") {
|
|
1969
|
+
await runOnboardServerCli(rest);
|
|
1970
|
+
return;
|
|
1971
|
+
}
|
|
1972
|
+
if (command === "help" || command === "--help" || command === "-h") {
|
|
1973
|
+
console.log("Usage:");
|
|
1974
|
+
console.log(" slack-max-api-mcp");
|
|
1975
|
+
console.log(" slack-max-api-mcp oauth <login|list|use|current|help>");
|
|
1976
|
+
console.log(" slack-max-api-mcp onboard <run|help>");
|
|
1977
|
+
console.log(" slack-max-api-mcp onboard-server <start|help>");
|
|
1978
|
+
return;
|
|
1979
|
+
}
|
|
1980
|
+
if (command) {
|
|
1981
|
+
throw new Error(
|
|
1982
|
+
`Unknown command: ${command}. Use 'slack-max-api-mcp help' for available commands.`
|
|
1983
|
+
);
|
|
2340
1984
|
}
|
|
2341
1985
|
await startMcpServer();
|
|
2342
1986
|
}
|