@aliyun-rds/supabase-mcp-server 1.0.8 → 1.0.10

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.
Files changed (2) hide show
  1. package/dist/index.js +182 -95
  2. package/package.json +1 -2
package/dist/index.js CHANGED
@@ -398,6 +398,58 @@ function getAuthModeDescription(mode) {
398
398
  }
399
399
  }
400
400
 
401
+ // src/sql/execute_sql_function.ts
402
+ var DROP_OLD_EXECUTE_SQL_FUNCTIONS = `
403
+ DO $$
404
+ BEGIN
405
+ -- Drop all previous versions to avoid overload conflicts
406
+ DROP FUNCTION IF EXISTS public.execute_sql(text, boolean);
407
+ DROP FUNCTION IF EXISTS public.execute_sql(text);
408
+ END $$;
409
+ `;
410
+ var CREATE_EXECUTE_SQL_FUNCTION = `
411
+ CREATE OR REPLACE FUNCTION public.execute_sql(query text)
412
+ RETURNS jsonb
413
+ LANGUAGE plpgsql
414
+ SECURITY DEFINER
415
+ SET search_path = public
416
+ AS $$
417
+ DECLARE
418
+ result jsonb;
419
+ cleaned_query text;
420
+ affected_rows integer;
421
+ is_read_query boolean;
422
+ BEGIN
423
+ -- Remove trailing semicolons and whitespace from query
424
+ cleaned_query := regexp_replace(trim(query), ';\\s*$', '');
425
+
426
+ -- Auto-detect: treat as read query if starts with SELECT/WITH/SHOW/EXPLAIN/DESCRIBE/TABLE
427
+ is_read_query := cleaned_query ~* '^[[:space:]]*(SELECT|WITH|SHOW|EXPLAIN|DESCRIBE|TABLE)';
428
+
429
+ IF is_read_query THEN
430
+ -- Read query: return results as JSON array
431
+ EXECUTE 'SELECT COALESCE(jsonb_agg(row_to_json(t)), ''[]''::jsonb) FROM (' || cleaned_query || ') t' INTO result;
432
+ ELSE
433
+ -- Write query (INSERT/UPDATE/DELETE): execute and return affected rows count
434
+ EXECUTE cleaned_query;
435
+ GET DIAGNOSTICS affected_rows = ROW_COUNT;
436
+ result := jsonb_build_array(jsonb_build_object('affected_rows', affected_rows));
437
+ END IF;
438
+
439
+ RETURN COALESCE(result, '[]'::jsonb);
440
+ EXCEPTION
441
+ WHEN OTHERS THEN
442
+ RAISE EXCEPTION 'Error executing SQL (SQLSTATE: %): %', SQLSTATE, SQLERRM;
443
+ END;
444
+ $$;
445
+ `;
446
+ var GRANT_EXECUTE_SQL_FUNCTION = `
447
+ -- SECURITY: do NOT expose arbitrary SQL execution to anon/authenticated roles.
448
+ REVOKE EXECUTE ON FUNCTION public.execute_sql(text) FROM PUBLIC;
449
+ GRANT EXECUTE ON FUNCTION public.execute_sql(text) TO service_role;
450
+ `;
451
+ var NOTIFY_POSTGREST = `NOTIFY pgrst, 'reload schema';`;
452
+
401
453
  // src/client/index.ts
402
454
  var SelfhostedSupabaseClient = class _SelfhostedSupabaseClient {
403
455
  options;
@@ -409,35 +461,6 @@ var SelfhostedSupabaseClient = class _SelfhostedSupabaseClient {
409
461
  authMode;
410
462
  /** User authentication client (only set for AuthMode.SINGLE_INSTANCE_USER) */
411
463
  userAuthClient = null;
412
- // SQL definition for the helper function
413
- static CREATE_EXECUTE_SQL_FUNCTION = `
414
- CREATE OR REPLACE FUNCTION public.execute_sql(query text, read_only boolean DEFAULT false)
415
- RETURNS jsonb -- Using jsonb is generally preferred over json
416
- LANGUAGE plpgsql
417
- AS $$
418
- DECLARE
419
- result jsonb;
420
- BEGIN
421
- -- Note: SET TRANSACTION READ ONLY might not behave as expected within a function
422
- -- depending on the outer transaction state. Handle read-only logic outside if needed.
423
-
424
- -- Execute the dynamic query and aggregate results into a JSONB array
425
- EXECUTE 'SELECT COALESCE(jsonb_agg(t), ''[]''::jsonb) FROM (' || query || ') t' INTO result;
426
-
427
- RETURN result;
428
- EXCEPTION
429
- WHEN others THEN
430
- -- Rethrow the error with context, including the original SQLSTATE
431
- RAISE EXCEPTION 'Error executing SQL (SQLSTATE: %): % ', SQLSTATE, SQLERRM;
432
- END;
433
- $$;
434
- `;
435
- // SQL to grant permissions
436
- static GRANT_EXECUTE_SQL_FUNCTION = `
437
- GRANT EXECUTE ON FUNCTION public.execute_sql(text, boolean) TO authenticated;
438
- -- Optionally grant to anon if needed (uncomment if required):
439
- -- GRANT EXECUTE ON FUNCTION public.execute_sql(text, boolean) TO anon;
440
- `;
441
464
  /**
442
465
  * Creates an instance of SelfhostedSupabaseClient.
443
466
  * Note: Call initialize() after creating the instance to check for RPC functions.
@@ -502,8 +525,7 @@ var SelfhostedSupabaseClient = class _SelfhostedSupabaseClient {
502
525
  console.error("Initializing SelfhostedSupabaseClient for user-level authentication...");
503
526
  try {
504
527
  const { error } = await this.supabase.rpc("execute_sql", {
505
- query: "SELECT 1",
506
- read_only: true
528
+ query: "SELECT 1"
507
529
  });
508
530
  this.rpcFunctionExists = !error;
509
531
  if (error) {
@@ -535,7 +557,7 @@ var SelfhostedSupabaseClient = class _SelfhostedSupabaseClient {
535
557
  /**
536
558
  * Executes SQL using the preferred RPC method.
537
559
  */
538
- async executeSqlViaRpc(query, readOnly = false) {
560
+ async executeSqlViaRpc(query) {
539
561
  if (!this.rpcFunctionExists) {
540
562
  console.error("Attempted to call executeSqlViaRpc, but RPC function is not available.");
541
563
  return {
@@ -545,12 +567,9 @@ var SelfhostedSupabaseClient = class _SelfhostedSupabaseClient {
545
567
  }
546
568
  };
547
569
  }
548
- console.error(`Executing via RPC (readOnly: ${readOnly}): ${query.substring(0, 100)}...`);
570
+ console.error(`Executing via RPC: ${query.substring(0, 100)}...`);
549
571
  try {
550
- const { data, error } = await this.supabase.rpc("execute_sql", {
551
- query,
552
- read_only: readOnly
553
- });
572
+ const { data, error } = await this.supabase.rpc("execute_sql", { query });
554
573
  if (error) {
555
574
  console.error("Error executing SQL via RPC:", error);
556
575
  return {
@@ -730,22 +749,30 @@ var SelfhostedSupabaseClient = class _SelfhostedSupabaseClient {
730
749
  JOIN pg_namespace n ON p.pronamespace = n.oid
731
750
  WHERE n.nspname = 'public'
732
751
  AND p.proname = 'execute_sql'
752
+ AND p.pronargs = 1
733
753
  ) as function_exists;
734
754
  `;
735
755
  const checkResult = await this.executeSqlViaPostgresMeta(checkQuery);
736
- if (checkResult && checkResult.length > 0 && checkResult[0].function_exists) {
737
- console.error("'public.execute_sql' function found.");
738
- this.rpcFunctionExists = true;
739
- return;
756
+ if (checkResult && checkResult.length > 0 && checkResult[0]?.function_exists) {
757
+ console.error("'public.execute_sql' function found. Enforcing least-privilege grants...");
758
+ try {
759
+ await this.executeSqlViaPostgresMeta(GRANT_EXECUTE_SQL_FUNCTION);
760
+ this.rpcFunctionExists = true;
761
+ return;
762
+ } catch (grantError) {
763
+ console.error("Failed to enforce grants for execute_sql. Will recreate the function to ensure a clean state:", grantError);
764
+ }
740
765
  }
741
766
  console.error("'public.execute_sql' function not found. Creating...");
742
767
  try {
768
+ console.error("Dropping old function versions...");
769
+ await this.executeSqlViaPostgresMeta(DROP_OLD_EXECUTE_SQL_FUNCTIONS);
743
770
  console.error("Creating 'public.execute_sql' function using postgres-meta API...");
744
- await this.executeSqlViaPostgresMeta(_SelfhostedSupabaseClient.CREATE_EXECUTE_SQL_FUNCTION);
745
- await this.executeSqlViaPostgresMeta(_SelfhostedSupabaseClient.GRANT_EXECUTE_SQL_FUNCTION);
771
+ await this.executeSqlViaPostgresMeta(CREATE_EXECUTE_SQL_FUNCTION);
772
+ await this.executeSqlViaPostgresMeta(GRANT_EXECUTE_SQL_FUNCTION);
746
773
  console.error("'public.execute_sql' function created and permissions granted successfully.");
747
774
  console.error("Notifying PostgREST to reload schema cache...");
748
- await this.executeSqlViaPostgresMeta("NOTIFY pgrst, 'reload schema'");
775
+ await this.executeSqlViaPostgresMeta(NOTIFY_POSTGREST);
749
776
  console.error("PostgREST schema reload notification sent.");
750
777
  this.rpcFunctionExists = true;
751
778
  } catch (postgresMetaError) {
@@ -753,9 +780,10 @@ var SelfhostedSupabaseClient = class _SelfhostedSupabaseClient {
753
780
  if (this.options.databaseUrl) {
754
781
  try {
755
782
  console.error("Falling back to direct DB connection...");
756
- await this.executeSqlWithPg(_SelfhostedSupabaseClient.CREATE_EXECUTE_SQL_FUNCTION);
757
- await this.executeSqlWithPg(_SelfhostedSupabaseClient.GRANT_EXECUTE_SQL_FUNCTION);
758
- await this.executeSqlWithPg("NOTIFY pgrst, 'reload schema'");
783
+ await this.executeSqlWithPg(DROP_OLD_EXECUTE_SQL_FUNCTIONS);
784
+ await this.executeSqlWithPg(CREATE_EXECUTE_SQL_FUNCTION);
785
+ await this.executeSqlWithPg(GRANT_EXECUTE_SQL_FUNCTION);
786
+ await this.executeSqlWithPg(NOTIFY_POSTGREST);
759
787
  console.error("'public.execute_sql' function created via direct DB connection.");
760
788
  this.rpcFunctionExists = true;
761
789
  } catch (pgError) {
@@ -872,6 +900,32 @@ import { z } from "zod";
872
900
  import { exec } from "node:child_process";
873
901
  import { promisify } from "node:util";
874
902
  var execAsync = promisify(exec);
903
+ async function execPostgresMetaQuery(postgresMetaUrl, apiKey, query, action) {
904
+ const response = await fetch(postgresMetaUrl, {
905
+ method: "POST",
906
+ headers: {
907
+ "Content-Type": "application/json",
908
+ "apikey": apiKey
909
+ },
910
+ body: JSON.stringify({ query })
911
+ });
912
+ const responseText = await response.text().catch(() => "");
913
+ if (!response.ok) {
914
+ throw new Error(`${action} failed (${response.status}): ${responseText || response.statusText}`);
915
+ }
916
+ const contentType = response.headers.get("content-type") || "";
917
+ if (contentType.includes("application/json") && responseText) {
918
+ let parsed;
919
+ try {
920
+ parsed = JSON.parse(responseText);
921
+ } catch {
922
+ parsed = void 0;
923
+ }
924
+ if (parsed && typeof parsed === "object" && "error" in parsed) {
925
+ throw new Error(`${action} failed: ${responseText}`);
926
+ }
927
+ }
928
+ }
875
929
  function isSqlErrorResponse(result) {
876
930
  return result.error !== void 0;
877
931
  }
@@ -902,13 +956,77 @@ async function runExternalCommand(command) {
902
956
  };
903
957
  }
904
958
  }
905
- async function executeSqlWithFallback(client, sql, readOnly = true) {
959
+ async function tryInstallRpcFunction(client) {
960
+ console.warn("Attempting to auto-install execute_sql RPC function...");
961
+ try {
962
+ const supabaseUrl = client.getSupabaseUrl();
963
+ const serviceKey = client.getServiceRoleKey();
964
+ if (!serviceKey) {
965
+ console.error("Auto-install failed: Service role key not available");
966
+ return false;
967
+ }
968
+ const postgresMetaUrl = `${supabaseUrl}/pg/query`;
969
+ console.warn("Dropping old function versions...");
970
+ await execPostgresMetaQuery(
971
+ postgresMetaUrl,
972
+ serviceKey,
973
+ DROP_OLD_EXECUTE_SQL_FUNCTIONS,
974
+ "Dropping old function versions"
975
+ );
976
+ await execPostgresMetaQuery(
977
+ postgresMetaUrl,
978
+ serviceKey,
979
+ CREATE_EXECUTE_SQL_FUNCTION,
980
+ "Creating execute_sql function"
981
+ );
982
+ await execPostgresMetaQuery(
983
+ postgresMetaUrl,
984
+ serviceKey,
985
+ GRANT_EXECUTE_SQL_FUNCTION,
986
+ "Granting execute_sql permissions"
987
+ );
988
+ try {
989
+ await execPostgresMetaQuery(
990
+ postgresMetaUrl,
991
+ serviceKey,
992
+ NOTIFY_POSTGREST,
993
+ "Notifying PostgREST to reload schema"
994
+ );
995
+ } catch (notifyError) {
996
+ console.error("Warning: Failed to notify PostgREST to reload schema. You may need to restart PostgREST or wait a few seconds:", notifyError);
997
+ }
998
+ client.setRpcAvailable(true);
999
+ console.warn("Auto-install succeeded! RPC function is now available.");
1000
+ return true;
1001
+ } catch (error) {
1002
+ console.error("Auto-install failed with exception:", error);
1003
+ console.error("Note: The function may have been partially created. Try running install_execute_sql_function manually, or restart PostgREST, then retry.");
1004
+ return false;
1005
+ }
1006
+ }
1007
+ async function executeSqlWithFallback(client, sql) {
906
1008
  if (client.isPgAvailable()) {
907
- console.info("Using direct database connection (bypassing JWT)...");
1009
+ console.warn("Using direct database connection (bypassing JWT)...");
908
1010
  return await client.executeSqlWithPg(sql);
909
1011
  }
910
- console.info("Falling back to RPC method...");
911
- return await client.executeSqlViaRpc(sql, readOnly);
1012
+ console.warn("Falling back to RPC method...");
1013
+ const result = await client.executeSqlViaRpc(sql);
1014
+ if (isSqlErrorResponse(result) && result.error.code === "MCP_CLIENT_ERROR" && result.error.message.includes("RPC function not found")) {
1015
+ console.warn("RPC function not found. Attempting auto-install and retry...");
1016
+ const installed = await tryInstallRpcFunction(client);
1017
+ if (installed) {
1018
+ await new Promise((resolve3) => setTimeout(resolve3, 1e3));
1019
+ console.warn("Retrying SQL execution after auto-install...");
1020
+ return await client.executeSqlViaRpc(sql);
1021
+ }
1022
+ return {
1023
+ error: {
1024
+ code: "MCP_CLIENT_ERROR",
1025
+ message: "execute_sql RPC function not found. Auto-install failed. Please run install_execute_sql_function tool manually, or restart PostgREST, or configure DATABASE_URL for direct connection."
1026
+ }
1027
+ };
1028
+ }
1029
+ return result;
912
1030
  }
913
1031
 
914
1032
  // src/tools/list_tables.ts
@@ -1113,17 +1231,13 @@ var applyMigrationTool = {
1113
1231
  // src/tools/execute_sql.ts
1114
1232
  import { z as z6 } from "zod";
1115
1233
  var ExecuteSqlInputSchema = z6.object({
1116
- sql: z6.string().describe("The SQL query to execute."),
1117
- read_only: z6.boolean().optional().default(false).describe("Hint for the RPC function whether the query is read-only (best effort).")
1118
- // Future enhancement: Add option to force direct connection?
1119
- // use_direct_connection: z.boolean().optional().default(false).describe('Attempt to use direct DB connection instead of RPC.'),
1234
+ sql: z6.string().describe("The SQL query to execute.")
1120
1235
  });
1121
1236
  var ExecuteSqlOutputSchema = z6.array(z6.unknown()).describe("The array of rows returned by the SQL query.");
1122
1237
  var mcpInputSchema5 = {
1123
1238
  type: "object",
1124
1239
  properties: {
1125
- sql: { type: "string", description: "The SQL query to execute." },
1126
- read_only: { type: "boolean", default: false, description: "Hint for the RPC function whether the query is read-only (best effort)." }
1240
+ sql: { type: "string", description: "The SQL query to execute." }
1127
1241
  },
1128
1242
  required: ["sql"]
1129
1243
  };
@@ -1135,8 +1249,8 @@ var executeSqlTool = {
1135
1249
  outputSchema: ExecuteSqlOutputSchema,
1136
1250
  execute: async (input, context) => {
1137
1251
  const client = context.selfhostedClient;
1138
- console.error(`Executing SQL (readOnly: ${input.read_only}): ${input.sql.substring(0, 100)}...`);
1139
- const result = await executeSqlWithFallback(client, input.sql, input.read_only);
1252
+ console.error(`Executing SQL: ${input.sql.substring(0, 100)}...`);
1253
+ const result = await executeSqlWithFallback(client, input.sql);
1140
1254
  return handleSqlResponse(result, ExecuteSqlOutputSchema);
1141
1255
  }
1142
1256
  };
@@ -2255,42 +2369,6 @@ var InstallExecuteSqlFunctionOutputZodSchema = z23.object({
2255
2369
  success: z23.boolean(),
2256
2370
  message: z23.string()
2257
2371
  });
2258
- var CREATE_EXECUTE_SQL_FUNCTION = `
2259
- CREATE OR REPLACE FUNCTION public.execute_sql(query text, read_only boolean DEFAULT true)
2260
- RETURNS jsonb
2261
- LANGUAGE plpgsql
2262
- SECURITY DEFINER
2263
- SET search_path = public
2264
- AS $$
2265
- DECLARE
2266
- result jsonb;
2267
- cleaned_query text;
2268
- BEGIN
2269
- -- Remove trailing semicolons and whitespace from query
2270
- cleaned_query := regexp_replace(trim(query), ';\\s*$', '');
2271
-
2272
- -- Execute query and convert result to JSON
2273
- IF read_only THEN
2274
- -- Read-only query
2275
- EXECUTE 'SELECT COALESCE(jsonb_agg(row_to_json(t)), ''[]''::jsonb) FROM (' || cleaned_query || ') t' INTO result;
2276
- ELSE
2277
- -- Write query (INSERT/UPDATE/DELETE)
2278
- EXECUTE cleaned_query;
2279
- result := '[]'::jsonb;
2280
- END IF;
2281
-
2282
- RETURN COALESCE(result, '[]'::jsonb);
2283
- EXCEPTION
2284
- WHEN OTHERS THEN
2285
- -- Return error information
2286
- RAISE EXCEPTION 'Error executing SQL (SQLSTATE: %): %', SQLSTATE, SQLERRM;
2287
- END;
2288
- $$;
2289
- `;
2290
- var GRANT_EXECUTE_SQL_FUNCTION = `
2291
- GRANT EXECUTE ON FUNCTION public.execute_sql(text, boolean) TO anon, authenticated, service_role;
2292
- `;
2293
- var NOTIFY_POSTGREST = `NOTIFY pgrst, 'reload schema';`;
2294
2372
  async function execute(input, context) {
2295
2373
  const client = context.selfhostedClient;
2296
2374
  context.log("Installing execute_sql RPC function...", "info");
@@ -2301,6 +2379,15 @@ async function execute(input, context) {
2301
2379
  throw new Error("Service role key is required to install the execute_sql function");
2302
2380
  }
2303
2381
  const postgresMetaUrl = `${supabaseUrl}/pg/query`;
2382
+ context.log("Dropping old function versions...", "info");
2383
+ await fetch(postgresMetaUrl, {
2384
+ method: "POST",
2385
+ headers: {
2386
+ "Content-Type": "application/json",
2387
+ "apikey": serviceKey
2388
+ },
2389
+ body: JSON.stringify({ query: DROP_OLD_EXECUTE_SQL_FUNCTIONS })
2390
+ });
2304
2391
  context.log("Creating execute_sql function...", "info");
2305
2392
  const createResponse = await fetch(postgresMetaUrl, {
2306
2393
  method: "POST",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aliyun-rds/supabase-mcp-server",
3
- "version": "1.0.8",
3
+ "version": "1.0.10",
4
4
  "description": "MCP (Model Context Protocol) server for self-hosted Supabase instances. Allows AI assistants to interact with your self-hosted Supabase database.",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
@@ -34,7 +34,6 @@
34
34
  "dependencies": {
35
35
  "@alicloud/openapi-client": "^0.4.12",
36
36
  "@alicloud/rdsai20250507": "^1.0.0",
37
- "@aliyun-rds/supabase-mcp-server": "^1.0.7",
38
37
  "@modelcontextprotocol/sdk": "latest",
39
38
  "@supabase/supabase-js": "^2.49.4",
40
39
  "commander": "^13.1.0",