@mgsoftwarebv/mg-dashboard-mcp 3.0.2 → 3.0.4
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/dist/index.js +336 -7
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1016,7 +1016,226 @@ var RateLimiter = class {
|
|
|
1016
1016
|
var authRateLimiter = new RateLimiter(5, 15 * 60 * 1e3);
|
|
1017
1017
|
setInterval(() => {
|
|
1018
1018
|
authRateLimiter.cleanup();
|
|
1019
|
+
toolRateLimiter.cleanup();
|
|
1019
1020
|
}, 5 * 60 * 1e3).unref();
|
|
1021
|
+
var TOOL_RATE_CATEGORIES = {
|
|
1022
|
+
"ssh-execute": "ssh",
|
|
1023
|
+
"sftp-write": "sftp_write",
|
|
1024
|
+
"sftp-delete": "sftp_write",
|
|
1025
|
+
"sftp-list": "sftp_read",
|
|
1026
|
+
"sftp-read": "sftp_read",
|
|
1027
|
+
"db-query": "db",
|
|
1028
|
+
"db-discover": "db",
|
|
1029
|
+
"db-tables": "db",
|
|
1030
|
+
"db-describe": "db"
|
|
1031
|
+
};
|
|
1032
|
+
var TOOL_RATE_LIMITS = {
|
|
1033
|
+
ssh: { max: 120, windowMs: 60 * 60 * 1e3 },
|
|
1034
|
+
sftp_write: { max: 60, windowMs: 60 * 60 * 1e3 },
|
|
1035
|
+
sftp_read: { max: 200, windowMs: 60 * 60 * 1e3 },
|
|
1036
|
+
db: { max: 200, windowMs: 60 * 60 * 1e3 },
|
|
1037
|
+
default: { max: 300, windowMs: 60 * 60 * 1e3 }
|
|
1038
|
+
};
|
|
1039
|
+
var toolRateLimiter = new RateLimiter(300, 60 * 60 * 1e3);
|
|
1040
|
+
var toolRateBuckets = /* @__PURE__ */ new Map();
|
|
1041
|
+
function checkToolRateLimit(toolName) {
|
|
1042
|
+
const category = TOOL_RATE_CATEGORIES[toolName] || "default";
|
|
1043
|
+
const limits = TOOL_RATE_LIMITS[category] ?? TOOL_RATE_LIMITS.default;
|
|
1044
|
+
let limiter = toolRateBuckets.get(category);
|
|
1045
|
+
if (!limiter) {
|
|
1046
|
+
limiter = new RateLimiter(limits.max, limits.windowMs);
|
|
1047
|
+
toolRateBuckets.set(category, limiter);
|
|
1048
|
+
}
|
|
1049
|
+
const result = limiter.check(authContext?.apiKeyId || "unknown");
|
|
1050
|
+
return { allowed: result.allowed, category, retryAfterMs: result.retryAfterMs };
|
|
1051
|
+
}
|
|
1052
|
+
var SENSITIVE_KEYS = /* @__PURE__ */ new Set(["password", "private_key", "passphrase", "secret", "token", "key", "api_key", "credentials"]);
|
|
1053
|
+
function redactSensitiveArgs(args2) {
|
|
1054
|
+
const redacted = {};
|
|
1055
|
+
for (const [k, v] of Object.entries(args2)) {
|
|
1056
|
+
if (SENSITIVE_KEYS.has(k.toLowerCase())) {
|
|
1057
|
+
redacted[k] = "[REDACTED]";
|
|
1058
|
+
} else if (typeof v === "string" && v.length > 500) {
|
|
1059
|
+
redacted[k] = v.slice(0, 500) + "...[truncated]";
|
|
1060
|
+
} else {
|
|
1061
|
+
redacted[k] = v;
|
|
1062
|
+
}
|
|
1063
|
+
}
|
|
1064
|
+
return redacted;
|
|
1065
|
+
}
|
|
1066
|
+
async function writeAuditLog(entry) {
|
|
1067
|
+
if (!authContext) return;
|
|
1068
|
+
try {
|
|
1069
|
+
await supabase.from("mcp_audit_log").insert({
|
|
1070
|
+
api_key_id: authContext.apiKeyId,
|
|
1071
|
+
user_id: authContext.userId,
|
|
1072
|
+
tool_name: entry.toolName,
|
|
1073
|
+
arguments: entry.arguments ? redactSensitiveArgs(entry.arguments) : null,
|
|
1074
|
+
ip_address: entry.ipAddress || null,
|
|
1075
|
+
server_id: entry.serverId || null,
|
|
1076
|
+
result_status: entry.resultStatus,
|
|
1077
|
+
error_message: entry.errorMessage?.slice(0, 1e3) || null,
|
|
1078
|
+
duration_ms: entry.durationMs || null
|
|
1079
|
+
});
|
|
1080
|
+
} catch (err) {
|
|
1081
|
+
console.error("[Audit] Failed to write audit log:", err instanceof Error ? err.message : err);
|
|
1082
|
+
}
|
|
1083
|
+
}
|
|
1084
|
+
var TELEGRAM_API = "https://api.telegram.org/bot";
|
|
1085
|
+
var cachedTelegramConfig = null;
|
|
1086
|
+
async function getTelegramBotConfig() {
|
|
1087
|
+
if (cachedTelegramConfig) return cachedTelegramConfig;
|
|
1088
|
+
const { data, error } = await supabase.from("telegram_bot_config").select("bot_token_encrypted, chat_id, notifications_enabled").limit(1).maybeSingle();
|
|
1089
|
+
if (error) {
|
|
1090
|
+
console.error("[Telegram Config] Query error:", error.message);
|
|
1091
|
+
return null;
|
|
1092
|
+
}
|
|
1093
|
+
if (!data?.bot_token_encrypted || !data?.chat_id) {
|
|
1094
|
+
console.error("[Telegram Config] Missing bot_token or chat_id in telegram_bot_config table");
|
|
1095
|
+
return null;
|
|
1096
|
+
}
|
|
1097
|
+
if (!data.notifications_enabled) {
|
|
1098
|
+
console.error("[Telegram Config] Notifications disabled in telegram_bot_config");
|
|
1099
|
+
return null;
|
|
1100
|
+
}
|
|
1101
|
+
try {
|
|
1102
|
+
const token = decrypt(data.bot_token_encrypted);
|
|
1103
|
+
cachedTelegramConfig = { botToken: token, chatId: data.chat_id };
|
|
1104
|
+
console.error("[Telegram Config] Loaded successfully");
|
|
1105
|
+
return cachedTelegramConfig;
|
|
1106
|
+
} catch (err) {
|
|
1107
|
+
console.error("[Telegram Config] Failed to decrypt bot token:", err instanceof Error ? err.message : err);
|
|
1108
|
+
return null;
|
|
1109
|
+
}
|
|
1110
|
+
}
|
|
1111
|
+
function escapeHtmlTg(str) {
|
|
1112
|
+
return str.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
1113
|
+
}
|
|
1114
|
+
async function sendTelegramMessage(token, chatId, text, replyMarkup) {
|
|
1115
|
+
try {
|
|
1116
|
+
const body = { chat_id: chatId, text, parse_mode: "HTML" };
|
|
1117
|
+
if (replyMarkup) body.reply_markup = replyMarkup;
|
|
1118
|
+
const res = await fetch(`${TELEGRAM_API}${token}/sendMessage`, {
|
|
1119
|
+
method: "POST",
|
|
1120
|
+
headers: { "Content-Type": "application/json" },
|
|
1121
|
+
body: JSON.stringify(body)
|
|
1122
|
+
});
|
|
1123
|
+
const result = await res.json();
|
|
1124
|
+
return { ok: result.ok, messageId: result.result?.message_id };
|
|
1125
|
+
} catch {
|
|
1126
|
+
return { ok: false };
|
|
1127
|
+
}
|
|
1128
|
+
}
|
|
1129
|
+
async function sendTelegramAlert(text) {
|
|
1130
|
+
const config = await getTelegramBotConfig();
|
|
1131
|
+
if (!config) return;
|
|
1132
|
+
await sendTelegramMessage(config.botToken, config.chatId, text);
|
|
1133
|
+
}
|
|
1134
|
+
async function checkIpAllowlist(ipAddress) {
|
|
1135
|
+
if (!authContext || !authContext.requireIpApproval) {
|
|
1136
|
+
console.error(`[IP Check] Skipped (requireIpApproval=${authContext?.requireIpApproval})`);
|
|
1137
|
+
return { allowed: true };
|
|
1138
|
+
}
|
|
1139
|
+
console.error(`[IP Check] Checking IP ${ipAddress} for key ${authContext.apiKeyId}`);
|
|
1140
|
+
const { data: existing, error: lookupErr } = await supabase.from("mcp_ip_allowlist").select("id").eq("api_key_id", authContext.apiKeyId).eq("ip_address", ipAddress).maybeSingle();
|
|
1141
|
+
if (lookupErr) {
|
|
1142
|
+
console.error(`[IP Check] Allowlist lookup error: ${lookupErr.message}`);
|
|
1143
|
+
}
|
|
1144
|
+
if (existing) {
|
|
1145
|
+
console.error(`[IP Check] IP ${ipAddress} found in allowlist`);
|
|
1146
|
+
return { allowed: true };
|
|
1147
|
+
}
|
|
1148
|
+
console.error(`[IP Check] IP ${ipAddress} NOT in allowlist, requesting Telegram approval...`);
|
|
1149
|
+
const tgConfig = await getTelegramBotConfig();
|
|
1150
|
+
if (!tgConfig) {
|
|
1151
|
+
console.error("[IP Check] BLOCKING: Unknown IP and no Telegram configured");
|
|
1152
|
+
return { allowed: false, reason: "Unknown IP and no approval channel configured" };
|
|
1153
|
+
}
|
|
1154
|
+
let geoInfo = null;
|
|
1155
|
+
try {
|
|
1156
|
+
const geoRes = await fetch(`http://ip-api.com/json/${ipAddress}?fields=country,city,isp`);
|
|
1157
|
+
if (geoRes.ok) geoInfo = await geoRes.json();
|
|
1158
|
+
} catch {
|
|
1159
|
+
}
|
|
1160
|
+
const geoLabel = geoInfo ? `${geoInfo.city || "?"}, ${geoInfo.country || "?"} (${geoInfo.isp || "?"})` : "Onbekend";
|
|
1161
|
+
const { data: request, error: insertErr } = await supabase.from("mcp_ip_approval_request").insert({
|
|
1162
|
+
api_key_id: authContext.apiKeyId,
|
|
1163
|
+
ip_address: ipAddress,
|
|
1164
|
+
status: "pending",
|
|
1165
|
+
geo_info: geoInfo
|
|
1166
|
+
}).select("id").single();
|
|
1167
|
+
if (insertErr || !request) {
|
|
1168
|
+
console.error(`[IP Check] Failed to create approval request: ${insertErr?.message || "no data returned"}`);
|
|
1169
|
+
return { allowed: false, reason: `Failed to create approval request: ${insertErr?.message || "unknown"}` };
|
|
1170
|
+
}
|
|
1171
|
+
console.error(`[IP Check] Created approval request ${request.id}`);
|
|
1172
|
+
const message = [
|
|
1173
|
+
`<b>MCP IP Approval</b>`,
|
|
1174
|
+
``,
|
|
1175
|
+
`<b>IP:</b> <code>${escapeHtmlTg(ipAddress)}</code>`,
|
|
1176
|
+
`<b>Locatie:</b> ${escapeHtmlTg(geoLabel)}`,
|
|
1177
|
+
`<b>API Key:</b> ${escapeHtmlTg(authContext.apiKeyName)}`,
|
|
1178
|
+
`<b>User:</b> ${escapeHtmlTg(authContext.userId)}`,
|
|
1179
|
+
``,
|
|
1180
|
+
`<i>Onbekend IP probeert verbinding te maken met de MCP server.</i>`
|
|
1181
|
+
].join("\n");
|
|
1182
|
+
const tgResult = await sendTelegramMessage(tgConfig.botToken, tgConfig.chatId, message, {
|
|
1183
|
+
inline_keyboard: [
|
|
1184
|
+
[
|
|
1185
|
+
{ text: "Goedkeuren", callback_data: `mcp_ip_approve:${request.id}` },
|
|
1186
|
+
{ text: "Weigeren", callback_data: `mcp_ip_deny:${request.id}` }
|
|
1187
|
+
]
|
|
1188
|
+
]
|
|
1189
|
+
});
|
|
1190
|
+
if (tgResult.messageId) {
|
|
1191
|
+
await supabase.from("mcp_ip_approval_request").update({ telegram_message_id: tgResult.messageId }).eq("id", request.id);
|
|
1192
|
+
}
|
|
1193
|
+
console.error(`[IP Check] Waiting for Telegram approval for IP ${ipAddress}...`);
|
|
1194
|
+
const deadline = Date.now() + 6e4;
|
|
1195
|
+
while (Date.now() < deadline) {
|
|
1196
|
+
await new Promise((r) => setTimeout(r, 2e3));
|
|
1197
|
+
const { data: check } = await supabase.from("mcp_ip_approval_request").select("status").eq("id", request.id).single();
|
|
1198
|
+
if (check?.status === "approved") {
|
|
1199
|
+
await supabase.from("mcp_ip_allowlist").insert({
|
|
1200
|
+
api_key_id: authContext.apiKeyId,
|
|
1201
|
+
ip_address: ipAddress,
|
|
1202
|
+
label: geoLabel,
|
|
1203
|
+
approved_by: "telegram"
|
|
1204
|
+
});
|
|
1205
|
+
console.error(`[IP Check] IP ${ipAddress} approved via Telegram`);
|
|
1206
|
+
return { allowed: true };
|
|
1207
|
+
}
|
|
1208
|
+
if (check?.status === "denied") {
|
|
1209
|
+
console.error(`[IP Check] IP ${ipAddress} denied via Telegram`);
|
|
1210
|
+
return { allowed: false, reason: "IP denied via Telegram" };
|
|
1211
|
+
}
|
|
1212
|
+
}
|
|
1213
|
+
await supabase.from("mcp_ip_approval_request").update({ status: "denied", resolved_at: (/* @__PURE__ */ new Date()).toISOString() }).eq("id", request.id);
|
|
1214
|
+
console.error(`[IP Check] IP ${ipAddress} approval timed out`);
|
|
1215
|
+
return { allowed: false, reason: "Approval request timed out (60s)" };
|
|
1216
|
+
}
|
|
1217
|
+
var SESSION_TTL_MS = 8 * 60 * 60 * 1e3;
|
|
1218
|
+
var SESSION_MAX_ABSOLUTE_MS = 24 * 60 * 60 * 1e3;
|
|
1219
|
+
async function getOrCreateSession(ipAddress) {
|
|
1220
|
+
if (!authContext) return { valid: false, reason: "Not authenticated" };
|
|
1221
|
+
const now = /* @__PURE__ */ new Date();
|
|
1222
|
+
const { data: existing } = await supabase.from("mcp_session").select("id, created_at, expires_at").eq("api_key_id", authContext.apiKeyId).eq("ip_address", ipAddress).gt("expires_at", now.toISOString()).order("created_at", { ascending: false }).limit(1).maybeSingle();
|
|
1223
|
+
if (existing) {
|
|
1224
|
+
const createdAt = new Date(existing.created_at).getTime();
|
|
1225
|
+
if (now.getTime() - createdAt > SESSION_MAX_ABSOLUTE_MS) {
|
|
1226
|
+
await supabase.from("mcp_session").delete().eq("id", existing.id);
|
|
1227
|
+
return { valid: false, reason: "Session exceeded absolute TTL (24h)" };
|
|
1228
|
+
}
|
|
1229
|
+
await supabase.from("mcp_session").update({ last_activity_at: now.toISOString() }).eq("id", existing.id);
|
|
1230
|
+
return { valid: true, sessionId: existing.id };
|
|
1231
|
+
}
|
|
1232
|
+
const { data: newSession } = await supabase.from("mcp_session").insert({
|
|
1233
|
+
api_key_id: authContext.apiKeyId,
|
|
1234
|
+
ip_address: ipAddress,
|
|
1235
|
+
expires_at: new Date(now.getTime() + SESSION_TTL_MS).toISOString()
|
|
1236
|
+
}).select("id").single();
|
|
1237
|
+
return { valid: true, sessionId: newSession?.id };
|
|
1238
|
+
}
|
|
1020
1239
|
var MODULE_KEYS = [
|
|
1021
1240
|
"users",
|
|
1022
1241
|
"ssh_servers",
|
|
@@ -1102,7 +1321,7 @@ async function validateApiKey(key) {
|
|
|
1102
1321
|
console.error(`Rate limited: too many failed auth attempts. Retry in ${retryMin} minute(s).`);
|
|
1103
1322
|
return null;
|
|
1104
1323
|
}
|
|
1105
|
-
const { data, error } = await supabase.from("dashboard_mcp_api_key").select("id, created_by, allowed_server_ids, is_active, expires_at").eq("api_key_hash", keyHash).eq("is_active", true).single();
|
|
1324
|
+
const { data, error } = await supabase.from("dashboard_mcp_api_key").select("id, name, created_by, allowed_server_ids, is_active, expires_at, require_ip_approval").eq("api_key_hash", keyHash).eq("is_active", true).single();
|
|
1106
1325
|
if (error || !data) {
|
|
1107
1326
|
console.error(`API key not found or inactive (${rateCheck.remaining} attempts remaining)`);
|
|
1108
1327
|
return null;
|
|
@@ -1128,10 +1347,13 @@ async function validateApiKey(key) {
|
|
|
1128
1347
|
const moduleCount = MODULE_KEYS.filter((k) => permissions.modules[k]).length;
|
|
1129
1348
|
console.error(`Authenticated as user ${data.created_by} (role: ${roleName}, modules: ${moduleCount}/${MODULE_KEYS.length})`);
|
|
1130
1349
|
return {
|
|
1350
|
+
apiKeyId: data.id,
|
|
1351
|
+
apiKeyName: data.name || "Unknown",
|
|
1131
1352
|
userId: data.created_by,
|
|
1132
1353
|
allowedServerIds,
|
|
1133
1354
|
permissions,
|
|
1134
|
-
roleName
|
|
1355
|
+
roleName,
|
|
1356
|
+
requireIpApproval: data.require_ip_approval ?? true
|
|
1135
1357
|
};
|
|
1136
1358
|
}
|
|
1137
1359
|
function assertServerAccess(serverId) {
|
|
@@ -2254,7 +2476,7 @@ var TOOLS = [
|
|
|
2254
2476
|
// ----- Agent Reporting -----
|
|
2255
2477
|
...AGENT_TOOLS
|
|
2256
2478
|
];
|
|
2257
|
-
var MCP_VERSION = "
|
|
2479
|
+
var MCP_VERSION = "3.0.4";
|
|
2258
2480
|
async function handleListTools() {
|
|
2259
2481
|
if (!authContext) return { tools: TOOLS };
|
|
2260
2482
|
const accessible = TOOLS.filter((tool) => {
|
|
@@ -2279,19 +2501,54 @@ async function handleCallTool(request) {
|
|
|
2279
2501
|
}]
|
|
2280
2502
|
};
|
|
2281
2503
|
}
|
|
2504
|
+
const rateResult = checkToolRateLimit(name);
|
|
2505
|
+
if (!rateResult.allowed) {
|
|
2506
|
+
const retryMin = Math.ceil(rateResult.retryAfterMs / 6e4);
|
|
2507
|
+
void sendTelegramAlert(
|
|
2508
|
+
`<b>MCP Rate Limit</b>
|
|
2509
|
+
|
|
2510
|
+
Tool category <code>${escapeHtmlTg(rateResult.category)}</code> rate limited.
|
|
2511
|
+
Key: ${escapeHtmlTg(authContext.apiKeyName)}
|
|
2512
|
+
Retry in ${retryMin} min.`
|
|
2513
|
+
);
|
|
2514
|
+
return {
|
|
2515
|
+
content: [{
|
|
2516
|
+
type: "text",
|
|
2517
|
+
text: `Rate limited: too many ${rateResult.category} calls. Retry in ${retryMin} minute(s).`
|
|
2518
|
+
}]
|
|
2519
|
+
};
|
|
2520
|
+
}
|
|
2521
|
+
const startTime = Date.now();
|
|
2522
|
+
const serverId = a.serverId || a.server_id;
|
|
2523
|
+
const result = await executeToolCall(name, a);
|
|
2524
|
+
const durationMs = Date.now() - startTime;
|
|
2525
|
+
const isError = result.content?.[0]?.text?.startsWith("Error:");
|
|
2526
|
+
void writeAuditLog({
|
|
2527
|
+
toolName: name,
|
|
2528
|
+
arguments: a,
|
|
2529
|
+
serverId,
|
|
2530
|
+
resultStatus: isError ? "error" : "success",
|
|
2531
|
+
errorMessage: isError ? result.content?.[0]?.text : void 0,
|
|
2532
|
+
durationMs
|
|
2533
|
+
});
|
|
2534
|
+
return result;
|
|
2535
|
+
}
|
|
2536
|
+
async function executeToolCall(name, a, _serverId) {
|
|
2537
|
+
const ctx = authContext;
|
|
2282
2538
|
try {
|
|
2283
2539
|
switch (name) {
|
|
2284
2540
|
// ----- Servers -----
|
|
2285
2541
|
case "list-servers": {
|
|
2286
|
-
let query = supabase.from("ssh_server").select("id, name, hostname, port, username, tags, hosted_by, created_at").order("name");
|
|
2287
|
-
if (
|
|
2288
|
-
query = query.in("id",
|
|
2542
|
+
let query = supabase.from("ssh_server").select("id, name, hostname, port, username, tags, hosted_by, os_type, created_at").order("name");
|
|
2543
|
+
if (ctx.allowedServerIds !== null) {
|
|
2544
|
+
query = query.in("id", ctx.allowedServerIds);
|
|
2289
2545
|
}
|
|
2290
2546
|
const { data, error } = await query;
|
|
2291
2547
|
if (error) throw new Error(error.message);
|
|
2292
2548
|
const lines = (data || []).map((s) => {
|
|
2293
2549
|
const tags = Array.isArray(s.tags) ? s.tags.join(", ") : "";
|
|
2294
|
-
|
|
2550
|
+
const os = s.os_type || "linux";
|
|
2551
|
+
return `${s.id} ${s.name} ${s.hostname}:${s.port} ${s.username} [${tags}] ${s.hosted_by || ""} os:${os}`;
|
|
2295
2552
|
});
|
|
2296
2553
|
return { content: [{ type: "text", text: lines.length ? lines.join("\n") : "No servers found" }] };
|
|
2297
2554
|
}
|
|
@@ -2738,6 +2995,32 @@ function createMcpServer() {
|
|
|
2738
2995
|
return s;
|
|
2739
2996
|
}
|
|
2740
2997
|
var server = createMcpServer();
|
|
2998
|
+
async function resolvePublicIp() {
|
|
2999
|
+
const services = [
|
|
3000
|
+
"https://api.ipify.org",
|
|
3001
|
+
"https://ifconfig.me/ip",
|
|
3002
|
+
"https://icanhazip.com"
|
|
3003
|
+
];
|
|
3004
|
+
for (const url of services) {
|
|
3005
|
+
try {
|
|
3006
|
+
const controller = new AbortController();
|
|
3007
|
+
const timer = setTimeout(() => controller.abort(), 5e3);
|
|
3008
|
+
const res = await fetch(url, { signal: controller.signal });
|
|
3009
|
+
clearTimeout(timer);
|
|
3010
|
+
if (res.ok) {
|
|
3011
|
+
const ip = (await res.text()).trim();
|
|
3012
|
+
if (ip && /^[\d.:a-f]+$/i.test(ip)) {
|
|
3013
|
+
console.error(`[IP Resolve] Got public IP: ${ip} (from ${url})`);
|
|
3014
|
+
return ip;
|
|
3015
|
+
}
|
|
3016
|
+
}
|
|
3017
|
+
} catch (err) {
|
|
3018
|
+
console.error(`[IP Resolve] Failed ${url}: ${err instanceof Error ? err.message : err}`);
|
|
3019
|
+
}
|
|
3020
|
+
}
|
|
3021
|
+
console.error("[IP Resolve] All services failed, returning unknown");
|
|
3022
|
+
return "unknown";
|
|
3023
|
+
}
|
|
2741
3024
|
async function main() {
|
|
2742
3025
|
console.error("Starting MG Dashboard MCP Server...");
|
|
2743
3026
|
authContext = await validateApiKey(apiKey);
|
|
@@ -2745,6 +3028,23 @@ async function main() {
|
|
|
2745
3028
|
console.error("API key validation failed");
|
|
2746
3029
|
process.exit(1);
|
|
2747
3030
|
}
|
|
3031
|
+
console.error(`[Security] MCP v${MCP_VERSION} | Key: ${authContext.apiKeyName} | requireIpApproval: ${authContext.requireIpApproval}`);
|
|
3032
|
+
if (authContext.requireIpApproval) {
|
|
3033
|
+
console.error("[Security] IP approval enabled, resolving public IP...");
|
|
3034
|
+
const publicIp = await resolvePublicIp();
|
|
3035
|
+
console.error(`[Security] Public IP: ${publicIp}`);
|
|
3036
|
+
const ipResult = await checkIpAllowlist(publicIp);
|
|
3037
|
+
if (!ipResult.allowed) {
|
|
3038
|
+
console.error(`[Security] BLOCKED \u2014 ${ipResult.reason}`);
|
|
3039
|
+
console.error("[Security] MCP server will not start. Approve the IP via Telegram or add it manually in the dashboard.");
|
|
3040
|
+
process.exit(1);
|
|
3041
|
+
}
|
|
3042
|
+
console.error("[Security] IP approved, creating session...");
|
|
3043
|
+
await getOrCreateSession(publicIp);
|
|
3044
|
+
console.error("[Security] Session created");
|
|
3045
|
+
} else {
|
|
3046
|
+
console.error("[Security] IP approval disabled for this key");
|
|
3047
|
+
}
|
|
2748
3048
|
const toolNames = TOOLS.map((t) => t.name).join(", ");
|
|
2749
3049
|
if (httpMode) {
|
|
2750
3050
|
console.error(`API key validated. Starting Streamable HTTP transport on port ${httpPort}...`);
|
|
@@ -2755,8 +3055,37 @@ async function main() {
|
|
|
2755
3055
|
};
|
|
2756
3056
|
const httpServer = createServer(async (req, res) => {
|
|
2757
3057
|
const url = new URL(req.url ?? "/", `http://localhost:${httpPort}`);
|
|
3058
|
+
const clientIp = (req.headers["x-forwarded-for"]?.split(",")[0]?.trim() || req.headers["x-real-ip"] || req.socket.remoteAddress || "unknown").replace(/^::ffff:/, "");
|
|
3059
|
+
if (authContext?.requireIpApproval) {
|
|
3060
|
+
const { data: allowed } = await supabase.from("mcp_ip_allowlist").select("id").eq("api_key_id", authContext.apiKeyId).eq("ip_address", clientIp).maybeSingle();
|
|
3061
|
+
if (!allowed) {
|
|
3062
|
+
res.writeHead(403, { "Content-Type": "application/json" });
|
|
3063
|
+
res.end(JSON.stringify({ error: `IP ${clientIp} not in allowlist. Approve via Telegram first.` }));
|
|
3064
|
+
return;
|
|
3065
|
+
}
|
|
3066
|
+
}
|
|
3067
|
+
if (authContext) {
|
|
3068
|
+
const sessionResult = await getOrCreateSession(clientIp);
|
|
3069
|
+
if (!sessionResult.valid) {
|
|
3070
|
+
console.error(`[Session] Session expired: ${sessionResult.reason}`);
|
|
3071
|
+
res.writeHead(401, { "Content-Type": "application/json" });
|
|
3072
|
+
res.end(JSON.stringify({ error: `Session expired: ${sessionResult.reason}. Restart the MCP server to re-authenticate.` }));
|
|
3073
|
+
return;
|
|
3074
|
+
}
|
|
3075
|
+
}
|
|
2758
3076
|
const restToolName = REST_TOOL_MAP[url.pathname];
|
|
2759
3077
|
if (restToolName && req.method === "POST") {
|
|
3078
|
+
if (!authContext) {
|
|
3079
|
+
res.writeHead(401, { "Content-Type": "application/json" });
|
|
3080
|
+
res.end(JSON.stringify({ error: "Not authenticated" }));
|
|
3081
|
+
return;
|
|
3082
|
+
}
|
|
3083
|
+
const requiredModule = TOOL_MODULE_MAP[restToolName];
|
|
3084
|
+
if (requiredModule && authContext.permissions.modules[requiredModule] !== true) {
|
|
3085
|
+
res.writeHead(403, { "Content-Type": "application/json" });
|
|
3086
|
+
res.end(JSON.stringify({ error: `Access denied: no permission for "${requiredModule}" module` }));
|
|
3087
|
+
return;
|
|
3088
|
+
}
|
|
2760
3089
|
const chunks = [];
|
|
2761
3090
|
for await (const chunk of req) chunks.push(chunk);
|
|
2762
3091
|
let toolArgs;
|