@motherbase/cli 0.2.4 → 0.2.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/README.md +247 -134
  2. package/package.json +2 -2
  3. package/src/api.js +14 -2
  4. package/src/commands/_util.js +49 -6
  5. package/src/commands/backups.js +16 -0
  6. package/src/commands/branches.js +113 -0
  7. package/src/commands/completion.js +90 -0
  8. package/src/commands/db-catalog.js +326 -0
  9. package/src/commands/db.js +16 -5
  10. package/src/commands/domains.js +124 -0
  11. package/src/commands/functions.js +297 -274
  12. package/src/commands/gen.js +172 -126
  13. package/src/commands/import.js +23 -3
  14. package/src/commands/index.js +27 -6
  15. package/src/commands/inspect.js +142 -5
  16. package/src/commands/keys.js +72 -72
  17. package/src/commands/local.js +273 -273
  18. package/src/commands/mcp.js +61 -43
  19. package/src/commands/migrate.js +179 -0
  20. package/src/commands/migration.js +81 -6
  21. package/src/commands/network.js +83 -0
  22. package/src/commands/orgs.js +40 -0
  23. package/src/commands/postgres-config.js +113 -0
  24. package/src/commands/projects.js +8 -3
  25. package/src/commands/queues.js +133 -0
  26. package/src/commands/secrets.js +29 -8
  27. package/src/commands/signing.js +130 -0
  28. package/src/commands/snippets.js +44 -0
  29. package/src/commands/storage.js +192 -44
  30. package/src/commands/test.js +9 -0
  31. package/src/commands/users.js +30 -1
  32. package/src/commands/vault.js +82 -0
  33. package/src/commands/wrappers.js +150 -0
  34. package/src/edge-templates.js +194 -0
  35. package/src/format.js +92 -0
  36. package/src/inspect/csv.js +22 -0
  37. package/src/inspect/queries.js +562 -0
  38. package/src/main.js +41 -7
  39. package/src/mcp/platform-tools.js +233 -0
  40. package/src/mcp/protocol.js +149 -149
  41. package/src/mcp/storage-tools.js +93 -0
  42. package/src/mcp/tools.js +478 -166
  43. package/src/migration/commands.js +375 -0
  44. package/src/migration/files.js +245 -0
  45. package/src/prompt.js +1 -1
  46. package/src/storage/remote.js +62 -0
  47. package/src/commands/migrations.js +0 -85
@@ -0,0 +1,233 @@
1
+ 'use strict';
2
+
3
+ // The Supabase MCP server's account and project tools, for what Motherbase
4
+ // already serves. Same names and arguments where they fit, so an agent that
5
+ // knows Supabase's tools can use these without learning new ones.
6
+ //
7
+ // Two scopes, because a session may have either or both:
8
+ // - console tools (organizations, projects) go through `api.consoleRequest`
9
+ // and need a console login, not a linked project;
10
+ // - project tools act on the linked (or direct-mode) project through
11
+ // `api.adminRequest`, like every tool in tools.js.
12
+ //
13
+ // Not here, because there is nothing behind them yet: branching, cost
14
+ // confirmation, docs search, and Supabase's per-call `project_id` on project
15
+ // tools (a session serves the one project it was started for).
16
+
17
+ const api = require('../api');
18
+ const { upsertFunction } = require('../commands/functions');
19
+
20
+ const str = description => ({ type: 'string', description });
21
+ const noArgs = { type: 'object', properties: {} };
22
+ const enc = encodeURIComponent;
23
+
24
+ const FUNCTION_NAME = /^[a-z0-9][a-z0-9-]*$/;
25
+
26
+ /** A project from the console listing, by name or id; the listing is the only read. */
27
+ async function findProject(ctx, id) {
28
+ const projects = await api.consoleRequest(ctx, 'GET', '/projects');
29
+ const project = (Array.isArray(projects) ? projects : []).find(p => p.name === id || p.id === id);
30
+ if (!project) throw new Error(`No project "${id}" on this console. list_projects shows the ones you can see.`);
31
+ return project;
32
+ }
33
+
34
+ /** An edge function by the slug Supabase's tools take: its route, name or id. */
35
+ async function findFunction(ctx, slug) {
36
+ const listing = await api.adminRequest(ctx, 'GET', '/edge-functions');
37
+ const fn = ((listing && listing.functions) || []).find(f => f.route === slug || f.name === slug || f.id === slug);
38
+ if (!fn) throw new Error(`No edge function "${slug}". list_edge_functions shows what is deployed.`);
39
+ return fn;
40
+ }
41
+
42
+ /**
43
+ * The single source file a function deploys as. Supabase's tool takes a file
44
+ * set; Motherbase stores one module per function, so a set that needs more than
45
+ * its entrypoint is refused rather than deployed half-complete.
46
+ */
47
+ function entrypointSource(files, entrypoint) {
48
+ if (!Array.isArray(files) || !files.length) throw new Error('files must hold at least the entrypoint.');
49
+ const wanted = entrypoint || 'index.ts';
50
+ // A lone file is the entrypoint whatever it is called, unless one was named.
51
+ const entry = files.find(f => f && f.name === wanted) || (!entrypoint && files.length === 1 ? files[0] : undefined);
52
+ if (!entry) throw new Error(`No file named ${wanted} among files.`);
53
+ if (files.length > 1) {
54
+ throw new Error('Edge functions deploy as a single module here: inline the other files into the entrypoint.');
55
+ }
56
+ return String(entry.content);
57
+ }
58
+
59
+ /** Account-level reads. Need a console login. */
60
+ function consoleReadTools(ctx) {
61
+ return [
62
+ {
63
+ name: 'list_organizations',
64
+ description: 'List the organizations the logged-in operator belongs to, with their role and project count.',
65
+ inputSchema: noArgs,
66
+ run: () => api.consoleRequest(ctx, 'GET', '/orgs'),
67
+ },
68
+ {
69
+ name: 'get_organization',
70
+ description: 'Details of one organization: name, members count and settings.',
71
+ inputSchema: { type: 'object', properties: { id: str('Organization id.') }, required: ['id'] },
72
+ run: args => api.consoleRequest(ctx, 'GET', `/orgs/${enc(args.id)}`),
73
+ },
74
+ {
75
+ name: 'list_projects',
76
+ description: 'List the projects on this console the operator can see, with their status and domain.',
77
+ inputSchema: {
78
+ type: 'object',
79
+ properties: { organization_id: str('Only the projects of this organization.') },
80
+ },
81
+ run: args => api.consoleRequest(ctx, 'GET', '/projects', { query: { org: args.organization_id } }),
82
+ },
83
+ {
84
+ name: 'get_project',
85
+ description: 'Details of one project: status, domain and organization.',
86
+ inputSchema: { type: 'object', properties: { id: str('Project name or id.') }, required: ['id'] },
87
+ run: args => findProject(ctx, args.id),
88
+ },
89
+ ];
90
+ }
91
+
92
+ /** Account-level changes. Only with `--write`. */
93
+ function consoleWriteTools(ctx) {
94
+ return [
95
+ {
96
+ name: 'create_project',
97
+ description:
98
+ 'Create a project: a new, isolated stack (database, auth, storage, functions). Provisioning takes a while; poll get_project for its status.',
99
+ inputSchema: {
100
+ type: 'object',
101
+ properties: {
102
+ name: str('Project name: lowercase letters, digits and hyphens. Also its id.'),
103
+ organization_id: str('Organization to create it in. Defaults to the default organization.'),
104
+ domain: str('Domain it is served on. Required unless the console runs in dev mode.'),
105
+ },
106
+ required: ['name'],
107
+ },
108
+ run: args =>
109
+ api.consoleRequest(ctx, 'POST', '/projects', {
110
+ body: { name: args.name, domain: args.domain, orgId: args.organization_id },
111
+ }),
112
+ },
113
+ {
114
+ name: 'pause_project',
115
+ description: 'Stop a project\'s containers. Its data is kept; restore_project starts it again.',
116
+ inputSchema: { type: 'object', properties: { project_id: str('Project name or id.') }, required: ['project_id'] },
117
+ run: args => api.consoleRequest(ctx, 'POST', `/projects/${enc(args.project_id)}/stop`),
118
+ },
119
+ {
120
+ name: 'restore_project',
121
+ description: 'Start a paused project again.',
122
+ inputSchema: { type: 'object', properties: { project_id: str('Project name or id.') }, required: ['project_id'] },
123
+ run: args => api.consoleRequest(ctx, 'POST', `/projects/${enc(args.project_id)}/start`),
124
+ },
125
+ ];
126
+ }
127
+
128
+ /** Reads on the served project. */
129
+ function projectReadTools(ctx) {
130
+ return [
131
+ {
132
+ name: 'get_project_url',
133
+ description: 'The API URL (and realtime URL) a client app uses to reach this project.',
134
+ inputSchema: noArgs,
135
+ run: async () => {
136
+ const info = await api.adminRequest(ctx, 'GET', '/connect-info');
137
+ return { url: info.apiUrl, realtimeUrl: info.wsUrl };
138
+ },
139
+ },
140
+ {
141
+ name: 'get_publishable_keys',
142
+ description:
143
+ 'The publishable API keys a client app ships with. Safe to embed: row level security decides what they reach. Secret keys are never returned.',
144
+ inputSchema: noArgs,
145
+ run: async () => {
146
+ const data = await api.adminRequest(ctx, 'GET', '/api-keys');
147
+ const keys = ((data && data.keys) || [])
148
+ .filter(k => k.type === 'publishable' && !k.revokedAt)
149
+ .map(k => ({ name: k.name, api_key: k.token }));
150
+ return { keys };
151
+ },
152
+ },
153
+ {
154
+ name: 'list_extensions',
155
+ description: 'Postgres extensions available in the project database, and which are installed.',
156
+ inputSchema: noArgs,
157
+ run: () => api.adminRequest(ctx, 'GET', '/schema/extensions'),
158
+ },
159
+ {
160
+ name: 'list_migrations',
161
+ description: 'Schema migrations recorded for the project, with their status and when they were applied.',
162
+ inputSchema: noArgs,
163
+ run: () => api.adminRequest(ctx, 'GET', '/migrations'),
164
+ },
165
+ {
166
+ name: 'list_edge_functions',
167
+ description: 'Deployed edge functions: name, route, whether enabled, and limits. get_edge_function returns the source.',
168
+ inputSchema: noArgs,
169
+ run: async () => {
170
+ const data = await api.adminRequest(ctx, 'GET', '/edge-functions');
171
+ // The source of every function is noise in a listing; get_edge_function has it.
172
+ const functions = ((data && data.functions) || []).map(({ code: _code, ...meta }) => meta);
173
+ return { functions };
174
+ },
175
+ },
176
+ {
177
+ name: 'get_edge_function',
178
+ description: 'One deployed edge function with its source code.',
179
+ inputSchema: {
180
+ type: 'object',
181
+ properties: { function_slug: str('The function\'s route (also accepts its name or id).') },
182
+ required: ['function_slug'],
183
+ },
184
+ run: async args => {
185
+ const fn = await findFunction(ctx, args.function_slug);
186
+ return api.adminRequest(ctx, 'GET', `/edge-functions/${enc(fn.id)}`);
187
+ },
188
+ },
189
+ {
190
+ name: 'list_storage_buckets',
191
+ description: 'Storage buckets with their visibility, quota and usage.',
192
+ inputSchema: noArgs,
193
+ run: () => api.adminRequest(ctx, 'GET', '/storage/buckets'),
194
+ },
195
+ ];
196
+ }
197
+
198
+ /** Changes to the served project. Only with `--write`. */
199
+ function projectWriteTools(ctx) {
200
+ return [
201
+ {
202
+ name: 'deploy_edge_function',
203
+ description:
204
+ 'Deploy an edge function, creating it or replacing the one at the same route. The name is also its route. The code runs on Deno with the Web platform; export a default handler (req: Request) => Response.',
205
+ inputSchema: {
206
+ type: 'object',
207
+ properties: {
208
+ name: str('Function name: lowercase letters, digits and hyphens.'),
209
+ entrypoint_path: str('Which of files is the entrypoint. Defaults to index.ts.'),
210
+ files: {
211
+ type: 'array',
212
+ description: 'The function source: its single entrypoint module.',
213
+ items: {
214
+ type: 'object',
215
+ properties: { name: str('File name, e.g. index.ts.'), content: str('File contents.') },
216
+ required: ['name', 'content'],
217
+ },
218
+ },
219
+ },
220
+ required: ['name', 'files'],
221
+ },
222
+ run: args => {
223
+ if (!FUNCTION_NAME.test(args.name || '')) {
224
+ throw new Error('Function names are lowercase letters, digits and hyphens (the name is also the route).');
225
+ }
226
+ const code = entrypointSource(args.files, args.entrypoint_path);
227
+ return upsertFunction(ctx, { name: args.name, route: args.name, code });
228
+ },
229
+ },
230
+ ];
231
+ }
232
+
233
+ module.exports = { consoleReadTools, consoleWriteTools, projectReadTools, projectWriteTools, entrypointSource };
@@ -1,149 +1,149 @@
1
- 'use strict';
2
-
3
- // Minimal MCP server over stdio: newline-delimited JSON-RPC 2.0 on stdin/stdout.
4
- //
5
- // Why not the official SDK: a stdio tools server needs five methods, and
6
- // `@modelcontextprotocol/sdk` pulls express, hono, cors, ajv, jose and a dozen
7
- // more to provide them. This CLI is deliberately zero-dependency, and making
8
- // `npx @motherbase/cli` install a web framework so that one subcommand can read
9
- // stdin is not a trade worth making.
10
- //
11
- // ponytail: stdio transport and the tools capability only. No resources, no
12
- // prompts, no sampling, no HTTP/SSE transport. Adopt the official SDK if any of
13
- // those is ever needed rather than growing this file into a second one.
14
-
15
- // Echoed back to the client. MCP clients negotiate by sending their own version;
16
- // answering with a version we implement is the contract, and every client in the
17
- // field accepts a server that names an older one.
18
- const PROTOCOL_VERSION = '2024-11-05';
19
-
20
- /** JSON-RPC error codes used here. */
21
- const METHOD_NOT_FOUND = -32601;
22
- const INTERNAL_ERROR = -32603;
23
- const PARSE_ERROR = -32700;
24
-
25
- /**
26
- * Reads newline-delimited JSON from `input`, dispatches to `handlers`, writes
27
- * responses to `output`.
28
- *
29
- * Split from the tool definitions so the transport can be driven by a test with
30
- * two in-memory streams and no child process.
31
- */
32
- function serve({ input, output, handlers, onError }) {
33
- let buffer = '';
34
-
35
- const write = message => {
36
- output.write(JSON.stringify(message) + '\n');
37
- };
38
-
39
- const respond = (id, result) => {
40
- // A notification (no id) gets no response. Sending one is a protocol error
41
- // that some clients treat as fatal.
42
- if (id === undefined || id === null) return;
43
- write({ jsonrpc: '2.0', id, result });
44
- };
45
-
46
- const fail = (id, code, message) => {
47
- if (id === undefined || id === null) return;
48
- write({ jsonrpc: '2.0', id, error: { code, message } });
49
- };
50
-
51
- const dispatch = async message => {
52
- const { id, method, params } = message;
53
- const handler = handlers[method];
54
- if (!handler) {
55
- // Unknown notifications are ignored rather than answered: the spec sends
56
- // several the server is not required to know about.
57
- if (id === undefined || id === null) return;
58
- return fail(id, METHOD_NOT_FOUND, `Unknown method: ${method}`);
59
- }
60
- try {
61
- respond(id, await handler(params || {}));
62
- } catch (err) {
63
- if (onError) onError(err);
64
- fail(id, INTERNAL_ERROR, err && err.message ? err.message : String(err));
65
- }
66
- };
67
-
68
- return new Promise(resolve => {
69
- input.setEncoding('utf8');
70
- input.on('data', chunk => {
71
- buffer += chunk;
72
- let index;
73
- while ((index = buffer.indexOf('\n')) >= 0) {
74
- const line = buffer.slice(0, index).trim();
75
- buffer = buffer.slice(index + 1);
76
- if (!line) continue;
77
- let message;
78
- try {
79
- message = JSON.parse(line);
80
- } catch {
81
- write({ jsonrpc: '2.0', id: null, error: { code: PARSE_ERROR, message: 'Invalid JSON' } });
82
- continue;
83
- }
84
- // Deliberately not awaited: a slow tool call must not stall the reader,
85
- // and JSON-RPC responses carry their id so they may arrive out of order.
86
- void dispatch(message);
87
- }
88
- });
89
- input.on('end', resolve);
90
- input.on('close', resolve);
91
- });
92
- }
93
-
94
- /**
95
- * Build the five handlers a tools-only server needs.
96
- *
97
- * `tools` is a list of `{ name, description, inputSchema, run }`. `run` returns
98
- * anything JSON-serialisable; it is rendered as a text content block, which is
99
- * what every current client displays and what models read best.
100
- */
101
- function toolServer({ name, version, tools }) {
102
- const byName = new Map(tools.map(tool => [tool.name, tool]));
103
-
104
- return {
105
- initialize: () => ({
106
- protocolVersion: PROTOCOL_VERSION,
107
- capabilities: { tools: {} },
108
- serverInfo: { name, version },
109
- }),
110
- 'notifications/initialized': () => undefined,
111
- ping: () => ({}),
112
- 'tools/list': () => ({
113
- tools: tools.map(({ name: toolName, description, inputSchema }) => ({
114
- name: toolName,
115
- description,
116
- inputSchema,
117
- })),
118
- }),
119
- 'tools/call': async params => {
120
- const tool = byName.get(params.name);
121
- if (!tool) {
122
- // A missing tool is reported in-band, not as a JSON-RPC error: the model
123
- // should see it and correct itself rather than the client showing a
124
- // transport failure.
125
- return {
126
- isError: true,
127
- content: [{ type: 'text', text: `Unknown tool: ${params.name}` }],
128
- };
129
- }
130
- try {
131
- const result = await tool.run(params.arguments || {});
132
- return { content: [{ type: 'text', text: render(result) }] };
133
- } catch (err) {
134
- return {
135
- isError: true,
136
- content: [{ type: 'text', text: err && err.message ? err.message : String(err) }],
137
- };
138
- }
139
- },
140
- };
141
- }
142
-
143
- function render(value) {
144
- if (typeof value === 'string') return value;
145
- if (value === undefined) return 'ok';
146
- return JSON.stringify(value, null, 2);
147
- }
148
-
149
- module.exports = { serve, toolServer, PROTOCOL_VERSION };
1
+ 'use strict';
2
+
3
+ // Minimal MCP server over stdio: newline-delimited JSON-RPC 2.0 on stdin/stdout.
4
+ //
5
+ // Why not the official SDK: a stdio tools server needs five methods, and
6
+ // `@modelcontextprotocol/sdk` pulls express, hono, cors, ajv, jose and a dozen
7
+ // more to provide them. This CLI is deliberately zero-dependency, and making
8
+ // `npx @motherbase/cli` install a web framework so that one subcommand can read
9
+ // stdin is not a trade worth making.
10
+ //
11
+ // ponytail: stdio transport and the tools capability only. No resources, no
12
+ // prompts, no sampling, no HTTP/SSE transport. Adopt the official SDK if any of
13
+ // those is ever needed rather than growing this file into a second one.
14
+
15
+ // Echoed back to the client. MCP clients negotiate by sending their own version;
16
+ // answering with a version we implement is the contract, and every client in the
17
+ // field accepts a server that names an older one.
18
+ const PROTOCOL_VERSION = '2024-11-05';
19
+
20
+ /** JSON-RPC error codes used here. */
21
+ const METHOD_NOT_FOUND = -32601;
22
+ const INTERNAL_ERROR = -32603;
23
+ const PARSE_ERROR = -32700;
24
+
25
+ /**
26
+ * Reads newline-delimited JSON from `input`, dispatches to `handlers`, writes
27
+ * responses to `output`.
28
+ *
29
+ * Split from the tool definitions so the transport can be driven by a test with
30
+ * two in-memory streams and no child process.
31
+ */
32
+ function serve({ input, output, handlers, onError }) {
33
+ let buffer = '';
34
+
35
+ const write = message => {
36
+ output.write(JSON.stringify(message) + '\n');
37
+ };
38
+
39
+ const respond = (id, result) => {
40
+ // A notification (no id) gets no response. Sending one is a protocol error
41
+ // that some clients treat as fatal.
42
+ if (id === undefined || id === null) return;
43
+ write({ jsonrpc: '2.0', id, result });
44
+ };
45
+
46
+ const fail = (id, code, message) => {
47
+ if (id === undefined || id === null) return;
48
+ write({ jsonrpc: '2.0', id, error: { code, message } });
49
+ };
50
+
51
+ const dispatch = async message => {
52
+ const { id, method, params } = message;
53
+ const handler = handlers[method];
54
+ if (!handler) {
55
+ // Unknown notifications are ignored rather than answered: the spec sends
56
+ // several the server is not required to know about.
57
+ if (id === undefined || id === null) return;
58
+ return fail(id, METHOD_NOT_FOUND, `Unknown method: ${method}`);
59
+ }
60
+ try {
61
+ respond(id, await handler(params || {}));
62
+ } catch (err) {
63
+ if (onError) onError(err);
64
+ fail(id, INTERNAL_ERROR, err && err.message ? err.message : String(err));
65
+ }
66
+ };
67
+
68
+ return new Promise(resolve => {
69
+ input.setEncoding('utf8');
70
+ input.on('data', chunk => {
71
+ buffer += chunk;
72
+ let index;
73
+ while ((index = buffer.indexOf('\n')) >= 0) {
74
+ const line = buffer.slice(0, index).trim();
75
+ buffer = buffer.slice(index + 1);
76
+ if (!line) continue;
77
+ let message;
78
+ try {
79
+ message = JSON.parse(line);
80
+ } catch {
81
+ write({ jsonrpc: '2.0', id: null, error: { code: PARSE_ERROR, message: 'Invalid JSON' } });
82
+ continue;
83
+ }
84
+ // Deliberately not awaited: a slow tool call must not stall the reader,
85
+ // and JSON-RPC responses carry their id so they may arrive out of order.
86
+ void dispatch(message);
87
+ }
88
+ });
89
+ input.on('end', resolve);
90
+ input.on('close', resolve);
91
+ });
92
+ }
93
+
94
+ /**
95
+ * Build the five handlers a tools-only server needs.
96
+ *
97
+ * `tools` is a list of `{ name, description, inputSchema, run }`. `run` returns
98
+ * anything JSON-serialisable; it is rendered as a text content block, which is
99
+ * what every current client displays and what models read best.
100
+ */
101
+ function toolServer({ name, version, tools }) {
102
+ const byName = new Map(tools.map(tool => [tool.name, tool]));
103
+
104
+ return {
105
+ initialize: () => ({
106
+ protocolVersion: PROTOCOL_VERSION,
107
+ capabilities: { tools: {} },
108
+ serverInfo: { name, version },
109
+ }),
110
+ 'notifications/initialized': () => undefined,
111
+ ping: () => ({}),
112
+ 'tools/list': () => ({
113
+ tools: tools.map(({ name: toolName, description, inputSchema }) => ({
114
+ name: toolName,
115
+ description,
116
+ inputSchema,
117
+ })),
118
+ }),
119
+ 'tools/call': async params => {
120
+ const tool = byName.get(params.name);
121
+ if (!tool) {
122
+ // A missing tool is reported in-band, not as a JSON-RPC error: the model
123
+ // should see it and correct itself rather than the client showing a
124
+ // transport failure.
125
+ return {
126
+ isError: true,
127
+ content: [{ type: 'text', text: `Unknown tool: ${params.name}` }],
128
+ };
129
+ }
130
+ try {
131
+ const result = await tool.run(params.arguments || {});
132
+ return { content: [{ type: 'text', text: render(result) }] };
133
+ } catch (err) {
134
+ return {
135
+ isError: true,
136
+ content: [{ type: 'text', text: err && err.message ? err.message : String(err) }],
137
+ };
138
+ }
139
+ },
140
+ };
141
+ }
142
+
143
+ function render(value) {
144
+ if (typeof value === 'string') return value;
145
+ if (value === undefined) return 'ok';
146
+ return JSON.stringify(value, null, 2);
147
+ }
148
+
149
+ module.exports = { serve, toolServer, PROTOCOL_VERSION };
@@ -0,0 +1,93 @@
1
+ 'use strict';
2
+
3
+ // Storage configuration over MCP, the counterpart of Supabase's
4
+ // get_storage_config / update_storage_config. Motherbase keeps that
5
+ // configuration in three settings groups plus the S3 endpoint, so these two
6
+ // tools fold them back into the single view an agent expects.
7
+
8
+ const api = require('../api');
9
+
10
+ /** Setting group -> the tool argument that edits each of its fields. */
11
+ const WRITABLE = {
12
+ 'storage.limits': { maxUploadBytes: 'maxUploadBytes', allowedMimeTypes: 'allowedMimeTypes' },
13
+ 'storage.imageTransform': { enabled: 'imageTransformEnabled', maxSourceBytes: 'imageTransformMaxSourceBytes' },
14
+ 'storage.policies': { enabled: 'policiesEnabled' },
15
+ };
16
+
17
+ /** Groups the arguments touch, each with the field values to change. */
18
+ function changesByGroup(args) {
19
+ const changes = {};
20
+ for (const [group, fields] of Object.entries(WRITABLE)) {
21
+ for (const [field, argName] of Object.entries(fields)) {
22
+ if (args[argName] === undefined) continue;
23
+ const value = Array.isArray(args[argName]) ? args[argName].join(', ') : args[argName];
24
+ (changes[group] ||= {})[field] = value;
25
+ }
26
+ }
27
+ return changes;
28
+ }
29
+
30
+ const valuesOf = group => (group && group.values) || {};
31
+
32
+ function readStorageTools(ctx) {
33
+ return [
34
+ {
35
+ name: 'get_storage_config',
36
+ description:
37
+ 'Storage configuration: the global upload size limit and allowed MIME types, image transformations, whether PostgreSQL policies authorize storage access, and the S3 protocol endpoint. Per-bucket limits are on list_storage_buckets.',
38
+ inputSchema: { type: 'object', properties: {} },
39
+ run: async () => {
40
+ const [limits, imageTransform, policies, s3] = await Promise.all([
41
+ api.adminRequest(ctx, 'GET', '/settings/storage.limits'),
42
+ api.adminRequest(ctx, 'GET', '/settings/storage.imageTransform'),
43
+ api.adminRequest(ctx, 'GET', '/settings/storage.policies'),
44
+ api.adminRequest(ctx, 'GET', '/storage/s3'),
45
+ ]);
46
+ return {
47
+ limits: valuesOf(limits),
48
+ imageTransform: valuesOf(imageTransform),
49
+ policies: valuesOf(policies),
50
+ s3,
51
+ };
52
+ },
53
+ },
54
+ ];
55
+ }
56
+
57
+ function writeStorageTools(ctx) {
58
+ return [
59
+ {
60
+ name: 'update_storage_config',
61
+ description:
62
+ 'Change the global storage configuration. Only the fields you pass change. Per-bucket size limits and MIME types are edited on the bucket itself.',
63
+ inputSchema: {
64
+ type: 'object',
65
+ properties: {
66
+ maxUploadBytes: { type: 'integer', minimum: 0, description: 'Largest upload in bytes. 0 uses the server default.' },
67
+ allowedMimeTypes: {
68
+ type: 'array',
69
+ items: { type: 'string' },
70
+ description: 'Allowed MIME types, wildcards like image/* supported. Empty allows everything.',
71
+ },
72
+ imageTransformEnabled: { type: 'boolean', description: 'Serve resized images from /storage/v1/render/image.' },
73
+ imageTransformMaxSourceBytes: { type: 'integer', minimum: 1, description: 'Largest image, in bytes, that will be transformed.' },
74
+ policiesEnabled: { type: 'boolean', description: 'Authorize storage access with RLS policies on storage.files.' },
75
+ },
76
+ },
77
+ run: async args => {
78
+ const changes = changesByGroup(args);
79
+ if (!Object.keys(changes).length) throw new Error('Pass at least one field to change.');
80
+ // The settings API replaces a group wholesale, so an untouched field
81
+ // would fall back to its default unless it is sent back as it is.
82
+ const saved = {};
83
+ for (const [group, fields] of Object.entries(changes)) {
84
+ const current = valuesOf(await api.adminRequest(ctx, 'GET', `/settings/${group}`));
85
+ saved[group] = await api.adminRequest(ctx, 'PUT', `/settings/${group}`, { body: { ...current, ...fields } });
86
+ }
87
+ return saved;
88
+ },
89
+ },
90
+ ];
91
+ }
92
+
93
+ module.exports = { readStorageTools, writeStorageTools, changesByGroup };