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

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 (3) hide show
  1. package/README.md +222 -39
  2. package/dist/index.js +805 -99
  3. package/package.json +2 -1
package/dist/index.js CHANGED
@@ -12,14 +12,403 @@ import {
12
12
  } from "@modelcontextprotocol/sdk/types.js";
13
13
 
14
14
  // src/client/index.ts
15
- import { createClient } from "@supabase/supabase-js";
15
+ import { createClient as createClient2 } from "@supabase/supabase-js";
16
16
  import { Pool } from "pg";
17
+
18
+ // src/auth/user-auth-client.ts
19
+ import { createClient } from "@supabase/supabase-js";
20
+ var UserAuthClient = class _UserAuthClient {
21
+ supabase;
22
+ session = null;
23
+ refreshTimer = null;
24
+ config;
25
+ authStateCallbacks = [];
26
+ isDestroyed = false;
27
+ /**
28
+ * Private constructor - use create() factory method instead
29
+ */
30
+ constructor(config) {
31
+ this.config = config;
32
+ this.supabase = createClient(config.supabaseUrl, config.supabaseAnonKey, {
33
+ auth: {
34
+ autoRefreshToken: true,
35
+ persistSession: false,
36
+ // Don't persist to localStorage in server environment
37
+ detectSessionInUrl: false
38
+ }
39
+ });
40
+ this.supabase.auth.onAuthStateChange((event, session) => {
41
+ this.session = session;
42
+ this.notifyAuthStateChange(event, session);
43
+ if (event === "TOKEN_REFRESHED" && session) {
44
+ console.error("[UserAuthClient] Token refreshed successfully");
45
+ this.scheduleTokenRefresh(session);
46
+ }
47
+ });
48
+ }
49
+ /**
50
+ * Factory method to create and initialize UserAuthClient
51
+ *
52
+ * @throws Error if authentication fails
53
+ */
54
+ static async create(config) {
55
+ const client = new _UserAuthClient(config);
56
+ await client.signIn();
57
+ return client;
58
+ }
59
+ /**
60
+ * Sign in with email and password
61
+ *
62
+ * @throws Error if authentication fails
63
+ */
64
+ async signIn() {
65
+ if (this.isDestroyed) {
66
+ throw new Error("UserAuthClient has been destroyed");
67
+ }
68
+ console.error(`[UserAuthClient] Signing in as ${this.config.userEmail}...`);
69
+ const { data, error } = await this.supabase.auth.signInWithPassword({
70
+ email: this.config.userEmail,
71
+ password: this.config.userPassword
72
+ });
73
+ if (error) {
74
+ console.error(`[UserAuthClient] Sign in failed: ${error.message}`);
75
+ throw new Error(`Authentication failed: ${error.message}`);
76
+ }
77
+ if (!data.session) {
78
+ throw new Error("Authentication succeeded but no session returned");
79
+ }
80
+ this.session = data.session;
81
+ console.error(`[UserAuthClient] Successfully signed in as ${data.user?.email} (ID: ${data.user?.id})`);
82
+ this.scheduleTokenRefresh(data.session);
83
+ }
84
+ /**
85
+ * Manually refresh the current session
86
+ *
87
+ * @throws Error if refresh fails
88
+ */
89
+ async refreshToken() {
90
+ if (this.isDestroyed) {
91
+ throw new Error("UserAuthClient has been destroyed");
92
+ }
93
+ if (!this.session?.refresh_token) {
94
+ console.error("[UserAuthClient] No refresh token available, re-authenticating...");
95
+ await this.signIn();
96
+ return;
97
+ }
98
+ console.error("[UserAuthClient] Refreshing token...");
99
+ const { data, error } = await this.supabase.auth.refreshSession({
100
+ refresh_token: this.session.refresh_token
101
+ });
102
+ if (error) {
103
+ console.error(`[UserAuthClient] Token refresh failed: ${error.message}`);
104
+ console.error("[UserAuthClient] Attempting to re-authenticate...");
105
+ await this.signIn();
106
+ return;
107
+ }
108
+ if (data.session) {
109
+ this.session = data.session;
110
+ this.scheduleTokenRefresh(data.session);
111
+ }
112
+ }
113
+ /**
114
+ * Schedule automatic token refresh before expiry
115
+ */
116
+ scheduleTokenRefresh(session) {
117
+ if (this.refreshTimer) {
118
+ clearTimeout(this.refreshTimer);
119
+ this.refreshTimer = null;
120
+ }
121
+ if (!session.expires_at) {
122
+ console.error("[UserAuthClient] Session has no expiry time, skipping auto-refresh scheduling");
123
+ return;
124
+ }
125
+ const expiresAt = session.expires_at * 1e3;
126
+ const now = Date.now();
127
+ const refreshBeforeExpiry = this.config.refreshBeforeExpiry || 6e4;
128
+ const refreshIn = expiresAt - now - refreshBeforeExpiry;
129
+ if (refreshIn <= 0) {
130
+ console.error("[UserAuthClient] Token near expiry, refreshing immediately...");
131
+ this.refreshToken().catch((err) => {
132
+ console.error("[UserAuthClient] Auto-refresh failed:", err);
133
+ });
134
+ return;
135
+ }
136
+ console.error(`[UserAuthClient] Token refresh scheduled in ${Math.round(refreshIn / 1e3)} seconds`);
137
+ this.refreshTimer = setTimeout(() => {
138
+ if (!this.isDestroyed) {
139
+ this.refreshToken().catch((err) => {
140
+ console.error("[UserAuthClient] Scheduled token refresh failed:", err);
141
+ });
142
+ }
143
+ }, refreshIn);
144
+ }
145
+ /**
146
+ * Get the current access token
147
+ */
148
+ getAccessToken() {
149
+ return this.session?.access_token || null;
150
+ }
151
+ /**
152
+ * Get the current user ID
153
+ */
154
+ getUserId() {
155
+ return this.session?.user?.id || null;
156
+ }
157
+ /**
158
+ * Get the current user email
159
+ */
160
+ getUserEmail() {
161
+ return this.session?.user?.email || null;
162
+ }
163
+ /**
164
+ * Check if the client is currently authenticated
165
+ */
166
+ isAuthenticated() {
167
+ if (!this.session) return false;
168
+ if (this.session.expires_at) {
169
+ const expiresAt = this.session.expires_at * 1e3;
170
+ if (Date.now() >= expiresAt) {
171
+ return false;
172
+ }
173
+ }
174
+ return true;
175
+ }
176
+ /**
177
+ * Get the current session
178
+ */
179
+ getSession() {
180
+ return this.session;
181
+ }
182
+ /**
183
+ * Get the underlying Supabase client
184
+ *
185
+ * Note: This client uses the authenticated user's session,
186
+ * so all operations are subject to RLS policies.
187
+ */
188
+ getSupabaseClient() {
189
+ return this.supabase;
190
+ }
191
+ /**
192
+ * Register a callback for auth state changes
193
+ */
194
+ onAuthStateChange(callback) {
195
+ this.authStateCallbacks.push(callback);
196
+ return () => {
197
+ const index = this.authStateCallbacks.indexOf(callback);
198
+ if (index > -1) {
199
+ this.authStateCallbacks.splice(index, 1);
200
+ }
201
+ };
202
+ }
203
+ /**
204
+ * Notify all registered callbacks of auth state change
205
+ */
206
+ notifyAuthStateChange(event, session) {
207
+ for (const callback of this.authStateCallbacks) {
208
+ try {
209
+ callback(event, session);
210
+ } catch (err) {
211
+ console.error("[UserAuthClient] Auth state callback error:", err);
212
+ }
213
+ }
214
+ }
215
+ /**
216
+ * Sign out and clean up resources
217
+ */
218
+ async signOut() {
219
+ if (this.refreshTimer) {
220
+ clearTimeout(this.refreshTimer);
221
+ this.refreshTimer = null;
222
+ }
223
+ if (this.session) {
224
+ try {
225
+ await this.supabase.auth.signOut();
226
+ } catch (err) {
227
+ console.error("[UserAuthClient] Sign out error:", err);
228
+ }
229
+ }
230
+ this.session = null;
231
+ }
232
+ /**
233
+ * Destroy the client and clean up all resources
234
+ */
235
+ async destroy() {
236
+ this.isDestroyed = true;
237
+ await this.signOut();
238
+ this.authStateCallbacks = [];
239
+ }
240
+ };
241
+
242
+ // src/config/types.ts
243
+ var ConfigValidationError = class extends Error {
244
+ constructor(message, missingParams, conflictingModes) {
245
+ super(message);
246
+ this.missingParams = missingParams;
247
+ this.conflictingModes = conflictingModes;
248
+ this.name = "ConfigValidationError";
249
+ }
250
+ };
251
+ function isAliyunConfig(config) {
252
+ return config.mode === "aliyun" /* ALIYUN_MULTI_INSTANCE */;
253
+ }
254
+ function isSingleInstanceAdminConfig(config) {
255
+ return config.mode === "admin" /* SINGLE_INSTANCE_ADMIN */;
256
+ }
257
+ function isSingleInstanceUserConfig(config) {
258
+ return config.mode === "user" /* SINGLE_INSTANCE_USER */;
259
+ }
260
+ function getPermissionLevel(mode) {
261
+ switch (mode) {
262
+ case "aliyun" /* ALIYUN_MULTI_INSTANCE */:
263
+ return "full" /* FULL */;
264
+ case "admin" /* SINGLE_INSTANCE_ADMIN */:
265
+ return "admin" /* ADMIN */;
266
+ case "user" /* SINGLE_INSTANCE_USER */:
267
+ return "user" /* USER */;
268
+ }
269
+ }
270
+
271
+ // src/config/parser.ts
272
+ function detectModes(options) {
273
+ const supabaseUrl = options.supabaseUrl || options.url;
274
+ const supabaseAnonKey = options.supabaseAnonKey || options.anonKey;
275
+ const supabaseServiceRoleKey = options.supabaseServiceRoleKey || options.serviceKey;
276
+ return {
277
+ aliyun: {
278
+ hasRequired: !!(options.aliyunAk && options.aliyunSk),
279
+ hasAny: !!(options.aliyunAk || options.aliyunSk || options.aliyunRegion)
280
+ },
281
+ admin: {
282
+ hasRequired: !!(supabaseUrl && supabaseAnonKey && supabaseServiceRoleKey),
283
+ hasAny: !!(supabaseUrl || supabaseAnonKey || supabaseServiceRoleKey)
284
+ },
285
+ user: {
286
+ hasRequired: !!(supabaseUrl && supabaseAnonKey && options.supabaseUserEmail && options.supabaseUserPassword),
287
+ hasAny: !!(options.supabaseUserEmail || options.supabaseUserPassword)
288
+ }
289
+ };
290
+ }
291
+ function generateHelpMessage(modes, options) {
292
+ const lines = [
293
+ "Error: No valid authentication configuration found.",
294
+ "",
295
+ "Please provide ONE of the following configurations:",
296
+ ""
297
+ ];
298
+ lines.push("Mode 1 - Alibaba Cloud Multi-Instance Management:");
299
+ lines.push(" --aliyun-ak <key> --aliyun-sk <secret> [--aliyun-region <region>]");
300
+ lines.push(" Or: ALIYUN_ACCESS_KEY_ID, ALIYUN_ACCESS_KEY_SECRET");
301
+ if (modes.aliyun.hasAny && !modes.aliyun.hasRequired) {
302
+ const missing = [];
303
+ if (!options.aliyunAk) missing.push("--aliyun-ak");
304
+ if (!options.aliyunSk) missing.push("--aliyun-sk");
305
+ lines.push(` \u26A0\uFE0F Missing: ${missing.join(", ")}`);
306
+ }
307
+ lines.push("");
308
+ const supabaseUrl = options.supabaseUrl || options.url;
309
+ const supabaseAnonKey = options.supabaseAnonKey || options.anonKey;
310
+ const supabaseServiceRoleKey = options.supabaseServiceRoleKey || options.serviceKey;
311
+ lines.push("Mode 2 - Single Instance Admin Access:");
312
+ lines.push(" --supabase-url <url> --supabase-anon-key <key> --supabase-service-role-key <key>");
313
+ lines.push(" Or: SUPABASE_URL, SUPABASE_ANON_KEY, SUPABASE_SERVICE_ROLE_KEY");
314
+ if (modes.admin.hasAny && !modes.admin.hasRequired) {
315
+ const missing = [];
316
+ if (!supabaseUrl) missing.push("--supabase-url");
317
+ if (!supabaseAnonKey) missing.push("--supabase-anon-key");
318
+ if (!supabaseServiceRoleKey) missing.push("--supabase-service-role-key");
319
+ lines.push(` \u26A0\uFE0F Missing: ${missing.join(", ")}`);
320
+ }
321
+ lines.push("");
322
+ lines.push("Mode 3 - Single Instance User Access (RLS restricted):");
323
+ lines.push(" --supabase-url <url> --supabase-anon-key <key> --supabase-user-email <email> --supabase-user-password <password>");
324
+ lines.push(" Or: SUPABASE_URL, SUPABASE_ANON_KEY, SUPABASE_USER_EMAIL, SUPABASE_USER_PASSWORD");
325
+ if (modes.user.hasAny && !modes.user.hasRequired) {
326
+ const missing = [];
327
+ if (!supabaseUrl) missing.push("--supabase-url");
328
+ if (!supabaseAnonKey) missing.push("--supabase-anon-key");
329
+ if (!options.supabaseUserEmail) missing.push("--supabase-user-email");
330
+ if (!options.supabaseUserPassword) missing.push("--supabase-user-password");
331
+ lines.push(` \u26A0\uFE0F Missing: ${missing.join(", ")}`);
332
+ }
333
+ return lines.join("\n");
334
+ }
335
+ function parseConfig(options) {
336
+ const modes = detectModes(options);
337
+ const supabaseUrl = options.supabaseUrl || options.url;
338
+ const supabaseAnonKey = options.supabaseAnonKey || options.anonKey;
339
+ const supabaseServiceRoleKey = options.supabaseServiceRoleKey || options.serviceKey;
340
+ const completeModes = [];
341
+ if (modes.aliyun.hasRequired) completeModes.push("aliyun" /* ALIYUN_MULTI_INSTANCE */);
342
+ if (modes.admin.hasRequired) completeModes.push("admin" /* SINGLE_INSTANCE_ADMIN */);
343
+ if (modes.user.hasRequired) completeModes.push("user" /* SINGLE_INSTANCE_USER */);
344
+ if (completeModes.length > 1) {
345
+ console.error(`Warning: Multiple authentication modes detected: ${completeModes.join(", ")}`);
346
+ console.error("Using highest priority mode based on: Aliyun > Admin > User");
347
+ }
348
+ if (modes.aliyun.hasRequired) {
349
+ const config = {
350
+ mode: "aliyun" /* ALIYUN_MULTI_INSTANCE */,
351
+ accessKeyId: options.aliyunAk,
352
+ accessKeySecret: options.aliyunSk,
353
+ regionId: options.aliyunRegion,
354
+ enableRagAgent: options.enableRagAgent,
355
+ workspacePath: options.workspacePath,
356
+ toolsConfig: options.toolsConfig
357
+ };
358
+ return config;
359
+ }
360
+ if (modes.user.hasRequired) {
361
+ const config = {
362
+ mode: "user" /* SINGLE_INSTANCE_USER */,
363
+ supabaseUrl,
364
+ supabaseAnonKey,
365
+ userEmail: options.supabaseUserEmail,
366
+ userPassword: options.supabaseUserPassword,
367
+ enableRagAgent: options.enableRagAgent,
368
+ workspacePath: options.workspacePath,
369
+ toolsConfig: options.toolsConfig
370
+ };
371
+ return config;
372
+ }
373
+ if (modes.admin.hasRequired) {
374
+ const config = {
375
+ mode: "admin" /* SINGLE_INSTANCE_ADMIN */,
376
+ supabaseUrl,
377
+ supabaseAnonKey,
378
+ supabaseServiceRoleKey,
379
+ databaseUrl: options.dbUrl,
380
+ jwtSecret: options.jwtSecret,
381
+ enableRagAgent: options.enableRagAgent,
382
+ workspacePath: options.workspacePath,
383
+ toolsConfig: options.toolsConfig
384
+ };
385
+ return config;
386
+ }
387
+ const helpMessage = generateHelpMessage(modes, options);
388
+ throw new ConfigValidationError(helpMessage);
389
+ }
390
+ function getAuthModeDescription(mode) {
391
+ switch (mode) {
392
+ case "aliyun" /* ALIYUN_MULTI_INSTANCE */:
393
+ return "Alibaba Cloud Multi-Instance Management";
394
+ case "admin" /* SINGLE_INSTANCE_ADMIN */:
395
+ return "Single Instance Admin Access";
396
+ case "user" /* SINGLE_INSTANCE_USER */:
397
+ return "Single Instance User Access (RLS restricted)";
398
+ }
399
+ }
400
+
401
+ // src/client/index.ts
17
402
  var SelfhostedSupabaseClient = class _SelfhostedSupabaseClient {
18
403
  options;
19
404
  supabase;
20
405
  pgPool = null;
21
406
  // Lazy initialized pool for direct DB access
22
407
  rpcFunctionExists = false;
408
+ /** The authentication mode this client was created with */
409
+ authMode;
410
+ /** User authentication client (only set for AuthMode.SINGLE_INSTANCE_USER) */
411
+ userAuthClient = null;
23
412
  // SQL definition for the helper function
24
413
  static CREATE_EXECUTE_SQL_FUNCTION = `
25
414
  CREATE OR REPLACE FUNCTION public.execute_sql(query text, read_only boolean DEFAULT false)
@@ -53,24 +442,81 @@ var SelfhostedSupabaseClient = class _SelfhostedSupabaseClient {
53
442
  * Creates an instance of SelfhostedSupabaseClient.
54
443
  * Note: Call initialize() after creating the instance to check for RPC functions.
55
444
  * @param options - Configuration options for the client.
445
+ * @param authMode - The authentication mode (defaults to SINGLE_INSTANCE_ADMIN)
446
+ * @param userAuthClient - Optional UserAuthClient for user-level authentication
56
447
  */
57
- constructor(options) {
448
+ constructor(options, authMode = "admin" /* SINGLE_INSTANCE_ADMIN */, userAuthClient = null) {
58
449
  this.options = options;
59
- const apiKey = options.supabaseServiceRoleKey || options.supabaseAnonKey;
60
- this.supabase = createClient(options.supabaseUrl, apiKey, options.supabaseClientOptions);
450
+ this.authMode = authMode;
451
+ this.userAuthClient = userAuthClient;
61
452
  if (!options.supabaseUrl || !options.supabaseAnonKey) {
62
453
  throw new Error("Supabase URL and Anon Key are required.");
63
454
  }
455
+ if (userAuthClient) {
456
+ this.supabase = userAuthClient.getSupabaseClient();
457
+ } else {
458
+ const apiKey = options.supabaseServiceRoleKey || options.supabaseAnonKey;
459
+ this.supabase = createClient2(options.supabaseUrl, apiKey, options.supabaseClientOptions);
460
+ }
64
461
  }
65
462
  /**
66
463
  * Factory function to create and asynchronously initialize the client.
67
464
  * Checks for the existence of the helper RPC function.
465
+ *
466
+ * @param options - Configuration options for the client
467
+ * @param authMode - The authentication mode (defaults to SINGLE_INSTANCE_ADMIN for backward compatibility)
68
468
  */
69
- static async create(options) {
70
- const client = new _SelfhostedSupabaseClient(options);
469
+ static async create(options, authMode = "admin" /* SINGLE_INSTANCE_ADMIN */) {
470
+ const client = new _SelfhostedSupabaseClient(options, authMode);
71
471
  await client.initialize();
72
472
  return client;
73
473
  }
474
+ /**
475
+ * Factory function to create a client with user-level authentication.
476
+ * This creates a client that operates under the user's RLS permissions.
477
+ *
478
+ * @param config - User authentication configuration
479
+ * @returns A SelfhostedSupabaseClient using the authenticated user's session
480
+ * @throws Error if user authentication fails
481
+ */
482
+ static async createWithUserAuth(config) {
483
+ const userAuthClient = await UserAuthClient.create(config);
484
+ const options = {
485
+ supabaseUrl: config.supabaseUrl,
486
+ supabaseAnonKey: config.supabaseAnonKey
487
+ // Note: No service role key for user-level auth
488
+ };
489
+ const client = new _SelfhostedSupabaseClient(
490
+ options,
491
+ "user" /* SINGLE_INSTANCE_USER */,
492
+ userAuthClient
493
+ );
494
+ await client.initializeForUserAuth();
495
+ return client;
496
+ }
497
+ /**
498
+ * Initialize client for user-level authentication.
499
+ * Skips RPC function creation since user doesn't have admin permissions.
500
+ */
501
+ async initializeForUserAuth() {
502
+ console.error("Initializing SelfhostedSupabaseClient for user-level authentication...");
503
+ try {
504
+ const { error } = await this.supabase.rpc("execute_sql", {
505
+ query: "SELECT 1",
506
+ read_only: true
507
+ });
508
+ this.rpcFunctionExists = !error;
509
+ if (error) {
510
+ console.error("RPC function not available for user auth mode:", error.message);
511
+ } else {
512
+ console.error("RPC function available for user auth mode.");
513
+ }
514
+ } catch (err) {
515
+ console.error("RPC function check failed:", err);
516
+ this.rpcFunctionExists = false;
517
+ }
518
+ console.error("User auth initialization complete.");
519
+ }
74
520
  /**
75
521
  * Initializes the client by checking for the required RPC function.
76
522
  * Attempts to create the function if it doesn't exist and a service role key is provided.
@@ -367,6 +813,55 @@ var SelfhostedSupabaseClient = class _SelfhostedSupabaseClient {
367
813
  isRpcAvailable() {
368
814
  return this.rpcFunctionExists;
369
815
  }
816
+ /**
817
+ * Sets the RPC function availability status.
818
+ * Called after successfully installing the execute_sql function.
819
+ */
820
+ setRpcAvailable(available) {
821
+ this.rpcFunctionExists = available;
822
+ }
823
+ /**
824
+ * Gets the authentication mode this client was created with.
825
+ */
826
+ getAuthMode() {
827
+ return this.authMode;
828
+ }
829
+ /**
830
+ * Checks if this client is using user-level authentication.
831
+ */
832
+ isUserAuth() {
833
+ return this.authMode === "user" /* SINGLE_INSTANCE_USER */;
834
+ }
835
+ /**
836
+ * Gets the user ID if using user-level authentication.
837
+ */
838
+ getUserId() {
839
+ return this.userAuthClient?.getUserId() || null;
840
+ }
841
+ /**
842
+ * Gets the user email if using user-level authentication.
843
+ */
844
+ getUserEmail() {
845
+ return this.userAuthClient?.getUserEmail() || null;
846
+ }
847
+ /**
848
+ * Gets the UserAuthClient if using user-level authentication.
849
+ */
850
+ getUserAuthClient() {
851
+ return this.userAuthClient;
852
+ }
853
+ /**
854
+ * Cleanup resources when done with the client.
855
+ */
856
+ async destroy() {
857
+ if (this.userAuthClient) {
858
+ await this.userAuthClient.destroy();
859
+ }
860
+ if (this.pgPool) {
861
+ await this.pgPool.end();
862
+ this.pgPool = null;
863
+ }
864
+ }
370
865
  };
371
866
 
372
867
  // src/tools/list_tables.ts
@@ -377,6 +872,9 @@ import { z } from "zod";
377
872
  import { exec } from "node:child_process";
378
873
  import { promisify } from "node:util";
379
874
  var execAsync = promisify(exec);
875
+ function isSqlErrorResponse(result) {
876
+ return result.error !== void 0;
877
+ }
380
878
  function handleSqlResponse(result, schema) {
381
879
  if ("error" in result) {
382
880
  throw new Error(`SQL Error (${result.error.code}): ${result.error.message}`);
@@ -789,7 +1287,22 @@ var getDatabaseStatsTool = {
789
1287
  stats_reset::text
790
1288
  FROM pg_stat_database
791
1289
  `;
792
- const getBgWriterStatsSql = `
1290
+ const getBgWriterStatsSqlPg17 = `
1291
+ SELECT
1292
+ (SELECT num_timed::text FROM pg_stat_checkpointer) as checkpoints_timed,
1293
+ (SELECT num_requested::text FROM pg_stat_checkpointer) as checkpoints_req,
1294
+ (SELECT write_time FROM pg_stat_checkpointer) as checkpoint_write_time,
1295
+ (SELECT sync_time FROM pg_stat_checkpointer) as checkpoint_sync_time,
1296
+ (SELECT buffers_written::text FROM pg_stat_checkpointer) as buffers_checkpoint,
1297
+ buffers_clean::text,
1298
+ maxwritten_clean::text,
1299
+ NULL::text as buffers_backend,
1300
+ NULL::text as buffers_backend_fsync,
1301
+ buffers_alloc::text,
1302
+ stats_reset::text
1303
+ FROM pg_stat_bgwriter
1304
+ `;
1305
+ const getBgWriterStatsSqlPg16 = `
793
1306
  SELECT
794
1307
  checkpoints_timed::text,
795
1308
  checkpoints_req::text,
@@ -804,10 +1317,12 @@ var getDatabaseStatsTool = {
804
1317
  stats_reset::text
805
1318
  FROM pg_stat_bgwriter
806
1319
  `;
807
- const [dbStatsResult, bgWriterStatsResult] = await Promise.all([
808
- executeSqlWithFallback(client, getDbStatsSql, true),
809
- executeSqlWithFallback(client, getBgWriterStatsSql, true)
810
- ]);
1320
+ const dbStatsResult = await executeSqlWithFallback(client, getDbStatsSql, true);
1321
+ let bgWriterStatsResult = await executeSqlWithFallback(client, getBgWriterStatsSqlPg17, true);
1322
+ if (isSqlErrorResponse(bgWriterStatsResult)) {
1323
+ console.error("PostgreSQL 17+ query failed, trying PostgreSQL 16 query...");
1324
+ bgWriterStatsResult = await executeSqlWithFallback(client, getBgWriterStatsSqlPg16, true);
1325
+ }
811
1326
  const dbStats = handleSqlResponse(dbStatsResult, GetDbStatsOutputSchema.shape.database_stats);
812
1327
  const bgWriterStats = handleSqlResponse(bgWriterStatsResult, GetDbStatsOutputSchema.shape.bgwriter_stats);
813
1328
  return {
@@ -1475,6 +1990,48 @@ var updateAuthUserTool = {
1475
1990
  // src/index.ts
1476
1991
  import { z as z30 } from "zod";
1477
1992
 
1993
+ // src/tools/types.ts
1994
+ var ADMIN_ONLY_TOOLS = /* @__PURE__ */ new Set([
1995
+ // Auth management tools
1996
+ "create_auth_user",
1997
+ "delete_auth_user",
1998
+ "update_auth_user",
1999
+ "list_auth_users",
2000
+ "get_auth_user",
2001
+ // Admin configuration tools
2002
+ "get_service_key",
2003
+ "verify_jwt_secret",
2004
+ "install_execute_sql_function",
2005
+ "rebuild_hooks"
2006
+ ]);
2007
+ var ALIYUN_ONLY_TOOLS = /* @__PURE__ */ new Set([
2008
+ "list_aliyun_supabase_instances",
2009
+ "connect_to_supabase_instance",
2010
+ "get_current_supabase_instance",
2011
+ "disconnect_supabase_instance"
2012
+ ]);
2013
+ function isToolAllowed(toolName, permissionLevel) {
2014
+ switch (permissionLevel) {
2015
+ case "full" /* FULL */:
2016
+ return true;
2017
+ case "admin" /* ADMIN */:
2018
+ return !ALIYUN_ONLY_TOOLS.has(toolName);
2019
+ case "user" /* USER */:
2020
+ return !ALIYUN_ONLY_TOOLS.has(toolName) && !ADMIN_ONLY_TOOLS.has(toolName);
2021
+ default:
2022
+ return false;
2023
+ }
2024
+ }
2025
+ function getToolNotAllowedMessage(toolName, permissionLevel) {
2026
+ if (ALIYUN_ONLY_TOOLS.has(toolName)) {
2027
+ return `Tool "${toolName}" is only available in Alibaba Cloud multi-instance management mode. Please use --aliyun-ak and --aliyun-sk to enable this mode.`;
2028
+ }
2029
+ if (ADMIN_ONLY_TOOLS.has(toolName) && permissionLevel === "user" /* USER */) {
2030
+ return `Tool "${toolName}" requires admin permissions and is not available in user-level access mode. Please use --supabase-service-role-key for admin access.`;
2031
+ }
2032
+ return `Tool "${toolName}" is not allowed for the current permission level (${permissionLevel}).`;
2033
+ }
2034
+
1478
2035
  // src/tools/list_storage_buckets.ts
1479
2036
  import { z as z20 } from "zod";
1480
2037
  var BucketSchema = z20.object({
@@ -1707,14 +2264,18 @@ SET search_path = public
1707
2264
  AS $$
1708
2265
  DECLARE
1709
2266
  result jsonb;
2267
+ cleaned_query text;
1710
2268
  BEGIN
2269
+ -- Remove trailing semicolons and whitespace from query
2270
+ cleaned_query := regexp_replace(trim(query), ';\\s*$', '');
2271
+
1711
2272
  -- Execute query and convert result to JSON
1712
2273
  IF read_only THEN
1713
2274
  -- Read-only query
1714
- EXECUTE 'SELECT COALESCE(jsonb_agg(row_to_json(t)), ''[]''::jsonb) FROM (' || query || ') t' INTO result;
2275
+ EXECUTE 'SELECT COALESCE(jsonb_agg(row_to_json(t)), ''[]''::jsonb) FROM (' || cleaned_query || ') t' INTO result;
1715
2276
  ELSE
1716
2277
  -- Write query (INSERT/UPDATE/DELETE)
1717
- EXECUTE query;
2278
+ EXECUTE cleaned_query;
1718
2279
  result := '[]'::jsonb;
1719
2280
  END IF;
1720
2281
 
@@ -1722,10 +2283,7 @@ BEGIN
1722
2283
  EXCEPTION
1723
2284
  WHEN OTHERS THEN
1724
2285
  -- Return error information
1725
- RETURN jsonb_build_object(
1726
- 'error', SQLERRM,
1727
- 'code', SQLSTATE
1728
- );
2286
+ RAISE EXCEPTION 'Error executing SQL (SQLSTATE: %): %', SQLSTATE, SQLERRM;
1729
2287
  END;
1730
2288
  $$;
1731
2289
  `;
@@ -1784,6 +2342,7 @@ async function execute(input, context) {
1784
2342
  }
1785
2343
  context.log("execute_sql function installed successfully!", "info");
1786
2344
  context.log("You may need to wait a few seconds for PostgREST to reload the schema.", "info");
2345
+ client.setRpcAvailable(true);
1787
2346
  return {
1788
2347
  success: true,
1789
2348
  message: "execute_sql function installed successfully. You can now use tools that require SQL execution (list_tables, execute_sql, etc.)."
@@ -1947,18 +2506,30 @@ import * as path from "node:path";
1947
2506
  // src/integrations/rag-agent-client.ts
1948
2507
  import { Client } from "@modelcontextprotocol/sdk/client/index.js";
1949
2508
  import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
2509
+ function isUserConfig(config) {
2510
+ return "userEmail" in config && "userPassword" in config;
2511
+ }
1950
2512
  var RagAgentClient = class {
1951
2513
  config;
1952
2514
  client = null;
1953
2515
  tools = [];
2516
+ authMode;
1954
2517
  constructor(config) {
1955
2518
  this.config = config;
2519
+ this.authMode = isUserConfig(config) ? "jwt" /* JWT */ : "service_key" /* SERVICE_KEY */;
2520
+ }
2521
+ /**
2522
+ * Get the authentication mode being used
2523
+ */
2524
+ getAuthMode() {
2525
+ return this.authMode;
1956
2526
  }
1957
2527
  /**
1958
2528
  * Initialize connection to rag-agent MCP server and fetch tools
1959
2529
  */
1960
2530
  async initialize() {
1961
- console.error("Initializing RAG Agent MCP client...");
2531
+ const modeDescription = this.authMode === "jwt" /* JWT */ ? "JWT mode (user-level access)" : "Service Key mode (admin access)";
2532
+ console.error(`Initializing RAG Agent MCP client in ${modeDescription}...`);
1962
2533
  try {
1963
2534
  this.client = new Client(
1964
2535
  {
@@ -1977,19 +2548,45 @@ var RagAgentClient = class {
1977
2548
  }
1978
2549
  env.NO_PROXY = "*";
1979
2550
  env.no_proxy = "*";
2551
+ const args = [
2552
+ "--from",
2553
+ "rag-agent-mcp",
2554
+ "rag-agent",
2555
+ "--host",
2556
+ this.config.host,
2557
+ "--api-key",
2558
+ this.config.apiKey
2559
+ ];
2560
+ console.error("[RAG Agent] Configuration:");
2561
+ console.error(` Host: ${this.config.host}`);
2562
+ console.error(` API Key: ${this.config.apiKey.substring(0, 20)}...${this.config.apiKey.substring(this.config.apiKey.length - 10)}`);
2563
+ console.error(` API Key length: ${this.config.apiKey.length}`);
2564
+ try {
2565
+ const payloadBase64 = this.config.apiKey.split(".")[1];
2566
+ if (payloadBase64) {
2567
+ const payload = JSON.parse(Buffer.from(payloadBase64, "base64").toString("utf-8"));
2568
+ console.error(` JWT Role: ${payload.role || "unknown"}`);
2569
+ console.error(` JWT Issuer: ${payload.iss || "unknown"}`);
2570
+ }
2571
+ } catch (e) {
2572
+ console.error(` (Could not decode API key as JWT)`);
2573
+ }
2574
+ if (this.authMode === "jwt" /* JWT */ && isUserConfig(this.config)) {
2575
+ args.push("--user", this.config.userEmail);
2576
+ args.push("--user-password", this.config.userPassword);
2577
+ console.error(` User: ${this.config.userEmail}`);
2578
+ console.error(` Password: ${"*".repeat(this.config.userPassword.length)}`);
2579
+ }
2580
+ const maskedArgs = args.map((arg, i) => {
2581
+ if (args[i - 1] === "--api-key") return "<API_KEY>";
2582
+ if (args[i - 1] === "--user-password") return "<PASSWORD>";
2583
+ return arg;
2584
+ });
2585
+ console.error(`[RAG Agent] Command: uvx ${maskedArgs.join(" ")}`);
2586
+ console.error(`[RAG Agent] Auth mode: ${this.authMode}`);
1980
2587
  const transport = new StdioClientTransport({
1981
2588
  command: "uvx",
1982
- args: [
1983
- "--from",
1984
- "rag-agent-mcp",
1985
- "rag-agent",
1986
- "--host",
1987
- this.config.host,
1988
- "--port",
1989
- this.config.port.toString(),
1990
- "--api-key",
1991
- this.config.apiKey
1992
- ],
2589
+ args,
1993
2590
  env
1994
2591
  });
1995
2592
  await this.client.connect(transport);
@@ -2094,32 +2691,67 @@ function wrapAllRagAgentTools(ragClient) {
2094
2691
  import RdsAiPkg from "@alicloud/rdsai20250507/dist/client.js";
2095
2692
  import * as $RdsAi from "@alicloud/rdsai20250507";
2096
2693
  var RdsAiClient = RdsAiPkg.default || RdsAiPkg;
2694
+ var RDSAI_DEFAULT_ENDPOINT = "rdsai.aliyuncs.com";
2695
+ var RDSAI_CENTER_UNIT_REGIONS = /* @__PURE__ */ new Set([
2696
+ "cn-beijing",
2697
+ "cn-wulanchabu",
2698
+ "cn-hangzhou",
2699
+ "cn-shanghai",
2700
+ "cn-shenzhen",
2701
+ "cn-guangzhou"
2702
+ ]);
2703
+ function resolveRdsAiEndpoint(regionId, explicitEndpoint) {
2704
+ if (explicitEndpoint) {
2705
+ return explicitEndpoint;
2706
+ }
2707
+ if (!regionId || RDSAI_CENTER_UNIT_REGIONS.has(regionId)) {
2708
+ return RDSAI_DEFAULT_ENDPOINT;
2709
+ }
2710
+ return `rdsai.${regionId}.aliyuncs.com`;
2711
+ }
2097
2712
  var AliyunRdsAiClient = class {
2098
- client;
2099
- // RdsAi Client instance
2713
+ clientCache;
2100
2714
  config;
2101
2715
  constructor(config) {
2102
2716
  this.config = config;
2717
+ this.clientCache = /* @__PURE__ */ new Map();
2718
+ const defaultEndpoint = resolveRdsAiEndpoint(config.regionId, config.endpoint);
2719
+ this.clientCache.set(defaultEndpoint, this.createClient(defaultEndpoint, config.regionId));
2720
+ }
2721
+ createClient(endpoint, regionId) {
2103
2722
  const openApiConfig = {
2104
- accessKeyId: config.accessKeyId,
2105
- accessKeySecret: config.accessKeySecret,
2106
- endpoint: config.endpoint || "rdsai.aliyuncs.com"
2723
+ accessKeyId: this.config.accessKeyId,
2724
+ accessKeySecret: this.config.accessKeySecret,
2725
+ endpoint,
2726
+ regionId
2107
2727
  };
2108
- this.client = new RdsAiClient(openApiConfig);
2728
+ return new RdsAiClient(openApiConfig);
2729
+ }
2730
+ getClientForRegion(regionId) {
2731
+ const effectiveRegionId = regionId || this.config.regionId;
2732
+ const endpoint = resolveRdsAiEndpoint(effectiveRegionId, this.config.endpoint);
2733
+ const cached = this.clientCache.get(endpoint);
2734
+ if (cached) {
2735
+ return cached;
2736
+ }
2737
+ const client = this.createClient(endpoint, effectiveRegionId);
2738
+ this.clientCache.set(endpoint, client);
2739
+ return client;
2109
2740
  }
2110
2741
  /**
2111
2742
  * List all Supabase instances
2112
2743
  */
2113
2744
  async listSupabaseInstances(options) {
2745
+ const regionId = options?.regionId || this.config.regionId;
2114
2746
  const request = new $RdsAi.DescribeAppInstancesRequest({
2115
2747
  appType: "supabase",
2116
- regionId: options?.regionId || this.config.regionId,
2748
+ regionId,
2117
2749
  DBInstanceName: options?.dbInstanceName,
2118
2750
  pageSize: options?.pageSize || 50,
2119
2751
  pageNumber: options?.pageNumber || 1
2120
2752
  });
2121
2753
  try {
2122
- const response = await this.client.describeAppInstances(request);
2754
+ const response = await this.getClientForRegion(regionId).describeAppInstances(request);
2123
2755
  if (!response.body) {
2124
2756
  throw new Error("Empty response from DescribeAppInstances API");
2125
2757
  }
@@ -2145,19 +2777,21 @@ var AliyunRdsAiClient = class {
2145
2777
  instances
2146
2778
  };
2147
2779
  } catch (error) {
2148
- console.error("Error calling DescribeAppInstances:", error);
2780
+ console.error("Error calling DescribeAppInstances:", JSON.stringify(error, null, 2));
2781
+ console.error("Error code:", error?.code);
2782
+ console.error("Error data:", error?.data);
2149
2783
  throw new Error(`Failed to list Supabase instances: ${error instanceof Error ? error.message : String(error)}`);
2150
2784
  }
2151
2785
  }
2152
2786
  /**
2153
2787
  * Get authentication information for a specific instance
2154
2788
  */
2155
- async getInstanceAuthInfo(instanceName) {
2789
+ async getInstanceAuthInfo(instanceName, regionId) {
2156
2790
  const request = new $RdsAi.DescribeInstanceAuthInfoRequest({
2157
2791
  instanceName
2158
2792
  });
2159
2793
  try {
2160
- const response = await this.client.describeInstanceAuthInfo(request);
2794
+ const response = await this.getClientForRegion(regionId || this.config.regionId).describeInstanceAuthInfo(request);
2161
2795
  if (!response.body) {
2162
2796
  throw new Error("Empty response from DescribeInstanceAuthInfo API");
2163
2797
  }
@@ -2191,7 +2825,7 @@ var AliyunRdsAiClient = class {
2191
2825
  if (!instance) {
2192
2826
  throw new Error(`Instance ${instanceName} not found. Available instances: ${instancesResponse.instances.map((i) => i.instanceName).join(", ")}`);
2193
2827
  }
2194
- const authInfo = await this.getInstanceAuthInfo(instanceName);
2828
+ const authInfo = await this.getInstanceAuthInfo(instanceName, instance.regionId || queryRegion);
2195
2829
  const connectionString = useVpc ? instance.vpcConnectionString : instance.publicConnectionString;
2196
2830
  const supabaseUrl = connectionString.startsWith("http") ? connectionString : `http://${connectionString}`;
2197
2831
  return {
@@ -2285,7 +2919,8 @@ import { z as z27 } from "zod";
2285
2919
  import { zodToJsonSchema as zodToJsonSchema3 } from "zod-to-json-schema";
2286
2920
  var ConnectInstanceInputZodSchema = z27.object({
2287
2921
  instance_name: z27.string().describe("The instance name (e.g., ra-supabase-8moov5lxba****)"),
2288
- use_vpc: z27.boolean().optional().default(false).describe("Whether to use VPC connection instead of public connection")
2922
+ use_vpc: z27.boolean().optional().default(false).describe("Whether to use VPC connection instead of public connection"),
2923
+ region_id: z27.string().optional().describe("Region ID of the instance (e.g., cn-hangzhou, cn-chengdu). Required when connecting to an instance in a different region than the default startup region.")
2289
2924
  });
2290
2925
  var ConnectInstanceOutputZodSchema = z27.object({
2291
2926
  success: z27.boolean(),
@@ -2300,7 +2935,7 @@ async function execute3(input, aliyunClient, createSupabaseClient, setCurrentIns
2300
2935
  }
2301
2936
  log(`Connecting to Supabase instance: ${input.instance_name}...`, "info");
2302
2937
  try {
2303
- const credentials = await aliyunClient.getSupabaseCredentials(input.instance_name, input.use_vpc);
2938
+ const credentials = await aliyunClient.getSupabaseCredentials(input.instance_name, input.use_vpc, input.region_id);
2304
2939
  log(`Retrieved credentials for instance ${input.instance_name}`, "info");
2305
2940
  log(`Supabase URL: ${credentials.supabaseUrl}`, "info");
2306
2941
  const supabaseClient = await createSupabaseClient(
@@ -2410,42 +3045,63 @@ var disconnect_instance_default = disconnectSupabaseInstanceTool;
2410
3045
  // src/index.ts
2411
3046
  async function main() {
2412
3047
  const program = new Command();
2413
- program.name("self-hosted-supabase-mcp").description("MCP Server for self-hosted Supabase instances").option("--url <url>", "Supabase project URL (legacy mode)", process.env.SUPABASE_URL).option("--anon-key <key>", "Supabase anonymous key (legacy mode)", process.env.SUPABASE_ANON_KEY).option("--service-key <key>", "Supabase service role key (legacy mode, 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 (legacy mode, optional)", process.env.SUPABASE_AUTH_JWT_SECRET).option("--aliyun-ak <key>", "Alibaba Cloud Access Key ID", process.env.ALIYUN_ACCESS_KEY_ID).option("--aliyun-sk <secret>", "Alibaba Cloud Access Key Secret", process.env.ALIYUN_ACCESS_KEY_SECRET).option("--aliyun-region <region>", "Alibaba Cloud region (optional)", process.env.ALIYUN_REGION).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.').option("--enable-rag-agent", "Enable RAG Agent MCP integration (uses --url host:port and --anon-key as API key)", process.env.ENABLE_RAG_AGENT === "true").parse(process.argv);
3048
+ program.name("self-hosted-supabase-mcp").description("MCP Server for self-hosted Supabase instances with three-tier authentication").option("--url <url>", "Supabase project URL (alias for --supabase-url)", process.env.SUPABASE_URL).option("--anon-key <key>", "Supabase anonymous key (alias for --supabase-anon-key)", process.env.SUPABASE_ANON_KEY).option("--service-key <key>", "Supabase service role key (alias for --supabase-service-role-key)", process.env.SUPABASE_SERVICE_ROLE_KEY).option("--supabase-url <url>", "Supabase project URL", process.env.SUPABASE_URL).option("--supabase-anon-key <key>", "Supabase anonymous key", process.env.SUPABASE_ANON_KEY).option("--supabase-service-role-key <key>", "Supabase service role key", process.env.SUPABASE_SERVICE_ROLE_KEY).option("--supabase-user-email <email>", "User email for Supabase Auth login", process.env.SUPABASE_USER_EMAIL).option("--supabase-user-password <password>", "User password for Supabase Auth login", process.env.SUPABASE_USER_PASSWORD).option("--db-url <url>", "Direct database connection string (optional, for pg fallback)", process.env.DATABASE_URL).option("--jwt-secret <secret>", "Supabase JWT secret (optional)", process.env.SUPABASE_AUTH_JWT_SECRET).option("--aliyun-ak <key>", "Alibaba Cloud Access Key ID", process.env.ALIYUN_ACCESS_KEY_ID).option("--aliyun-sk <secret>", "Alibaba Cloud Access Key Secret", process.env.ALIYUN_ACCESS_KEY_SECRET).option("--aliyun-region <region>", "Alibaba Cloud region (optional)", process.env.ALIYUN_REGION).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.').option("--enable-rag-agent", "Enable RAG Agent MCP integration", process.env.ENABLE_RAG_AGENT === "true").parse(process.argv);
2414
3049
  const options = program.opts();
2415
- const isAliyunMode = !!(options.aliyunAk && options.aliyunSk);
2416
- const isLegacyMode = !!(options.url && options.anonKey);
2417
- if (!isAliyunMode && !isLegacyMode) {
2418
- console.error("Error: Either Alibaba Cloud credentials (--aliyun-ak, --aliyun-sk) or legacy Supabase credentials (--url, --anon-key) are required.");
2419
- throw new Error("Missing required credentials.");
2420
- }
2421
- if (isAliyunMode && isLegacyMode) {
2422
- console.error("Warning: Both Alibaba Cloud and legacy credentials provided. Using Alibaba Cloud mode.");
3050
+ let config;
3051
+ try {
3052
+ config = parseConfig(options);
3053
+ } catch (error) {
3054
+ if (error instanceof ConfigValidationError) {
3055
+ console.error(error.message);
3056
+ process.exit(1);
3057
+ }
3058
+ throw error;
2423
3059
  }
2424
- console.error("Initializing Self-Hosted Supabase MCP Server...");
3060
+ const authMode = config.mode;
3061
+ const permissionLevel = getPermissionLevel(authMode);
3062
+ console.error(`Initializing Self-Hosted Supabase MCP Server in ${getAuthModeDescription(authMode)} mode...`);
2425
3063
  try {
2426
3064
  let aliyunClient = null;
2427
- if (isAliyunMode) {
3065
+ let selfhostedClient = null;
3066
+ let userInfo;
3067
+ let currentInstanceName = null;
3068
+ let currentSupabaseClient = null;
3069
+ if (isAliyunConfig(config)) {
2428
3070
  console.error("Alibaba Cloud mode enabled, initializing client...");
2429
3071
  aliyunClient = createAliyunClient({
2430
- accessKeyId: options.aliyunAk,
2431
- accessKeySecret: options.aliyunSk,
2432
- regionId: options.aliyunRegion
3072
+ accessKeyId: config.accessKeyId,
3073
+ accessKeySecret: config.accessKeySecret,
3074
+ regionId: config.regionId
2433
3075
  });
2434
3076
  console.error("Alibaba Cloud client initialized successfully.");
2435
- }
2436
- let selfhostedClient = null;
2437
- if (isLegacyMode && !isAliyunMode) {
2438
- selfhostedClient = await SelfhostedSupabaseClient.create({
2439
- supabaseUrl: options.url,
2440
- supabaseAnonKey: options.anonKey,
2441
- supabaseServiceRoleKey: options.serviceKey,
2442
- databaseUrl: options.dbUrl,
2443
- jwtSecret: options.jwtSecret
3077
+ } else if (isSingleInstanceUserConfig(config)) {
3078
+ console.error("Single Instance User mode enabled, authenticating...");
3079
+ selfhostedClient = await SelfhostedSupabaseClient.createWithUserAuth({
3080
+ supabaseUrl: config.supabaseUrl,
3081
+ supabaseAnonKey: config.supabaseAnonKey,
3082
+ userEmail: config.userEmail,
3083
+ userPassword: config.userPassword
2444
3084
  });
2445
- console.error("Supabase client initialized successfully (legacy mode).");
3085
+ currentSupabaseClient = selfhostedClient;
3086
+ const userId = selfhostedClient.getUserId();
3087
+ const email = selfhostedClient.getUserEmail();
3088
+ if (userId && email) {
3089
+ userInfo = { userId, email };
3090
+ }
3091
+ console.error(`Authenticated as user: ${email} (ID: ${userId})`);
3092
+ console.error("Note: Access is restricted by Row Level Security (RLS) policies.");
3093
+ } else if (isSingleInstanceAdminConfig(config)) {
3094
+ console.error("Single Instance Admin mode enabled, initializing client...");
3095
+ selfhostedClient = await SelfhostedSupabaseClient.create({
3096
+ supabaseUrl: config.supabaseUrl,
3097
+ supabaseAnonKey: config.supabaseAnonKey,
3098
+ supabaseServiceRoleKey: config.supabaseServiceRoleKey,
3099
+ databaseUrl: config.databaseUrl,
3100
+ jwtSecret: config.jwtSecret
3101
+ }, "admin" /* SINGLE_INSTANCE_ADMIN */);
3102
+ currentSupabaseClient = selfhostedClient;
3103
+ console.error("Supabase client initialized successfully (admin mode).");
2446
3104
  }
2447
- let currentInstanceName = null;
2448
- let currentSupabaseClient = selfhostedClient;
2449
3105
  const getCurrentInstanceName = () => currentInstanceName;
2450
3106
  const setCurrentInstance = (instanceName, client) => {
2451
3107
  currentInstanceName = instanceName;
@@ -2462,25 +3118,53 @@ async function main() {
2462
3118
  supabaseServiceRoleKey: serviceKey,
2463
3119
  databaseUrl: options.dbUrl,
2464
3120
  jwtSecret
2465
- });
3121
+ }, "aliyun" /* ALIYUN_MULTI_INSTANCE */);
2466
3122
  };
2467
3123
  let ragAgentClient = null;
2468
3124
  const initializeRagAgent = async (supabaseClient) => {
2469
- if (!options.enableRagAgent) {
3125
+ if (!config.enableRagAgent) {
2470
3126
  return null;
2471
3127
  }
2472
3128
  try {
2473
3129
  console.error("Initializing RAG Agent client...");
2474
- const urlToUse = supabaseClient.getSupabaseUrl();
2475
- const anonKeyToUse = supabaseClient.getAnonKey();
2476
- const urlObj = new URL(urlToUse);
2477
- const host = urlObj.hostname;
3130
+ const supabaseUrl = supabaseClient.getSupabaseUrl();
3131
+ const urlObj = new URL(supabaseUrl);
2478
3132
  const port = urlObj.port ? parseInt(urlObj.port, 10) : urlObj.protocol === "https:" ? 443 : 80;
2479
- const client = await createRagAgentClient({
2480
- host,
2481
- port,
2482
- apiKey: anonKeyToUse
2483
- });
3133
+ const hostUrl = `${urlObj.protocol}//${urlObj.hostname}:${port}`;
3134
+ console.error(`RAG Agent host: ${hostUrl}`);
3135
+ let client;
3136
+ if (isSingleInstanceUserConfig(config)) {
3137
+ const anonKey = supabaseClient.getAnonKey();
3138
+ console.error("[initializeRagAgent] Tier 3: User mode detected");
3139
+ console.error(`[initializeRagAgent] Using anon key for JWT auth`);
3140
+ console.error(`[initializeRagAgent] Anon key preview: ${anonKey.substring(0, 20)}...`);
3141
+ console.error(`[initializeRagAgent] User email: ${config.userEmail}`);
3142
+ client = await createRagAgentClient({
3143
+ host: hostUrl,
3144
+ apiKey: anonKey,
3145
+ userEmail: config.userEmail,
3146
+ userPassword: config.userPassword
3147
+ });
3148
+ } else {
3149
+ const serviceKey = supabaseClient.getServiceRoleKey();
3150
+ const anonKey = supabaseClient.getAnonKey();
3151
+ console.error("[initializeRagAgent] Tier 1/2: Admin mode detected");
3152
+ console.error(`[initializeRagAgent] Service key available: ${!!serviceKey}`);
3153
+ if (serviceKey) {
3154
+ console.error(`[initializeRagAgent] Service key preview: ${serviceKey.substring(0, 20)}...`);
3155
+ }
3156
+ console.error(`[initializeRagAgent] Anon key preview: ${anonKey.substring(0, 20)}...`);
3157
+ if (!serviceKey) {
3158
+ console.error("[initializeRagAgent] WARNING: Service role key not available!");
3159
+ console.error("[initializeRagAgent] This may cause 401 errors if RAG Agent requires admin access");
3160
+ }
3161
+ const apiKey = serviceKey || anonKey;
3162
+ console.error(`[initializeRagAgent] Using: ${serviceKey ? "service_role key" : "anon key (fallback)"}`);
3163
+ client = await createRagAgentClient({
3164
+ host: hostUrl,
3165
+ apiKey
3166
+ });
3167
+ }
2484
3168
  console.error("RAG Agent client initialized successfully.");
2485
3169
  return client;
2486
3170
  } catch (error) {
@@ -2489,14 +3173,14 @@ async function main() {
2489
3173
  return null;
2490
3174
  }
2491
3175
  };
2492
- if (options.enableRagAgent && isLegacyMode && selfhostedClient) {
2493
- ragAgentClient = await initializeRagAgent(selfhostedClient);
2494
- } else if (options.enableRagAgent && isAliyunMode) {
3176
+ if (config.enableRagAgent && currentSupabaseClient) {
3177
+ ragAgentClient = await initializeRagAgent(currentSupabaseClient);
3178
+ } else if (config.enableRagAgent && isAliyunConfig(config)) {
2495
3179
  console.error("RAG Agent integration enabled.");
2496
3180
  console.error("RAG Agent tools will be available after connecting to a Supabase instance.");
2497
3181
  }
2498
3182
  const wrappedAliyunTools = {};
2499
- if (isAliyunMode) {
3183
+ if (isAliyunConfig(config)) {
2500
3184
  wrappedAliyunTools[list_instances_default.name] = {
2501
3185
  ...list_instances_default,
2502
3186
  execute: async (input, context) => {
@@ -2517,7 +3201,7 @@ async function main() {
2517
3201
  setCurrentInstance,
2518
3202
  context.log
2519
3203
  );
2520
- if (result.success && currentSupabaseClient && options.enableRagAgent && !ragAgentClient) {
3204
+ if (result.success && currentSupabaseClient && config.enableRagAgent && !ragAgentClient) {
2521
3205
  ragAgentClient = await initializeRagAgent(currentSupabaseClient);
2522
3206
  }
2523
3207
  return result;
@@ -2573,17 +3257,16 @@ async function main() {
2573
3257
  [install_execute_sql_function_default.name]: install_execute_sql_function_default,
2574
3258
  [searchDocsTool.name]: searchDocsTool
2575
3259
  };
2576
- if (options.enableRagAgent) {
3260
+ if (config.enableRagAgent) {
2577
3261
  if (ragAgentClient) {
2578
3262
  const ragTools = wrapAllRagAgentTools(ragAgentClient);
2579
3263
  Object.assign(availableTools, ragTools);
2580
3264
  console.error(`Added ${Object.keys(ragTools).length} RAG Agent tools to available tools.`);
2581
- } else if (isAliyunMode) {
3265
+ } else if (isAliyunConfig(config)) {
2582
3266
  const dummyRagTools = await (async () => {
2583
3267
  try {
2584
3268
  const tempClient = await createRagAgentClient({
2585
- host: "localhost",
2586
- port: 80,
3269
+ host: "http://localhost:80",
2587
3270
  apiKey: "dummy"
2588
3271
  });
2589
3272
  const tools = wrapAllRagAgentTools(tempClient);
@@ -2609,8 +3292,16 @@ async function main() {
2609
3292
  console.error(`Added ${Object.keys(dummyRagTools).length} RAG Agent tools (will be initialized after connection).`);
2610
3293
  }
2611
3294
  }
2612
- let registeredTools = { ...availableTools };
2613
- const toolsConfigPath = options.toolsConfig;
3295
+ const permissionFilteredTools = {};
3296
+ for (const [toolName, tool] of Object.entries(availableTools)) {
3297
+ if (isToolAllowed(toolName, permissionLevel)) {
3298
+ permissionFilteredTools[toolName] = tool;
3299
+ } else {
3300
+ console.error(`Tool ${toolName} disabled (not allowed for ${permissionLevel} permission level).`);
3301
+ }
3302
+ }
3303
+ let registeredTools = { ...permissionFilteredTools };
3304
+ const toolsConfigPath = config.toolsConfig;
2614
3305
  let enabledToolNames = null;
2615
3306
  if (toolsConfigPath) {
2616
3307
  try {
@@ -2631,27 +3322,31 @@ async function main() {
2631
3322
  enabledToolNames = new Set(toolNames.map((name) => name.trim()).filter((name) => name.length > 0));
2632
3323
  } catch (error) {
2633
3324
  console.error(`Error loading or parsing tool config file '${toolsConfigPath}':`, error instanceof Error ? error.message : String(error));
2634
- console.error("Falling back to enabling all tools due to config error.");
3325
+ console.error("Falling back to enabling all permission-allowed tools due to config error.");
2635
3326
  enabledToolNames = null;
2636
3327
  }
2637
3328
  }
2638
3329
  if (enabledToolNames !== null) {
2639
3330
  console.error(`Whitelisting tools based on config: ${Array.from(enabledToolNames).join(", ")}`);
2640
3331
  registeredTools = {};
2641
- for (const toolName in availableTools) {
3332
+ for (const toolName in permissionFilteredTools) {
2642
3333
  if (enabledToolNames.has(toolName)) {
2643
- registeredTools[toolName] = availableTools[toolName];
3334
+ registeredTools[toolName] = permissionFilteredTools[toolName];
2644
3335
  } else {
2645
3336
  console.error(`Tool ${toolName} disabled (not in config whitelist).`);
2646
3337
  }
2647
3338
  }
2648
3339
  for (const requestedName of enabledToolNames) {
2649
- if (!availableTools[requestedName]) {
2650
- console.warn(`Warning: Tool "${requestedName}" specified in config file not found.`);
3340
+ if (!permissionFilteredTools[requestedName]) {
3341
+ if (availableTools[requestedName]) {
3342
+ console.warn(`Warning: Tool "${requestedName}" is not available for the current permission level.`);
3343
+ } else {
3344
+ console.warn(`Warning: Tool "${requestedName}" specified in config file not found.`);
3345
+ }
2651
3346
  }
2652
3347
  }
2653
3348
  } else {
2654
- console.error("No valid --tools-config specified or error loading config, enabling all available tools.");
3349
+ console.error("No valid --tools-config specified or error loading config, enabling all permission-allowed tools.");
2655
3350
  }
2656
3351
  const capabilitiesTools = {};
2657
3352
  for (const tool of Object.values(registeredTools)) {
@@ -2666,6 +3361,7 @@ async function main() {
2666
3361
  };
2667
3362
  }
2668
3363
  const capabilities = { tools: capabilitiesTools };
3364
+ console.error(`Registered ${Object.keys(registeredTools).length} tools.`);
2669
3365
  console.error("Initializing MCP Server...");
2670
3366
  const server = new Server(
2671
3367
  {
@@ -2684,6 +3380,10 @@ async function main() {
2684
3380
  const tool = registeredTools[toolName];
2685
3381
  if (!tool) {
2686
3382
  if (availableTools[toolName]) {
3383
+ const message = getToolNotAllowedMessage(toolName, permissionLevel);
3384
+ throw new McpError(ErrorCode.MethodNotFound, message);
3385
+ }
3386
+ if (permissionFilteredTools[toolName]) {
2687
3387
  throw new McpError(ErrorCode.MethodNotFound, `Tool "${toolName}" is available but not enabled by the current server configuration.`);
2688
3388
  }
2689
3389
  throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${toolName}`);
@@ -2709,7 +3409,7 @@ async function main() {
2709
3409
  "get_current_supabase_instance",
2710
3410
  "disconnect_supabase_instance"
2711
3411
  ].includes(toolName);
2712
- if (isAliyunMode && !isAliyunManagementTool && !currentSupabaseClient) {
3412
+ if (isAliyunConfig(config) && !isAliyunManagementTool && !currentSupabaseClient) {
2713
3413
  throw new McpError(
2714
3414
  ErrorCode.InvalidRequest,
2715
3415
  "Not connected to any Supabase instance. Please use connect_to_supabase_instance tool first."
@@ -2717,7 +3417,10 @@ async function main() {
2717
3417
  }
2718
3418
  const context = {
2719
3419
  selfhostedClient: currentSupabaseClient,
2720
- workspacePath: options.workspacePath,
3420
+ workspacePath: config.workspacePath || process.cwd(),
3421
+ authMode,
3422
+ permissionLevel,
3423
+ userInfo,
2721
3424
  log: (message, level = "info") => {
2722
3425
  console.error(`[${level.toUpperCase()}] ${message}`);
2723
3426
  }
@@ -2756,6 +3459,9 @@ async function main() {
2756
3459
  if (ragAgentClient) {
2757
3460
  await ragAgentClient.close();
2758
3461
  }
3462
+ if (selfhostedClient) {
3463
+ await selfhostedClient.destroy();
3464
+ }
2759
3465
  };
2760
3466
  process.on("SIGINT", async () => {
2761
3467
  await cleanup();