@motherbase/cli 0.2.4 → 0.2.6
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 -0
- package/README.md +131 -18
- package/bin/motherbase.js +10 -10
- package/package.json +35 -26
- package/src/api.js +233 -229
- package/src/commands/_util.js +106 -50
- package/src/commands/backups.js +16 -0
- package/src/commands/branches.js +113 -0
- package/src/commands/completion.js +90 -0
- package/src/commands/config.js +47 -47
- package/src/commands/cron.js +65 -65
- package/src/commands/db-catalog.js +326 -0
- package/src/commands/db.js +103 -92
- package/src/commands/domains.js +124 -0
- package/src/commands/functions.js +63 -33
- package/src/commands/gen.js +63 -17
- package/src/commands/import.js +55 -35
- package/src/commands/index.js +85 -64
- package/src/commands/inspect.js +178 -41
- package/src/commands/link.js +35 -35
- package/src/commands/local.js +23 -2
- package/src/commands/login.js +39 -39
- package/src/commands/logout.js +16 -16
- package/src/commands/mcp.js +27 -9
- package/src/commands/migrate.js +207 -0
- package/src/commands/migration.js +108 -33
- package/src/commands/network.js +83 -0
- package/src/commands/orgs.js +40 -0
- package/src/commands/postgres-config.js +113 -0
- package/src/commands/projects.js +130 -125
- package/src/commands/queues.js +133 -0
- package/src/commands/realtime.js +35 -35
- package/src/commands/rls.js +176 -176
- package/src/commands/secrets.js +66 -45
- package/src/commands/signing.js +130 -0
- package/src/commands/snippets.js +44 -0
- package/src/commands/status.js +42 -42
- package/src/commands/storage.js +261 -113
- package/src/commands/test.js +9 -0
- package/src/commands/unlink.js +15 -15
- package/src/commands/users.js +142 -113
- package/src/commands/vault.js +82 -0
- package/src/commands/webhooks.js +62 -62
- package/src/commands/whoami.js +20 -20
- package/src/commands/wrappers.js +150 -0
- package/src/config.js +98 -98
- package/src/context.js +106 -102
- package/src/docs.js +68 -0
- package/src/edge-templates.js +194 -0
- package/src/format.js +92 -0
- package/src/inspect/csv.js +22 -0
- package/src/inspect/queries.js +562 -0
- package/src/main.js +224 -160
- package/src/mcp/platform-tools.js +233 -0
- package/src/mcp/protocol.js +4 -1
- package/src/mcp/storage-tools.js +93 -0
- package/src/mcp/tools.js +318 -6
- package/src/migration/commands.js +375 -0
- package/src/migration/files.js +245 -0
- package/src/prompt.js +91 -91
- package/src/storage/remote.js +62 -0
- package/src/ui.js +111 -111
- package/src/commands/migrations.js +0 -85
package/src/main.js
CHANGED
|
@@ -1,160 +1,224 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
const path = require('path');
|
|
4
|
-
const { registry, groups } = require('./commands');
|
|
5
|
-
const { createContext, CliError } = require('./context');
|
|
6
|
-
const
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
// `--check
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
//
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
if (
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
if (!
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const { registry, groups } = require('./commands');
|
|
5
|
+
const { createContext, CliError } = require('./context');
|
|
6
|
+
const { FORMATS } = require('./format');
|
|
7
|
+
const ui = require('./ui');
|
|
8
|
+
|
|
9
|
+
// Flags that never consume a following value.
|
|
10
|
+
const PURE_BOOLEAN = new Set([
|
|
11
|
+
'json', 'yes', 'help', 'version', 'force', 'preview', 'app', 'no-data', 'no-auth', 'f', 'debug',
|
|
12
|
+
// Codegen: `--check` and `--local` never take a value, so a trailing
|
|
13
|
+
// `--check database.types.ts` must not silently swallow the path.
|
|
14
|
+
'check', 'local',
|
|
15
|
+
// `rls preset <table> <preset> --replace` must not swallow a following word.
|
|
16
|
+
'replace',
|
|
17
|
+
// `storage cp --recursive ./dir ss:///bucket` must not swallow the source.
|
|
18
|
+
'recursive',
|
|
19
|
+
// Migration workflow switches: `db push --dry-run` must not eat a positional.
|
|
20
|
+
'dry-run', 'include-seed', 'include-all', 'schema-only', 'data-only', 'use-copy', 'confirm', 'no-seed', 'linked',
|
|
21
|
+
// `postgres-config` and `db publications` switches, never followed by a value.
|
|
22
|
+
'replace-existing-overrides', 'no-restart', 'all-tables',
|
|
23
|
+
// Branching, keys and database access: trailing switches that must not
|
|
24
|
+
// swallow the next word.
|
|
25
|
+
'with-data', 'append', 'enable-db-ssl-enforcement', 'disable-db-ssl-enforcement',
|
|
26
|
+
]);
|
|
27
|
+
// Short flag aliases → canonical (with whether they take a value).
|
|
28
|
+
const SHORT = {
|
|
29
|
+
y: { name: 'yes', boolean: true },
|
|
30
|
+
h: { name: 'help', boolean: true },
|
|
31
|
+
v: { name: 'version', boolean: true },
|
|
32
|
+
p: { name: 'project', boolean: false },
|
|
33
|
+
u: { name: 'url', boolean: false },
|
|
34
|
+
e: { name: 'e', boolean: false },
|
|
35
|
+
f: { name: 'f', boolean: true },
|
|
36
|
+
o: { name: 'output', boolean: false },
|
|
37
|
+
r: { name: 'recursive', boolean: true },
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Is this token the next flag rather than a value that happens to start with
|
|
42
|
+
* `-`? `--anything` is: long flags are free-form here, so the parser accepts
|
|
43
|
+
* any name. A single-dash token only is when `SHORT` declares it, which is why
|
|
44
|
+
* `-s3cret!` and `-5` are not.
|
|
45
|
+
*
|
|
46
|
+
* PURE_BOOLEAN cannot answer this on its own. It lists the flags that must not
|
|
47
|
+
* swallow a following *positional* (`db push --dry-run` before a path), and a
|
|
48
|
+
* switch with no positional after it was never added to it -- `--superuser`,
|
|
49
|
+
* `--write`, `--no-storage`. Treating every switch missing from that list as a
|
|
50
|
+
* flag that needs a value is what turned `db roles create app --login
|
|
51
|
+
* --superuser` into an error.
|
|
52
|
+
*/
|
|
53
|
+
function isFlagToken(token) {
|
|
54
|
+
if (token.startsWith('--')) return token.length > 2;
|
|
55
|
+
return Object.prototype.hasOwnProperty.call(SHORT, token.slice(1));
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* A value flag followed by a dash-leading token that is *not* a flag is
|
|
60
|
+
* ambiguous: the token could be the value or a typo. This used to resolve
|
|
61
|
+
* silently in the worst direction -- the flag became a bare `true` and the
|
|
62
|
+
* value was re-parsed as short flags, so `--password -s3cret!` sent no password
|
|
63
|
+
* and set `-s`, `-3`, `-c`... The `--flag=value` form is unambiguous, so say so
|
|
64
|
+
* instead of guessing (PKG-16).
|
|
65
|
+
*/
|
|
66
|
+
function dashValue(flag, value) {
|
|
67
|
+
return new CliError(`${flag} needs a value, and "${value}" starts with "-". Write ${flag}=${value} instead.`);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Tokenize argv into positionals + flags. Repeated flags collapse into arrays.
|
|
71
|
+
function tokenize(argv) {
|
|
72
|
+
const positionals = [];
|
|
73
|
+
const flags = {};
|
|
74
|
+
const setFlag = (key, value) => {
|
|
75
|
+
if (key in flags) {
|
|
76
|
+
flags[key] = [].concat(flags[key], value);
|
|
77
|
+
} else {
|
|
78
|
+
flags[key] = value;
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
for (let i = 0; i < argv.length; i++) {
|
|
83
|
+
const tok = argv[i];
|
|
84
|
+
if (tok === '--') { positionals.push(...argv.slice(i + 1)); break; }
|
|
85
|
+
|
|
86
|
+
if (tok.startsWith('--')) {
|
|
87
|
+
const body = tok.slice(2);
|
|
88
|
+
const eq = body.indexOf('=');
|
|
89
|
+
if (eq >= 0) { setFlag(body.slice(0, eq), body.slice(eq + 1)); continue; }
|
|
90
|
+
const next = argv[i + 1];
|
|
91
|
+
if (PURE_BOOLEAN.has(body)) { setFlag(body, true); continue; }
|
|
92
|
+
if (next === undefined || isFlagToken(next)) { setFlag(body, true); continue; }
|
|
93
|
+
if (next.startsWith('-')) throw dashValue(`--${body}`, next);
|
|
94
|
+
setFlag(body, next); i++;
|
|
95
|
+
} else if (tok.startsWith('-') && tok.length > 1 && tok !== '-') {
|
|
96
|
+
const spec = SHORT[tok.slice(1)];
|
|
97
|
+
if (!spec) { setFlag(tok.slice(1), true); continue; }
|
|
98
|
+
const next = argv[i + 1];
|
|
99
|
+
if (spec.boolean || next === undefined || isFlagToken(next)) { setFlag(spec.name, true); continue; }
|
|
100
|
+
if (next.startsWith('-')) throw dashValue(`--${spec.name}`, next);
|
|
101
|
+
setFlag(spec.name, next); i++;
|
|
102
|
+
} else {
|
|
103
|
+
positionals.push(tok);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return { positionals, flags };
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Fold the Supabase spellings of global flags into the names commands read:
|
|
110
|
+
// `--project-ref` is `--project`, and `-o/--output <format>` becomes `format`
|
|
111
|
+
// (with `json` kept in step, since commands test it for machine output).
|
|
112
|
+
// `--output` stays a file path on commands that declare `outputIsPath`.
|
|
113
|
+
function normalizeFlags(flags, command = {}) {
|
|
114
|
+
const out = { ...flags };
|
|
115
|
+
if (out.project === undefined && out['project-ref'] !== undefined) out.project = out['project-ref'];
|
|
116
|
+
if (out.output !== undefined && FORMATS.includes(out.output)) {
|
|
117
|
+
out.format = out.output;
|
|
118
|
+
delete out.output;
|
|
119
|
+
} else if (out.output !== undefined && !command.outputIsPath) {
|
|
120
|
+
throw new CliError(`--output must be one of: ${FORMATS.join(', ')}`);
|
|
121
|
+
}
|
|
122
|
+
out.format = out.format || (out.json ? 'json' : 'pretty');
|
|
123
|
+
out.json = out.format === 'json';
|
|
124
|
+
return out;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function version() {
|
|
128
|
+
try {
|
|
129
|
+
return require(path.join(__dirname, '..', 'package.json')).version;
|
|
130
|
+
} catch { return '0.0.0'; }
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function pad(s, n) { return String(s).padEnd(n); }
|
|
134
|
+
|
|
135
|
+
function topLevelHelp() {
|
|
136
|
+
ui.print(ui.color.bold('Motherbase CLI') + ` v${version()}`);
|
|
137
|
+
ui.print('');
|
|
138
|
+
ui.print('Usage: motherbase <command> [subcommand] [args] [flags]');
|
|
139
|
+
ui.print('');
|
|
140
|
+
const width = Object.keys(registry).reduce((m, k) => Math.max(m, k.length), 0) + 2;
|
|
141
|
+
for (const group of groups) {
|
|
142
|
+
ui.print(ui.color.bold(group.title));
|
|
143
|
+
for (const name of group.names) {
|
|
144
|
+
const entry = registry[name];
|
|
145
|
+
if (!entry) continue;
|
|
146
|
+
ui.print(' ' + ui.color.cyan(pad(name, width)) + ui.color.dim(entry.summary || ''));
|
|
147
|
+
}
|
|
148
|
+
ui.print('');
|
|
149
|
+
}
|
|
150
|
+
ui.print('Global flags: -o/--output pretty|json|yaml|env --json --yes/-y --project/-p/--project-ref <id> --url/-u <consoleUrl>');
|
|
151
|
+
ui.print(' --token <t> --workdir <dir> --debug -h/--help -v/--version');
|
|
152
|
+
ui.print('Run `motherbase <command> -h` for command-specific help.');
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function commandHelp(name, entry, trail) {
|
|
156
|
+
const full = ['motherbase', ...trail].join(' ');
|
|
157
|
+
ui.print(ui.color.bold(full) + (entry.summary ? `, ${entry.summary}` : ''));
|
|
158
|
+
ui.print('');
|
|
159
|
+
if (entry.usage) { ui.print('Usage: ' + entry.usage); ui.print(''); }
|
|
160
|
+
if (entry.subcommands) {
|
|
161
|
+
ui.print(ui.color.bold('Subcommands'));
|
|
162
|
+
const subs = Object.keys(entry.subcommands);
|
|
163
|
+
const width = subs.reduce((m, k) => Math.max(m, k.length), 0) + 2;
|
|
164
|
+
for (const sub of subs) {
|
|
165
|
+
const s = entry.subcommands[sub];
|
|
166
|
+
ui.print(' ' + ui.color.cyan(pad(sub, width)) + ui.color.dim(s.summary || ''));
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
async function main(argv) {
|
|
172
|
+
const { positionals, flags } = tokenize(argv);
|
|
173
|
+
|
|
174
|
+
// Only bare: `migration squash --version <v>` is Supabase's own flag.
|
|
175
|
+
if (flags.version && positionals.length === 0) { ui.print(version()); return 0; }
|
|
176
|
+
|
|
177
|
+
if (positionals.length === 0) { topLevelHelp(); return 0; }
|
|
178
|
+
|
|
179
|
+
const name = positionals[0];
|
|
180
|
+
const entry = registry[name];
|
|
181
|
+
if (!entry) {
|
|
182
|
+
ui.printErr(ui.color.red(`Unknown command: ${name}`));
|
|
183
|
+
ui.printErr('Run `motherbase --help` to see available commands.');
|
|
184
|
+
return 1;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Descend into subcommands.
|
|
188
|
+
let current = entry;
|
|
189
|
+
const trail = [name];
|
|
190
|
+
const rest = positionals.slice(1);
|
|
191
|
+
while (current.subcommands && rest.length && current.subcommands[rest[0]]) {
|
|
192
|
+
current = current.subcommands[rest[0]];
|
|
193
|
+
trail.push(rest.shift());
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Help requested, or a group with no runnable action here.
|
|
197
|
+
if (flags.help || (!current.run && !current.subcommands)) {
|
|
198
|
+
commandHelp(name, current, trail);
|
|
199
|
+
return 0;
|
|
200
|
+
}
|
|
201
|
+
if (!current.run) {
|
|
202
|
+
// Group node: either no subcommand given or an unknown one.
|
|
203
|
+
if (rest.length) ui.printErr(ui.color.red(`Unknown subcommand: ${trail.join(' ')} ${rest[0]}`));
|
|
204
|
+
commandHelp(name, current, trail);
|
|
205
|
+
return rest.length ? 1 : 0;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
try {
|
|
209
|
+
const resolved = normalizeFlags(flags, current);
|
|
210
|
+
if (resolved.debug) process.env.MOTHERBASE_DEBUG = '1';
|
|
211
|
+
const cwd = resolved.workdir ? path.resolve(String(resolved.workdir)) : process.cwd();
|
|
212
|
+
await current.run(createContext(resolved, process.env, cwd), rest, resolved);
|
|
213
|
+
return 0;
|
|
214
|
+
} catch (err) {
|
|
215
|
+
if (err instanceof CliError || err.isCliError) {
|
|
216
|
+
ui.printErr(ui.color.red(err.message));
|
|
217
|
+
} else {
|
|
218
|
+
ui.printErr(ui.color.red(`Error: ${err.message}`));
|
|
219
|
+
}
|
|
220
|
+
return 1;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
module.exports = { main, tokenize, normalizeFlags, version };
|
|
@@ -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 };
|
package/src/mcp/protocol.js
CHANGED
|
@@ -49,8 +49,11 @@ function serve({ input, output, handlers, onError }) {
|
|
|
49
49
|
};
|
|
50
50
|
|
|
51
51
|
const dispatch = async message => {
|
|
52
|
+
// Valid JSON is not necessarily a request: `null` or `42` would throw on
|
|
53
|
+
// destructuring and, un-awaited, take the process down.
|
|
54
|
+
if (message === null || typeof message !== 'object') return;
|
|
52
55
|
const { id, method, params } = message;
|
|
53
|
-
const handler = handlers[method];
|
|
56
|
+
const handler = Object.hasOwn(handlers, method) ? handlers[method] : undefined;
|
|
54
57
|
if (!handler) {
|
|
55
58
|
// Unknown notifications are ignored rather than answered: the spec sends
|
|
56
59
|
// several the server is not required to know about.
|