@aliyun-rds/supabase-mcp-server 1.0.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/README.md +203 -0
- package/dist/index.js +1875 -0
- package/package.json +50 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1875 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/index.ts
|
|
4
|
+
import { Command } from "commander";
|
|
5
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
6
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
7
|
+
import {
|
|
8
|
+
CallToolRequestSchema,
|
|
9
|
+
ErrorCode,
|
|
10
|
+
ListToolsRequestSchema,
|
|
11
|
+
McpError
|
|
12
|
+
} from "@modelcontextprotocol/sdk/types.js";
|
|
13
|
+
|
|
14
|
+
// src/client/index.ts
|
|
15
|
+
import { createClient } from "@supabase/supabase-js";
|
|
16
|
+
import { Pool } from "pg";
|
|
17
|
+
var SelfhostedSupabaseClient = class _SelfhostedSupabaseClient {
|
|
18
|
+
options;
|
|
19
|
+
supabase;
|
|
20
|
+
pgPool = null;
|
|
21
|
+
// Lazy initialized pool for direct DB access
|
|
22
|
+
rpcFunctionExists = false;
|
|
23
|
+
// SQL definition for the helper function
|
|
24
|
+
static CREATE_EXECUTE_SQL_FUNCTION = `
|
|
25
|
+
CREATE OR REPLACE FUNCTION public.execute_sql(query text, read_only boolean DEFAULT false)
|
|
26
|
+
RETURNS jsonb -- Using jsonb is generally preferred over json
|
|
27
|
+
LANGUAGE plpgsql
|
|
28
|
+
AS $$
|
|
29
|
+
DECLARE
|
|
30
|
+
result jsonb;
|
|
31
|
+
BEGIN
|
|
32
|
+
-- Note: SET TRANSACTION READ ONLY might not behave as expected within a function
|
|
33
|
+
-- depending on the outer transaction state. Handle read-only logic outside if needed.
|
|
34
|
+
|
|
35
|
+
-- Execute the dynamic query and aggregate results into a JSONB array
|
|
36
|
+
EXECUTE 'SELECT COALESCE(jsonb_agg(t), ''[]''::jsonb) FROM (' || query || ') t' INTO result;
|
|
37
|
+
|
|
38
|
+
RETURN result;
|
|
39
|
+
EXCEPTION
|
|
40
|
+
WHEN others THEN
|
|
41
|
+
-- Rethrow the error with context, including the original SQLSTATE
|
|
42
|
+
RAISE EXCEPTION 'Error executing SQL (SQLSTATE: %): % ', SQLSTATE, SQLERRM;
|
|
43
|
+
END;
|
|
44
|
+
$$;
|
|
45
|
+
`;
|
|
46
|
+
// SQL to grant permissions
|
|
47
|
+
static GRANT_EXECUTE_SQL_FUNCTION = `
|
|
48
|
+
GRANT EXECUTE ON FUNCTION public.execute_sql(text, boolean) TO authenticated;
|
|
49
|
+
-- Optionally grant to anon if needed (uncomment if required):
|
|
50
|
+
-- GRANT EXECUTE ON FUNCTION public.execute_sql(text, boolean) TO anon;
|
|
51
|
+
`;
|
|
52
|
+
/**
|
|
53
|
+
* Creates an instance of SelfhostedSupabaseClient.
|
|
54
|
+
* Note: Call initialize() after creating the instance to check for RPC functions.
|
|
55
|
+
* @param options - Configuration options for the client.
|
|
56
|
+
*/
|
|
57
|
+
constructor(options) {
|
|
58
|
+
this.options = options;
|
|
59
|
+
this.supabase = createClient(options.supabaseUrl, options.supabaseAnonKey, options.supabaseClientOptions);
|
|
60
|
+
if (!options.supabaseUrl || !options.supabaseAnonKey) {
|
|
61
|
+
throw new Error("Supabase URL and Anon Key are required.");
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Factory function to create and asynchronously initialize the client.
|
|
66
|
+
* Checks for the existence of the helper RPC function.
|
|
67
|
+
*/
|
|
68
|
+
static async create(options) {
|
|
69
|
+
const client = new _SelfhostedSupabaseClient(options);
|
|
70
|
+
await client.initialize();
|
|
71
|
+
return client;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Initializes the client by checking for the required RPC function.
|
|
75
|
+
* Attempts to create the function if it doesn't exist and a service role key is provided.
|
|
76
|
+
*/
|
|
77
|
+
async initialize() {
|
|
78
|
+
console.error("Initializing SelfhostedSupabaseClient...");
|
|
79
|
+
try {
|
|
80
|
+
await this.checkAndCreateRpcFunction();
|
|
81
|
+
console.error(`RPC function 'public.execute_sql' status: ${this.rpcFunctionExists ? "Available" : "Unavailable"}`);
|
|
82
|
+
} catch (error) {
|
|
83
|
+
console.error("Error during client initialization:", error);
|
|
84
|
+
}
|
|
85
|
+
console.error("Initialization complete.");
|
|
86
|
+
}
|
|
87
|
+
// --- Public Methods (to be implemented) ---
|
|
88
|
+
/**
|
|
89
|
+
* Executes SQL using the preferred RPC method.
|
|
90
|
+
*/
|
|
91
|
+
async executeSqlViaRpc(query, readOnly = false) {
|
|
92
|
+
if (!this.rpcFunctionExists) {
|
|
93
|
+
console.error("Attempted to call executeSqlViaRpc, but RPC function is not available.");
|
|
94
|
+
return {
|
|
95
|
+
error: {
|
|
96
|
+
message: "execute_sql RPC function not found or client not properly initialized.",
|
|
97
|
+
code: "MCP_CLIENT_ERROR"
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
console.error(`Executing via RPC (readOnly: ${readOnly}): ${query.substring(0, 100)}...`);
|
|
102
|
+
try {
|
|
103
|
+
const { data, error } = await this.supabase.rpc("execute_sql", {
|
|
104
|
+
query,
|
|
105
|
+
read_only: readOnly
|
|
106
|
+
});
|
|
107
|
+
if (error) {
|
|
108
|
+
console.error("Error executing SQL via RPC:", error);
|
|
109
|
+
return {
|
|
110
|
+
error: {
|
|
111
|
+
message: error.message,
|
|
112
|
+
code: error.code,
|
|
113
|
+
// Propagate Supabase/PostgREST error code
|
|
114
|
+
details: error.details,
|
|
115
|
+
hint: error.hint
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
if (Array.isArray(data)) {
|
|
120
|
+
return data;
|
|
121
|
+
}
|
|
122
|
+
console.error("Unexpected response format from execute_sql RPC:", data);
|
|
123
|
+
return {
|
|
124
|
+
error: {
|
|
125
|
+
message: "Unexpected response format from execute_sql RPC. Expected JSON array.",
|
|
126
|
+
code: "MCP_RPC_FORMAT_ERROR"
|
|
127
|
+
}
|
|
128
|
+
};
|
|
129
|
+
} catch (rpcError) {
|
|
130
|
+
const errorMessage = rpcError instanceof Error ? rpcError.message : String(rpcError);
|
|
131
|
+
console.error("Exception during executeSqlViaRpc call:", rpcError);
|
|
132
|
+
return {
|
|
133
|
+
error: {
|
|
134
|
+
message: `Exception during RPC call: ${errorMessage}`,
|
|
135
|
+
code: "MCP_RPC_EXCEPTION"
|
|
136
|
+
}
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Executes SQL directly against the database using the pg library.
|
|
142
|
+
* Requires DATABASE_URL to be configured.
|
|
143
|
+
* Useful for simple queries when RPC is unavailable or direct access is preferred.
|
|
144
|
+
* NOTE: Does not support transactions or parameterization directly.
|
|
145
|
+
* Consider executeTransactionWithPg for more complex operations.
|
|
146
|
+
*/
|
|
147
|
+
async executeSqlWithPg(query) {
|
|
148
|
+
if (!this.options.databaseUrl) {
|
|
149
|
+
return { error: { message: "DATABASE_URL is not configured. Cannot execute SQL directly.", code: "MCP_CONFIG_ERROR" } };
|
|
150
|
+
}
|
|
151
|
+
await this.ensurePgPool();
|
|
152
|
+
if (!this.pgPool) {
|
|
153
|
+
return { error: { message: "pg Pool not available after initialization attempt.", code: "MCP_POOL_ERROR" } };
|
|
154
|
+
}
|
|
155
|
+
let client;
|
|
156
|
+
try {
|
|
157
|
+
client = await this.pgPool.connect();
|
|
158
|
+
console.error(`Executing via pg: ${query.substring(0, 100)}...`);
|
|
159
|
+
const result = await client.query(query);
|
|
160
|
+
return result.rows;
|
|
161
|
+
} catch (dbError) {
|
|
162
|
+
const error = dbError instanceof Error ? dbError : new Error(String(dbError));
|
|
163
|
+
console.error("Error executing SQL with pg:", error);
|
|
164
|
+
const code = dbError?.code || "PG_ERROR";
|
|
165
|
+
return { error: { message: error.message, code } };
|
|
166
|
+
} finally {
|
|
167
|
+
client?.release();
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* Ensures the pg connection pool is initialized.
|
|
172
|
+
* Should be called before accessing this.pgPool.
|
|
173
|
+
*/
|
|
174
|
+
async ensurePgPool() {
|
|
175
|
+
if (this.pgPool) return;
|
|
176
|
+
if (!this.options.databaseUrl) {
|
|
177
|
+
throw new Error("DATABASE_URL is not configured. Cannot initialize pg pool.");
|
|
178
|
+
}
|
|
179
|
+
console.error("Initializing pg pool...");
|
|
180
|
+
this.pgPool = new Pool({ connectionString: this.options.databaseUrl });
|
|
181
|
+
this.pgPool.on("error", (err, client) => {
|
|
182
|
+
console.error("PG Pool Error: Unexpected error on idle client", err);
|
|
183
|
+
});
|
|
184
|
+
try {
|
|
185
|
+
const client = await this.pgPool.connect();
|
|
186
|
+
console.error("pg pool connected successfully.");
|
|
187
|
+
client.release();
|
|
188
|
+
} catch (err) {
|
|
189
|
+
console.error("Failed to connect pg pool:", err);
|
|
190
|
+
await this.pgPool.end();
|
|
191
|
+
this.pgPool = null;
|
|
192
|
+
throw new Error(`Failed to connect pg pool: ${err instanceof Error ? err.message : String(err)}`);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* Executes a series of operations within a single database transaction using the pg library.
|
|
197
|
+
* Requires DATABASE_URL to be configured.
|
|
198
|
+
* @param callback A function that receives a connected pg client and performs queries.
|
|
199
|
+
* It should return a promise that resolves on success or rejects on failure.
|
|
200
|
+
* The transaction will be committed if the promise resolves,
|
|
201
|
+
* and rolled back if it rejects.
|
|
202
|
+
*/
|
|
203
|
+
async executeTransactionWithPg(callback) {
|
|
204
|
+
if (!this.options.databaseUrl) {
|
|
205
|
+
throw new Error("DATABASE_URL is not configured. Cannot execute transaction directly.");
|
|
206
|
+
}
|
|
207
|
+
await this.ensurePgPool();
|
|
208
|
+
if (!this.pgPool) {
|
|
209
|
+
throw new Error("pg Pool not available for transaction.");
|
|
210
|
+
}
|
|
211
|
+
const client = await this.pgPool.connect();
|
|
212
|
+
try {
|
|
213
|
+
await client.query("BEGIN");
|
|
214
|
+
console.error("BEGIN transaction");
|
|
215
|
+
const result = await callback(client);
|
|
216
|
+
await client.query("COMMIT");
|
|
217
|
+
console.error("COMMIT transaction");
|
|
218
|
+
return result;
|
|
219
|
+
} catch (error) {
|
|
220
|
+
console.error("Transaction Error - Rolling back:", error);
|
|
221
|
+
await client.query("ROLLBACK");
|
|
222
|
+
console.error("ROLLBACK transaction");
|
|
223
|
+
throw error;
|
|
224
|
+
} finally {
|
|
225
|
+
client.release();
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
// --- Helper/Private Methods (to be implemented) ---
|
|
229
|
+
async checkAndCreateRpcFunction() {
|
|
230
|
+
console.error("Checking for public.execute_sql RPC function...");
|
|
231
|
+
try {
|
|
232
|
+
const { error } = await this.supabase.rpc("execute_sql", { query: "SELECT 1" });
|
|
233
|
+
if (!error) {
|
|
234
|
+
console.error("'public.execute_sql' function found.");
|
|
235
|
+
this.rpcFunctionExists = true;
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
const UNDEFINED_FUNCTION_ERROR_CODE = "42883";
|
|
239
|
+
const POSTGREST_FUNCTION_NOT_FOUND_CODE = "PGRST202";
|
|
240
|
+
if (error.code === UNDEFINED_FUNCTION_ERROR_CODE || error.code === POSTGREST_FUNCTION_NOT_FOUND_CODE) {
|
|
241
|
+
console.error(
|
|
242
|
+
`'public.execute_sql' function not found (Code: ${error.code}). Attempting creation...`
|
|
243
|
+
);
|
|
244
|
+
if (!this.options.supabaseServiceRoleKey) {
|
|
245
|
+
console.error("Cannot create 'public.execute_sql': supabaseServiceRoleKey not provided.");
|
|
246
|
+
this.rpcFunctionExists = false;
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
if (!this.options.databaseUrl) {
|
|
250
|
+
console.error("Cannot create 'public.execute_sql' reliably without databaseUrl for direct connection.");
|
|
251
|
+
this.rpcFunctionExists = false;
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
try {
|
|
255
|
+
console.error("Creating 'public.execute_sql' function using direct DB connection...");
|
|
256
|
+
await this.executeSqlWithPg(_SelfhostedSupabaseClient.CREATE_EXECUTE_SQL_FUNCTION);
|
|
257
|
+
await this.executeSqlWithPg(_SelfhostedSupabaseClient.GRANT_EXECUTE_SQL_FUNCTION);
|
|
258
|
+
console.error("'public.execute_sql' function created and permissions granted successfully.");
|
|
259
|
+
console.error("Notifying PostgREST to reload schema cache...");
|
|
260
|
+
await this.executeSqlWithPg("NOTIFY pgrst, 'reload schema'");
|
|
261
|
+
console.error("PostgREST schema reload notification sent.");
|
|
262
|
+
this.rpcFunctionExists = true;
|
|
263
|
+
} catch (creationError) {
|
|
264
|
+
const errorMessage = creationError instanceof Error ? creationError.message : String(creationError);
|
|
265
|
+
console.error("Failed to create 'public.execute_sql' function or notify PostgREST:", creationError);
|
|
266
|
+
this.rpcFunctionExists = false;
|
|
267
|
+
throw new Error(`Failed to create execute_sql function/notify: ${errorMessage}`);
|
|
268
|
+
}
|
|
269
|
+
} else {
|
|
270
|
+
console.error(
|
|
271
|
+
"Unexpected error checking for 'public.execute_sql' function:",
|
|
272
|
+
error
|
|
273
|
+
);
|
|
274
|
+
this.rpcFunctionExists = false;
|
|
275
|
+
throw new Error(
|
|
276
|
+
`Error checking for execute_sql function: ${error.message}`
|
|
277
|
+
);
|
|
278
|
+
}
|
|
279
|
+
} catch (err) {
|
|
280
|
+
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
281
|
+
console.error("Exception during RPC function check/creation:", err);
|
|
282
|
+
this.rpcFunctionExists = false;
|
|
283
|
+
throw new Error(`Exception during RPC function check/creation: ${errorMessage}`);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
// --- Getters ---
|
|
287
|
+
getSupabaseUrl() {
|
|
288
|
+
return this.options.supabaseUrl;
|
|
289
|
+
}
|
|
290
|
+
getAnonKey() {
|
|
291
|
+
return this.options.supabaseAnonKey;
|
|
292
|
+
}
|
|
293
|
+
getServiceRoleKey() {
|
|
294
|
+
return this.options.supabaseServiceRoleKey;
|
|
295
|
+
}
|
|
296
|
+
/**
|
|
297
|
+
* Gets the configured JWT secret, if provided.
|
|
298
|
+
*/
|
|
299
|
+
getJwtSecret() {
|
|
300
|
+
return this.options.jwtSecret;
|
|
301
|
+
}
|
|
302
|
+
/**
|
|
303
|
+
* Gets the configured direct database connection URL, if provided.
|
|
304
|
+
*/
|
|
305
|
+
getDbUrl() {
|
|
306
|
+
return this.options.databaseUrl;
|
|
307
|
+
}
|
|
308
|
+
/**
|
|
309
|
+
* Checks if the direct database connection (pg) is configured.
|
|
310
|
+
*/
|
|
311
|
+
isPgAvailable() {
|
|
312
|
+
return !!this.options.databaseUrl;
|
|
313
|
+
}
|
|
314
|
+
};
|
|
315
|
+
|
|
316
|
+
// src/tools/list_tables.ts
|
|
317
|
+
import { z as z2 } from "zod";
|
|
318
|
+
|
|
319
|
+
// src/tools/utils.ts
|
|
320
|
+
import { z } from "zod";
|
|
321
|
+
import { exec } from "node:child_process";
|
|
322
|
+
import { promisify } from "node:util";
|
|
323
|
+
var execAsync = promisify(exec);
|
|
324
|
+
function handleSqlResponse(result, schema) {
|
|
325
|
+
if ("error" in result) {
|
|
326
|
+
throw new Error(`SQL Error (${result.error.code}): ${result.error.message}`);
|
|
327
|
+
}
|
|
328
|
+
try {
|
|
329
|
+
return schema.parse(result);
|
|
330
|
+
} catch (validationError) {
|
|
331
|
+
if (validationError instanceof z.ZodError) {
|
|
332
|
+
throw new Error(`Schema validation failed: ${validationError.errors.map((e) => `${e.path.join(".")}: ${e.message}`).join(", ")}`);
|
|
333
|
+
}
|
|
334
|
+
throw new Error(`Unexpected validation error: ${validationError}`);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
async function runExternalCommand(command) {
|
|
338
|
+
try {
|
|
339
|
+
const { stdout, stderr } = await execAsync(command);
|
|
340
|
+
return { stdout, stderr, error: null };
|
|
341
|
+
} catch (error) {
|
|
342
|
+
const execError = error;
|
|
343
|
+
return {
|
|
344
|
+
stdout: execError.stdout || "",
|
|
345
|
+
stderr: execError.stderr || execError.message,
|
|
346
|
+
// Use message if stderr is empty
|
|
347
|
+
error: execError
|
|
348
|
+
};
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
async function executeSqlWithFallback(client, sql, readOnly = true) {
|
|
352
|
+
if (client.isPgAvailable()) {
|
|
353
|
+
console.info("Using direct database connection (bypassing JWT)...");
|
|
354
|
+
return await client.executeSqlWithPg(sql);
|
|
355
|
+
}
|
|
356
|
+
console.info("Falling back to RPC method...");
|
|
357
|
+
return await client.executeSqlViaRpc(sql, readOnly);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// src/tools/list_tables.ts
|
|
361
|
+
var ListTablesOutputSchema = z2.array(z2.object({
|
|
362
|
+
schema: z2.string(),
|
|
363
|
+
name: z2.string(),
|
|
364
|
+
comment: z2.string().nullable().optional()
|
|
365
|
+
// Add comment if available
|
|
366
|
+
}));
|
|
367
|
+
var ListTablesInputSchema = z2.object({
|
|
368
|
+
// No specific input needed for listing tables
|
|
369
|
+
// Optional: add schema filter later if needed
|
|
370
|
+
// schema: z.string().optional().describe('Filter tables by schema name.'),
|
|
371
|
+
});
|
|
372
|
+
var mcpInputSchema = {
|
|
373
|
+
type: "object",
|
|
374
|
+
properties: {},
|
|
375
|
+
required: []
|
|
376
|
+
};
|
|
377
|
+
var listTablesTool = {
|
|
378
|
+
name: "list_tables",
|
|
379
|
+
description: "Lists all accessible tables in the connected database, grouped by schema.",
|
|
380
|
+
inputSchema: ListTablesInputSchema,
|
|
381
|
+
// Use defined schema
|
|
382
|
+
mcpInputSchema,
|
|
383
|
+
// Add the static JSON schema for MCP
|
|
384
|
+
outputSchema: ListTablesOutputSchema,
|
|
385
|
+
// Use explicit types for input and context
|
|
386
|
+
execute: async (input, context) => {
|
|
387
|
+
const client = context.selfhostedClient;
|
|
388
|
+
const listTablesSql = `
|
|
389
|
+
SELECT
|
|
390
|
+
n.nspname as schema,
|
|
391
|
+
c.relname as name,
|
|
392
|
+
pgd.description as comment
|
|
393
|
+
FROM
|
|
394
|
+
pg_catalog.pg_class c
|
|
395
|
+
JOIN
|
|
396
|
+
pg_catalog.pg_namespace n ON n.oid = c.relnamespace
|
|
397
|
+
LEFT JOIN
|
|
398
|
+
pg_catalog.pg_description pgd ON pgd.objoid = c.oid AND pgd.objsubid = 0
|
|
399
|
+
WHERE
|
|
400
|
+
c.relkind = 'r' -- r = ordinary table
|
|
401
|
+
AND n.nspname NOT IN ('pg_catalog', 'information_schema', 'pg_toast')
|
|
402
|
+
AND n.nspname NOT LIKE 'pg_temp_%'
|
|
403
|
+
AND n.nspname NOT LIKE 'pg_toast_temp_%'
|
|
404
|
+
-- Exclude Supabase internal schemas
|
|
405
|
+
AND n.nspname NOT IN ('auth', 'storage', 'extensions', 'graphql', 'graphql_public', 'pgbouncer', 'realtime', 'supabase_functions', 'supabase_migrations', '_realtime')
|
|
406
|
+
AND has_schema_privilege(n.oid, 'USAGE')
|
|
407
|
+
AND has_table_privilege(c.oid, 'SELECT')
|
|
408
|
+
ORDER BY
|
|
409
|
+
n.nspname,
|
|
410
|
+
c.relname
|
|
411
|
+
`;
|
|
412
|
+
const result = await executeSqlWithFallback(client, listTablesSql, true);
|
|
413
|
+
return handleSqlResponse(result, ListTablesOutputSchema);
|
|
414
|
+
}
|
|
415
|
+
};
|
|
416
|
+
|
|
417
|
+
// src/tools/list_extensions.ts
|
|
418
|
+
import { z as z3 } from "zod";
|
|
419
|
+
var ListExtensionsOutputSchema = z3.array(z3.object({
|
|
420
|
+
name: z3.string(),
|
|
421
|
+
schema: z3.string(),
|
|
422
|
+
version: z3.string(),
|
|
423
|
+
description: z3.string().nullable().optional()
|
|
424
|
+
}));
|
|
425
|
+
var ListExtensionsInputSchema = z3.object({});
|
|
426
|
+
var mcpInputSchema2 = {
|
|
427
|
+
type: "object",
|
|
428
|
+
properties: {},
|
|
429
|
+
required: []
|
|
430
|
+
};
|
|
431
|
+
var listExtensionsTool = {
|
|
432
|
+
name: "list_extensions",
|
|
433
|
+
description: "Lists all installed PostgreSQL extensions in the database.",
|
|
434
|
+
inputSchema: ListExtensionsInputSchema,
|
|
435
|
+
mcpInputSchema: mcpInputSchema2,
|
|
436
|
+
outputSchema: ListExtensionsOutputSchema,
|
|
437
|
+
execute: async (input, context) => {
|
|
438
|
+
const client = context.selfhostedClient;
|
|
439
|
+
const listExtensionsSql = `
|
|
440
|
+
SELECT
|
|
441
|
+
pe.extname AS name,
|
|
442
|
+
pn.nspname AS schema,
|
|
443
|
+
pe.extversion AS version,
|
|
444
|
+
pd.description
|
|
445
|
+
FROM
|
|
446
|
+
pg_catalog.pg_extension pe
|
|
447
|
+
LEFT JOIN
|
|
448
|
+
pg_catalog.pg_namespace pn ON pn.oid = pe.extnamespace
|
|
449
|
+
LEFT JOIN
|
|
450
|
+
pg_catalog.pg_description pd ON pd.objoid = pe.oid AND pd.classoid = 'pg_catalog.pg_extension'::regclass
|
|
451
|
+
WHERE
|
|
452
|
+
pe.extname != 'plpgsql' -- Exclude the default plpgsql extension
|
|
453
|
+
ORDER BY
|
|
454
|
+
pe.extname
|
|
455
|
+
`;
|
|
456
|
+
const result = await executeSqlWithFallback(client, listExtensionsSql, true);
|
|
457
|
+
return handleSqlResponse(result, ListExtensionsOutputSchema);
|
|
458
|
+
}
|
|
459
|
+
};
|
|
460
|
+
|
|
461
|
+
// src/tools/list_migrations.ts
|
|
462
|
+
import { z as z4 } from "zod";
|
|
463
|
+
var ListMigrationsOutputSchema = z4.array(z4.object({
|
|
464
|
+
version: z4.string(),
|
|
465
|
+
name: z4.string(),
|
|
466
|
+
inserted_at: z4.string()
|
|
467
|
+
// Keep as string from DB
|
|
468
|
+
}));
|
|
469
|
+
var ListMigrationsInputSchema = z4.object({});
|
|
470
|
+
var mcpInputSchema3 = {
|
|
471
|
+
type: "object",
|
|
472
|
+
properties: {},
|
|
473
|
+
required: []
|
|
474
|
+
};
|
|
475
|
+
var listMigrationsTool = {
|
|
476
|
+
name: "list_migrations",
|
|
477
|
+
description: "Lists applied database migrations recorded in supabase_migrations.schema_migrations table.",
|
|
478
|
+
inputSchema: ListMigrationsInputSchema,
|
|
479
|
+
mcpInputSchema: mcpInputSchema3,
|
|
480
|
+
outputSchema: ListMigrationsOutputSchema,
|
|
481
|
+
execute: async (input, context) => {
|
|
482
|
+
const client = context.selfhostedClient;
|
|
483
|
+
const listMigrationsSql = `
|
|
484
|
+
SELECT
|
|
485
|
+
version,
|
|
486
|
+
name,
|
|
487
|
+
inserted_at
|
|
488
|
+
FROM
|
|
489
|
+
supabase_migrations.schema_migrations
|
|
490
|
+
ORDER BY
|
|
491
|
+
version
|
|
492
|
+
`;
|
|
493
|
+
const result = await executeSqlWithFallback(client, listMigrationsSql, true);
|
|
494
|
+
return handleSqlResponse(result, ListMigrationsOutputSchema);
|
|
495
|
+
}
|
|
496
|
+
};
|
|
497
|
+
|
|
498
|
+
// src/tools/apply_migration.ts
|
|
499
|
+
import { z as z5 } from "zod";
|
|
500
|
+
var ApplyMigrationInputSchema = z5.object({
|
|
501
|
+
version: z5.string().describe("The migration version string (e.g., '20240101120000')."),
|
|
502
|
+
name: z5.string().optional().describe("An optional descriptive name for the migration."),
|
|
503
|
+
sql: z5.string().describe("The SQL DDL content of the migration.")
|
|
504
|
+
});
|
|
505
|
+
var ApplyMigrationOutputSchema = z5.object({
|
|
506
|
+
success: z5.boolean(),
|
|
507
|
+
version: z5.string(),
|
|
508
|
+
message: z5.string().optional()
|
|
509
|
+
});
|
|
510
|
+
var mcpInputSchema4 = {
|
|
511
|
+
type: "object",
|
|
512
|
+
properties: {
|
|
513
|
+
version: { type: "string", description: "The migration version string (e.g., '20240101120000')." },
|
|
514
|
+
name: { type: "string", description: "An optional descriptive name for the migration." },
|
|
515
|
+
sql: { type: "string", description: "The SQL DDL content of the migration." }
|
|
516
|
+
},
|
|
517
|
+
required: ["version", "sql"]
|
|
518
|
+
};
|
|
519
|
+
var applyMigrationTool = {
|
|
520
|
+
name: "apply_migration",
|
|
521
|
+
description: "Applies a SQL migration script and records it in the supabase_migrations.schema_migrations table within a transaction.",
|
|
522
|
+
inputSchema: ApplyMigrationInputSchema,
|
|
523
|
+
mcpInputSchema: mcpInputSchema4,
|
|
524
|
+
outputSchema: ApplyMigrationOutputSchema,
|
|
525
|
+
execute: async (input, context) => {
|
|
526
|
+
const client = context.selfhostedClient;
|
|
527
|
+
try {
|
|
528
|
+
if (!client.isPgAvailable()) {
|
|
529
|
+
throw new Error("Direct database connection (DATABASE_URL) is required for applying migrations but is not configured or available.");
|
|
530
|
+
}
|
|
531
|
+
await client.executeTransactionWithPg(async (pgClient) => {
|
|
532
|
+
console.error(`Executing migration SQL for version ${input.version}...`);
|
|
533
|
+
await pgClient.query(input.sql);
|
|
534
|
+
console.error("Migration SQL executed successfully.");
|
|
535
|
+
console.error(`Recording migration version ${input.version} in schema_migrations...`);
|
|
536
|
+
await pgClient.query(
|
|
537
|
+
"INSERT INTO supabase_migrations.schema_migrations (version, name) VALUES ($1, $2);",
|
|
538
|
+
[input.version, input.name ?? ""]
|
|
539
|
+
);
|
|
540
|
+
console.error(`Migration version ${input.version} recorded.`);
|
|
541
|
+
});
|
|
542
|
+
return {
|
|
543
|
+
success: true,
|
|
544
|
+
version: input.version,
|
|
545
|
+
message: `Migration ${input.version} applied successfully.`
|
|
546
|
+
};
|
|
547
|
+
} catch (error) {
|
|
548
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
549
|
+
console.error(`Failed to apply migration ${input.version}:`, errorMessage);
|
|
550
|
+
throw new Error(`Failed to apply migration ${input.version}: ${errorMessage}`);
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
};
|
|
554
|
+
|
|
555
|
+
// src/tools/execute_sql.ts
|
|
556
|
+
import { z as z6 } from "zod";
|
|
557
|
+
var ExecuteSqlInputSchema = z6.object({
|
|
558
|
+
sql: z6.string().describe("The SQL query to execute."),
|
|
559
|
+
read_only: z6.boolean().optional().default(false).describe("Hint for the RPC function whether the query is read-only (best effort).")
|
|
560
|
+
// Future enhancement: Add option to force direct connection?
|
|
561
|
+
// use_direct_connection: z.boolean().optional().default(false).describe('Attempt to use direct DB connection instead of RPC.'),
|
|
562
|
+
});
|
|
563
|
+
var ExecuteSqlOutputSchema = z6.array(z6.unknown()).describe("The array of rows returned by the SQL query.");
|
|
564
|
+
var mcpInputSchema5 = {
|
|
565
|
+
type: "object",
|
|
566
|
+
properties: {
|
|
567
|
+
sql: { type: "string", description: "The SQL query to execute." },
|
|
568
|
+
read_only: { type: "boolean", default: false, description: "Hint for the RPC function whether the query is read-only (best effort)." }
|
|
569
|
+
},
|
|
570
|
+
required: ["sql"]
|
|
571
|
+
};
|
|
572
|
+
var executeSqlTool = {
|
|
573
|
+
name: "execute_sql",
|
|
574
|
+
description: "Executes an arbitrary SQL query against the database, using direct database connection when available or RPC function as fallback.",
|
|
575
|
+
inputSchema: ExecuteSqlInputSchema,
|
|
576
|
+
mcpInputSchema: mcpInputSchema5,
|
|
577
|
+
outputSchema: ExecuteSqlOutputSchema,
|
|
578
|
+
execute: async (input, context) => {
|
|
579
|
+
const client = context.selfhostedClient;
|
|
580
|
+
console.error(`Executing SQL (readOnly: ${input.read_only}): ${input.sql.substring(0, 100)}...`);
|
|
581
|
+
const result = await executeSqlWithFallback(client, input.sql, input.read_only);
|
|
582
|
+
return handleSqlResponse(result, ExecuteSqlOutputSchema);
|
|
583
|
+
}
|
|
584
|
+
};
|
|
585
|
+
|
|
586
|
+
// src/tools/get_database_connections.ts
|
|
587
|
+
import { z as z7 } from "zod";
|
|
588
|
+
var GetDbConnectionsOutputSchema = z7.array(z7.object({
|
|
589
|
+
datname: z7.string().nullable().describe("Database name"),
|
|
590
|
+
usename: z7.string().nullable().describe("User name"),
|
|
591
|
+
application_name: z7.string().nullable().describe("Application name (e.g., PostgREST, psql)"),
|
|
592
|
+
client_addr: z7.string().nullable().describe("Client IP address"),
|
|
593
|
+
backend_start: z7.string().nullable().describe("Time when the backend process started"),
|
|
594
|
+
state: z7.string().nullable().describe("Current connection state (e.g., active, idle)"),
|
|
595
|
+
query: z7.string().nullable().describe("Last or current query being executed"),
|
|
596
|
+
pid: z7.number().describe("Process ID of the backend")
|
|
597
|
+
}));
|
|
598
|
+
var GetDbConnectionsInputSchema = z7.object({});
|
|
599
|
+
var mcpInputSchema6 = {
|
|
600
|
+
type: "object",
|
|
601
|
+
properties: {},
|
|
602
|
+
required: []
|
|
603
|
+
};
|
|
604
|
+
var getDatabaseConnectionsTool = {
|
|
605
|
+
name: "get_database_connections",
|
|
606
|
+
description: "Retrieves information about active database connections from pg_stat_activity.",
|
|
607
|
+
inputSchema: GetDbConnectionsInputSchema,
|
|
608
|
+
mcpInputSchema: mcpInputSchema6,
|
|
609
|
+
outputSchema: GetDbConnectionsOutputSchema,
|
|
610
|
+
execute: async (input, context) => {
|
|
611
|
+
const client = context.selfhostedClient;
|
|
612
|
+
const getConnectionsSql = `
|
|
613
|
+
SELECT
|
|
614
|
+
pid,
|
|
615
|
+
datname,
|
|
616
|
+
usename,
|
|
617
|
+
application_name,
|
|
618
|
+
client_addr::text, -- Cast inet to text
|
|
619
|
+
backend_start::text, -- Cast timestamp to text
|
|
620
|
+
state,
|
|
621
|
+
query
|
|
622
|
+
FROM
|
|
623
|
+
pg_stat_activity
|
|
624
|
+
WHERE
|
|
625
|
+
backend_type = 'client backend' -- Exclude background workers, etc.
|
|
626
|
+
-- Optionally filter out self?
|
|
627
|
+
-- AND pid != pg_backend_pid()
|
|
628
|
+
ORDER BY
|
|
629
|
+
backend_start
|
|
630
|
+
`;
|
|
631
|
+
const result = await executeSqlWithFallback(client, getConnectionsSql, true);
|
|
632
|
+
return handleSqlResponse(result, GetDbConnectionsOutputSchema);
|
|
633
|
+
}
|
|
634
|
+
};
|
|
635
|
+
|
|
636
|
+
// src/tools/get_database_stats.ts
|
|
637
|
+
import { z as z8 } from "zod";
|
|
638
|
+
var GetDbStatsOutputSchema = z8.object({
|
|
639
|
+
database_stats: z8.array(z8.object({
|
|
640
|
+
datname: z8.string().nullable(),
|
|
641
|
+
numbackends: z8.number().nullable(),
|
|
642
|
+
xact_commit: z8.string().nullable(),
|
|
643
|
+
// bigint as string
|
|
644
|
+
xact_rollback: z8.string().nullable(),
|
|
645
|
+
// bigint as string
|
|
646
|
+
blks_read: z8.string().nullable(),
|
|
647
|
+
// bigint as string
|
|
648
|
+
blks_hit: z8.string().nullable(),
|
|
649
|
+
// bigint as string
|
|
650
|
+
tup_returned: z8.string().nullable(),
|
|
651
|
+
// bigint as string
|
|
652
|
+
tup_fetched: z8.string().nullable(),
|
|
653
|
+
// bigint as string
|
|
654
|
+
tup_inserted: z8.string().nullable(),
|
|
655
|
+
// bigint as string
|
|
656
|
+
tup_updated: z8.string().nullable(),
|
|
657
|
+
// bigint as string
|
|
658
|
+
tup_deleted: z8.string().nullable(),
|
|
659
|
+
// bigint as string
|
|
660
|
+
conflicts: z8.string().nullable(),
|
|
661
|
+
// bigint as string
|
|
662
|
+
temp_files: z8.string().nullable(),
|
|
663
|
+
// bigint as string
|
|
664
|
+
temp_bytes: z8.string().nullable(),
|
|
665
|
+
// bigint as string
|
|
666
|
+
deadlocks: z8.string().nullable(),
|
|
667
|
+
// bigint as string
|
|
668
|
+
checksum_failures: z8.string().nullable(),
|
|
669
|
+
// bigint as string
|
|
670
|
+
checksum_last_failure: z8.string().nullable(),
|
|
671
|
+
// timestamp as string
|
|
672
|
+
blk_read_time: z8.number().nullable(),
|
|
673
|
+
// double precision
|
|
674
|
+
blk_write_time: z8.number().nullable(),
|
|
675
|
+
// double precision
|
|
676
|
+
stats_reset: z8.string().nullable()
|
|
677
|
+
// timestamp as string
|
|
678
|
+
})).describe("Statistics per database from pg_stat_database"),
|
|
679
|
+
bgwriter_stats: z8.array(z8.object({
|
|
680
|
+
// Usually a single row
|
|
681
|
+
checkpoints_timed: z8.string().nullable(),
|
|
682
|
+
checkpoints_req: z8.string().nullable(),
|
|
683
|
+
checkpoint_write_time: z8.number().nullable(),
|
|
684
|
+
checkpoint_sync_time: z8.number().nullable(),
|
|
685
|
+
buffers_checkpoint: z8.string().nullable(),
|
|
686
|
+
buffers_clean: z8.string().nullable(),
|
|
687
|
+
maxwritten_clean: z8.string().nullable(),
|
|
688
|
+
buffers_backend: z8.string().nullable(),
|
|
689
|
+
buffers_backend_fsync: z8.string().nullable(),
|
|
690
|
+
buffers_alloc: z8.string().nullable(),
|
|
691
|
+
stats_reset: z8.string().nullable()
|
|
692
|
+
})).describe("Statistics from the background writer process from pg_stat_bgwriter")
|
|
693
|
+
});
|
|
694
|
+
var GetDbStatsInputSchema = z8.object({});
|
|
695
|
+
var mcpInputSchema7 = {
|
|
696
|
+
type: "object",
|
|
697
|
+
properties: {},
|
|
698
|
+
required: []
|
|
699
|
+
};
|
|
700
|
+
var getDatabaseStatsTool = {
|
|
701
|
+
name: "get_database_stats",
|
|
702
|
+
description: "Retrieves statistics about database activity and the background writer from pg_stat_database and pg_stat_bgwriter.",
|
|
703
|
+
inputSchema: GetDbStatsInputSchema,
|
|
704
|
+
mcpInputSchema: mcpInputSchema7,
|
|
705
|
+
outputSchema: GetDbStatsOutputSchema,
|
|
706
|
+
execute: async (input, context) => {
|
|
707
|
+
const client = context.selfhostedClient;
|
|
708
|
+
const getDbStatsSql = `
|
|
709
|
+
SELECT
|
|
710
|
+
datname,
|
|
711
|
+
numbackends,
|
|
712
|
+
xact_commit::text,
|
|
713
|
+
xact_rollback::text,
|
|
714
|
+
blks_read::text,
|
|
715
|
+
blks_hit::text,
|
|
716
|
+
tup_returned::text,
|
|
717
|
+
tup_fetched::text,
|
|
718
|
+
tup_inserted::text,
|
|
719
|
+
tup_updated::text,
|
|
720
|
+
tup_deleted::text,
|
|
721
|
+
conflicts::text,
|
|
722
|
+
temp_files::text,
|
|
723
|
+
temp_bytes::text,
|
|
724
|
+
deadlocks::text,
|
|
725
|
+
checksum_failures::text,
|
|
726
|
+
checksum_last_failure::text,
|
|
727
|
+
blk_read_time,
|
|
728
|
+
blk_write_time,
|
|
729
|
+
stats_reset::text
|
|
730
|
+
FROM pg_stat_database
|
|
731
|
+
`;
|
|
732
|
+
const getBgWriterStatsSql = `
|
|
733
|
+
SELECT
|
|
734
|
+
checkpoints_timed::text,
|
|
735
|
+
checkpoints_req::text,
|
|
736
|
+
checkpoint_write_time,
|
|
737
|
+
checkpoint_sync_time,
|
|
738
|
+
buffers_checkpoint::text,
|
|
739
|
+
buffers_clean::text,
|
|
740
|
+
maxwritten_clean::text,
|
|
741
|
+
buffers_backend::text,
|
|
742
|
+
buffers_backend_fsync::text,
|
|
743
|
+
buffers_alloc::text,
|
|
744
|
+
stats_reset::text
|
|
745
|
+
FROM pg_stat_bgwriter
|
|
746
|
+
`;
|
|
747
|
+
const [dbStatsResult, bgWriterStatsResult] = await Promise.all([
|
|
748
|
+
executeSqlWithFallback(client, getDbStatsSql, true),
|
|
749
|
+
executeSqlWithFallback(client, getBgWriterStatsSql, true)
|
|
750
|
+
]);
|
|
751
|
+
const dbStats = handleSqlResponse(dbStatsResult, GetDbStatsOutputSchema.shape.database_stats);
|
|
752
|
+
const bgWriterStats = handleSqlResponse(bgWriterStatsResult, GetDbStatsOutputSchema.shape.bgwriter_stats);
|
|
753
|
+
return {
|
|
754
|
+
database_stats: dbStats,
|
|
755
|
+
bgwriter_stats: bgWriterStats
|
|
756
|
+
};
|
|
757
|
+
}
|
|
758
|
+
};
|
|
759
|
+
|
|
760
|
+
// src/tools/get_project_url.ts
|
|
761
|
+
import { z as z9 } from "zod";
|
|
762
|
+
var GetProjectUrlInputSchema = z9.object({});
|
|
763
|
+
var GetProjectUrlOutputSchema = z9.object({
|
|
764
|
+
project_url: z9.string().url()
|
|
765
|
+
});
|
|
766
|
+
var mcpInputSchema8 = {
|
|
767
|
+
type: "object",
|
|
768
|
+
properties: {},
|
|
769
|
+
required: []
|
|
770
|
+
};
|
|
771
|
+
var getProjectUrlTool = {
|
|
772
|
+
name: "get_project_url",
|
|
773
|
+
description: "Returns the configured Supabase project URL for this server.",
|
|
774
|
+
inputSchema: GetProjectUrlInputSchema,
|
|
775
|
+
mcpInputSchema: mcpInputSchema8,
|
|
776
|
+
// Add static JSON schema
|
|
777
|
+
outputSchema: GetProjectUrlOutputSchema,
|
|
778
|
+
execute: async (input, context) => {
|
|
779
|
+
const client = context.selfhostedClient;
|
|
780
|
+
const url = client.getSupabaseUrl();
|
|
781
|
+
return { project_url: url };
|
|
782
|
+
}
|
|
783
|
+
};
|
|
784
|
+
|
|
785
|
+
// src/tools/get_anon_key.ts
|
|
786
|
+
import { z as z10 } from "zod";
|
|
787
|
+
var GetAnonKeyInputSchema = z10.object({});
|
|
788
|
+
var GetAnonKeyOutputSchema = z10.object({
|
|
789
|
+
anon_key: z10.string()
|
|
790
|
+
});
|
|
791
|
+
var mcpInputSchema9 = {
|
|
792
|
+
type: "object",
|
|
793
|
+
properties: {},
|
|
794
|
+
required: []
|
|
795
|
+
};
|
|
796
|
+
var getAnonKeyTool = {
|
|
797
|
+
name: "get_anon_key",
|
|
798
|
+
description: "Returns the configured Supabase anon key for this server.",
|
|
799
|
+
inputSchema: GetAnonKeyInputSchema,
|
|
800
|
+
mcpInputSchema: mcpInputSchema9,
|
|
801
|
+
outputSchema: GetAnonKeyOutputSchema,
|
|
802
|
+
execute: async (input, context) => {
|
|
803
|
+
const client = context.selfhostedClient;
|
|
804
|
+
const key = client.getAnonKey();
|
|
805
|
+
return { anon_key: key };
|
|
806
|
+
}
|
|
807
|
+
};
|
|
808
|
+
|
|
809
|
+
// src/tools/get_service_key.ts
|
|
810
|
+
import { z as z11 } from "zod";
|
|
811
|
+
var GetServiceKeyInputSchema = z11.object({});
|
|
812
|
+
var GetServiceKeyOutputSchema = z11.object({
|
|
813
|
+
service_key_status: z11.enum(["found", "not_configured"]).describe("Whether the service key was provided to the server."),
|
|
814
|
+
service_key: z11.string().optional().describe("The configured Supabase service role key (if configured).")
|
|
815
|
+
});
|
|
816
|
+
var mcpInputSchema10 = {
|
|
817
|
+
type: "object",
|
|
818
|
+
properties: {},
|
|
819
|
+
required: []
|
|
820
|
+
};
|
|
821
|
+
var getServiceKeyTool = {
|
|
822
|
+
name: "get_service_key",
|
|
823
|
+
description: "Returns the configured Supabase service role key for this server, if available.",
|
|
824
|
+
inputSchema: GetServiceKeyInputSchema,
|
|
825
|
+
mcpInputSchema: mcpInputSchema10,
|
|
826
|
+
outputSchema: GetServiceKeyOutputSchema,
|
|
827
|
+
execute: async (input, context) => {
|
|
828
|
+
const client = context.selfhostedClient;
|
|
829
|
+
const key = client.getServiceRoleKey();
|
|
830
|
+
if (key) {
|
|
831
|
+
return { service_key_status: "found", service_key: key };
|
|
832
|
+
}
|
|
833
|
+
return { service_key_status: "not_configured" };
|
|
834
|
+
}
|
|
835
|
+
};
|
|
836
|
+
|
|
837
|
+
// src/tools/generate_typescript_types.ts
|
|
838
|
+
import { z as z12 } from "zod";
|
|
839
|
+
import { writeFileSync } from "fs";
|
|
840
|
+
import { resolve, dirname } from "path";
|
|
841
|
+
import { mkdirSync } from "fs";
|
|
842
|
+
function normalizeOutputPath(inputPath) {
|
|
843
|
+
if (process.platform === "win32" && inputPath.match(/^\/[a-zA-Z]:/)) {
|
|
844
|
+
inputPath = inputPath.substring(1);
|
|
845
|
+
inputPath = inputPath.charAt(0).toUpperCase() + inputPath.slice(1);
|
|
846
|
+
}
|
|
847
|
+
return resolve(inputPath);
|
|
848
|
+
}
|
|
849
|
+
var GenerateTypesInputSchema = z12.object({
|
|
850
|
+
included_schemas: z12.array(z12.string()).optional().default(["public"]).describe("Database schemas to include in type generation."),
|
|
851
|
+
output_filename: z12.string().optional().default("database.types.ts").describe("Filename to save the generated types to in the workspace root."),
|
|
852
|
+
output_path: z12.string().describe("Absolute path where to save the file. If provided, output_filename will be ignored.")
|
|
853
|
+
});
|
|
854
|
+
var GenerateTypesOutputSchema = z12.object({
|
|
855
|
+
success: z12.boolean(),
|
|
856
|
+
message: z12.string().describe("Output message from the generation process."),
|
|
857
|
+
types: z12.string().optional().describe("The generated TypeScript types, if successful."),
|
|
858
|
+
file_path: z12.string().optional().describe("The absolute path to the saved types file, if successful."),
|
|
859
|
+
platform: z12.string().describe("Operating system platform (win32, darwin, linux).")
|
|
860
|
+
});
|
|
861
|
+
var mcpInputSchema11 = {
|
|
862
|
+
type: "object",
|
|
863
|
+
properties: {
|
|
864
|
+
included_schemas: {
|
|
865
|
+
type: "array",
|
|
866
|
+
items: { type: "string" },
|
|
867
|
+
default: ["public"],
|
|
868
|
+
description: "Database schemas to include in type generation."
|
|
869
|
+
},
|
|
870
|
+
output_filename: {
|
|
871
|
+
type: "string",
|
|
872
|
+
default: "database.types.ts",
|
|
873
|
+
description: "Filename to save the generated types to in the workspace root."
|
|
874
|
+
},
|
|
875
|
+
output_path: {
|
|
876
|
+
type: "string",
|
|
877
|
+
description: 'Absolute path where to download the generated TypeScript file. Examples: Windows: "C:\\\\path\\\\to\\\\project\\\\database.types.ts", macOS/Linux: "/path/to/project/database.types.ts". This parameter is required.'
|
|
878
|
+
}
|
|
879
|
+
},
|
|
880
|
+
required: ["output_path"]
|
|
881
|
+
// output_path is required for file download
|
|
882
|
+
};
|
|
883
|
+
var generateTypesTool = {
|
|
884
|
+
name: "generate_typescript_types",
|
|
885
|
+
description: "Generates TypeScript types from the database schema using the Supabase CLI (`supabase gen types`) and downloads the file to the specified absolute path. The tool returns the current platform (win32, darwin, linux) to help with path formatting. Requires DATABASE_URL configuration and Supabase CLI installed.",
|
|
886
|
+
inputSchema: GenerateTypesInputSchema,
|
|
887
|
+
mcpInputSchema: mcpInputSchema11,
|
|
888
|
+
// Add static JSON schema
|
|
889
|
+
outputSchema: GenerateTypesOutputSchema,
|
|
890
|
+
execute: async (input, context) => {
|
|
891
|
+
const client = context.selfhostedClient;
|
|
892
|
+
const dbUrl = client.getDbUrl();
|
|
893
|
+
if (!dbUrl) {
|
|
894
|
+
return {
|
|
895
|
+
success: false,
|
|
896
|
+
message: "Error: DATABASE_URL is not configured. Cannot generate types.",
|
|
897
|
+
platform: process.platform
|
|
898
|
+
};
|
|
899
|
+
}
|
|
900
|
+
const schemas = input.included_schemas.join(",");
|
|
901
|
+
const command = `supabase gen types typescript --db-url "${dbUrl}" --schema "${schemas}"`;
|
|
902
|
+
console.error(`Running command: ${command}`);
|
|
903
|
+
try {
|
|
904
|
+
const { stdout, stderr, error } = await runExternalCommand(command);
|
|
905
|
+
if (error) {
|
|
906
|
+
console.error(`Error executing supabase gen types: ${stderr || error.message}`);
|
|
907
|
+
return {
|
|
908
|
+
success: false,
|
|
909
|
+
message: `Command failed: ${stderr || error.message}`,
|
|
910
|
+
platform: process.platform
|
|
911
|
+
};
|
|
912
|
+
}
|
|
913
|
+
if (stderr) {
|
|
914
|
+
console.error(`supabase gen types produced stderr output: ${stderr}`);
|
|
915
|
+
}
|
|
916
|
+
let outputPath;
|
|
917
|
+
try {
|
|
918
|
+
outputPath = normalizeOutputPath(input.output_path);
|
|
919
|
+
console.error(`Normalized output path: ${outputPath}`);
|
|
920
|
+
} catch (pathError) {
|
|
921
|
+
const pathErrorMessage = pathError instanceof Error ? pathError.message : String(pathError);
|
|
922
|
+
console.error(`Invalid output path: ${pathErrorMessage}`);
|
|
923
|
+
return {
|
|
924
|
+
success: false,
|
|
925
|
+
message: `Invalid output path "${input.output_path}": ${pathErrorMessage}`,
|
|
926
|
+
platform: process.platform
|
|
927
|
+
};
|
|
928
|
+
}
|
|
929
|
+
try {
|
|
930
|
+
const outputDir = dirname(outputPath);
|
|
931
|
+
try {
|
|
932
|
+
mkdirSync(outputDir, { recursive: true });
|
|
933
|
+
} catch (dirError) {
|
|
934
|
+
if (dirError.code !== "EEXIST") {
|
|
935
|
+
throw dirError;
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
writeFileSync(outputPath, stdout, "utf8");
|
|
939
|
+
console.error(`Types saved to: ${outputPath}`);
|
|
940
|
+
} catch (writeError) {
|
|
941
|
+
const writeErrorMessage = writeError instanceof Error ? writeError.message : String(writeError);
|
|
942
|
+
console.error(`Failed to write types file: ${writeErrorMessage}`);
|
|
943
|
+
return {
|
|
944
|
+
success: false,
|
|
945
|
+
message: `Type generation succeeded but failed to save file: ${writeErrorMessage}. Platform: ${process.platform}. Attempted path: ${outputPath}`,
|
|
946
|
+
types: stdout,
|
|
947
|
+
platform: process.platform
|
|
948
|
+
};
|
|
949
|
+
}
|
|
950
|
+
console.error("Type generation and file save successful.");
|
|
951
|
+
return {
|
|
952
|
+
success: true,
|
|
953
|
+
message: `Types generated successfully and saved to ${outputPath}.${stderr ? `
|
|
954
|
+
Warnings:
|
|
955
|
+
${stderr}` : ""}`,
|
|
956
|
+
types: stdout,
|
|
957
|
+
file_path: outputPath,
|
|
958
|
+
platform: process.platform
|
|
959
|
+
};
|
|
960
|
+
} catch (err) {
|
|
961
|
+
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
962
|
+
console.error(`Exception during type generation: ${errorMessage}`);
|
|
963
|
+
return {
|
|
964
|
+
success: false,
|
|
965
|
+
message: `Exception during type generation: ${errorMessage}. Platform: ${process.platform}`,
|
|
966
|
+
platform: process.platform
|
|
967
|
+
};
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
};
|
|
971
|
+
|
|
972
|
+
// src/tools/rebuild_hooks.ts
|
|
973
|
+
import { z as z13 } from "zod";
|
|
974
|
+
var RebuildHooksInputSchema = z13.object({});
|
|
975
|
+
var RebuildHooksOutputSchema = z13.object({
|
|
976
|
+
success: z13.boolean(),
|
|
977
|
+
message: z13.string()
|
|
978
|
+
});
|
|
979
|
+
var mcpInputSchema12 = {
|
|
980
|
+
type: "object",
|
|
981
|
+
properties: {},
|
|
982
|
+
required: []
|
|
983
|
+
};
|
|
984
|
+
var rebuildHooksTool = {
|
|
985
|
+
name: "rebuild_hooks",
|
|
986
|
+
description: "Attempts to restart the pg_net worker. Requires the pg_net extension to be installed and available.",
|
|
987
|
+
inputSchema: RebuildHooksInputSchema,
|
|
988
|
+
mcpInputSchema: mcpInputSchema12,
|
|
989
|
+
outputSchema: RebuildHooksOutputSchema,
|
|
990
|
+
execute: async (input, context) => {
|
|
991
|
+
const client = context.selfhostedClient;
|
|
992
|
+
const restartSql = "SELECT net.worker_restart()";
|
|
993
|
+
try {
|
|
994
|
+
console.error("Attempting to restart pg_net worker...");
|
|
995
|
+
const result = await executeSqlWithFallback(client, restartSql, false);
|
|
996
|
+
if ("error" in result) {
|
|
997
|
+
const notFound = result.error.code === "42883";
|
|
998
|
+
const message = `Failed to restart pg_net worker: ${result.error.message}${notFound ? " (Is pg_net installed and enabled?)" : ""}`;
|
|
999
|
+
console.error(message);
|
|
1000
|
+
return { success: false, message };
|
|
1001
|
+
}
|
|
1002
|
+
console.error("pg_net worker restart requested successfully.");
|
|
1003
|
+
return { success: true, message: "pg_net worker restart requested successfully." };
|
|
1004
|
+
} catch (error) {
|
|
1005
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
1006
|
+
console.error(`Exception attempting to restart pg_net worker: ${errorMessage}`);
|
|
1007
|
+
return { success: false, message: `Exception attempting to restart pg_net worker: ${errorMessage}` };
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
};
|
|
1011
|
+
|
|
1012
|
+
// src/tools/verify_jwt_secret.ts
|
|
1013
|
+
import { z as z14 } from "zod";
|
|
1014
|
+
var VerifyJwtInputSchema = z14.object({});
|
|
1015
|
+
var VerifyJwtOutputSchema = z14.object({
|
|
1016
|
+
jwt_secret_status: z14.enum(["found", "not_configured"]).describe("Whether the JWT secret was provided to the server."),
|
|
1017
|
+
jwt_secret_preview: z14.string().optional().describe("A preview of the JWT secret (first few characters) if configured.")
|
|
1018
|
+
});
|
|
1019
|
+
var mcpInputSchema13 = {
|
|
1020
|
+
type: "object",
|
|
1021
|
+
properties: {},
|
|
1022
|
+
required: []
|
|
1023
|
+
};
|
|
1024
|
+
var verifyJwtSecretTool = {
|
|
1025
|
+
name: "verify_jwt_secret",
|
|
1026
|
+
description: "Checks if the Supabase JWT secret is configured for this server and returns a preview.",
|
|
1027
|
+
inputSchema: VerifyJwtInputSchema,
|
|
1028
|
+
mcpInputSchema: mcpInputSchema13,
|
|
1029
|
+
outputSchema: VerifyJwtOutputSchema,
|
|
1030
|
+
execute: async (input, context) => {
|
|
1031
|
+
const client = context.selfhostedClient;
|
|
1032
|
+
const secret = client.getJwtSecret();
|
|
1033
|
+
if (secret) {
|
|
1034
|
+
const preview = `${secret.substring(0, Math.min(secret.length, 5))}...`;
|
|
1035
|
+
return {
|
|
1036
|
+
jwt_secret_status: "found",
|
|
1037
|
+
jwt_secret_preview: preview
|
|
1038
|
+
};
|
|
1039
|
+
}
|
|
1040
|
+
return { jwt_secret_status: "not_configured" };
|
|
1041
|
+
}
|
|
1042
|
+
};
|
|
1043
|
+
|
|
1044
|
+
// src/tools/list_auth_users.ts
|
|
1045
|
+
import { z as z15 } from "zod";
|
|
1046
|
+
var ListAuthUsersInputSchema = z15.object({
|
|
1047
|
+
limit: z15.number().int().positive().optional().default(50).describe("Max number of users to return"),
|
|
1048
|
+
offset: z15.number().int().nonnegative().optional().default(0).describe("Number of users to skip")
|
|
1049
|
+
// Add filters later (e.g., by email pattern, role)
|
|
1050
|
+
});
|
|
1051
|
+
var AuthUserZodSchema = z15.object({
|
|
1052
|
+
id: z15.string().uuid(),
|
|
1053
|
+
email: z15.string().email().nullable(),
|
|
1054
|
+
role: z15.string().nullable(),
|
|
1055
|
+
// Timestamps returned as text from DB might not strictly be ISO 8601 / Zod datetime compliant
|
|
1056
|
+
created_at: z15.string().nullable(),
|
|
1057
|
+
last_sign_in_at: z15.string().nullable(),
|
|
1058
|
+
raw_app_meta_data: z15.record(z15.unknown()).nullable(),
|
|
1059
|
+
raw_user_meta_data: z15.record(z15.unknown()).nullable()
|
|
1060
|
+
// Add more fields as needed (e.g., email_confirmed_at, phone)
|
|
1061
|
+
});
|
|
1062
|
+
var ListAuthUsersOutputSchema = z15.array(AuthUserZodSchema);
|
|
1063
|
+
var mcpInputSchema14 = {
|
|
1064
|
+
type: "object",
|
|
1065
|
+
properties: {
|
|
1066
|
+
limit: {
|
|
1067
|
+
type: "number",
|
|
1068
|
+
description: "Max number of users to return",
|
|
1069
|
+
default: 50
|
|
1070
|
+
},
|
|
1071
|
+
offset: {
|
|
1072
|
+
type: "number",
|
|
1073
|
+
description: "Number of users to skip",
|
|
1074
|
+
default: 0
|
|
1075
|
+
}
|
|
1076
|
+
},
|
|
1077
|
+
required: []
|
|
1078
|
+
};
|
|
1079
|
+
var listAuthUsersTool = {
|
|
1080
|
+
name: "list_auth_users",
|
|
1081
|
+
description: "Lists users from the auth.users table.",
|
|
1082
|
+
inputSchema: ListAuthUsersInputSchema,
|
|
1083
|
+
mcpInputSchema: mcpInputSchema14,
|
|
1084
|
+
outputSchema: ListAuthUsersOutputSchema,
|
|
1085
|
+
execute: async (input, context) => {
|
|
1086
|
+
const client = context.selfhostedClient;
|
|
1087
|
+
const { limit, offset } = input;
|
|
1088
|
+
if (!client.isPgAvailable()) {
|
|
1089
|
+
context.log("Direct database connection (DATABASE_URL) is required to list auth users.", "error");
|
|
1090
|
+
throw new Error("Direct database connection (DATABASE_URL) is required to list auth users.");
|
|
1091
|
+
}
|
|
1092
|
+
const listUsersSql = `
|
|
1093
|
+
SELECT
|
|
1094
|
+
id,
|
|
1095
|
+
email,
|
|
1096
|
+
role,
|
|
1097
|
+
raw_app_meta_data,
|
|
1098
|
+
raw_user_meta_data,
|
|
1099
|
+
created_at::text, -- Cast timestamp to text for JSON
|
|
1100
|
+
last_sign_in_at::text -- Cast timestamp to text for JSON
|
|
1101
|
+
FROM
|
|
1102
|
+
auth.users
|
|
1103
|
+
ORDER BY
|
|
1104
|
+
created_at DESC
|
|
1105
|
+
LIMIT ${limit}
|
|
1106
|
+
OFFSET ${offset}
|
|
1107
|
+
`;
|
|
1108
|
+
console.error("Attempting to list auth users using direct DB connection...");
|
|
1109
|
+
const result = await client.executeSqlWithPg(listUsersSql);
|
|
1110
|
+
const validatedUsers = handleSqlResponse(result, ListAuthUsersOutputSchema);
|
|
1111
|
+
console.error(`Found ${validatedUsers.length} users.`);
|
|
1112
|
+
context.log(`Found ${validatedUsers.length} users.`);
|
|
1113
|
+
return validatedUsers;
|
|
1114
|
+
}
|
|
1115
|
+
};
|
|
1116
|
+
|
|
1117
|
+
// src/tools/get_auth_user.ts
|
|
1118
|
+
import { z as z16 } from "zod";
|
|
1119
|
+
var GetAuthUserInputSchema = z16.object({
|
|
1120
|
+
user_id: z16.string().uuid().describe("The UUID of the user to retrieve.")
|
|
1121
|
+
});
|
|
1122
|
+
var AuthUserZodSchema2 = z16.object({
|
|
1123
|
+
id: z16.string().uuid(),
|
|
1124
|
+
email: z16.string().email().nullable(),
|
|
1125
|
+
role: z16.string().nullable(),
|
|
1126
|
+
created_at: z16.string().nullable(),
|
|
1127
|
+
last_sign_in_at: z16.string().nullable(),
|
|
1128
|
+
raw_app_meta_data: z16.record(z16.unknown()).nullable(),
|
|
1129
|
+
raw_user_meta_data: z16.record(z16.unknown()).nullable()
|
|
1130
|
+
// Add more fields as needed
|
|
1131
|
+
});
|
|
1132
|
+
var mcpInputSchema15 = {
|
|
1133
|
+
type: "object",
|
|
1134
|
+
properties: {
|
|
1135
|
+
user_id: {
|
|
1136
|
+
type: "string",
|
|
1137
|
+
description: "The UUID of the user to retrieve.",
|
|
1138
|
+
format: "uuid"
|
|
1139
|
+
// Hint format if possible
|
|
1140
|
+
}
|
|
1141
|
+
},
|
|
1142
|
+
required: ["user_id"]
|
|
1143
|
+
};
|
|
1144
|
+
var getAuthUserTool = {
|
|
1145
|
+
name: "get_auth_user",
|
|
1146
|
+
description: "Retrieves details for a specific user from auth.users by their ID.",
|
|
1147
|
+
inputSchema: GetAuthUserInputSchema,
|
|
1148
|
+
mcpInputSchema: mcpInputSchema15,
|
|
1149
|
+
outputSchema: AuthUserZodSchema2,
|
|
1150
|
+
// Use the single user Zod schema
|
|
1151
|
+
execute: async (input, context) => {
|
|
1152
|
+
const client = context.selfhostedClient;
|
|
1153
|
+
const { user_id } = input;
|
|
1154
|
+
if (!client.isPgAvailable()) {
|
|
1155
|
+
context.log("Direct database connection (DATABASE_URL) is required to get auth user details.", "error");
|
|
1156
|
+
throw new Error("Direct database connection (DATABASE_URL) is required to get auth user details.");
|
|
1157
|
+
}
|
|
1158
|
+
const sql = `
|
|
1159
|
+
SELECT
|
|
1160
|
+
id,
|
|
1161
|
+
email,
|
|
1162
|
+
role,
|
|
1163
|
+
raw_app_meta_data,
|
|
1164
|
+
raw_user_meta_data,
|
|
1165
|
+
created_at::text,
|
|
1166
|
+
last_sign_in_at::text
|
|
1167
|
+
FROM auth.users
|
|
1168
|
+
WHERE id = $1
|
|
1169
|
+
`;
|
|
1170
|
+
const params = [user_id];
|
|
1171
|
+
console.error(`Attempting to get auth user ${user_id} using direct DB connection...`);
|
|
1172
|
+
const user = await client.executeTransactionWithPg(async (pgClient) => {
|
|
1173
|
+
const result = await pgClient.query(sql, params);
|
|
1174
|
+
if (result.rows.length === 0) {
|
|
1175
|
+
throw new Error(`User with ID ${user_id} not found.`);
|
|
1176
|
+
}
|
|
1177
|
+
try {
|
|
1178
|
+
const singleUser = AuthUserZodSchema2.parse(result.rows[0]);
|
|
1179
|
+
return singleUser;
|
|
1180
|
+
} catch (validationError) {
|
|
1181
|
+
if (validationError instanceof z16.ZodError) {
|
|
1182
|
+
console.error("Zod validation failed:", validationError.errors);
|
|
1183
|
+
throw new Error(`Output validation failed: ${validationError.errors.map((e) => `${e.path.join(".")}: ${e.message}`).join(", ")}`);
|
|
1184
|
+
}
|
|
1185
|
+
throw validationError;
|
|
1186
|
+
}
|
|
1187
|
+
});
|
|
1188
|
+
console.error(`Found user ${user_id}.`);
|
|
1189
|
+
context.log(`Found user ${user_id}.`);
|
|
1190
|
+
return user;
|
|
1191
|
+
}
|
|
1192
|
+
};
|
|
1193
|
+
|
|
1194
|
+
// src/tools/delete_auth_user.ts
|
|
1195
|
+
import { z as z17 } from "zod";
|
|
1196
|
+
var DeleteAuthUserInputSchema = z17.object({
|
|
1197
|
+
user_id: z17.string().uuid().describe("The UUID of the user to delete.")
|
|
1198
|
+
});
|
|
1199
|
+
var DeleteAuthUserOutputSchema = z17.object({
|
|
1200
|
+
success: z17.boolean(),
|
|
1201
|
+
message: z17.string()
|
|
1202
|
+
});
|
|
1203
|
+
var mcpInputSchema16 = {
|
|
1204
|
+
type: "object",
|
|
1205
|
+
properties: {
|
|
1206
|
+
user_id: {
|
|
1207
|
+
type: "string",
|
|
1208
|
+
format: "uuid",
|
|
1209
|
+
description: "The UUID of the user to delete."
|
|
1210
|
+
}
|
|
1211
|
+
},
|
|
1212
|
+
required: ["user_id"]
|
|
1213
|
+
};
|
|
1214
|
+
var deleteAuthUserTool = {
|
|
1215
|
+
name: "delete_auth_user",
|
|
1216
|
+
description: "Deletes a user from auth.users by their ID. Requires service_role key and direct DB connection.",
|
|
1217
|
+
inputSchema: DeleteAuthUserInputSchema,
|
|
1218
|
+
mcpInputSchema: mcpInputSchema16,
|
|
1219
|
+
outputSchema: DeleteAuthUserOutputSchema,
|
|
1220
|
+
execute: async (input, context) => {
|
|
1221
|
+
const client = context.selfhostedClient;
|
|
1222
|
+
const { user_id } = input;
|
|
1223
|
+
if (!client.isPgAvailable()) {
|
|
1224
|
+
throw new Error("Direct database connection (DATABASE_URL) is required for deleting users but is not configured or available.");
|
|
1225
|
+
}
|
|
1226
|
+
try {
|
|
1227
|
+
const result = await client.executeTransactionWithPg(async (pgClient) => {
|
|
1228
|
+
const deleteResult = await pgClient.query(
|
|
1229
|
+
"DELETE FROM auth.users WHERE id = $1",
|
|
1230
|
+
[user_id]
|
|
1231
|
+
);
|
|
1232
|
+
return deleteResult;
|
|
1233
|
+
});
|
|
1234
|
+
if (result.rowCount === 1) {
|
|
1235
|
+
return {
|
|
1236
|
+
success: true,
|
|
1237
|
+
message: `Successfully deleted user with ID: ${user_id}`
|
|
1238
|
+
};
|
|
1239
|
+
}
|
|
1240
|
+
return {
|
|
1241
|
+
success: false,
|
|
1242
|
+
message: `User with ID ${user_id} not found or could not be deleted.`
|
|
1243
|
+
};
|
|
1244
|
+
} catch (error) {
|
|
1245
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
1246
|
+
console.error(`Error deleting user ${user_id}:`, errorMessage);
|
|
1247
|
+
throw new Error(`Failed to delete user ${user_id}: ${errorMessage}`);
|
|
1248
|
+
}
|
|
1249
|
+
}
|
|
1250
|
+
};
|
|
1251
|
+
|
|
1252
|
+
// src/tools/create_auth_user.ts
|
|
1253
|
+
import { z as z18 } from "zod";
|
|
1254
|
+
var CreateAuthUserInputSchema = z18.object({
|
|
1255
|
+
email: z18.string().email().describe("The email address for the new user."),
|
|
1256
|
+
password: z18.string().min(6).describe("Plain text password (min 6 chars). WARNING: Insecure."),
|
|
1257
|
+
role: z18.string().optional().describe("User role."),
|
|
1258
|
+
app_metadata: z18.record(z18.unknown()).optional().describe("Optional app metadata."),
|
|
1259
|
+
user_metadata: z18.record(z18.unknown()).optional().describe("Optional user metadata.")
|
|
1260
|
+
});
|
|
1261
|
+
var CreatedAuthUserZodSchema = z18.object({
|
|
1262
|
+
id: z18.string().uuid(),
|
|
1263
|
+
email: z18.string().email().nullable(),
|
|
1264
|
+
role: z18.string().nullable(),
|
|
1265
|
+
created_at: z18.string().nullable(),
|
|
1266
|
+
last_sign_in_at: z18.string().nullable(),
|
|
1267
|
+
// Will likely be null on creation
|
|
1268
|
+
raw_app_meta_data: z18.record(z18.unknown()).nullable(),
|
|
1269
|
+
raw_user_meta_data: z18.record(z18.unknown()).nullable()
|
|
1270
|
+
// Add other fields returned by the INSERT if necessary
|
|
1271
|
+
});
|
|
1272
|
+
var mcpInputSchema17 = {
|
|
1273
|
+
type: "object",
|
|
1274
|
+
properties: {
|
|
1275
|
+
email: { type: "string", format: "email", description: "The email address for the new user." },
|
|
1276
|
+
password: { type: "string", minLength: 6, description: "Plain text password (min 6 chars). WARNING: Insecure." },
|
|
1277
|
+
role: { type: "string", default: "authenticated", description: "User role." },
|
|
1278
|
+
user_metadata: { type: "object", description: "Optional user metadata." },
|
|
1279
|
+
app_metadata: { type: "object", description: "Optional app metadata." }
|
|
1280
|
+
},
|
|
1281
|
+
required: ["email", "password"]
|
|
1282
|
+
};
|
|
1283
|
+
var createAuthUserTool = {
|
|
1284
|
+
name: "create_auth_user",
|
|
1285
|
+
description: "Creates a new user directly in auth.users. WARNING: Requires plain password, insecure. Use with extreme caution.",
|
|
1286
|
+
inputSchema: CreateAuthUserInputSchema,
|
|
1287
|
+
mcpInputSchema: mcpInputSchema17,
|
|
1288
|
+
// Ensure defined above
|
|
1289
|
+
outputSchema: CreatedAuthUserZodSchema,
|
|
1290
|
+
execute: async (input, context) => {
|
|
1291
|
+
const client = context.selfhostedClient;
|
|
1292
|
+
const { email, password, role, app_metadata, user_metadata } = input;
|
|
1293
|
+
if (!client.isPgAvailable()) {
|
|
1294
|
+
context.log("Direct database connection (DATABASE_URL) is required to create an auth user directly.", "error");
|
|
1295
|
+
throw new Error("Direct database connection (DATABASE_URL) is required to create an auth user directly.");
|
|
1296
|
+
}
|
|
1297
|
+
console.warn(`SECURITY WARNING: Creating user ${email} with plain text password via direct DB insert.`);
|
|
1298
|
+
context.log(`Attempting to create user ${email}...`, "warn");
|
|
1299
|
+
const createdUser = await client.executeTransactionWithPg(async (pgClient) => {
|
|
1300
|
+
try {
|
|
1301
|
+
await pgClient.query("SELECT crypt('test', gen_salt('bf'))");
|
|
1302
|
+
} catch (err) {
|
|
1303
|
+
throw new Error("Failed to execute crypt function. Ensure pgcrypto extension is enabled in the database.");
|
|
1304
|
+
}
|
|
1305
|
+
const sql = `
|
|
1306
|
+
INSERT INTO auth.users (
|
|
1307
|
+
instance_id, email, encrypted_password, role,
|
|
1308
|
+
raw_app_meta_data, raw_user_meta_data,
|
|
1309
|
+
aud, email_confirmed_at, confirmation_sent_at -- Set required defaults
|
|
1310
|
+
)
|
|
1311
|
+
VALUES (
|
|
1312
|
+
COALESCE(current_setting('app.instance_id', TRUE), '00000000-0000-0000-0000-000000000000')::uuid,
|
|
1313
|
+
$1, crypt($2, gen_salt('bf')),
|
|
1314
|
+
$3,
|
|
1315
|
+
$4::jsonb,
|
|
1316
|
+
$5::jsonb,
|
|
1317
|
+
'authenticated', now(), now()
|
|
1318
|
+
)
|
|
1319
|
+
RETURNING id, email, role, raw_app_meta_data, raw_user_meta_data, created_at::text, last_sign_in_at::text;
|
|
1320
|
+
`;
|
|
1321
|
+
const params = [
|
|
1322
|
+
email,
|
|
1323
|
+
password,
|
|
1324
|
+
role || "authenticated",
|
|
1325
|
+
// Default role
|
|
1326
|
+
JSON.stringify(app_metadata || {}),
|
|
1327
|
+
JSON.stringify(user_metadata || {})
|
|
1328
|
+
];
|
|
1329
|
+
try {
|
|
1330
|
+
const result = await pgClient.query(sql, params);
|
|
1331
|
+
if (result.rows.length === 0) {
|
|
1332
|
+
throw new Error("User creation failed, no user returned after insert.");
|
|
1333
|
+
}
|
|
1334
|
+
return CreatedAuthUserZodSchema.parse(result.rows[0]);
|
|
1335
|
+
} catch (dbError) {
|
|
1336
|
+
let errorMessage = "Unknown database error during user creation";
|
|
1337
|
+
let isUniqueViolation = false;
|
|
1338
|
+
if (typeof dbError === "object" && dbError !== null && "code" in dbError) {
|
|
1339
|
+
if (dbError.code === "23505") {
|
|
1340
|
+
isUniqueViolation = true;
|
|
1341
|
+
errorMessage = `User creation failed: Email '${email}' likely already exists.`;
|
|
1342
|
+
} else if ("message" in dbError && typeof dbError.message === "string") {
|
|
1343
|
+
errorMessage = `Database error (${dbError.code}): ${dbError.message}`;
|
|
1344
|
+
} else {
|
|
1345
|
+
errorMessage = `Database error code: ${dbError.code}`;
|
|
1346
|
+
}
|
|
1347
|
+
} else if (dbError instanceof Error) {
|
|
1348
|
+
errorMessage = `Database error during user creation: ${dbError.message}`;
|
|
1349
|
+
} else {
|
|
1350
|
+
errorMessage = `Database error during user creation: ${String(dbError)}`;
|
|
1351
|
+
}
|
|
1352
|
+
console.error("Error creating user in DB:", dbError);
|
|
1353
|
+
throw new Error(errorMessage);
|
|
1354
|
+
}
|
|
1355
|
+
});
|
|
1356
|
+
console.error(`Successfully created user ${email} with ID ${createdUser.id}.`);
|
|
1357
|
+
context.log(`Successfully created user ${email} with ID ${createdUser.id}.`);
|
|
1358
|
+
return createdUser;
|
|
1359
|
+
}
|
|
1360
|
+
};
|
|
1361
|
+
|
|
1362
|
+
// src/tools/update_auth_user.ts
|
|
1363
|
+
import { z as z19 } from "zod";
|
|
1364
|
+
var UpdateAuthUserInputSchema = z19.object({
|
|
1365
|
+
user_id: z19.string().uuid().describe("The UUID of the user to update."),
|
|
1366
|
+
email: z19.string().email().optional().describe("New email address."),
|
|
1367
|
+
password: z19.string().min(6).optional().describe("New plain text password (min 6 chars). WARNING: Insecure."),
|
|
1368
|
+
role: z19.string().optional().describe("New role."),
|
|
1369
|
+
app_metadata: z19.record(z19.unknown()).optional().describe("New app metadata (will overwrite existing)."),
|
|
1370
|
+
user_metadata: z19.record(z19.unknown()).optional().describe("New user metadata (will overwrite existing).")
|
|
1371
|
+
}).refine(
|
|
1372
|
+
(data) => data.email || data.password || data.role || data.app_metadata || data.user_metadata,
|
|
1373
|
+
{ message: "At least one field to update (email, password, role, app_metadata, user_metadata) must be provided." }
|
|
1374
|
+
);
|
|
1375
|
+
var UpdatedAuthUserZodSchema = z19.object({
|
|
1376
|
+
id: z19.string().uuid(),
|
|
1377
|
+
email: z19.string().email().nullable(),
|
|
1378
|
+
role: z19.string().nullable(),
|
|
1379
|
+
created_at: z19.string().nullable(),
|
|
1380
|
+
updated_at: z19.string().nullable(),
|
|
1381
|
+
// Expect this to be updated
|
|
1382
|
+
last_sign_in_at: z19.string().nullable(),
|
|
1383
|
+
raw_app_meta_data: z19.record(z19.unknown()).nullable(),
|
|
1384
|
+
raw_user_meta_data: z19.record(z19.unknown()).nullable()
|
|
1385
|
+
});
|
|
1386
|
+
var mcpInputSchema18 = {
|
|
1387
|
+
type: "object",
|
|
1388
|
+
properties: {
|
|
1389
|
+
user_id: { type: "string", format: "uuid", description: "The UUID of the user to update." },
|
|
1390
|
+
email: { type: "string", format: "email", description: "New email address." },
|
|
1391
|
+
password: { type: "string", minLength: 6, description: "New plain text password (min 6 chars). WARNING: Insecure." },
|
|
1392
|
+
role: { type: "string", description: "New role." },
|
|
1393
|
+
user_metadata: { type: "object", description: "New user metadata (will overwrite existing)." },
|
|
1394
|
+
app_metadata: { type: "object", description: "New app metadata (will overwrite existing)." }
|
|
1395
|
+
},
|
|
1396
|
+
required: ["user_id"]
|
|
1397
|
+
};
|
|
1398
|
+
var updateAuthUserTool = {
|
|
1399
|
+
name: "update_auth_user",
|
|
1400
|
+
description: "Updates fields for a user in auth.users. WARNING: Password handling is insecure. Requires service_role key and direct DB connection.",
|
|
1401
|
+
inputSchema: UpdateAuthUserInputSchema,
|
|
1402
|
+
mcpInputSchema: mcpInputSchema18,
|
|
1403
|
+
// Ensure defined
|
|
1404
|
+
outputSchema: UpdatedAuthUserZodSchema,
|
|
1405
|
+
execute: async (input, context) => {
|
|
1406
|
+
const client = context.selfhostedClient;
|
|
1407
|
+
const { user_id, email, password, role, app_metadata, user_metadata } = input;
|
|
1408
|
+
if (!client.isPgAvailable()) {
|
|
1409
|
+
context.log("Direct database connection (DATABASE_URL) is required to update auth user details.", "error");
|
|
1410
|
+
throw new Error("Direct database connection (DATABASE_URL) is required to update auth user details.");
|
|
1411
|
+
}
|
|
1412
|
+
const updates = [];
|
|
1413
|
+
const params = [];
|
|
1414
|
+
let paramIndex = 1;
|
|
1415
|
+
if (email !== void 0) {
|
|
1416
|
+
updates.push(`email = $${paramIndex++}`);
|
|
1417
|
+
params.push(email);
|
|
1418
|
+
}
|
|
1419
|
+
if (password !== void 0) {
|
|
1420
|
+
updates.push(`encrypted_password = crypt($${paramIndex++}, gen_salt('bf'))`);
|
|
1421
|
+
params.push(password);
|
|
1422
|
+
console.warn(`SECURITY WARNING: Updating password for user ${user_id} with plain text password via direct DB update.`);
|
|
1423
|
+
}
|
|
1424
|
+
if (role !== void 0) {
|
|
1425
|
+
updates.push(`role = $${paramIndex++}`);
|
|
1426
|
+
params.push(role);
|
|
1427
|
+
}
|
|
1428
|
+
if (app_metadata !== void 0) {
|
|
1429
|
+
updates.push(`raw_app_meta_data = $${paramIndex++}::jsonb`);
|
|
1430
|
+
params.push(JSON.stringify(app_metadata));
|
|
1431
|
+
}
|
|
1432
|
+
if (user_metadata !== void 0) {
|
|
1433
|
+
updates.push(`raw_user_meta_data = $${paramIndex++}::jsonb`);
|
|
1434
|
+
params.push(JSON.stringify(user_metadata));
|
|
1435
|
+
}
|
|
1436
|
+
params.push(user_id);
|
|
1437
|
+
const userIdParamIndex = paramIndex;
|
|
1438
|
+
const sql = `
|
|
1439
|
+
UPDATE auth.users
|
|
1440
|
+
SET ${updates.join(", ")}, updated_at = NOW()
|
|
1441
|
+
WHERE id = $${userIdParamIndex}
|
|
1442
|
+
RETURNING id, email, role, raw_app_meta_data, raw_user_meta_data, created_at::text, updated_at::text, last_sign_in_at::text;
|
|
1443
|
+
`;
|
|
1444
|
+
console.error(`Attempting to update auth user ${user_id}...`);
|
|
1445
|
+
context.log(`Attempting to update auth user ${user_id}...`);
|
|
1446
|
+
const updatedUser = await client.executeTransactionWithPg(async (pgClient) => {
|
|
1447
|
+
if (password !== void 0) {
|
|
1448
|
+
try {
|
|
1449
|
+
await pgClient.query("SELECT crypt('test', gen_salt('bf'))");
|
|
1450
|
+
} catch (err) {
|
|
1451
|
+
throw new Error("Failed to execute crypt function for password update. Ensure pgcrypto extension is enabled.");
|
|
1452
|
+
}
|
|
1453
|
+
}
|
|
1454
|
+
try {
|
|
1455
|
+
const result = await pgClient.query(sql, params);
|
|
1456
|
+
if (result.rows.length === 0) {
|
|
1457
|
+
throw new Error(`User update failed: User with ID ${user_id} not found or no rows affected.`);
|
|
1458
|
+
}
|
|
1459
|
+
return UpdatedAuthUserZodSchema.parse(result.rows[0]);
|
|
1460
|
+
} catch (dbError) {
|
|
1461
|
+
let errorMessage = "Unknown database error during user update";
|
|
1462
|
+
let isUniqueViolation = false;
|
|
1463
|
+
if (typeof dbError === "object" && dbError !== null && "code" in dbError) {
|
|
1464
|
+
if (email !== void 0 && dbError.code === "23505") {
|
|
1465
|
+
isUniqueViolation = true;
|
|
1466
|
+
errorMessage = `User update failed: Email '${email}' likely already exists for another user.`;
|
|
1467
|
+
} else if ("message" in dbError && typeof dbError.message === "string") {
|
|
1468
|
+
errorMessage = `Database error (${dbError.code}): ${dbError.message}`;
|
|
1469
|
+
} else {
|
|
1470
|
+
errorMessage = `Database error code: ${dbError.code}`;
|
|
1471
|
+
}
|
|
1472
|
+
} else if (dbError instanceof Error) {
|
|
1473
|
+
errorMessage = `Database error during user update: ${dbError.message}`;
|
|
1474
|
+
} else {
|
|
1475
|
+
errorMessage = `Database error during user update: ${String(dbError)}`;
|
|
1476
|
+
}
|
|
1477
|
+
console.error("Error updating user in DB:", dbError);
|
|
1478
|
+
throw new Error(errorMessage);
|
|
1479
|
+
}
|
|
1480
|
+
});
|
|
1481
|
+
console.error(`Successfully updated user ${user_id}.`);
|
|
1482
|
+
context.log(`Successfully updated user ${user_id}.`);
|
|
1483
|
+
return updatedUser;
|
|
1484
|
+
}
|
|
1485
|
+
};
|
|
1486
|
+
|
|
1487
|
+
// src/index.ts
|
|
1488
|
+
import { z as z23 } from "zod";
|
|
1489
|
+
|
|
1490
|
+
// src/tools/list_storage_buckets.ts
|
|
1491
|
+
import { z as z20 } from "zod";
|
|
1492
|
+
var BucketSchema = z20.object({
|
|
1493
|
+
id: z20.string(),
|
|
1494
|
+
name: z20.string(),
|
|
1495
|
+
owner: z20.string().nullable(),
|
|
1496
|
+
public: z20.boolean(),
|
|
1497
|
+
avif_autodetection: z20.boolean(),
|
|
1498
|
+
file_size_limit: z20.number().nullable(),
|
|
1499
|
+
allowed_mime_types: z20.array(z20.string()).nullable(),
|
|
1500
|
+
// Keep timestamps as strings as returned by DB/pg
|
|
1501
|
+
created_at: z20.string().nullable(),
|
|
1502
|
+
updated_at: z20.string().nullable()
|
|
1503
|
+
});
|
|
1504
|
+
var ListStorageBucketsOutputSchema = z20.array(BucketSchema);
|
|
1505
|
+
var mcpInputSchema19 = {
|
|
1506
|
+
type: "object",
|
|
1507
|
+
properties: {},
|
|
1508
|
+
required: []
|
|
1509
|
+
};
|
|
1510
|
+
var inputSchema = z20.object({});
|
|
1511
|
+
var listStorageBucketsTool = {
|
|
1512
|
+
name: "list_storage_buckets",
|
|
1513
|
+
description: "Lists all storage buckets in the project.",
|
|
1514
|
+
mcpInputSchema: mcpInputSchema19,
|
|
1515
|
+
inputSchema,
|
|
1516
|
+
outputSchema: ListStorageBucketsOutputSchema,
|
|
1517
|
+
execute: async (input, context) => {
|
|
1518
|
+
const client = context.selfhostedClient;
|
|
1519
|
+
console.error("Listing storage buckets...");
|
|
1520
|
+
if (!client.isPgAvailable()) {
|
|
1521
|
+
context.log("Direct database connection (DATABASE_URL) is required to list storage buckets.", "error");
|
|
1522
|
+
throw new Error("Direct database connection (DATABASE_URL) is required to list storage buckets.");
|
|
1523
|
+
}
|
|
1524
|
+
const sql = `
|
|
1525
|
+
SELECT
|
|
1526
|
+
id,
|
|
1527
|
+
name,
|
|
1528
|
+
owner,
|
|
1529
|
+
public,
|
|
1530
|
+
avif_autodetection,
|
|
1531
|
+
file_size_limit,
|
|
1532
|
+
allowed_mime_types,
|
|
1533
|
+
created_at::text, -- Cast to text
|
|
1534
|
+
updated_at::text -- Cast to text
|
|
1535
|
+
FROM storage.buckets;
|
|
1536
|
+
`;
|
|
1537
|
+
console.error("Attempting to list storage buckets using direct DB connection...");
|
|
1538
|
+
const result = await client.executeSqlWithPg(sql);
|
|
1539
|
+
const validatedBuckets = handleSqlResponse(result, ListStorageBucketsOutputSchema);
|
|
1540
|
+
console.error(`Found ${validatedBuckets.length} buckets.`);
|
|
1541
|
+
context.log(`Found ${validatedBuckets.length} buckets.`);
|
|
1542
|
+
return validatedBuckets;
|
|
1543
|
+
}
|
|
1544
|
+
};
|
|
1545
|
+
var list_storage_buckets_default = listStorageBucketsTool;
|
|
1546
|
+
|
|
1547
|
+
// src/tools/list_storage_objects.ts
|
|
1548
|
+
import { z as z21 } from "zod";
|
|
1549
|
+
var ListStorageObjectsInputSchema = z21.object({
|
|
1550
|
+
bucket_id: z21.string().describe("The ID of the bucket to list objects from."),
|
|
1551
|
+
limit: z21.number().int().positive().optional().default(100).describe("Max number of objects to return"),
|
|
1552
|
+
offset: z21.number().int().nonnegative().optional().default(0).describe("Number of objects to skip"),
|
|
1553
|
+
prefix: z21.string().optional().describe("Filter objects by a path prefix (e.g., 'public/')")
|
|
1554
|
+
});
|
|
1555
|
+
var StorageObjectSchema = z21.object({
|
|
1556
|
+
id: z21.string().uuid(),
|
|
1557
|
+
name: z21.string().nullable(),
|
|
1558
|
+
// Name can be null according to schema
|
|
1559
|
+
bucket_id: z21.string(),
|
|
1560
|
+
owner: z21.string().uuid().nullable(),
|
|
1561
|
+
version: z21.string().nullable(),
|
|
1562
|
+
// Get mimetype directly from SQL extraction
|
|
1563
|
+
mimetype: z21.string().nullable(),
|
|
1564
|
+
// size comes from metadata
|
|
1565
|
+
size: z21.string().pipe(z21.coerce.number().int()).nullable(),
|
|
1566
|
+
// Keep raw metadata as well
|
|
1567
|
+
metadata: z21.record(z21.any()).nullable(),
|
|
1568
|
+
created_at: z21.string().nullable(),
|
|
1569
|
+
updated_at: z21.string().nullable(),
|
|
1570
|
+
last_accessed_at: z21.string().nullable()
|
|
1571
|
+
});
|
|
1572
|
+
var ListStorageObjectsOutputSchema = z21.array(StorageObjectSchema);
|
|
1573
|
+
var mcpInputSchema20 = {
|
|
1574
|
+
type: "object",
|
|
1575
|
+
properties: {
|
|
1576
|
+
bucket_id: { type: "string", description: "The ID of the bucket to list objects from." },
|
|
1577
|
+
limit: { type: "number", description: "Max number of objects to return", default: 100 },
|
|
1578
|
+
offset: { type: "number", description: "Number of objects to skip", default: 0 },
|
|
1579
|
+
prefix: { type: "string", description: "Filter objects by a path prefix (e.g., 'public/')" }
|
|
1580
|
+
},
|
|
1581
|
+
required: ["bucket_id"]
|
|
1582
|
+
};
|
|
1583
|
+
var listStorageObjectsTool = {
|
|
1584
|
+
name: "list_storage_objects",
|
|
1585
|
+
description: "Lists objects within a specific storage bucket, optionally filtering by prefix.",
|
|
1586
|
+
mcpInputSchema: mcpInputSchema20,
|
|
1587
|
+
inputSchema: ListStorageObjectsInputSchema,
|
|
1588
|
+
outputSchema: ListStorageObjectsOutputSchema,
|
|
1589
|
+
execute: async (input, context) => {
|
|
1590
|
+
const client = context.selfhostedClient;
|
|
1591
|
+
const { bucket_id, limit, offset, prefix } = input;
|
|
1592
|
+
console.error(`Listing objects for bucket ${bucket_id} (Prefix: ${prefix || "N/A"})...`);
|
|
1593
|
+
if (!client.isPgAvailable()) {
|
|
1594
|
+
context.log("Direct database connection (DATABASE_URL) is required to list storage objects.", "error");
|
|
1595
|
+
throw new Error("Direct database connection (DATABASE_URL) is required to list storage objects.");
|
|
1596
|
+
}
|
|
1597
|
+
const objects = await client.executeTransactionWithPg(async (pgClient) => {
|
|
1598
|
+
let sql = `
|
|
1599
|
+
SELECT
|
|
1600
|
+
id,
|
|
1601
|
+
name,
|
|
1602
|
+
bucket_id,
|
|
1603
|
+
owner,
|
|
1604
|
+
version,
|
|
1605
|
+
metadata ->> 'mimetype' AS mimetype,
|
|
1606
|
+
metadata ->> 'size' AS size, -- Extract size from metadata
|
|
1607
|
+
metadata,
|
|
1608
|
+
created_at::text,
|
|
1609
|
+
updated_at::text,
|
|
1610
|
+
last_accessed_at::text
|
|
1611
|
+
FROM storage.objects
|
|
1612
|
+
WHERE bucket_id = $1
|
|
1613
|
+
`;
|
|
1614
|
+
const params = [bucket_id];
|
|
1615
|
+
let paramIndex = 2;
|
|
1616
|
+
if (prefix) {
|
|
1617
|
+
sql += ` AND name LIKE $${paramIndex++}`;
|
|
1618
|
+
params.push(`${prefix}%`);
|
|
1619
|
+
}
|
|
1620
|
+
sql += " ORDER BY name ASC NULLS FIRST";
|
|
1621
|
+
sql += ` LIMIT $${paramIndex++}`;
|
|
1622
|
+
params.push(limit);
|
|
1623
|
+
sql += ` OFFSET $${paramIndex++}`;
|
|
1624
|
+
params.push(offset);
|
|
1625
|
+
sql += ";";
|
|
1626
|
+
console.error("Executing parameterized SQL to list storage objects within transaction...");
|
|
1627
|
+
const result = await pgClient.query(sql, params);
|
|
1628
|
+
return handleSqlResponse(result.rows, ListStorageObjectsOutputSchema);
|
|
1629
|
+
});
|
|
1630
|
+
console.error(`Found ${objects.length} objects.`);
|
|
1631
|
+
context.log(`Found ${objects.length} objects.`);
|
|
1632
|
+
return objects;
|
|
1633
|
+
}
|
|
1634
|
+
};
|
|
1635
|
+
var list_storage_objects_default = listStorageObjectsTool;
|
|
1636
|
+
|
|
1637
|
+
// src/tools/list_realtime_publications.ts
|
|
1638
|
+
import { z as z22 } from "zod";
|
|
1639
|
+
var ListRealtimePublicationsInputSchema = z22.object({});
|
|
1640
|
+
var PublicationSchema = z22.object({
|
|
1641
|
+
oid: z22.number().int(),
|
|
1642
|
+
pubname: z22.string(),
|
|
1643
|
+
pubowner: z22.number().int(),
|
|
1644
|
+
// Owner OID
|
|
1645
|
+
puballtables: z22.boolean(),
|
|
1646
|
+
pubinsert: z22.boolean(),
|
|
1647
|
+
pubupdate: z22.boolean(),
|
|
1648
|
+
pubdelete: z22.boolean(),
|
|
1649
|
+
pubtruncate: z22.boolean(),
|
|
1650
|
+
pubviaroot: z22.boolean()
|
|
1651
|
+
// Potentially add pubownername if needed via join
|
|
1652
|
+
});
|
|
1653
|
+
var ListRealtimePublicationsOutputSchema = z22.array(PublicationSchema);
|
|
1654
|
+
var mcpInputSchema21 = {
|
|
1655
|
+
type: "object",
|
|
1656
|
+
properties: {},
|
|
1657
|
+
required: []
|
|
1658
|
+
};
|
|
1659
|
+
var listRealtimePublicationsTool = {
|
|
1660
|
+
name: "list_realtime_publications",
|
|
1661
|
+
description: "Lists PostgreSQL publications, often used by Supabase Realtime.",
|
|
1662
|
+
mcpInputSchema: mcpInputSchema21,
|
|
1663
|
+
inputSchema: ListRealtimePublicationsInputSchema,
|
|
1664
|
+
outputSchema: ListRealtimePublicationsOutputSchema,
|
|
1665
|
+
execute: async (input, context) => {
|
|
1666
|
+
const client = context.selfhostedClient;
|
|
1667
|
+
console.error("Listing Realtime publications...");
|
|
1668
|
+
if (!client.isPgAvailable()) {
|
|
1669
|
+
context.log("Direct database connection (DATABASE_URL) is required to list publications.", "error");
|
|
1670
|
+
throw new Error("Direct database connection (DATABASE_URL) is required to list publications.");
|
|
1671
|
+
}
|
|
1672
|
+
const sql = `
|
|
1673
|
+
SELECT
|
|
1674
|
+
oid,
|
|
1675
|
+
pubname,
|
|
1676
|
+
pubowner,
|
|
1677
|
+
puballtables,
|
|
1678
|
+
pubinsert,
|
|
1679
|
+
pubupdate,
|
|
1680
|
+
pubdelete,
|
|
1681
|
+
pubtruncate,
|
|
1682
|
+
pubviaroot
|
|
1683
|
+
FROM pg_catalog.pg_publication;
|
|
1684
|
+
`;
|
|
1685
|
+
console.error("Attempting to list publications using direct DB connection...");
|
|
1686
|
+
const result = await client.executeSqlWithPg(sql);
|
|
1687
|
+
const validatedPublications = handleSqlResponse(result, ListRealtimePublicationsOutputSchema);
|
|
1688
|
+
console.error(`Found ${validatedPublications.length} publications.`);
|
|
1689
|
+
context.log(`Found ${validatedPublications.length} publications.`);
|
|
1690
|
+
return validatedPublications;
|
|
1691
|
+
}
|
|
1692
|
+
};
|
|
1693
|
+
var list_realtime_publications_default = listRealtimePublicationsTool;
|
|
1694
|
+
|
|
1695
|
+
// src/index.ts
|
|
1696
|
+
import * as fs from "node:fs";
|
|
1697
|
+
import * as path from "node:path";
|
|
1698
|
+
async function main() {
|
|
1699
|
+
const program = new Command();
|
|
1700
|
+
program.name("self-hosted-supabase-mcp").description("MCP Server for self-hosted Supabase instances").option("--url <url>", "Supabase project URL", process.env.SUPABASE_URL).option("--anon-key <key>", "Supabase anonymous key", process.env.SUPABASE_ANON_KEY).option("--service-key <key>", "Supabase service role key (optional)", process.env.SUPABASE_SERVICE_ROLE_KEY).option("--db-url <url>", "Direct database connection string (optional, for pg fallback)", process.env.DATABASE_URL).option("--jwt-secret <secret>", "Supabase JWT secret (optional, needed for some tools)", process.env.SUPABASE_AUTH_JWT_SECRET).option("--workspace-path <path>", "Workspace root path (for file operations)", process.cwd()).option("--tools-config <path>", 'Path to a JSON file specifying which tools to enable (e.g., { "enabledTools": ["tool1", "tool2"] }). If omitted, all tools are enabled.').parse(process.argv);
|
|
1701
|
+
const options = program.opts();
|
|
1702
|
+
if (!options.url) {
|
|
1703
|
+
console.error("Error: Supabase URL is required. Use --url or SUPABASE_URL.");
|
|
1704
|
+
throw new Error("Supabase URL is required.");
|
|
1705
|
+
}
|
|
1706
|
+
if (!options.anonKey) {
|
|
1707
|
+
console.error("Error: Supabase Anon Key is required. Use --anon-key or SUPABASE_ANON_KEY.");
|
|
1708
|
+
throw new Error("Supabase Anon Key is required.");
|
|
1709
|
+
}
|
|
1710
|
+
console.error("Initializing Self-Hosted Supabase MCP Server...");
|
|
1711
|
+
try {
|
|
1712
|
+
const selfhostedClient = await SelfhostedSupabaseClient.create({
|
|
1713
|
+
supabaseUrl: options.url,
|
|
1714
|
+
supabaseAnonKey: options.anonKey,
|
|
1715
|
+
supabaseServiceRoleKey: options.serviceKey,
|
|
1716
|
+
databaseUrl: options.dbUrl,
|
|
1717
|
+
jwtSecret: options.jwtSecret
|
|
1718
|
+
});
|
|
1719
|
+
console.error("Supabase client initialized successfully.");
|
|
1720
|
+
const availableTools = {
|
|
1721
|
+
// Cast here assumes tools will implement AppTool structure
|
|
1722
|
+
[listTablesTool.name]: listTablesTool,
|
|
1723
|
+
[listExtensionsTool.name]: listExtensionsTool,
|
|
1724
|
+
[listMigrationsTool.name]: listMigrationsTool,
|
|
1725
|
+
[applyMigrationTool.name]: applyMigrationTool,
|
|
1726
|
+
[executeSqlTool.name]: executeSqlTool,
|
|
1727
|
+
[getDatabaseConnectionsTool.name]: getDatabaseConnectionsTool,
|
|
1728
|
+
[getDatabaseStatsTool.name]: getDatabaseStatsTool,
|
|
1729
|
+
[getProjectUrlTool.name]: getProjectUrlTool,
|
|
1730
|
+
[getAnonKeyTool.name]: getAnonKeyTool,
|
|
1731
|
+
[getServiceKeyTool.name]: getServiceKeyTool,
|
|
1732
|
+
[generateTypesTool.name]: generateTypesTool,
|
|
1733
|
+
[rebuildHooksTool.name]: rebuildHooksTool,
|
|
1734
|
+
[verifyJwtSecretTool.name]: verifyJwtSecretTool,
|
|
1735
|
+
[listAuthUsersTool.name]: listAuthUsersTool,
|
|
1736
|
+
[getAuthUserTool.name]: getAuthUserTool,
|
|
1737
|
+
[deleteAuthUserTool.name]: deleteAuthUserTool,
|
|
1738
|
+
[createAuthUserTool.name]: createAuthUserTool,
|
|
1739
|
+
[updateAuthUserTool.name]: updateAuthUserTool,
|
|
1740
|
+
[list_storage_buckets_default.name]: list_storage_buckets_default,
|
|
1741
|
+
[list_storage_objects_default.name]: list_storage_objects_default,
|
|
1742
|
+
[list_realtime_publications_default.name]: list_realtime_publications_default
|
|
1743
|
+
};
|
|
1744
|
+
let registeredTools = { ...availableTools };
|
|
1745
|
+
const toolsConfigPath = options.toolsConfig;
|
|
1746
|
+
let enabledToolNames = null;
|
|
1747
|
+
if (toolsConfigPath) {
|
|
1748
|
+
try {
|
|
1749
|
+
const resolvedPath = path.resolve(toolsConfigPath);
|
|
1750
|
+
console.error(`Attempting to load tool configuration from: ${resolvedPath}`);
|
|
1751
|
+
if (!fs.existsSync(resolvedPath)) {
|
|
1752
|
+
throw new Error(`Tool configuration file not found at ${resolvedPath}`);
|
|
1753
|
+
}
|
|
1754
|
+
const configFileContent = fs.readFileSync(resolvedPath, "utf-8");
|
|
1755
|
+
const configJson = JSON.parse(configFileContent);
|
|
1756
|
+
if (!configJson || typeof configJson !== "object" || !Array.isArray(configJson.enabledTools)) {
|
|
1757
|
+
throw new Error('Invalid config file format. Expected { "enabledTools": ["tool1", ...] }.');
|
|
1758
|
+
}
|
|
1759
|
+
const toolNames = configJson.enabledTools;
|
|
1760
|
+
if (!toolNames.every((name) => typeof name === "string")) {
|
|
1761
|
+
throw new Error('Invalid config file content. "enabledTools" must be an array of strings.');
|
|
1762
|
+
}
|
|
1763
|
+
enabledToolNames = new Set(toolNames.map((name) => name.trim()).filter((name) => name.length > 0));
|
|
1764
|
+
} catch (error) {
|
|
1765
|
+
console.error(`Error loading or parsing tool config file '${toolsConfigPath}':`, error instanceof Error ? error.message : String(error));
|
|
1766
|
+
console.error("Falling back to enabling all tools due to config error.");
|
|
1767
|
+
enabledToolNames = null;
|
|
1768
|
+
}
|
|
1769
|
+
}
|
|
1770
|
+
if (enabledToolNames !== null) {
|
|
1771
|
+
console.error(`Whitelisting tools based on config: ${Array.from(enabledToolNames).join(", ")}`);
|
|
1772
|
+
registeredTools = {};
|
|
1773
|
+
for (const toolName in availableTools) {
|
|
1774
|
+
if (enabledToolNames.has(toolName)) {
|
|
1775
|
+
registeredTools[toolName] = availableTools[toolName];
|
|
1776
|
+
} else {
|
|
1777
|
+
console.error(`Tool ${toolName} disabled (not in config whitelist).`);
|
|
1778
|
+
}
|
|
1779
|
+
}
|
|
1780
|
+
for (const requestedName of enabledToolNames) {
|
|
1781
|
+
if (!availableTools[requestedName]) {
|
|
1782
|
+
console.warn(`Warning: Tool "${requestedName}" specified in config file not found.`);
|
|
1783
|
+
}
|
|
1784
|
+
}
|
|
1785
|
+
} else {
|
|
1786
|
+
console.error("No valid --tools-config specified or error loading config, enabling all available tools.");
|
|
1787
|
+
}
|
|
1788
|
+
const capabilitiesTools = {};
|
|
1789
|
+
for (const tool of Object.values(registeredTools)) {
|
|
1790
|
+
const staticInputSchema = tool.mcpInputSchema || { type: "object", properties: {} };
|
|
1791
|
+
if (!tool.mcpInputSchema) {
|
|
1792
|
+
console.error(`Tool ${tool.name} is missing mcpInputSchema. Using default empty schema.`);
|
|
1793
|
+
}
|
|
1794
|
+
capabilitiesTools[tool.name] = {
|
|
1795
|
+
name: tool.name,
|
|
1796
|
+
description: tool.description || "Tool description missing",
|
|
1797
|
+
inputSchema: staticInputSchema
|
|
1798
|
+
};
|
|
1799
|
+
}
|
|
1800
|
+
const capabilities = { tools: capabilitiesTools };
|
|
1801
|
+
console.error("Initializing MCP Server...");
|
|
1802
|
+
const server = new Server(
|
|
1803
|
+
{
|
|
1804
|
+
name: "self-hosted-supabase-mcp",
|
|
1805
|
+
version: "1.0.0"
|
|
1806
|
+
},
|
|
1807
|
+
{
|
|
1808
|
+
capabilities
|
|
1809
|
+
}
|
|
1810
|
+
);
|
|
1811
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
1812
|
+
tools: Object.values(capabilities.tools)
|
|
1813
|
+
}));
|
|
1814
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
1815
|
+
const toolName = request.params.name;
|
|
1816
|
+
const tool = registeredTools[toolName];
|
|
1817
|
+
if (!tool) {
|
|
1818
|
+
if (availableTools[toolName]) {
|
|
1819
|
+
throw new McpError(ErrorCode.MethodNotFound, `Tool "${toolName}" is available but not enabled by the current server configuration.`);
|
|
1820
|
+
}
|
|
1821
|
+
throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${toolName}`);
|
|
1822
|
+
}
|
|
1823
|
+
try {
|
|
1824
|
+
if (typeof tool.execute !== "function") {
|
|
1825
|
+
throw new Error(`Tool ${toolName} does not have an execute method.`);
|
|
1826
|
+
}
|
|
1827
|
+
let parsedArgs = request.params.arguments;
|
|
1828
|
+
if (tool.inputSchema && typeof tool.inputSchema.parse === "function") {
|
|
1829
|
+
parsedArgs = tool.inputSchema.parse(request.params.arguments);
|
|
1830
|
+
}
|
|
1831
|
+
const context = {
|
|
1832
|
+
selfhostedClient,
|
|
1833
|
+
workspacePath: options.workspacePath,
|
|
1834
|
+
log: (message, level = "info") => {
|
|
1835
|
+
console.error(`[${level.toUpperCase()}] ${message}`);
|
|
1836
|
+
}
|
|
1837
|
+
};
|
|
1838
|
+
const result = await tool.execute(parsedArgs, context);
|
|
1839
|
+
return {
|
|
1840
|
+
content: [
|
|
1841
|
+
{
|
|
1842
|
+
type: "text",
|
|
1843
|
+
text: typeof result === "string" ? result : JSON.stringify(result, null, 2)
|
|
1844
|
+
}
|
|
1845
|
+
]
|
|
1846
|
+
};
|
|
1847
|
+
} catch (error) {
|
|
1848
|
+
console.error(`Error executing tool ${toolName}:`, error);
|
|
1849
|
+
let errorMessage = `Error executing tool ${toolName}: `;
|
|
1850
|
+
if (error instanceof z23.ZodError) {
|
|
1851
|
+
errorMessage += `Input validation failed: ${error.errors.map((e) => `${e.path.join(".")}: ${e.message}`).join(", ")}`;
|
|
1852
|
+
} else if (error instanceof Error) {
|
|
1853
|
+
errorMessage += error.message;
|
|
1854
|
+
} else {
|
|
1855
|
+
errorMessage += String(error);
|
|
1856
|
+
}
|
|
1857
|
+
return {
|
|
1858
|
+
content: [{ type: "text", text: errorMessage }],
|
|
1859
|
+
isError: true
|
|
1860
|
+
};
|
|
1861
|
+
}
|
|
1862
|
+
});
|
|
1863
|
+
console.error("Starting MCP Server in stdio mode...");
|
|
1864
|
+
const transport = new StdioServerTransport();
|
|
1865
|
+
await server.connect(transport);
|
|
1866
|
+
console.error("MCP Server connected to stdio.");
|
|
1867
|
+
} catch (error) {
|
|
1868
|
+
console.error("Failed to initialize or start the MCP server:", error);
|
|
1869
|
+
throw error;
|
|
1870
|
+
}
|
|
1871
|
+
}
|
|
1872
|
+
main().catch((error) => {
|
|
1873
|
+
console.error("Unhandled error in main function:", error);
|
|
1874
|
+
process.exit(1);
|
|
1875
|
+
});
|