@mgsoftwarebv/mg-dashboard-mcp 6.1.0 → 6.2.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
@@ -1191,7 +1191,7 @@ async function handleVercelTool(name, args2, deps) {
1191
1191
  sinceMs
1192
1192
  );
1193
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 the supabase MCP (vercel_deployment_log table) for archived runtime logs.' : "";
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
1195
  return { content: [{ type: "text", text: `Error: ${error}${hint}` }] };
1196
1196
  }
1197
1197
  const body = formatRuntimeLogs(logs);
@@ -1199,7 +1199,7 @@ async function handleVercelTool(name, args2, deps) {
1199
1199
  const footer = hitDurationLimit ? `
1200
1200
 
1201
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" / the supabase MCP vercel_deployment_log table for archived logs.` : `
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
1203
 
1204
1204
  [${windowNote}]`;
1205
1205
  return { content: [{ type: "text", text: body + footer }] };
@@ -2420,6 +2420,10 @@ function assertServerAccess(serverId) {
2420
2420
  throw new Error(`Access denied: you do not have permission for server ${serverId}`);
2421
2421
  }
2422
2422
  }
2423
+ function uuidArrayParam(values) {
2424
+ if (values.length === 0) return sql`ARRAY[]::uuid[]`;
2425
+ return sql`ARRAY[${sql.join(values.map((v) => sql`${v}::uuid`), sql`, `)}]`;
2426
+ }
2423
2427
  async function resolveReleaseProfileStageIds(profileName) {
2424
2428
  const profileRows = await db.execute(sql`
2425
2429
  SELECT id, name FROM release_profile
@@ -2449,13 +2453,13 @@ async function getProfileNamesForStageIds(stageIds) {
2449
2453
  if (stageIds.length === 0) return {};
2450
2454
  const stages = await db.execute(sql`
2451
2455
  SELECT id, release_profile_id FROM release_profile_stage
2452
- WHERE id = ANY(${stageIds}::uuid[])
2456
+ WHERE id = ANY(${uuidArrayParam(stageIds)})
2453
2457
  `);
2454
2458
  if (stages.length === 0) return {};
2455
2459
  const profileIds = [...new Set(stages.map((s) => s.release_profile_id))];
2456
2460
  const profiles = await db.execute(sql`
2457
2461
  SELECT id, name FROM release_profile
2458
- WHERE id = ANY(${profileIds}::uuid[])
2462
+ WHERE id = ANY(${uuidArrayParam(profileIds)})
2459
2463
  `);
2460
2464
  const profileMap = {};
2461
2465
  for (const p of profiles) profileMap[p.id] = p.name;
@@ -2690,7 +2694,35 @@ async function getProxyConnection() {
2690
2694
  };
2691
2695
  return _proxyConnCache;
2692
2696
  }
2693
- async function getServerConnection(serverId) {
2697
+ var UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
2698
+ async function resolveServerId(input) {
2699
+ const trimmed = (input ?? "").trim();
2700
+ if (!trimmed) throw new Error("serverId is required");
2701
+ if (UUID_RE.test(trimmed)) return trimmed;
2702
+ const exact = await db.execute(sql`
2703
+ SELECT id, name FROM ssh_server WHERE name = ${trimmed} LIMIT 2
2704
+ `);
2705
+ if (exact.length === 1) {
2706
+ KNOWN_SERVER_NAMES.set(exact[0].id, { id: exact[0].id, name: exact[0].name });
2707
+ return exact[0].id;
2708
+ }
2709
+ if (exact.length > 1) {
2710
+ throw new Error(`Multiple servers share the exact name "${trimmed}". Pass the UUID instead.`);
2711
+ }
2712
+ const fuzzy = await db.execute(sql`
2713
+ SELECT id, name FROM ssh_server WHERE name ILIKE ${"%" + trimmed + "%"} ORDER BY name LIMIT 5
2714
+ `);
2715
+ if (fuzzy.length === 1) {
2716
+ KNOWN_SERVER_NAMES.set(fuzzy[0].id, { id: fuzzy[0].id, name: fuzzy[0].name });
2717
+ return fuzzy[0].id;
2718
+ }
2719
+ if (fuzzy.length === 0) {
2720
+ throw new Error(`Server "${trimmed}" not found by id or name. Call \`list-servers\` to see what's available.`);
2721
+ }
2722
+ throw new Error(`Multiple servers match "${trimmed}": ${fuzzy.map((f) => f.name).join(", ")}. Use an exact name or UUID.`);
2723
+ }
2724
+ async function getServerConnection(serverIdOrName) {
2725
+ const serverId = await resolveServerId(serverIdOrName);
2694
2726
  assertServerAccess(serverId);
2695
2727
  const rows = await db.execute(sql`
2696
2728
  SELECT hostname, port, username, password_encrypted, ssh_key_encrypted,
@@ -4442,7 +4474,7 @@ async function mijnhostFetch(path, options = {}) {
4442
4474
  var TOOLS = [
4443
4475
  {
4444
4476
  name: "list-servers",
4445
- description: 'List all SSH servers you have access to. Returns id, name, hostname, tags, and os_type per server. Pass `includeStats: true` to also probe each server in parallel for container count, disk-free, and uptime (skips unreachable hosts gracefully). Cached for 60s \u2014 pass `noCache: true` to force a refresh. Adds an "auto-context" footer with one-line tags inferred from server name/tags so the LLM knows which host runs Trigger / Supabase / proxy / etc. (disable with `context: false`).',
4477
+ description: 'List all SSH servers you have access to. Returns id, name, hostname, tags, and os_type per server. Pass `includeStats: true` to also probe each server in parallel for container count, disk-free, and uptime (skips unreachable hosts gracefully). Cached for 60s \u2014 pass `noCache: true` to force a refresh. Adds an "auto-context" footer with one-line tags inferred from server name/tags so the LLM knows which host runs Trigger / Supabase / proxy / etc. (disable with `context: false`).\n\n**Tip:** every tool that takes `serverId` also accepts the server `name` directly (exact match or fuzzy `ILIKE %x%`). E.g. `ssh-execute serverId="MG Giga Server"` works without a separate UUID lookup.',
4446
4478
  inputSchema: {
4447
4479
  type: "object",
4448
4480
  properties: {
@@ -4670,7 +4702,7 @@ var TOOLS = [
4670
4702
  },
4671
4703
  {
4672
4704
  name: "db-query",
4673
- description: 'Execute SQL against any database. **Use this for ALL queries (SELECT, INSERT, UPDATE, DELETE, schema introspection) against vanilla Postgres / MySQL / MSSQL containers** \u2014 do NOT fall back to `ssh-execute` + `docker cp` + `psql -f` with .tmp.sql files; the container path here already pipes via stdin so quotes / `$` / `;` are safe. Multi-statement scripts work fine: separate with `;` and they will all be executed by psql/mysql.\n\nTwo routing modes:\n- MySQL via `/var/www` autodiscover (default): pass `sitePath` (and `engine: "mysql"` implicitly). Credentials are read from wp-config.php, parameters.php, .env.\n- Direct via Docker container: pass `containerName` (the container running the DB server). Engine defaults to `postgres`; pass `engine: "mysql"` or `"mssql"` to override. Use `db-discover include=["postgres-containers"]` to find available containers + creds.\n\nPostgres example: `{ serverId, containerName: "refront-postgres-vanilla", dbName: "main", dbUser: "postgres", query: "SELECT 1" }`. Defaults: dbUser=postgres, dbName=postgres.\nMSSQL example: `{ serverId, containerName: "mssql-1", engine: "mssql", dbUser: "sa", dbPass: "...", query: "SELECT 1" }`.\n\nResult safety: `maxRows` (default 1000, max 10000) auto-injects a `LIMIT` for SELECT queries that don\'t have one. The footer reports whether the cap was applied. Pass `maxRows: 0` to disable.\n\nDiagnostics: `explain: true` prepends EXPLAIN (postgres / mysql) or runs `SET SHOWPLAN_TEXT ON` (mssql).\n\nSchema introspection: pass `describe: "tablename"` to get columns + indexes (works for mysql, postgres, and mssql). Pass `describe: "*"` to list all tables. When `describe` is set, `query` is ignored.\n\nDestructive operations (DROP, TRUNCATE, ALTER \u2026 DROP, naked DELETE) are blocked by default. For schema migrations, **prefer `db-apply-migration`** \u2014 it has an audit ledger and idempotency. As an escape hatch for ad-hoc hot-fixes, pass `allowDestructive: "yes-i-understand-this-is-not-logged"` (literal string) to bypass the gate. The bypass is logged in the response footer.',
4705
+ description: 'Execute SQL against any database. **Use this for ALL queries (SELECT, INSERT, UPDATE, DELETE, schema introspection) against vanilla Postgres / MySQL / MSSQL containers** \u2014 do NOT fall back to `ssh-execute` + `docker cp` + `psql -f` with .tmp.sql files; the container path here already pipes via stdin so quotes / `$` / `;` are safe. Multi-statement scripts work fine: separate with `;` and they will all be executed by psql/mysql.\n\nTwo routing modes:\n- MySQL via `/var/www` autodiscover (default): pass `sitePath` (and `engine: "mysql"` implicitly). Credentials are read from wp-config.php, parameters.php, .env.\n- Direct via Docker container: pass `containerName` (the container running the DB server). Engine defaults to `postgres`; pass `engine: "mysql"` or `"mssql"` to override. Use `db-discover include=["postgres-containers"]` to find available containers + creds.\n\nPostgres example: `{ serverId, containerName: "refront-postgres-vanilla", dbName: "main", dbUser: "postgres", query: "SELECT 1" }`. Defaults: dbUser=postgres, dbName=postgres.\nMSSQL example: `{ serverId, containerName: "mssql-1", engine: "mssql", dbUser: "sa", dbPass: "...", query: "SELECT 1" }`.\n\nResult safety: `maxRows` (default 1000, max 10000) auto-injects a `LIMIT` for SELECT queries that don\'t have one. The footer reports whether the cap was applied. Pass `maxRows: 0` to disable.\n\nDiagnostics: `explain: true` prepends EXPLAIN (postgres / mysql) or runs `SET SHOWPLAN_TEXT ON` (mssql).\n\nAtomic multi-statement: pass `transaction: true` to wrap the query in BEGIN/COMMIT (postgres / mysql) so a failure in any statement rolls back the lot. Use for ad-hoc fixes that are not schema migrations (e.g. `UPDATE tax_rate SET pct = pct * 100; UPDATE order SET ...`).\n\nSchema introspection: pass `describe: "tablename"` to get columns + indexes (works for mysql, postgres, and mssql). Pass `describe: "*"` to list all tables. When `describe` is set, `query` is ignored.\n\nDestructive operations (DROP, TRUNCATE, ALTER \u2026 DROP, naked DELETE) are blocked by default. For schema migrations, **prefer `db-apply-migration`** \u2014 it has an audit ledger and idempotency. As an escape hatch for ad-hoc hot-fixes, pass `allowDestructive: "yes-i-understand-this-is-not-logged"` (literal string) to bypass the gate. The bypass is logged in the response footer.',
4674
4706
  inputSchema: {
4675
4707
  type: "object",
4676
4708
  properties: {
@@ -4685,7 +4717,8 @@ var TOOLS = [
4685
4717
  dbPass: { type: "string", description: "Database password (containerName mode). Required for mssql; optional for mysql; postgres uses trust auth by default." },
4686
4718
  maxRows: { type: "number", description: "Auto-inject LIMIT for unbounded SELECTs. Default 1000, max 10000, set 0 to disable." },
4687
4719
  explain: { type: "boolean", description: "Prepend EXPLAIN (postgres/mysql) or SHOWPLAN (mssql) instead of executing. Useful for slow-query diagnosis." },
4688
- allowDestructive: { type: "string", description: 'Escape hatch for ad-hoc DROP/TRUNCATE/ALTER\u2026DROP/naked DELETE. Pass the literal string "yes-i-understand-this-is-not-logged". For schema migrations prefer db-apply-migration (audit-logged, idempotent).' }
4720
+ allowDestructive: { type: "string", description: 'Escape hatch for ad-hoc DROP/TRUNCATE/ALTER\u2026DROP/naked DELETE. Pass the literal string "yes-i-understand-this-is-not-logged". For schema migrations prefer db-apply-migration (audit-logged, idempotent).' },
4721
+ transaction: { type: "boolean", description: "Wrap the query in BEGIN/COMMIT so multiple statements either all succeed or all roll back. Use for atomic ad-hoc updates like `UPDATE x SET y = y * 100; UPDATE z ...` that are not migration-worthy. Ignored in EXPLAIN mode and on MSSQL (use its own transaction syntax). Postgres/MySQL only." }
4689
4722
  },
4690
4723
  required: ["serverId"]
4691
4724
  }
@@ -4705,7 +4738,8 @@ var TOOLS = [
4705
4738
  localFile: { type: "string", description: "Path on this machine to a .sql file (read here, streamed via stdin)" },
4706
4739
  remoteFile: { type: "string", description: "Absolute path of a .sql file already on the server (read with `cat`)" },
4707
4740
  noTransaction: { type: "boolean", description: "Skip the implicit BEGIN/COMMIT wrapper. Required for CONCURRENTLY / ALTER TYPE ADD VALUE / VACUUM. Default false." },
4708
- force: { type: "boolean", description: "Re-apply even when the ledger says it ran with a different sha256. Use after a deliberate edit; logged in the response." }
4741
+ force: { type: "boolean", description: "Re-apply even when the ledger says it ran with a different sha256. Use after a deliberate edit; logged in the response." },
4742
+ recordOnly: { type: "boolean", description: "Register the migration in the ledger WITHOUT executing the SQL. Use to backfill migrations that were applied out-of-band (e.g. by hand via ssh + psql). The SQL is still required so the recorded sha256 matches what was run." }
4709
4743
  },
4710
4744
  required: ["serverId", "containerName", "name"]
4711
4745
  }
@@ -4824,7 +4858,7 @@ var TOOLS = [
4824
4858
  // ----- Repo reference -----
4825
4859
  ...REPO_TOOLS
4826
4860
  ];
4827
- var MCP_VERSION = "6.1.0";
4861
+ var MCP_VERSION = "6.2.0";
4828
4862
  async function handleListTools() {
4829
4863
  if (!authContext) return { tools: TOOLS };
4830
4864
  const accessible = TOOLS.filter((tool) => {
@@ -4892,7 +4926,7 @@ async function executeToolCall(name, a, _serverId) {
4892
4926
  const data = ctx.allowedServerIds !== null ? await db.execute(sql`
4893
4927
  SELECT id, name, hostname, port, username, tags, hosted_by, os_type, created_at
4894
4928
  FROM ssh_server
4895
- WHERE id = ANY(${ctx.allowedServerIds}::uuid[])
4929
+ WHERE id = ANY(${uuidArrayParam(ctx.allowedServerIds)})
4896
4930
  ORDER BY name
4897
4931
  `) : await db.execute(sql`
4898
4932
  SELECT id, name, hostname, port, username, tags, hosted_by, os_type, created_at
@@ -5878,6 +5912,7 @@ For a one-off ad-hoc destructive query, pass \`allowDestructive: "${allowDestruc
5878
5912
  }
5879
5913
  let query = rawQuery.replace(/;\s*$/, "");
5880
5914
  let appliedLimit = false;
5915
+ const wrapTransaction = a.transaction === true && !explainMode && !describeArg && engine !== "mssql";
5881
5916
  if (explainMode && !describeArg) {
5882
5917
  if (engine === "mssql") {
5883
5918
  query = `SET SHOWPLAN_TEXT ON;
@@ -5894,13 +5929,21 @@ LIMIT ${maxRows + 1}`;
5894
5929
  appliedLimit = true;
5895
5930
  }
5896
5931
  }
5932
+ if (wrapTransaction) {
5933
+ query = `BEGIN;
5934
+ ${query.replace(/;\s*$/, "")};
5935
+ COMMIT;`;
5936
+ }
5937
+ const txBanner = wrapTransaction ? `
5938
+
5939
+ [transaction] wrapped in BEGIN/COMMIT \u2014 all statements committed atomically.` : "";
5897
5940
  const destructiveBanner = allowDestructive ? "\n\n[allowDestructive] safety gate bypassed for this query \u2014 NOT recorded in any audit ledger. Consider db-apply-migration for repeatable schema changes." : "";
5898
5941
  if (!containerName && engine === "mysql") {
5899
5942
  if (!a.sitePath) return { content: [{ type: "text", text: "Error: sitePath is required for mysql autodiscover (or pass containerName + engine for direct DB queries)" }] };
5900
5943
  const { conn: conn2, proxy: proxy2 } = await getServerConnection(String(a.serverId));
5901
5944
  const output2 = await execSiteMysql(conn2, String(a.sitePath), query, proxy2);
5902
5945
  const footer2 = formatDbQueryFooter(output2, appliedLimit, maxRows, explainMode);
5903
- return { content: [{ type: "text", text: (output2 || "Query executed successfully (no output)") + footer2 + destructiveBanner }] };
5946
+ return { content: [{ type: "text", text: (output2 || "Query executed successfully (no output)") + footer2 + destructiveBanner + txBanner }] };
5904
5947
  }
5905
5948
  if (!containerName) {
5906
5949
  return { content: [{ type: "text", text: `Error: engine=${engine} requires containerName (the docker container running the DB server, e.g. "supabase-db" or "trigger-postgres")` }] };
@@ -5940,7 +5983,7 @@ GO
5940
5983
  return { content: [{ type: "text", text: `Error (exit ${result.exitCode}, ${engine}): ${output}` }] };
5941
5984
  }
5942
5985
  const footer = formatDbQueryFooter(output, appliedLimit, maxRows, explainMode);
5943
- return { content: [{ type: "text", text: output + footer + destructiveBanner }] };
5986
+ return { content: [{ type: "text", text: output + footer + destructiveBanner + txBanner }] };
5944
5987
  }
5945
5988
  case "db-apply-migration": {
5946
5989
  const containerName = String(a.containerName || "").replace(/[^a-zA-Z0-9._-]/g, "");
@@ -5954,6 +5997,9 @@ GO
5954
5997
  }
5955
5998
  const noTransaction = a.noTransaction === true;
5956
5999
  const force = a.force === true;
6000
+ const recordOnly = a.recordOnly === true;
6001
+ const t0 = Date.now();
6002
+ const took = () => `${((Date.now() - t0) / 1e3).toFixed(2)}s`;
5957
6003
  const sources = [
5958
6004
  typeof a.sql === "string" && a.sql ? "sql" : null,
5959
6005
  typeof a.localFile === "string" && a.localFile ? "localFile" : null,
@@ -5992,20 +6038,23 @@ GO
5992
6038
  return { content: [{ type: "text", text: "Error: resolved SQL is empty" }] };
5993
6039
  }
5994
6040
  const sha = migrationSha256(migrationSql);
5995
- const probeSql = `${MIGRATION_LEDGER_DDL}
5996
- SELECT sha256 FROM _mcp_migrations WHERE name = ${dollarQuote(name2)};
6041
+ const ddl = await psqlInContainer(conn, proxy, containerName, dbUser, dbName, `${MIGRATION_LEDGER_DDL}
6042
+ `, "");
6043
+ if (ddl.exitCode !== 0) {
6044
+ return { content: [{ type: "text", text: `Error initialising ledger on ${containerName} (${dbName}): ${ddl.stderr || ddl.stdout || `exit ${ddl.exitCode}`}` }] };
6045
+ }
6046
+ const probeSelect = `SELECT sha256 FROM _mcp_migrations WHERE name = ${dollarQuote(name2)};
5997
6047
  `;
5998
- const probe = await psqlInContainer(conn, proxy, containerName, dbUser, dbName, probeSql, "-tA");
6048
+ const probe = await psqlInContainer(conn, proxy, containerName, dbUser, dbName, probeSelect, "-tA");
5999
6049
  if (probe.exitCode !== 0) {
6000
- return { content: [{ type: "text", text: `Error initialising ledger on ${containerName} (${dbName}): ${probe.stderr || probe.stdout || `exit ${probe.exitCode}`}` }] };
6050
+ return { content: [{ type: "text", text: `Error reading ledger on ${containerName} (${dbName}): ${probe.stderr || probe.stdout || `exit ${probe.exitCode}`}` }] };
6001
6051
  }
6002
- const probeLines = probe.stdout.split("\n").map((l) => l.trim()).filter((l) => l.length > 0);
6003
- const existingSha = probeLines.length > 0 ? probeLines[probeLines.length - 1] : "";
6052
+ const existingSha = probe.stdout.split("\n").map((l) => l.trim()).find((l) => /^[0-9a-f]{64}$/.test(l)) || "";
6004
6053
  if (existingSha && existingSha === sha) {
6005
6054
  return {
6006
6055
  content: [{
6007
6056
  type: "text",
6008
- text: `[noop] Migration "${name2}" already applied on ${containerName}/${dbName} (sha256 matches).
6057
+ text: `[noop] Migration "${name2}" already applied on ${containerName}/${dbName} (sha256 matches, took ${took()}).
6009
6058
  Ledger sha: ${sha.slice(0, 12)}\u2026`
6010
6059
  }]
6011
6060
  };
@@ -6014,7 +6063,7 @@ Ledger sha: ${sha.slice(0, 12)}\u2026`
6014
6063
  return {
6015
6064
  content: [{
6016
6065
  type: "text",
6017
- text: `[drift] Migration "${name2}" exists in ledger with DIFFERENT sha256.
6066
+ text: `[drift] Migration "${name2}" exists in ledger with DIFFERENT sha256 (took ${took()}).
6018
6067
  ledger sha: ${existingSha.slice(0, 12)}\u2026
6019
6068
  new sha: ${sha.slice(0, 12)}\u2026
6020
6069
 
@@ -6026,6 +6075,29 @@ This usually means the migration file was edited after it was first applied. Ins
6026
6075
  const ledgerInsert = `INSERT INTO _mcp_migrations (name, sha256, applied_by) VALUES (${dollarQuote(name2)}, ${dollarQuote(sha)}, ${dollarQuote(appliedBy)})
6027
6076
  ON CONFLICT (name) DO UPDATE SET sha256 = EXCLUDED.sha256, applied_at = now(), applied_by = EXCLUDED.applied_by;
6028
6077
  `;
6078
+ if (recordOnly) {
6079
+ const record = await psqlInContainer(conn, proxy, containerName, dbUser, dbName, ledgerInsert, "");
6080
+ if (record.exitCode !== 0) {
6081
+ return {
6082
+ content: [{
6083
+ type: "text",
6084
+ text: `[failed] Ledger insert for "${name2}" failed (recordOnly=true, no SQL was executed).
6085
+ psql exit: ${record.exitCode}
6086
+ ` + (record.stderr ? `--- stderr ---
6087
+ ${record.stderr}
6088
+ ` : "") + (record.stdout ? `--- stdout ---
6089
+ ${record.stdout}` : "")
6090
+ }]
6091
+ };
6092
+ }
6093
+ return {
6094
+ content: [{
6095
+ type: "text",
6096
+ text: `[recorded] "${name2}" written to ledger on ${containerName}/${dbName} WITHOUT executing the SQL (recordOnly=true${existingSha ? ", overwrote existing entry" : ""}, took ${took()}).
6097
+ ledger sha: ${sha.slice(0, 12)}\u2026`
6098
+ }]
6099
+ };
6100
+ }
6029
6101
  if (noTransaction) {
6030
6102
  const apply = await psqlInContainer(conn, proxy, containerName, dbUser, dbName, normalised, "");
6031
6103
  if (apply.exitCode !== 0) {
@@ -6058,7 +6130,7 @@ ${record.stdout}` : "")
6058
6130
  return {
6059
6131
  content: [{
6060
6132
  type: "text",
6061
- text: `[applied] "${name2}" on ${containerName}/${dbName} (noTransaction=true${existingSha ? ", force" : ""}).
6133
+ text: `[applied] "${name2}" on ${containerName}/${dbName} (noTransaction=true${existingSha ? ", force" : ""}, took ${took()}).
6062
6134
  ledger sha: ${sha.slice(0, 12)}\u2026
6063
6135
  ` + (apply.stdout ? `--- output ---
6064
6136
  ${apply.stdout.trim()}` : "(no output)")
@@ -6086,7 +6158,7 @@ ${res.stdout}` : "")
6086
6158
  return {
6087
6159
  content: [{
6088
6160
  type: "text",
6089
- text: `[applied] "${name2}" on ${containerName}/${dbName}${existingSha ? " (forced overwrite)" : ""}.
6161
+ text: `[applied] "${name2}" on ${containerName}/${dbName}${existingSha ? " (forced overwrite)" : ""} (took ${took()}).
6090
6162
  ledger sha: ${sha.slice(0, 12)}\u2026
6091
6163
  ` + (res.stdout ? `--- output ---
6092
6164
  ${res.stdout.trim()}` : "(no output)")
@@ -6101,8 +6173,7 @@ ${res.stdout.trim()}` : "(no output)")
6101
6173
  const limitRaw = Number(a.limit);
6102
6174
  const limit = Math.min(Math.max(Number.isFinite(limitRaw) && limitRaw > 0 ? Math.floor(limitRaw) : 50, 1), 500);
6103
6175
  const { conn, proxy } = await getServerConnection(String(a.serverId));
6104
- const sqlText = `DO $$ BEGIN IF NOT EXISTS (SELECT 1 FROM pg_class WHERE relname = '_mcp_migrations') THEN RAISE NOTICE 'NO_LEDGER'; END IF; END $$;
6105
- SELECT name, substr(sha256, 1, 12) || '\u2026' AS sha, applied_at, applied_by
6176
+ const sqlText = `SELECT name, sha256, applied_at, applied_by
6106
6177
  FROM _mcp_migrations
6107
6178
  ORDER BY applied_at DESC
6108
6179
  LIMIT ${limit};
@@ -6127,7 +6198,7 @@ LIMIT ${limit};
6127
6198
  const data = stageFilterIds ? await db.execute(sql`
6128
6199
  SELECT id, app_name, environment, description, updated_at, release_profile_stage_id
6129
6200
  FROM env_config
6130
- WHERE release_profile_stage_id = ANY(${stageFilterIds}::uuid[])
6201
+ WHERE release_profile_stage_id = ANY(${uuidArrayParam(stageFilterIds)})
6131
6202
  ORDER BY app_name, environment
6132
6203
  `) : await db.execute(sql`
6133
6204
  SELECT id, app_name, environment, description, updated_at, release_profile_stage_id
@@ -6155,7 +6226,7 @@ LIMIT ${limit};
6155
6226
  FROM env_config
6156
6227
  WHERE app_name = ${appName}
6157
6228
  AND environment = ${environment}
6158
- AND release_profile_stage_id = ANY(${stageFilterIds}::uuid[])
6229
+ AND release_profile_stage_id = ANY(${uuidArrayParam(stageFilterIds)})
6159
6230
  `) : await db.execute(sql`
6160
6231
  SELECT env_data_encrypted, release_profile_stage_id
6161
6232
  FROM env_config
@@ -6190,7 +6261,7 @@ LIMIT ${limit};
6190
6261
  FROM env_config
6191
6262
  WHERE app_name = ${appName}
6192
6263
  AND environment = ${environment}
6193
- AND release_profile_stage_id = ANY(${resolvedStageIds}::uuid[])
6264
+ AND release_profile_stage_id = ANY(${uuidArrayParam(resolvedStageIds)})
6194
6265
  `) : await db.execute(sql`
6195
6266
  SELECT id, release_profile_stage_id
6196
6267
  FROM env_config