slack-max-api-mcp 1.0.6 → 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.
package/.env.example CHANGED
@@ -25,6 +25,10 @@ SLACK_CLIENT_SECRET=
25
25
  # SLACK_GATEWAY_SHARED_SECRET=change-this-to-long-random-secret
26
26
  # SLACK_GATEWAY_CLIENT_API_KEY=change-this-to-random-client-key
27
27
  # SLACK_GATEWAY_ALLOW_PUBLIC=false
28
+ # SLACK_GATEWAY_PUBLIC_ONBOARD=true
29
+ # SLACK_GATEWAY_PUBLIC_ONBOARD_API_KEY=change-this-to-random-client-key
30
+ # SLACK_GATEWAY_PUBLIC_ONBOARD_EXPOSE_API_KEY=false
31
+ # SLACK_GATEWAY_PUBLIC_ONBOARD_PROFILE_PREFIX=auto
28
32
  # SLACK_INVITE_TOKEN_DEFAULT_DAYS=7
29
33
  #
30
34
  # Client-side (each local Codex/Claude machine):
@@ -38,6 +42,11 @@ SLACK_CLIENT_SECRET=
38
42
  # or
39
43
  # SLACK_AUTO_ONBOARD_GATEWAY=https://mcp-gateway.example.com
40
44
  # SLACK_AUTO_ONBOARD_TOKEN=invite-token-issued-by-gateway
45
+ # Optional tokenless variant:
46
+ # SLACK_AUTO_ONBOARD_GATEWAY=https://mcp-gateway.example.com
47
+ # SLACK_AUTO_ONBOARD_PROFILE_PREFIX=auto
48
+ # SLACK_AUTO_ONBOARD_PROFILE=
49
+ # SLACK_ONBOARD_SKIP_TLS_VERIFY=false
41
50
 
42
51
  # Legacy/manual token mode (optional)
43
52
  # SLACK_BOT_TOKEN=xoxb-your-bot-token
package/README.md CHANGED
@@ -110,32 +110,47 @@ setx SLACK_GATEWAY_PORT "8790"
110
110
  setx SLACK_GATEWAY_PUBLIC_BASE_URL "https://your-gateway.example.com"
111
111
  setx SLACK_GATEWAY_SHARED_SECRET "long-random-shared-secret"
112
112
  setx SLACK_GATEWAY_CLIENT_API_KEY "long-random-client-api-key"
113
+ setx SLACK_GATEWAY_PUBLIC_ONBOARD "true"
114
+ setx SLACK_GATEWAY_PUBLIC_ONBOARD_API_KEY "long-random-client-api-key"
113
115
  npx -y slack-max-api-mcp gateway start
114
116
  ```
115
117
 
116
- 운영자가 팀원용 원클릭 초대 커맨드 생성:
118
+ 주의:
119
+ 1. `SLACK_GATEWAY_PUBLIC_ONBOARD=true`는 토큰 없는 온보딩을 허용합니다.
120
+ 2. 게이트웨이가 비공개(`SLACK_GATEWAY_ALLOW_PUBLIC=false`)라면 `SLACK_GATEWAY_PUBLIC_ONBOARD_API_KEY`를 함께 설정해야 팀원 로컬 클라이언트가 API 호출할 수 있습니다.
121
+
122
+ ### 팀원 경험 (토큰 전달 없이 권장)
117
123
 
118
124
  ```powershell
119
- npx -y slack-max-api-mcp gateway invite --profile woobin --team T0AHNJ8QN0N
125
+ $env:NODE_TLS_REJECT_UNAUTHORIZED='0'
126
+ npx -y slack-max-api-mcp onboard run --gateway "https://your-gateway.example.com"
127
+ Remove-Item Env:NODE_TLS_REJECT_UNAUTHORIZED
120
128
  ```
121
129
 
122
- 명령이 팀원에게 전달할 "원클릭 설치 커맨드"를 출력합니다.
130
+ 자동 동작:
131
+ 1. 로컬 클라이언트 설정 파일(`~/.slack-max-api-mcp/client.json`) 작성
132
+ 2. 브라우저 OAuth 승인 페이지 자동 오픈
133
+ 3. Slack Allow 승인
134
+ 4. 완료 후 Codex에서 바로 사용
123
135
 
124
- ### 팀원 경험 (입력값 없이 원클릭)
136
+ 승인 Codex 연결(최초 1회):
125
137
 
126
138
  ```powershell
127
- powershell -ExecutionPolicy Bypass -Command "irm 'https://your-gateway.example.com/onboard.ps1?token=...' | iex"
139
+ codex mcp add slack-max -- npx -y slack-max-api-mcp
128
140
  ```
129
141
 
130
- 스크립트가 자동으로 수행:
131
- 1. `slack-max-api-mcp` 설치
132
- 2. 로컬 클라이언트 설정 파일(`~/.slack-max-api-mcp/client.json`) 작성
133
- 3. 브라우저 OAuth 승인 페이지 자동 오픈
142
+ ### 팀원 경험 (초대토큰 기반, 기존 방식)
134
143
 
135
- 승인 Codex 연결(최초 1회):
144
+ 운영자가 팀원용 원클릭 초대 커맨드 생성:
136
145
 
137
146
  ```powershell
138
- codex mcp add slack-max -- npx -y slack-max-api-mcp
147
+ npx -y slack-max-api-mcp gateway invite --profile woobin --team T0AHNJ8QN0N
148
+ ```
149
+
150
+ 위 명령이 팀원에게 전달할 "원클릭 설치 커맨드"를 출력합니다.
151
+
152
+ ```powershell
153
+ powershell -ExecutionPolicy Bypass -Command "irm 'https://your-gateway.example.com/onboard.ps1?token=...' | iex"
139
154
  ```
140
155
 
141
156
  ### 팀원 경험 (설치 후 `slack-max-api-mcp`만 실행)
@@ -156,6 +171,7 @@ slack-max-api-mcp
156
171
  필요한 사전 배포값(팀원이 직접 입력하지 않아도 됨):
157
172
  1. `SLACK_AUTO_ONBOARD_URL` 또는
158
173
  2. `SLACK_AUTO_ONBOARD_GATEWAY` + `SLACK_AUTO_ONBOARD_TOKEN`
174
+ 3. 토큰 없는 자동 온보딩은 `SLACK_AUTO_ONBOARD_GATEWAY` 단독도 가능 (게이트웨이 `SLACK_GATEWAY_PUBLIC_ONBOARD=true` 필요)
159
175
 
160
176
  ## 2) 단독/개인 운영: 로컬 OAuth 모드
161
177
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "slack-max-api-mcp",
3
- "version": "1.0.6",
3
+ "version": "1.0.7",
4
4
  "description": "Slack MCP server (stdio) for Codex and Claude Code",
5
5
  "main": "src/slack-mcp-server.js",
6
6
  "bin": {
@@ -48,17 +48,26 @@ const GATEWAY_ALLOW_PUBLIC = process.env.SLACK_GATEWAY_ALLOW_PUBLIC === "true";
48
48
  const GATEWAY_SHARED_SECRET = process.env.SLACK_GATEWAY_SHARED_SECRET || GATEWAY_API_KEY;
49
49
  const GATEWAY_CLIENT_API_KEY =
50
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";
51
57
  const GATEWAY_STATE_TTL_MS = Number(process.env.SLACK_GATEWAY_STATE_TTL_MS || 15 * 60 * 1000);
52
58
  const INVITE_TOKEN_DEFAULT_DAYS = Number(process.env.SLACK_INVITE_TOKEN_DEFAULT_DAYS || 7);
53
59
  const AUTO_ONBOARD_ENABLED = process.env.SLACK_AUTO_ONBOARD !== "false";
54
60
  const AUTO_ONBOARD_GATEWAY =
55
61
  process.env.SLACK_AUTO_ONBOARD_GATEWAY || process.env.SLACK_ONBOARD_GATEWAY_URL || "";
62
+ const AUTO_ONBOARD_PROFILE = process.env.SLACK_AUTO_ONBOARD_PROFILE || "";
56
63
  const AUTO_ONBOARD_TOKEN = process.env.SLACK_AUTO_ONBOARD_TOKEN || process.env.SLACK_ONBOARD_TOKEN || "";
57
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";
58
66
  const ONBOARD_PACKAGE_SPEC =
59
67
  process.env.SLACK_ONBOARD_PACKAGE_SPEC ||
60
68
  process.env.SLACK_ONBOARD_INSTALL_SPEC ||
61
69
  "slack-max-api-mcp@latest";
70
+ const ONBOARD_SKIP_TLS_VERIFY = process.env.SLACK_ONBOARD_SKIP_TLS_VERIFY === "true";
62
71
 
63
72
  function parseSimpleEnvFile(filePath) {
64
73
  if (!fs.existsSync(filePath)) return {};
@@ -88,6 +97,31 @@ function parseScopeList(raw) {
88
97
  return [...new Set(String(raw).split(",").map((part) => part.trim()).filter(Boolean))];
89
98
  }
90
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";
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
+
91
125
  function ensureParentDirectory(filePath) {
92
126
  const dirPath = path.dirname(filePath);
93
127
  fs.mkdirSync(dirPath, { recursive: true });
@@ -632,12 +666,20 @@ async function runAutoOnboardingIfPossible() {
632
666
  }
633
667
 
634
668
  if (AUTO_ONBOARD_GATEWAY && AUTO_ONBOARD_TOKEN) {
635
- await runOnboardStart([
636
- "--gateway",
637
- AUTO_ONBOARD_GATEWAY,
638
- "--token",
639
- AUTO_ONBOARD_TOKEN,
640
- ]);
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);
641
683
  return true;
642
684
  }
643
685
 
@@ -976,9 +1018,12 @@ function printOnboardHelp() {
976
1018
  "Slack Max onboarding helper",
977
1019
  "",
978
1020
  "Usage:",
979
- " slack-max-api-mcp onboard run --gateway https://gateway.example.com --token <invite_token>",
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",
980
1024
  " slack-max-api-mcp onboard help",
981
1025
  "",
1026
+ "If --token is omitted, it uses gateway public onboarding endpoint (/onboard/bootstrap).",
982
1027
  "This command writes local client config and opens the Slack OAuth approval page automatically.",
983
1028
  ];
984
1029
  console.log(lines.join("\n"));
@@ -988,11 +1033,31 @@ async function runOnboardStart(args) {
988
1033
  const { options } = parseCliArgs(args);
989
1034
  const gateway = String(options.gateway || options.url || "").replace(/\/+$/, "");
990
1035
  const token = String(options.token || "");
991
- if (!gateway || !token) {
992
- throw new Error("Usage: slack-max-api-mcp onboard run --gateway <url> --token <invite_token>");
1036
+ if (!gateway) {
1037
+ throw new Error(
1038
+ "Usage: slack-max-api-mcp onboard run --gateway <url> [--token <invite_token>] [--profile <name>]"
1039
+ );
993
1040
  }
994
1041
 
995
- const response = await fetch(`${gateway}/onboard/resolve?token=${encodeURIComponent(token)}`, {
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, {
996
1061
  method: "GET",
997
1062
  headers: { Accept: "application/json" },
998
1063
  });
@@ -1006,14 +1071,23 @@ async function runOnboardStart(args) {
1006
1071
  }
1007
1072
 
1008
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
+ }
1009
1077
  throw new Error(`Onboarding failed: ${data?.error || `http_${response.status}`}`);
1010
1078
  }
1011
1079
 
1012
1080
  const resolvedGatewayUrl = String(data.gateway_url || gateway).replace(/\/+$/, "");
1013
1081
  const resolvedApiKey = String(data.gateway_api_key || "");
1014
- const profile = String(data.profile || "");
1082
+ const profile = String(data.profile || requestedProfile || "");
1015
1083
  const oauthStartUrl = String(data.oauth_start_url || "");
1016
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
+
1017
1091
  saveClientConfig({
1018
1092
  version: 1,
1019
1093
  gateway_url: resolvedGatewayUrl,
@@ -1032,6 +1106,9 @@ async function runOnboardStart(args) {
1032
1106
  console.log(`[onboard] client config saved: ${CLIENT_CONFIG_PATH}`);
1033
1107
  console.log(`[onboard] gateway: ${resolvedGatewayUrl}`);
1034
1108
  if (profile) console.log(`[onboard] profile: ${profile}`);
1109
+ if (data.mode === "public_onboard") {
1110
+ console.log("[onboard] mode: public_onboard (tokenless)");
1111
+ }
1035
1112
  console.log("[onboard] Next: approve in browser, then use Codex MCP as usual.");
1036
1113
  }
1037
1114
 
@@ -1042,7 +1119,7 @@ async function runOnboardCli(args) {
1042
1119
  printOnboardHelp();
1043
1120
  return;
1044
1121
  }
1045
- if (subcommand === "run" || subcommand === "start") {
1122
+ if (subcommand === "run" || subcommand === "start" || subcommand === "quick") {
1046
1123
  await runOnboardStart(rest);
1047
1124
  return;
1048
1125
  }
@@ -1150,15 +1227,65 @@ function buildOauthStartUrlFromInvitePayload(gatewayBaseUrl, payload) {
1150
1227
  return `${gatewayBaseUrl.replace(/\/+$/, "")}/oauth/start${params.toString() ? `?${params.toString()}` : ""}`;
1151
1228
  }
1152
1229
 
1153
- function buildOnboardPowerShellScript({ gatewayBaseUrl, token }) {
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 }) {
1154
1263
  const safeGateway = String(gatewayBaseUrl || "").replace(/'/g, "''");
1155
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, "''");
1156
1269
  const safePackageSpec = String(ONBOARD_PACKAGE_SPEC || "").replace(/'/g, "''");
1157
- return [
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 = [
1158
1278
  "$ErrorActionPreference = 'Stop'",
1159
1279
  "if (-not (Get-Command npx -ErrorAction SilentlyContinue)) { throw 'npx is required. Install Node.js first.' }",
1160
- `npx -y '${safePackageSpec}' onboard run --gateway '${safeGateway}' --token '${safeToken}'`,
1161
- ].join("\r\n");
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");
1162
1289
  }
1163
1290
 
1164
1291
  function createGatewayInviteTokenFromOptions(options = {}) {
@@ -1259,12 +1386,33 @@ async function startGatewayServer() {
1259
1386
 
1260
1387
  if (method === "GET" && requestUrl.pathname === "/onboard.ps1") {
1261
1388
  const token = requestUrl.searchParams.get("token") || "";
1262
- const secret = requireGatewayInviteSecret();
1263
- const payload = parseAndVerifyInviteToken(token, secret);
1264
- const script = buildOnboardPowerShellScript({
1265
- gatewayBaseUrl: payload.gateway_url || gatewayBaseUrl,
1266
- token,
1267
- });
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
+ }
1268
1416
  res.writeHead(200, {
1269
1417
  "Content-Type": "text/plain; charset=utf-8",
1270
1418
  "Cache-Control": "no-store",
@@ -1273,6 +1421,21 @@ async function startGatewayServer() {
1273
1421
  return;
1274
1422
  }
1275
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;
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
+
1276
1439
  if (method === "GET" && requestUrl.pathname === "/onboard/resolve") {
1277
1440
  const token = requestUrl.searchParams.get("token") || "";
1278
1441
  const secret = requireGatewayInviteSecret();
@@ -1280,6 +1443,7 @@ async function startGatewayServer() {
1280
1443
  const oauthStartUrl = buildOauthStartUrlFromInvitePayload(gatewayBaseUrl, payload);
1281
1444
  sendJson(res, 200, {
1282
1445
  ok: true,
1446
+ mode: "invite_token",
1283
1447
  gateway_url: payload.gateway_url || gatewayBaseUrl,
1284
1448
  gateway_api_key: payload.gateway_api_key || "",
1285
1449
  profile: payload.profile || "",
@@ -1475,6 +1639,9 @@ async function startGatewayServer() {
1475
1639
  );
1476
1640
  console.error(`[${SERVER_NAME}] oauth start URL: ${gatewayBaseUrl}/oauth/start`);
1477
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
+ }
1478
1645
  }
1479
1646
 
1480
1647
  function printGatewayHelp() {
@@ -1484,6 +1651,8 @@ function printGatewayHelp() {
1484
1651
  "Usage:",
1485
1652
  " slack-max-api-mcp gateway start",
1486
1653
  " slack-max-api-mcp gateway invite --profile woobin --team T123",
1654
+ " # tokenless onboarding endpoint (when enabled):",
1655
+ " # https://gateway.example.com/onboard/bootstrap",
1487
1656
  " slack-max-api-mcp gateway help",
1488
1657
  "",
1489
1658
  "Gateway env vars (server-side):",
@@ -1491,6 +1660,9 @@ function printGatewayHelp() {
1491
1660
  " SLACK_GATEWAY_HOST, SLACK_GATEWAY_PORT, SLACK_GATEWAY_PUBLIC_BASE_URL",
1492
1661
  " SLACK_GATEWAY_SHARED_SECRET (recommended)",
1493
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",
1494
1666
  " SLACK_OAUTH_BOT_SCOPES, SLACK_OAUTH_USER_SCOPES",
1495
1667
  "",
1496
1668
  "Client env vars (mcp caller-side):",
@@ -1507,6 +1679,12 @@ function runGatewayInvite(args) {
1507
1679
  const onboardScriptUrl = `${gatewayBaseUrl}/onboard.ps1?token=${encodeURIComponent(token)}`;
1508
1680
  const oauthStartUrl = buildOauthStartUrlFromInvitePayload(gatewayBaseUrl, payload);
1509
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("; ");
1510
1688
 
1511
1689
  console.log("[gateway] invite token created");
1512
1690
  console.log(`[gateway] expires_at: ${new Date(Number(payload.exp)).toISOString()}`);
@@ -1514,6 +1692,8 @@ function runGatewayInvite(args) {
1514
1692
  console.log(`[gateway] oauth_start_url: ${oauthStartUrl}`);
1515
1693
  console.log("[gateway] one-click command for team member:");
1516
1694
  console.log(command);
1695
+ console.log("[gateway] fallback command (self-signed TLS):");
1696
+ console.log(commandCurlFallback);
1517
1697
  }
1518
1698
 
1519
1699
  async function runGatewayCli(args) {