slack-max-api-mcp 1.0.10 → 1.0.12
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/package.json +1 -1
- package/src/slack-mcp-server.js +393 -58
package/package.json
CHANGED
package/src/slack-mcp-server.js
CHANGED
|
@@ -71,6 +71,25 @@ const GATEWAY_COMPAT_HOST = "43.202.54.65.sslip.io";
|
|
|
71
71
|
const GATEWAY_URL_ENV = process.env.SLACK_GATEWAY_URL || "";
|
|
72
72
|
const GATEWAY_API_KEY = process.env.SLACK_GATEWAY_API_KEY || "";
|
|
73
73
|
const GATEWAY_PROFILE = process.env.SLACK_GATEWAY_PROFILE || "";
|
|
74
|
+
const GATEWAY_PUBLIC_BASE_URL =
|
|
75
|
+
process.env.SLACK_GATEWAY_PUBLIC_BASE_URL || ONBOARD_PUBLIC_BASE_URL;
|
|
76
|
+
const GATEWAY_ALLOW_PUBLIC = parseBooleanEnv(process.env.SLACK_GATEWAY_ALLOW_PUBLIC, false);
|
|
77
|
+
const GATEWAY_SHARED_SECRET =
|
|
78
|
+
process.env.SLACK_GATEWAY_SHARED_SECRET || process.env.SLACK_GATEWAY_API_KEY || "";
|
|
79
|
+
const GATEWAY_CLIENT_API_KEY =
|
|
80
|
+
process.env.SLACK_GATEWAY_CLIENT_API_KEY || GATEWAY_API_KEY || GATEWAY_SHARED_SECRET;
|
|
81
|
+
const GATEWAY_PUBLIC_ONBOARD_ENABLED = parseBooleanEnv(
|
|
82
|
+
process.env.SLACK_GATEWAY_PUBLIC_ONBOARD,
|
|
83
|
+
true
|
|
84
|
+
);
|
|
85
|
+
const GATEWAY_PUBLIC_ONBOARD_EXPOSE_API_KEY = parseBooleanEnv(
|
|
86
|
+
process.env.SLACK_GATEWAY_PUBLIC_ONBOARD_EXPOSE_API_KEY,
|
|
87
|
+
false
|
|
88
|
+
);
|
|
89
|
+
const GATEWAY_PUBLIC_ONBOARD_API_KEY = process.env.SLACK_GATEWAY_PUBLIC_ONBOARD_API_KEY || "";
|
|
90
|
+
const GATEWAY_PUBLIC_ONBOARD_PROFILE_PREFIX =
|
|
91
|
+
process.env.SLACK_GATEWAY_PUBLIC_ONBOARD_PROFILE_PREFIX || "auto";
|
|
92
|
+
const GATEWAY_STATE_TTL_MS = Number(process.env.SLACK_GATEWAY_STATE_TTL_MS || 15 * 60 * 1000);
|
|
74
93
|
const INSECURE_TLS = parseBooleanEnv(process.env.SLACK_INSECURE_TLS, false);
|
|
75
94
|
const GATEWAY_SKIP_TLS_VERIFY = parseBooleanEnv(
|
|
76
95
|
process.env.SLACK_GATEWAY_SKIP_TLS_VERIFY,
|
|
@@ -1702,6 +1721,132 @@ function formatClientConfigSummary(config) {
|
|
|
1702
1721
|
].join("\n");
|
|
1703
1722
|
}
|
|
1704
1723
|
|
|
1724
|
+
function buildGatewayRedirectUri(publicBaseUrl, callbackPath) {
|
|
1725
|
+
const url = new URL(callbackPath, `${String(publicBaseUrl || "").replace(/\/+$/, "")}/`);
|
|
1726
|
+
return url.toString();
|
|
1727
|
+
}
|
|
1728
|
+
|
|
1729
|
+
function parseScopesFromQuery(searchParams, key, fallback) {
|
|
1730
|
+
const value = searchParams.get(key);
|
|
1731
|
+
return parseScopeList(value || fallback);
|
|
1732
|
+
}
|
|
1733
|
+
|
|
1734
|
+
function buildOauthStartUrlFromPayload(gatewayBaseUrl, payload) {
|
|
1735
|
+
const params = new URLSearchParams();
|
|
1736
|
+
if (payload.profile) params.set("profile", payload.profile);
|
|
1737
|
+
if (payload.team) params.set("team", payload.team);
|
|
1738
|
+
if (payload.scope) params.set("scope", payload.scope);
|
|
1739
|
+
if (payload.user_scope) params.set("user_scope", payload.user_scope);
|
|
1740
|
+
return `${String(gatewayBaseUrl || "").replace(/\/+$/, "")}/oauth/start${
|
|
1741
|
+
params.toString() ? `?${params.toString()}` : ""
|
|
1742
|
+
}`;
|
|
1743
|
+
}
|
|
1744
|
+
|
|
1745
|
+
function buildPublicOnboardPayload(gatewayBaseUrl, params = {}) {
|
|
1746
|
+
const profile =
|
|
1747
|
+
String(params.profile || "").trim() ||
|
|
1748
|
+
createAutoOnboardProfileName(GATEWAY_PUBLIC_ONBOARD_PROFILE_PREFIX);
|
|
1749
|
+
const team = String(params.team || process.env.SLACK_OAUTH_TEAM_ID || "").trim();
|
|
1750
|
+
const scope = parseScopeList(params.scope || DEFAULT_OAUTH_BOT_SCOPES).join(",");
|
|
1751
|
+
const userScope = parseScopeList(params.user_scope || DEFAULT_OAUTH_USER_SCOPES).join(",");
|
|
1752
|
+
const payload = {
|
|
1753
|
+
gateway_url: String(gatewayBaseUrl || "").replace(/\/+$/, ""),
|
|
1754
|
+
gateway_api_key: "",
|
|
1755
|
+
profile,
|
|
1756
|
+
team,
|
|
1757
|
+
scope,
|
|
1758
|
+
user_scope: userScope,
|
|
1759
|
+
};
|
|
1760
|
+
if (GATEWAY_ALLOW_PUBLIC) {
|
|
1761
|
+
payload.gateway_api_key = "";
|
|
1762
|
+
} else if (GATEWAY_PUBLIC_ONBOARD_API_KEY) {
|
|
1763
|
+
payload.gateway_api_key = GATEWAY_PUBLIC_ONBOARD_API_KEY;
|
|
1764
|
+
} else if (GATEWAY_PUBLIC_ONBOARD_EXPOSE_API_KEY) {
|
|
1765
|
+
payload.gateway_api_key = GATEWAY_CLIENT_API_KEY || "";
|
|
1766
|
+
}
|
|
1767
|
+
return {
|
|
1768
|
+
ok: true,
|
|
1769
|
+
mode: "public_onboard",
|
|
1770
|
+
gateway_url: payload.gateway_url,
|
|
1771
|
+
gateway_api_key: payload.gateway_api_key,
|
|
1772
|
+
profile: payload.profile,
|
|
1773
|
+
oauth_start_url: buildOauthStartUrlFromPayload(gatewayBaseUrl, payload),
|
|
1774
|
+
requires_gateway_api_key: !GATEWAY_ALLOW_PUBLIC,
|
|
1775
|
+
};
|
|
1776
|
+
}
|
|
1777
|
+
|
|
1778
|
+
async function readRequestText(req, maxBytes = 1024 * 1024) {
|
|
1779
|
+
return new Promise((resolve, reject) => {
|
|
1780
|
+
const chunks = [];
|
|
1781
|
+
let total = 0;
|
|
1782
|
+
req.on("data", (chunk) => {
|
|
1783
|
+
total += chunk.length;
|
|
1784
|
+
if (total > maxBytes) {
|
|
1785
|
+
reject(new Error("Request body too large."));
|
|
1786
|
+
req.destroy();
|
|
1787
|
+
return;
|
|
1788
|
+
}
|
|
1789
|
+
chunks.push(chunk);
|
|
1790
|
+
});
|
|
1791
|
+
req.on("end", () => resolve(Buffer.concat(chunks).toString("utf8")));
|
|
1792
|
+
req.on("error", (error) => reject(error));
|
|
1793
|
+
});
|
|
1794
|
+
}
|
|
1795
|
+
|
|
1796
|
+
async function readRequestJson(req, maxBytes) {
|
|
1797
|
+
const text = await readRequestText(req, maxBytes);
|
|
1798
|
+
if (!text.trim()) return {};
|
|
1799
|
+
try {
|
|
1800
|
+
return JSON.parse(text);
|
|
1801
|
+
} catch {
|
|
1802
|
+
throw new Error("Invalid JSON body.");
|
|
1803
|
+
}
|
|
1804
|
+
}
|
|
1805
|
+
|
|
1806
|
+
function getRequestApiKey(req) {
|
|
1807
|
+
const authHeader = req.headers.authorization || "";
|
|
1808
|
+
if (authHeader.toLowerCase().startsWith("bearer ")) {
|
|
1809
|
+
return authHeader.slice(7).trim();
|
|
1810
|
+
}
|
|
1811
|
+
const xApiKey = req.headers["x-api-key"];
|
|
1812
|
+
return typeof xApiKey === "string" ? xApiKey.trim() : "";
|
|
1813
|
+
}
|
|
1814
|
+
|
|
1815
|
+
function isGatewayAuthorized(req) {
|
|
1816
|
+
if (GATEWAY_ALLOW_PUBLIC) return true;
|
|
1817
|
+
const allowedKeys = [GATEWAY_SHARED_SECRET, GATEWAY_CLIENT_API_KEY].filter(Boolean);
|
|
1818
|
+
if (allowedKeys.length === 0) return false;
|
|
1819
|
+
const provided = getRequestApiKey(req);
|
|
1820
|
+
return Boolean(provided && allowedKeys.includes(provided));
|
|
1821
|
+
}
|
|
1822
|
+
|
|
1823
|
+
function requireGatewayClientCredentials() {
|
|
1824
|
+
const clientId = process.env.SLACK_CLIENT_ID || "";
|
|
1825
|
+
const clientSecret = process.env.SLACK_CLIENT_SECRET || "";
|
|
1826
|
+
if (!clientId || !clientSecret) {
|
|
1827
|
+
throw new Error("Gateway OAuth requires SLACK_CLIENT_ID and SLACK_CLIENT_SECRET on the gateway server.");
|
|
1828
|
+
}
|
|
1829
|
+
return { clientId, clientSecret };
|
|
1830
|
+
}
|
|
1831
|
+
|
|
1832
|
+
function profileSummariesFromStore(store) {
|
|
1833
|
+
const summaries = [];
|
|
1834
|
+
for (const [key, profile] of Object.entries(store.profiles || {})) {
|
|
1835
|
+
summaries.push({
|
|
1836
|
+
key,
|
|
1837
|
+
profile_name: profile.profile_name || "",
|
|
1838
|
+
team_id: profile.team_id || "",
|
|
1839
|
+
team_name: profile.team_name || "",
|
|
1840
|
+
authed_user_id: profile.authed_user_id || "",
|
|
1841
|
+
has_bot_token: Boolean(profile.bot_token),
|
|
1842
|
+
has_user_token: Boolean(profile.user_token),
|
|
1843
|
+
updated_at: profile.updated_at || null,
|
|
1844
|
+
is_default: store.default_profile === key,
|
|
1845
|
+
});
|
|
1846
|
+
}
|
|
1847
|
+
return summaries;
|
|
1848
|
+
}
|
|
1849
|
+
|
|
1705
1850
|
async function runOauthLogin(args) {
|
|
1706
1851
|
const { options } = parseCliArgs(args);
|
|
1707
1852
|
const clientId = options["client-id"] || process.env.SLACK_CLIENT_ID;
|
|
@@ -1923,6 +2068,9 @@ function printOnboardServerHelp() {
|
|
|
1923
2068
|
" SLACK_ONBOARD_SERVER_HOST, SLACK_ONBOARD_SERVER_PORT",
|
|
1924
2069
|
" SLACK_ONBOARD_PUBLIC_BASE_URL, SLACK_ONBOARD_SERVER_CALLBACK_PATH",
|
|
1925
2070
|
" SLACK_ONBOARD_CLAIM_TTL_MS",
|
|
2071
|
+
" SLACK_GATEWAY_PUBLIC_BASE_URL",
|
|
2072
|
+
" SLACK_GATEWAY_SHARED_SECRET / SLACK_GATEWAY_CLIENT_API_KEY",
|
|
2073
|
+
" SLACK_GATEWAY_ALLOW_PUBLIC, SLACK_GATEWAY_PUBLIC_ONBOARD",
|
|
1926
2074
|
];
|
|
1927
2075
|
console.log(lines.join("\n"));
|
|
1928
2076
|
}
|
|
@@ -2060,21 +2208,20 @@ async function runOnboardClient(args) {
|
|
|
2060
2208
|
|
|
2061
2209
|
async function runOnboardServerStart(args) {
|
|
2062
2210
|
const { options } = parseCliArgs(args);
|
|
2063
|
-
const clientId
|
|
2064
|
-
const clientSecret = options["client-secret"] || process.env.SLACK_CLIENT_SECRET;
|
|
2065
|
-
if (!clientId || !clientSecret) {
|
|
2066
|
-
throw new Error("Missing SLACK_CLIENT_ID or SLACK_CLIENT_SECRET on onboarding server.");
|
|
2067
|
-
}
|
|
2211
|
+
const { clientId, clientSecret } = requireGatewayClientCredentials();
|
|
2068
2212
|
|
|
2069
2213
|
const host = String(options.host || ONBOARD_SERVER_HOST);
|
|
2070
2214
|
const port = Number(options.port || ONBOARD_SERVER_PORT);
|
|
2071
2215
|
const callbackPath = String(options["callback-path"] || ONBOARD_CALLBACK_PATH);
|
|
2072
|
-
const publicBaseUrl = String(
|
|
2216
|
+
const publicBaseUrl = String(
|
|
2217
|
+
options["public-base-url"] || GATEWAY_PUBLIC_BASE_URL || ONBOARD_PUBLIC_BASE_URL
|
|
2218
|
+
).replace(/\/+$/, "");
|
|
2073
2219
|
const claimTtlMs = Math.max(60_000, Number(options["claim-ttl-ms"] || ONBOARD_CLAIM_TTL_MS));
|
|
2074
2220
|
const redirectUri = new URL(callbackPath, `${publicBaseUrl}/`).toString();
|
|
2075
2221
|
|
|
2076
2222
|
const claimSessions = new Map();
|
|
2077
2223
|
const stateToClaim = new Map();
|
|
2224
|
+
const pendingStates = new Map();
|
|
2078
2225
|
|
|
2079
2226
|
const server = http.createServer(async (req, res) => {
|
|
2080
2227
|
try {
|
|
@@ -2082,21 +2229,34 @@ async function runOnboardServerStart(args) {
|
|
|
2082
2229
|
const method = req.method || "GET";
|
|
2083
2230
|
const requestUrl = new URL(req.url || "/", `http://${host}:${port}`);
|
|
2084
2231
|
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
|
|
2232
|
+
if (method === "GET" && requestUrl.pathname === "/health") {
|
|
2233
|
+
sendJson(res, 200, {
|
|
2234
|
+
ok: true,
|
|
2235
|
+
service: SERVER_NAME,
|
|
2236
|
+
mode: "gateway",
|
|
2237
|
+
public_base_url: publicBaseUrl,
|
|
2238
|
+
callback_path: callbackPath,
|
|
2239
|
+
token_store_path: TOKEN_STORE_PATH,
|
|
2240
|
+
client_config_path: CLIENT_CONFIG_PATH,
|
|
2241
|
+
});
|
|
2242
|
+
return;
|
|
2243
|
+
}
|
|
2095
2244
|
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
|
|
2099
|
-
|
|
2245
|
+
if (method === "GET" && requestUrl.pathname === "/onboard/bootstrap") {
|
|
2246
|
+
if (GATEWAY_PUBLIC_ONBOARD_ENABLED) {
|
|
2247
|
+
const payload = buildPublicOnboardPayload(publicBaseUrl, {
|
|
2248
|
+
profile: requestUrl.searchParams.get("profile") || "",
|
|
2249
|
+
team: requestUrl.searchParams.get("team") || "",
|
|
2250
|
+
scope: requestUrl.searchParams.get("scope") || "",
|
|
2251
|
+
user_scope: requestUrl.searchParams.get("user_scope") || "",
|
|
2252
|
+
});
|
|
2253
|
+
sendJson(res, 200, payload);
|
|
2254
|
+
return;
|
|
2255
|
+
}
|
|
2256
|
+
|
|
2257
|
+
const profile = String(requestUrl.searchParams.get("profile") || "").trim();
|
|
2258
|
+
const team = String(requestUrl.searchParams.get("team") || "").trim();
|
|
2259
|
+
const botScopes = parseScopeList(requestUrl.searchParams.get("scope") || DEFAULT_OAUTH_BOT_SCOPES);
|
|
2100
2260
|
const userScopes = parseScopeList(
|
|
2101
2261
|
requestUrl.searchParams.get("user_scope") || DEFAULT_OAUTH_USER_SCOPES
|
|
2102
2262
|
);
|
|
@@ -2157,42 +2317,106 @@ async function runOnboardServerStart(args) {
|
|
|
2157
2317
|
res.writeHead(302, { Location: authorizeUrl });
|
|
2158
2318
|
res.end();
|
|
2159
2319
|
return;
|
|
2160
|
-
|
|
2320
|
+
}
|
|
2321
|
+
|
|
2322
|
+
if (method === "GET" && requestUrl.pathname === "/oauth/start") {
|
|
2323
|
+
const profileName = requestUrl.searchParams.get("profile") || "";
|
|
2324
|
+
const teamId = requestUrl.searchParams.get("team") || process.env.SLACK_OAUTH_TEAM_ID || "";
|
|
2325
|
+
const botScopes = parseScopesFromQuery(
|
|
2326
|
+
requestUrl.searchParams,
|
|
2327
|
+
"scope",
|
|
2328
|
+
DEFAULT_OAUTH_BOT_SCOPES
|
|
2329
|
+
);
|
|
2330
|
+
const userScopes = parseScopesFromQuery(
|
|
2331
|
+
requestUrl.searchParams,
|
|
2332
|
+
"user_scope",
|
|
2333
|
+
DEFAULT_OAUTH_USER_SCOPES
|
|
2334
|
+
);
|
|
2335
|
+
|
|
2336
|
+
if (botScopes.length === 0 && userScopes.length === 0) {
|
|
2337
|
+
sendJson(res, 400, { ok: false, error: "missing_scope" });
|
|
2338
|
+
return;
|
|
2339
|
+
}
|
|
2161
2340
|
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
|
|
2341
|
+
const state = crypto.randomBytes(24).toString("hex");
|
|
2342
|
+
pendingStates.set(state, {
|
|
2343
|
+
created_at: Date.now(),
|
|
2344
|
+
profile_name: profileName,
|
|
2345
|
+
team_id: teamId,
|
|
2346
|
+
bot_scopes: botScopes,
|
|
2347
|
+
user_scopes: userScopes,
|
|
2348
|
+
});
|
|
2349
|
+
|
|
2350
|
+
const authorizeUrl = buildOauthAuthorizeUrl({
|
|
2351
|
+
clientId,
|
|
2352
|
+
state,
|
|
2353
|
+
redirectUri,
|
|
2354
|
+
botScopes,
|
|
2355
|
+
userScopes,
|
|
2356
|
+
teamId,
|
|
2357
|
+
});
|
|
2358
|
+
|
|
2359
|
+
res.writeHead(302, { Location: authorizeUrl });
|
|
2360
|
+
res.end();
|
|
2166
2361
|
return;
|
|
2167
2362
|
}
|
|
2168
2363
|
|
|
2364
|
+
if (method === "GET" && requestUrl.pathname === callbackPath) {
|
|
2365
|
+
const receivedError = requestUrl.searchParams.get("error");
|
|
2366
|
+
if (receivedError) {
|
|
2367
|
+
sendText(res, 400, `Slack OAuth failed: ${receivedError}`);
|
|
2368
|
+
return;
|
|
2369
|
+
}
|
|
2370
|
+
|
|
2169
2371
|
const state = requestUrl.searchParams.get("state");
|
|
2170
2372
|
const code = requestUrl.searchParams.get("code");
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
|
|
2373
|
+
if (!state || !code) {
|
|
2374
|
+
sendText(res, 400, "Missing state/code.");
|
|
2375
|
+
return;
|
|
2376
|
+
}
|
|
2175
2377
|
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
|
|
2378
|
+
const claimToken = stateToClaim.get(state);
|
|
2379
|
+
if (claimToken) {
|
|
2380
|
+
const session = claimSessions.get(claimToken);
|
|
2381
|
+
if (!session || isClaimSessionExpired(session)) {
|
|
2382
|
+
sendText(res, 400, "Expired onboarding claim.");
|
|
2383
|
+
return;
|
|
2384
|
+
}
|
|
2385
|
+
|
|
2386
|
+
const oauthResponse = await exchangeOauthCode({ clientId, clientSecret, code, redirectUri });
|
|
2387
|
+
session.oauth_response = oauthResponse;
|
|
2388
|
+
stateToClaim.delete(state);
|
|
2389
|
+
sendText(res, 200, "Slack OAuth completed. Return to your CLI and wait for onboarding to finish.");
|
|
2390
|
+
return;
|
|
2391
|
+
}
|
|
2181
2392
|
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
|
|
2393
|
+
const pending = pendingStates.get(state);
|
|
2394
|
+
pendingStates.delete(state);
|
|
2395
|
+
if (!pending) {
|
|
2396
|
+
sendText(res, 400, "Invalid or expired OAuth state.");
|
|
2397
|
+
return;
|
|
2398
|
+
}
|
|
2399
|
+
if (Date.now() - pending.created_at > GATEWAY_STATE_TTL_MS) {
|
|
2400
|
+
sendText(res, 400, "Expired OAuth state.");
|
|
2401
|
+
return;
|
|
2402
|
+
}
|
|
2403
|
+
|
|
2404
|
+
const oauthResponse = await exchangeOauthCode({ clientId, clientSecret, code, redirectUri });
|
|
2405
|
+
const { key, profile } = upsertOauthProfile(oauthResponse, pending.profile_name);
|
|
2406
|
+
sendText(
|
|
2407
|
+
res,
|
|
2408
|
+
200,
|
|
2409
|
+
[
|
|
2410
|
+
"Slack OAuth authorization completed.",
|
|
2411
|
+
`Saved profile: ${profile.profile_name || key}`,
|
|
2412
|
+
`Profile key: ${key}`,
|
|
2413
|
+
"You can close this tab.",
|
|
2414
|
+
].join("\n")
|
|
2415
|
+
);
|
|
2185
2416
|
return;
|
|
2186
2417
|
}
|
|
2187
2418
|
|
|
2188
|
-
|
|
2189
|
-
session.oauth_response = oauthResponse;
|
|
2190
|
-
stateToClaim.delete(state);
|
|
2191
|
-
sendText(res, 200, "Slack OAuth completed. Return to your CLI and wait for onboarding to finish.");
|
|
2192
|
-
return;
|
|
2193
|
-
}
|
|
2194
|
-
|
|
2195
|
-
if (method === "GET" && requestUrl.pathname === "/onboard/claim") {
|
|
2419
|
+
if (method === "GET" && requestUrl.pathname === "/onboard/claim") {
|
|
2196
2420
|
const claimToken = String(requestUrl.searchParams.get("claim") || "");
|
|
2197
2421
|
const session = claimSessions.get(claimToken);
|
|
2198
2422
|
if (!session || isClaimSessionExpired(session)) {
|
|
@@ -2211,26 +2435,114 @@ async function runOnboardServerStart(args) {
|
|
|
2211
2435
|
profile: session.profile || "",
|
|
2212
2436
|
oauth_response: session.oauth_response,
|
|
2213
2437
|
});
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
|
|
2438
|
+
claimSessions.delete(claimToken);
|
|
2439
|
+
return;
|
|
2440
|
+
}
|
|
2217
2441
|
|
|
2218
|
-
|
|
2219
|
-
|
|
2220
|
-
|
|
2221
|
-
|
|
2222
|
-
|
|
2223
|
-
|
|
2224
|
-
|
|
2225
|
-
|
|
2442
|
+
if (method === "GET" && requestUrl.pathname === "/oauth/link") {
|
|
2443
|
+
const params = new URLSearchParams();
|
|
2444
|
+
const profile = requestUrl.searchParams.get("profile") || "";
|
|
2445
|
+
const team = requestUrl.searchParams.get("team") || "";
|
|
2446
|
+
const scope = requestUrl.searchParams.get("scope") || "";
|
|
2447
|
+
const userScope = requestUrl.searchParams.get("user_scope") || "";
|
|
2448
|
+
if (profile) params.set("profile", profile);
|
|
2449
|
+
if (team) params.set("team", team);
|
|
2450
|
+
if (scope) params.set("scope", scope);
|
|
2451
|
+
if (userScope) params.set("user_scope", userScope);
|
|
2452
|
+
sendJson(res, 200, {
|
|
2453
|
+
ok: true,
|
|
2454
|
+
url: `${publicBaseUrl}/oauth/start${params.toString() ? `?${params.toString()}` : ""}`,
|
|
2455
|
+
});
|
|
2456
|
+
return;
|
|
2457
|
+
}
|
|
2458
|
+
|
|
2459
|
+
if (method === "GET" && requestUrl.pathname === "/profiles") {
|
|
2460
|
+
if (!isGatewayAuthorized(req)) {
|
|
2461
|
+
sendJson(res, 401, { ok: false, error: "unauthorized" });
|
|
2462
|
+
return;
|
|
2463
|
+
}
|
|
2464
|
+
const tokenStore = loadTokenStore();
|
|
2465
|
+
sendJson(res, 200, {
|
|
2466
|
+
ok: true,
|
|
2467
|
+
default_profile: tokenStore.default_profile,
|
|
2468
|
+
profiles: profileSummariesFromStore(tokenStore),
|
|
2469
|
+
});
|
|
2470
|
+
return;
|
|
2471
|
+
}
|
|
2472
|
+
|
|
2473
|
+
if (method === "POST" && requestUrl.pathname === "/api/slack/call") {
|
|
2474
|
+
if (!isGatewayAuthorized(req)) {
|
|
2475
|
+
sendJson(res, 401, { ok: false, error: "unauthorized" });
|
|
2476
|
+
return;
|
|
2477
|
+
}
|
|
2478
|
+
|
|
2479
|
+
const payload = await readRequestJson(req, 1024 * 1024);
|
|
2480
|
+
const methodName = payload.method;
|
|
2481
|
+
if (!methodName || typeof methodName !== "string") {
|
|
2482
|
+
sendJson(res, 400, { ok: false, error: "missing_method" });
|
|
2483
|
+
return;
|
|
2484
|
+
}
|
|
2485
|
+
|
|
2486
|
+
const candidates = getSlackTokenCandidates(payload.token_override, {
|
|
2487
|
+
profileSelector:
|
|
2488
|
+
payload.profile_selector || process.env.SLACK_PROFILE || GATEWAY_PROFILE || undefined,
|
|
2489
|
+
preferredTokenType:
|
|
2490
|
+
payload.preferred_token_type || process.env.SLACK_DEFAULT_TOKEN_TYPE || undefined,
|
|
2491
|
+
});
|
|
2492
|
+
if (candidates.length === 0) {
|
|
2493
|
+
sendJson(res, 400, { ok: false, error: "missing_token" });
|
|
2494
|
+
return;
|
|
2495
|
+
}
|
|
2496
|
+
|
|
2497
|
+
const data = await callSlackApiWithCandidates(methodName, payload.params || {}, candidates);
|
|
2498
|
+
sendJson(res, 200, { ok: true, data });
|
|
2499
|
+
return;
|
|
2500
|
+
}
|
|
2501
|
+
|
|
2502
|
+
if (method === "POST" && requestUrl.pathname === "/api/slack/http") {
|
|
2503
|
+
if (!isGatewayAuthorized(req)) {
|
|
2504
|
+
sendJson(res, 401, { ok: false, error: "unauthorized" });
|
|
2505
|
+
return;
|
|
2506
|
+
}
|
|
2507
|
+
|
|
2508
|
+
const payload = await readRequestJson(req, 1024 * 1024);
|
|
2509
|
+
if (!payload.url || typeof payload.url !== "string") {
|
|
2510
|
+
sendJson(res, 400, { ok: false, error: "missing_url" });
|
|
2511
|
+
return;
|
|
2512
|
+
}
|
|
2513
|
+
|
|
2514
|
+
const data = await proxySlackHttpRequest(payload);
|
|
2515
|
+
sendJson(res, 200, { ok: true, data });
|
|
2516
|
+
return;
|
|
2517
|
+
}
|
|
2518
|
+
|
|
2519
|
+
sendJson(res, 404, { ok: false, error: "not_found" });
|
|
2520
|
+
} catch (error) {
|
|
2521
|
+
sendJson(res, 500, {
|
|
2522
|
+
ok: false,
|
|
2523
|
+
error: error instanceof Error ? error.message : String(error),
|
|
2524
|
+
slack_error: error?.slack_error || null,
|
|
2525
|
+
needed: error?.needed || null,
|
|
2526
|
+
provided: error?.provided || null,
|
|
2527
|
+
token_source: error?.token_source || null,
|
|
2528
|
+
});
|
|
2529
|
+
} finally {
|
|
2530
|
+
for (const [state, value] of pendingStates.entries()) {
|
|
2531
|
+
if (Date.now() - value.created_at > GATEWAY_STATE_TTL_MS) {
|
|
2532
|
+
pendingStates.delete(state);
|
|
2533
|
+
}
|
|
2534
|
+
}
|
|
2535
|
+
}
|
|
2536
|
+
});
|
|
2226
2537
|
|
|
2227
2538
|
await new Promise((resolve, reject) => {
|
|
2228
2539
|
server.once("error", reject);
|
|
2229
2540
|
server.listen(port, host, resolve);
|
|
2230
2541
|
});
|
|
2231
2542
|
|
|
2232
|
-
console.error(`[${SERVER_NAME}]
|
|
2233
|
-
console.error(`[${SERVER_NAME}]
|
|
2543
|
+
console.error(`[${SERVER_NAME}] gateway listening at http://${host}:${port} | public_base=${publicBaseUrl}`);
|
|
2544
|
+
console.error(`[${SERVER_NAME}] oauth start URL: ${publicBaseUrl}/oauth/start`);
|
|
2545
|
+
console.error(`[${SERVER_NAME}] profile list URL: ${publicBaseUrl}/profiles`);
|
|
2234
2546
|
console.error(`[${SERVER_NAME}] bootstrap URL: ${publicBaseUrl}/onboard/bootstrap`);
|
|
2235
2547
|
}
|
|
2236
2548
|
|
|
@@ -2266,6 +2578,24 @@ async function runOnboardServerCli(args) {
|
|
|
2266
2578
|
throw new Error(`Unknown onboard-server command: ${subcommand}`);
|
|
2267
2579
|
}
|
|
2268
2580
|
|
|
2581
|
+
async function runGatewayCompatCli(args) {
|
|
2582
|
+
const subcommand = (args[0] || "help").toLowerCase();
|
|
2583
|
+
if (subcommand === "help" || subcommand === "--help" || subcommand === "-h") {
|
|
2584
|
+
console.log("[gateway] deprecated alias detected. Use `onboard-server start` instead.");
|
|
2585
|
+
printOnboardServerHelp();
|
|
2586
|
+
return;
|
|
2587
|
+
}
|
|
2588
|
+
if (subcommand === "start") {
|
|
2589
|
+
console.error("[gateway] deprecated alias detected. Redirecting to `onboard-server start`.");
|
|
2590
|
+
await runOnboardServerStart(args.slice(1));
|
|
2591
|
+
return;
|
|
2592
|
+
}
|
|
2593
|
+
|
|
2594
|
+
throw new Error(
|
|
2595
|
+
`Unknown gateway command: ${subcommand}. Use 'slack-max-api-mcp onboard-server help' for available commands.`
|
|
2596
|
+
);
|
|
2597
|
+
}
|
|
2598
|
+
|
|
2269
2599
|
function loadCatalog() {
|
|
2270
2600
|
if (!fs.existsSync(CATALOG_PATH)) {
|
|
2271
2601
|
return { methods: [], scopes: [], totals: {} };
|
|
@@ -4483,12 +4813,17 @@ async function runEntryPoint() {
|
|
|
4483
4813
|
await runOnboardServerCli(rest);
|
|
4484
4814
|
return;
|
|
4485
4815
|
}
|
|
4816
|
+
if (command === "gateway") {
|
|
4817
|
+
await runGatewayCompatCli(rest);
|
|
4818
|
+
return;
|
|
4819
|
+
}
|
|
4486
4820
|
if (command === "help" || command === "--help" || command === "-h") {
|
|
4487
4821
|
console.log("Usage:");
|
|
4488
4822
|
console.log(" slack-max-api-mcp");
|
|
4489
4823
|
console.log(" slack-max-api-mcp oauth <login|list|use|current|help>");
|
|
4490
4824
|
console.log(" slack-max-api-mcp onboard <run|help>");
|
|
4491
4825
|
console.log(" slack-max-api-mcp onboard-server <start|help>");
|
|
4826
|
+
console.log(" slack-max-api-mcp gateway <start|help> # deprecated alias");
|
|
4492
4827
|
return;
|
|
4493
4828
|
}
|
|
4494
4829
|
if (command) {
|