@mgsoftwarebv/mg-dashboard-mcp 6.1.1 → 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 +183 -57
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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
|
|
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
|
|
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}
|
|
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}
|
|
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
|
-
|
|
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,
|
|
@@ -2718,7 +2750,7 @@ async function getServerConnection(serverId) {
|
|
|
2718
2750
|
const needsProxy = data.allowed_ssh_ips !== null && serverId !== SSH_PROXY_SERVER_ID;
|
|
2719
2751
|
const proxy = needsProxy ? await getProxyConnection() : void 0;
|
|
2720
2752
|
const os = data.os_type === "windows" ? "windows" : "linux";
|
|
2721
|
-
return { conn, proxy, os };
|
|
2753
|
+
return { serverId, conn, proxy, os };
|
|
2722
2754
|
}
|
|
2723
2755
|
async function sshExec(opts, command, proxy, options) {
|
|
2724
2756
|
const first = await sshExecOnce(opts, command, proxy, options);
|
|
@@ -4200,6 +4232,34 @@ done
|
|
|
4200
4232
|
}
|
|
4201
4233
|
return out;
|
|
4202
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
|
+
}
|
|
4203
4263
|
async function psqlInContainer(conn, proxy, containerName, dbUser, dbName, scriptSql, flags) {
|
|
4204
4264
|
const safeContainer = containerName.replace(/[^a-zA-Z0-9._-]/g, "");
|
|
4205
4265
|
const safeUser = dbUser.replace(/[^a-zA-Z0-9_-]/g, "") || "postgres";
|
|
@@ -4442,7 +4502,7 @@ async function mijnhostFetch(path, options = {}) {
|
|
|
4442
4502
|
var TOOLS = [
|
|
4443
4503
|
{
|
|
4444
4504
|
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`).',
|
|
4505
|
+
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
4506
|
inputSchema: {
|
|
4447
4507
|
type: "object",
|
|
4448
4508
|
properties: {
|
|
@@ -4655,22 +4715,22 @@ var TOOLS = [
|
|
|
4655
4715
|
},
|
|
4656
4716
|
{
|
|
4657
4717
|
name: "db-tables",
|
|
4658
|
-
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
|
|
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.",
|
|
4659
4719
|
inputSchema: {
|
|
4660
4720
|
type: "object",
|
|
4661
4721
|
properties: {
|
|
4662
|
-
serverId: { type: "string", description: "UUID of the SSH server" },
|
|
4722
|
+
serverId: { type: "string", description: "UUID or (fuzzy) name of the SSH server" },
|
|
4663
4723
|
sitePath: { type: "string", description: "Site root path (e.g. /var/www/example.com). MySQL autodiscover mode." },
|
|
4664
4724
|
containerName: { type: "string", description: "Postgres container name. Activates direct Postgres mode." },
|
|
4665
|
-
dbName: { type: "string", description:
|
|
4666
|
-
dbUser: { type: "string", description:
|
|
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." }
|
|
4667
4727
|
},
|
|
4668
4728
|
required: ["serverId"]
|
|
4669
4729
|
}
|
|
4670
4730
|
},
|
|
4671
4731
|
{
|
|
4672
4732
|
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",
|
|
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.',
|
|
4674
4734
|
inputSchema: {
|
|
4675
4735
|
type: "object",
|
|
4676
4736
|
properties: {
|
|
@@ -4680,46 +4740,48 @@ var TOOLS = [
|
|
|
4680
4740
|
sitePath: { type: "string", description: "Site root path (e.g. /var/www/example.com). Used only with engine=mysql autodiscover." },
|
|
4681
4741
|
containerName: { type: "string", description: 'Docker container running the DB server (e.g. "refront-postgres-vanilla", "supabase-db"). Activates direct-query mode.' },
|
|
4682
4742
|
engine: { type: "string", enum: ["mysql", "postgres", "mssql"], description: 'DB engine. Defaults to "mysql" with sitePath, "postgres" with containerName.' },
|
|
4683
|
-
dbName: { type: "string", description:
|
|
4684
|
-
dbUser: { type: "string", description: 'Database user (containerName mode).
|
|
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".' },
|
|
4685
4745
|
dbPass: { type: "string", description: "Database password (containerName mode). Required for mssql; optional for mysql; postgres uses trust auth by default." },
|
|
4686
4746
|
maxRows: { type: "number", description: "Auto-inject LIMIT for unbounded SELECTs. Default 1000, max 10000, set 0 to disable." },
|
|
4687
4747
|
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).' }
|
|
4748
|
+
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).' },
|
|
4749
|
+
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
4750
|
},
|
|
4690
4751
|
required: ["serverId"]
|
|
4691
4752
|
}
|
|
4692
4753
|
},
|
|
4693
4754
|
{
|
|
4694
4755
|
name: "db-apply-migration",
|
|
4695
|
-
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",
|
|
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).',
|
|
4696
4757
|
inputSchema: {
|
|
4697
4758
|
type: "object",
|
|
4698
4759
|
properties: {
|
|
4699
|
-
serverId: { type: "string", description: "UUID of the SSH server" },
|
|
4760
|
+
serverId: { type: "string", description: "UUID or (fuzzy) name of the SSH server" },
|
|
4700
4761
|
containerName: { type: "string", description: 'Postgres container name (e.g. "refront-postgres-vanilla")' },
|
|
4701
|
-
dbName: { type: "string", description:
|
|
4702
|
-
dbUser: { type: "string", description:
|
|
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." },
|
|
4703
4764
|
name: { type: "string", description: "Unique migration name (recommended format: `YYYYMMDDhhmmss_description`)" },
|
|
4704
4765
|
sql: { type: "string", description: "Inline SQL string (mutually exclusive with localFile / remoteFile)" },
|
|
4705
4766
|
localFile: { type: "string", description: "Path on this machine to a .sql file (read here, streamed via stdin)" },
|
|
4706
4767
|
remoteFile: { type: "string", description: "Absolute path of a .sql file already on the server (read with `cat`)" },
|
|
4707
4768
|
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." }
|
|
4769
|
+
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." },
|
|
4770
|
+
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
4771
|
},
|
|
4710
4772
|
required: ["serverId", "containerName", "name"]
|
|
4711
4773
|
}
|
|
4712
4774
|
},
|
|
4713
4775
|
{
|
|
4714
4776
|
name: "db-list-migrations",
|
|
4715
|
-
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
|
|
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.',
|
|
4716
4778
|
inputSchema: {
|
|
4717
4779
|
type: "object",
|
|
4718
4780
|
properties: {
|
|
4719
|
-
serverId: { type: "string", description: "UUID of the SSH server" },
|
|
4781
|
+
serverId: { type: "string", description: "UUID or (fuzzy) name of the SSH server" },
|
|
4720
4782
|
containerName: { type: "string", description: "Postgres container name" },
|
|
4721
|
-
dbName: { type: "string", description:
|
|
4722
|
-
dbUser: { type: "string", description:
|
|
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." },
|
|
4723
4785
|
limit: { type: "number", description: "Max number of entries (default 50, max 500)" }
|
|
4724
4786
|
},
|
|
4725
4787
|
required: ["serverId", "containerName"]
|
|
@@ -4824,7 +4886,7 @@ var TOOLS = [
|
|
|
4824
4886
|
// ----- Repo reference -----
|
|
4825
4887
|
...REPO_TOOLS
|
|
4826
4888
|
];
|
|
4827
|
-
var MCP_VERSION = "6.
|
|
4889
|
+
var MCP_VERSION = "6.3.0";
|
|
4828
4890
|
async function handleListTools() {
|
|
4829
4891
|
if (!authContext) return { tools: TOOLS };
|
|
4830
4892
|
const accessible = TOOLS.filter((tool) => {
|
|
@@ -4892,7 +4954,7 @@ async function executeToolCall(name, a, _serverId) {
|
|
|
4892
4954
|
const data = ctx.allowedServerIds !== null ? await db.execute(sql`
|
|
4893
4955
|
SELECT id, name, hostname, port, username, tags, hosted_by, os_type, created_at
|
|
4894
4956
|
FROM ssh_server
|
|
4895
|
-
WHERE id = ANY(${ctx.allowedServerIds}
|
|
4957
|
+
WHERE id = ANY(${uuidArrayParam(ctx.allowedServerIds)})
|
|
4896
4958
|
ORDER BY name
|
|
4897
4959
|
`) : await db.execute(sql`
|
|
4898
4960
|
SELECT id, name, hostname, port, username, tags, hosted_by, os_type, created_at
|
|
@@ -5780,7 +5842,7 @@ ${trail.join("\n")}` }] };
|
|
|
5780
5842
|
}
|
|
5781
5843
|
// ----- Database -----
|
|
5782
5844
|
case "db-discover": {
|
|
5783
|
-
const { conn, proxy } = await getServerConnection(String(a.serverId));
|
|
5845
|
+
const { serverId, conn, proxy } = await getServerConnection(String(a.serverId));
|
|
5784
5846
|
const rawInclude = Array.isArray(a.include) ? a.include.map(String) : ["www"];
|
|
5785
5847
|
const include = new Set(rawInclude.filter((m) => m === "www" || m === "postgres-containers"));
|
|
5786
5848
|
if (include.size === 0) include.add("www");
|
|
@@ -5799,6 +5861,13 @@ ${lines.join("\n")}`);
|
|
|
5799
5861
|
}
|
|
5800
5862
|
if (include.has("postgres-containers")) {
|
|
5801
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
|
+
}
|
|
5802
5871
|
if (!containers.length) {
|
|
5803
5872
|
sections.push("## Postgres containers\n(none \u2014 no running container exposes POSTGRES_USER/POSTGRES_DB)");
|
|
5804
5873
|
} else {
|
|
@@ -5808,17 +5877,22 @@ ${lines.join("\n")}`);
|
|
|
5808
5877
|
sections.push(`## Postgres containers (${containers.length})
|
|
5809
5878
|
${lines.join("\n")}
|
|
5810
5879
|
|
|
5811
|
-
Tip:
|
|
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).`);
|
|
5812
5881
|
}
|
|
5813
5882
|
}
|
|
5814
5883
|
return { content: [{ type: "text", text: sections.join("\n\n") }] };
|
|
5815
5884
|
}
|
|
5816
5885
|
case "db-tables": {
|
|
5817
|
-
const { conn, proxy } = await getServerConnection(String(a.serverId));
|
|
5886
|
+
const { serverId, conn, proxy } = await getServerConnection(String(a.serverId));
|
|
5818
5887
|
const containerName = typeof a.containerName === "string" && a.containerName ? a.containerName : "";
|
|
5819
5888
|
if (containerName) {
|
|
5820
|
-
|
|
5821
|
-
|
|
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
|
+
}
|
|
5822
5896
|
const sqlText2 = `SELECT n.nspname AS schema,
|
|
5823
5897
|
c.relname AS "table",
|
|
5824
5898
|
pg_size_pretty(pg_total_relation_size(c.oid)) AS size,
|
|
@@ -5878,6 +5952,7 @@ For a one-off ad-hoc destructive query, pass \`allowDestructive: "${allowDestruc
|
|
|
5878
5952
|
}
|
|
5879
5953
|
let query = rawQuery.replace(/;\s*$/, "");
|
|
5880
5954
|
let appliedLimit = false;
|
|
5955
|
+
const wrapTransaction = a.transaction === true && !explainMode && !describeArg && engine !== "mssql";
|
|
5881
5956
|
if (explainMode && !describeArg) {
|
|
5882
5957
|
if (engine === "mssql") {
|
|
5883
5958
|
query = `SET SHOWPLAN_TEXT ON;
|
|
@@ -5894,20 +5969,33 @@ LIMIT ${maxRows + 1}`;
|
|
|
5894
5969
|
appliedLimit = true;
|
|
5895
5970
|
}
|
|
5896
5971
|
}
|
|
5972
|
+
if (wrapTransaction) {
|
|
5973
|
+
query = `BEGIN;
|
|
5974
|
+
${query.replace(/;\s*$/, "")};
|
|
5975
|
+
COMMIT;`;
|
|
5976
|
+
}
|
|
5977
|
+
const txBanner = wrapTransaction ? `
|
|
5978
|
+
|
|
5979
|
+
[transaction] wrapped in BEGIN/COMMIT \u2014 all statements committed atomically.` : "";
|
|
5897
5980
|
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
5981
|
if (!containerName && engine === "mysql") {
|
|
5899
5982
|
if (!a.sitePath) return { content: [{ type: "text", text: "Error: sitePath is required for mysql autodiscover (or pass containerName + engine for direct DB queries)" }] };
|
|
5900
5983
|
const { conn: conn2, proxy: proxy2 } = await getServerConnection(String(a.serverId));
|
|
5901
5984
|
const output2 = await execSiteMysql(conn2, String(a.sitePath), query, proxy2);
|
|
5902
5985
|
const footer2 = formatDbQueryFooter(output2, appliedLimit, maxRows, explainMode);
|
|
5903
|
-
return { content: [{ type: "text", text: (output2 || "Query executed successfully (no output)") + footer2 + destructiveBanner }] };
|
|
5986
|
+
return { content: [{ type: "text", text: (output2 || "Query executed successfully (no output)") + footer2 + destructiveBanner + txBanner }] };
|
|
5904
5987
|
}
|
|
5905
5988
|
if (!containerName) {
|
|
5906
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")` }] };
|
|
5907
5990
|
}
|
|
5908
|
-
|
|
5909
|
-
|
|
5910
|
-
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
|
+
}
|
|
5911
5999
|
let cmd;
|
|
5912
6000
|
let stdinPayload;
|
|
5913
6001
|
if (engine === "postgres") {
|
|
@@ -5940,13 +6028,13 @@ GO
|
|
|
5940
6028
|
return { content: [{ type: "text", text: `Error (exit ${result.exitCode}, ${engine}): ${output}` }] };
|
|
5941
6029
|
}
|
|
5942
6030
|
const footer = formatDbQueryFooter(output, appliedLimit, maxRows, explainMode);
|
|
5943
|
-
return { content: [{ type: "text", text: output + footer + destructiveBanner }] };
|
|
6031
|
+
return { content: [{ type: "text", text: output + footer + destructiveBanner + txBanner }] };
|
|
5944
6032
|
}
|
|
5945
6033
|
case "db-apply-migration": {
|
|
5946
6034
|
const containerName = String(a.containerName || "").replace(/[^a-zA-Z0-9._-]/g, "");
|
|
5947
6035
|
if (!containerName) return { content: [{ type: "text", text: "Error: containerName is required" }] };
|
|
5948
|
-
|
|
5949
|
-
|
|
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, "");
|
|
5950
6038
|
const name2 = String(a.name || "").trim();
|
|
5951
6039
|
if (!name2) return { content: [{ type: "text", text: 'Error: name is required (e.g. "20260517120000_add_foo")' }] };
|
|
5952
6040
|
if (!/^[\w.@:+\-]+$/.test(name2) || name2.length > 200) {
|
|
@@ -5954,6 +6042,9 @@ GO
|
|
|
5954
6042
|
}
|
|
5955
6043
|
const noTransaction = a.noTransaction === true;
|
|
5956
6044
|
const force = a.force === true;
|
|
6045
|
+
const recordOnly = a.recordOnly === true;
|
|
6046
|
+
const t0 = Date.now();
|
|
6047
|
+
const took = () => `${((Date.now() - t0) / 1e3).toFixed(2)}s`;
|
|
5957
6048
|
const sources = [
|
|
5958
6049
|
typeof a.sql === "string" && a.sql ? "sql" : null,
|
|
5959
6050
|
typeof a.localFile === "string" && a.localFile ? "localFile" : null,
|
|
@@ -5962,7 +6053,12 @@ GO
|
|
|
5962
6053
|
if (sources.length !== 1) {
|
|
5963
6054
|
return { content: [{ type: "text", text: `Error: pass exactly one of \`sql\`, \`localFile\`, or \`remoteFile\` (got ${sources.length || "none"}: ${sources.join(", ") || "\u2014"})` }] };
|
|
5964
6055
|
}
|
|
5965
|
-
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
|
+
}
|
|
5966
6062
|
let migrationSql;
|
|
5967
6063
|
if (typeof a.sql === "string" && a.sql) {
|
|
5968
6064
|
migrationSql = a.sql;
|
|
@@ -5992,20 +6088,23 @@ GO
|
|
|
5992
6088
|
return { content: [{ type: "text", text: "Error: resolved SQL is empty" }] };
|
|
5993
6089
|
}
|
|
5994
6090
|
const sha = migrationSha256(migrationSql);
|
|
5995
|
-
const
|
|
5996
|
-
|
|
6091
|
+
const ddl = await psqlInContainer(conn, proxy, containerName, dbUser, dbName, `${MIGRATION_LEDGER_DDL}
|
|
6092
|
+
`, "");
|
|
6093
|
+
if (ddl.exitCode !== 0) {
|
|
6094
|
+
return { content: [{ type: "text", text: `Error initialising ledger on ${containerName} (${dbName}): ${ddl.stderr || ddl.stdout || `exit ${ddl.exitCode}`}` }] };
|
|
6095
|
+
}
|
|
6096
|
+
const probeSelect = `SELECT sha256 FROM _mcp_migrations WHERE name = ${dollarQuote(name2)};
|
|
5997
6097
|
`;
|
|
5998
|
-
const probe = await psqlInContainer(conn, proxy, containerName, dbUser, dbName,
|
|
6098
|
+
const probe = await psqlInContainer(conn, proxy, containerName, dbUser, dbName, probeSelect, "-tA");
|
|
5999
6099
|
if (probe.exitCode !== 0) {
|
|
6000
|
-
return { content: [{ type: "text", text: `Error
|
|
6100
|
+
return { content: [{ type: "text", text: `Error reading ledger on ${containerName} (${dbName}): ${probe.stderr || probe.stdout || `exit ${probe.exitCode}`}` }] };
|
|
6001
6101
|
}
|
|
6002
|
-
const
|
|
6003
|
-
const existingSha = probeLines.length > 0 ? probeLines[probeLines.length - 1] : "";
|
|
6102
|
+
const existingSha = probe.stdout.split("\n").map((l) => l.trim()).find((l) => /^[0-9a-f]{64}$/.test(l)) || "";
|
|
6004
6103
|
if (existingSha && existingSha === sha) {
|
|
6005
6104
|
return {
|
|
6006
6105
|
content: [{
|
|
6007
6106
|
type: "text",
|
|
6008
|
-
text: `[noop] Migration "${name2}" already applied on ${containerName}/${dbName} (sha256 matches).
|
|
6107
|
+
text: `[noop] Migration "${name2}" already applied on ${containerName}/${dbName} (sha256 matches, took ${took()}).
|
|
6009
6108
|
Ledger sha: ${sha.slice(0, 12)}\u2026`
|
|
6010
6109
|
}]
|
|
6011
6110
|
};
|
|
@@ -6014,7 +6113,7 @@ Ledger sha: ${sha.slice(0, 12)}\u2026`
|
|
|
6014
6113
|
return {
|
|
6015
6114
|
content: [{
|
|
6016
6115
|
type: "text",
|
|
6017
|
-
text: `[drift] Migration "${name2}" exists in ledger with DIFFERENT sha256.
|
|
6116
|
+
text: `[drift] Migration "${name2}" exists in ledger with DIFFERENT sha256 (took ${took()}).
|
|
6018
6117
|
ledger sha: ${existingSha.slice(0, 12)}\u2026
|
|
6019
6118
|
new sha: ${sha.slice(0, 12)}\u2026
|
|
6020
6119
|
|
|
@@ -6026,6 +6125,29 @@ This usually means the migration file was edited after it was first applied. Ins
|
|
|
6026
6125
|
const ledgerInsert = `INSERT INTO _mcp_migrations (name, sha256, applied_by) VALUES (${dollarQuote(name2)}, ${dollarQuote(sha)}, ${dollarQuote(appliedBy)})
|
|
6027
6126
|
ON CONFLICT (name) DO UPDATE SET sha256 = EXCLUDED.sha256, applied_at = now(), applied_by = EXCLUDED.applied_by;
|
|
6028
6127
|
`;
|
|
6128
|
+
if (recordOnly) {
|
|
6129
|
+
const record = await psqlInContainer(conn, proxy, containerName, dbUser, dbName, ledgerInsert, "");
|
|
6130
|
+
if (record.exitCode !== 0) {
|
|
6131
|
+
return {
|
|
6132
|
+
content: [{
|
|
6133
|
+
type: "text",
|
|
6134
|
+
text: `[failed] Ledger insert for "${name2}" failed (recordOnly=true, no SQL was executed).
|
|
6135
|
+
psql exit: ${record.exitCode}
|
|
6136
|
+
` + (record.stderr ? `--- stderr ---
|
|
6137
|
+
${record.stderr}
|
|
6138
|
+
` : "") + (record.stdout ? `--- stdout ---
|
|
6139
|
+
${record.stdout}` : "")
|
|
6140
|
+
}]
|
|
6141
|
+
};
|
|
6142
|
+
}
|
|
6143
|
+
return {
|
|
6144
|
+
content: [{
|
|
6145
|
+
type: "text",
|
|
6146
|
+
text: `[recorded] "${name2}" written to ledger on ${containerName}/${dbName} WITHOUT executing the SQL (recordOnly=true${existingSha ? ", overwrote existing entry" : ""}, took ${took()}).
|
|
6147
|
+
ledger sha: ${sha.slice(0, 12)}\u2026`
|
|
6148
|
+
}]
|
|
6149
|
+
};
|
|
6150
|
+
}
|
|
6029
6151
|
if (noTransaction) {
|
|
6030
6152
|
const apply = await psqlInContainer(conn, proxy, containerName, dbUser, dbName, normalised, "");
|
|
6031
6153
|
if (apply.exitCode !== 0) {
|
|
@@ -6058,7 +6180,7 @@ ${record.stdout}` : "")
|
|
|
6058
6180
|
return {
|
|
6059
6181
|
content: [{
|
|
6060
6182
|
type: "text",
|
|
6061
|
-
text: `[applied] "${name2}" on ${containerName}/${dbName} (noTransaction=true${existingSha ? ", force" : ""}).
|
|
6183
|
+
text: `[applied] "${name2}" on ${containerName}/${dbName} (noTransaction=true${existingSha ? ", force" : ""}, took ${took()}).
|
|
6062
6184
|
ledger sha: ${sha.slice(0, 12)}\u2026
|
|
6063
6185
|
` + (apply.stdout ? `--- output ---
|
|
6064
6186
|
${apply.stdout.trim()}` : "(no output)")
|
|
@@ -6086,7 +6208,7 @@ ${res.stdout}` : "")
|
|
|
6086
6208
|
return {
|
|
6087
6209
|
content: [{
|
|
6088
6210
|
type: "text",
|
|
6089
|
-
text: `[applied] "${name2}" on ${containerName}/${dbName}${existingSha ? " (forced overwrite)" : ""}.
|
|
6211
|
+
text: `[applied] "${name2}" on ${containerName}/${dbName}${existingSha ? " (forced overwrite)" : ""} (took ${took()}).
|
|
6090
6212
|
ledger sha: ${sha.slice(0, 12)}\u2026
|
|
6091
6213
|
` + (res.stdout ? `--- output ---
|
|
6092
6214
|
${res.stdout.trim()}` : "(no output)")
|
|
@@ -6096,13 +6218,17 @@ ${res.stdout.trim()}` : "(no output)")
|
|
|
6096
6218
|
case "db-list-migrations": {
|
|
6097
6219
|
const containerName = String(a.containerName || "").replace(/[^a-zA-Z0-9._-]/g, "");
|
|
6098
6220
|
if (!containerName) return { content: [{ type: "text", text: "Error: containerName is required" }] };
|
|
6099
|
-
|
|
6100
|
-
|
|
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, "");
|
|
6101
6223
|
const limitRaw = Number(a.limit);
|
|
6102
6224
|
const limit = Math.min(Math.max(Number.isFinite(limitRaw) && limitRaw > 0 ? Math.floor(limitRaw) : 50, 1), 500);
|
|
6103
|
-
const { conn, proxy } = await getServerConnection(String(a.serverId));
|
|
6104
|
-
|
|
6105
|
-
|
|
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
|
+
}
|
|
6231
|
+
const sqlText = `SELECT name, sha256, applied_at, applied_by
|
|
6106
6232
|
FROM _mcp_migrations
|
|
6107
6233
|
ORDER BY applied_at DESC
|
|
6108
6234
|
LIMIT ${limit};
|
|
@@ -6127,7 +6253,7 @@ LIMIT ${limit};
|
|
|
6127
6253
|
const data = stageFilterIds ? await db.execute(sql`
|
|
6128
6254
|
SELECT id, app_name, environment, description, updated_at, release_profile_stage_id
|
|
6129
6255
|
FROM env_config
|
|
6130
|
-
WHERE release_profile_stage_id = ANY(${stageFilterIds}
|
|
6256
|
+
WHERE release_profile_stage_id = ANY(${uuidArrayParam(stageFilterIds)})
|
|
6131
6257
|
ORDER BY app_name, environment
|
|
6132
6258
|
`) : await db.execute(sql`
|
|
6133
6259
|
SELECT id, app_name, environment, description, updated_at, release_profile_stage_id
|
|
@@ -6155,7 +6281,7 @@ LIMIT ${limit};
|
|
|
6155
6281
|
FROM env_config
|
|
6156
6282
|
WHERE app_name = ${appName}
|
|
6157
6283
|
AND environment = ${environment}
|
|
6158
|
-
AND release_profile_stage_id = ANY(${stageFilterIds}
|
|
6284
|
+
AND release_profile_stage_id = ANY(${uuidArrayParam(stageFilterIds)})
|
|
6159
6285
|
`) : await db.execute(sql`
|
|
6160
6286
|
SELECT env_data_encrypted, release_profile_stage_id
|
|
6161
6287
|
FROM env_config
|
|
@@ -6190,7 +6316,7 @@ LIMIT ${limit};
|
|
|
6190
6316
|
FROM env_config
|
|
6191
6317
|
WHERE app_name = ${appName}
|
|
6192
6318
|
AND environment = ${environment}
|
|
6193
|
-
AND release_profile_stage_id = ANY(${resolvedStageIds}
|
|
6319
|
+
AND release_profile_stage_id = ANY(${uuidArrayParam(resolvedStageIds)})
|
|
6194
6320
|
`) : await db.execute(sql`
|
|
6195
6321
|
SELECT id, release_profile_stage_id
|
|
6196
6322
|
FROM env_config
|