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 +9 -0
- package/README.md +27 -11
- package/package.json +1 -1
- package/src/slack-mcp-server.js +202 -22
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
144
|
+
운영자가 팀원용 원클릭 초대 커맨드 생성:
|
|
136
145
|
|
|
137
146
|
```powershell
|
|
138
|
-
|
|
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
package/src/slack-mcp-server.js
CHANGED
|
@@ -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
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
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
|
|
992
|
-
throw new Error(
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
1161
|
-
|
|
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
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
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) {
|