@mgsoftwarebv/mg-dashboard-mcp 6.1.1 → 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 +99 -28
- 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,
|
|
@@ -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.
|
|
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}
|
|
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
|
|
5996
|
-
|
|
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,
|
|
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
|
|
6050
|
+
return { content: [{ type: "text", text: `Error reading ledger on ${containerName} (${dbName}): ${probe.stderr || probe.stdout || `exit ${probe.exitCode}`}` }] };
|
|
6001
6051
|
}
|
|
6002
|
-
const
|
|
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 = `
|
|
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}
|
|
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}
|
|
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}
|
|
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
|