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 +13 -0
- package/README.md +49 -26
- package/package.json +2 -1
- package/src/slack-mcp-server.js +254 -26
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
|
-
|
|
75
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
151
|
+
운영자가 팀원용 원클릭 초대 커맨드 생성:
|
|
136
152
|
|
|
137
153
|
```powershell
|
|
138
|
-
|
|
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
|
-
|
|
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. `
|
|
158
|
-
2. `
|
|
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.
|
|
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
|
}
|
package/src/slack-mcp-server.js
CHANGED
|
@@ -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 ||
|
|
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
|
|
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
|
|
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
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
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 =
|
|
1082
|
+
const gateway = normalizeBaseUrl(options.gateway || options.url || DEFAULT_TEAM_GATEWAY_URL);
|
|
990
1083
|
const token = String(options.token || "");
|
|
991
|
-
if (!gateway
|
|
992
|
-
throw new Error(
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
1161
|
-
|
|
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
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
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) {
|