@exposify/supabase 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +228 -0
- package/dist/cli.js +266 -0
- package/dist/index.cjs +718 -0
- package/dist/index.d.cts +171 -0
- package/dist/index.d.ts +171 -0
- package/dist/index.js +687 -0
- package/package.json +43 -0
- package/sql/agentify_meta.sql +46 -0
package/README.md
ADDED
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
# @exposify/supabase — setup guide
|
|
2
|
+
|
|
3
|
+
Make a Supabase-backed app's database and edge functions usable by AI agents:
|
|
4
|
+
one edge function serves MCP, with tools generated from your database
|
|
5
|
+
(PostgREST + Row Level Security) plus any edge functions you explicitly
|
|
6
|
+
register. The caller's JWT passes straight through to PostgREST — **RLS is
|
|
7
|
+
the authorization layer**. The agent can do exactly what that logged-in user
|
|
8
|
+
can do, nothing more.
|
|
9
|
+
|
|
10
|
+
This guide walks through setup end to end. For the design rationale, see
|
|
11
|
+
`docs/superpowers/specs/2026-08-15-agentify-supabase-design.md` in the repo
|
|
12
|
+
root.
|
|
13
|
+
|
|
14
|
+
## Prerequisites
|
|
15
|
+
|
|
16
|
+
- A Supabase project (local via the [Supabase CLI](https://supabase.com/docs/guides/cli), or hosted).
|
|
17
|
+
- The Supabase CLI (`supabase` on your `PATH`) for `db push` / `functions deploy`.
|
|
18
|
+
- Node or Yarn to run `npx agentify-supabase` (the CLI is a regular npm
|
|
19
|
+
package; the edge function itself runs on Deno and needs no local install).
|
|
20
|
+
|
|
21
|
+
## 1. Scaffold
|
|
22
|
+
|
|
23
|
+
From your Supabase project's root (the directory containing `supabase/`):
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
npx agentify-supabase init
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
This writes two things and never overwrites an existing function file:
|
|
30
|
+
|
|
31
|
+
- `supabase/migrations/<timestamp>_agentify_meta.sql` — a SQL migration that
|
|
32
|
+
adds `agentify_meta()`, a `SECURITY DEFINER` function returning, per table,
|
|
33
|
+
whether RLS is enabled and any `COMMENT ON TABLE`/`COMMENT ON COLUMN` text
|
|
34
|
+
you've written. It only ever returns catalog metadata (table/column names,
|
|
35
|
+
flags, comments) — never row data — which is what makes it safe to grant
|
|
36
|
+
to `anon`/`authenticated` and call with a low-privilege key.
|
|
37
|
+
- `supabase/functions/mcp/index.ts` — the edge function itself, a few lines
|
|
38
|
+
around `aiReady()`.
|
|
39
|
+
- `supabase/functions/mcp/docs/` — an empty directory for optional bundled
|
|
40
|
+
docs (see "Knowledge base" below).
|
|
41
|
+
|
|
42
|
+
`init` is interactive from there. If the Supabase CLI is on your `PATH` and
|
|
43
|
+
you're running it in a real terminal, it asks:
|
|
44
|
+
|
|
45
|
+
```
|
|
46
|
+
Apply the migration and deploy the function to whatever Supabase project is
|
|
47
|
+
currently linked, now? [y/N]
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Answering **y** runs `supabase db push` then `supabase functions deploy mcp`
|
|
51
|
+
for you, streaming their output. The default is **no** — pressing Enter, the
|
|
52
|
+
Supabase CLI not being installed, or running `init` non-interactively (CI, a
|
|
53
|
+
script, piped input) all skip straight to printing the commands instead:
|
|
54
|
+
|
|
55
|
+
```
|
|
56
|
+
Run these to finish setup:
|
|
57
|
+
1. supabase db push
|
|
58
|
+
2. supabase functions deploy mcp
|
|
59
|
+
3. Paste the function URL + a user JWT into an MCP client.
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
That's deliberate: `db push`/`functions deploy` act on whatever project is
|
|
63
|
+
*currently linked*, which may not be a throwaway one, so nothing gets pushed
|
|
64
|
+
without an explicit yes. If you want to edit `writes`/`rpc`/`functions` (next
|
|
65
|
+
section) before the first deploy, just say no here and run the printed
|
|
66
|
+
commands yourself once you're done editing.
|
|
67
|
+
|
|
68
|
+
## 2. Decide what's exposed
|
|
69
|
+
|
|
70
|
+
**Every RLS-enabled table is automatically readable.** A table only shows up
|
|
71
|
+
at all if `agentify_meta()` reports RLS is turned on for it *and* PostgREST
|
|
72
|
+
actually exposes it (the calling role has grants) — an RLS-less table is
|
|
73
|
+
invisible, full stop, with no override. For each exposed table you get a
|
|
74
|
+
`query_<table>` tool: typed filters (`eq`/`neq`/`gt`/`lt`/`like`), a `select`
|
|
75
|
+
column list, `order`, and a `limit` capped server-side (100 rows by default,
|
|
76
|
+
regardless of what the caller asks for).
|
|
77
|
+
|
|
78
|
+
Writes and Postgres functions are **opt-in**, edited in
|
|
79
|
+
`supabase/functions/mcp/index.ts`:
|
|
80
|
+
|
|
81
|
+
```ts
|
|
82
|
+
import { aiReady } from "npm:@exposify/supabase";
|
|
83
|
+
|
|
84
|
+
Deno.serve(aiReady({
|
|
85
|
+
// "<table>.insert" | "<table>.update" | "<table>.delete" | "<table>.*"
|
|
86
|
+
writes: ["orders.insert", "orders.update"],
|
|
87
|
+
|
|
88
|
+
// Postgres functions exposed as rpc_<fn> tools
|
|
89
|
+
rpc: ["place_order"],
|
|
90
|
+
|
|
91
|
+
// Sibling edge functions, invoked via fetch with the caller's forwarded JWT
|
|
92
|
+
functions: {
|
|
93
|
+
refund: {
|
|
94
|
+
description: "Refund an order",
|
|
95
|
+
input: {
|
|
96
|
+
type: "object",
|
|
97
|
+
properties: { orderId: { type: "string", description: "Order to refund" } },
|
|
98
|
+
required: ["orderId"],
|
|
99
|
+
},
|
|
100
|
+
},
|
|
101
|
+
},
|
|
102
|
+
}));
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
Notes:
|
|
106
|
+
- `update_<table>` and `delete_<table>` always require at least one filter —
|
|
107
|
+
there is no way to opt into a table-wide mutation, even where RLS would
|
|
108
|
+
technically allow it.
|
|
109
|
+
- Column/table descriptions shown to the agent come from SQL comments
|
|
110
|
+
(`COMMENT ON TABLE orders IS '...'`, `COMMENT ON COLUMN orders.status IS
|
|
111
|
+
'...'`) — write these once and every generated tool reads them.
|
|
112
|
+
- A registered `functions` entry's `input` is a small JSON Schema subset
|
|
113
|
+
(`object`/`string`/`number`/`integer`/`boolean`/`array`, `enum`,
|
|
114
|
+
`required`) — enough for a flat tool-input shape, not a full JSON Schema
|
|
115
|
+
implementation.
|
|
116
|
+
|
|
117
|
+
Other options you can pass to `aiReady()`:
|
|
118
|
+
|
|
119
|
+
| Option | Default | What it does |
|
|
120
|
+
|---|---|---|
|
|
121
|
+
| `docs` | `["docs/**/*.md"]` | Glob(s), relative to the function directory, bundled into the knowledge base |
|
|
122
|
+
| `docsAuth` | `"public"` | `"required"` hides `search_docs` until the caller sends *any* `Authorization` header (presence-only, not validated) |
|
|
123
|
+
| `maxRows` | `100` | Hard cap applied to every `query_<table>` call |
|
|
124
|
+
| `rateLimit` / `rateWindowMs` | `60` / `60000` | In-memory, per-instance, best-effort rate limit — **not** distributed across edge instances; real limits belong to Supabase's own gateway |
|
|
125
|
+
| `supabaseUrl` / `anonKey` | read from `SUPABASE_URL` / `SUPABASE_ANON_KEY` env vars | Override for local dev/testing; in a deployed function the env vars are automatic |
|
|
126
|
+
|
|
127
|
+
## 3. Apply the migration and deploy
|
|
128
|
+
|
|
129
|
+
If you didn't let `init` do this for you (or you've since edited `writes`/
|
|
130
|
+
`rpc`/`functions`/`docs` and need to redeploy):
|
|
131
|
+
|
|
132
|
+
```bash
|
|
133
|
+
supabase db push
|
|
134
|
+
supabase functions deploy mcp
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
Only `functions deploy` is needed for a config-only change to `index.ts`;
|
|
138
|
+
`db push` is only needed again if the migration itself changed (it hasn't,
|
|
139
|
+
unless you're editing `sql/agentify_meta.sql` directly).
|
|
140
|
+
|
|
141
|
+
(Local dev: `supabase start` then `supabase functions serve mcp` instead of
|
|
142
|
+
`deploy`.)
|
|
143
|
+
|
|
144
|
+
## 4. Connect an MCP client
|
|
145
|
+
|
|
146
|
+
Point any MCP client at the function's URL with a real user's JWT as the
|
|
147
|
+
bearer token — for example, with the Claude Code CLI:
|
|
148
|
+
|
|
149
|
+
```bash
|
|
150
|
+
claude mcp add --transport http my-app https://<project-ref>.supabase.co/functions/v1/mcp \
|
|
151
|
+
--header "Authorization: Bearer <user-jwt>"
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
Ask the agent to query or mutate data. **It should succeed for that user's
|
|
155
|
+
own rows and be refused everyone else's** — that's the definition of done:
|
|
156
|
+
RLS enforcement, not anything in this SDK, is what makes that true.
|
|
157
|
+
|
|
158
|
+
## Knowledge base
|
|
159
|
+
|
|
160
|
+
Any markdown files matching the `docs` glob (default `docs/**/*.md`,
|
|
161
|
+
relative to `supabase/functions/mcp/`) are bundled with the function,
|
|
162
|
+
chunked, and indexed with BM25 the first time the function handles a
|
|
163
|
+
request in a given instance. There's no cache file and no invalidation
|
|
164
|
+
logic — redeploying the function *is* the invalidation. Agents can search
|
|
165
|
+
them via the `search_docs` tool.
|
|
166
|
+
|
|
167
|
+
## When PostgREST is unreachable: the fallback ladder
|
|
168
|
+
|
|
169
|
+
Every request tries, in order:
|
|
170
|
+
|
|
171
|
+
1. **Runtime introspection** — fetch PostgREST's OpenAPI document and call
|
|
172
|
+
`agentify_meta()`. This is the normal path; the result is cached in
|
|
173
|
+
memory for the life of the edge instance, not refetched per call.
|
|
174
|
+
2. **Bundled manifest** — if step 1 fails, and a `agentify.manifest.json`
|
|
175
|
+
was generated ahead of time (see below), tools are built from that
|
|
176
|
+
instead.
|
|
177
|
+
3. **Functions + knowledge base only** — if neither works, the function
|
|
178
|
+
still serves any registered `functions` tools and `search_docs`, never
|
|
179
|
+
crashes, and says so.
|
|
180
|
+
|
|
181
|
+
Whichever rung actually served the request is disclosed to the MCP client
|
|
182
|
+
in the `initialize` handshake's `instructions` field, so a degraded server
|
|
183
|
+
never looks silently healthy.
|
|
184
|
+
|
|
185
|
+
To generate the fallback manifest ahead of time, run against a reachable
|
|
186
|
+
project (local `supabase start` or linked):
|
|
187
|
+
|
|
188
|
+
```bash
|
|
189
|
+
SUPABASE_URL=http://localhost:54321 \
|
|
190
|
+
SUPABASE_SERVICE_ROLE_KEY=<service-role-key> \
|
|
191
|
+
npx agentify-supabase generate
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
This writes `supabase/functions/mcp/agentify.manifest.json`. Load it in
|
|
195
|
+
`aiReady({ manifest: ... })` — since the function runs on Deno, import it
|
|
196
|
+
with a JSON import:
|
|
197
|
+
|
|
198
|
+
```ts
|
|
199
|
+
import manifest from "./agentify.manifest.json" with { type: "json" };
|
|
200
|
+
|
|
201
|
+
Deno.serve(aiReady({ manifest, /* ...writes/rpc/functions... */ }));
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
## Troubleshooting
|
|
205
|
+
|
|
206
|
+
**"agentify_meta() is not reachable" / "PGRST202" errors in function logs.**
|
|
207
|
+
The migration hasn't been applied to this database. Run `supabase db push`
|
|
208
|
+
(or apply `sql/agentify_meta.sql` from this package directly), then
|
|
209
|
+
redeploy the function — the error message names this exact fix.
|
|
210
|
+
|
|
211
|
+
**A table I expected doesn't show up as a tool.** Check, in order: is RLS
|
|
212
|
+
actually enabled on it (`alter table ... enable row level security;`)? Does
|
|
213
|
+
the calling role (`anon`/`authenticated`) have grants PostgREST can see? If
|
|
214
|
+
both are true and it's still missing, confirm the migration is applied —
|
|
215
|
+
`agentify_meta()` is what reports the RLS flag in the first place.
|
|
216
|
+
|
|
217
|
+
**An `rpc_<fn>` tool takes a single opaque `args` object instead of named
|
|
218
|
+
parameters.** PostgREST's OpenAPI document didn't have a recognizable
|
|
219
|
+
argument schema for that function (older PostgREST, or a function with no
|
|
220
|
+
declared parameters in a shape this SDK's parser recognizes) — pass a JSON
|
|
221
|
+
object of the function's actual parameter names as `args` and it's forwarded
|
|
222
|
+
as-is.
|
|
223
|
+
|
|
224
|
+
**Writes/RPCs I configured aren't callable.** Double-check the `writes`
|
|
225
|
+
entries are `"<table>.insert"` / `"<table>.update"` / `"<table>.delete"` /
|
|
226
|
+
`"<table>.*"` — a malformed entry is dropped (with a console warning), not
|
|
227
|
+
an error, so a typo silently disables just that grant rather than crashing
|
|
228
|
+
the function.
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// bin/cli.ts
|
|
4
|
+
import { mkdir, readFile, writeFile, access } from "fs/promises";
|
|
5
|
+
import { dirname, join } from "path";
|
|
6
|
+
import { fileURLToPath } from "url";
|
|
7
|
+
import { execFileSync } from "child_process";
|
|
8
|
+
import { createInterface } from "readline/promises";
|
|
9
|
+
|
|
10
|
+
// src/fetch-openapi.ts
|
|
11
|
+
async function fetchOpenApi(postgrestUrl, headers, fetchImpl) {
|
|
12
|
+
const res = await fetchImpl(postgrestUrl, {
|
|
13
|
+
headers: { ...Object.fromEntries(headers), accept: "application/openapi+json" }
|
|
14
|
+
});
|
|
15
|
+
if (!res.ok) {
|
|
16
|
+
throw new Error(`PostgREST OpenAPI fetch failed: HTTP ${res.status}`);
|
|
17
|
+
}
|
|
18
|
+
return await res.json();
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// src/meta.ts
|
|
22
|
+
var MissingAgentifyMetaError = class extends Error {
|
|
23
|
+
constructor(cause) {
|
|
24
|
+
super(cause);
|
|
25
|
+
this.name = "MissingAgentifyMetaError";
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
async function fetchAgentifyMeta(postgrestUrl, headers, fetchImpl) {
|
|
29
|
+
const res = await fetchImpl(`${postgrestUrl}/rpc/agentify_meta`, {
|
|
30
|
+
method: "POST",
|
|
31
|
+
headers: { ...Object.fromEntries(headers), "content-type": "application/json" },
|
|
32
|
+
body: "{}"
|
|
33
|
+
});
|
|
34
|
+
if (res.status === 404) {
|
|
35
|
+
throw new MissingAgentifyMetaError(
|
|
36
|
+
"agentify_meta() is not reachable at /rpc/agentify_meta (404). Run `npx agentify-supabase init` (or apply sql/agentify_meta.sql directly) and `supabase db push`, then redeploy the function."
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
if (!res.ok) {
|
|
40
|
+
const body = await res.text().catch(() => "");
|
|
41
|
+
if (/PGRST202|function .* does not exist/i.test(body)) {
|
|
42
|
+
throw new MissingAgentifyMetaError(
|
|
43
|
+
"agentify_meta() is missing from the database (PostgREST error PGRST202). Apply the migration in sql/agentify_meta.sql via `supabase db push`, then redeploy the function."
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
throw new Error(`agentify_meta() call failed: HTTP ${res.status} ${body}`.trim());
|
|
47
|
+
}
|
|
48
|
+
return await res.json();
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// src/openapi.ts
|
|
52
|
+
var TYPE_MAP = {
|
|
53
|
+
integer: "integer",
|
|
54
|
+
number: "number",
|
|
55
|
+
boolean: "boolean",
|
|
56
|
+
object: "object",
|
|
57
|
+
array: "array",
|
|
58
|
+
string: "string"
|
|
59
|
+
};
|
|
60
|
+
function isPrimaryKey(prop) {
|
|
61
|
+
return /<pk\/>/i.test(prop.description ?? "");
|
|
62
|
+
}
|
|
63
|
+
function columnsFromOpenApi(doc, table) {
|
|
64
|
+
const def = doc.definitions?.[table];
|
|
65
|
+
if (!def?.properties) return [];
|
|
66
|
+
const required = new Set(def.required ?? []);
|
|
67
|
+
return Object.entries(def.properties).map(([name, prop]) => ({
|
|
68
|
+
name,
|
|
69
|
+
type: TYPE_MAP[prop.type ?? "string"] ?? "string",
|
|
70
|
+
format: prop.format,
|
|
71
|
+
required: required.has(name),
|
|
72
|
+
isPrimaryKey: isPrimaryKey(prop),
|
|
73
|
+
// Falls back to the OpenAPI-embedded comment (PostgREST mirrors `COMMENT
|
|
74
|
+
// ON COLUMN` into `description`, then appends its own "Note:\nThis is a
|
|
75
|
+
// Primary/Foreign Key...<pk/|fk/>" boilerplate) -- strip that boilerplate
|
|
76
|
+
// so a column with no real comment ends up `undefined`, not the
|
|
77
|
+
// boilerplate text. agentify_meta()'s copy wins when both exist anyway
|
|
78
|
+
// -- see introspect.ts's merge.
|
|
79
|
+
description: prop.description?.replace(/\n*Note:[\s\S]*$/, "").trim() || void 0
|
|
80
|
+
}));
|
|
81
|
+
}
|
|
82
|
+
function tablesInOpenApi(doc) {
|
|
83
|
+
return new Set(Object.keys(doc.definitions ?? {}));
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// src/introspect.ts
|
|
87
|
+
function introspectTables(openapi, metaRows) {
|
|
88
|
+
const exposedInOpenApi = tablesInOpenApi(openapi);
|
|
89
|
+
const byTable = groupByTable(metaRows);
|
|
90
|
+
const tables = [];
|
|
91
|
+
for (const [name, rows] of byTable) {
|
|
92
|
+
const rlsEnabled = rows[0].rls_enabled;
|
|
93
|
+
if (!rlsEnabled) continue;
|
|
94
|
+
if (!exposedInOpenApi.has(name)) continue;
|
|
95
|
+
const openApiColumns = new Map(columnsFromOpenApi(openapi, name).map((c) => [c.name, c]));
|
|
96
|
+
const columns = [];
|
|
97
|
+
for (const row of rows) {
|
|
98
|
+
const fromOpenApi = openApiColumns.get(row.column_name);
|
|
99
|
+
if (!fromOpenApi) continue;
|
|
100
|
+
columns.push({
|
|
101
|
+
...fromOpenApi,
|
|
102
|
+
// agentify_meta()'s comment wins over OpenAPI's mirrored copy (spec:
|
|
103
|
+
// "Column comments → param descriptions" sourced from the migration).
|
|
104
|
+
description: row.column_comment ?? fromOpenApi.description
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
if (columns.length === 0) continue;
|
|
108
|
+
tables.push({
|
|
109
|
+
name,
|
|
110
|
+
rlsEnabled: true,
|
|
111
|
+
description: rows[0].table_comment ?? void 0,
|
|
112
|
+
columns
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
return tables.sort((a, b) => a.name.localeCompare(b.name));
|
|
116
|
+
}
|
|
117
|
+
function groupByTable(rows) {
|
|
118
|
+
const map = /* @__PURE__ */ new Map();
|
|
119
|
+
for (const row of rows) {
|
|
120
|
+
const list = map.get(row.table_name) ?? [];
|
|
121
|
+
list.push(row);
|
|
122
|
+
map.set(row.table_name, list);
|
|
123
|
+
}
|
|
124
|
+
return map;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// src/manifest.ts
|
|
128
|
+
var MANIFEST_VERSION = 1;
|
|
129
|
+
function buildManifest(openapi, metaRows, generatedAt) {
|
|
130
|
+
return { version: MANIFEST_VERSION, generatedAt, tables: introspectTables(openapi, metaRows) };
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// bin/cli.ts
|
|
134
|
+
var PACKAGE_ROOT = dirname(dirname(fileURLToPath(import.meta.url)));
|
|
135
|
+
var FUNCTION_DIR = "supabase/functions/mcp";
|
|
136
|
+
var FUNCTION_TEMPLATE = `import { aiReady } from "npm:@exposify/supabase";
|
|
137
|
+
|
|
138
|
+
Deno.serve(aiReady({
|
|
139
|
+
// writes: ["orders.insert", "orders.update"],
|
|
140
|
+
// rpc: ["place_order"],
|
|
141
|
+
// functions: {
|
|
142
|
+
// refund: { description: "Refund an order", input: { type: "object", properties: {} } },
|
|
143
|
+
// },
|
|
144
|
+
}));
|
|
145
|
+
`;
|
|
146
|
+
async function exists(path) {
|
|
147
|
+
try {
|
|
148
|
+
await access(path);
|
|
149
|
+
return true;
|
|
150
|
+
} catch {
|
|
151
|
+
return false;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
function hasSupabaseCli() {
|
|
155
|
+
try {
|
|
156
|
+
execFileSync("supabase", ["--version"], { stdio: "ignore" });
|
|
157
|
+
return true;
|
|
158
|
+
} catch {
|
|
159
|
+
return false;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
async function promptYesNo(question) {
|
|
163
|
+
if (!process.stdin.isTTY) return false;
|
|
164
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
165
|
+
try {
|
|
166
|
+
const answer = (await rl.question(`${question} [y/N] `)).trim().toLowerCase();
|
|
167
|
+
return answer === "y" || answer === "yes";
|
|
168
|
+
} finally {
|
|
169
|
+
rl.close();
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
function printManualCommands(opts = {}) {
|
|
173
|
+
console.log("\nRun these to finish setup:");
|
|
174
|
+
const steps = [];
|
|
175
|
+
if (!opts.alreadyPushed) steps.push("supabase db push");
|
|
176
|
+
if (!opts.alreadyDeployed) steps.push("supabase functions deploy mcp");
|
|
177
|
+
steps.forEach((cmd, i) => console.log(` ${i + 1}. ${cmd}`));
|
|
178
|
+
console.log(` ${steps.length + 1}. Paste the function URL + a user JWT into an MCP client.`);
|
|
179
|
+
}
|
|
180
|
+
async function cmdInit() {
|
|
181
|
+
const migrationSql = await readFile(join(PACKAGE_ROOT, "sql/agentify_meta.sql"), "utf8");
|
|
182
|
+
const migrationsDir = "supabase/migrations";
|
|
183
|
+
await mkdir(migrationsDir, { recursive: true });
|
|
184
|
+
const migrationPath = join(migrationsDir, `${timestamp()}_agentify_meta.sql`);
|
|
185
|
+
await writeFile(migrationPath, migrationSql);
|
|
186
|
+
console.log(`Wrote ${migrationPath}`);
|
|
187
|
+
await mkdir(FUNCTION_DIR, { recursive: true });
|
|
188
|
+
const functionPath = join(FUNCTION_DIR, "index.ts");
|
|
189
|
+
if (await exists(functionPath)) {
|
|
190
|
+
console.log(`Skipped ${functionPath} (already exists)`);
|
|
191
|
+
} else {
|
|
192
|
+
await writeFile(functionPath, FUNCTION_TEMPLATE);
|
|
193
|
+
console.log(`Wrote ${functionPath}`);
|
|
194
|
+
}
|
|
195
|
+
await mkdir(join(FUNCTION_DIR, "docs"), { recursive: true });
|
|
196
|
+
if (!hasSupabaseCli()) {
|
|
197
|
+
console.log("\nSupabase CLI not found on PATH -- install it to run the next steps automatically next time.");
|
|
198
|
+
printManualCommands();
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
const connect = await promptYesNo(
|
|
202
|
+
"\nApply the migration and deploy the function to whatever Supabase project is currently linked, now?"
|
|
203
|
+
);
|
|
204
|
+
if (!connect) {
|
|
205
|
+
printManualCommands();
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
let pushed = false;
|
|
209
|
+
try {
|
|
210
|
+
console.log("\n$ supabase db push");
|
|
211
|
+
execFileSync("supabase", ["db", "push"], { stdio: "inherit" });
|
|
212
|
+
pushed = true;
|
|
213
|
+
console.log("\n$ supabase functions deploy mcp");
|
|
214
|
+
execFileSync("supabase", ["functions", "deploy", "mcp"], { stdio: "inherit" });
|
|
215
|
+
console.log("\nDone. Paste the function URL + a user JWT into an MCP client to try it.");
|
|
216
|
+
} catch (err) {
|
|
217
|
+
console.error(`
|
|
218
|
+
Supabase CLI command failed: ${err instanceof Error ? err.message : err}`);
|
|
219
|
+
printManualCommands({ alreadyPushed: pushed });
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
async function cmdGenerate() {
|
|
223
|
+
const supabaseUrl = process.env.SUPABASE_URL ?? argValue("--url");
|
|
224
|
+
const serviceKey = process.env.SUPABASE_SERVICE_ROLE_KEY ?? argValue("--key");
|
|
225
|
+
if (!supabaseUrl || !serviceKey) {
|
|
226
|
+
console.error(
|
|
227
|
+
"generate needs SUPABASE_URL and SUPABASE_SERVICE_ROLE_KEY (env vars, or --url/--key flags) pointing at a running local or linked project (e.g. `supabase start`)."
|
|
228
|
+
);
|
|
229
|
+
process.exitCode = 1;
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
const postgrestUrl = `${supabaseUrl.replace(/\/+$/, "")}/rest/v1`;
|
|
233
|
+
const headers = new Headers({ authorization: `Bearer ${serviceKey}`, apikey: serviceKey });
|
|
234
|
+
const [openapi, metaRows] = await Promise.all([
|
|
235
|
+
fetchOpenApi(postgrestUrl, headers, fetch),
|
|
236
|
+
fetchAgentifyMeta(postgrestUrl, headers, fetch)
|
|
237
|
+
]);
|
|
238
|
+
const manifest = buildManifest(openapi, metaRows, (/* @__PURE__ */ new Date()).toISOString());
|
|
239
|
+
const outPath = join(FUNCTION_DIR, "agentify.manifest.json");
|
|
240
|
+
await mkdir(dirname(outPath), { recursive: true });
|
|
241
|
+
await writeFile(outPath, JSON.stringify(manifest, null, 2));
|
|
242
|
+
console.log(`Wrote ${outPath} (${manifest.tables.length} table(s))`);
|
|
243
|
+
}
|
|
244
|
+
function argValue(flag) {
|
|
245
|
+
const i = process.argv.indexOf(flag);
|
|
246
|
+
return i >= 0 ? process.argv[i + 1] : void 0;
|
|
247
|
+
}
|
|
248
|
+
function timestamp() {
|
|
249
|
+
const d = /* @__PURE__ */ new Date();
|
|
250
|
+
const pad = (n) => String(n).padStart(2, "0");
|
|
251
|
+
return `${d.getFullYear()}${pad(d.getMonth() + 1)}${pad(d.getDate())}${pad(d.getHours())}${pad(d.getMinutes())}${pad(d.getSeconds())}`;
|
|
252
|
+
}
|
|
253
|
+
async function main() {
|
|
254
|
+
const [, , cmd] = process.argv;
|
|
255
|
+
if (cmd === "init") return cmdInit();
|
|
256
|
+
if (cmd === "generate") return cmdGenerate();
|
|
257
|
+
console.log("Usage: agentify-supabase <init|generate>");
|
|
258
|
+
console.log(" init Scaffold the agentify_meta() migration + supabase/functions/mcp/index.ts,");
|
|
259
|
+
console.log(" then interactively offer to run `supabase db push` + `functions deploy`");
|
|
260
|
+
console.log(" generate Build agentify.manifest.json from a running local/linked project");
|
|
261
|
+
process.exitCode = cmd ? 1 : 0;
|
|
262
|
+
}
|
|
263
|
+
main().catch((err) => {
|
|
264
|
+
console.error(err instanceof Error ? err.message : err);
|
|
265
|
+
process.exitCode = 1;
|
|
266
|
+
});
|