@aliyun-rds/supabase-mcp-server 1.0.6 → 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.
- package/README.md +222 -39
- package/dist/index.js +720 -80
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -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
|
-
###
|
|
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
|
-
|
|
217
|
+
### Single Instance User Mode Setup (RLS Restricted)
|
|
164
218
|
|
|
165
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
293
|
+
Legacy flag aliases are still accepted: `--url`, `--anon-key`, `--service-key`, `--db-url`, `--jwt-secret`.
|
|
186
294
|
|
|
187
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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-
|
|
248
|
-
"--aliyun-
|
|
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,
|
|
425
|
+
For Claude Desktop, open Settings → Developer → enable "Custom MCP Servers", then add one configuration matching your mode:
|
|
272
426
|
|
|
273
|
-
1
|
|
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
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
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
|
-
|
|
60
|
-
this.
|
|
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 (' ||
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
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
|
-
|
|
2416
|
-
|
|
2417
|
-
|
|
2418
|
-
|
|
2419
|
-
|
|
2420
|
-
|
|
2421
|
-
|
|
2422
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
2431
|
-
accessKeySecret:
|
|
2432
|
-
regionId:
|
|
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
|
-
|
|
2437
|
-
|
|
2438
|
-
|
|
2439
|
-
|
|
2440
|
-
|
|
2441
|
-
|
|
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
|
-
|
|
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 (!
|
|
3059
|
+
if (!config.enableRagAgent) {
|
|
2470
3060
|
return null;
|
|
2471
3061
|
}
|
|
2472
3062
|
try {
|
|
2473
3063
|
console.error("Initializing RAG Agent client...");
|
|
2474
|
-
const
|
|
2475
|
-
const
|
|
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
|
|
2480
|
-
|
|
2481
|
-
|
|
2482
|
-
|
|
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 (
|
|
2493
|
-
ragAgentClient = await initializeRagAgent(
|
|
2494
|
-
} else if (
|
|
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 (
|
|
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 &&
|
|
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 (
|
|
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 (
|
|
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
|
-
|
|
2613
|
-
const
|
|
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
|
|
3266
|
+
for (const toolName in permissionFilteredTools) {
|
|
2642
3267
|
if (enabledToolNames.has(toolName)) {
|
|
2643
|
-
registeredTools[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 (!
|
|
2650
|
-
|
|
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
|
|
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 (
|
|
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:
|
|
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.
|
|
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": {
|