@motherbase/cli 0.2.6 → 0.2.9
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/LICENSE +21 -21
- package/README.md +8 -1
- package/bin/.motherbase/config.json +5 -0
- package/bin/motherbase.js +10 -10
- package/package.json +4 -4
- package/src/api.js +39 -16
- package/src/commands/_util.js +106 -106
- package/src/commands/backups.js +16 -16
- package/src/commands/branches.js +113 -113
- package/src/commands/completion.js +90 -90
- package/src/commands/config.js +47 -47
- package/src/commands/cron.js +65 -65
- package/src/commands/db-catalog.js +39 -1
- package/src/commands/db.js +103 -103
- package/src/commands/domains.js +124 -124
- package/src/commands/gen.js +178 -172
- package/src/commands/import.js +55 -55
- package/src/commands/index.js +86 -85
- package/src/commands/inspect.js +178 -178
- package/src/commands/keys.js +72 -72
- package/src/commands/link.js +35 -35
- package/src/commands/local.js +101 -24
- package/src/commands/login.js +9 -3
- package/src/commands/logout.js +16 -16
- package/src/commands/mcp.js +61 -61
- package/src/commands/migrate.js +0 -5
- package/src/commands/migration.js +1 -1
- package/src/commands/network.js +83 -83
- package/src/commands/orgs.js +40 -40
- package/src/commands/postgres-config.js +113 -113
- package/src/commands/projects.js +131 -130
- package/src/commands/queues.js +133 -133
- package/src/commands/realtime.js +35 -35
- package/src/commands/rls.js +176 -176
- package/src/commands/secrets.js +66 -66
- package/src/commands/signing.js +130 -130
- package/src/commands/snippets.js +44 -44
- package/src/commands/status.js +42 -42
- package/src/commands/storage.js +261 -261
- package/src/commands/test.js +9 -9
- package/src/commands/unlink.js +15 -15
- package/src/commands/users.js +29 -1
- package/src/commands/vault.js +82 -82
- package/src/commands/webhooks.js +62 -62
- package/src/commands/whoami.js +20 -20
- package/src/commands/wrappers.js +150 -150
- package/src/config.js +98 -98
- package/src/context.js +106 -106
- package/src/edge-templates.js +194 -194
- package/src/format.js +92 -92
- package/src/inspect/csv.js +22 -22
- package/src/inspect/queries.js +562 -562
- package/src/main.js +145 -66
- package/src/mcp/platform-tools.js +233 -233
- package/src/mcp/protocol.js +152 -152
- package/src/mcp/storage-tools.js +93 -93
- package/src/mcp/tools.js +478 -478
- package/src/migration/commands.js +2 -2
- package/src/migration/files.js +13 -0
- package/src/prompt.js +91 -91
- package/src/storage/remote.js +62 -62
- package/src/ui.js +111 -111
package/src/mcp/tools.js
CHANGED
|
@@ -1,478 +1,478 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
// The tools an agent gets over MCP.
|
|
4
|
-
//
|
|
5
|
-
// Every one is a call through the CLI's own HTTP layer: `api.adminRequest` for
|
|
6
|
-
// the project, `api.consoleRequest` for the account tools in platform-tools.js,
|
|
7
|
-
// the same paths every CLI command takes. That is the whole design: authentication,
|
|
8
|
-
// console-proxy vs direct mode, and error mapping are solved once in the CLI and
|
|
9
|
-
// reused here, so an MCP session cannot reach anything a `motherbase` command
|
|
10
|
-
// could not, and cannot drift from it either.
|
|
11
|
-
//
|
|
12
|
-
// The gap this closes is the one self-hosters name about Supabase's MCP: "DDL
|
|
13
|
-
// statements not supported". `apply_migration` runs DDL, because the admin API
|
|
14
|
-
// this is built on already does.
|
|
15
|
-
|
|
16
|
-
const api = require('../api');
|
|
17
|
-
const platform = require('./platform-tools');
|
|
18
|
-
const storage = require('./storage-tools');
|
|
19
|
-
const { consoleProjectPath } = require('../commands/_util');
|
|
20
|
-
|
|
21
|
-
const str = description => ({ type: 'string', description });
|
|
22
|
-
|
|
23
|
-
/** Tools that only read. Safe to run without confirmation. */
|
|
24
|
-
function readTools(ctx) {
|
|
25
|
-
return [
|
|
26
|
-
{
|
|
27
|
-
name: 'list_tables',
|
|
28
|
-
description: 'List the tables in the project database, with their columns and row counts.',
|
|
29
|
-
inputSchema: { type: 'object', properties: {} },
|
|
30
|
-
run: () => api.adminRequest(ctx, 'GET', '/tables'),
|
|
31
|
-
},
|
|
32
|
-
{
|
|
33
|
-
name: 'describe_table',
|
|
34
|
-
description: 'Columns, types, nullability, defaults, primary key and indexes of one table.',
|
|
35
|
-
inputSchema: {
|
|
36
|
-
type: 'object',
|
|
37
|
-
properties: { table: str('Table name, in the public schema.') },
|
|
38
|
-
required: ['table'],
|
|
39
|
-
},
|
|
40
|
-
run: async args => {
|
|
41
|
-
const [tables, indexes] = await Promise.all([
|
|
42
|
-
api.adminRequest(ctx, 'GET', '/schema/tables'),
|
|
43
|
-
api.adminRequest(ctx, 'GET', `/schema/tables/${encodeURIComponent(args.table)}/indexes`),
|
|
44
|
-
]);
|
|
45
|
-
const list = Array.isArray(tables) ? tables : tables && tables.tables;
|
|
46
|
-
const table = (list || []).find(t => t.name === args.table || t.table === args.table);
|
|
47
|
-
if (!table) throw new Error(`No table named "${args.table}" in the public schema.`);
|
|
48
|
-
return { ...table, indexes };
|
|
49
|
-
},
|
|
50
|
-
},
|
|
51
|
-
{
|
|
52
|
-
name: 'list_policies',
|
|
53
|
-
description:
|
|
54
|
-
'Row level security state for every table: whether RLS is on, and the policies attached with their USING and WITH CHECK expressions.',
|
|
55
|
-
inputSchema: { type: 'object', properties: {} },
|
|
56
|
-
run: () => api.adminRequest(ctx, 'GET', '/rls/tables'),
|
|
57
|
-
},
|
|
58
|
-
{
|
|
59
|
-
name: 'simulate_rls',
|
|
60
|
-
description:
|
|
61
|
-
'Run a SELECT as a given caller and report the rows they can see plus the policies that decided. The statement really executes, inside a transaction that is always rolled back.',
|
|
62
|
-
inputSchema: {
|
|
63
|
-
type: 'object',
|
|
64
|
-
properties: {
|
|
65
|
-
sql: str('A single SELECT statement.'),
|
|
66
|
-
role: str("'anon' or 'authenticated'. Ignored when userId is given."),
|
|
67
|
-
userId: str('Run as this existing user instead of a bare role.'),
|
|
68
|
-
},
|
|
69
|
-
required: ['sql'],
|
|
70
|
-
},
|
|
71
|
-
run: args =>
|
|
72
|
-
api.adminRequest(ctx, 'POST', '/rls/simulate', {
|
|
73
|
-
body: { sql: args.sql, role: args.role, userId: args.userId },
|
|
74
|
-
}),
|
|
75
|
-
},
|
|
76
|
-
{
|
|
77
|
-
name: 'get_advisors',
|
|
78
|
-
description:
|
|
79
|
-
'Lint the schema and policies: tables exposed with RLS off, RLS on with no policy, policies that leak through a join, SECURITY DEFINER functions anon can call, unindexed policy columns.',
|
|
80
|
-
inputSchema: { type: 'object', properties: {} },
|
|
81
|
-
run: () => api.adminRequest(ctx, 'GET', '/advisor'),
|
|
82
|
-
},
|
|
83
|
-
{
|
|
84
|
-
name: 'get_logs',
|
|
85
|
-
description: 'Recent log entries of one service, or of all of them.',
|
|
86
|
-
inputSchema: {
|
|
87
|
-
type: 'object',
|
|
88
|
-
properties: {
|
|
89
|
-
service: {
|
|
90
|
-
type: 'string',
|
|
91
|
-
enum: ['api', 'postgres', 'auth', 'storage', 'realtime', 'edge-function'],
|
|
92
|
-
description: 'Only this service. api is every HTTP request. Omit for all services.',
|
|
93
|
-
},
|
|
94
|
-
limit: { type: 'number', description: 'How many entries. Defaults to 100.' },
|
|
95
|
-
level: str("Filter by level: 'info', 'warn' or 'error'."),
|
|
96
|
-
},
|
|
97
|
-
},
|
|
98
|
-
run: args =>
|
|
99
|
-
api.adminRequest(ctx, 'GET', '/logs', {
|
|
100
|
-
query: { limit: args.limit || 100, level: args.level, service: args.service },
|
|
101
|
-
}),
|
|
102
|
-
},
|
|
103
|
-
{
|
|
104
|
-
name: 'generate_types',
|
|
105
|
-
description:
|
|
106
|
-
'TypeScript types for the whole schema, in the supabase-js Database shape. The same output as `motherbase gen types typescript`.',
|
|
107
|
-
inputSchema: { type: 'object', properties: {} },
|
|
108
|
-
run: () => api.adminRequest(ctx, 'GET', '/schema/types'),
|
|
109
|
-
},
|
|
110
|
-
{
|
|
111
|
-
name: 'execute_sql',
|
|
112
|
-
description:
|
|
113
|
-
'Run a read-only SQL query. Statements that write are refused: use apply_migration for those.',
|
|
114
|
-
inputSchema: {
|
|
115
|
-
type: 'object',
|
|
116
|
-
properties: { sql: str('A read-only SQL statement.') },
|
|
117
|
-
required: ['sql'],
|
|
118
|
-
},
|
|
119
|
-
// The read-only endpoint, not /sql: a tool the model reaches for by default
|
|
120
|
-
// must not be able to write, whatever the model believed it was doing.
|
|
121
|
-
run: args => api.adminRequest(ctx, 'POST', '/sql/readonly', { body: { sql: args.sql } }),
|
|
122
|
-
},
|
|
123
|
-
|
|
124
|
-
// ── db-core: Database section (indexes, enums, roles, publications, config) ──
|
|
125
|
-
{
|
|
126
|
-
name: 'list_indexes',
|
|
127
|
-
description: 'Every index in a schema, with its table, access method, key columns, uniqueness, size and definition.',
|
|
128
|
-
inputSchema: { type: 'object', properties: { schema: str('Postgres schema. Defaults to public.') } },
|
|
129
|
-
run: args => api.adminRequest(ctx, 'GET', '/schema/indexes', { query: { schema: args.schema } }),
|
|
130
|
-
},
|
|
131
|
-
{
|
|
132
|
-
name: 'list_enum_types',
|
|
133
|
-
description: 'Enumerated types in a schema, with their values in order and the columns that use each one.',
|
|
134
|
-
inputSchema: { type: 'object', properties: { schema: str('Postgres schema. Defaults to public.') } },
|
|
135
|
-
run: args => api.adminRequest(ctx, 'GET', '/schema/enums', { query: { schema: args.schema } }),
|
|
136
|
-
},
|
|
137
|
-
{
|
|
138
|
-
name: 'list_roles',
|
|
139
|
-
description:
|
|
140
|
-
'Postgres roles with their attributes (login, superuser, bypassrls, connection limit), live connections and memberships. Reserved platform roles carry the reason they cannot be changed.',
|
|
141
|
-
inputSchema: { type: 'object', properties: {} },
|
|
142
|
-
run: () => api.adminRequest(ctx, 'GET', '/database/roles'),
|
|
143
|
-
},
|
|
144
|
-
{
|
|
145
|
-
name: 'list_column_privileges',
|
|
146
|
-
description: 'For one table, which columns each role may SELECT, INSERT, UPDATE or REFERENCE.',
|
|
147
|
-
inputSchema: {
|
|
148
|
-
type: 'object',
|
|
149
|
-
properties: { table: str('Table name.'), schema: str('Postgres schema. Defaults to public.') },
|
|
150
|
-
required: ['table'],
|
|
151
|
-
},
|
|
152
|
-
run: args => api.adminRequest(ctx, 'GET', '/database/column-privileges', { query: { table: args.table, schema: args.schema } }),
|
|
153
|
-
},
|
|
154
|
-
{
|
|
155
|
-
name: 'list_publications',
|
|
156
|
-
description:
|
|
157
|
-
'Logical-replication publications, their tables and published actions, and wal_level. Motherbase Realtime does not use publications.',
|
|
158
|
-
inputSchema: { type: 'object', properties: {} },
|
|
159
|
-
run: () => api.adminRequest(ctx, 'GET', '/database/publications'),
|
|
160
|
-
},
|
|
161
|
-
{
|
|
162
|
-
name: 'get_postgres_config',
|
|
163
|
-
description:
|
|
164
|
-
'Managed Postgres settings (statement_timeout, work_mem, max_connections, ...) with current values, allowed scopes and every database, role or system override.',
|
|
165
|
-
inputSchema: { type: 'object', properties: {} },
|
|
166
|
-
run: () => api.adminRequest(ctx, 'GET', '/database/config'),
|
|
167
|
-
},
|
|
168
|
-
// ── end db-core ──
|
|
169
|
-
|
|
170
|
-
// ── cli-mcp lot: Supabase MCP project tools (platform-tools.js) ──────────
|
|
171
|
-
...platform.projectReadTools(ctx),
|
|
172
|
-
// ── Lot platform: preview branches (Console routes, console mode only) ──
|
|
173
|
-
{
|
|
174
|
-
name: 'list_branches',
|
|
175
|
-
description:
|
|
176
|
-
'The preview branches of the linked project, main first, with status (creating, seeding, active, failed) and whether each is running.',
|
|
177
|
-
inputSchema: { type: 'object', properties: {} },
|
|
178
|
-
run: () => api.consoleRequest(ctx, 'GET', consoleProjectPath(ctx, '/branches')),
|
|
179
|
-
},
|
|
180
|
-
|
|
181
|
-
// ── db-ext ──────────────────────────────────────────────────────────────
|
|
182
|
-
{
|
|
183
|
-
name: 'list_queues',
|
|
184
|
-
description: 'List the message queues (pgmq) with their depth, type and whether RLS is on. Empty when queues are not enabled.',
|
|
185
|
-
inputSchema: { type: 'object', properties: {} },
|
|
186
|
-
run: () => api.adminRequest(ctx, 'GET', '/queues'),
|
|
187
|
-
},
|
|
188
|
-
{
|
|
189
|
-
name: 'list_vault_secrets',
|
|
190
|
-
description: 'List the Vault secrets: id, name, description and dates. Never returns a secret value.',
|
|
191
|
-
inputSchema: { type: 'object', properties: {} },
|
|
192
|
-
run: () => api.adminRequest(ctx, 'GET', '/vault'),
|
|
193
|
-
},
|
|
194
|
-
{
|
|
195
|
-
name: 'list_wrappers',
|
|
196
|
-
description: 'List the foreign data wrappers, foreign servers (never their passwords) and foreign tables.',
|
|
197
|
-
inputSchema: { type: 'object', properties: {} },
|
|
198
|
-
run: () => api.adminRequest(ctx, 'GET', '/wrappers'),
|
|
199
|
-
},
|
|
200
|
-
{
|
|
201
|
-
name: 'advise_indexes',
|
|
202
|
-
description:
|
|
203
|
-
'Suggest indexes for one SELECT/UPDATE/DELETE statement: the CREATE INDEX statements and the estimated cost drop. The statement is only planned over hypothetical indexes, never run.',
|
|
204
|
-
inputSchema: {
|
|
205
|
-
type: 'object',
|
|
206
|
-
properties: { sql: str('The statement to advise on.') },
|
|
207
|
-
required: ['sql'],
|
|
208
|
-
},
|
|
209
|
-
run: args => api.adminRequest(ctx, 'POST', '/performance/index-advisor', { body: { query: args.sql } }),
|
|
210
|
-
},
|
|
211
|
-
// ── /db-ext ─────────────────────────────────────────────────────────────
|
|
212
|
-
|
|
213
|
-
// ── auth ──
|
|
214
|
-
{
|
|
215
|
-
name: 'get_auth_config',
|
|
216
|
-
description:
|
|
217
|
-
'Every auth.* settings group (sign-in methods, URL configuration, rate limits, attack protection, third-party auth, hooks...) with its current values. Secrets are masked.',
|
|
218
|
-
inputSchema: { type: 'object', properties: {} },
|
|
219
|
-
run: async () => {
|
|
220
|
-
const list = await api.adminRequest(ctx, 'GET', '/settings');
|
|
221
|
-
const keys = ((list && list.groups) || []).map(g => g.key).filter(key => key.startsWith('auth.'));
|
|
222
|
-
const groups = await Promise.all(keys.map(key => api.adminRequest(ctx, 'GET', `/settings/${key}`)));
|
|
223
|
-
return { groups };
|
|
224
|
-
},
|
|
225
|
-
},
|
|
226
|
-
{
|
|
227
|
-
name: 'get_auth_audit_logs',
|
|
228
|
-
description:
|
|
229
|
-
'Auth audit trail (auth.audit_log_entries), newest first: sign-ins, sign-ups, token refreshes, recoveries, admin changes. Same data as the Studio Audit Logs page.',
|
|
230
|
-
inputSchema: {
|
|
231
|
-
type: 'object',
|
|
232
|
-
properties: {
|
|
233
|
-
action: str("Filter by action, e.g. 'login', 'user_signedup', 'user_deleted'."),
|
|
234
|
-
actor: str('Substring of the actor id or email.'),
|
|
235
|
-
logType: str("Filter by log type: 'account', 'team', 'token', 'user'..."),
|
|
236
|
-
from: str('ISO date; only entries at or after it.'),
|
|
237
|
-
to: str('ISO date; only entries at or before it.'),
|
|
238
|
-
limit: { type: 'number', description: 'How many entries. Defaults to 50.' },
|
|
239
|
-
},
|
|
240
|
-
},
|
|
241
|
-
run: args =>
|
|
242
|
-
api.adminRequest(ctx, 'GET', '/auth/audit-logs', {
|
|
243
|
-
query: {
|
|
244
|
-
action: args.action,
|
|
245
|
-
actor: args.actor,
|
|
246
|
-
log_type: args.logType,
|
|
247
|
-
from: args.from,
|
|
248
|
-
to: args.to,
|
|
249
|
-
per_page: args.limit || 50,
|
|
250
|
-
},
|
|
251
|
-
}),
|
|
252
|
-
},
|
|
253
|
-
// ── /auth ──
|
|
254
|
-
|
|
255
|
-
// ── storage ──────────────────────────────────────────────────────────────
|
|
256
|
-
...storage.readStorageTools(ctx),
|
|
257
|
-
// ── /storage ─────────────────────────────────────────────────────────────
|
|
258
|
-
];
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
/** `/branches/<name>/<action>` on the linked project. */
|
|
262
|
-
const branchAction = (ctx, action) => args =>
|
|
263
|
-
api.consoleRequest(ctx, 'POST', consoleProjectPath(ctx, `/branches/${encodeURIComponent(args.name)}/${action}`));
|
|
264
|
-
|
|
265
|
-
/** Tools that change the database. Only exposed with `--write`. */
|
|
266
|
-
function writeTools(ctx) {
|
|
267
|
-
return [
|
|
268
|
-
{
|
|
269
|
-
name: 'apply_migration',
|
|
270
|
-
description:
|
|
271
|
-
'Create a named migration from DDL and apply it. This is the tool for CREATE TABLE, ALTER TABLE, CREATE POLICY and every other schema change. The migration is recorded, so it can be rolled back.',
|
|
272
|
-
inputSchema: {
|
|
273
|
-
type: 'object',
|
|
274
|
-
properties: {
|
|
275
|
-
name: str('Short migration name, e.g. add_orders_table.'),
|
|
276
|
-
sql: str('The DDL to apply.'),
|
|
277
|
-
downSql: str('Optional SQL that undoes it, enabling rollback.'),
|
|
278
|
-
},
|
|
279
|
-
required: ['name', 'sql'],
|
|
280
|
-
},
|
|
281
|
-
run: async args => {
|
|
282
|
-
const created = await api.adminRequest(ctx, 'POST', '/migrations', {
|
|
283
|
-
body: { name: args.name, upSql: args.sql, downSql: args.downSql },
|
|
284
|
-
});
|
|
285
|
-
const id = created && (created.id || (created.migration && created.migration.id));
|
|
286
|
-
if (!id) throw new Error('The migration was created but the server returned no id to apply.');
|
|
287
|
-
const applied = await api.adminRequest(ctx, 'POST', `/migrations/${encodeURIComponent(id)}/apply`);
|
|
288
|
-
return { id, applied };
|
|
289
|
-
},
|
|
290
|
-
},
|
|
291
|
-
{
|
|
292
|
-
name: 'execute_sql_write',
|
|
293
|
-
description:
|
|
294
|
-
'Run arbitrary SQL, including writes. Prefer apply_migration for schema changes: this leaves no record and cannot be rolled back.',
|
|
295
|
-
inputSchema: {
|
|
296
|
-
type: 'object',
|
|
297
|
-
properties: { sql: str('The SQL to run.') },
|
|
298
|
-
required: ['sql'],
|
|
299
|
-
},
|
|
300
|
-
run: args => api.adminRequest(ctx, 'POST', '/sql', { body: { sql: args.sql } }),
|
|
301
|
-
},
|
|
302
|
-
|
|
303
|
-
// ── db-core: Postgres settings ──────────────────────────────────────────
|
|
304
|
-
// A dedicated tool rather than execute_sql_write: the server checks the
|
|
305
|
-
// name against its curated list and picks the narrowest scope Postgres allows.
|
|
306
|
-
{
|
|
307
|
-
name: 'update_postgres_config',
|
|
308
|
-
description:
|
|
309
|
-
'Set or reset (value null) managed Postgres settings. Without scope, a session setting such as statement_timeout is pinned on the database; server-wide ones use ALTER SYSTEM and may need a restart, which the result reports.',
|
|
310
|
-
inputSchema: {
|
|
311
|
-
type: 'object',
|
|
312
|
-
properties: {
|
|
313
|
-
changes: {
|
|
314
|
-
type: 'array',
|
|
315
|
-
items: {
|
|
316
|
-
type: 'object',
|
|
317
|
-
properties: {
|
|
318
|
-
name: str('Setting name, e.g. statement_timeout.'),
|
|
319
|
-
value: { type: ['string', 'null'], description: "New value with its unit, e.g. '30s', or null to reset." },
|
|
320
|
-
scope: str("'database', 'role' or 'system'. Optional."),
|
|
321
|
-
role: str('Role name, for scope role.'),
|
|
322
|
-
},
|
|
323
|
-
required: ['name', 'value'],
|
|
324
|
-
},
|
|
325
|
-
},
|
|
326
|
-
},
|
|
327
|
-
required: ['changes'],
|
|
328
|
-
},
|
|
329
|
-
run: args => api.adminRequest(ctx, 'PATCH', '/database/config', { body: { changes: args.changes } }),
|
|
330
|
-
},
|
|
331
|
-
// ── end db-core ──
|
|
332
|
-
|
|
333
|
-
// ── cli-mcp lot: Supabase MCP project tools (platform-tools.js) ──────────
|
|
334
|
-
...platform.projectWriteTools(ctx),
|
|
335
|
-
// ── Lot platform: preview branches (Console routes, console mode only) ──
|
|
336
|
-
{
|
|
337
|
-
name: 'create_branch',
|
|
338
|
-
description:
|
|
339
|
-
'Create a preview branch: an isolated copy of the project with its own database, cut from its applied migrations (or its data with withData). Takes minutes; poll list_branches until it is active.',
|
|
340
|
-
inputSchema: {
|
|
341
|
-
type: 'object',
|
|
342
|
-
properties: {
|
|
343
|
-
name: str('Branch name, e.g. the git branch.'),
|
|
344
|
-
withData: { type: 'boolean', description: 'Copy the rows too, not only the schema. Defaults to false.' },
|
|
345
|
-
},
|
|
346
|
-
required: ['name'],
|
|
347
|
-
},
|
|
348
|
-
run: args =>
|
|
349
|
-
api.consoleRequest(ctx, 'POST', consoleProjectPath(ctx, '/branches'), {
|
|
350
|
-
body: { name: args.name, withData: Boolean(args.withData) },
|
|
351
|
-
}),
|
|
352
|
-
},
|
|
353
|
-
{
|
|
354
|
-
name: 'delete_branch',
|
|
355
|
-
description: 'Delete a preview branch and its whole stack.',
|
|
356
|
-
inputSchema: { type: 'object', properties: { name: str('Branch name.') }, required: ['name'] },
|
|
357
|
-
run: args =>
|
|
358
|
-
api.consoleRequest(ctx, 'DELETE', consoleProjectPath(ctx, `/branches/${encodeURIComponent(args.name)}`)),
|
|
359
|
-
},
|
|
360
|
-
{
|
|
361
|
-
name: 'merge_branch',
|
|
362
|
-
description:
|
|
363
|
-
"Apply the branch's new migrations to the main project. Refused while the branch is behind main: rebase_branch first.",
|
|
364
|
-
inputSchema: { type: 'object', properties: { name: str('Branch name.') }, required: ['name'] },
|
|
365
|
-
run: branchAction(ctx, 'merge'),
|
|
366
|
-
},
|
|
367
|
-
{
|
|
368
|
-
name: 'rebase_branch',
|
|
369
|
-
description: "Apply the main project's newer migrations to the branch.",
|
|
370
|
-
inputSchema: { type: 'object', properties: { name: str('Branch name.') }, required: ['name'] },
|
|
371
|
-
run: branchAction(ctx, 'rebase'),
|
|
372
|
-
},
|
|
373
|
-
{
|
|
374
|
-
name: 'reset_branch',
|
|
375
|
-
description: 'Recreate the branch from the main project as it is now. Everything done on the branch is lost.',
|
|
376
|
-
inputSchema: { type: 'object', properties: { name: str('Branch name.') }, required: ['name'] },
|
|
377
|
-
run: branchAction(ctx, 'reset'),
|
|
378
|
-
},
|
|
379
|
-
|
|
380
|
-
// ── db-ext ──────────────────────────────────────────────────────────────
|
|
381
|
-
{
|
|
382
|
-
name: 'create_queue',
|
|
383
|
-
description: 'Create a message queue (pgmq).',
|
|
384
|
-
inputSchema: {
|
|
385
|
-
type: 'object',
|
|
386
|
-
properties: {
|
|
387
|
-
name: str('Queue name.'),
|
|
388
|
-
type: str("'basic' (default), 'unlogged' or 'partitioned'."),
|
|
389
|
-
},
|
|
390
|
-
required: ['name'],
|
|
391
|
-
},
|
|
392
|
-
run: args => api.adminRequest(ctx, 'POST', '/queues', { body: { name: args.name, type: args.type } }),
|
|
393
|
-
},
|
|
394
|
-
{
|
|
395
|
-
name: 'drop_queue',
|
|
396
|
-
description: 'Drop a queue and every message in it. Cannot be undone.',
|
|
397
|
-
inputSchema: { type: 'object', properties: { name: str('Queue name.') }, required: ['name'] },
|
|
398
|
-
run: args => api.adminRequest(ctx, 'DELETE', `/queues/${encodeURIComponent(args.name)}`),
|
|
399
|
-
},
|
|
400
|
-
{
|
|
401
|
-
name: 'send_queue_message',
|
|
402
|
-
description: 'Send a JSON message to a queue.',
|
|
403
|
-
inputSchema: {
|
|
404
|
-
type: 'object',
|
|
405
|
-
properties: {
|
|
406
|
-
name: str('Queue name.'),
|
|
407
|
-
message: { description: 'Any JSON value.' },
|
|
408
|
-
delay: { type: 'number', description: 'Seconds before the message becomes visible. Defaults to 0.' },
|
|
409
|
-
},
|
|
410
|
-
required: ['name', 'message'],
|
|
411
|
-
},
|
|
412
|
-
run: args =>
|
|
413
|
-
api.adminRequest(ctx, 'POST', `/queues/${encodeURIComponent(args.name)}/messages`, {
|
|
414
|
-
body: { message: args.message, delay: args.delay },
|
|
415
|
-
}),
|
|
416
|
-
},
|
|
417
|
-
{
|
|
418
|
-
name: 'read_queue_messages',
|
|
419
|
-
description:
|
|
420
|
-
'Read messages from a queue. A write, because it hides them from other readers for the visibility timeout.',
|
|
421
|
-
inputSchema: {
|
|
422
|
-
type: 'object',
|
|
423
|
-
properties: {
|
|
424
|
-
name: str('Queue name.'),
|
|
425
|
-
qty: { type: 'number', description: 'How many messages, 1 to 100. Defaults to 1.' },
|
|
426
|
-
vt: { type: 'number', description: 'Visibility timeout in seconds. Defaults to 30.' },
|
|
427
|
-
},
|
|
428
|
-
required: ['name'],
|
|
429
|
-
},
|
|
430
|
-
run: args =>
|
|
431
|
-
api.adminRequest(ctx, 'POST', `/queues/${encodeURIComponent(args.name)}/read`, {
|
|
432
|
-
body: { qty: args.qty, vt: args.vt },
|
|
433
|
-
}),
|
|
434
|
-
},
|
|
435
|
-
{
|
|
436
|
-
name: 'create_vault_secret',
|
|
437
|
-
description: 'Store an encrypted secret in the Vault. There is no tool that reads a value back.',
|
|
438
|
-
inputSchema: {
|
|
439
|
-
type: 'object',
|
|
440
|
-
properties: { name: str('Unique secret name.'), secret: str('The value.'), description: str('Optional note.') },
|
|
441
|
-
required: ['name', 'secret'],
|
|
442
|
-
},
|
|
443
|
-
run: args =>
|
|
444
|
-
api.adminRequest(ctx, 'POST', '/vault/secrets', {
|
|
445
|
-
body: { name: args.name, secret: args.secret, description: args.description },
|
|
446
|
-
}),
|
|
447
|
-
},
|
|
448
|
-
{
|
|
449
|
-
name: 'delete_vault_secret',
|
|
450
|
-
description: 'Delete a Vault secret by id. Cannot be undone.',
|
|
451
|
-
inputSchema: { type: 'object', properties: { id: str('Secret id, from list_vault_secrets.') }, required: ['id'] },
|
|
452
|
-
run: args => api.adminRequest(ctx, 'DELETE', `/vault/secrets/${encodeURIComponent(args.id)}`),
|
|
453
|
-
},
|
|
454
|
-
// ── /db-ext ─────────────────────────────────────────────────────────────
|
|
455
|
-
|
|
456
|
-
// ── storage ──────────────────────────────────────────────────────────────
|
|
457
|
-
...storage.writeStorageTools(ctx),
|
|
458
|
-
// ── /storage ─────────────────────────────────────────────────────────────
|
|
459
|
-
];
|
|
460
|
-
}
|
|
461
|
-
|
|
462
|
-
/**
|
|
463
|
-
* `write` defaults to false, so an agent that connects without being told
|
|
464
|
-
* otherwise can read the schema, lint it and simulate policies, but cannot
|
|
465
|
-
* change anything. Handing an agent DDL is a decision the operator makes.
|
|
466
|
-
*
|
|
467
|
-
* `project` and `console` say what this session can reach: the project tools
|
|
468
|
-
* need a linked (or direct) project, the account tools a console login, and a
|
|
469
|
-
* tool that can only fail is left out rather than offered.
|
|
470
|
-
*/
|
|
471
|
-
function buildTools(ctx, { write = false, project = true, console = false } = {}) {
|
|
472
|
-
const tools = [];
|
|
473
|
-
if (project) tools.push(...readTools(ctx), ...(write ? writeTools(ctx) : []));
|
|
474
|
-
if (console) tools.push(...platform.consoleReadTools(ctx), ...(write ? platform.consoleWriteTools(ctx) : []));
|
|
475
|
-
return tools;
|
|
476
|
-
}
|
|
477
|
-
|
|
478
|
-
module.exports = { buildTools, readTools, writeTools };
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// The tools an agent gets over MCP.
|
|
4
|
+
//
|
|
5
|
+
// Every one is a call through the CLI's own HTTP layer: `api.adminRequest` for
|
|
6
|
+
// the project, `api.consoleRequest` for the account tools in platform-tools.js,
|
|
7
|
+
// the same paths every CLI command takes. That is the whole design: authentication,
|
|
8
|
+
// console-proxy vs direct mode, and error mapping are solved once in the CLI and
|
|
9
|
+
// reused here, so an MCP session cannot reach anything a `motherbase` command
|
|
10
|
+
// could not, and cannot drift from it either.
|
|
11
|
+
//
|
|
12
|
+
// The gap this closes is the one self-hosters name about Supabase's MCP: "DDL
|
|
13
|
+
// statements not supported". `apply_migration` runs DDL, because the admin API
|
|
14
|
+
// this is built on already does.
|
|
15
|
+
|
|
16
|
+
const api = require('../api');
|
|
17
|
+
const platform = require('./platform-tools');
|
|
18
|
+
const storage = require('./storage-tools');
|
|
19
|
+
const { consoleProjectPath } = require('../commands/_util');
|
|
20
|
+
|
|
21
|
+
const str = description => ({ type: 'string', description });
|
|
22
|
+
|
|
23
|
+
/** Tools that only read. Safe to run without confirmation. */
|
|
24
|
+
function readTools(ctx) {
|
|
25
|
+
return [
|
|
26
|
+
{
|
|
27
|
+
name: 'list_tables',
|
|
28
|
+
description: 'List the tables in the project database, with their columns and row counts.',
|
|
29
|
+
inputSchema: { type: 'object', properties: {} },
|
|
30
|
+
run: () => api.adminRequest(ctx, 'GET', '/tables'),
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
name: 'describe_table',
|
|
34
|
+
description: 'Columns, types, nullability, defaults, primary key and indexes of one table.',
|
|
35
|
+
inputSchema: {
|
|
36
|
+
type: 'object',
|
|
37
|
+
properties: { table: str('Table name, in the public schema.') },
|
|
38
|
+
required: ['table'],
|
|
39
|
+
},
|
|
40
|
+
run: async args => {
|
|
41
|
+
const [tables, indexes] = await Promise.all([
|
|
42
|
+
api.adminRequest(ctx, 'GET', '/schema/tables'),
|
|
43
|
+
api.adminRequest(ctx, 'GET', `/schema/tables/${encodeURIComponent(args.table)}/indexes`),
|
|
44
|
+
]);
|
|
45
|
+
const list = Array.isArray(tables) ? tables : tables && tables.tables;
|
|
46
|
+
const table = (list || []).find(t => t.name === args.table || t.table === args.table);
|
|
47
|
+
if (!table) throw new Error(`No table named "${args.table}" in the public schema.`);
|
|
48
|
+
return { ...table, indexes };
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
name: 'list_policies',
|
|
53
|
+
description:
|
|
54
|
+
'Row level security state for every table: whether RLS is on, and the policies attached with their USING and WITH CHECK expressions.',
|
|
55
|
+
inputSchema: { type: 'object', properties: {} },
|
|
56
|
+
run: () => api.adminRequest(ctx, 'GET', '/rls/tables'),
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
name: 'simulate_rls',
|
|
60
|
+
description:
|
|
61
|
+
'Run a SELECT as a given caller and report the rows they can see plus the policies that decided. The statement really executes, inside a transaction that is always rolled back.',
|
|
62
|
+
inputSchema: {
|
|
63
|
+
type: 'object',
|
|
64
|
+
properties: {
|
|
65
|
+
sql: str('A single SELECT statement.'),
|
|
66
|
+
role: str("'anon' or 'authenticated'. Ignored when userId is given."),
|
|
67
|
+
userId: str('Run as this existing user instead of a bare role.'),
|
|
68
|
+
},
|
|
69
|
+
required: ['sql'],
|
|
70
|
+
},
|
|
71
|
+
run: args =>
|
|
72
|
+
api.adminRequest(ctx, 'POST', '/rls/simulate', {
|
|
73
|
+
body: { sql: args.sql, role: args.role, userId: args.userId },
|
|
74
|
+
}),
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
name: 'get_advisors',
|
|
78
|
+
description:
|
|
79
|
+
'Lint the schema and policies: tables exposed with RLS off, RLS on with no policy, policies that leak through a join, SECURITY DEFINER functions anon can call, unindexed policy columns.',
|
|
80
|
+
inputSchema: { type: 'object', properties: {} },
|
|
81
|
+
run: () => api.adminRequest(ctx, 'GET', '/advisor'),
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
name: 'get_logs',
|
|
85
|
+
description: 'Recent log entries of one service, or of all of them.',
|
|
86
|
+
inputSchema: {
|
|
87
|
+
type: 'object',
|
|
88
|
+
properties: {
|
|
89
|
+
service: {
|
|
90
|
+
type: 'string',
|
|
91
|
+
enum: ['api', 'postgres', 'auth', 'storage', 'realtime', 'edge-function'],
|
|
92
|
+
description: 'Only this service. api is every HTTP request. Omit for all services.',
|
|
93
|
+
},
|
|
94
|
+
limit: { type: 'number', description: 'How many entries. Defaults to 100.' },
|
|
95
|
+
level: str("Filter by level: 'info', 'warn' or 'error'."),
|
|
96
|
+
},
|
|
97
|
+
},
|
|
98
|
+
run: args =>
|
|
99
|
+
api.adminRequest(ctx, 'GET', '/logs', {
|
|
100
|
+
query: { limit: args.limit || 100, level: args.level, service: args.service },
|
|
101
|
+
}),
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
name: 'generate_types',
|
|
105
|
+
description:
|
|
106
|
+
'TypeScript types for the whole schema, in the supabase-js Database shape. The same output as `motherbase gen types typescript`.',
|
|
107
|
+
inputSchema: { type: 'object', properties: {} },
|
|
108
|
+
run: () => api.adminRequest(ctx, 'GET', '/schema/types'),
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
name: 'execute_sql',
|
|
112
|
+
description:
|
|
113
|
+
'Run a read-only SQL query. Statements that write are refused: use apply_migration for those.',
|
|
114
|
+
inputSchema: {
|
|
115
|
+
type: 'object',
|
|
116
|
+
properties: { sql: str('A read-only SQL statement.') },
|
|
117
|
+
required: ['sql'],
|
|
118
|
+
},
|
|
119
|
+
// The read-only endpoint, not /sql: a tool the model reaches for by default
|
|
120
|
+
// must not be able to write, whatever the model believed it was doing.
|
|
121
|
+
run: args => api.adminRequest(ctx, 'POST', '/sql/readonly', { body: { sql: args.sql } }),
|
|
122
|
+
},
|
|
123
|
+
|
|
124
|
+
// ── db-core: Database section (indexes, enums, roles, publications, config) ──
|
|
125
|
+
{
|
|
126
|
+
name: 'list_indexes',
|
|
127
|
+
description: 'Every index in a schema, with its table, access method, key columns, uniqueness, size and definition.',
|
|
128
|
+
inputSchema: { type: 'object', properties: { schema: str('Postgres schema. Defaults to public.') } },
|
|
129
|
+
run: args => api.adminRequest(ctx, 'GET', '/schema/indexes', { query: { schema: args.schema } }),
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
name: 'list_enum_types',
|
|
133
|
+
description: 'Enumerated types in a schema, with their values in order and the columns that use each one.',
|
|
134
|
+
inputSchema: { type: 'object', properties: { schema: str('Postgres schema. Defaults to public.') } },
|
|
135
|
+
run: args => api.adminRequest(ctx, 'GET', '/schema/enums', { query: { schema: args.schema } }),
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
name: 'list_roles',
|
|
139
|
+
description:
|
|
140
|
+
'Postgres roles with their attributes (login, superuser, bypassrls, connection limit), live connections and memberships. Reserved platform roles carry the reason they cannot be changed.',
|
|
141
|
+
inputSchema: { type: 'object', properties: {} },
|
|
142
|
+
run: () => api.adminRequest(ctx, 'GET', '/database/roles'),
|
|
143
|
+
},
|
|
144
|
+
{
|
|
145
|
+
name: 'list_column_privileges',
|
|
146
|
+
description: 'For one table, which columns each role may SELECT, INSERT, UPDATE or REFERENCE.',
|
|
147
|
+
inputSchema: {
|
|
148
|
+
type: 'object',
|
|
149
|
+
properties: { table: str('Table name.'), schema: str('Postgres schema. Defaults to public.') },
|
|
150
|
+
required: ['table'],
|
|
151
|
+
},
|
|
152
|
+
run: args => api.adminRequest(ctx, 'GET', '/database/column-privileges', { query: { table: args.table, schema: args.schema } }),
|
|
153
|
+
},
|
|
154
|
+
{
|
|
155
|
+
name: 'list_publications',
|
|
156
|
+
description:
|
|
157
|
+
'Logical-replication publications, their tables and published actions, and wal_level. Motherbase Realtime does not use publications.',
|
|
158
|
+
inputSchema: { type: 'object', properties: {} },
|
|
159
|
+
run: () => api.adminRequest(ctx, 'GET', '/database/publications'),
|
|
160
|
+
},
|
|
161
|
+
{
|
|
162
|
+
name: 'get_postgres_config',
|
|
163
|
+
description:
|
|
164
|
+
'Managed Postgres settings (statement_timeout, work_mem, max_connections, ...) with current values, allowed scopes and every database, role or system override.',
|
|
165
|
+
inputSchema: { type: 'object', properties: {} },
|
|
166
|
+
run: () => api.adminRequest(ctx, 'GET', '/database/config'),
|
|
167
|
+
},
|
|
168
|
+
// ── end db-core ──
|
|
169
|
+
|
|
170
|
+
// ── cli-mcp lot: Supabase MCP project tools (platform-tools.js) ──────────
|
|
171
|
+
...platform.projectReadTools(ctx),
|
|
172
|
+
// ── Lot platform: preview branches (Console routes, console mode only) ──
|
|
173
|
+
{
|
|
174
|
+
name: 'list_branches',
|
|
175
|
+
description:
|
|
176
|
+
'The preview branches of the linked project, main first, with status (creating, seeding, active, failed) and whether each is running.',
|
|
177
|
+
inputSchema: { type: 'object', properties: {} },
|
|
178
|
+
run: () => api.consoleRequest(ctx, 'GET', consoleProjectPath(ctx, '/branches')),
|
|
179
|
+
},
|
|
180
|
+
|
|
181
|
+
// ── db-ext ──────────────────────────────────────────────────────────────
|
|
182
|
+
{
|
|
183
|
+
name: 'list_queues',
|
|
184
|
+
description: 'List the message queues (pgmq) with their depth, type and whether RLS is on. Empty when queues are not enabled.',
|
|
185
|
+
inputSchema: { type: 'object', properties: {} },
|
|
186
|
+
run: () => api.adminRequest(ctx, 'GET', '/queues'),
|
|
187
|
+
},
|
|
188
|
+
{
|
|
189
|
+
name: 'list_vault_secrets',
|
|
190
|
+
description: 'List the Vault secrets: id, name, description and dates. Never returns a secret value.',
|
|
191
|
+
inputSchema: { type: 'object', properties: {} },
|
|
192
|
+
run: () => api.adminRequest(ctx, 'GET', '/vault'),
|
|
193
|
+
},
|
|
194
|
+
{
|
|
195
|
+
name: 'list_wrappers',
|
|
196
|
+
description: 'List the foreign data wrappers, foreign servers (never their passwords) and foreign tables.',
|
|
197
|
+
inputSchema: { type: 'object', properties: {} },
|
|
198
|
+
run: () => api.adminRequest(ctx, 'GET', '/wrappers'),
|
|
199
|
+
},
|
|
200
|
+
{
|
|
201
|
+
name: 'advise_indexes',
|
|
202
|
+
description:
|
|
203
|
+
'Suggest indexes for one SELECT/UPDATE/DELETE statement: the CREATE INDEX statements and the estimated cost drop. The statement is only planned over hypothetical indexes, never run.',
|
|
204
|
+
inputSchema: {
|
|
205
|
+
type: 'object',
|
|
206
|
+
properties: { sql: str('The statement to advise on.') },
|
|
207
|
+
required: ['sql'],
|
|
208
|
+
},
|
|
209
|
+
run: args => api.adminRequest(ctx, 'POST', '/performance/index-advisor', { body: { query: args.sql } }),
|
|
210
|
+
},
|
|
211
|
+
// ── /db-ext ─────────────────────────────────────────────────────────────
|
|
212
|
+
|
|
213
|
+
// ── auth ──
|
|
214
|
+
{
|
|
215
|
+
name: 'get_auth_config',
|
|
216
|
+
description:
|
|
217
|
+
'Every auth.* settings group (sign-in methods, URL configuration, rate limits, attack protection, third-party auth, hooks...) with its current values. Secrets are masked.',
|
|
218
|
+
inputSchema: { type: 'object', properties: {} },
|
|
219
|
+
run: async () => {
|
|
220
|
+
const list = await api.adminRequest(ctx, 'GET', '/settings');
|
|
221
|
+
const keys = ((list && list.groups) || []).map(g => g.key).filter(key => key.startsWith('auth.'));
|
|
222
|
+
const groups = await Promise.all(keys.map(key => api.adminRequest(ctx, 'GET', `/settings/${key}`)));
|
|
223
|
+
return { groups };
|
|
224
|
+
},
|
|
225
|
+
},
|
|
226
|
+
{
|
|
227
|
+
name: 'get_auth_audit_logs',
|
|
228
|
+
description:
|
|
229
|
+
'Auth audit trail (auth.audit_log_entries), newest first: sign-ins, sign-ups, token refreshes, recoveries, admin changes. Same data as the Studio Audit Logs page.',
|
|
230
|
+
inputSchema: {
|
|
231
|
+
type: 'object',
|
|
232
|
+
properties: {
|
|
233
|
+
action: str("Filter by action, e.g. 'login', 'user_signedup', 'user_deleted'."),
|
|
234
|
+
actor: str('Substring of the actor id or email.'),
|
|
235
|
+
logType: str("Filter by log type: 'account', 'team', 'token', 'user'..."),
|
|
236
|
+
from: str('ISO date; only entries at or after it.'),
|
|
237
|
+
to: str('ISO date; only entries at or before it.'),
|
|
238
|
+
limit: { type: 'number', description: 'How many entries. Defaults to 50.' },
|
|
239
|
+
},
|
|
240
|
+
},
|
|
241
|
+
run: args =>
|
|
242
|
+
api.adminRequest(ctx, 'GET', '/auth/audit-logs', {
|
|
243
|
+
query: {
|
|
244
|
+
action: args.action,
|
|
245
|
+
actor: args.actor,
|
|
246
|
+
log_type: args.logType,
|
|
247
|
+
from: args.from,
|
|
248
|
+
to: args.to,
|
|
249
|
+
per_page: args.limit || 50,
|
|
250
|
+
},
|
|
251
|
+
}),
|
|
252
|
+
},
|
|
253
|
+
// ── /auth ──
|
|
254
|
+
|
|
255
|
+
// ── storage ──────────────────────────────────────────────────────────────
|
|
256
|
+
...storage.readStorageTools(ctx),
|
|
257
|
+
// ── /storage ─────────────────────────────────────────────────────────────
|
|
258
|
+
];
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/** `/branches/<name>/<action>` on the linked project. */
|
|
262
|
+
const branchAction = (ctx, action) => args =>
|
|
263
|
+
api.consoleRequest(ctx, 'POST', consoleProjectPath(ctx, `/branches/${encodeURIComponent(args.name)}/${action}`));
|
|
264
|
+
|
|
265
|
+
/** Tools that change the database. Only exposed with `--write`. */
|
|
266
|
+
function writeTools(ctx) {
|
|
267
|
+
return [
|
|
268
|
+
{
|
|
269
|
+
name: 'apply_migration',
|
|
270
|
+
description:
|
|
271
|
+
'Create a named migration from DDL and apply it. This is the tool for CREATE TABLE, ALTER TABLE, CREATE POLICY and every other schema change. The migration is recorded, so it can be rolled back.',
|
|
272
|
+
inputSchema: {
|
|
273
|
+
type: 'object',
|
|
274
|
+
properties: {
|
|
275
|
+
name: str('Short migration name, e.g. add_orders_table.'),
|
|
276
|
+
sql: str('The DDL to apply.'),
|
|
277
|
+
downSql: str('Optional SQL that undoes it, enabling rollback.'),
|
|
278
|
+
},
|
|
279
|
+
required: ['name', 'sql'],
|
|
280
|
+
},
|
|
281
|
+
run: async args => {
|
|
282
|
+
const created = await api.adminRequest(ctx, 'POST', '/migrations', {
|
|
283
|
+
body: { name: args.name, upSql: args.sql, downSql: args.downSql },
|
|
284
|
+
});
|
|
285
|
+
const id = created && (created.id || (created.migration && created.migration.id));
|
|
286
|
+
if (!id) throw new Error('The migration was created but the server returned no id to apply.');
|
|
287
|
+
const applied = await api.adminRequest(ctx, 'POST', `/migrations/${encodeURIComponent(id)}/apply`);
|
|
288
|
+
return { id, applied };
|
|
289
|
+
},
|
|
290
|
+
},
|
|
291
|
+
{
|
|
292
|
+
name: 'execute_sql_write',
|
|
293
|
+
description:
|
|
294
|
+
'Run arbitrary SQL, including writes. Prefer apply_migration for schema changes: this leaves no record and cannot be rolled back.',
|
|
295
|
+
inputSchema: {
|
|
296
|
+
type: 'object',
|
|
297
|
+
properties: { sql: str('The SQL to run.') },
|
|
298
|
+
required: ['sql'],
|
|
299
|
+
},
|
|
300
|
+
run: args => api.adminRequest(ctx, 'POST', '/sql', { body: { sql: args.sql } }),
|
|
301
|
+
},
|
|
302
|
+
|
|
303
|
+
// ── db-core: Postgres settings ──────────────────────────────────────────
|
|
304
|
+
// A dedicated tool rather than execute_sql_write: the server checks the
|
|
305
|
+
// name against its curated list and picks the narrowest scope Postgres allows.
|
|
306
|
+
{
|
|
307
|
+
name: 'update_postgres_config',
|
|
308
|
+
description:
|
|
309
|
+
'Set or reset (value null) managed Postgres settings. Without scope, a session setting such as statement_timeout is pinned on the database; server-wide ones use ALTER SYSTEM and may need a restart, which the result reports.',
|
|
310
|
+
inputSchema: {
|
|
311
|
+
type: 'object',
|
|
312
|
+
properties: {
|
|
313
|
+
changes: {
|
|
314
|
+
type: 'array',
|
|
315
|
+
items: {
|
|
316
|
+
type: 'object',
|
|
317
|
+
properties: {
|
|
318
|
+
name: str('Setting name, e.g. statement_timeout.'),
|
|
319
|
+
value: { type: ['string', 'null'], description: "New value with its unit, e.g. '30s', or null to reset." },
|
|
320
|
+
scope: str("'database', 'role' or 'system'. Optional."),
|
|
321
|
+
role: str('Role name, for scope role.'),
|
|
322
|
+
},
|
|
323
|
+
required: ['name', 'value'],
|
|
324
|
+
},
|
|
325
|
+
},
|
|
326
|
+
},
|
|
327
|
+
required: ['changes'],
|
|
328
|
+
},
|
|
329
|
+
run: args => api.adminRequest(ctx, 'PATCH', '/database/config', { body: { changes: args.changes } }),
|
|
330
|
+
},
|
|
331
|
+
// ── end db-core ──
|
|
332
|
+
|
|
333
|
+
// ── cli-mcp lot: Supabase MCP project tools (platform-tools.js) ──────────
|
|
334
|
+
...platform.projectWriteTools(ctx),
|
|
335
|
+
// ── Lot platform: preview branches (Console routes, console mode only) ──
|
|
336
|
+
{
|
|
337
|
+
name: 'create_branch',
|
|
338
|
+
description:
|
|
339
|
+
'Create a preview branch: an isolated copy of the project with its own database, cut from its applied migrations (or its data with withData). Takes minutes; poll list_branches until it is active.',
|
|
340
|
+
inputSchema: {
|
|
341
|
+
type: 'object',
|
|
342
|
+
properties: {
|
|
343
|
+
name: str('Branch name, e.g. the git branch.'),
|
|
344
|
+
withData: { type: 'boolean', description: 'Copy the rows too, not only the schema. Defaults to false.' },
|
|
345
|
+
},
|
|
346
|
+
required: ['name'],
|
|
347
|
+
},
|
|
348
|
+
run: args =>
|
|
349
|
+
api.consoleRequest(ctx, 'POST', consoleProjectPath(ctx, '/branches'), {
|
|
350
|
+
body: { name: args.name, withData: Boolean(args.withData) },
|
|
351
|
+
}),
|
|
352
|
+
},
|
|
353
|
+
{
|
|
354
|
+
name: 'delete_branch',
|
|
355
|
+
description: 'Delete a preview branch and its whole stack.',
|
|
356
|
+
inputSchema: { type: 'object', properties: { name: str('Branch name.') }, required: ['name'] },
|
|
357
|
+
run: args =>
|
|
358
|
+
api.consoleRequest(ctx, 'DELETE', consoleProjectPath(ctx, `/branches/${encodeURIComponent(args.name)}`)),
|
|
359
|
+
},
|
|
360
|
+
{
|
|
361
|
+
name: 'merge_branch',
|
|
362
|
+
description:
|
|
363
|
+
"Apply the branch's new migrations to the main project. Refused while the branch is behind main: rebase_branch first.",
|
|
364
|
+
inputSchema: { type: 'object', properties: { name: str('Branch name.') }, required: ['name'] },
|
|
365
|
+
run: branchAction(ctx, 'merge'),
|
|
366
|
+
},
|
|
367
|
+
{
|
|
368
|
+
name: 'rebase_branch',
|
|
369
|
+
description: "Apply the main project's newer migrations to the branch.",
|
|
370
|
+
inputSchema: { type: 'object', properties: { name: str('Branch name.') }, required: ['name'] },
|
|
371
|
+
run: branchAction(ctx, 'rebase'),
|
|
372
|
+
},
|
|
373
|
+
{
|
|
374
|
+
name: 'reset_branch',
|
|
375
|
+
description: 'Recreate the branch from the main project as it is now. Everything done on the branch is lost.',
|
|
376
|
+
inputSchema: { type: 'object', properties: { name: str('Branch name.') }, required: ['name'] },
|
|
377
|
+
run: branchAction(ctx, 'reset'),
|
|
378
|
+
},
|
|
379
|
+
|
|
380
|
+
// ── db-ext ──────────────────────────────────────────────────────────────
|
|
381
|
+
{
|
|
382
|
+
name: 'create_queue',
|
|
383
|
+
description: 'Create a message queue (pgmq).',
|
|
384
|
+
inputSchema: {
|
|
385
|
+
type: 'object',
|
|
386
|
+
properties: {
|
|
387
|
+
name: str('Queue name.'),
|
|
388
|
+
type: str("'basic' (default), 'unlogged' or 'partitioned'."),
|
|
389
|
+
},
|
|
390
|
+
required: ['name'],
|
|
391
|
+
},
|
|
392
|
+
run: args => api.adminRequest(ctx, 'POST', '/queues', { body: { name: args.name, type: args.type } }),
|
|
393
|
+
},
|
|
394
|
+
{
|
|
395
|
+
name: 'drop_queue',
|
|
396
|
+
description: 'Drop a queue and every message in it. Cannot be undone.',
|
|
397
|
+
inputSchema: { type: 'object', properties: { name: str('Queue name.') }, required: ['name'] },
|
|
398
|
+
run: args => api.adminRequest(ctx, 'DELETE', `/queues/${encodeURIComponent(args.name)}`),
|
|
399
|
+
},
|
|
400
|
+
{
|
|
401
|
+
name: 'send_queue_message',
|
|
402
|
+
description: 'Send a JSON message to a queue.',
|
|
403
|
+
inputSchema: {
|
|
404
|
+
type: 'object',
|
|
405
|
+
properties: {
|
|
406
|
+
name: str('Queue name.'),
|
|
407
|
+
message: { description: 'Any JSON value.' },
|
|
408
|
+
delay: { type: 'number', description: 'Seconds before the message becomes visible. Defaults to 0.' },
|
|
409
|
+
},
|
|
410
|
+
required: ['name', 'message'],
|
|
411
|
+
},
|
|
412
|
+
run: args =>
|
|
413
|
+
api.adminRequest(ctx, 'POST', `/queues/${encodeURIComponent(args.name)}/messages`, {
|
|
414
|
+
body: { message: args.message, delay: args.delay },
|
|
415
|
+
}),
|
|
416
|
+
},
|
|
417
|
+
{
|
|
418
|
+
name: 'read_queue_messages',
|
|
419
|
+
description:
|
|
420
|
+
'Read messages from a queue. A write, because it hides them from other readers for the visibility timeout.',
|
|
421
|
+
inputSchema: {
|
|
422
|
+
type: 'object',
|
|
423
|
+
properties: {
|
|
424
|
+
name: str('Queue name.'),
|
|
425
|
+
qty: { type: 'number', description: 'How many messages, 1 to 100. Defaults to 1.' },
|
|
426
|
+
vt: { type: 'number', description: 'Visibility timeout in seconds. Defaults to 30.' },
|
|
427
|
+
},
|
|
428
|
+
required: ['name'],
|
|
429
|
+
},
|
|
430
|
+
run: args =>
|
|
431
|
+
api.adminRequest(ctx, 'POST', `/queues/${encodeURIComponent(args.name)}/read`, {
|
|
432
|
+
body: { qty: args.qty, vt: args.vt },
|
|
433
|
+
}),
|
|
434
|
+
},
|
|
435
|
+
{
|
|
436
|
+
name: 'create_vault_secret',
|
|
437
|
+
description: 'Store an encrypted secret in the Vault. There is no tool that reads a value back.',
|
|
438
|
+
inputSchema: {
|
|
439
|
+
type: 'object',
|
|
440
|
+
properties: { name: str('Unique secret name.'), secret: str('The value.'), description: str('Optional note.') },
|
|
441
|
+
required: ['name', 'secret'],
|
|
442
|
+
},
|
|
443
|
+
run: args =>
|
|
444
|
+
api.adminRequest(ctx, 'POST', '/vault/secrets', {
|
|
445
|
+
body: { name: args.name, secret: args.secret, description: args.description },
|
|
446
|
+
}),
|
|
447
|
+
},
|
|
448
|
+
{
|
|
449
|
+
name: 'delete_vault_secret',
|
|
450
|
+
description: 'Delete a Vault secret by id. Cannot be undone.',
|
|
451
|
+
inputSchema: { type: 'object', properties: { id: str('Secret id, from list_vault_secrets.') }, required: ['id'] },
|
|
452
|
+
run: args => api.adminRequest(ctx, 'DELETE', `/vault/secrets/${encodeURIComponent(args.id)}`),
|
|
453
|
+
},
|
|
454
|
+
// ── /db-ext ─────────────────────────────────────────────────────────────
|
|
455
|
+
|
|
456
|
+
// ── storage ──────────────────────────────────────────────────────────────
|
|
457
|
+
...storage.writeStorageTools(ctx),
|
|
458
|
+
// ── /storage ─────────────────────────────────────────────────────────────
|
|
459
|
+
];
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
/**
|
|
463
|
+
* `write` defaults to false, so an agent that connects without being told
|
|
464
|
+
* otherwise can read the schema, lint it and simulate policies, but cannot
|
|
465
|
+
* change anything. Handing an agent DDL is a decision the operator makes.
|
|
466
|
+
*
|
|
467
|
+
* `project` and `console` say what this session can reach: the project tools
|
|
468
|
+
* need a linked (or direct) project, the account tools a console login, and a
|
|
469
|
+
* tool that can only fail is left out rather than offered.
|
|
470
|
+
*/
|
|
471
|
+
function buildTools(ctx, { write = false, project = true, console = false } = {}) {
|
|
472
|
+
const tools = [];
|
|
473
|
+
if (project) tools.push(...readTools(ctx), ...(write ? writeTools(ctx) : []));
|
|
474
|
+
if (console) tools.push(...platform.consoleReadTools(ctx), ...(write ? platform.consoleWriteTools(ctx) : []));
|
|
475
|
+
return tools;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
module.exports = { buildTools, readTools, writeTools };
|