postgresai 0.16.0-dev.2 → 0.16.0-dev.4
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/bin/postgres-ai.ts +657 -0
- package/dist/bin/postgres-ai.js +2454 -271
- package/dist/sql/06.helpers.sql +0 -122
- package/dist/sql/sql/06.helpers.sql +0 -122
- package/lib/dblab.ts +428 -0
- package/lib/init.ts +0 -8
- package/lib/joe.ts +739 -0
- package/lib/mcp-server.ts +597 -0
- package/lib/supabase.ts +0 -18
- package/package.json +1 -1
- package/sql/06.helpers.sql +0 -122
- package/test/dblab.cli.test.ts +339 -0
- package/test/dblab.test.ts +415 -0
- package/test/init.integration.test.ts +9 -79
- package/test/joe.cli.test.ts +298 -0
- package/test/joe.test.ts +726 -0
package/lib/mcp-server.ts
CHANGED
|
@@ -15,7 +15,23 @@ import {
|
|
|
15
15
|
type ConfigChange,
|
|
16
16
|
} from "./issues";
|
|
17
17
|
import { fetchReports, fetchAllReports, fetchReportFiles, fetchReportFileData, parseFlexibleDate } from "./reports";
|
|
18
|
+
import {
|
|
19
|
+
resolveDblabInstanceId,
|
|
20
|
+
createClone,
|
|
21
|
+
listClones,
|
|
22
|
+
getClone,
|
|
23
|
+
resetClone,
|
|
24
|
+
destroyClone,
|
|
25
|
+
listBranches,
|
|
26
|
+
createBranch,
|
|
27
|
+
deleteBranch,
|
|
28
|
+
branchLog,
|
|
29
|
+
listSnapshots,
|
|
30
|
+
createSnapshot,
|
|
31
|
+
destroySnapshot,
|
|
32
|
+
} from "./dblab";
|
|
18
33
|
import { uploadFile, downloadFile, buildMarkdownLink, uploadAttachments, appendAttachmentsToContent } from "./storage";
|
|
34
|
+
import { executeJoeCommand, listProjects, type JoeCommand } from "./joe";
|
|
19
35
|
import { resolveBaseUrls } from "./util";
|
|
20
36
|
|
|
21
37
|
// MCP SDK imports - Bun handles these directly
|
|
@@ -50,6 +66,255 @@ export interface McpToolResponse {
|
|
|
50
66
|
isError?: boolean;
|
|
51
67
|
}
|
|
52
68
|
|
|
69
|
+
/** MCP tool name → Joe command (SPEC §5 names). */
|
|
70
|
+
export const JOE_TOOL_TO_COMMAND: Record<string, JoeCommand> = {
|
|
71
|
+
plan_query: "plan",
|
|
72
|
+
explain_query: "explain",
|
|
73
|
+
exec_sql: "exec",
|
|
74
|
+
hypo_index: "hypo",
|
|
75
|
+
activity: "activity",
|
|
76
|
+
terminate_backend: "terminate",
|
|
77
|
+
reset_clone: "reset",
|
|
78
|
+
describe: "describe",
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
interface McpToolDefinition {
|
|
82
|
+
name: string;
|
|
83
|
+
description: string;
|
|
84
|
+
inputSchema: Record<string, unknown>;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* MCP tool definitions for the Joe command surface + org-level project discovery.
|
|
89
|
+
* Names match SPEC §5. Descriptions disclose the egress (finding B2): plan/explain
|
|
90
|
+
* feed the plan/stats to your LLM when AI features are enabled; exec_sql carries the
|
|
91
|
+
* stronger raw-data warning (real own-data table rows can enter the agent's LLM
|
|
92
|
+
* context). Every command tool carries an optional session_id (finding B-1).
|
|
93
|
+
*/
|
|
94
|
+
export function joeToolDefinitions(): McpToolDefinition[] {
|
|
95
|
+
const sessionProps = {
|
|
96
|
+
project_id: {
|
|
97
|
+
type: "string",
|
|
98
|
+
description: "Target project by numeric id OR alias/name (id-or-alias). --project 12 ≡ main-db.",
|
|
99
|
+
},
|
|
100
|
+
session_id: {
|
|
101
|
+
type: "string",
|
|
102
|
+
description: "Run on a specific session's clone (64-bit id as a string). Auto-reused per project when omitted.",
|
|
103
|
+
},
|
|
104
|
+
new_session: { type: "boolean", description: "Force a fresh session/clone (null session)." },
|
|
105
|
+
timeout_ms: { type: "number", description: "One-shot poll budget in ms (≤ 25000); on expiry returns a command_id to resume." },
|
|
106
|
+
debug: { type: "boolean", description: "Enable verbose debug logs." },
|
|
107
|
+
} as const;
|
|
108
|
+
|
|
109
|
+
return [
|
|
110
|
+
{
|
|
111
|
+
name: "plan_query",
|
|
112
|
+
description:
|
|
113
|
+
"Plan a query (EXPLAIN, plan-only — NEVER executes; the fast/safe default). Returns a sanitized plan tree (literals redacted). LLM-facing: feeds the plan/stats to your LLM when the org's AI features are enabled. Requires joe:plan.",
|
|
114
|
+
inputSchema: {
|
|
115
|
+
type: "object",
|
|
116
|
+
properties: { sql: { type: "string", description: "The query to plan (one explainable statement)." }, ...sessionProps },
|
|
117
|
+
required: ["sql", "project_id"],
|
|
118
|
+
additionalProperties: false,
|
|
119
|
+
},
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
name: "explain_query",
|
|
123
|
+
description:
|
|
124
|
+
"EXPLAIN ANALYZE a query — EXECUTES it on the hardened, ephemeral DBLab clone and returns the measured, annotated plan (node types, costs, actual-rows/timings, buffers — literals redacted), NOT raw SELECT result rows. LLM-facing: feeds the measured plan/stats to your LLM when AI features are enabled. Requires joe:exec; subject to the org execution policy (read_only allows SELECT-only).",
|
|
125
|
+
inputSchema: {
|
|
126
|
+
type: "object",
|
|
127
|
+
properties: { sql: { type: "string", description: "The query to EXPLAIN ANALYZE (executes on the clone)." }, ...sessionProps },
|
|
128
|
+
required: ["sql", "project_id"],
|
|
129
|
+
additionalProperties: false,
|
|
130
|
+
},
|
|
131
|
+
},
|
|
132
|
+
{
|
|
133
|
+
name: "exec_sql",
|
|
134
|
+
description:
|
|
135
|
+
"WARNING: executes arbitrary SQL (DDL/DML) on the disposable clone and CAN RETURN REAL (own-data) TABLE ROWS that will enter this agent's LLM context (subject to the agent row-cap / metadata-only minimization). Prefer plan_query (non-executing) when you only need a plan. Requires an explicit joe:exec grant and is subject to the org's joe_execution_policy.",
|
|
136
|
+
inputSchema: {
|
|
137
|
+
type: "object",
|
|
138
|
+
properties: { sql: { type: "string", description: "Arbitrary DDL/DML to run on the clone (e.g. create index, analyze)." }, ...sessionProps },
|
|
139
|
+
required: ["sql", "project_id"],
|
|
140
|
+
additionalProperties: false,
|
|
141
|
+
},
|
|
142
|
+
},
|
|
143
|
+
{
|
|
144
|
+
name: "hypo_index",
|
|
145
|
+
description:
|
|
146
|
+
"HypoPG hypothetical index + re-plan (no real index built, no data change) — would the planner switch to it? LLM-facing. Requires joe:plan.",
|
|
147
|
+
inputSchema: {
|
|
148
|
+
type: "object",
|
|
149
|
+
properties: {
|
|
150
|
+
sql: { type: "string", description: "Candidate index DDL (e.g. create index on orders (customer_id))." },
|
|
151
|
+
query: { type: "string", description: "Target query the hypothetical index is evaluated against." },
|
|
152
|
+
...sessionProps,
|
|
153
|
+
},
|
|
154
|
+
required: ["sql", "query", "project_id"],
|
|
155
|
+
additionalProperties: false,
|
|
156
|
+
},
|
|
157
|
+
},
|
|
158
|
+
{
|
|
159
|
+
name: "activity",
|
|
160
|
+
description: "pg_stat_activity snapshot on the clone (query text redacted). LLM-facing. Requires joe:plan.",
|
|
161
|
+
inputSchema: { type: "object", properties: { ...sessionProps }, required: ["project_id"], additionalProperties: false },
|
|
162
|
+
},
|
|
163
|
+
{
|
|
164
|
+
name: "terminate_backend",
|
|
165
|
+
description: "pg_terminate_backend(pid) on the clone. Status-only (no data to the LLM). Requires joe:admin.",
|
|
166
|
+
inputSchema: {
|
|
167
|
+
type: "object",
|
|
168
|
+
properties: { pid: { type: "number", description: "Backend pid to terminate." }, ...sessionProps },
|
|
169
|
+
required: ["pid", "project_id"],
|
|
170
|
+
additionalProperties: false,
|
|
171
|
+
},
|
|
172
|
+
},
|
|
173
|
+
{
|
|
174
|
+
name: "reset_clone",
|
|
175
|
+
description: "Reset/recreate the session's thin clone. Status-only (no data to the LLM). Requires joe:admin.",
|
|
176
|
+
inputSchema: { type: "object", properties: { ...sessionProps }, required: ["project_id"], additionalProperties: false },
|
|
177
|
+
},
|
|
178
|
+
{
|
|
179
|
+
name: "describe",
|
|
180
|
+
description: "\\d-family schema/relation/index/db/view metadata introspection. LLM-facing (metadata). Requires joe:plan.",
|
|
181
|
+
inputSchema: {
|
|
182
|
+
type: "object",
|
|
183
|
+
properties: {
|
|
184
|
+
object: { type: "string", description: "Object to describe (e.g. users)." },
|
|
185
|
+
variant: { type: "string", description: "\\d-family variant (e.g. \\d+, \\di, \\dt)." },
|
|
186
|
+
...sessionProps,
|
|
187
|
+
},
|
|
188
|
+
required: ["object", "project_id"],
|
|
189
|
+
additionalProperties: false,
|
|
190
|
+
},
|
|
191
|
+
},
|
|
192
|
+
{
|
|
193
|
+
name: "list_projects",
|
|
194
|
+
description:
|
|
195
|
+
"List the org's projects and which have Joe ready (org-level discovery — NOT a Joe endpoint; mirrors `pgai projects`). Use it to find the project_ids/aliases you may address with the Joe command tools.",
|
|
196
|
+
inputSchema: {
|
|
197
|
+
type: "object",
|
|
198
|
+
properties: {
|
|
199
|
+
org_id: { type: "number", description: "Organization ID (optional, falls back to config)." },
|
|
200
|
+
debug: { type: "boolean", description: "Enable verbose debug logs." },
|
|
201
|
+
},
|
|
202
|
+
additionalProperties: false,
|
|
203
|
+
},
|
|
204
|
+
},
|
|
205
|
+
];
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
interface RunJoeToolParams {
|
|
209
|
+
toolName: string;
|
|
210
|
+
apiKey: string;
|
|
211
|
+
apiBaseUrl: string;
|
|
212
|
+
orgId?: number;
|
|
213
|
+
args: Record<string, unknown>;
|
|
214
|
+
debug: boolean;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Execute a Joe command MCP tool: map the MCP args 1:1 to the rpc params, resolve
|
|
219
|
+
* `project_id` (id-or-alias), thread/persist the session, run the submit-then-poll
|
|
220
|
+
* one-shot, and return the outcome as JSON text. Mirrors the CLI verbs exactly.
|
|
221
|
+
*/
|
|
222
|
+
async function runJoeTool(params: RunJoeToolParams): Promise<McpToolResponse> {
|
|
223
|
+
const command = JOE_TOOL_TO_COMMAND[params.toolName];
|
|
224
|
+
const a = params.args;
|
|
225
|
+
|
|
226
|
+
const project = String(a.project_id ?? "").trim();
|
|
227
|
+
if (!project) {
|
|
228
|
+
return { content: [{ type: "text", text: "project_id is required" }], isError: true };
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
let sql: string | null = null;
|
|
232
|
+
let cmdArgs: Record<string, unknown> | null = null;
|
|
233
|
+
|
|
234
|
+
if (command === "plan" || command === "explain" || command === "exec" || command === "hypo") {
|
|
235
|
+
sql = String(a.sql ?? "").trim();
|
|
236
|
+
if (!sql) {
|
|
237
|
+
return { content: [{ type: "text", text: "sql is required" }], isError: true };
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
if (command === "hypo") {
|
|
241
|
+
const query = String(a.query ?? "").trim();
|
|
242
|
+
if (!query) {
|
|
243
|
+
return { content: [{ type: "text", text: "query is required for hypo_index" }], isError: true };
|
|
244
|
+
}
|
|
245
|
+
cmdArgs = { query };
|
|
246
|
+
}
|
|
247
|
+
if (command === "terminate") {
|
|
248
|
+
const pid = Number(a.pid);
|
|
249
|
+
if (!Number.isFinite(pid)) {
|
|
250
|
+
return { content: [{ type: "text", text: "pid (number) is required for terminate_backend" }], isError: true };
|
|
251
|
+
}
|
|
252
|
+
cmdArgs = { pid };
|
|
253
|
+
}
|
|
254
|
+
if (command === "describe") {
|
|
255
|
+
const object = String(a.object ?? "").trim();
|
|
256
|
+
if (!object) {
|
|
257
|
+
return { content: [{ type: "text", text: "object is required for describe" }], isError: true };
|
|
258
|
+
}
|
|
259
|
+
cmdArgs = { object };
|
|
260
|
+
if (a.variant !== undefined) cmdArgs.variant = String(a.variant);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
const session = a.session_id !== undefined && a.session_id !== null ? String(a.session_id) : null;
|
|
264
|
+
const newSession = a.new_session === true;
|
|
265
|
+
const budgetMs = a.timeout_ms !== undefined ? Number(a.timeout_ms) : undefined;
|
|
266
|
+
|
|
267
|
+
const outcome = await executeJoeCommand({
|
|
268
|
+
apiKey: params.apiKey,
|
|
269
|
+
apiBaseUrl: params.apiBaseUrl,
|
|
270
|
+
command,
|
|
271
|
+
project,
|
|
272
|
+
sql,
|
|
273
|
+
args: cmdArgs,
|
|
274
|
+
session,
|
|
275
|
+
newSession,
|
|
276
|
+
orgId: params.orgId,
|
|
277
|
+
budgetMs,
|
|
278
|
+
debug: params.debug,
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
const payload = {
|
|
282
|
+
command,
|
|
283
|
+
command_id: outcome.commandId,
|
|
284
|
+
session_id: outcome.sessionId,
|
|
285
|
+
status: outcome.status,
|
|
286
|
+
budget_expired: outcome.budgetExpired,
|
|
287
|
+
resume: outcome.budgetExpired ? `pgai result ${outcome.commandId}` : undefined,
|
|
288
|
+
result: outcome.result,
|
|
289
|
+
};
|
|
290
|
+
const isError = outcome.status === "error" || outcome.status === "timed_out";
|
|
291
|
+
return { content: [{ type: "text", text: JSON.stringify(payload, null, 2) }], isError };
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Resolve a DBLab-companion tool's `project` arg to its single DBLab instance id
|
|
296
|
+
* (SPEC §8). Mirrors the CLI's `resolveDblabTarget`. Throws on a missing/unknown
|
|
297
|
+
* project — the caller's try/catch turns that into an `isError` tool response.
|
|
298
|
+
*/
|
|
299
|
+
async function resolveDblabInstanceForTool(
|
|
300
|
+
apiKey: string,
|
|
301
|
+
apiBaseUrl: string,
|
|
302
|
+
args: Record<string, unknown>,
|
|
303
|
+
cfgOrgId: number | null,
|
|
304
|
+
debug: boolean
|
|
305
|
+
): Promise<string> {
|
|
306
|
+
const project = String(args.project || "").trim();
|
|
307
|
+
if (!project) {
|
|
308
|
+
throw new Error("project is required (project id or alias)");
|
|
309
|
+
}
|
|
310
|
+
const orgId = args.org_id !== undefined ? Number(args.org_id) : cfgOrgId ?? undefined;
|
|
311
|
+
return resolveDblabInstanceId({ apiKey, apiBaseUrl, project, orgId, debug });
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function optStr(v: unknown): string | undefined {
|
|
315
|
+
return v !== undefined && v !== null && String(v).length > 0 ? String(v) : undefined;
|
|
316
|
+
}
|
|
317
|
+
|
|
53
318
|
/** Handle MCP tool calls - exported for testing */
|
|
54
319
|
export async function handleToolCall(
|
|
55
320
|
req: McpToolRequest,
|
|
@@ -369,6 +634,144 @@ export async function handleToolCall(
|
|
|
369
634
|
return { content: [{ type: "text", text: JSON.stringify(files, null, 2) }] };
|
|
370
635
|
}
|
|
371
636
|
|
|
637
|
+
// Org-level project discovery (NOT a Joe endpoint) — mirrors `pgai projects`.
|
|
638
|
+
if (toolName === "list_projects") {
|
|
639
|
+
const orgId = args.org_id !== undefined ? Number(args.org_id) : cfg.orgId ?? undefined;
|
|
640
|
+
const projects = await listProjects({ apiKey, apiBaseUrl, orgId, debug });
|
|
641
|
+
return { content: [{ type: "text", text: JSON.stringify(projects, null, 2) }] };
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
// Joe command tools (SPEC §5) — each a thin wrapper over the same joe.ts pipeline.
|
|
645
|
+
if (toolName in JOE_TOOL_TO_COMMAND) {
|
|
646
|
+
const orgId = cfg.orgId ?? undefined;
|
|
647
|
+
return await runJoeTool({ toolName, apiKey, apiBaseUrl, orgId, args, debug });
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
// ---- DBLab companion tools (SPEC §8) --------------------------------
|
|
651
|
+
// Each mirrors the CLI `pgai clone|branch|snapshot …` verb 1:1: resolve the
|
|
652
|
+
// project's single DBLab instance, then proxy the existing Platform DBLab
|
|
653
|
+
// API (v1.dblab_api_call). Destructive verbs are `joe:admin`-gated backend
|
|
654
|
+
// side (a PT403 surfaces as an isError response via the outer catch).
|
|
655
|
+
if (toolName === "clone_create") {
|
|
656
|
+
const instanceId = await resolveDblabInstanceForTool(apiKey, apiBaseUrl, args, cfg.orgId, debug);
|
|
657
|
+
const result = await createClone({
|
|
658
|
+
apiKey, apiBaseUrl, instanceId,
|
|
659
|
+
cloneId: optStr(args.clone_id),
|
|
660
|
+
branch: optStr(args.branch),
|
|
661
|
+
snapshotId: optStr(args.snapshot_id),
|
|
662
|
+
dbUser: optStr(args.db_user),
|
|
663
|
+
dbPassword: optStr(args.db_password),
|
|
664
|
+
isProtected: args.protected === true,
|
|
665
|
+
debug,
|
|
666
|
+
});
|
|
667
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
if (toolName === "clone_list") {
|
|
671
|
+
const instanceId = await resolveDblabInstanceForTool(apiKey, apiBaseUrl, args, cfg.orgId, debug);
|
|
672
|
+
const result = await listClones({ apiKey, apiBaseUrl, instanceId, debug });
|
|
673
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
if (toolName === "clone_status") {
|
|
677
|
+
const cloneId = String(args.clone_id || "").trim();
|
|
678
|
+
if (!cloneId) return { content: [{ type: "text", text: "clone_id is required" }], isError: true };
|
|
679
|
+
const instanceId = await resolveDblabInstanceForTool(apiKey, apiBaseUrl, args, cfg.orgId, debug);
|
|
680
|
+
const result = await getClone({ apiKey, apiBaseUrl, instanceId, cloneId, debug });
|
|
681
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
if (toolName === "clone_reset") {
|
|
685
|
+
const cloneId = String(args.clone_id || "").trim();
|
|
686
|
+
if (!cloneId) return { content: [{ type: "text", text: "clone_id is required" }], isError: true };
|
|
687
|
+
const instanceId = await resolveDblabInstanceForTool(apiKey, apiBaseUrl, args, cfg.orgId, debug);
|
|
688
|
+
const result = await resetClone({
|
|
689
|
+
apiKey, apiBaseUrl, instanceId, cloneId,
|
|
690
|
+
snapshotId: optStr(args.snapshot_id),
|
|
691
|
+
latest: args.latest === true ? true : undefined,
|
|
692
|
+
debug,
|
|
693
|
+
});
|
|
694
|
+
return { content: [{ type: "text", text: JSON.stringify(result ?? { reset: true, cloneId }, null, 2) }] };
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
if (toolName === "clone_destroy") {
|
|
698
|
+
const cloneId = String(args.clone_id || "").trim();
|
|
699
|
+
if (!cloneId) return { content: [{ type: "text", text: "clone_id is required" }], isError: true };
|
|
700
|
+
const instanceId = await resolveDblabInstanceForTool(apiKey, apiBaseUrl, args, cfg.orgId, debug);
|
|
701
|
+
const result = await destroyClone({ apiKey, apiBaseUrl, instanceId, cloneId, debug });
|
|
702
|
+
return { content: [{ type: "text", text: JSON.stringify(result ?? { destroyed: true, cloneId }, null, 2) }] };
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
if (toolName === "branch_list") {
|
|
706
|
+
const instanceId = await resolveDblabInstanceForTool(apiKey, apiBaseUrl, args, cfg.orgId, debug);
|
|
707
|
+
const result = await listBranches({ apiKey, apiBaseUrl, instanceId, debug });
|
|
708
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
if (toolName === "branch_create") {
|
|
712
|
+
const branchName = String(args.name || "").trim();
|
|
713
|
+
if (!branchName) return { content: [{ type: "text", text: "name is required" }], isError: true };
|
|
714
|
+
const instanceId = await resolveDblabInstanceForTool(apiKey, apiBaseUrl, args, cfg.orgId, debug);
|
|
715
|
+
const result = await createBranch({
|
|
716
|
+
apiKey, apiBaseUrl, instanceId, branchName,
|
|
717
|
+
baseBranch: optStr(args.base_branch),
|
|
718
|
+
snapshotId: optStr(args.snapshot_id),
|
|
719
|
+
debug,
|
|
720
|
+
});
|
|
721
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
if (toolName === "branch_delete") {
|
|
725
|
+
const branchName = String(args.name || "").trim();
|
|
726
|
+
if (!branchName) return { content: [{ type: "text", text: "name is required" }], isError: true };
|
|
727
|
+
const instanceId = await resolveDblabInstanceForTool(apiKey, apiBaseUrl, args, cfg.orgId, debug);
|
|
728
|
+
const result = await deleteBranch({ apiKey, apiBaseUrl, instanceId, branchName, debug });
|
|
729
|
+
return { content: [{ type: "text", text: JSON.stringify(result ?? { deleted: true, branch: branchName }, null, 2) }] };
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
if (toolName === "branch_log") {
|
|
733
|
+
const branchName = String(args.name || "").trim();
|
|
734
|
+
if (!branchName) return { content: [{ type: "text", text: "name is required" }], isError: true };
|
|
735
|
+
const instanceId = await resolveDblabInstanceForTool(apiKey, apiBaseUrl, args, cfg.orgId, debug);
|
|
736
|
+
const result = await branchLog({ apiKey, apiBaseUrl, instanceId, branchName, debug });
|
|
737
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
if (toolName === "snapshot_list") {
|
|
741
|
+
const instanceId = await resolveDblabInstanceForTool(apiKey, apiBaseUrl, args, cfg.orgId, debug);
|
|
742
|
+
const result = await listSnapshots({
|
|
743
|
+
apiKey, apiBaseUrl, instanceId,
|
|
744
|
+
branchName: optStr(args.branch),
|
|
745
|
+
dataset: optStr(args.dataset),
|
|
746
|
+
debug,
|
|
747
|
+
});
|
|
748
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
if (toolName === "snapshot_create") {
|
|
752
|
+
const cloneId = String(args.clone_id || "").trim();
|
|
753
|
+
if (!cloneId) return { content: [{ type: "text", text: "clone_id is required" }], isError: true };
|
|
754
|
+
const instanceId = await resolveDblabInstanceForTool(apiKey, apiBaseUrl, args, cfg.orgId, debug);
|
|
755
|
+
const result = await createSnapshot({
|
|
756
|
+
apiKey, apiBaseUrl, instanceId, cloneId,
|
|
757
|
+
message: optStr(args.message),
|
|
758
|
+
debug,
|
|
759
|
+
});
|
|
760
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
if (toolName === "snapshot_destroy") {
|
|
764
|
+
const snapshotId = String(args.snapshot_id || "").trim();
|
|
765
|
+
if (!snapshotId) return { content: [{ type: "text", text: "snapshot_id is required" }], isError: true };
|
|
766
|
+
const instanceId = await resolveDblabInstanceForTool(apiKey, apiBaseUrl, args, cfg.orgId, debug);
|
|
767
|
+
const result = await destroySnapshot({
|
|
768
|
+
apiKey, apiBaseUrl, instanceId, snapshotId,
|
|
769
|
+
force: args.force === true,
|
|
770
|
+
debug,
|
|
771
|
+
});
|
|
772
|
+
return { content: [{ type: "text", text: JSON.stringify(result ?? { destroyed: true, snapshot: snapshotId }, null, 2) }] };
|
|
773
|
+
}
|
|
774
|
+
|
|
372
775
|
throw new Error(`Unknown tool: ${toolName}`);
|
|
373
776
|
} catch (err) {
|
|
374
777
|
const message = err instanceof Error ? err.message : String(err);
|
|
@@ -653,6 +1056,200 @@ export async function startMcpServer(rootOpts?: RootOptsLike, extra?: { debug?:
|
|
|
653
1056
|
additionalProperties: false,
|
|
654
1057
|
},
|
|
655
1058
|
},
|
|
1059
|
+
...joeToolDefinitions(),
|
|
1060
|
+
// DBLab companion tools (SPEC §8) — proxy the existing Platform DBLab API
|
|
1061
|
+
// (v1.dblab_api_call) the Console drives; mirror `pgai clone|branch|snapshot`.
|
|
1062
|
+
// `project` (id or alias) resolves the project's single DBLab instance.
|
|
1063
|
+
{
|
|
1064
|
+
name: "clone_create",
|
|
1065
|
+
description: "Create a DBLab thin clone of the project's database (same as CLI 'clone create'). Requires the joe:plan scope.",
|
|
1066
|
+
inputSchema: {
|
|
1067
|
+
type: "object",
|
|
1068
|
+
properties: {
|
|
1069
|
+
project: { type: "string", description: "Project id or alias" },
|
|
1070
|
+
branch: { type: "string", description: "Branch to clone from" },
|
|
1071
|
+
snapshot_id: { type: "string", description: "Snapshot id to clone from" },
|
|
1072
|
+
clone_id: { type: "string", description: "Clone id (DBLab generates one when omitted)" },
|
|
1073
|
+
db_user: { type: "string", description: "Clone DB user" },
|
|
1074
|
+
db_password: { type: "string", description: "Clone DB password" },
|
|
1075
|
+
protected: { type: "boolean", description: "Protect the clone from auto-deletion" },
|
|
1076
|
+
org_id: { type: "number", description: "Organization id (optional, falls back to config)" },
|
|
1077
|
+
debug: { type: "boolean", description: "Enable verbose debug logs" },
|
|
1078
|
+
},
|
|
1079
|
+
required: ["project"],
|
|
1080
|
+
additionalProperties: false,
|
|
1081
|
+
},
|
|
1082
|
+
},
|
|
1083
|
+
{
|
|
1084
|
+
name: "clone_list",
|
|
1085
|
+
description: "List the project's DBLab thin clones (same as CLI 'clone list').",
|
|
1086
|
+
inputSchema: {
|
|
1087
|
+
type: "object",
|
|
1088
|
+
properties: {
|
|
1089
|
+
project: { type: "string", description: "Project id or alias" },
|
|
1090
|
+
org_id: { type: "number", description: "Organization id (optional, falls back to config)" },
|
|
1091
|
+
debug: { type: "boolean", description: "Enable verbose debug logs" },
|
|
1092
|
+
},
|
|
1093
|
+
required: ["project"],
|
|
1094
|
+
additionalProperties: false,
|
|
1095
|
+
},
|
|
1096
|
+
},
|
|
1097
|
+
{
|
|
1098
|
+
name: "clone_status",
|
|
1099
|
+
description: "Show a DBLab clone's status (same as CLI 'clone status').",
|
|
1100
|
+
inputSchema: {
|
|
1101
|
+
type: "object",
|
|
1102
|
+
properties: {
|
|
1103
|
+
project: { type: "string", description: "Project id or alias" },
|
|
1104
|
+
clone_id: { type: "string", description: "Clone id" },
|
|
1105
|
+
org_id: { type: "number", description: "Organization id (optional, falls back to config)" },
|
|
1106
|
+
debug: { type: "boolean", description: "Enable verbose debug logs" },
|
|
1107
|
+
},
|
|
1108
|
+
required: ["project", "clone_id"],
|
|
1109
|
+
additionalProperties: false,
|
|
1110
|
+
},
|
|
1111
|
+
},
|
|
1112
|
+
{
|
|
1113
|
+
name: "clone_reset",
|
|
1114
|
+
description: "Reset a DBLab clone to a pristine snapshot (same as CLI 'clone reset'). Requires the joe:admin scope.",
|
|
1115
|
+
inputSchema: {
|
|
1116
|
+
type: "object",
|
|
1117
|
+
properties: {
|
|
1118
|
+
project: { type: "string", description: "Project id or alias" },
|
|
1119
|
+
clone_id: { type: "string", description: "Clone id" },
|
|
1120
|
+
snapshot_id: { type: "string", description: "Snapshot id to reset to (default: latest)" },
|
|
1121
|
+
latest: { type: "boolean", description: "Reset to the latest snapshot" },
|
|
1122
|
+
org_id: { type: "number", description: "Organization id (optional, falls back to config)" },
|
|
1123
|
+
debug: { type: "boolean", description: "Enable verbose debug logs" },
|
|
1124
|
+
},
|
|
1125
|
+
required: ["project", "clone_id"],
|
|
1126
|
+
additionalProperties: false,
|
|
1127
|
+
},
|
|
1128
|
+
},
|
|
1129
|
+
{
|
|
1130
|
+
name: "clone_destroy",
|
|
1131
|
+
description: "Destroy a DBLab clone (same as CLI 'clone destroy'). Requires the joe:admin scope.",
|
|
1132
|
+
inputSchema: {
|
|
1133
|
+
type: "object",
|
|
1134
|
+
properties: {
|
|
1135
|
+
project: { type: "string", description: "Project id or alias" },
|
|
1136
|
+
clone_id: { type: "string", description: "Clone id" },
|
|
1137
|
+
org_id: { type: "number", description: "Organization id (optional, falls back to config)" },
|
|
1138
|
+
debug: { type: "boolean", description: "Enable verbose debug logs" },
|
|
1139
|
+
},
|
|
1140
|
+
required: ["project", "clone_id"],
|
|
1141
|
+
additionalProperties: false,
|
|
1142
|
+
},
|
|
1143
|
+
},
|
|
1144
|
+
{
|
|
1145
|
+
name: "branch_list",
|
|
1146
|
+
description: "List the project's DBLab branches (same as CLI 'branch list').",
|
|
1147
|
+
inputSchema: {
|
|
1148
|
+
type: "object",
|
|
1149
|
+
properties: {
|
|
1150
|
+
project: { type: "string", description: "Project id or alias" },
|
|
1151
|
+
org_id: { type: "number", description: "Organization id (optional, falls back to config)" },
|
|
1152
|
+
debug: { type: "boolean", description: "Enable verbose debug logs" },
|
|
1153
|
+
},
|
|
1154
|
+
required: ["project"],
|
|
1155
|
+
additionalProperties: false,
|
|
1156
|
+
},
|
|
1157
|
+
},
|
|
1158
|
+
{
|
|
1159
|
+
name: "branch_create",
|
|
1160
|
+
description: "Create a DBLab branch (same as CLI 'branch create'). Requires the joe:plan scope.",
|
|
1161
|
+
inputSchema: {
|
|
1162
|
+
type: "object",
|
|
1163
|
+
properties: {
|
|
1164
|
+
project: { type: "string", description: "Project id or alias" },
|
|
1165
|
+
name: { type: "string", description: "Branch name" },
|
|
1166
|
+
snapshot_id: { type: "string", description: "Snapshot id to base the branch on" },
|
|
1167
|
+
base_branch: { type: "string", description: "Parent branch to fork from" },
|
|
1168
|
+
org_id: { type: "number", description: "Organization id (optional, falls back to config)" },
|
|
1169
|
+
debug: { type: "boolean", description: "Enable verbose debug logs" },
|
|
1170
|
+
},
|
|
1171
|
+
required: ["project", "name"],
|
|
1172
|
+
additionalProperties: false,
|
|
1173
|
+
},
|
|
1174
|
+
},
|
|
1175
|
+
{
|
|
1176
|
+
name: "branch_delete",
|
|
1177
|
+
description: "Delete a DBLab branch (same as CLI 'branch delete'). Requires the joe:admin scope.",
|
|
1178
|
+
inputSchema: {
|
|
1179
|
+
type: "object",
|
|
1180
|
+
properties: {
|
|
1181
|
+
project: { type: "string", description: "Project id or alias" },
|
|
1182
|
+
name: { type: "string", description: "Branch name" },
|
|
1183
|
+
org_id: { type: "number", description: "Organization id (optional, falls back to config)" },
|
|
1184
|
+
debug: { type: "boolean", description: "Enable verbose debug logs" },
|
|
1185
|
+
},
|
|
1186
|
+
required: ["project", "name"],
|
|
1187
|
+
additionalProperties: false,
|
|
1188
|
+
},
|
|
1189
|
+
},
|
|
1190
|
+
{
|
|
1191
|
+
name: "branch_log",
|
|
1192
|
+
description: "Show a DBLab branch's snapshot log (same as CLI 'branch log').",
|
|
1193
|
+
inputSchema: {
|
|
1194
|
+
type: "object",
|
|
1195
|
+
properties: {
|
|
1196
|
+
project: { type: "string", description: "Project id or alias" },
|
|
1197
|
+
name: { type: "string", description: "Branch name" },
|
|
1198
|
+
org_id: { type: "number", description: "Organization id (optional, falls back to config)" },
|
|
1199
|
+
debug: { type: "boolean", description: "Enable verbose debug logs" },
|
|
1200
|
+
},
|
|
1201
|
+
required: ["project", "name"],
|
|
1202
|
+
additionalProperties: false,
|
|
1203
|
+
},
|
|
1204
|
+
},
|
|
1205
|
+
{
|
|
1206
|
+
name: "snapshot_list",
|
|
1207
|
+
description: "List the project's DBLab snapshots (same as CLI 'snapshot list').",
|
|
1208
|
+
inputSchema: {
|
|
1209
|
+
type: "object",
|
|
1210
|
+
properties: {
|
|
1211
|
+
project: { type: "string", description: "Project id or alias" },
|
|
1212
|
+
branch: { type: "string", description: "Filter by branch" },
|
|
1213
|
+
dataset: { type: "string", description: "Filter by dataset" },
|
|
1214
|
+
org_id: { type: "number", description: "Organization id (optional, falls back to config)" },
|
|
1215
|
+
debug: { type: "boolean", description: "Enable verbose debug logs" },
|
|
1216
|
+
},
|
|
1217
|
+
required: ["project"],
|
|
1218
|
+
additionalProperties: false,
|
|
1219
|
+
},
|
|
1220
|
+
},
|
|
1221
|
+
{
|
|
1222
|
+
name: "snapshot_create",
|
|
1223
|
+
description: "Create a DBLab snapshot from a clone (same as CLI 'snapshot create'). Requires the joe:plan scope.",
|
|
1224
|
+
inputSchema: {
|
|
1225
|
+
type: "object",
|
|
1226
|
+
properties: {
|
|
1227
|
+
project: { type: "string", description: "Project id or alias" },
|
|
1228
|
+
clone_id: { type: "string", description: "Clone id to snapshot" },
|
|
1229
|
+
message: { type: "string", description: "Snapshot message" },
|
|
1230
|
+
org_id: { type: "number", description: "Organization id (optional, falls back to config)" },
|
|
1231
|
+
debug: { type: "boolean", description: "Enable verbose debug logs" },
|
|
1232
|
+
},
|
|
1233
|
+
required: ["project", "clone_id"],
|
|
1234
|
+
additionalProperties: false,
|
|
1235
|
+
},
|
|
1236
|
+
},
|
|
1237
|
+
{
|
|
1238
|
+
name: "snapshot_destroy",
|
|
1239
|
+
description: "Destroy a DBLab snapshot (same as CLI 'snapshot destroy'). Requires the joe:admin scope.",
|
|
1240
|
+
inputSchema: {
|
|
1241
|
+
type: "object",
|
|
1242
|
+
properties: {
|
|
1243
|
+
project: { type: "string", description: "Project id or alias" },
|
|
1244
|
+
snapshot_id: { type: "string", description: "Snapshot id" },
|
|
1245
|
+
force: { type: "boolean", description: "Force-delete even when dependent clones exist" },
|
|
1246
|
+
org_id: { type: "number", description: "Organization id (optional, falls back to config)" },
|
|
1247
|
+
debug: { type: "boolean", description: "Enable verbose debug logs" },
|
|
1248
|
+
},
|
|
1249
|
+
required: ["project", "snapshot_id"],
|
|
1250
|
+
additionalProperties: false,
|
|
1251
|
+
},
|
|
1252
|
+
},
|
|
656
1253
|
],
|
|
657
1254
|
};
|
|
658
1255
|
});
|
package/lib/supabase.ts
CHANGED
|
@@ -733,24 +733,6 @@ export async function verifyInitSetupViaSupabase(params: {
|
|
|
733
733
|
}
|
|
734
734
|
|
|
735
735
|
// Check helper functions - first verify they exist to avoid has_function_privilege errors
|
|
736
|
-
const explainFnExistsRes = await params.client.query(
|
|
737
|
-
"SELECT oid FROM pg_proc WHERE proname = 'explain_generic' AND pronamespace = (SELECT oid FROM pg_namespace WHERE nspname = 'postgres_ai')",
|
|
738
|
-
true
|
|
739
|
-
);
|
|
740
|
-
if (explainFnExistsRes.rowCount === 0) {
|
|
741
|
-
missingRequired.push("function postgres_ai.explain_generic exists");
|
|
742
|
-
} else {
|
|
743
|
-
const explainFnRes = await params.client.query(
|
|
744
|
-
`SELECT has_function_privilege('${escapeLiteral(role)}', 'postgres_ai.explain_generic(text, text, text)', 'EXECUTE') as ok`,
|
|
745
|
-
true
|
|
746
|
-
);
|
|
747
|
-
if (!explainFnRes.rows?.[0]?.ok) {
|
|
748
|
-
missingRequired.push(
|
|
749
|
-
"EXECUTE on postgres_ai.explain_generic(text, text, text)"
|
|
750
|
-
);
|
|
751
|
-
}
|
|
752
|
-
}
|
|
753
|
-
|
|
754
736
|
const tableDescribeFnExistsRes = await params.client.query(
|
|
755
737
|
"SELECT oid FROM pg_proc WHERE proname = 'table_describe' AND pronamespace = (SELECT oid FROM pg_namespace WHERE nspname = 'postgres_ai')",
|
|
756
738
|
true
|