@mgsoftwarebv/mg-dashboard-mcp 6.2.0 → 6.3.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
@@ -2750,7 +2750,7 @@ async function getServerConnection(serverIdOrName) {
2750
2750
  const needsProxy = data.allowed_ssh_ips !== null && serverId !== SSH_PROXY_SERVER_ID;
2751
2751
  const proxy = needsProxy ? await getProxyConnection() : void 0;
2752
2752
  const os = data.os_type === "windows" ? "windows" : "linux";
2753
- return { conn, proxy, os };
2753
+ return { serverId, conn, proxy, os };
2754
2754
  }
2755
2755
  async function sshExec(opts, command, proxy, options) {
2756
2756
  const first = await sshExecOnce(opts, command, proxy, options);
@@ -4232,6 +4232,34 @@ done
4232
4232
  }
4233
4233
  return out;
4234
4234
  }
4235
+ var POSTGRES_CONTAINER_CREDS_CACHE = /* @__PURE__ */ new Map();
4236
+ var PG_CREDS_TTL_MS = 60 * 60 * 1e3;
4237
+ function cachePostgresContainerCreds(serverId, containerName, creds) {
4238
+ POSTGRES_CONTAINER_CREDS_CACHE.set(`${serverId}|${containerName}`, { ...creds, capturedAt: Date.now() });
4239
+ }
4240
+ async function resolvePostgresContainerCreds(conn, proxy, serverId, containerName) {
4241
+ const key = `${serverId}|${containerName}`;
4242
+ const cached = POSTGRES_CONTAINER_CREDS_CACHE.get(key);
4243
+ if (cached && Date.now() - cached.capturedAt < PG_CREDS_TTL_MS) return cached;
4244
+ const safeContainer = containerName.replace(/[^a-zA-Z0-9._-]/g, "");
4245
+ const res = await sshExec(
4246
+ conn,
4247
+ `docker exec ${safeContainer} env 2>/dev/null | grep -E '^POSTGRES_(USER|DB|PASSWORD)='`,
4248
+ proxy
4249
+ );
4250
+ let dbUser = "postgres";
4251
+ let dbName = "postgres";
4252
+ let hasPassword = false;
4253
+ for (const raw of res.stdout.split("\n")) {
4254
+ const line = raw.trim();
4255
+ if (line.startsWith("POSTGRES_USER=")) dbUser = line.slice("POSTGRES_USER=".length) || "postgres";
4256
+ else if (line.startsWith("POSTGRES_DB=")) dbName = line.slice("POSTGRES_DB=".length) || "postgres";
4257
+ else if (line.startsWith("POSTGRES_PASSWORD=")) hasPassword = true;
4258
+ }
4259
+ const creds = { dbName, dbUser, hasPassword, capturedAt: Date.now() };
4260
+ POSTGRES_CONTAINER_CREDS_CACHE.set(key, creds);
4261
+ return creds;
4262
+ }
4235
4263
  async function psqlInContainer(conn, proxy, containerName, dbUser, dbName, scriptSql, flags) {
4236
4264
  const safeContainer = containerName.replace(/[^a-zA-Z0-9._-]/g, "");
4237
4265
  const safeUser = dbUser.replace(/[^a-zA-Z0-9_-]/g, "") || "postgres";
@@ -4687,22 +4715,22 @@ var TOOLS = [
4687
4715
  },
4688
4716
  {
4689
4717
  name: "db-tables",
4690
- description: "List tables with row counts + sizes. Two routing modes:\n- MySQL via `/var/www` autodiscover: pass `sitePath`. Credentials are read from wp-config.php / parameters.php / .env.\n- Postgres container: pass `containerName` (and optionally `dbName` / `dbUser`, defaulting to `postgres`). Returns one row per user table sorted by total size, with estimated row count from pg_class.reltuples.",
4718
+ description: "List tables with row counts + sizes. Two routing modes:\n- MySQL via `/var/www` autodiscover: pass `sitePath`. Credentials are read from wp-config.php / parameters.php / .env.\n- Postgres container: pass `containerName`. dbName + dbUser are auto-resolved from the container `POSTGRES_DB` / `POSTGRES_USER` env vars (cached 1h). Returns one row per user table sorted by total size, with estimated row count from pg_class.reltuples.",
4691
4719
  inputSchema: {
4692
4720
  type: "object",
4693
4721
  properties: {
4694
- serverId: { type: "string", description: "UUID of the SSH server" },
4722
+ serverId: { type: "string", description: "UUID or (fuzzy) name of the SSH server" },
4695
4723
  sitePath: { type: "string", description: "Site root path (e.g. /var/www/example.com). MySQL autodiscover mode." },
4696
4724
  containerName: { type: "string", description: "Postgres container name. Activates direct Postgres mode." },
4697
- dbName: { type: "string", description: 'Database name (containerName mode, defaults to "postgres")' },
4698
- dbUser: { type: "string", description: 'Database user (containerName mode, defaults to "postgres")' }
4725
+ dbName: { type: "string", description: "Database name (containerName mode). Auto-resolved from container env when omitted." },
4726
+ dbUser: { type: "string", description: "Database user (containerName mode). Auto-resolved from container env when omitted." }
4699
4727
  },
4700
4728
  required: ["serverId"]
4701
4729
  }
4702
4730
  },
4703
4731
  {
4704
4732
  name: "db-query",
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.',
4733
+ 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", query: "SELECT 1" }` \u2014 dbName and dbUser are AUTO-RESOLVED from the container\'s `POSTGRES_USER` / `POSTGRES_DB` env vars (cached 1h per server). Only pass `dbName`/`dbUser` explicitly when the instance hosts multiple databases or you want to query as a non-default role.\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.',
4706
4734
  inputSchema: {
4707
4735
  type: "object",
4708
4736
  properties: {
@@ -4712,8 +4740,8 @@ var TOOLS = [
4712
4740
  sitePath: { type: "string", description: "Site root path (e.g. /var/www/example.com). Used only with engine=mysql autodiscover." },
4713
4741
  containerName: { type: "string", description: 'Docker container running the DB server (e.g. "refront-postgres-vanilla", "supabase-db"). Activates direct-query mode.' },
4714
4742
  engine: { type: "string", enum: ["mysql", "postgres", "mssql"], description: 'DB engine. Defaults to "mysql" with sitePath, "postgres" with containerName.' },
4715
- dbName: { type: "string", description: 'Database name (containerName mode). Defaults: postgres \u2192 "postgres", mysql \u2192 server default, mssql \u2192 server default.' },
4716
- dbUser: { type: "string", description: 'Database user (containerName mode). Defaults: postgres \u2192 "postgres", mysql \u2192 "root", mssql \u2192 "sa".' },
4743
+ dbName: { type: "string", description: "Database name (containerName mode). For postgres: auto-resolved from container `POSTGRES_DB` env var when omitted. For mysql: server default. For mssql: server default." },
4744
+ dbUser: { type: "string", description: 'Database user (containerName mode). For postgres: auto-resolved from container `POSTGRES_USER` env var when omitted. For mysql: defaults to "root". For mssql: defaults to "sa".' },
4717
4745
  dbPass: { type: "string", description: "Database password (containerName mode). Required for mssql; optional for mysql; postgres uses trust auth by default." },
4718
4746
  maxRows: { type: "number", description: "Auto-inject LIMIT for unbounded SELECTs. Default 1000, max 10000, set 0 to disable." },
4719
4747
  explain: { type: "boolean", description: "Prepend EXPLAIN (postgres/mysql) or SHOWPLAN (mssql) instead of executing. Useful for slow-query diagnosis." },
@@ -4725,14 +4753,14 @@ var TOOLS = [
4725
4753
  },
4726
4754
  {
4727
4755
  name: "db-apply-migration",
4728
- description: 'Apply a SQL migration to a Postgres container and record it in an MCP-managed ledger table (`_mcp_migrations`) so re-runs are idempotent. Use this for ALL schema changes (CREATE/ALTER/DROP/TRUNCATE) \u2014 it allows DDL that `db-query` blocks, and gives you audit + drift detection in exchange.\n\nLedger schema (auto-created on first call): `_mcp_migrations(name TEXT PRIMARY KEY, sha256 TEXT, applied_at TIMESTAMPTZ, applied_by TEXT)`.\n\nBehaviour:\n- If `name` is already in the ledger AND the sha256 of the supplied SQL matches \u2192 no-op, returns "already applied".\n- If `name` is already in the ledger with a DIFFERENT sha256 \u2192 fails loudly ("drift detected"). Pass `force: true` to overwrite (logged).\n- Otherwise the SQL is wrapped in BEGIN/COMMIT and applied, then a row is inserted into `_mcp_migrations`.\n\nPass `noTransaction: true` for statements that cannot run inside a transaction (`CREATE INDEX CONCURRENTLY`, `ALTER TYPE \u2026 ADD VALUE`, `VACUUM`, etc.). In that mode the user SQL runs first and the ledger is recorded in a separate follow-up call.\n\nSQL source: provide ONE of `sql` (inline string), `localFile` (path on this machine, read and piped via stdin), or `remoteFile` (absolute path on the SSH server, read with `cat` first).\n\nExample: `{ serverId, containerName: "refront-postgres-vanilla", dbName: "main", name: "20260517120000_add_unified_classification", localFile: "./migrations/20260517120000_add_unified_classification.sql" }`.',
4756
+ description: 'Apply a SQL migration to a Postgres container and record it in an MCP-managed ledger table (`_mcp_migrations`) so re-runs are idempotent. Use this for ALL schema changes (CREATE/ALTER/DROP/TRUNCATE) \u2014 it allows DDL that `db-query` blocks, and gives you audit + drift detection in exchange.\n\nLedger schema (auto-created on first call): `_mcp_migrations(name TEXT PRIMARY KEY, sha256 TEXT, applied_at TIMESTAMPTZ, applied_by TEXT)`.\n\nBehaviour:\n- If `name` is already in the ledger AND the sha256 of the supplied SQL matches \u2192 no-op, returns "already applied".\n- If `name` is already in the ledger with a DIFFERENT sha256 \u2192 fails loudly ("drift detected"). Pass `force: true` to overwrite (logged).\n- Otherwise the SQL is wrapped in BEGIN/COMMIT and applied, then a row is inserted into `_mcp_migrations`.\n\nPass `noTransaction: true` for statements that cannot run inside a transaction (`CREATE INDEX CONCURRENTLY`, `ALTER TYPE \u2026 ADD VALUE`, `VACUUM`, etc.). In that mode the user SQL runs first and the ledger is recorded in a separate follow-up call.\n\nSQL source: provide ONE of `sql` (inline string), `localFile` (path on this machine, read and piped via stdin), or `remoteFile` (absolute path on the SSH server, read with `cat` first).\n\nExample: `{ serverId, containerName: "refront-postgres-vanilla", name: "20260517120000_add_unified_classification", localFile: "./migrations/20260517120000_add_unified_classification.sql" }` \u2014 dbName + dbUser are auto-resolved from the container env (cached 1h).',
4729
4757
  inputSchema: {
4730
4758
  type: "object",
4731
4759
  properties: {
4732
- serverId: { type: "string", description: "UUID of the SSH server" },
4760
+ serverId: { type: "string", description: "UUID or (fuzzy) name of the SSH server" },
4733
4761
  containerName: { type: "string", description: 'Postgres container name (e.g. "refront-postgres-vanilla")' },
4734
- dbName: { type: "string", description: 'Database (defaults to "postgres" \u2014 set explicitly to your app DB)' },
4735
- dbUser: { type: "string", description: 'User (defaults to "postgres")' },
4762
+ dbName: { type: "string", description: "Database. Auto-resolved from container `POSTGRES_DB` env when omitted. Override only when the instance hosts multiple databases." },
4763
+ dbUser: { type: "string", description: "User. Auto-resolved from container `POSTGRES_USER` env when omitted." },
4736
4764
  name: { type: "string", description: "Unique migration name (recommended format: `YYYYMMDDhhmmss_description`)" },
4737
4765
  sql: { type: "string", description: "Inline SQL string (mutually exclusive with localFile / remoteFile)" },
4738
4766
  localFile: { type: "string", description: "Path on this machine to a .sql file (read here, streamed via stdin)" },
@@ -4746,14 +4774,14 @@ var TOOLS = [
4746
4774
  },
4747
4775
  {
4748
4776
  name: "db-list-migrations",
4749
- description: 'List entries from the `_mcp_migrations` ledger on a Postgres container. Answers "what migrations have already been applied here?" without having to write SQL. Returns name, sha256 (truncated), applied_at, applied_by, ordered by most recent first. If the ledger table does not exist yet (no migrations applied via db-apply-migration), returns an empty list.',
4777
+ description: 'List entries from the `_mcp_migrations` ledger on a Postgres container. Answers "what migrations have already been applied here?" without having to write SQL. Returns full name, sha256, applied_at, applied_by, ordered by most recent first. If the ledger table does not exist yet (no migrations applied via db-apply-migration), returns a friendly empty response. dbName + dbUser are auto-resolved from the container env when omitted.',
4750
4778
  inputSchema: {
4751
4779
  type: "object",
4752
4780
  properties: {
4753
- serverId: { type: "string", description: "UUID of the SSH server" },
4781
+ serverId: { type: "string", description: "UUID or (fuzzy) name of the SSH server" },
4754
4782
  containerName: { type: "string", description: "Postgres container name" },
4755
- dbName: { type: "string", description: 'Database (defaults to "postgres")' },
4756
- dbUser: { type: "string", description: 'User (defaults to "postgres")' },
4783
+ dbName: { type: "string", description: "Database. Auto-resolved from container `POSTGRES_DB` env when omitted." },
4784
+ dbUser: { type: "string", description: "User. Auto-resolved from container `POSTGRES_USER` env when omitted." },
4757
4785
  limit: { type: "number", description: "Max number of entries (default 50, max 500)" }
4758
4786
  },
4759
4787
  required: ["serverId", "containerName"]
@@ -4858,7 +4886,7 @@ var TOOLS = [
4858
4886
  // ----- Repo reference -----
4859
4887
  ...REPO_TOOLS
4860
4888
  ];
4861
- var MCP_VERSION = "6.2.0";
4889
+ var MCP_VERSION = "6.3.0";
4862
4890
  async function handleListTools() {
4863
4891
  if (!authContext) return { tools: TOOLS };
4864
4892
  const accessible = TOOLS.filter((tool) => {
@@ -5814,7 +5842,7 @@ ${trail.join("\n")}` }] };
5814
5842
  }
5815
5843
  // ----- Database -----
5816
5844
  case "db-discover": {
5817
- const { conn, proxy } = await getServerConnection(String(a.serverId));
5845
+ const { serverId, conn, proxy } = await getServerConnection(String(a.serverId));
5818
5846
  const rawInclude = Array.isArray(a.include) ? a.include.map(String) : ["www"];
5819
5847
  const include = new Set(rawInclude.filter((m) => m === "www" || m === "postgres-containers"));
5820
5848
  if (include.size === 0) include.add("www");
@@ -5833,6 +5861,13 @@ ${lines.join("\n")}`);
5833
5861
  }
5834
5862
  if (include.has("postgres-containers")) {
5835
5863
  const containers = await discoverPostgresContainers(conn, proxy);
5864
+ for (const c of containers) {
5865
+ cachePostgresContainerCreds(serverId, c.container, {
5866
+ dbName: c.db,
5867
+ dbUser: c.user,
5868
+ hasPassword: c.hasPassword
5869
+ });
5870
+ }
5836
5871
  if (!containers.length) {
5837
5872
  sections.push("## Postgres containers\n(none \u2014 no running container exposes POSTGRES_USER/POSTGRES_DB)");
5838
5873
  } else {
@@ -5842,17 +5877,22 @@ ${lines.join("\n")}`);
5842
5877
  sections.push(`## Postgres containers (${containers.length})
5843
5878
  ${lines.join("\n")}
5844
5879
 
5845
- Tip: use these directly with \`db-query containerName=\u2026 dbName=\u2026 dbUser=\u2026\` or \`db-apply-migration\`.`);
5880
+ Tip: pass just \`containerName\` to \`db-query\` / \`db-apply-migration\` / \`db-list-migrations\` \u2014 dbName + dbUser are auto-resolved from the container env (cached for 1h per server).`);
5846
5881
  }
5847
5882
  }
5848
5883
  return { content: [{ type: "text", text: sections.join("\n\n") }] };
5849
5884
  }
5850
5885
  case "db-tables": {
5851
- const { conn, proxy } = await getServerConnection(String(a.serverId));
5886
+ const { serverId, conn, proxy } = await getServerConnection(String(a.serverId));
5852
5887
  const containerName = typeof a.containerName === "string" && a.containerName ? a.containerName : "";
5853
5888
  if (containerName) {
5854
- const dbUser = typeof a.dbUser === "string" && a.dbUser ? a.dbUser : "postgres";
5855
- const dbName = typeof a.dbName === "string" && a.dbName ? a.dbName : "postgres";
5889
+ let dbUser = typeof a.dbUser === "string" && a.dbUser ? a.dbUser : "";
5890
+ let dbName = typeof a.dbName === "string" && a.dbName ? a.dbName : "";
5891
+ if (!dbUser || !dbName) {
5892
+ const creds = await resolvePostgresContainerCreds(conn, proxy, serverId, containerName);
5893
+ if (!dbUser) dbUser = creds.dbUser;
5894
+ if (!dbName) dbName = creds.dbName;
5895
+ }
5856
5896
  const sqlText2 = `SELECT n.nspname AS schema,
5857
5897
  c.relname AS "table",
5858
5898
  pg_size_pretty(pg_total_relation_size(c.oid)) AS size,
@@ -5948,9 +5988,14 @@ COMMIT;`;
5948
5988
  if (!containerName) {
5949
5989
  return { content: [{ type: "text", text: `Error: engine=${engine} requires containerName (the docker container running the DB server, e.g. "supabase-db" or "trigger-postgres")` }] };
5950
5990
  }
5951
- const dbName = typeof a.dbName === "string" && a.dbName ? a.dbName.replace(/[^a-zA-Z0-9_-]/g, "") : "";
5952
- const dbUser = typeof a.dbUser === "string" && a.dbUser ? a.dbUser.replace(/[^a-zA-Z0-9_-]/g, "") : "";
5953
- const { conn, proxy } = await getServerConnection(String(a.serverId));
5991
+ let dbName = typeof a.dbName === "string" && a.dbName ? a.dbName.replace(/[^a-zA-Z0-9_-]/g, "") : "";
5992
+ let dbUser = typeof a.dbUser === "string" && a.dbUser ? a.dbUser.replace(/[^a-zA-Z0-9_-]/g, "") : "";
5993
+ const { serverId, conn, proxy } = await getServerConnection(String(a.serverId));
5994
+ if (engine === "postgres" && (!dbName || !dbUser)) {
5995
+ const creds = await resolvePostgresContainerCreds(conn, proxy, serverId, containerName);
5996
+ if (!dbName) dbName = creds.dbName.replace(/[^a-zA-Z0-9_-]/g, "");
5997
+ if (!dbUser) dbUser = creds.dbUser.replace(/[^a-zA-Z0-9_-]/g, "");
5998
+ }
5954
5999
  let cmd;
5955
6000
  let stdinPayload;
5956
6001
  if (engine === "postgres") {
@@ -5988,8 +6033,8 @@ GO
5988
6033
  case "db-apply-migration": {
5989
6034
  const containerName = String(a.containerName || "").replace(/[^a-zA-Z0-9._-]/g, "");
5990
6035
  if (!containerName) return { content: [{ type: "text", text: "Error: containerName is required" }] };
5991
- const dbUser = (typeof a.dbUser === "string" && a.dbUser ? a.dbUser : "postgres").replace(/[^a-zA-Z0-9_-]/g, "") || "postgres";
5992
- const dbName = (typeof a.dbName === "string" && a.dbName ? a.dbName : "postgres").replace(/[^a-zA-Z0-9_-]/g, "") || "postgres";
6036
+ let dbUser = (typeof a.dbUser === "string" && a.dbUser ? a.dbUser : "").replace(/[^a-zA-Z0-9_-]/g, "");
6037
+ let dbName = (typeof a.dbName === "string" && a.dbName ? a.dbName : "").replace(/[^a-zA-Z0-9_-]/g, "");
5993
6038
  const name2 = String(a.name || "").trim();
5994
6039
  if (!name2) return { content: [{ type: "text", text: 'Error: name is required (e.g. "20260517120000_add_foo")' }] };
5995
6040
  if (!/^[\w.@:+\-]+$/.test(name2) || name2.length > 200) {
@@ -6008,7 +6053,12 @@ GO
6008
6053
  if (sources.length !== 1) {
6009
6054
  return { content: [{ type: "text", text: `Error: pass exactly one of \`sql\`, \`localFile\`, or \`remoteFile\` (got ${sources.length || "none"}: ${sources.join(", ") || "\u2014"})` }] };
6010
6055
  }
6011
- const { conn, proxy } = await getServerConnection(String(a.serverId));
6056
+ const { serverId, conn, proxy } = await getServerConnection(String(a.serverId));
6057
+ if (!dbUser || !dbName) {
6058
+ const creds = await resolvePostgresContainerCreds(conn, proxy, serverId, containerName);
6059
+ if (!dbUser) dbUser = creds.dbUser.replace(/[^a-zA-Z0-9_-]/g, "") || "postgres";
6060
+ if (!dbName) dbName = creds.dbName.replace(/[^a-zA-Z0-9_-]/g, "") || "postgres";
6061
+ }
6012
6062
  let migrationSql;
6013
6063
  if (typeof a.sql === "string" && a.sql) {
6014
6064
  migrationSql = a.sql;
@@ -6168,11 +6218,16 @@ ${res.stdout.trim()}` : "(no output)")
6168
6218
  case "db-list-migrations": {
6169
6219
  const containerName = String(a.containerName || "").replace(/[^a-zA-Z0-9._-]/g, "");
6170
6220
  if (!containerName) return { content: [{ type: "text", text: "Error: containerName is required" }] };
6171
- const dbUser = (typeof a.dbUser === "string" && a.dbUser ? a.dbUser : "postgres").replace(/[^a-zA-Z0-9_-]/g, "") || "postgres";
6172
- const dbName = (typeof a.dbName === "string" && a.dbName ? a.dbName : "postgres").replace(/[^a-zA-Z0-9_-]/g, "") || "postgres";
6221
+ let dbUser = (typeof a.dbUser === "string" && a.dbUser ? a.dbUser : "").replace(/[^a-zA-Z0-9_-]/g, "");
6222
+ let dbName = (typeof a.dbName === "string" && a.dbName ? a.dbName : "").replace(/[^a-zA-Z0-9_-]/g, "");
6173
6223
  const limitRaw = Number(a.limit);
6174
6224
  const limit = Math.min(Math.max(Number.isFinite(limitRaw) && limitRaw > 0 ? Math.floor(limitRaw) : 50, 1), 500);
6175
- const { conn, proxy } = await getServerConnection(String(a.serverId));
6225
+ const { serverId, conn, proxy } = await getServerConnection(String(a.serverId));
6226
+ if (!dbUser || !dbName) {
6227
+ const creds = await resolvePostgresContainerCreds(conn, proxy, serverId, containerName);
6228
+ if (!dbUser) dbUser = creds.dbUser.replace(/[^a-zA-Z0-9_-]/g, "") || "postgres";
6229
+ if (!dbName) dbName = creds.dbName.replace(/[^a-zA-Z0-9_-]/g, "") || "postgres";
6230
+ }
6176
6231
  const sqlText = `SELECT name, sha256, applied_at, applied_by
6177
6232
  FROM _mcp_migrations
6178
6233
  ORDER BY applied_at DESC