@aliyun-rds/supabase-mcp-server 1.0.5 → 1.0.7

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 +223 -40
  2. package/dist/index.js +720 -80
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  MCP (Model Context Protocol) server for Supabase instances running on Aliyun RDS. Enables AI assistants like Claude to interact with your Supabase instance hosted on Aliyun cloud infrastructure.
4
4
 
5
- Adapted from the original self-hosted version by [HenkDz](https://github.com/HenkDz), optimized for Aliyun RDS deployment.
5
+ This project was adapted from the original work by [HenkDz](https://github.com/HenkDz) and is now maintained by the Aliyun RDS Research and Development Team.
6
6
 
7
7
  ## Alibaba Cloud Operation
8
8
 
@@ -70,6 +70,23 @@ For example, when you ask an AI assistant "List all tables in my database", it w
70
70
  3. Execute the tool against your Supabase instance
71
71
  4. Present the results in a human-readable format
72
72
 
73
+ ## Authentication Modes & Permission Levels
74
+
75
+ The server supports three authentication modes with automatic tool filtering:
76
+
77
+ - **Mode 1 – Alibaba Cloud Multi-Instance (AuthMode `aliyun`, permission `full`)**
78
+ Use `--aliyun-ak`, `--aliyun-sk`, and `--aliyun-region` to discover and manage multiple Aliyun RDS Supabase instances. Grants access to all tools, including Aliyun management tools.
79
+ - **Mode 2 – Single Instance Admin (AuthMode `admin`, permission `admin`)**
80
+ Use `--supabase-url`, `--supabase-anon-key`, and `--supabase-service-role-key` for a single project. Admin-only tools stay available; Aliyun management tools are hidden.
81
+ - **Mode 3 – Single Instance User (AuthMode `user`, permission `user`)**
82
+ Use `--supabase-url`, `--supabase-anon-key`, plus `--supabase-user-email` and `--supabase-user-password`. Runs under the user’s RLS scope; admin tools and Aliyun management tools are disabled.
83
+
84
+ Tool visibility is enforced automatically:
85
+ - **Aliyun-only tools** (e.g., `list_aliyun_supabase_instances`, `connect_to_supabase_instance`, `get_current_supabase_instance`, `disconnect_supabase_instance`) require `full` permissions.
86
+ - **Admin-only tools** (auth management, `get_service_key`, `verify_jwt_secret`, `install_execute_sql_function`, `rebuild_hooks`) require `full` or `admin` permissions.
87
+
88
+ Mode selection priority: if multiple configurations are provided, the server picks Aliyun first; if Aliyun is absent and user credentials are complete, user mode is selected; otherwise admin mode is used. A warning is logged when multiple modes are detected.
89
+
73
90
  ## Setup and Installation
74
91
 
75
92
  ### Alibaba Cloud Mode Setup
@@ -158,16 +175,87 @@ Or use environment variables:
158
175
  3. **Use tools**: Now you can use all Supabase tools (list_tables, execute_sql, etc.)
159
176
  4. **Disconnect** (optional): Use `disconnect_supabase_instance` to switch instances
160
177
 
161
- ### Additional Installation Options
178
+ ### Single Instance Admin Mode Setup
179
+
180
+ ```bash
181
+ npx @aliyun-rds/supabase-mcp-server \
182
+ --supabase-url https://<your-project>.supabase.co \
183
+ --supabase-anon-key <anon-key> \
184
+ --supabase-service-role-key <service-role-key> \
185
+ [--db-url <postgres-connection-string>] \
186
+ [--jwt-secret <jwt-secret>] \
187
+ [--enable-rag-agent]
188
+ ```
189
+
190
+ Environment variable alternative:
191
+
192
+ ```bash
193
+ SUPABASE_URL=https://<your-project>.supabase.co \
194
+ SUPABASE_ANON_KEY=<anon-key> \
195
+ SUPABASE_SERVICE_ROLE_KEY=<service-role-key> \
196
+ supabase-mcp
197
+ ```
198
+
199
+ Claude Desktop / Cursor JSON 示例:
200
+
201
+ ```json
202
+ {
203
+ "mcpServers": {
204
+ "supabase-admin": {
205
+ "command": "npx",
206
+ "args": [
207
+ "@aliyun-rds/supabase-mcp-server",
208
+ "--supabase-url", "https://<your-project>.supabase.co",
209
+ "--supabase-anon-key", "<anon-key>",
210
+ "--supabase-service-role-key", "<service-role-key>"
211
+ ]
212
+ }
213
+ }
214
+ }
215
+ ```
162
216
 
163
- #### Install via Smithery
217
+ ### Single Instance User Mode Setup (RLS Restricted)
164
218
 
165
- Install the Aliyun RDS Supabase MCP Server for Claude Desktop automatically via [Smithery](https://smithery.ai/server/@HenkDz/selfhosted-supabase-mcp):
219
+ ```bash
220
+ npx @aliyun-rds/supabase-mcp-server \
221
+ --supabase-url https://<your-project>.supabase.co \
222
+ --supabase-anon-key <anon-key> \
223
+ --supabase-user-email <user-email> \
224
+ --supabase-user-password <user-password> \
225
+ [--enable-rag-agent]
226
+ ```
227
+
228
+ Environment variable alternative:
166
229
 
167
230
  ```bash
168
- npx -y @smithery/cli install @HenkDz/selfhosted-supabase-mcp --client claude
231
+ SUPABASE_URL=https://<your-project>.supabase.co \
232
+ SUPABASE_ANON_KEY=<anon-key> \
233
+ SUPABASE_USER_EMAIL=<user-email> \
234
+ SUPABASE_USER_PASSWORD=<user-password> \
235
+ supabase-mcp
236
+ ```
237
+
238
+ Claude Desktop / Cursor JSON 示例:
239
+
240
+ ```json
241
+ {
242
+ "mcpServers": {
243
+ "supabase-user": {
244
+ "command": "npx",
245
+ "args": [
246
+ "@aliyun-rds/supabase-mcp-server",
247
+ "--supabase-url", "https://<your-project>.supabase.co",
248
+ "--supabase-anon-key", "<anon-key>",
249
+ "--supabase-user-email", "<user-email>",
250
+ "--supabase-user-password", "<user-password>"
251
+ ]
252
+ }
253
+ }
254
+ }
169
255
  ```
170
256
 
257
+ ### Additional Installation Options
258
+
171
259
  #### Global Installation
172
260
 
173
261
  ```bash
@@ -180,20 +268,45 @@ supabase-mcp \
180
268
 
181
269
  ## Configuration
182
270
 
183
- The server now relies exclusively on Alibaba Cloud credentials. Provide them via command-line arguments or environment variables (CLI arguments take precedence). Once authenticated, the MCP server automatically retrieves the Supabase URL, anon key, service key, database connection string, and JWT secret for the selected Aliyun RDS instance—no manual entry is needed.
271
+ Choose one configuration path. CLI flags override environment variables.
272
+
273
+ ### Mode 1 — Alibaba Cloud Multi-Instance (permission: full)
274
+
275
+ Required:
276
+ - `--aliyun-ak <key>` or `ALIYUN_ACCESS_KEY_ID=<key>`
277
+ - `--aliyun-sk <secret>` or `ALIYUN_ACCESS_KEY_SECRET=<secret>`
278
+ - `--aliyun-region <region>` or `ALIYUN_REGION=<region>` (e.g., `cn-hangzhou`, `cn-beijing`; required for discovery)
279
+
280
+ Behavior: pulls Supabase URL/keys/DB URL/JWT secret from Aliyun for the selected instance.
281
+
282
+ ### Mode 2 — Single Instance Admin (permission: admin)
283
+
284
+ Required:
285
+ - `--supabase-url <url>` or `SUPABASE_URL`
286
+ - `--supabase-anon-key <key>` or `SUPABASE_ANON_KEY`
287
+ - `--supabase-service-role-key <key>` or `SUPABASE_SERVICE_ROLE_KEY`
288
+
289
+ Optional:
290
+ - `--db-url <postgres-connection-string>` or `DB_URL`
291
+ - `--jwt-secret <secret>` or `JWT_SECRET`
184
292
 
185
- ### Required Parameters
293
+ Legacy flag aliases are still accepted: `--url`, `--anon-key`, `--service-key`, `--db-url`, `--jwt-secret`.
186
294
 
187
- * `--aliyun-ak <key>` or `ALIYUN_ACCESS_KEY_ID=<key>`: Alibaba Cloud Access Key ID.
188
- * `--aliyun-sk <secret>` or `ALIYUN_ACCESS_KEY_SECRET=<secret>`: Alibaba Cloud Access Key Secret.
189
- * `--aliyun-region <region>` or `ALIYUN_REGION=<region>`: **Mandatory** region where your Supabase instances live (e.g., `cn-hangzhou`, `cn-beijing`). Without this, the OpenAPI call cannot list instances. This value becomes the default region, but tools like `list_aliyun_supabase_instances` accept a `region_id` parameter so you can query other regions on demand.
295
+ ### Mode 3 Single Instance User (permission: user, RLS enforced)
190
296
 
191
- ### Optional Parameters
297
+ Required:
298
+ - `--supabase-url <url>` or `SUPABASE_URL`
299
+ - `--supabase-anon-key <key>` or `SUPABASE_ANON_KEY`
300
+ - `--supabase-user-email <email>` or `SUPABASE_USER_EMAIL`
301
+ - `--supabase-user-password <password>` or `SUPABASE_USER_PASSWORD`
192
302
 
193
- * `--tools-config <path>`: JSON file specifying which tools to enable (whitelist). Format: `{"enabledTools": ["tool_name_1", "tool_name_2"]}`.
194
- * `--enable-rag-agent` or `ENABLE_RAG_AGENT=true`: Enable RAG Agent MCP integration. When enabled, the server resolves the Supabase host/port from the selected instance and uses the retrieved anon key as the API key for rag-agent; no manual `--url` or `--anon-key` flags are needed.
303
+ Behavior: operates under the provided user's RLS policies; admin-only and Aliyun management tools are filtered out.
195
304
 
196
- Legacy CLI options (`--url`, `--anon-key`, `--service-key`, `--db-url`, `--jwt-secret`) have been removed from the user-facing surface because Aliyun RDS now supplies those details automatically.
305
+ ### Common Options
306
+
307
+ - `--tools-config <path>`: JSON file specifying which tools to enable (whitelist). Format: `{"enabledTools": ["tool_name_1", "tool_name_2"]}`.
308
+ - `--enable-rag-agent` or `ENABLE_RAG_AGENT=true`: Enable RAG Agent MCP integration. When enabled, the server resolves the Supabase host/port from the selected instance and uses the retrieved anon key as the API key for rag-agent.
309
+ - `--workspace-path <path>`: Workspace root for file operations (optional).
197
310
 
198
311
  ### RAG Agent Integration
199
312
 
@@ -234,7 +347,10 @@ npx @aliyun-rds/supabase-mcp-server \
234
347
  ### Cursor
235
348
 
236
349
  1. Create or open the file `.cursor/mcp.json` in your project root.
237
- 2. Add the following configuration:
350
+ 2. Add one of the following configurations based on your authentication mode:
351
+
352
+ **Mode 1 (Aliyun multi-instance, permission: full)**
353
+ Grants all tools, including Aliyun management.
238
354
 
239
355
  ```json
240
356
  {
@@ -243,12 +359,9 @@ npx @aliyun-rds/supabase-mcp-server \
243
359
  "command": "npx",
244
360
  "args": [
245
361
  "@aliyun-rds/supabase-mcp-server",
246
- "--aliyun-ak",
247
- "<your-access-key-id>",
248
- "--aliyun-sk",
249
- "<your-access-key-secret>",
250
- "--aliyun-region",
251
- "cn-hangzhou",
362
+ "--aliyun-ak", "<your-access-key-id>",
363
+ "--aliyun-sk", "<your-access-key-secret>",
364
+ "--aliyun-region", "cn-hangzhou",
252
365
  "--enable-rag-agent"
253
366
  ],
254
367
  "env": {
@@ -261,6 +374,47 @@ npx @aliyun-rds/supabase-mcp-server \
261
374
  }
262
375
  ```
263
376
 
377
+ **Mode 2 (Single instance admin, permission: admin)**
378
+ Admin tools available; Aliyun management tools hidden.
379
+
380
+ ```json
381
+ {
382
+ "mcpServers": {
383
+ "supabase-admin": {
384
+ "command": "npx",
385
+ "args": [
386
+ "@aliyun-rds/supabase-mcp-server",
387
+ "--supabase-url", "https://<your-project>.supabase.co",
388
+ "--supabase-anon-key", "<anon-key>",
389
+ "--supabase-service-role-key", "<service-role-key>",
390
+ "--enable-rag-agent"
391
+ ]
392
+ }
393
+ }
394
+ }
395
+ ```
396
+
397
+ **Mode 3 (Single instance user, permission: user, RLS enforced)**
398
+ Runs under user RLS; admin/Aliyun tools disabled.
399
+
400
+ ```json
401
+ {
402
+ "mcpServers": {
403
+ "supabase-user": {
404
+ "command": "npx",
405
+ "args": [
406
+ "@aliyun-rds/supabase-mcp-server",
407
+ "--supabase-url", "https://<your-project>.supabase.co",
408
+ "--supabase-anon-key", "<anon-key>",
409
+ "--supabase-user-email", "<user-email>",
410
+ "--supabase-user-password", "<user-password>",
411
+ "--enable-rag-agent"
412
+ ]
413
+ }
414
+ }
415
+ }
416
+ ```
417
+
264
418
  **Important Notes for RAG Agent:**
265
419
  - RAG Agent tools stay inactive until you call `connect_to_supabase_instance` and select an Aliyun RDS Supabase project.
266
420
  - Switching instances automatically re-initializes the rag-agent connection with the new host/port.
@@ -268,27 +422,56 @@ npx @aliyun-rds/supabase-mcp-server \
268
422
 
269
423
  ### Claude for Desktop
270
424
 
271
- For Claude Desktop, you can add the following to your configuration:
425
+ For Claude Desktop, open Settings → Developer → enable "Custom MCP Servers", then add one configuration matching your mode:
272
426
 
273
- 1. Open Claude Desktop Settings
274
- 2. Go to "Developer" and enable "Custom MCP Servers"
275
- 3. Add a new server with the following configuration:
427
+ **Mode 1 (Aliyun, permission: full)**
276
428
 
277
- ```json
278
- {
279
- "name": "Aliyun Supabase",
280
- "command": "npx",
281
- "args": [
282
- "@aliyun-rds/supabase-mcp-server",
283
- "--aliyun-ak",
284
- "YOUR_ACCESS_KEY_ID",
285
- "--aliyun-sk",
286
- "YOUR_ACCESS_KEY_SECRET",
287
- "--aliyun-region",
288
- "cn-hangzhou"
289
- ]
290
- }
291
- ```
429
+ ```json
430
+ {
431
+ "name": "Aliyun Supabase",
432
+ "command": "npx",
433
+ "args": [
434
+ "@aliyun-rds/supabase-mcp-server",
435
+ "--aliyun-ak", "YOUR_ACCESS_KEY_ID",
436
+ "--aliyun-sk", "YOUR_ACCESS_KEY_SECRET",
437
+ "--aliyun-region", "cn-hangzhou",
438
+ "--enable-rag-agent"
439
+ ]
440
+ }
441
+ ```
442
+
443
+ **Mode 2 (Single instance admin, permission: admin)**
444
+
445
+ ```json
446
+ {
447
+ "name": "Supabase Admin",
448
+ "command": "npx",
449
+ "args": [
450
+ "@aliyun-rds/supabase-mcp-server",
451
+ "--supabase-url", "https://<your-project>.supabase.co",
452
+ "--supabase-anon-key", "<anon-key>",
453
+ "--supabase-service-role-key", "<service-role-key>",
454
+ "--enable-rag-agent"
455
+ ]
456
+ }
457
+ ```
458
+
459
+ **Mode 3 (Single instance user, permission: user, RLS enforced)**
460
+
461
+ ```json
462
+ {
463
+ "name": "Supabase User",
464
+ "command": "npx",
465
+ "args": [
466
+ "@aliyun-rds/supabase-mcp-server",
467
+ "--supabase-url", "https://<your-project>.supabase.co",
468
+ "--supabase-anon-key", "<anon-key>",
469
+ "--supabase-user-email", "<user-email>",
470
+ "--supabase-user-password", "<user-password>",
471
+ "--enable-rag-agent"
472
+ ]
473
+ }
474
+ ```
292
475
 
293
476
  ### Other MCP-Compatible Tools
294
477
 
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,48 @@ var SelfhostedSupabaseClient = class _SelfhostedSupabaseClient {
367
813
  isRpcAvailable() {
368
814
  return this.rpcFunctionExists;
369
815
  }
816
+ /**
817
+ * Gets the authentication mode this client was created with.
818
+ */
819
+ getAuthMode() {
820
+ return this.authMode;
821
+ }
822
+ /**
823
+ * Checks if this client is using user-level authentication.
824
+ */
825
+ isUserAuth() {
826
+ return this.authMode === "user" /* SINGLE_INSTANCE_USER */;
827
+ }
828
+ /**
829
+ * Gets the user ID if using user-level authentication.
830
+ */
831
+ getUserId() {
832
+ return this.userAuthClient?.getUserId() || null;
833
+ }
834
+ /**
835
+ * Gets the user email if using user-level authentication.
836
+ */
837
+ getUserEmail() {
838
+ return this.userAuthClient?.getUserEmail() || null;
839
+ }
840
+ /**
841
+ * Gets the UserAuthClient if using user-level authentication.
842
+ */
843
+ getUserAuthClient() {
844
+ return this.userAuthClient;
845
+ }
846
+ /**
847
+ * Cleanup resources when done with the client.
848
+ */
849
+ async destroy() {
850
+ if (this.userAuthClient) {
851
+ await this.userAuthClient.destroy();
852
+ }
853
+ if (this.pgPool) {
854
+ await this.pgPool.end();
855
+ this.pgPool = null;
856
+ }
857
+ }
370
858
  };
371
859
 
372
860
  // src/tools/list_tables.ts
@@ -1475,6 +1963,48 @@ var updateAuthUserTool = {
1475
1963
  // src/index.ts
1476
1964
  import { z as z30 } from "zod";
1477
1965
 
1966
+ // src/tools/types.ts
1967
+ var ADMIN_ONLY_TOOLS = /* @__PURE__ */ new Set([
1968
+ // Auth management tools
1969
+ "create_auth_user",
1970
+ "delete_auth_user",
1971
+ "update_auth_user",
1972
+ "list_auth_users",
1973
+ "get_auth_user",
1974
+ // Admin configuration tools
1975
+ "get_service_key",
1976
+ "verify_jwt_secret",
1977
+ "install_execute_sql_function",
1978
+ "rebuild_hooks"
1979
+ ]);
1980
+ var ALIYUN_ONLY_TOOLS = /* @__PURE__ */ new Set([
1981
+ "list_aliyun_supabase_instances",
1982
+ "connect_to_supabase_instance",
1983
+ "get_current_supabase_instance",
1984
+ "disconnect_supabase_instance"
1985
+ ]);
1986
+ function isToolAllowed(toolName, permissionLevel) {
1987
+ switch (permissionLevel) {
1988
+ case "full" /* FULL */:
1989
+ return true;
1990
+ case "admin" /* ADMIN */:
1991
+ return !ALIYUN_ONLY_TOOLS.has(toolName);
1992
+ case "user" /* USER */:
1993
+ return !ALIYUN_ONLY_TOOLS.has(toolName) && !ADMIN_ONLY_TOOLS.has(toolName);
1994
+ default:
1995
+ return false;
1996
+ }
1997
+ }
1998
+ function getToolNotAllowedMessage(toolName, permissionLevel) {
1999
+ if (ALIYUN_ONLY_TOOLS.has(toolName)) {
2000
+ return `Tool "${toolName}" is only available in Alibaba Cloud multi-instance management mode. Please use --aliyun-ak and --aliyun-sk to enable this mode.`;
2001
+ }
2002
+ if (ADMIN_ONLY_TOOLS.has(toolName) && permissionLevel === "user" /* USER */) {
2003
+ return `Tool "${toolName}" requires admin permissions and is not available in user-level access mode. Please use --supabase-service-role-key for admin access.`;
2004
+ }
2005
+ return `Tool "${toolName}" is not allowed for the current permission level (${permissionLevel}).`;
2006
+ }
2007
+
1478
2008
  // src/tools/list_storage_buckets.ts
1479
2009
  import { z as z20 } from "zod";
1480
2010
  var BucketSchema = z20.object({
@@ -1707,14 +2237,18 @@ SET search_path = public
1707
2237
  AS $$
1708
2238
  DECLARE
1709
2239
  result jsonb;
2240
+ cleaned_query text;
1710
2241
  BEGIN
2242
+ -- Remove trailing semicolons and whitespace from query
2243
+ cleaned_query := regexp_replace(trim(query), ';\\s*$', '');
2244
+
1711
2245
  -- Execute query and convert result to JSON
1712
2246
  IF read_only THEN
1713
2247
  -- Read-only query
1714
- EXECUTE 'SELECT COALESCE(jsonb_agg(row_to_json(t)), ''[]''::jsonb) FROM (' || query || ') t' INTO result;
2248
+ EXECUTE 'SELECT COALESCE(jsonb_agg(row_to_json(t)), ''[]''::jsonb) FROM (' || cleaned_query || ') t' INTO result;
1715
2249
  ELSE
1716
2250
  -- Write query (INSERT/UPDATE/DELETE)
1717
- EXECUTE query;
2251
+ EXECUTE cleaned_query;
1718
2252
  result := '[]'::jsonb;
1719
2253
  END IF;
1720
2254
 
@@ -1722,10 +2256,7 @@ BEGIN
1722
2256
  EXCEPTION
1723
2257
  WHEN OTHERS THEN
1724
2258
  -- Return error information
1725
- RETURN jsonb_build_object(
1726
- 'error', SQLERRM,
1727
- 'code', SQLSTATE
1728
- );
2259
+ RAISE EXCEPTION 'Error executing SQL (SQLSTATE: %): %', SQLSTATE, SQLERRM;
1729
2260
  END;
1730
2261
  $$;
1731
2262
  `;
@@ -1947,18 +2478,30 @@ import * as path from "node:path";
1947
2478
  // src/integrations/rag-agent-client.ts
1948
2479
  import { Client } from "@modelcontextprotocol/sdk/client/index.js";
1949
2480
  import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
2481
+ function isUserConfig(config) {
2482
+ return "userEmail" in config && "userPassword" in config;
2483
+ }
1950
2484
  var RagAgentClient = class {
1951
2485
  config;
1952
2486
  client = null;
1953
2487
  tools = [];
2488
+ authMode;
1954
2489
  constructor(config) {
1955
2490
  this.config = config;
2491
+ this.authMode = isUserConfig(config) ? "jwt" /* JWT */ : "service_key" /* SERVICE_KEY */;
2492
+ }
2493
+ /**
2494
+ * Get the authentication mode being used
2495
+ */
2496
+ getAuthMode() {
2497
+ return this.authMode;
1956
2498
  }
1957
2499
  /**
1958
2500
  * Initialize connection to rag-agent MCP server and fetch tools
1959
2501
  */
1960
2502
  async initialize() {
1961
- console.error("Initializing RAG Agent MCP client...");
2503
+ const modeDescription = this.authMode === "jwt" /* JWT */ ? "JWT mode (user-level access)" : "Service Key mode (admin access)";
2504
+ console.error(`Initializing RAG Agent MCP client in ${modeDescription}...`);
1962
2505
  try {
1963
2506
  this.client = new Client(
1964
2507
  {
@@ -1977,19 +2520,45 @@ var RagAgentClient = class {
1977
2520
  }
1978
2521
  env.NO_PROXY = "*";
1979
2522
  env.no_proxy = "*";
2523
+ const args = [
2524
+ "--from",
2525
+ "rag-agent-mcp",
2526
+ "rag-agent",
2527
+ "--host",
2528
+ this.config.host,
2529
+ "--api-key",
2530
+ this.config.apiKey
2531
+ ];
2532
+ console.error("[RAG Agent] Configuration:");
2533
+ console.error(` Host: ${this.config.host}`);
2534
+ console.error(` API Key: ${this.config.apiKey.substring(0, 20)}...${this.config.apiKey.substring(this.config.apiKey.length - 10)}`);
2535
+ console.error(` API Key length: ${this.config.apiKey.length}`);
2536
+ try {
2537
+ const payloadBase64 = this.config.apiKey.split(".")[1];
2538
+ if (payloadBase64) {
2539
+ const payload = JSON.parse(Buffer.from(payloadBase64, "base64").toString("utf-8"));
2540
+ console.error(` JWT Role: ${payload.role || "unknown"}`);
2541
+ console.error(` JWT Issuer: ${payload.iss || "unknown"}`);
2542
+ }
2543
+ } catch (e) {
2544
+ console.error(` (Could not decode API key as JWT)`);
2545
+ }
2546
+ if (this.authMode === "jwt" /* JWT */ && isUserConfig(this.config)) {
2547
+ args.push("--user", this.config.userEmail);
2548
+ args.push("--user-password", this.config.userPassword);
2549
+ console.error(` User: ${this.config.userEmail}`);
2550
+ console.error(` Password: ${"*".repeat(this.config.userPassword.length)}`);
2551
+ }
2552
+ const maskedArgs = args.map((arg, i) => {
2553
+ if (args[i - 1] === "--api-key") return "<API_KEY>";
2554
+ if (args[i - 1] === "--user-password") return "<PASSWORD>";
2555
+ return arg;
2556
+ });
2557
+ console.error(`[RAG Agent] Command: uvx ${maskedArgs.join(" ")}`);
2558
+ console.error(`[RAG Agent] Auth mode: ${this.authMode}`);
1980
2559
  const transport = new StdioClientTransport({
1981
2560
  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
- ],
2561
+ args,
1993
2562
  env
1994
2563
  });
1995
2564
  await this.client.connect(transport);
@@ -2410,42 +2979,63 @@ var disconnect_instance_default = disconnectSupabaseInstanceTool;
2410
2979
  // src/index.ts
2411
2980
  async function main() {
2412
2981
  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);
2982
+ 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
2983
  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.");
2984
+ let config;
2985
+ try {
2986
+ config = parseConfig(options);
2987
+ } catch (error) {
2988
+ if (error instanceof ConfigValidationError) {
2989
+ console.error(error.message);
2990
+ process.exit(1);
2991
+ }
2992
+ throw error;
2423
2993
  }
2424
- console.error("Initializing Self-Hosted Supabase MCP Server...");
2994
+ const authMode = config.mode;
2995
+ const permissionLevel = getPermissionLevel(authMode);
2996
+ console.error(`Initializing Self-Hosted Supabase MCP Server in ${getAuthModeDescription(authMode)} mode...`);
2425
2997
  try {
2426
2998
  let aliyunClient = null;
2427
- if (isAliyunMode) {
2999
+ let selfhostedClient = null;
3000
+ let userInfo;
3001
+ let currentInstanceName = null;
3002
+ let currentSupabaseClient = null;
3003
+ if (isAliyunConfig(config)) {
2428
3004
  console.error("Alibaba Cloud mode enabled, initializing client...");
2429
3005
  aliyunClient = createAliyunClient({
2430
- accessKeyId: options.aliyunAk,
2431
- accessKeySecret: options.aliyunSk,
2432
- regionId: options.aliyunRegion
3006
+ accessKeyId: config.accessKeyId,
3007
+ accessKeySecret: config.accessKeySecret,
3008
+ regionId: config.regionId
2433
3009
  });
2434
3010
  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
3011
+ } else if (isSingleInstanceUserConfig(config)) {
3012
+ console.error("Single Instance User mode enabled, authenticating...");
3013
+ selfhostedClient = await SelfhostedSupabaseClient.createWithUserAuth({
3014
+ supabaseUrl: config.supabaseUrl,
3015
+ supabaseAnonKey: config.supabaseAnonKey,
3016
+ userEmail: config.userEmail,
3017
+ userPassword: config.userPassword
2444
3018
  });
2445
- console.error("Supabase client initialized successfully (legacy mode).");
3019
+ currentSupabaseClient = selfhostedClient;
3020
+ const userId = selfhostedClient.getUserId();
3021
+ const email = selfhostedClient.getUserEmail();
3022
+ if (userId && email) {
3023
+ userInfo = { userId, email };
3024
+ }
3025
+ console.error(`Authenticated as user: ${email} (ID: ${userId})`);
3026
+ console.error("Note: Access is restricted by Row Level Security (RLS) policies.");
3027
+ } else if (isSingleInstanceAdminConfig(config)) {
3028
+ console.error("Single Instance Admin mode enabled, initializing client...");
3029
+ selfhostedClient = await SelfhostedSupabaseClient.create({
3030
+ supabaseUrl: config.supabaseUrl,
3031
+ supabaseAnonKey: config.supabaseAnonKey,
3032
+ supabaseServiceRoleKey: config.supabaseServiceRoleKey,
3033
+ databaseUrl: config.databaseUrl,
3034
+ jwtSecret: config.jwtSecret
3035
+ }, "admin" /* SINGLE_INSTANCE_ADMIN */);
3036
+ currentSupabaseClient = selfhostedClient;
3037
+ console.error("Supabase client initialized successfully (admin mode).");
2446
3038
  }
2447
- let currentInstanceName = null;
2448
- let currentSupabaseClient = selfhostedClient;
2449
3039
  const getCurrentInstanceName = () => currentInstanceName;
2450
3040
  const setCurrentInstance = (instanceName, client) => {
2451
3041
  currentInstanceName = instanceName;
@@ -2462,25 +3052,53 @@ async function main() {
2462
3052
  supabaseServiceRoleKey: serviceKey,
2463
3053
  databaseUrl: options.dbUrl,
2464
3054
  jwtSecret
2465
- });
3055
+ }, "aliyun" /* ALIYUN_MULTI_INSTANCE */);
2466
3056
  };
2467
3057
  let ragAgentClient = null;
2468
3058
  const initializeRagAgent = async (supabaseClient) => {
2469
- if (!options.enableRagAgent) {
3059
+ if (!config.enableRagAgent) {
2470
3060
  return null;
2471
3061
  }
2472
3062
  try {
2473
3063
  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;
3064
+ const supabaseUrl = supabaseClient.getSupabaseUrl();
3065
+ const urlObj = new URL(supabaseUrl);
2478
3066
  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
- });
3067
+ const hostUrl = `${urlObj.protocol}//${urlObj.hostname}:${port}`;
3068
+ console.error(`RAG Agent host: ${hostUrl}`);
3069
+ let client;
3070
+ if (isSingleInstanceUserConfig(config)) {
3071
+ const anonKey = supabaseClient.getAnonKey();
3072
+ console.error("[initializeRagAgent] Tier 3: User mode detected");
3073
+ console.error(`[initializeRagAgent] Using anon key for JWT auth`);
3074
+ console.error(`[initializeRagAgent] Anon key preview: ${anonKey.substring(0, 20)}...`);
3075
+ console.error(`[initializeRagAgent] User email: ${config.userEmail}`);
3076
+ client = await createRagAgentClient({
3077
+ host: hostUrl,
3078
+ apiKey: anonKey,
3079
+ userEmail: config.userEmail,
3080
+ userPassword: config.userPassword
3081
+ });
3082
+ } else {
3083
+ const serviceKey = supabaseClient.getServiceRoleKey();
3084
+ const anonKey = supabaseClient.getAnonKey();
3085
+ console.error("[initializeRagAgent] Tier 1/2: Admin mode detected");
3086
+ console.error(`[initializeRagAgent] Service key available: ${!!serviceKey}`);
3087
+ if (serviceKey) {
3088
+ console.error(`[initializeRagAgent] Service key preview: ${serviceKey.substring(0, 20)}...`);
3089
+ }
3090
+ console.error(`[initializeRagAgent] Anon key preview: ${anonKey.substring(0, 20)}...`);
3091
+ if (!serviceKey) {
3092
+ console.error("[initializeRagAgent] WARNING: Service role key not available!");
3093
+ console.error("[initializeRagAgent] This may cause 401 errors if RAG Agent requires admin access");
3094
+ }
3095
+ const apiKey = serviceKey || anonKey;
3096
+ console.error(`[initializeRagAgent] Using: ${serviceKey ? "service_role key" : "anon key (fallback)"}`);
3097
+ client = await createRagAgentClient({
3098
+ host: hostUrl,
3099
+ apiKey
3100
+ });
3101
+ }
2484
3102
  console.error("RAG Agent client initialized successfully.");
2485
3103
  return client;
2486
3104
  } catch (error) {
@@ -2489,14 +3107,14 @@ async function main() {
2489
3107
  return null;
2490
3108
  }
2491
3109
  };
2492
- if (options.enableRagAgent && isLegacyMode && selfhostedClient) {
2493
- ragAgentClient = await initializeRagAgent(selfhostedClient);
2494
- } else if (options.enableRagAgent && isAliyunMode) {
3110
+ if (config.enableRagAgent && currentSupabaseClient) {
3111
+ ragAgentClient = await initializeRagAgent(currentSupabaseClient);
3112
+ } else if (config.enableRagAgent && isAliyunConfig(config)) {
2495
3113
  console.error("RAG Agent integration enabled.");
2496
3114
  console.error("RAG Agent tools will be available after connecting to a Supabase instance.");
2497
3115
  }
2498
3116
  const wrappedAliyunTools = {};
2499
- if (isAliyunMode) {
3117
+ if (isAliyunConfig(config)) {
2500
3118
  wrappedAliyunTools[list_instances_default.name] = {
2501
3119
  ...list_instances_default,
2502
3120
  execute: async (input, context) => {
@@ -2517,7 +3135,7 @@ async function main() {
2517
3135
  setCurrentInstance,
2518
3136
  context.log
2519
3137
  );
2520
- if (result.success && currentSupabaseClient && options.enableRagAgent && !ragAgentClient) {
3138
+ if (result.success && currentSupabaseClient && config.enableRagAgent && !ragAgentClient) {
2521
3139
  ragAgentClient = await initializeRagAgent(currentSupabaseClient);
2522
3140
  }
2523
3141
  return result;
@@ -2573,17 +3191,16 @@ async function main() {
2573
3191
  [install_execute_sql_function_default.name]: install_execute_sql_function_default,
2574
3192
  [searchDocsTool.name]: searchDocsTool
2575
3193
  };
2576
- if (options.enableRagAgent) {
3194
+ if (config.enableRagAgent) {
2577
3195
  if (ragAgentClient) {
2578
3196
  const ragTools = wrapAllRagAgentTools(ragAgentClient);
2579
3197
  Object.assign(availableTools, ragTools);
2580
3198
  console.error(`Added ${Object.keys(ragTools).length} RAG Agent tools to available tools.`);
2581
- } else if (isAliyunMode) {
3199
+ } else if (isAliyunConfig(config)) {
2582
3200
  const dummyRagTools = await (async () => {
2583
3201
  try {
2584
3202
  const tempClient = await createRagAgentClient({
2585
- host: "localhost",
2586
- port: 80,
3203
+ host: "http://localhost:80",
2587
3204
  apiKey: "dummy"
2588
3205
  });
2589
3206
  const tools = wrapAllRagAgentTools(tempClient);
@@ -2609,8 +3226,16 @@ async function main() {
2609
3226
  console.error(`Added ${Object.keys(dummyRagTools).length} RAG Agent tools (will be initialized after connection).`);
2610
3227
  }
2611
3228
  }
2612
- let registeredTools = { ...availableTools };
2613
- const toolsConfigPath = options.toolsConfig;
3229
+ const permissionFilteredTools = {};
3230
+ for (const [toolName, tool] of Object.entries(availableTools)) {
3231
+ if (isToolAllowed(toolName, permissionLevel)) {
3232
+ permissionFilteredTools[toolName] = tool;
3233
+ } else {
3234
+ console.error(`Tool ${toolName} disabled (not allowed for ${permissionLevel} permission level).`);
3235
+ }
3236
+ }
3237
+ let registeredTools = { ...permissionFilteredTools };
3238
+ const toolsConfigPath = config.toolsConfig;
2614
3239
  let enabledToolNames = null;
2615
3240
  if (toolsConfigPath) {
2616
3241
  try {
@@ -2631,27 +3256,31 @@ async function main() {
2631
3256
  enabledToolNames = new Set(toolNames.map((name) => name.trim()).filter((name) => name.length > 0));
2632
3257
  } catch (error) {
2633
3258
  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.");
3259
+ console.error("Falling back to enabling all permission-allowed tools due to config error.");
2635
3260
  enabledToolNames = null;
2636
3261
  }
2637
3262
  }
2638
3263
  if (enabledToolNames !== null) {
2639
3264
  console.error(`Whitelisting tools based on config: ${Array.from(enabledToolNames).join(", ")}`);
2640
3265
  registeredTools = {};
2641
- for (const toolName in availableTools) {
3266
+ for (const toolName in permissionFilteredTools) {
2642
3267
  if (enabledToolNames.has(toolName)) {
2643
- registeredTools[toolName] = availableTools[toolName];
3268
+ registeredTools[toolName] = permissionFilteredTools[toolName];
2644
3269
  } else {
2645
3270
  console.error(`Tool ${toolName} disabled (not in config whitelist).`);
2646
3271
  }
2647
3272
  }
2648
3273
  for (const requestedName of enabledToolNames) {
2649
- if (!availableTools[requestedName]) {
2650
- console.warn(`Warning: Tool "${requestedName}" specified in config file not found.`);
3274
+ if (!permissionFilteredTools[requestedName]) {
3275
+ if (availableTools[requestedName]) {
3276
+ console.warn(`Warning: Tool "${requestedName}" is not available for the current permission level.`);
3277
+ } else {
3278
+ console.warn(`Warning: Tool "${requestedName}" specified in config file not found.`);
3279
+ }
2651
3280
  }
2652
3281
  }
2653
3282
  } else {
2654
- console.error("No valid --tools-config specified or error loading config, enabling all available tools.");
3283
+ console.error("No valid --tools-config specified or error loading config, enabling all permission-allowed tools.");
2655
3284
  }
2656
3285
  const capabilitiesTools = {};
2657
3286
  for (const tool of Object.values(registeredTools)) {
@@ -2666,6 +3295,7 @@ async function main() {
2666
3295
  };
2667
3296
  }
2668
3297
  const capabilities = { tools: capabilitiesTools };
3298
+ console.error(`Registered ${Object.keys(registeredTools).length} tools.`);
2669
3299
  console.error("Initializing MCP Server...");
2670
3300
  const server = new Server(
2671
3301
  {
@@ -2684,6 +3314,10 @@ async function main() {
2684
3314
  const tool = registeredTools[toolName];
2685
3315
  if (!tool) {
2686
3316
  if (availableTools[toolName]) {
3317
+ const message = getToolNotAllowedMessage(toolName, permissionLevel);
3318
+ throw new McpError(ErrorCode.MethodNotFound, message);
3319
+ }
3320
+ if (permissionFilteredTools[toolName]) {
2687
3321
  throw new McpError(ErrorCode.MethodNotFound, `Tool "${toolName}" is available but not enabled by the current server configuration.`);
2688
3322
  }
2689
3323
  throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${toolName}`);
@@ -2709,7 +3343,7 @@ async function main() {
2709
3343
  "get_current_supabase_instance",
2710
3344
  "disconnect_supabase_instance"
2711
3345
  ].includes(toolName);
2712
- if (isAliyunMode && !isAliyunManagementTool && !currentSupabaseClient) {
3346
+ if (isAliyunConfig(config) && !isAliyunManagementTool && !currentSupabaseClient) {
2713
3347
  throw new McpError(
2714
3348
  ErrorCode.InvalidRequest,
2715
3349
  "Not connected to any Supabase instance. Please use connect_to_supabase_instance tool first."
@@ -2717,7 +3351,10 @@ async function main() {
2717
3351
  }
2718
3352
  const context = {
2719
3353
  selfhostedClient: currentSupabaseClient,
2720
- workspacePath: options.workspacePath,
3354
+ workspacePath: config.workspacePath || process.cwd(),
3355
+ authMode,
3356
+ permissionLevel,
3357
+ userInfo,
2721
3358
  log: (message, level = "info") => {
2722
3359
  console.error(`[${level.toUpperCase()}] ${message}`);
2723
3360
  }
@@ -2756,6 +3393,9 @@ async function main() {
2756
3393
  if (ragAgentClient) {
2757
3394
  await ragAgentClient.close();
2758
3395
  }
3396
+ if (selfhostedClient) {
3397
+ await selfhostedClient.destroy();
3398
+ }
2759
3399
  };
2760
3400
  process.on("SIGINT", async () => {
2761
3401
  await cleanup();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aliyun-rds/supabase-mcp-server",
3
- "version": "1.0.5",
3
+ "version": "1.0.7",
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": {