slack-max-api-mcp 1.0.6 → 1.0.8

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):
@@ -34,10 +38,19 @@ SLACK_CLIENT_SECRET=
34
38
 
35
39
  # Optional auto-onboarding trigger for interactive `slack-max-api-mcp` runs
36
40
  # SLACK_AUTO_ONBOARD=true
41
+ # Built-in default team gateway used when no auto-onboard gateway is set:
42
+ # SLACK_DEFAULT_TEAM_GATEWAY_URL=https://43.202.54.65.sslip.io
43
+ # SLACK_DEFAULT_TEAM_GATEWAY_INSECURE_TLS=true
44
+ # SLACK_GATEWAY_INSECURE_TLS=true
37
45
  # SLACK_AUTO_ONBOARD_URL=https://mcp-gateway.example.com/onboard.ps1?token=...
38
46
  # or
39
47
  # SLACK_AUTO_ONBOARD_GATEWAY=https://mcp-gateway.example.com
40
48
  # SLACK_AUTO_ONBOARD_TOKEN=invite-token-issued-by-gateway
49
+ # Optional tokenless variant:
50
+ # SLACK_AUTO_ONBOARD_GATEWAY=https://mcp-gateway.example.com
51
+ # SLACK_AUTO_ONBOARD_PROFILE_PREFIX=auto
52
+ # SLACK_AUTO_ONBOARD_PROFILE=
53
+ # SLACK_ONBOARD_SKIP_TLS_VERIFY=false
41
54
 
42
55
  # Legacy/manual token mode (optional)
43
56
  # SLACK_BOT_TOKEN=xoxb-your-bot-token
package/README.md CHANGED
@@ -62,17 +62,26 @@ Slack Web API를 Codex/Claude Code에서 바로 사용할 수 있게 만든 `std
62
62
  2. USER 토큰 사용 시 메시지/파일 검색, 채널 읽기, 메시지 전송 가능
63
63
  3. BOT으로 검색은 토큰 타입 제한(`not_allowed_token_type`)이 있어 USER 토큰 사용 권장
64
64
 
65
- ## 설치 및 실행
66
-
67
- ```powershell
68
- npm install -g slack-max-api-mcp@latest
69
- slack-max-api-mcp
70
- ```
71
-
72
- 또는:
73
-
74
- ```powershell
75
- npx -y slack-max-api-mcp
65
+ ## 설치 및 실행
66
+
67
+ ```powershell
68
+ npm install -g slack-max-api-mcp@latest
69
+ slack-max-api-mcp
70
+ ```
71
+
72
+ 팀원 기본 온보딩(현재 패키지 기본값):
73
+ 1. 추가 환경변수 없이 `slack-max-api-mcp` 실행 시 자동 온보딩 시도
74
+ 2. 기본 게이트웨이: `https://43.202.54.65.sslip.io`
75
+ 3. 승인 후 Codex 등록 1회:
76
+
77
+ ```powershell
78
+ codex mcp add slack-max -- npx -y slack-max-api-mcp
79
+ ```
80
+
81
+ 또는:
82
+
83
+ ```powershell
84
+ npx -y slack-max-api-mcp
76
85
  ```
77
86
 
78
87
  ## Codex / Claude Code 연결
@@ -110,37 +119,50 @@ setx SLACK_GATEWAY_PORT "8790"
110
119
  setx SLACK_GATEWAY_PUBLIC_BASE_URL "https://your-gateway.example.com"
111
120
  setx SLACK_GATEWAY_SHARED_SECRET "long-random-shared-secret"
112
121
  setx SLACK_GATEWAY_CLIENT_API_KEY "long-random-client-api-key"
122
+ setx SLACK_GATEWAY_PUBLIC_ONBOARD "true"
123
+ setx SLACK_GATEWAY_PUBLIC_ONBOARD_API_KEY "long-random-client-api-key"
113
124
  npx -y slack-max-api-mcp gateway start
114
125
  ```
115
126
 
116
- 운영자가 팀원용 원클릭 초대 커맨드 생성:
127
+ 주의:
128
+ 1. `SLACK_GATEWAY_PUBLIC_ONBOARD=true`는 토큰 없는 온보딩을 허용합니다.
129
+ 2. 게이트웨이가 비공개(`SLACK_GATEWAY_ALLOW_PUBLIC=false`)라면 `SLACK_GATEWAY_PUBLIC_ONBOARD_API_KEY`를 함께 설정해야 팀원 로컬 클라이언트가 API 호출할 수 있습니다.
130
+
131
+ ### 팀원 경험 (토큰 전달 없이 권장)
117
132
 
118
133
  ```powershell
119
- npx -y slack-max-api-mcp gateway invite --profile woobin --team T0AHNJ8QN0N
134
+ npx -y slack-max-api-mcp onboard run
120
135
  ```
121
136
 
122
- 명령이 팀원에게 전달할 "원클릭 설치 커맨드"를 출력합니다.
137
+ 자동 동작:
138
+ 1. 로컬 클라이언트 설정 파일(`~/.slack-max-api-mcp/client.json`) 작성
139
+ 2. 브라우저 OAuth 승인 페이지 자동 오픈
140
+ 3. Slack Allow 승인
141
+ 4. 완료 후 Codex에서 바로 사용
123
142
 
124
- ### 팀원 경험 (입력값 없이 원클릭)
143
+ 승인 Codex 연결(최초 1회):
125
144
 
126
145
  ```powershell
127
- powershell -ExecutionPolicy Bypass -Command "irm 'https://your-gateway.example.com/onboard.ps1?token=...' | iex"
146
+ codex mcp add slack-max -- npx -y slack-max-api-mcp
128
147
  ```
129
148
 
130
- 스크립트가 자동으로 수행:
131
- 1. `slack-max-api-mcp` 설치
132
- 2. 로컬 클라이언트 설정 파일(`~/.slack-max-api-mcp/client.json`) 작성
133
- 3. 브라우저 OAuth 승인 페이지 자동 오픈
149
+ ### 팀원 경험 (초대토큰 기반, 기존 방식)
134
150
 
135
- 승인 Codex 연결(최초 1회):
151
+ 운영자가 팀원용 원클릭 초대 커맨드 생성:
136
152
 
137
153
  ```powershell
138
- codex mcp add slack-max -- npx -y slack-max-api-mcp
154
+ npx -y slack-max-api-mcp gateway invite --profile woobin --team T0AHNJ8QN0N
155
+ ```
156
+
157
+ 위 명령이 팀원에게 전달할 "원클릭 설치 커맨드"를 출력합니다.
158
+
159
+ ```powershell
160
+ powershell -ExecutionPolicy Bypass -Command "irm 'https://your-gateway.example.com/onboard.ps1?token=...' | iex"
139
161
  ```
140
162
 
141
163
  ### 팀원 경험 (설치 후 `slack-max-api-mcp`만 실행)
142
164
 
143
- 운영자가 아래 값을 사전에 배포(이미지/스크립트/MDM)하면 팀원은 다음만 수행하면 됩니다.
165
+ 현재 패키지 기본 게이트웨이(`https://43.202.54.65.sslip.io`) 계속 사용할 경우 팀원은 다음만 수행하면 됩니다.
144
166
 
145
167
  ```powershell
146
168
  npm install -g slack-max-api-mcp@latest
@@ -153,9 +175,10 @@ slack-max-api-mcp
153
175
  3. Slack Allow 승인
154
176
  4. 완료 후 Codex에서 바로 사용
155
177
 
156
- 필요한 사전 배포값(팀원이 직접 입력하지 않아도 됨):
157
- 1. `SLACK_AUTO_ONBOARD_URL` 또는
158
- 2. `SLACK_AUTO_ONBOARD_GATEWAY` + `SLACK_AUTO_ONBOARD_TOKEN`
178
+ 게이트웨이 주소를 바꿔야 때만(운영자):
179
+ 1. `SLACK_DEFAULT_TEAM_GATEWAY_URL`
180
+ 2. `SLACK_DEFAULT_TEAM_GATEWAY_INSECURE_TLS` (`true/false`)
181
+ 3. 또는 기존 방식대로 `SLACK_AUTO_ONBOARD_URL`, `SLACK_AUTO_ONBOARD_GATEWAY` 사용 가능
159
182
 
160
183
  ## 2) 단독/개인 운영: 로컬 OAuth 모드
161
184
 
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.8",
4
4
  "description": "Slack MCP server (stdio) for Codex and Claude Code",
5
5
  "main": "src/slack-mcp-server.js",
6
6
  "bin": {
@@ -27,6 +27,7 @@
27
27
  "type": "commonjs",
28
28
  "dependencies": {
29
29
  "@modelcontextprotocol/sdk": "^1.27.1",
30
+ "undici": "^7.16.0",
30
31
  "zod": "^4.3.6"
31
32
  }
32
33
  }
@@ -6,6 +6,7 @@ const http = require("node:http");
6
6
  const path = require("node:path");
7
7
  const crypto = require("node:crypto");
8
8
  const { spawn } = require("node:child_process");
9
+ const { Agent, fetch: undiciFetch } = require("undici");
9
10
  const { McpServer } = require("@modelcontextprotocol/sdk/server/mcp.js");
10
11
  const { StdioServerTransport } = require("@modelcontextprotocol/sdk/server/stdio.js");
11
12
  const { z } = require("zod");
@@ -48,17 +49,43 @@ const GATEWAY_ALLOW_PUBLIC = process.env.SLACK_GATEWAY_ALLOW_PUBLIC === "true";
48
49
  const GATEWAY_SHARED_SECRET = process.env.SLACK_GATEWAY_SHARED_SECRET || GATEWAY_API_KEY;
49
50
  const GATEWAY_CLIENT_API_KEY =
50
51
  process.env.SLACK_GATEWAY_CLIENT_API_KEY || GATEWAY_API_KEY || GATEWAY_SHARED_SECRET;
52
+ const GATEWAY_PUBLIC_ONBOARD_ENABLED = process.env.SLACK_GATEWAY_PUBLIC_ONBOARD === "true";
53
+ const GATEWAY_PUBLIC_ONBOARD_EXPOSE_API_KEY =
54
+ process.env.SLACK_GATEWAY_PUBLIC_ONBOARD_EXPOSE_API_KEY === "true";
55
+ const GATEWAY_PUBLIC_ONBOARD_API_KEY = process.env.SLACK_GATEWAY_PUBLIC_ONBOARD_API_KEY || "";
56
+ const GATEWAY_PUBLIC_ONBOARD_PROFILE_PREFIX =
57
+ process.env.SLACK_GATEWAY_PUBLIC_ONBOARD_PROFILE_PREFIX || "auto";
51
58
  const GATEWAY_STATE_TTL_MS = Number(process.env.SLACK_GATEWAY_STATE_TTL_MS || 15 * 60 * 1000);
52
59
  const INVITE_TOKEN_DEFAULT_DAYS = Number(process.env.SLACK_INVITE_TOKEN_DEFAULT_DAYS || 7);
53
60
  const AUTO_ONBOARD_ENABLED = process.env.SLACK_AUTO_ONBOARD !== "false";
61
+ const DEFAULT_TEAM_GATEWAY_URL =
62
+ process.env.SLACK_DEFAULT_TEAM_GATEWAY_URL || "https://43.202.54.65.sslip.io";
63
+ const DEFAULT_TEAM_GATEWAY_INSECURE_TLS =
64
+ process.env.SLACK_DEFAULT_TEAM_GATEWAY_INSECURE_TLS !== "false";
65
+ const GATEWAY_INSECURE_TLS =
66
+ process.env.SLACK_GATEWAY_INSECURE_TLS === "true"
67
+ ? true
68
+ : process.env.SLACK_GATEWAY_INSECURE_TLS === "false"
69
+ ? false
70
+ : null;
54
71
  const AUTO_ONBOARD_GATEWAY =
55
- process.env.SLACK_AUTO_ONBOARD_GATEWAY || process.env.SLACK_ONBOARD_GATEWAY_URL || "";
72
+ process.env.SLACK_AUTO_ONBOARD_GATEWAY ||
73
+ process.env.SLACK_ONBOARD_GATEWAY_URL ||
74
+ DEFAULT_TEAM_GATEWAY_URL;
75
+ const AUTO_ONBOARD_PROFILE = process.env.SLACK_AUTO_ONBOARD_PROFILE || "";
56
76
  const AUTO_ONBOARD_TOKEN = process.env.SLACK_AUTO_ONBOARD_TOKEN || process.env.SLACK_ONBOARD_TOKEN || "";
57
77
  const AUTO_ONBOARD_URL = process.env.SLACK_AUTO_ONBOARD_URL || process.env.SLACK_ONBOARD_URL || "";
78
+ const AUTO_ONBOARD_PROFILE_PREFIX = process.env.SLACK_AUTO_ONBOARD_PROFILE_PREFIX || "auto";
58
79
  const ONBOARD_PACKAGE_SPEC =
59
80
  process.env.SLACK_ONBOARD_PACKAGE_SPEC ||
60
81
  process.env.SLACK_ONBOARD_INSTALL_SPEC ||
61
82
  "slack-max-api-mcp@latest";
83
+ const ONBOARD_SKIP_TLS_VERIFY = process.env.SLACK_ONBOARD_SKIP_TLS_VERIFY === "true";
84
+ const INSECURE_TLS_DISPATCHER = new Agent({
85
+ connect: {
86
+ rejectUnauthorized: false,
87
+ },
88
+ });
62
89
 
63
90
  function parseSimpleEnvFile(filePath) {
64
91
  if (!fs.existsSync(filePath)) return {};
@@ -88,11 +115,63 @@ function parseScopeList(raw) {
88
115
  return [...new Set(String(raw).split(",").map((part) => part.trim()).filter(Boolean))];
89
116
  }
90
117
 
118
+ function normalizeOnboardNamePart(value, fallback) {
119
+ const normalized = String(value || "")
120
+ .trim()
121
+ .toLowerCase()
122
+ .replace(/[^a-z0-9_-]+/g, "-")
123
+ .replace(/^-+|-+$/g, "");
124
+ if (!normalized) return fallback;
125
+ return normalized;
126
+ }
127
+
128
+ function createAutoOnboardProfileName(prefix = "auto") {
129
+ let username = "user";
130
+ try {
131
+ username = os.userInfo().username || process.env.USERNAME || process.env.USER || "user";
132
+ } catch {
133
+ username = process.env.USERNAME || process.env.USER || "user";
134
+ }
135
+ const host = os.hostname() || "host";
136
+ const profilePrefix = normalizeOnboardNamePart(prefix, "auto");
137
+ const userPart = normalizeOnboardNamePart(username, "user");
138
+ const hostPart = normalizeOnboardNamePart(host, "host");
139
+ const rand = crypto.randomBytes(3).toString("hex");
140
+ return `${profilePrefix}-${userPart}-${hostPart}-${rand}`.slice(0, 80);
141
+ }
142
+
91
143
  function ensureParentDirectory(filePath) {
92
144
  const dirPath = path.dirname(filePath);
93
145
  fs.mkdirSync(dirPath, { recursive: true });
94
146
  }
95
147
 
148
+ function normalizeBaseUrl(url) {
149
+ return String(url || "").trim().replace(/\/+$/, "");
150
+ }
151
+
152
+ function normalizeUrlOrigin(url) {
153
+ try {
154
+ const parsed = new URL(String(url || "").trim());
155
+ return `${parsed.protocol}//${parsed.host}`.replace(/\/+$/, "");
156
+ } catch {
157
+ return normalizeBaseUrl(url);
158
+ }
159
+ }
160
+
161
+ function shouldUseInsecureGatewayTls(url) {
162
+ if (!url) return false;
163
+ if (GATEWAY_INSECURE_TLS !== null) return GATEWAY_INSECURE_TLS;
164
+ if (!DEFAULT_TEAM_GATEWAY_INSECURE_TLS) return false;
165
+ return normalizeUrlOrigin(url) === normalizeUrlOrigin(DEFAULT_TEAM_GATEWAY_URL);
166
+ }
167
+
168
+ async function fetchWithOptionalInsecureGatewayTls(url, options) {
169
+ if (!shouldUseInsecureGatewayTls(url)) {
170
+ return undiciFetch(url, options);
171
+ }
172
+ return undiciFetch(url, { ...(options || {}), dispatcher: INSECURE_TLS_DISPATCHER });
173
+ }
174
+
96
175
  function emptyTokenStore() {
97
176
  return { version: 1, default_profile: null, profiles: {} };
98
177
  }
@@ -320,7 +399,8 @@ async function callSlackApiViaGateway(method, params = {}, tokenOverride, option
320
399
  throw new Error("Gateway URL is missing. Set SLACK_GATEWAY_URL to use gateway mode.");
321
400
  }
322
401
 
323
- const response = await fetch(`${runtimeGateway.url}/api/slack/call`, {
402
+ const gatewayCallUrl = `${runtimeGateway.url}/api/slack/call`;
403
+ const response = await fetchWithOptionalInsecureGatewayTls(gatewayCallUrl, {
324
404
  method: "POST",
325
405
  headers: buildGatewayAuthHeaders(runtimeGateway.apiKey),
326
406
  body: JSON.stringify({
@@ -370,7 +450,8 @@ async function slackHttpViaGateway(input) {
370
450
  throw new Error("Gateway URL is missing. Set SLACK_GATEWAY_URL to use gateway mode.");
371
451
  }
372
452
 
373
- const response = await fetch(`${runtimeGateway.url}/api/slack/http`, {
453
+ const gatewayHttpUrl = `${runtimeGateway.url}/api/slack/http`;
454
+ const response = await fetchWithOptionalInsecureGatewayTls(gatewayHttpUrl, {
374
455
  method: "POST",
375
456
  headers: buildGatewayAuthHeaders(runtimeGateway.apiKey),
376
457
  body: JSON.stringify({
@@ -632,12 +713,20 @@ async function runAutoOnboardingIfPossible() {
632
713
  }
633
714
 
634
715
  if (AUTO_ONBOARD_GATEWAY && AUTO_ONBOARD_TOKEN) {
635
- await runOnboardStart([
636
- "--gateway",
637
- AUTO_ONBOARD_GATEWAY,
638
- "--token",
639
- AUTO_ONBOARD_TOKEN,
640
- ]);
716
+ const args = ["--gateway", AUTO_ONBOARD_GATEWAY, "--token", AUTO_ONBOARD_TOKEN];
717
+ if (AUTO_ONBOARD_PROFILE) args.push("--profile", AUTO_ONBOARD_PROFILE);
718
+ await runOnboardStart(args);
719
+ return true;
720
+ }
721
+
722
+ if (AUTO_ONBOARD_GATEWAY) {
723
+ const args = ["--gateway", AUTO_ONBOARD_GATEWAY];
724
+ if (AUTO_ONBOARD_PROFILE) {
725
+ args.push("--profile", AUTO_ONBOARD_PROFILE);
726
+ } else if (AUTO_ONBOARD_PROFILE_PREFIX) {
727
+ args.push("--profile", createAutoOnboardProfileName(AUTO_ONBOARD_PROFILE_PREFIX));
728
+ }
729
+ await runOnboardStart(args);
641
730
  return true;
642
731
  }
643
732
 
@@ -976,9 +1065,13 @@ function printOnboardHelp() {
976
1065
  "Slack Max onboarding helper",
977
1066
  "",
978
1067
  "Usage:",
979
- " slack-max-api-mcp onboard run --gateway https://gateway.example.com --token <invite_token>",
1068
+ " slack-max-api-mcp onboard run [--gateway https://gateway.example.com] [--token <invite_token>]",
1069
+ " [--profile NAME] [--team T123] [--scope a,b] [--user-scope c,d]",
1070
+ " slack-max-api-mcp onboard quick [--gateway https://gateway.example.com]",
980
1071
  " slack-max-api-mcp onboard help",
981
1072
  "",
1073
+ `Default gateway (if omitted): ${DEFAULT_TEAM_GATEWAY_URL}`,
1074
+ "If --token is omitted, it uses gateway public onboarding endpoint (/onboard/bootstrap).",
982
1075
  "This command writes local client config and opens the Slack OAuth approval page automatically.",
983
1076
  ];
984
1077
  console.log(lines.join("\n"));
@@ -986,13 +1079,33 @@ function printOnboardHelp() {
986
1079
 
987
1080
  async function runOnboardStart(args) {
988
1081
  const { options } = parseCliArgs(args);
989
- const gateway = String(options.gateway || options.url || "").replace(/\/+$/, "");
1082
+ const gateway = normalizeBaseUrl(options.gateway || options.url || DEFAULT_TEAM_GATEWAY_URL);
990
1083
  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>");
1084
+ if (!gateway) {
1085
+ throw new Error(
1086
+ "Usage: slack-max-api-mcp onboard run [--gateway <url>] [--token <invite_token>] [--profile <name>]"
1087
+ );
993
1088
  }
994
1089
 
995
- const response = await fetch(`${gateway}/onboard/resolve?token=${encodeURIComponent(token)}`, {
1090
+ const requestedProfile =
1091
+ String(options.profile || "").trim() || createAutoOnboardProfileName(AUTO_ONBOARD_PROFILE_PREFIX);
1092
+ const requestedTeam = String(options.team || "").trim();
1093
+ const requestedScope = parseScopeList(options.scope || "").join(",");
1094
+ const requestedUserScope = parseScopeList(options["user-scope"] || options.user_scope || "").join(",");
1095
+
1096
+ const onboardingUrl = token
1097
+ ? `${gateway}/onboard/resolve?token=${encodeURIComponent(token)}`
1098
+ : (() => {
1099
+ const params = new URLSearchParams();
1100
+ if (requestedProfile) params.set("profile", requestedProfile);
1101
+ if (requestedTeam) params.set("team", requestedTeam);
1102
+ if (requestedScope) params.set("scope", requestedScope);
1103
+ if (requestedUserScope) params.set("user_scope", requestedUserScope);
1104
+ const query = params.toString();
1105
+ return `${gateway}/onboard/bootstrap${query ? `?${query}` : ""}`;
1106
+ })();
1107
+
1108
+ const response = await fetchWithOptionalInsecureGatewayTls(onboardingUrl, {
996
1109
  method: "GET",
997
1110
  headers: { Accept: "application/json" },
998
1111
  });
@@ -1006,14 +1119,23 @@ async function runOnboardStart(args) {
1006
1119
  }
1007
1120
 
1008
1121
  if (!response.ok || !data?.ok) {
1122
+ if (!token && response.status === 404) {
1123
+ throw new Error("Onboarding failed: public onboarding is disabled on gateway (enable SLACK_GATEWAY_PUBLIC_ONBOARD=true).");
1124
+ }
1009
1125
  throw new Error(`Onboarding failed: ${data?.error || `http_${response.status}`}`);
1010
1126
  }
1011
1127
 
1012
1128
  const resolvedGatewayUrl = String(data.gateway_url || gateway).replace(/\/+$/, "");
1013
1129
  const resolvedApiKey = String(data.gateway_api_key || "");
1014
- const profile = String(data.profile || "");
1130
+ const profile = String(data.profile || requestedProfile || "");
1015
1131
  const oauthStartUrl = String(data.oauth_start_url || "");
1016
1132
 
1133
+ if (data.requires_gateway_api_key && !resolvedApiKey) {
1134
+ throw new Error(
1135
+ "Gateway requires API key but onboarding response did not provide one. Enable public gateway access or set SLACK_GATEWAY_PUBLIC_ONBOARD_API_KEY."
1136
+ );
1137
+ }
1138
+
1017
1139
  saveClientConfig({
1018
1140
  version: 1,
1019
1141
  gateway_url: resolvedGatewayUrl,
@@ -1032,6 +1154,9 @@ async function runOnboardStart(args) {
1032
1154
  console.log(`[onboard] client config saved: ${CLIENT_CONFIG_PATH}`);
1033
1155
  console.log(`[onboard] gateway: ${resolvedGatewayUrl}`);
1034
1156
  if (profile) console.log(`[onboard] profile: ${profile}`);
1157
+ if (data.mode === "public_onboard") {
1158
+ console.log("[onboard] mode: public_onboard (tokenless)");
1159
+ }
1035
1160
  console.log("[onboard] Next: approve in browser, then use Codex MCP as usual.");
1036
1161
  }
1037
1162
 
@@ -1042,7 +1167,7 @@ async function runOnboardCli(args) {
1042
1167
  printOnboardHelp();
1043
1168
  return;
1044
1169
  }
1045
- if (subcommand === "run" || subcommand === "start") {
1170
+ if (subcommand === "run" || subcommand === "start" || subcommand === "quick") {
1046
1171
  await runOnboardStart(rest);
1047
1172
  return;
1048
1173
  }
@@ -1150,15 +1275,65 @@ function buildOauthStartUrlFromInvitePayload(gatewayBaseUrl, payload) {
1150
1275
  return `${gatewayBaseUrl.replace(/\/+$/, "")}/oauth/start${params.toString() ? `?${params.toString()}` : ""}`;
1151
1276
  }
1152
1277
 
1153
- function buildOnboardPowerShellScript({ gatewayBaseUrl, token }) {
1278
+ function buildPublicOnboardPayload(gatewayBaseUrl, params = {}) {
1279
+ const profile = String(params.profile || "").trim() || createAutoOnboardProfileName(GATEWAY_PUBLIC_ONBOARD_PROFILE_PREFIX);
1280
+ const team = String(params.team || process.env.SLACK_OAUTH_TEAM_ID || "").trim();
1281
+ const scope = parseScopeList(params.scope || DEFAULT_OAUTH_BOT_SCOPES).join(",");
1282
+ const userScope = parseScopeList(params.user_scope || DEFAULT_OAUTH_USER_SCOPES).join(",");
1283
+ const payload = {
1284
+ gateway_url: gatewayBaseUrl,
1285
+ gateway_api_key: "",
1286
+ profile,
1287
+ team,
1288
+ scope,
1289
+ user_scope: userScope,
1290
+ };
1291
+ if (GATEWAY_ALLOW_PUBLIC) {
1292
+ payload.gateway_api_key = "";
1293
+ } else if (GATEWAY_PUBLIC_ONBOARD_API_KEY) {
1294
+ payload.gateway_api_key = GATEWAY_PUBLIC_ONBOARD_API_KEY;
1295
+ } else if (GATEWAY_PUBLIC_ONBOARD_EXPOSE_API_KEY) {
1296
+ payload.gateway_api_key = GATEWAY_CLIENT_API_KEY || "";
1297
+ }
1298
+ const oauthStartUrl = buildOauthStartUrlFromInvitePayload(gatewayBaseUrl, payload);
1299
+ return {
1300
+ ok: true,
1301
+ mode: "public_onboard",
1302
+ gateway_url: payload.gateway_url,
1303
+ gateway_api_key: payload.gateway_api_key,
1304
+ profile: payload.profile,
1305
+ oauth_start_url: oauthStartUrl,
1306
+ requires_gateway_api_key: !GATEWAY_ALLOW_PUBLIC,
1307
+ };
1308
+ }
1309
+
1310
+ function buildOnboardPowerShellScript({ gatewayBaseUrl, token, profile, team, scope, userScope }) {
1154
1311
  const safeGateway = String(gatewayBaseUrl || "").replace(/'/g, "''");
1155
1312
  const safeToken = String(token || "").replace(/'/g, "''");
1313
+ const safeProfile = String(profile || "").replace(/'/g, "''");
1314
+ const safeTeam = String(team || "").replace(/'/g, "''");
1315
+ const safeScope = String(scope || "").replace(/'/g, "''");
1316
+ const safeUserScope = String(userScope || "").replace(/'/g, "''");
1156
1317
  const safePackageSpec = String(ONBOARD_PACKAGE_SPEC || "").replace(/'/g, "''");
1157
- return [
1318
+ const onboardCommandParts = [`npx -y '${safePackageSpec}' onboard run --gateway '${safeGateway}'`];
1319
+ if (safeToken) onboardCommandParts.push(`--token '${safeToken}'`);
1320
+ if (safeProfile) onboardCommandParts.push(`--profile '${safeProfile}'`);
1321
+ if (safeTeam) onboardCommandParts.push(`--team '${safeTeam}'`);
1322
+ if (safeScope) onboardCommandParts.push(`--scope '${safeScope}'`);
1323
+ if (safeUserScope) onboardCommandParts.push(`--user-scope '${safeUserScope}'`);
1324
+
1325
+ const lines = [
1158
1326
  "$ErrorActionPreference = 'Stop'",
1159
1327
  "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");
1328
+ ];
1329
+ if (ONBOARD_SKIP_TLS_VERIFY) {
1330
+ lines.push("$env:NODE_TLS_REJECT_UNAUTHORIZED='0'");
1331
+ }
1332
+ lines.push(onboardCommandParts.join(" "));
1333
+ if (ONBOARD_SKIP_TLS_VERIFY) {
1334
+ lines.push("Remove-Item Env:NODE_TLS_REJECT_UNAUTHORIZED -ErrorAction SilentlyContinue");
1335
+ }
1336
+ return lines.join("\r\n");
1162
1337
  }
1163
1338
 
1164
1339
  function createGatewayInviteTokenFromOptions(options = {}) {
@@ -1259,12 +1434,33 @@ async function startGatewayServer() {
1259
1434
 
1260
1435
  if (method === "GET" && requestUrl.pathname === "/onboard.ps1") {
1261
1436
  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
- });
1437
+ let script = "";
1438
+ if (token) {
1439
+ const secret = requireGatewayInviteSecret();
1440
+ const payload = parseAndVerifyInviteToken(token, secret);
1441
+ script = buildOnboardPowerShellScript({
1442
+ gatewayBaseUrl: payload.gateway_url || gatewayBaseUrl,
1443
+ token,
1444
+ });
1445
+ } else {
1446
+ if (!GATEWAY_PUBLIC_ONBOARD_ENABLED) {
1447
+ sendJson(res, 404, { ok: false, error: "public_onboard_disabled" });
1448
+ return;
1449
+ }
1450
+ const profile =
1451
+ requestUrl.searchParams.get("profile") ||
1452
+ createAutoOnboardProfileName(GATEWAY_PUBLIC_ONBOARD_PROFILE_PREFIX);
1453
+ const team = requestUrl.searchParams.get("team") || "";
1454
+ const scope = requestUrl.searchParams.get("scope") || "";
1455
+ const userScope = requestUrl.searchParams.get("user_scope") || "";
1456
+ script = buildOnboardPowerShellScript({
1457
+ gatewayBaseUrl,
1458
+ profile,
1459
+ team,
1460
+ scope,
1461
+ userScope,
1462
+ });
1463
+ }
1268
1464
  res.writeHead(200, {
1269
1465
  "Content-Type": "text/plain; charset=utf-8",
1270
1466
  "Cache-Control": "no-store",
@@ -1273,6 +1469,21 @@ async function startGatewayServer() {
1273
1469
  return;
1274
1470
  }
1275
1471
 
1472
+ if (method === "GET" && requestUrl.pathname === "/onboard/bootstrap") {
1473
+ if (!GATEWAY_PUBLIC_ONBOARD_ENABLED) {
1474
+ sendJson(res, 404, { ok: false, error: "public_onboard_disabled" });
1475
+ return;
1476
+ }
1477
+ const payload = buildPublicOnboardPayload(gatewayBaseUrl, {
1478
+ profile: requestUrl.searchParams.get("profile") || "",
1479
+ team: requestUrl.searchParams.get("team") || "",
1480
+ scope: requestUrl.searchParams.get("scope") || "",
1481
+ user_scope: requestUrl.searchParams.get("user_scope") || "",
1482
+ });
1483
+ sendJson(res, 200, payload);
1484
+ return;
1485
+ }
1486
+
1276
1487
  if (method === "GET" && requestUrl.pathname === "/onboard/resolve") {
1277
1488
  const token = requestUrl.searchParams.get("token") || "";
1278
1489
  const secret = requireGatewayInviteSecret();
@@ -1280,6 +1491,7 @@ async function startGatewayServer() {
1280
1491
  const oauthStartUrl = buildOauthStartUrlFromInvitePayload(gatewayBaseUrl, payload);
1281
1492
  sendJson(res, 200, {
1282
1493
  ok: true,
1494
+ mode: "invite_token",
1283
1495
  gateway_url: payload.gateway_url || gatewayBaseUrl,
1284
1496
  gateway_api_key: payload.gateway_api_key || "",
1285
1497
  profile: payload.profile || "",
@@ -1475,6 +1687,9 @@ async function startGatewayServer() {
1475
1687
  );
1476
1688
  console.error(`[${SERVER_NAME}] oauth start URL: ${gatewayBaseUrl}/oauth/start`);
1477
1689
  console.error(`[${SERVER_NAME}] profile list URL: ${gatewayBaseUrl}/profiles`);
1690
+ if (GATEWAY_PUBLIC_ONBOARD_ENABLED) {
1691
+ console.error(`[${SERVER_NAME}] public onboard URL: ${gatewayBaseUrl}/onboard/bootstrap`);
1692
+ }
1478
1693
  }
1479
1694
 
1480
1695
  function printGatewayHelp() {
@@ -1484,6 +1699,8 @@ function printGatewayHelp() {
1484
1699
  "Usage:",
1485
1700
  " slack-max-api-mcp gateway start",
1486
1701
  " slack-max-api-mcp gateway invite --profile woobin --team T123",
1702
+ " # tokenless onboarding endpoint (when enabled):",
1703
+ " # https://gateway.example.com/onboard/bootstrap",
1487
1704
  " slack-max-api-mcp gateway help",
1488
1705
  "",
1489
1706
  "Gateway env vars (server-side):",
@@ -1491,6 +1708,9 @@ function printGatewayHelp() {
1491
1708
  " SLACK_GATEWAY_HOST, SLACK_GATEWAY_PORT, SLACK_GATEWAY_PUBLIC_BASE_URL",
1492
1709
  " SLACK_GATEWAY_SHARED_SECRET (recommended)",
1493
1710
  " SLACK_GATEWAY_CLIENT_API_KEY (optional, defaults to shared secret)",
1711
+ " SLACK_GATEWAY_PUBLIC_ONBOARD=true # allow tokenless onboarding endpoint",
1712
+ " SLACK_GATEWAY_PUBLIC_ONBOARD_API_KEY=<client key> # optional, used when gateway is not fully public",
1713
+ " SLACK_GATEWAY_PUBLIC_ONBOARD_EXPOSE_API_KEY=true # fallback: expose client key as-is",
1494
1714
  " SLACK_OAUTH_BOT_SCOPES, SLACK_OAUTH_USER_SCOPES",
1495
1715
  "",
1496
1716
  "Client env vars (mcp caller-side):",
@@ -1507,6 +1727,12 @@ function runGatewayInvite(args) {
1507
1727
  const onboardScriptUrl = `${gatewayBaseUrl}/onboard.ps1?token=${encodeURIComponent(token)}`;
1508
1728
  const oauthStartUrl = buildOauthStartUrlFromInvitePayload(gatewayBaseUrl, payload);
1509
1729
  const command = `powershell -ExecutionPolicy Bypass -Command "irm '${onboardScriptUrl}' | iex"`;
1730
+ const commandCurlFallback = [
1731
+ `$tmp = Join-Path $env:TEMP 'slack-onboard.ps1'`,
1732
+ `curl.exe -k -sS '${onboardScriptUrl}' -o $tmp`,
1733
+ `powershell -ExecutionPolicy Bypass -File $tmp`,
1734
+ `Remove-Item $tmp -Force`,
1735
+ ].join("; ");
1510
1736
 
1511
1737
  console.log("[gateway] invite token created");
1512
1738
  console.log(`[gateway] expires_at: ${new Date(Number(payload.exp)).toISOString()}`);
@@ -1514,6 +1740,8 @@ function runGatewayInvite(args) {
1514
1740
  console.log(`[gateway] oauth_start_url: ${oauthStartUrl}`);
1515
1741
  console.log("[gateway] one-click command for team member:");
1516
1742
  console.log(command);
1743
+ console.log("[gateway] fallback command (self-signed TLS):");
1744
+ console.log(commandCurlFallback);
1517
1745
  }
1518
1746
 
1519
1747
  async function runGatewayCli(args) {