@mgsoftwarebv/mg-dashboard-mcp 3.0.3 → 3.0.5

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 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, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
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) {
@@ -1975,6 +2197,11 @@ async function mijnhostFetch(path, options = {}) {
1975
2197
  return body;
1976
2198
  }
1977
2199
  var TOOLS = [
2200
+ {
2201
+ name: "mcp-debug",
2202
+ description: "Returns MCP server version, security state, and diagnostics. Use to verify which version is running.",
2203
+ inputSchema: { type: "object", properties: {}, required: [] }
2204
+ },
1978
2205
  {
1979
2206
  name: "list-servers",
1980
2207
  description: "List all SSH servers you have access to. Returns id, name, hostname, and tags for each server.",
@@ -2254,7 +2481,7 @@ var TOOLS = [
2254
2481
  // ----- Agent Reporting -----
2255
2482
  ...AGENT_TOOLS
2256
2483
  ];
2257
- var MCP_VERSION = "2.3.1";
2484
+ var MCP_VERSION = "3.0.5";
2258
2485
  async function handleListTools() {
2259
2486
  if (!authContext) return { tools: TOOLS };
2260
2487
  const accessible = TOOLS.filter((tool) => {
@@ -2279,19 +2506,85 @@ async function handleCallTool(request) {
2279
2506
  }]
2280
2507
  };
2281
2508
  }
2509
+ const rateResult = checkToolRateLimit(name);
2510
+ if (!rateResult.allowed) {
2511
+ const retryMin = Math.ceil(rateResult.retryAfterMs / 6e4);
2512
+ void sendTelegramAlert(
2513
+ `<b>MCP Rate Limit</b>
2514
+
2515
+ Tool category <code>${escapeHtmlTg(rateResult.category)}</code> rate limited.
2516
+ Key: ${escapeHtmlTg(authContext.apiKeyName)}
2517
+ Retry in ${retryMin} min.`
2518
+ );
2519
+ return {
2520
+ content: [{
2521
+ type: "text",
2522
+ text: `Rate limited: too many ${rateResult.category} calls. Retry in ${retryMin} minute(s).`
2523
+ }]
2524
+ };
2525
+ }
2526
+ const startTime = Date.now();
2527
+ const serverId = a.serverId || a.server_id;
2528
+ const result = await executeToolCall(name, a);
2529
+ const durationMs = Date.now() - startTime;
2530
+ const isError = result.content?.[0]?.text?.startsWith("Error:");
2531
+ void writeAuditLog({
2532
+ toolName: name,
2533
+ arguments: a,
2534
+ serverId,
2535
+ resultStatus: isError ? "error" : "success",
2536
+ errorMessage: isError ? result.content?.[0]?.text : void 0,
2537
+ durationMs
2538
+ });
2539
+ return result;
2540
+ }
2541
+ async function executeToolCall(name, a, _serverId) {
2542
+ const ctx = authContext;
2282
2543
  try {
2283
2544
  switch (name) {
2545
+ // ----- Debug -----
2546
+ case "mcp-debug": {
2547
+ const { count: auditCount } = await supabase.from("mcp_audit_log").select("*", { count: "exact", head: true });
2548
+ const { count: sessionCount } = await supabase.from("mcp_session").select("*", { count: "exact", head: true });
2549
+ const { count: allowlistCount } = await supabase.from("mcp_ip_allowlist").select("*", { count: "exact", head: true });
2550
+ const { count: approvalCount } = await supabase.from("mcp_ip_approval_request").select("*", { count: "exact", head: true });
2551
+ let publicIp = "not resolved";
2552
+ try {
2553
+ publicIp = await resolvePublicIp();
2554
+ } catch {
2555
+ }
2556
+ const debug = {
2557
+ version: MCP_VERSION,
2558
+ nodeVersion: process.version,
2559
+ httpMode,
2560
+ apiKey: {
2561
+ id: ctx.apiKeyId,
2562
+ name: ctx.apiKeyName,
2563
+ requireIpApproval: ctx.requireIpApproval
2564
+ },
2565
+ publicIp,
2566
+ tables: {
2567
+ mcp_audit_log: auditCount ?? 0,
2568
+ mcp_session: sessionCount ?? 0,
2569
+ mcp_ip_allowlist: allowlistCount ?? 0,
2570
+ mcp_ip_approval_request: approvalCount ?? 0
2571
+ },
2572
+ uptime: `${Math.round(process.uptime())}s`
2573
+ };
2574
+ return { content: [{ type: "text", text: JSON.stringify(debug, null, 2) }] };
2575
+ }
2284
2576
  // ----- Servers -----
2285
2577
  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 (authContext.allowedServerIds !== null) {
2288
- query = query.in("id", authContext.allowedServerIds);
2578
+ let query = supabase.from("ssh_server").select("id, name, hostname, port, username, tags, hosted_by, os_type, created_at").order("name");
2579
+ if (ctx.allowedServerIds !== null) {
2580
+ query = query.in("id", ctx.allowedServerIds);
2289
2581
  }
2290
2582
  const { data, error } = await query;
2291
2583
  if (error) throw new Error(error.message);
2292
2584
  const lines = (data || []).map((s) => {
2293
2585
  const tags = Array.isArray(s.tags) ? s.tags.join(", ") : "";
2294
- return `${s.id} ${s.name} ${s.hostname}:${s.port} ${s.username} [${tags}] ${s.hosted_by || ""}`;
2586
+ const os = s.os_type || "linux";
2587
+ return `${s.id} ${s.name} ${s.hostname}:${s.port} ${s.username} [${tags}] ${s.hosted_by || ""} os:${os}`;
2295
2588
  });
2296
2589
  return { content: [{ type: "text", text: lines.length ? lines.join("\n") : "No servers found" }] };
2297
2590
  }
@@ -2738,6 +3031,32 @@ function createMcpServer() {
2738
3031
  return s;
2739
3032
  }
2740
3033
  var server = createMcpServer();
3034
+ async function resolvePublicIp() {
3035
+ const services = [
3036
+ "https://api.ipify.org",
3037
+ "https://ifconfig.me/ip",
3038
+ "https://icanhazip.com"
3039
+ ];
3040
+ for (const url of services) {
3041
+ try {
3042
+ const controller = new AbortController();
3043
+ const timer = setTimeout(() => controller.abort(), 5e3);
3044
+ const res = await fetch(url, { signal: controller.signal });
3045
+ clearTimeout(timer);
3046
+ if (res.ok) {
3047
+ const ip = (await res.text()).trim();
3048
+ if (ip && /^[\d.:a-f]+$/i.test(ip)) {
3049
+ console.error(`[IP Resolve] Got public IP: ${ip} (from ${url})`);
3050
+ return ip;
3051
+ }
3052
+ }
3053
+ } catch (err) {
3054
+ console.error(`[IP Resolve] Failed ${url}: ${err instanceof Error ? err.message : err}`);
3055
+ }
3056
+ }
3057
+ console.error("[IP Resolve] All services failed, returning unknown");
3058
+ return "unknown";
3059
+ }
2741
3060
  async function main() {
2742
3061
  console.error("Starting MG Dashboard MCP Server...");
2743
3062
  authContext = await validateApiKey(apiKey);
@@ -2745,6 +3064,23 @@ async function main() {
2745
3064
  console.error("API key validation failed");
2746
3065
  process.exit(1);
2747
3066
  }
3067
+ console.error(`[Security] MCP v${MCP_VERSION} | Key: ${authContext.apiKeyName} | requireIpApproval: ${authContext.requireIpApproval}`);
3068
+ if (authContext.requireIpApproval) {
3069
+ console.error("[Security] IP approval enabled, resolving public IP...");
3070
+ const publicIp = await resolvePublicIp();
3071
+ console.error(`[Security] Public IP: ${publicIp}`);
3072
+ const ipResult = await checkIpAllowlist(publicIp);
3073
+ if (!ipResult.allowed) {
3074
+ console.error(`[Security] BLOCKED \u2014 ${ipResult.reason}`);
3075
+ console.error("[Security] MCP server will not start. Approve the IP via Telegram or add it manually in the dashboard.");
3076
+ process.exit(1);
3077
+ }
3078
+ console.error("[Security] IP approved, creating session...");
3079
+ await getOrCreateSession(publicIp);
3080
+ console.error("[Security] Session created");
3081
+ } else {
3082
+ console.error("[Security] IP approval disabled for this key");
3083
+ }
2748
3084
  const toolNames = TOOLS.map((t) => t.name).join(", ");
2749
3085
  if (httpMode) {
2750
3086
  console.error(`API key validated. Starting Streamable HTTP transport on port ${httpPort}...`);
@@ -2755,8 +3091,37 @@ async function main() {
2755
3091
  };
2756
3092
  const httpServer = createServer(async (req, res) => {
2757
3093
  const url = new URL(req.url ?? "/", `http://localhost:${httpPort}`);
3094
+ const clientIp = (req.headers["x-forwarded-for"]?.split(",")[0]?.trim() || req.headers["x-real-ip"] || req.socket.remoteAddress || "unknown").replace(/^::ffff:/, "");
3095
+ if (authContext?.requireIpApproval) {
3096
+ const { data: allowed } = await supabase.from("mcp_ip_allowlist").select("id").eq("api_key_id", authContext.apiKeyId).eq("ip_address", clientIp).maybeSingle();
3097
+ if (!allowed) {
3098
+ res.writeHead(403, { "Content-Type": "application/json" });
3099
+ res.end(JSON.stringify({ error: `IP ${clientIp} not in allowlist. Approve via Telegram first.` }));
3100
+ return;
3101
+ }
3102
+ }
3103
+ if (authContext) {
3104
+ const sessionResult = await getOrCreateSession(clientIp);
3105
+ if (!sessionResult.valid) {
3106
+ console.error(`[Session] Session expired: ${sessionResult.reason}`);
3107
+ res.writeHead(401, { "Content-Type": "application/json" });
3108
+ res.end(JSON.stringify({ error: `Session expired: ${sessionResult.reason}. Restart the MCP server to re-authenticate.` }));
3109
+ return;
3110
+ }
3111
+ }
2758
3112
  const restToolName = REST_TOOL_MAP[url.pathname];
2759
3113
  if (restToolName && req.method === "POST") {
3114
+ if (!authContext) {
3115
+ res.writeHead(401, { "Content-Type": "application/json" });
3116
+ res.end(JSON.stringify({ error: "Not authenticated" }));
3117
+ return;
3118
+ }
3119
+ const requiredModule = TOOL_MODULE_MAP[restToolName];
3120
+ if (requiredModule && authContext.permissions.modules[requiredModule] !== true) {
3121
+ res.writeHead(403, { "Content-Type": "application/json" });
3122
+ res.end(JSON.stringify({ error: `Access denied: no permission for "${requiredModule}" module` }));
3123
+ return;
3124
+ }
2760
3125
  const chunks = [];
2761
3126
  for await (const chunk of req) chunks.push(chunk);
2762
3127
  let toolArgs;