@mgsoftwarebv/mg-dashboard-mcp 6.3.0 → 6.5.0

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
@@ -7,15 +7,16 @@ import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/
7
7
  import { Server } from '@modelcontextprotocol/sdk/server/index.js';
8
8
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
9
9
  import { ListToolsRequestSchema, CallToolRequestSchema, isInitializeRequest, ListPromptsRequestSchema, GetPromptRequestSchema, ListResourcesRequestSchema, ReadResourceRequestSchema, ListResourceTemplatesRequestSchema, SubscribeRequestSchema, UnsubscribeRequestSchema, ListToolsResultSchema, CallToolResultSchema, ListPromptsResultSchema, GetPromptResultSchema, ListResourcesResultSchema, ReadResourceResultSchema, ListResourceTemplatesResultSchema, EmptyResultSchema } from '@modelcontextprotocol/sdk/types.js';
10
+ import { createServer as createServer$1 } from 'net';
11
+ import { Client } from 'ssh2';
10
12
  import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
11
13
  import { createServer } from 'http';
12
- import { randomUUID, createHash, randomBytes, createDecipheriv, createCipheriv } from 'crypto';
14
+ import { randomUUID, createHash, randomBytes, createCipheriv, createDecipheriv } from 'crypto';
13
15
  import { sql } from 'drizzle-orm';
14
16
  import { drizzle } from 'drizzle-orm/postgres-js';
15
17
  import postgres from 'postgres';
16
18
  import { readFile, mkdtemp, writeFile, rm } from 'fs/promises';
17
19
  import { tmpdir } from 'os';
18
- import { Client } from 'ssh2';
19
20
  import { HeadObjectCommand, S3Client, ListObjectsV2Command, DeleteObjectsCommand, DeleteObjectCommand, CreateMultipartUploadCommand, UploadPartCommand, CompleteMultipartUploadCommand, AbortMultipartUploadCommand, PutObjectCommand, GetObjectCommand, CopyObjectCommand } from '@aws-sdk/client-s3';
20
21
 
21
22
  var __defProp = Object.defineProperty;
@@ -272,6 +273,181 @@ var init_proxy_mode = __esm({
272
273
  "src/proxy-mode.ts"() {
273
274
  }
274
275
  });
276
+
277
+ // src/db-ssh-tunnel.ts
278
+ var db_ssh_tunnel_exports = {};
279
+ __export(db_ssh_tunnel_exports, {
280
+ fetchRemoteEnvValue: () => fetchRemoteEnvValue,
281
+ openDbSshTunnel: () => openDbSshTunnel
282
+ });
283
+ function expandHome2(path) {
284
+ if (!path.startsWith("~")) return path;
285
+ const home = process.env.HOME || process.env.USERPROFILE || "";
286
+ return join(home, path.slice(1));
287
+ }
288
+ function resolvePrivateKeyPath(input) {
289
+ const candidate = expandHome2(input);
290
+ const stripped = candidate.endsWith(".pub") ? candidate.slice(0, -4) : candidate;
291
+ if (existsSync(stripped) && statSync(stripped).isFile()) return stripped;
292
+ throw new Error(
293
+ `SSH private key not found at ${stripped}. The --db-ssh-tunnel flag needs the private key (not just .pub) to open the tunnel.`
294
+ );
295
+ }
296
+ function parseSshTarget(target) {
297
+ const at = target.indexOf("@");
298
+ if (at === -1) {
299
+ throw new Error(`--db-ssh-tunnel must be in the form user@host[:port], got: ${target}`);
300
+ }
301
+ const username = target.slice(0, at);
302
+ const hostPart = target.slice(at + 1);
303
+ const colon = hostPart.lastIndexOf(":");
304
+ if (colon === -1) return { username, host: hostPart, port: 22 };
305
+ const port = Number(hostPart.slice(colon + 1));
306
+ if (!Number.isFinite(port) || port < 1 || port > 65535) {
307
+ throw new Error(`Invalid SSH port in --db-ssh-tunnel: ${hostPart.slice(colon + 1)}`);
308
+ }
309
+ return { username, host: hostPart.slice(0, colon), port };
310
+ }
311
+ function rewriteUrl(originalUrl, localPort) {
312
+ const parsed = new URL(originalUrl);
313
+ parsed.hostname = "127.0.0.1";
314
+ parsed.port = String(localPort);
315
+ return parsed.toString();
316
+ }
317
+ async function connectSsh(opts) {
318
+ return await new Promise((resolve, reject) => {
319
+ const conn = new Client();
320
+ const onError = (err) => {
321
+ conn.removeAllListeners();
322
+ reject(err);
323
+ };
324
+ conn.once("ready", () => {
325
+ conn.removeListener("error", onError);
326
+ resolve(conn);
327
+ });
328
+ conn.once("error", onError);
329
+ conn.connect({
330
+ host: opts.host,
331
+ port: opts.port,
332
+ username: opts.username,
333
+ privateKey: opts.privateKey,
334
+ keepaliveInterval: opts.keepaliveIntervalMs,
335
+ keepaliveCountMax: 3,
336
+ readyTimeout: 2e4
337
+ });
338
+ });
339
+ }
340
+ function execOverSsh(conn, cmd) {
341
+ return new Promise((resolve, reject) => {
342
+ conn.exec(cmd, (err, stream) => {
343
+ if (err) {
344
+ reject(err);
345
+ return;
346
+ }
347
+ let stdout = "";
348
+ let stderr = "";
349
+ stream.on("data", (chunk) => {
350
+ stdout += chunk.toString("utf8");
351
+ });
352
+ stream.stderr.on("data", (chunk) => {
353
+ stderr += chunk.toString("utf8");
354
+ });
355
+ stream.on("close", (code) => {
356
+ resolve({ stdout, stderr, code: code ?? -1 });
357
+ });
358
+ });
359
+ });
360
+ }
361
+ function parseEnvValue(content, key) {
362
+ const re = new RegExp(`^${key}\\s*=\\s*(.*)$`, "m");
363
+ const m = content.match(re);
364
+ if (!m || !m[1]) return void 0;
365
+ let value = m[1].trim();
366
+ if (value.length >= 2 && (value[0] === '"' || value[0] === "'") && value[value.length - 1] === value[0]) {
367
+ value = value.slice(1, -1);
368
+ }
369
+ return value;
370
+ }
371
+ async function fetchRemoteEnvValue(options) {
372
+ const { username, host, port } = parseSshTarget(options.sshTarget);
373
+ const privateKeyPath = resolvePrivateKeyPath(options.sshKeyPath);
374
+ const privateKey = readFileSync(privateKeyPath);
375
+ const keepaliveIntervalMs = options.keepaliveIntervalMs ?? 3e4;
376
+ const envKey = options.envKey ?? "DATABASE_PRIMARY_URL";
377
+ const conn = await connectSsh({ host, port, username, privateKey, keepaliveIntervalMs });
378
+ try {
379
+ const safePath = options.remoteEnvPath.replace(/'/g, "'\\''");
380
+ const result = await execOverSsh(conn, `cat -- '${safePath}'`);
381
+ if (result.code !== 0) {
382
+ throw new Error(
383
+ `remote cat ${options.remoteEnvPath} failed (exit ${result.code}): ${result.stderr.trim()}`
384
+ );
385
+ }
386
+ const value = parseEnvValue(result.stdout, envKey);
387
+ if (!value) {
388
+ throw new Error(`${envKey} not found in ${options.remoteEnvPath}`);
389
+ }
390
+ return value;
391
+ } finally {
392
+ conn.end();
393
+ }
394
+ }
395
+ async function openDbSshTunnel(options) {
396
+ const { username, host, port } = parseSshTarget(options.sshTarget);
397
+ const privateKeyPath = resolvePrivateKeyPath(options.sshKeyPath);
398
+ const privateKey = readFileSync(privateKeyPath);
399
+ const keepaliveIntervalMs = options.keepaliveIntervalMs ?? 3e4;
400
+ const parsedDbUrl = new URL(options.databaseUrl);
401
+ const remoteHost = parsedDbUrl.hostname;
402
+ const remotePort = Number(parsedDbUrl.port || "5432");
403
+ const conn = await connectSsh({ host, port, username, privateKey, keepaliveIntervalMs });
404
+ conn.on("error", (err) => {
405
+ console.error(`[mcp][db-ssh-tunnel] ssh connection error: ${err.message}`);
406
+ });
407
+ conn.on("close", () => {
408
+ console.error("[mcp][db-ssh-tunnel] ssh connection closed; MCP will exit so Cursor can respawn it");
409
+ process.exit(1);
410
+ });
411
+ const server2 = createServer$1((local) => {
412
+ conn.forwardOut("127.0.0.1", 0, remoteHost, remotePort, (err, stream) => {
413
+ if (err) {
414
+ console.error(`[mcp][db-ssh-tunnel] forwardOut failed: ${err.message}`);
415
+ local.destroy(err);
416
+ return;
417
+ }
418
+ local.on("error", () => stream.destroy());
419
+ stream.on("error", () => local.destroy());
420
+ local.pipe(stream).pipe(local);
421
+ });
422
+ });
423
+ const localPort = await new Promise((resolve, reject) => {
424
+ server2.once("error", reject);
425
+ server2.listen(0, "127.0.0.1", () => {
426
+ const addr = server2.address();
427
+ if (!addr || typeof addr === "string") {
428
+ reject(new Error("Failed to bind local tunnel listener"));
429
+ return;
430
+ }
431
+ resolve(addr.port);
432
+ });
433
+ });
434
+ const rewrittenDatabaseUrl = rewriteUrl(options.databaseUrl, localPort);
435
+ console.error(
436
+ `[mcp][db-ssh-tunnel] forwarding 127.0.0.1:${localPort} -> ${remoteHost}:${remotePort} via ssh ${username}@${host}:${port}`
437
+ );
438
+ return {
439
+ rewrittenDatabaseUrl,
440
+ localPort,
441
+ async close() {
442
+ await new Promise((resolve) => server2.close(() => resolve()));
443
+ conn.end();
444
+ }
445
+ };
446
+ }
447
+ var init_db_ssh_tunnel = __esm({
448
+ "src/db-ssh-tunnel.ts"() {
449
+ }
450
+ });
275
451
  var connectionConfig = {
276
452
  prepare: false,
277
453
  idle_timeout: 20,
@@ -380,10 +556,10 @@ var TRIGGER_TOOL_MODULE_MAP = {
380
556
  "trigger-run": "ci_cd"
381
557
  };
382
558
  async function discoverInstance(projectSlug, conn, proxy, sshExec2) {
383
- const sql4 = `SELECT re.\\"apiKey\\" FROM \\"RuntimeEnvironment\\" re JOIN \\"Project\\" p ON re.\\"projectId\\" = p.id WHERE p.slug='${projectSlug}' AND re.slug='prod' LIMIT 1`;
559
+ const sql3 = `SELECT re.\\"apiKey\\" FROM \\"RuntimeEnvironment\\" re JOIN \\"Project\\" p ON re.\\"projectId\\" = p.id WHERE p.slug='${projectSlug}' AND re.slug='prod' LIMIT 1`;
384
560
  const cmd = [
385
561
  `PORT=$(docker port "${WA_CONTAINER}" 3000/tcp 2>/dev/null | head -1 | sed 's/.*://')`,
386
- `KEY=$(docker exec "${PG_CONTAINER}" psql -U postgres -d main -t -A -c "${sql4}" 2>/dev/null | tr -d '[:space:]')`,
562
+ `KEY=$(docker exec "${PG_CONTAINER}" psql -U postgres -d main -t -A -c "${sql3}" 2>/dev/null | tr -d '[:space:]')`,
387
563
  'echo "$PORT|$KEY"'
388
564
  ].join(" && ");
389
565
  const result = await sshExec2(conn, cmd, proxy);
@@ -404,8 +580,8 @@ async function discoverInstance(projectSlug, conn, proxy, sshExec2) {
404
580
  return { port, apiKey: apiKey2 };
405
581
  }
406
582
  async function fetchRunLogs(runId, conn, proxy, sshExec2) {
407
- const sql4 = `SELECT level, message, \\"isError\\", \\"createdAt\\" FROM \\"TaskEvent\\" WHERE \\"runId\\" = '${runId}' AND level IN ('INFO','WARN','ERROR','DEBUG','LOG','TRACE') ORDER BY \\"startTime\\" ASC LIMIT 200`;
408
- const cmd = `docker exec "${PG_CONTAINER}" psql -U postgres -d main -t -A -c "${sql4}" 2>/dev/null`;
583
+ const sql3 = `SELECT level, message, \\"isError\\", \\"createdAt\\" FROM \\"TaskEvent\\" WHERE \\"runId\\" = '${runId}' AND level IN ('INFO','WARN','ERROR','DEBUG','LOG','TRACE') ORDER BY \\"startTime\\" ASC LIMIT 200`;
584
+ const cmd = `docker exec "${PG_CONTAINER}" psql -U postgres -d main -t -A -c "${sql3}" 2>/dev/null`;
409
585
  const result = await sshExec2(conn, cmd, proxy);
410
586
  const output = result.stdout.trim();
411
587
  if (!output) return "";
@@ -487,8 +663,8 @@ async function handleTriggerTool(name, args2, deps) {
487
663
  switch (name) {
488
664
  // -----------------------------------------------------------------
489
665
  case "trigger-list": {
490
- const sql4 = 'SELECT slug, name FROM \\"Project\\" ORDER BY name';
491
- const cmd = `docker exec "${PG_CONTAINER}" psql -U postgres -d main -t -A -c "${sql4}" 2>/dev/null`;
666
+ const sql3 = 'SELECT slug, name FROM \\"Project\\" ORDER BY name';
667
+ const cmd = `docker exec "${PG_CONTAINER}" psql -U postgres -d main -t -A -c "${sql3}" 2>/dev/null`;
492
668
  const result = await sshExec2(conn, cmd, proxy);
493
669
  const output = result.stdout.trim();
494
670
  if (!output) {
@@ -683,621 +859,6 @@ async function waitForCompletion(conn, proxy, sshExec2, instance, runId, waitSec
683
859
  }]
684
860
  };
685
861
  }
686
- var VERCEL_API = "https://api.vercel.com";
687
- var VERCEL_TOOLS = [
688
- {
689
- name: "vercel-projects",
690
- description: "List Vercel projects in the configured account/team. Returns id, name, framework, GitHub repo link, production URL, and the latest deployment summary. Use the project name or id as input to vercel-deployments.",
691
- inputSchema: {
692
- type: "object",
693
- properties: {
694
- limit: { type: "number", description: "Max projects to return (default 50, max 200)" },
695
- search: { type: "string", description: "Substring filter on project name (case-insensitive)" }
696
- }
697
- }
698
- },
699
- {
700
- name: "vercel-deployments",
701
- description: "List recent deployments for a Vercel project. Returns deployment ID, state, target, branch, commit and timestamps. Use the deployment ID with vercel-logs.",
702
- inputSchema: {
703
- type: "object",
704
- properties: {
705
- project: { type: "string", description: "Vercel project ID or name (from vercel-projects)" },
706
- state: {
707
- type: "string",
708
- description: "Optional state filter: BUILDING, ERROR, INITIALIZING, QUEUED, READY, CANCELED"
709
- },
710
- target: { type: "string", description: "Optional target filter: production or preview" },
711
- limit: { type: "number", description: "Max deployments to return (default 20, max 100)" }
712
- },
713
- required: ["project"]
714
- }
715
- },
716
- {
717
- name: "vercel-domains",
718
- description: 'Manage domains attached to a Vercel project. Use `action` to pick the operation:\n- "list" (default): list all domains attached to the project, including verification status and any redirect.\n- "add": attach a new domain to the project. Returns DNS records to set if the domain is not yet verified.\n- "verify": re-check verification status and surface required CNAME / A / TXT records if misconfigured.\n- "remove": detach a domain from the project.\nPick the project via vercel-projects first.',
719
- inputSchema: {
720
- type: "object",
721
- properties: {
722
- action: {
723
- type: "string",
724
- enum: ["list", "add", "remove", "verify"],
725
- description: "Which operation to perform (default: list)."
726
- },
727
- project: {
728
- type: "string",
729
- description: "Vercel project ID or name (from vercel-projects)."
730
- },
731
- domain: {
732
- type: "string",
733
- description: 'Domain name (required for action="add", "remove", or "verify").'
734
- },
735
- gitBranch: {
736
- type: "string",
737
- description: 'Optional git branch this domain should deploy from (action="add" only).'
738
- },
739
- redirect: {
740
- type: "string",
741
- description: 'Optional redirect target domain (action="add" only).'
742
- },
743
- redirectStatusCode: {
744
- type: "number",
745
- description: 'Optional redirect status code, e.g. 301 / 302 / 307 / 308 (action="add" only).'
746
- }
747
- },
748
- required: ["project"]
749
- }
750
- },
751
- {
752
- name: "vercel-logs",
753
- description: 'Unified log inspector for Vercel. Use `kind` to pick the source:\n- "build" (default): build / deployment console events (stdout, stderr, command, exit). Requires deploymentId.\n- "runtime": runtime / function logs after a successful build. Requires project + deploymentId.\n- "webhooks": our own vercel_webhook_logs table (Telegram / push delivery history). No deployment needed.\nPick deployments via vercel-deployments first.',
754
- inputSchema: {
755
- type: "object",
756
- properties: {
757
- kind: {
758
- type: "string",
759
- enum: ["build", "runtime", "webhooks"],
760
- description: "Which log stream to fetch (default: build)."
761
- },
762
- project: {
763
- type: "string",
764
- description: 'Vercel project ID or name (required for kind="runtime").'
765
- },
766
- deploymentId: {
767
- type: "string",
768
- description: 'Vercel deployment ID (required for kind="build" or "runtime").'
769
- },
770
- projectName: {
771
- type: "string",
772
- description: 'Optional project_name filter (kind="webhooks" only).'
773
- },
774
- status: {
775
- type: "string",
776
- description: 'Optional status filter (kind="webhooks" only): sent, skipped, error.'
777
- },
778
- sinceMinutes: {
779
- type: "number",
780
- description: `Time window in minutes (kind="runtime" only, max 7 days). When omitted, the tool auto-detects the deployment's created timestamp and queries from there with a 5-minute buffer \u2014 so you don't miss logs by picking a too-small window.`
781
- },
782
- limit: {
783
- type: "number",
784
- description: "Max entries to return. Defaults per kind: build=500 (max 5000), runtime=200 (max 1000), webhooks=25 (max 200)."
785
- }
786
- }
787
- }
788
- }
789
- ];
790
- var VERCEL_TOOL_NAMES = new Set(VERCEL_TOOLS.map((t) => t.name));
791
- var VERCEL_TOOL_MODULE_MAP = {
792
- "vercel-projects": "ci_cd",
793
- "vercel-deployments": "ci_cd",
794
- "vercel-logs": "ci_cd",
795
- "vercel-domains": "ci_cd"
796
- };
797
- async function vercelFetch(token, path, init) {
798
- const res = await fetch(`${VERCEL_API}${path}`, {
799
- method: init?.method ?? "GET",
800
- headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json" },
801
- body: init?.body !== void 0 ? JSON.stringify(init.body) : void 0
802
- });
803
- const body = await res.text();
804
- let parsed = null;
805
- try {
806
- parsed = body ? JSON.parse(body) : null;
807
- } catch {
808
- }
809
- if (!res.ok) {
810
- const msg = parsed?.error?.message ?? parsed?.message ?? body ?? `HTTP ${res.status}`;
811
- return { data: null, error: `Vercel API ${res.status}: ${msg}`, status: res.status };
812
- }
813
- return { data: parsed, error: null, status: res.status };
814
- }
815
- async function listVercelProjectsAll(token, limit) {
816
- const collected = [];
817
- let nextTimestamp;
818
- while (collected.length < limit) {
819
- const params = new URLSearchParams({ limit: String(Math.min(limit - collected.length, 100)) });
820
- if (nextTimestamp) params.set("until", String(nextTimestamp));
821
- const res = await vercelFetch(token, `/v9/projects?${params.toString()}`);
822
- if (res.error) return { projects: [], error: res.error };
823
- if (!res.data?.projects?.length) break;
824
- collected.push(...res.data.projects);
825
- if (!res.data.pagination?.next) break;
826
- nextTimestamp = res.data.pagination.next;
827
- }
828
- return { projects: collected, error: null };
829
- }
830
- async function listVercelDeployments(token, options) {
831
- const params = new URLSearchParams({
832
- limit: String(options.limit),
833
- projectId: options.projectId
834
- });
835
- if (options.state) params.set("state", options.state);
836
- if (options.target) params.set("target", options.target);
837
- const res = await vercelFetch(
838
- token,
839
- `/v6/deployments?${params.toString()}`
840
- );
841
- if (res.error) return { deployments: [], error: res.error };
842
- return { deployments: res.data?.deployments ?? [], error: null };
843
- }
844
- async function getDeploymentBuildEvents(token, deploymentId, limit) {
845
- const params = new URLSearchParams({
846
- limit: String(limit),
847
- direction: "forward",
848
- follow: "0"
849
- });
850
- const url = `${VERCEL_API}/v3/deployments/${encodeURIComponent(deploymentId)}/events?${params.toString()}`;
851
- const res = await fetch(url, {
852
- headers: { Authorization: `Bearer ${token}` }
853
- });
854
- const body = await res.text();
855
- if (!res.ok) {
856
- let msg = body;
857
- try {
858
- const parsed = JSON.parse(body);
859
- msg = parsed?.error?.message ?? parsed?.message ?? body;
860
- } catch {
861
- }
862
- return { events: [], error: `Vercel API ${res.status}: ${msg}` };
863
- }
864
- const events = [];
865
- const trimmed = body.trim();
866
- if (!trimmed) return { events, error: null };
867
- try {
868
- const parsed = JSON.parse(trimmed);
869
- if (Array.isArray(parsed)) {
870
- for (const r of parsed) events.push(r);
871
- return { events, error: null };
872
- }
873
- } catch {
874
- }
875
- for (const line of trimmed.split("\n")) {
876
- const s = line.trim();
877
- if (!s) continue;
878
- try {
879
- events.push(JSON.parse(s));
880
- } catch {
881
- }
882
- }
883
- return { events, error: null };
884
- }
885
- async function getDeploymentCreatedMs(token, deploymentId) {
886
- const res = await vercelFetch(
887
- token,
888
- `/v13/deployments/${encodeURIComponent(deploymentId)}`
889
- );
890
- if (res.error || !res.data) return null;
891
- return res.data.createdAt ?? res.data.created ?? null;
892
- }
893
- async function getRuntimeLogs(token, projectIdOrName, deploymentId, limit, sinceMs) {
894
- const params = new URLSearchParams({
895
- limit: String(Math.min(Math.max(limit, 1), 1e3))
896
- });
897
- if (sinceMs) params.set("since", String(sinceMs));
898
- const url = `${VERCEL_API}/v1/projects/${encodeURIComponent(projectIdOrName)}/deployments/${encodeURIComponent(deploymentId)}/runtime-logs?${params.toString()}`;
899
- const res = await fetch(url, {
900
- headers: { Authorization: `Bearer ${token}` }
901
- });
902
- const body = await res.text();
903
- if (!res.ok) {
904
- let msg = body;
905
- try {
906
- const parsed = JSON.parse(body);
907
- msg = parsed?.error?.message ?? parsed?.message ?? body;
908
- } catch {
909
- }
910
- return { logs: [], error: `Vercel API ${res.status}: ${msg}` };
911
- }
912
- const logs = [];
913
- const trimmed = body.trim();
914
- if (!trimmed) return { logs, error: null };
915
- try {
916
- const parsed = JSON.parse(trimmed);
917
- if (Array.isArray(parsed)) {
918
- for (const r of parsed) logs.push(r);
919
- return { logs, error: null };
920
- }
921
- } catch {
922
- }
923
- for (const line of trimmed.split("\n")) {
924
- const s = line.trim();
925
- if (!s) continue;
926
- try {
927
- logs.push(JSON.parse(s));
928
- } catch {
929
- }
930
- }
931
- return { logs, error: null };
932
- }
933
- async function listProjectDomains(token, projectId) {
934
- const res = await vercelFetch(
935
- token,
936
- `/v9/projects/${encodeURIComponent(projectId)}/domains?limit=100`
937
- );
938
- if (res.error) return { domains: [], error: res.error };
939
- return { domains: res.data?.domains ?? [], error: null };
940
- }
941
- async function addProjectDomain(token, projectId, body) {
942
- const res = await vercelFetch(
943
- token,
944
- `/v10/projects/${encodeURIComponent(projectId)}/domains`,
945
- { method: "POST", body }
946
- );
947
- if (res.error) return { domain: null, error: res.error };
948
- return { domain: res.data, error: null };
949
- }
950
- async function removeProjectDomain(token, projectId, domain) {
951
- const res = await vercelFetch(
952
- token,
953
- `/v9/projects/${encodeURIComponent(projectId)}/domains/${encodeURIComponent(domain)}`,
954
- { method: "DELETE" }
955
- );
956
- return { error: res.error };
957
- }
958
- async function getProjectDomain(token, projectId, domain) {
959
- const res = await vercelFetch(
960
- token,
961
- `/v9/projects/${encodeURIComponent(projectId)}/domains/${encodeURIComponent(domain)}`
962
- );
963
- if (res.error) return { domain: null, error: res.error };
964
- return { domain: res.data, error: null };
965
- }
966
- async function getDomainConfig(token, domain) {
967
- const res = await vercelFetch(
968
- token,
969
- `/v6/domains/${encodeURIComponent(domain)}/config`
970
- );
971
- if (res.error) return { config: null, error: res.error };
972
- return { config: res.data, error: null };
973
- }
974
- async function getVercelToken(deps) {
975
- const rows = await deps.db.execute(sql`
976
- SELECT vercel_token_encrypted FROM app_setting LIMIT 1
977
- `);
978
- const data = rows[0];
979
- if (!data?.vercel_token_encrypted) {
980
- throw new Error("Vercel API token is not configured. Add it in dashboard Settings.");
981
- }
982
- try {
983
- return deps.decrypt(data.vercel_token_encrypted);
984
- } catch {
985
- throw new Error("Failed to decrypt the stored Vercel API token.");
986
- }
987
- }
988
- async function resolveProjectId(token, projectInput) {
989
- const res = await vercelFetch(
990
- token,
991
- `/v9/projects/${encodeURIComponent(projectInput)}`
992
- );
993
- if (res.error || !res.data?.id) {
994
- throw new Error(
995
- `Could not resolve Vercel project "${projectInput}": ${res.error ?? "not found"}`
996
- );
997
- }
998
- return res.data.id;
999
- }
1000
- function formatTimestamp(ms) {
1001
- if (!ms) return "";
1002
- return new Date(ms).toLocaleString("nl-NL", { timeZone: "Europe/Amsterdam" });
1003
- }
1004
- function formatProjectsTable(projects) {
1005
- if (projects.length === 0) return "No Vercel projects found";
1006
- const lines = projects.map((p) => {
1007
- const repo = p.link ? `${p.link.org ?? ""}/${p.link.repo ?? ""}` : "";
1008
- const prodUrl = p.targets?.production?.alias?.[0] ?? p.targets?.production?.url ?? "";
1009
- const latest = p.latestDeployments?.[0];
1010
- const latestStr = latest ? `${latest.state} @ ${formatTimestamp(latest.createdAt)}` : "";
1011
- return `${p.id.padEnd(28)} ${(p.name || "").padEnd(40)} ${(p.framework || "-").padEnd(10)} ${repo.padEnd(40)} ${prodUrl.padEnd(35)} ${latestStr}`;
1012
- });
1013
- const header = `${"ID".padEnd(28)} ${"NAME".padEnd(40)} ${"FRAMEWORK".padEnd(10)} ${"REPO".padEnd(40)} ${"PROD URL".padEnd(35)} LATEST`;
1014
- return `${header}
1015
- ${"-".repeat(header.length)}
1016
- ${lines.join("\n")}`;
1017
- }
1018
- function formatDeploymentsTable(deployments) {
1019
- if (deployments.length === 0) return "No deployments found";
1020
- const lines = deployments.map((d) => {
1021
- const branch = d.meta?.githubCommitRef ?? "";
1022
- const sha = d.meta?.githubCommitSha?.slice(0, 7) ?? "";
1023
- const msg = (d.meta?.githubCommitMessage ?? "").replace(/\n.*/s, "").slice(0, 60);
1024
- return `${d.uid.padEnd(28)} ${d.state.padEnd(11)} ${(d.target || "-").padEnd(10)} ${branch.padEnd(20)} ${sha.padEnd(8)} ${formatTimestamp(d.created).padEnd(20)} ${msg}`;
1025
- });
1026
- const header = `${"ID".padEnd(28)} ${"STATE".padEnd(11)} ${"TARGET".padEnd(10)} ${"BRANCH".padEnd(20)} ${"COMMIT".padEnd(8)} ${"CREATED".padEnd(20)} MESSAGE`;
1027
- return `${header}
1028
- ${"-".repeat(header.length)}
1029
- ${lines.join("\n")}`;
1030
- }
1031
- function formatBuildEvents(events) {
1032
- if (events.length === 0) return "No build events found for this deployment";
1033
- const filtered = events.filter(
1034
- (e) => ["command", "stdout", "stderr", "exit", "delimiter", "deployment-state"].includes(e.type)
1035
- );
1036
- return filtered.map((e) => {
1037
- const time = e.created ? new Date(e.created).toISOString().slice(11, 19) : "--:--:--";
1038
- const text = (e.payload?.text ?? e.text ?? "").replace(/\s+$/u, "");
1039
- const prefix = e.type === "stderr" ? "!" : " ";
1040
- return `${prefix}${time} [${e.type.padEnd(16)}] ${text}`;
1041
- }).join("\n");
1042
- }
1043
- function formatRuntimeLogs(logs) {
1044
- if (logs.length === 0) {
1045
- return "No runtime logs found for this deployment in the requested window";
1046
- }
1047
- return logs.map((l) => {
1048
- const ts = l.timestampInMs ?? l.timestamp ?? 0;
1049
- const time = ts ? new Date(ts).toISOString().slice(11, 23) : "--:--:--";
1050
- const level = (l.level ?? "info").toUpperCase().padEnd(5);
1051
- const status = l.proxy?.statusCode ?? l.responseStatusCode;
1052
- const reqInfo = l.proxy?.method ? `${l.proxy.method} ${l.proxy.path ?? ""} ${status ?? ""}`.trim() : "";
1053
- const message = (l.message ?? l.text ?? "").replace(/\s+$/u, "");
1054
- const reqId = l.requestId ? ` [${l.requestId.slice(0, 12)}]` : "";
1055
- const head = reqInfo ? `${reqInfo}${reqId}` : reqId.trim();
1056
- return `${time} [${level}] ${head ? head + " " : ""}${message}`;
1057
- }).join("\n");
1058
- }
1059
- function formatWebhookHistory(rows) {
1060
- if (rows.length === 0) return "No webhook log rows match those filters";
1061
- const lines = rows.map((r) => {
1062
- const ts = new Date(r.created_at).toLocaleString("nl-NL", { timeZone: "Europe/Amsterdam" });
1063
- const note = r.error_message ?? r.message ?? "";
1064
- return `${ts.padEnd(20)} ${r.event_type.padEnd(22)} ${r.status.padEnd(8)} ${(r.project_name ?? "").padEnd(40)} ${(r.target ?? "").padEnd(10)} ${(r.deployment_id ?? "").padEnd(28)} ${note}`;
1065
- });
1066
- const header = `${"WHEN".padEnd(20)} ${"EVENT".padEnd(22)} ${"STATUS".padEnd(8)} ${"PROJECT".padEnd(40)} ${"TARGET".padEnd(10)} ${"DEPLOYMENT".padEnd(28)} NOTE`;
1067
- return `${header}
1068
- ${"-".repeat(header.length)}
1069
- ${lines.join("\n")}`;
1070
- }
1071
- function formatDomainsTable(domains) {
1072
- if (domains.length === 0) return "No domains attached to this project";
1073
- const lines = domains.map((d) => {
1074
- const verified = d.verified ? "yes" : "no";
1075
- const branch = d.gitBranch ?? "";
1076
- const redirect = d.redirect ? `${d.redirect}${d.redirectStatusCode ? ` (${d.redirectStatusCode})` : ""}` : "";
1077
- return `${d.name.padEnd(45)} ${verified.padEnd(9)} ${branch.padEnd(20)} ${redirect.padEnd(35)} ${formatTimestamp(d.createdAt)}`;
1078
- });
1079
- const header = `${"DOMAIN".padEnd(45)} ${"VERIFIED".padEnd(9)} ${"GIT BRANCH".padEnd(20)} ${"REDIRECT".padEnd(35)} CREATED`;
1080
- return `${header}
1081
- ${"-".repeat(header.length)}
1082
- ${lines.join("\n")}`;
1083
- }
1084
- function formatDomainStatus(domain, config) {
1085
- const lines = [];
1086
- lines.push(`Domain: ${domain.name}`);
1087
- lines.push(`Verified: ${domain.verified ? "yes" : "no"}`);
1088
- if (config) {
1089
- lines.push(`Misconfigured: ${config.misconfigured ? "yes" : "no"}`);
1090
- if (config.configuredBy) lines.push(`Configured by: ${config.configuredBy}`);
1091
- }
1092
- if (domain.gitBranch) lines.push(`Git branch: ${domain.gitBranch}`);
1093
- if (domain.redirect) {
1094
- lines.push(
1095
- `Redirect: ${domain.redirect}${domain.redirectStatusCode ? ` (${domain.redirectStatusCode})` : ""}`
1096
- );
1097
- }
1098
- if (domain.verification && domain.verification.length > 0) {
1099
- lines.push("");
1100
- lines.push("Required DNS records to verify ownership:");
1101
- for (const v of domain.verification) {
1102
- lines.push(` ${v.type.padEnd(6)} ${v.domain.padEnd(45)} ${v.value}${v.reason ? ` // ${v.reason}` : ""}`);
1103
- }
1104
- }
1105
- return lines.join("\n");
1106
- }
1107
- async function handleVercelTool(name, args2, deps) {
1108
- switch (name) {
1109
- case "vercel-projects": {
1110
- const token = await getVercelToken(deps);
1111
- const limit = Math.min(Math.max(Number(args2.limit) || 50, 1), 200);
1112
- const search = typeof args2.search === "string" ? args2.search.toLowerCase() : null;
1113
- const { projects, error } = await listVercelProjectsAll(token, limit);
1114
- if (error) return { content: [{ type: "text", text: `Error: ${error}` }] };
1115
- const filtered = search ? projects.filter((p) => (p.name || "").toLowerCase().includes(search)) : projects;
1116
- return { content: [{ type: "text", text: formatProjectsTable(filtered) }] };
1117
- }
1118
- case "vercel-deployments": {
1119
- const token = await getVercelToken(deps);
1120
- const projectInput = String(args2.project);
1121
- const limit = Math.min(Math.max(Number(args2.limit) || 20, 1), 100);
1122
- const projectId = await resolveProjectId(token, projectInput);
1123
- const { deployments, error } = await listVercelDeployments(token, {
1124
- projectId,
1125
- limit,
1126
- state: args2.state ? String(args2.state) : void 0,
1127
- target: args2.target ? String(args2.target) : void 0
1128
- });
1129
- if (error) return { content: [{ type: "text", text: `Error: ${error}` }] };
1130
- return { content: [{ type: "text", text: formatDeploymentsTable(deployments) }] };
1131
- }
1132
- case "vercel-logs": {
1133
- const kind = (args2.kind ? String(args2.kind) : "build").toLowerCase();
1134
- if (kind === "build") {
1135
- if (!args2.deploymentId) {
1136
- return { content: [{ type: "text", text: 'Error: kind="build" requires deploymentId.' }] };
1137
- }
1138
- const token = await getVercelToken(deps);
1139
- const deploymentId = String(args2.deploymentId);
1140
- const limit = Math.min(Math.max(Number(args2.limit) || 500, 1), 5e3);
1141
- const { events, error } = await getDeploymentBuildEvents(token, deploymentId, limit);
1142
- if (error) return { content: [{ type: "text", text: `Error: ${error}` }] };
1143
- return { content: [{ type: "text", text: formatBuildEvents(events) }] };
1144
- }
1145
- if (kind === "runtime") {
1146
- if (!args2.project || !args2.deploymentId) {
1147
- return {
1148
- content: [
1149
- { type: "text", text: 'Error: kind="runtime" requires both project and deploymentId.' }
1150
- ]
1151
- };
1152
- }
1153
- const token = await getVercelToken(deps);
1154
- const projectInput = String(args2.project);
1155
- const deploymentId = String(args2.deploymentId);
1156
- const limit = Math.min(Math.max(Number(args2.limit) || 200, 1), 1e3);
1157
- const sinceMinutesRaw = Number(args2.sinceMinutes);
1158
- const sinceExplicit = Number.isFinite(sinceMinutesRaw) && sinceMinutesRaw > 0;
1159
- const [projectId, deploymentCreatedMs] = await Promise.all([
1160
- resolveProjectId(token, projectInput),
1161
- sinceExplicit ? Promise.resolve(null) : getDeploymentCreatedMs(token, deploymentId)
1162
- ]);
1163
- const maxWindowMin = 7 * 24 * 60;
1164
- const autoCapMin = 30;
1165
- let sinceMs;
1166
- let windowNote = "";
1167
- if (sinceExplicit) {
1168
- const capped = Math.min(sinceMinutesRaw, maxWindowMin);
1169
- sinceMs = Date.now() - capped * 6e4;
1170
- windowNote = `window: last ${capped} min (caller-specified)`;
1171
- } else if (deploymentCreatedMs) {
1172
- const bufferMs = 5 * 6e4;
1173
- const sinceDeploymentMs = deploymentCreatedMs - bufferMs;
1174
- const ageMin = Math.max(1, Math.round((Date.now() - sinceDeploymentMs) / 6e4));
1175
- if (ageMin <= autoCapMin) {
1176
- sinceMs = sinceDeploymentMs;
1177
- windowNote = `window: auto ${ageMin} min from deployment createdAt - 5 min buffer`;
1178
- } else {
1179
- sinceMs = Date.now() - autoCapMin * 6e4;
1180
- windowNote = `window: capped to last ${autoCapMin} min (deployment is ${ageMin} min old \u2014 pass sinceMinutes to widen up to ${maxWindowMin})`;
1181
- }
1182
- } else {
1183
- sinceMs = Date.now() - autoCapMin * 6e4;
1184
- windowNote = `window: last ${autoCapMin} min (deployment metadata unavailable, used fallback)`;
1185
- }
1186
- const { logs, error } = await getRuntimeLogs(
1187
- token,
1188
- projectId,
1189
- deploymentId,
1190
- limit,
1191
- sinceMs
1192
- );
1193
- if (error) {
1194
- const hint = error.includes("404") || error.includes("400") ? '\n\nThis endpoint requires both project ID and deployment ID and may not be available on every Vercel plan. Use kind="webhooks" or query the vercel_deployment_log table directly for archived runtime logs.' : "";
1195
- return { content: [{ type: "text", text: `Error: ${error}${hint}` }] };
1196
- }
1197
- const body = formatRuntimeLogs(logs);
1198
- const hitDurationLimit = /Exceeded query duration limit/i.test(body);
1199
- const footer = hitDurationLimit ? `
1200
-
1201
- [${windowNote}]
1202
- [hint] Vercel hit its 5-min query budget for this window. Try a smaller sinceMinutes (e.g. 5-10), lower limit, or use kind="webhooks" / query the vercel_deployment_log table for archived logs.` : `
1203
-
1204
- [${windowNote}]`;
1205
- return { content: [{ type: "text", text: body + footer }] };
1206
- }
1207
- if (kind === "webhooks") {
1208
- const limit = Math.min(Math.max(Number(args2.limit) || 25, 1), 200);
1209
- const conds = [sql`TRUE`];
1210
- if (args2.projectName) conds.push(sql`project_name = ${String(args2.projectName)}`);
1211
- if (args2.status) conds.push(sql`status = ${String(args2.status)}`);
1212
- const where = sql.join(conds, sql` AND `);
1213
- try {
1214
- const rows = await deps.db.execute(sql`
1215
- SELECT id, event_type, status, project_name, deployment_id, target,
1216
- message, error_message, created_at
1217
- FROM vercel_webhook_logs
1218
- WHERE ${where}
1219
- ORDER BY created_at DESC
1220
- LIMIT ${limit}
1221
- `);
1222
- return {
1223
- content: [{ type: "text", text: formatWebhookHistory([...rows]) }]
1224
- };
1225
- } catch (err) {
1226
- const msg = err instanceof Error ? err.message : String(err);
1227
- return { content: [{ type: "text", text: `Error: ${msg}` }] };
1228
- }
1229
- }
1230
- return {
1231
- content: [
1232
- { type: "text", text: `Error: unknown kind "${kind}". Use build, runtime, or webhooks.` }
1233
- ]
1234
- };
1235
- }
1236
- case "vercel-domains": {
1237
- const action = (args2.action ? String(args2.action) : "list").toLowerCase();
1238
- if (!args2.project) {
1239
- return { content: [{ type: "text", text: 'Error: vercel-domains requires "project".' }] };
1240
- }
1241
- const token = await getVercelToken(deps);
1242
- const projectInput = String(args2.project);
1243
- const projectId = await resolveProjectId(token, projectInput);
1244
- if (action === "list") {
1245
- const { domains, error } = await listProjectDomains(token, projectId);
1246
- if (error) return { content: [{ type: "text", text: `Error: ${error}` }] };
1247
- return { content: [{ type: "text", text: formatDomainsTable(domains) }] };
1248
- }
1249
- if (action === "add") {
1250
- if (!args2.domain) {
1251
- return { content: [{ type: "text", text: 'Error: action="add" requires "domain".' }] };
1252
- }
1253
- const body = { name: String(args2.domain) };
1254
- if (args2.gitBranch) body.gitBranch = String(args2.gitBranch);
1255
- if (args2.redirect) body.redirect = String(args2.redirect);
1256
- if (args2.redirectStatusCode) body.redirectStatusCode = Number(args2.redirectStatusCode);
1257
- const { domain, error } = await addProjectDomain(token, projectId, body);
1258
- if (error) return { content: [{ type: "text", text: `Error: ${error}` }] };
1259
- if (!domain) {
1260
- return { content: [{ type: "text", text: `Domain ${body.name} added (no detail returned).` }] };
1261
- }
1262
- const { config } = await getDomainConfig(token, domain.name);
1263
- const status = formatDomainStatus(domain, config);
1264
- const headline = domain.verified ? `Domain ${domain.name} added and verified.` : `Domain ${domain.name} added. DNS verification still pending.`;
1265
- return { content: [{ type: "text", text: `${headline}
1266
-
1267
- ${status}` }] };
1268
- }
1269
- if (action === "verify") {
1270
- if (!args2.domain) {
1271
- return { content: [{ type: "text", text: 'Error: action="verify" requires "domain".' }] };
1272
- }
1273
- const domainName = String(args2.domain);
1274
- const { domain, error } = await getProjectDomain(token, projectId, domainName);
1275
- if (error) return { content: [{ type: "text", text: `Error: ${error}` }] };
1276
- if (!domain) {
1277
- return { content: [{ type: "text", text: `Domain ${domainName} not found on this project.` }] };
1278
- }
1279
- const { config } = await getDomainConfig(token, domainName);
1280
- return { content: [{ type: "text", text: formatDomainStatus(domain, config) }] };
1281
- }
1282
- if (action === "remove") {
1283
- if (!args2.domain) {
1284
- return { content: [{ type: "text", text: 'Error: action="remove" requires "domain".' }] };
1285
- }
1286
- const domainName = String(args2.domain);
1287
- const { error } = await removeProjectDomain(token, projectId, domainName);
1288
- if (error) return { content: [{ type: "text", text: `Error: ${error}` }] };
1289
- return { content: [{ type: "text", text: `Domain ${domainName} removed from project.` }] };
1290
- }
1291
- return {
1292
- content: [
1293
- { type: "text", text: `Error: unknown action "${action}". Use list, add, verify, or remove.` }
1294
- ]
1295
- };
1296
- }
1297
- default:
1298
- return { content: [{ type: "text", text: `Unknown vercel tool: ${name}` }] };
1299
- }
1300
- }
1301
862
  var REPO_TOOLS = [
1302
863
  {
1303
864
  name: "repo-list",
@@ -1979,19 +1540,42 @@ var sshKeyPath = getArg2("ssh-key") || process.env.MG_DASHBOARD_SSH_KEY;
1979
1540
  var databaseUrl = getArg2("database-url") || process.env.DATABASE_PRIMARY_POOLER_URL || process.env.DATABASE_PRIMARY_URL;
1980
1541
  var encryptionKey = getArg2("encryption-key") || process.env.ENCRYPTION_KEY;
1981
1542
  var mijnhostApiKey = getArg2("mijnhost-api-key") || process.env.MIJNHOST_API_KEY;
1543
+ var dbSshTunnel = getArg2("db-ssh-tunnel") || process.env.MG_DASHBOARD_DB_SSH_TUNNEL;
1544
+ var dbRemoteEnvFile = getArg2("db-remote-env-file") || process.env.MG_DASHBOARD_DB_REMOTE_ENV_FILE;
1545
+ var dbRemoteEnvKey = getArg2("db-remote-env-key") || process.env.MG_DASHBOARD_DB_REMOTE_ENV_KEY || "DATABASE_PRIMARY_URL";
1982
1546
  var httpMode = args.includes("--http");
1983
1547
  var httpPort = Number(getArg2("port")) || 3100;
1984
1548
  if (!apiKey || !sshKeyPath) {
1985
1549
  console.error("Authentication required. Use both --api-key=dk_xxx and --ssh-key=PATH (path to your SSH private or public key), or set MG_DASHBOARD_API_KEY and MG_DASHBOARD_SSH_KEY.");
1986
1550
  process.exit(1);
1987
1551
  }
1552
+ if (dbRemoteEnvFile && !dbSshTunnel) {
1553
+ console.error("--db-remote-env-file requires --db-ssh-tunnel (the same SSH connection is used to read the env value).");
1554
+ process.exit(1);
1555
+ }
1556
+ if (dbRemoteEnvFile) {
1557
+ const { fetchRemoteEnvValue: fetchRemoteEnvValue2 } = await Promise.resolve().then(() => (init_db_ssh_tunnel(), db_ssh_tunnel_exports));
1558
+ databaseUrl = await fetchRemoteEnvValue2({
1559
+ sshTarget: dbSshTunnel,
1560
+ sshKeyPath,
1561
+ remoteEnvPath: dbRemoteEnvFile,
1562
+ envKey: dbRemoteEnvKey
1563
+ });
1564
+ }
1988
1565
  if (!databaseUrl) {
1989
- console.error("Database URL required. Use --database-url=postgres://... or set DATABASE_PRIMARY_URL (or DATABASE_PRIMARY_POOLER_URL).");
1566
+ console.error("Database URL required. Provide one of:\n --database-url=postgres://...\n --db-remote-env-file=/path/.env (combined with --db-ssh-tunnel)\n DATABASE_PRIMARY_URL or DATABASE_PRIMARY_POOLER_URL env var");
1990
1567
  process.exit(1);
1991
1568
  }
1992
- if (!process.env.DATABASE_PRIMARY_URL && !process.env.DATABASE_PRIMARY_POOLER_URL) {
1993
- process.env.DATABASE_PRIMARY_URL = databaseUrl;
1569
+ if (dbSshTunnel) {
1570
+ const { openDbSshTunnel: openDbSshTunnel2 } = await Promise.resolve().then(() => (init_db_ssh_tunnel(), db_ssh_tunnel_exports));
1571
+ const tunnel = await openDbSshTunnel2({ sshTarget: dbSshTunnel, sshKeyPath, databaseUrl });
1572
+ databaseUrl = tunnel.rewrittenDatabaseUrl;
1573
+ process.on("exit", () => {
1574
+ void tunnel.close();
1575
+ });
1994
1576
  }
1577
+ process.env.DATABASE_PRIMARY_URL = databaseUrl;
1578
+ process.env.DATABASE_PRIMARY_POOLER_URL = databaseUrl;
1995
1579
  var db = getDb();
1996
1580
  var RateLimiter = class {
1997
1581
  buckets = /* @__PURE__ */ new Map();
@@ -2079,7 +1663,6 @@ async function writeAuditLog(entry) {
2079
1663
  var MODULE_KEYS = [
2080
1664
  "users",
2081
1665
  "ssh_servers",
2082
- "supabase",
2083
1666
  "wiki",
2084
1667
  "ci_cd",
2085
1668
  "domains",
@@ -2088,7 +1671,7 @@ var MODULE_KEYS = [
2088
1671
  ];
2089
1672
  var FULL_PERMISSIONS = {
2090
1673
  modules: Object.fromEntries(MODULE_KEYS.map((k) => [k, true])),
2091
- resources: { ssh_servers: ["*"], supabase_instances: ["*"] }
1674
+ resources: { ssh_servers: ["*"] }
2092
1675
  };
2093
1676
  function parsePermissions(raw) {
2094
1677
  if (!raw || typeof raw !== "object") return null;
@@ -2105,8 +1688,7 @@ function resolvePermissions(roleName, roleDefaults, userOverrides) {
2105
1688
  modules[key] = userVal !== void 0 ? userVal : roleVal !== void 0 ? roleVal : false;
2106
1689
  }
2107
1690
  const resources = {
2108
- ssh_servers: overrides?.resources?.ssh_servers ?? base?.resources?.ssh_servers ?? [],
2109
- supabase_instances: overrides?.resources?.supabase_instances ?? base?.resources?.supabase_instances ?? []
1691
+ ssh_servers: overrides?.resources?.ssh_servers ?? base?.resources?.ssh_servers ?? []
2110
1692
  };
2111
1693
  return { modules, resources };
2112
1694
  }
@@ -2146,7 +1728,6 @@ var TOOL_MODULE_MAP = {
2146
1728
  "dns-list": "domains",
2147
1729
  "dns-record": "domains",
2148
1730
  ...TRIGGER_TOOL_MODULE_MAP,
2149
- ...VERCEL_TOOL_MODULE_MAP,
2150
1731
  ...REPO_TOOL_MODULE_MAP
2151
1732
  };
2152
1733
  var authContext = null;
@@ -2503,151 +2084,6 @@ function decrypt(payload) {
2503
2084
  decrypted += decipher.final("utf8");
2504
2085
  return decrypted;
2505
2086
  }
2506
- var VERCEL_API2 = "https://api.vercel.com";
2507
- function parseEnvContent(content) {
2508
- const result = {};
2509
- for (const line of content.split("\n")) {
2510
- const trimmed = line.trim();
2511
- if (!trimmed || trimmed.startsWith("#")) continue;
2512
- const eqIdx = trimmed.indexOf("=");
2513
- if (eqIdx === -1) continue;
2514
- const key = trimmed.slice(0, eqIdx).trim();
2515
- let value = trimmed.slice(eqIdx + 1).trim();
2516
- if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
2517
- value = value.slice(1, -1);
2518
- }
2519
- if (key) result[key] = value;
2520
- }
2521
- return result;
2522
- }
2523
- function stageToVercelTargets(stageType, customEnvId) {
2524
- if (stageType === "prod") return { target: ["production"] };
2525
- if (customEnvId) return { customEnvironmentIds: [customEnvId] };
2526
- return { target: ["preview"] };
2527
- }
2528
- function getVercelEnvSyncTargetings(stageType, customEnvId) {
2529
- const primary = stageToVercelTargets(stageType, customEnvId);
2530
- if (stageType !== "dev") return [primary];
2531
- return [primary, { target: ["development"] }];
2532
- }
2533
- async function syncEnvVarsToVercel(token, projectId, envVars) {
2534
- if (envVars.length === 0) return { created: 0, error: null };
2535
- const res = await fetch(
2536
- `${VERCEL_API2}/v10/projects/${encodeURIComponent(projectId)}/env?upsert=true`,
2537
- {
2538
- method: "POST",
2539
- headers: {
2540
- Authorization: `Bearer ${token}`,
2541
- "Content-Type": "application/json"
2542
- },
2543
- body: JSON.stringify(envVars)
2544
- }
2545
- );
2546
- if (!res.ok) {
2547
- const body = await res.text().catch(() => "");
2548
- return { created: 0, error: `Vercel API ${res.status}: ${body}` };
2549
- }
2550
- const data = await res.json().catch(() => ({}));
2551
- return { created: data?.created?.length ?? envVars.length, error: null };
2552
- }
2553
- async function attemptVercelSync(appName, environment, knownStageId) {
2554
- try {
2555
- let stageId = knownStageId;
2556
- if (!stageId) {
2557
- const directRows = await db.execute(sql`
2558
- SELECT release_profile_stage_id
2559
- FROM env_config
2560
- WHERE app_name = ${appName} AND release_profile_stage_id IS NOT NULL
2561
- LIMIT 1
2562
- `);
2563
- stageId = directRows[0]?.release_profile_stage_id;
2564
- }
2565
- if (!stageId) return "Vercel sync skipped: no stage link found";
2566
- const settingsRows = await db.execute(sql`
2567
- SELECT vercel_token_encrypted FROM app_setting LIMIT 1
2568
- `);
2569
- const settings = settingsRows[0];
2570
- if (!settings?.vercel_token_encrypted) return "Vercel sync skipped: no Vercel token configured";
2571
- let token;
2572
- try {
2573
- token = decrypt(settings.vercel_token_encrypted);
2574
- } catch {
2575
- return "Vercel sync failed: could not decrypt Vercel token";
2576
- }
2577
- const stageRows = await db.execute(sql`
2578
- SELECT id, stage, stage_apps FROM release_profile_stage
2579
- WHERE id = ${stageId}
2580
- LIMIT 1
2581
- `);
2582
- const stage = stageRows[0];
2583
- if (!stage) return "Vercel sync skipped: stage not found";
2584
- const stageType = stage.stage;
2585
- const stageApps = stage.stage_apps || [];
2586
- const vercelApps = stageApps.filter(
2587
- (a) => a.deployMethod === "vercel" && a.enabled && a.vercelProjectId
2588
- );
2589
- if (vercelApps.length === 0) return "Vercel sync skipped: no Vercel apps in stage";
2590
- const envConfigs = await db.execute(sql`
2591
- SELECT id, app_name, environment, variant, env_data_encrypted, release_profile_stage_id
2592
- FROM env_config
2593
- WHERE release_profile_stage_id = ${stageId}
2594
- `);
2595
- if (envConfigs.length === 0) return "Vercel sync skipped: no env configs for stage";
2596
- const variantMap = {
2597
- dev: "development",
2598
- staging: "staging",
2599
- prod: "production"
2600
- };
2601
- const deployedVariant = variantMap[stageType] ?? stageType;
2602
- const syncResults = [];
2603
- for (const app of vercelApps) {
2604
- const name = app.path.replace("apps/", "");
2605
- const config = envConfigs.find(
2606
- (c) => c.app_name === name && c.variant === deployedVariant
2607
- );
2608
- if (!config) {
2609
- syncResults.push(`${app.label}: skipped (no config for variant "${deployedVariant}")`);
2610
- continue;
2611
- }
2612
- let envContent;
2613
- try {
2614
- envContent = decrypt(config.env_data_encrypted);
2615
- } catch {
2616
- syncResults.push(`${app.label}: decrypt failed`);
2617
- continue;
2618
- }
2619
- const pairs = parseEnvContent(envContent);
2620
- const keys = Object.keys(pairs);
2621
- if (keys.length === 0) {
2622
- syncResults.push(`${app.label}: empty config`);
2623
- continue;
2624
- }
2625
- const targetings = getVercelEnvSyncTargetings(stageType, app.vercelCustomEnvId);
2626
- let createdTotal = 0;
2627
- let lastErr = null;
2628
- for (const targeting of targetings) {
2629
- const envVars = keys.map((key) => ({
2630
- key,
2631
- value: pairs[key],
2632
- type: "encrypted",
2633
- ...targeting
2634
- }));
2635
- const { created, error } = await syncEnvVarsToVercel(token, app.vercelProjectId, envVars);
2636
- if (error) lastErr = error;
2637
- else createdTotal += created;
2638
- }
2639
- if (lastErr) {
2640
- syncResults.push(`${app.label}: FAILED - ${lastErr}`);
2641
- } else {
2642
- syncResults.push(`${app.label}: ${createdTotal} var upsert(s) synced`);
2643
- }
2644
- }
2645
- return `Vercel sync: ${syncResults.join("; ")}`;
2646
- } catch (err) {
2647
- const msg = err instanceof Error ? err.message : String(err);
2648
- return `Vercel sync error: ${msg}`;
2649
- }
2650
- }
2651
2087
  function posixQuote(arg) {
2652
2088
  if (arg === "") return "''";
2653
2089
  if (/^[A-Za-z0-9._\/=:@%+\-]+$/.test(arg)) return arg;
@@ -4276,11 +3712,11 @@ CREATE TABLE IF NOT EXISTS _mcp_migrations (
4276
3712
  applied_by TEXT
4277
3713
  );
4278
3714
  `.trim();
4279
- function normaliseMigrationSql(sql4) {
4280
- return sql4.replace(/\r\n/g, "\n").trim() + "\n";
3715
+ function normaliseMigrationSql(sql3) {
3716
+ return sql3.replace(/\r\n/g, "\n").trim() + "\n";
4281
3717
  }
4282
- function migrationSha256(sql4) {
4283
- return createHash("sha256").update(normaliseMigrationSql(sql4), "utf8").digest("hex");
3718
+ function migrationSha256(sql3) {
3719
+ return createHash("sha256").update(normaliseMigrationSql(sql3), "utf8").digest("hex");
4284
3720
  }
4285
3721
  function dollarQuoteTag(value) {
4286
3722
  let tag = "_mcp";
@@ -4881,8 +4317,6 @@ var TOOLS = [
4881
4317
  },
4882
4318
  // ----- Trigger.dev -----
4883
4319
  ...TRIGGER_TOOLS,
4884
- // ----- Vercel -----
4885
- ...VERCEL_TOOLS,
4886
4320
  // ----- Repo reference -----
4887
4321
  ...REPO_TOOLS
4888
4322
  ];
@@ -6363,9 +5797,7 @@ LIMIT ${limit};
6363
5797
  `);
6364
5798
  saveMsg = `Stored env config: ${appName}/${environment}`;
6365
5799
  }
6366
- const syncStageId = existing?.release_profile_stage_id ?? resolvedStageIds?.[0];
6367
- const vercelStatus = await attemptVercelSync(appName, environment, syncStageId);
6368
- return { content: [{ type: "text", text: `${saveMsg}. ${vercelStatus}` }] };
5800
+ return { content: [{ type: "text", text: saveMsg }] };
6369
5801
  }
6370
5802
  // ----- Cache Purge -----
6371
5803
  case "cache-purge": {
@@ -6626,9 +6058,6 @@ ${lines.join("\n")}` }] };
6626
6058
  if (TRIGGER_TOOL_NAMES.has(name)) {
6627
6059
  return handleTriggerTool(name, a, { sshExec, getServerConnection });
6628
6060
  }
6629
- if (VERCEL_TOOL_NAMES.has(name)) {
6630
- return handleVercelTool(name, a, { db, decrypt });
6631
- }
6632
6061
  if (REPO_TOOL_NAMES.has(name)) {
6633
6062
  return handleRepoTool(name, a, { db, sshExec, getServerConnection });
6634
6063
  }