@exposify/supabase 0.1.0 → 0.2.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/LICENSE +21 -0
- package/README.md +74 -22
- package/dist/cli.js +338 -69
- package/dist/index.cjs +24 -23
- package/dist/index.d.cts +19 -18
- package/dist/index.d.ts +19 -18
- package/dist/index.js +22 -21
- package/package.json +12 -3
- package/sql/{agentify_meta.sql → exposify_meta.sql} +12 -4
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Ali
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -15,7 +15,7 @@ root.
|
|
|
15
15
|
|
|
16
16
|
- A Supabase project (local via the [Supabase CLI](https://supabase.com/docs/guides/cli), or hosted).
|
|
17
17
|
- The Supabase CLI (`supabase` on your `PATH`) for `db push` / `functions deploy`.
|
|
18
|
-
- Node or Yarn to run `npx
|
|
18
|
+
- Node or Yarn to run `npx exposify-supabase` (the CLI is a regular npm
|
|
19
19
|
package; the edge function itself runs on Deno and needs no local install).
|
|
20
20
|
|
|
21
21
|
## 1. Scaffold
|
|
@@ -23,13 +23,13 @@ root.
|
|
|
23
23
|
From your Supabase project's root (the directory containing `supabase/`):
|
|
24
24
|
|
|
25
25
|
```bash
|
|
26
|
-
npx
|
|
26
|
+
npx exposify-supabase init
|
|
27
27
|
```
|
|
28
28
|
|
|
29
29
|
This writes two things and never overwrites an existing function file:
|
|
30
30
|
|
|
31
|
-
- `supabase/migrations/<timestamp>
|
|
32
|
-
adds `
|
|
31
|
+
- `supabase/migrations/<timestamp>_exposify_meta.sql` — a SQL migration that
|
|
32
|
+
adds `exposify_meta()`, a `SECURITY DEFINER` function returning, per table,
|
|
33
33
|
whether RLS is enabled and any `COMMENT ON TABLE`/`COMMENT ON COLUMN` text
|
|
34
34
|
you've written. It only ever returns catalog metadata (table/column names,
|
|
35
35
|
flags, comments) — never row data — which is what makes it safe to grant
|
|
@@ -39,8 +39,9 @@ This writes two things and never overwrites an existing function file:
|
|
|
39
39
|
- `supabase/functions/mcp/docs/` — an empty directory for optional bundled
|
|
40
40
|
docs (see "Knowledge base" below).
|
|
41
41
|
|
|
42
|
-
`init` is interactive from there
|
|
43
|
-
|
|
42
|
+
`init` is interactive from there (skips straight to the old fixed behavior
|
|
43
|
+
below if stdin isn't a real terminal — CI, a script, piped input). If the
|
|
44
|
+
Supabase CLI is on your `PATH`, it asks:
|
|
44
45
|
|
|
45
46
|
```
|
|
46
47
|
Apply the migration and deploy the function to whatever Supabase project is
|
|
@@ -49,8 +50,8 @@ currently linked, now? [y/N]
|
|
|
49
50
|
|
|
50
51
|
Answering **y** runs `supabase db push` then `supabase functions deploy mcp`
|
|
51
52
|
for you, streaming their output. The default is **no** — pressing Enter, the
|
|
52
|
-
Supabase CLI not being installed, or running `init` non-interactively
|
|
53
|
-
|
|
53
|
+
Supabase CLI not being installed, or running `init` non-interactively all
|
|
54
|
+
skip straight to printing the commands instead:
|
|
54
55
|
|
|
55
56
|
```
|
|
56
57
|
Run these to finish setup:
|
|
@@ -61,14 +62,50 @@ Run these to finish setup:
|
|
|
61
62
|
|
|
62
63
|
That's deliberate: `db push`/`functions deploy` act on whatever project is
|
|
63
64
|
*currently linked*, which may not be a throwaway one, so nothing gets pushed
|
|
64
|
-
without an explicit yes.
|
|
65
|
-
|
|
66
|
-
|
|
65
|
+
without an explicit yes.
|
|
66
|
+
|
|
67
|
+
### The setup wizard
|
|
68
|
+
|
|
69
|
+
If `init` can reach the project you're setting up — either because it was
|
|
70
|
+
already deployed (a re-run), or because you say yes above and the deploy
|
|
71
|
+
succeeds — it walks through a short interactive wizard instead of leaving
|
|
72
|
+
you to hand-edit `writes`/`rpc`/comments yourself:
|
|
73
|
+
|
|
74
|
+
1. **Anonymous sign-in.** `init` tries to auto-detect this via the project's
|
|
75
|
+
public `/auth/v1/settings` endpoint and asks you to confirm; if it can't
|
|
76
|
+
detect it, it just asks. If enabled, it adds a warning comment to the
|
|
77
|
+
generated `index.ts`: an anonymous Supabase Auth session gets
|
|
78
|
+
`role: authenticated` (not `role: anon`), so it passes any RLS policy
|
|
79
|
+
scoped to `authenticated` unless that policy explicitly excludes
|
|
80
|
+
`auth.jwt()->>'is_anonymous' = 'true'`. Worth knowing before granting
|
|
81
|
+
`writes` on anything an anonymous session could reach.
|
|
82
|
+
2. **Per-table writes.** For every RLS-enabled table, asks which of
|
|
83
|
+
insert/update/delete to grant (`i`, `u`, `d`, comma-separated, or Enter
|
|
84
|
+
for none) — the resulting `writes` array is written for you instead of
|
|
85
|
+
left as a commented-out example.
|
|
86
|
+
3. **RPC functions.** Lists Postgres functions PostgREST exposes and lets
|
|
87
|
+
you pick which to expose as tools.
|
|
88
|
+
4. **Documentation.** For every table or column `exposify_meta()` reports
|
|
89
|
+
has no comment, offers to write one right there — the answers become a
|
|
90
|
+
`COMMENT ON TABLE`/`COMMENT ON COLUMN` migration
|
|
91
|
+
(`<timestamp>_exposify_comments.sql`) that gets applied alongside the
|
|
92
|
+
rest. This is what gives `query_<table>`/`insert_<table>`/etc. their tool
|
|
93
|
+
and parameter descriptions (see "Column/table descriptions" below) —
|
|
94
|
+
worth doing even for tables you're not granting writes on, since it's
|
|
95
|
+
the only thing that makes read tools self-explanatory to an agent.
|
|
96
|
+
|
|
97
|
+
If `supabase/functions/mcp/index.ts` already exists, none of this touches
|
|
98
|
+
it — your selections are printed as a config snippet to merge in by hand
|
|
99
|
+
instead, and the documentation migration still gets written either way.
|
|
100
|
+
Connects using `SUPABASE_URL`/`SUPABASE_ANON_KEY` (env vars or `--url`/`--key`
|
|
101
|
+
flags) — without those, `init` can't reach anything to introspect and falls
|
|
102
|
+
back to the fixed scaffold-and-ask-to-deploy flow above, same as before this
|
|
103
|
+
wizard existed.
|
|
67
104
|
|
|
68
105
|
## 2. Decide what's exposed
|
|
69
106
|
|
|
70
107
|
**Every RLS-enabled table is automatically readable.** A table only shows up
|
|
71
|
-
at all if `
|
|
108
|
+
at all if `exposify_meta()` reports RLS is turned on for it *and* PostgREST
|
|
72
109
|
actually exposes it (the calling role has grants) — an RLS-less table is
|
|
73
110
|
invisible, full stop, with no override. For each exposed table you get a
|
|
74
111
|
`query_<table>` tool: typed filters (`eq`/`neq`/`gt`/`lt`/`like`), a `select`
|
|
@@ -136,7 +173,7 @@ supabase functions deploy mcp
|
|
|
136
173
|
|
|
137
174
|
Only `functions deploy` is needed for a config-only change to `index.ts`;
|
|
138
175
|
`db push` is only needed again if the migration itself changed (it hasn't,
|
|
139
|
-
unless you're editing `sql/
|
|
176
|
+
unless you're editing `sql/exposify_meta.sql` directly).
|
|
140
177
|
|
|
141
178
|
(Local dev: `supabase start` then `supabase functions serve mcp` instead of
|
|
142
179
|
`deploy`.)
|
|
@@ -169,9 +206,9 @@ them via the `search_docs` tool.
|
|
|
169
206
|
Every request tries, in order:
|
|
170
207
|
|
|
171
208
|
1. **Runtime introspection** — fetch PostgREST's OpenAPI document and call
|
|
172
|
-
`
|
|
209
|
+
`exposify_meta()`. This is the normal path; the result is cached in
|
|
173
210
|
memory for the life of the edge instance, not refetched per call.
|
|
174
|
-
2. **Bundled manifest** — if step 1 fails, and a `
|
|
211
|
+
2. **Bundled manifest** — if step 1 fails, and a `exposify.manifest.json`
|
|
175
212
|
was generated ahead of time (see below), tools are built from that
|
|
176
213
|
instead.
|
|
177
214
|
3. **Functions + knowledge base only** — if neither works, the function
|
|
@@ -187,32 +224,38 @@ project (local `supabase start` or linked):
|
|
|
187
224
|
|
|
188
225
|
```bash
|
|
189
226
|
SUPABASE_URL=http://localhost:54321 \
|
|
190
|
-
|
|
191
|
-
npx
|
|
227
|
+
SUPABASE_ANON_KEY=<anon-key> \
|
|
228
|
+
npx exposify-supabase generate
|
|
192
229
|
```
|
|
193
230
|
|
|
194
|
-
|
|
231
|
+
Uses the **anon key**, deliberately the same key runtime introspection
|
|
232
|
+
itself uses (see "Fallback ladder" above) — a service-role key would see
|
|
233
|
+
tables/columns the `anon`/`authenticated` roles can't actually reach,
|
|
234
|
+
producing a fallback manifest that offers tools live introspection never
|
|
235
|
+
would.
|
|
236
|
+
|
|
237
|
+
This writes `supabase/functions/mcp/exposify.manifest.json`. Load it in
|
|
195
238
|
`aiReady({ manifest: ... })` — since the function runs on Deno, import it
|
|
196
239
|
with a JSON import:
|
|
197
240
|
|
|
198
241
|
```ts
|
|
199
|
-
import manifest from "./
|
|
242
|
+
import manifest from "./exposify.manifest.json" with { type: "json" };
|
|
200
243
|
|
|
201
244
|
Deno.serve(aiReady({ manifest, /* ...writes/rpc/functions... */ }));
|
|
202
245
|
```
|
|
203
246
|
|
|
204
247
|
## Troubleshooting
|
|
205
248
|
|
|
206
|
-
**"
|
|
249
|
+
**"exposify_meta() is not reachable" / "PGRST202" errors in function logs.**
|
|
207
250
|
The migration hasn't been applied to this database. Run `supabase db push`
|
|
208
|
-
(or apply `sql/
|
|
251
|
+
(or apply `sql/exposify_meta.sql` from this package directly), then
|
|
209
252
|
redeploy the function — the error message names this exact fix.
|
|
210
253
|
|
|
211
254
|
**A table I expected doesn't show up as a tool.** Check, in order: is RLS
|
|
212
255
|
actually enabled on it (`alter table ... enable row level security;`)? Does
|
|
213
256
|
the calling role (`anon`/`authenticated`) have grants PostgREST can see? If
|
|
214
257
|
both are true and it's still missing, confirm the migration is applied —
|
|
215
|
-
`
|
|
258
|
+
`exposify_meta()` is what reports the RLS flag in the first place.
|
|
216
259
|
|
|
217
260
|
**An `rpc_<fn>` tool takes a single opaque `args` object instead of named
|
|
218
261
|
parameters.** PostgREST's OpenAPI document didn't have a recognizable
|
|
@@ -226,3 +269,12 @@ entries are `"<table>.insert"` / `"<table>.update"` / `"<table>.delete"` /
|
|
|
226
269
|
`"<table>.*"` — a malformed entry is dropped (with a console warning), not
|
|
227
270
|
an error, so a typo silently disables just that grant rather than crashing
|
|
228
271
|
the function.
|
|
272
|
+
|
|
273
|
+
**`init` just scaffolds and asks to deploy — it never asks about writes/RPCs/
|
|
274
|
+
documentation.** The wizard only runs when `init` can actually reach the
|
|
275
|
+
project (`SUPABASE_URL`/`SUPABASE_ANON_KEY` env vars, or `--url`/`--key`
|
|
276
|
+
flags, pointing at a project where the migration is already applied). On a
|
|
277
|
+
brand-new project's first `init`, that's not true yet — say yes to the
|
|
278
|
+
deploy prompt (or run `supabase db push` yourself) and the wizard picks up
|
|
279
|
+
automatically once the migration is live, in the same run if you said yes,
|
|
280
|
+
or on your next `exposify-supabase init` otherwise.
|
package/dist/cli.js
CHANGED
|
@@ -19,31 +19,31 @@ async function fetchOpenApi(postgrestUrl, headers, fetchImpl) {
|
|
|
19
19
|
}
|
|
20
20
|
|
|
21
21
|
// src/meta.ts
|
|
22
|
-
var
|
|
22
|
+
var MissingExposifyMetaError = class extends Error {
|
|
23
23
|
constructor(cause) {
|
|
24
24
|
super(cause);
|
|
25
|
-
this.name = "
|
|
25
|
+
this.name = "MissingExposifyMetaError";
|
|
26
26
|
}
|
|
27
27
|
};
|
|
28
|
-
async function
|
|
29
|
-
const res = await fetchImpl(`${postgrestUrl}/rpc/
|
|
28
|
+
async function fetchExposifyMeta(postgrestUrl, headers, fetchImpl) {
|
|
29
|
+
const res = await fetchImpl(`${postgrestUrl}/rpc/exposify_meta`, {
|
|
30
30
|
method: "POST",
|
|
31
31
|
headers: { ...Object.fromEntries(headers), "content-type": "application/json" },
|
|
32
32
|
body: "{}"
|
|
33
33
|
});
|
|
34
34
|
if (res.status === 404) {
|
|
35
|
-
throw new
|
|
36
|
-
"
|
|
35
|
+
throw new MissingExposifyMetaError(
|
|
36
|
+
"exposify_meta() is not reachable at /rpc/exposify_meta (404). Run `npx exposify-supabase init` (or apply sql/exposify_meta.sql directly) and `supabase db push`, then redeploy the function."
|
|
37
37
|
);
|
|
38
38
|
}
|
|
39
39
|
if (!res.ok) {
|
|
40
40
|
const body = await res.text().catch(() => "");
|
|
41
41
|
if (/PGRST202|function .* does not exist/i.test(body)) {
|
|
42
|
-
throw new
|
|
43
|
-
"
|
|
42
|
+
throw new MissingExposifyMetaError(
|
|
43
|
+
"exposify_meta() is missing from the database (PostgREST error PGRST202). Apply the migration in sql/exposify_meta.sql via `supabase db push`, then redeploy the function."
|
|
44
44
|
);
|
|
45
45
|
}
|
|
46
|
-
throw new Error(`
|
|
46
|
+
throw new Error(`exposify_meta() call failed: HTTP ${res.status} ${body}`.trim());
|
|
47
47
|
}
|
|
48
48
|
return await res.json();
|
|
49
49
|
}
|
|
@@ -74,7 +74,7 @@ function columnsFromOpenApi(doc, table) {
|
|
|
74
74
|
// ON COLUMN` into `description`, then appends its own "Note:\nThis is a
|
|
75
75
|
// Primary/Foreign Key...<pk/|fk/>" boilerplate) -- strip that boilerplate
|
|
76
76
|
// so a column with no real comment ends up `undefined`, not the
|
|
77
|
-
// boilerplate text.
|
|
77
|
+
// boilerplate text. exposify_meta()'s copy wins when both exist anyway
|
|
78
78
|
// -- see introspect.ts's merge.
|
|
79
79
|
description: prop.description?.replace(/\n*Note:[\s\S]*$/, "").trim() || void 0
|
|
80
80
|
}));
|
|
@@ -99,7 +99,7 @@ function introspectTables(openapi, metaRows) {
|
|
|
99
99
|
if (!fromOpenApi) continue;
|
|
100
100
|
columns.push({
|
|
101
101
|
...fromOpenApi,
|
|
102
|
-
//
|
|
102
|
+
// exposify_meta()'s comment wins over OpenAPI's mirrored copy (spec:
|
|
103
103
|
// "Column comments → param descriptions" sourced from the migration).
|
|
104
104
|
description: row.column_comment ?? fromOpenApi.description
|
|
105
105
|
});
|
|
@@ -130,19 +130,98 @@ function buildManifest(openapi, metaRows, generatedAt) {
|
|
|
130
130
|
return { version: MANIFEST_VERSION, generatedAt, tables: introspectTables(openapi, metaRows) };
|
|
131
131
|
}
|
|
132
132
|
|
|
133
|
+
// bin/wizard.ts
|
|
134
|
+
function parseAnonymousSignInSetting(settings) {
|
|
135
|
+
if (!settings || typeof settings !== "object") return null;
|
|
136
|
+
const s = settings;
|
|
137
|
+
const external = s.external;
|
|
138
|
+
if (external && typeof external.anonymous_users === "boolean") return external.anonymous_users;
|
|
139
|
+
if (typeof s.anonymous_users === "boolean") return s.anonymous_users;
|
|
140
|
+
if (typeof s.external_anonymous_users === "boolean") return s.external_anonymous_users;
|
|
141
|
+
return null;
|
|
142
|
+
}
|
|
143
|
+
function rpcCandidatesFromOpenApi(doc) {
|
|
144
|
+
return Object.keys(doc.paths ?? {}).filter((p) => p.startsWith("/rpc/")).map((p) => p.slice("/rpc/".length)).filter((name) => name.length > 0 && name !== "exposify_meta").sort();
|
|
145
|
+
}
|
|
146
|
+
function flattenWriteGrants(grants) {
|
|
147
|
+
return grants.flatMap((g) => g.ops.map((op) => `${g.table}.${op}`));
|
|
148
|
+
}
|
|
149
|
+
function parseWriteOpsAnswer(answer) {
|
|
150
|
+
const seen = /* @__PURE__ */ new Set();
|
|
151
|
+
for (const raw of answer.split(",")) {
|
|
152
|
+
const token = raw.trim().toLowerCase();
|
|
153
|
+
if (token === "i" || token === "insert") seen.add("insert");
|
|
154
|
+
else if (token === "u" || token === "update") seen.add("update");
|
|
155
|
+
else if (token === "d" || token === "delete") seen.add("delete");
|
|
156
|
+
}
|
|
157
|
+
return ["insert", "update", "delete"].filter((op) => seen.has(op));
|
|
158
|
+
}
|
|
159
|
+
function parseSelectionAnswer(answer, candidates) {
|
|
160
|
+
const wanted = new Set(
|
|
161
|
+
answer.split(",").map((s) => s.trim()).filter(Boolean)
|
|
162
|
+
);
|
|
163
|
+
return candidates.filter((c) => wanted.has(c));
|
|
164
|
+
}
|
|
165
|
+
function tablesMissingDocs(tables) {
|
|
166
|
+
return tables.map((t) => ({
|
|
167
|
+
name: t.name,
|
|
168
|
+
missingTableComment: !t.description,
|
|
169
|
+
columnsWithoutComments: t.columns.filter((c) => !c.description).map((c) => c.name)
|
|
170
|
+
}));
|
|
171
|
+
}
|
|
172
|
+
function sqlStringLiteral(text) {
|
|
173
|
+
return `'${text.replace(/'/g, "''")}'`;
|
|
174
|
+
}
|
|
175
|
+
function sqlIdentifier(name) {
|
|
176
|
+
return `"${name.replace(/"/g, '""')}"`;
|
|
177
|
+
}
|
|
178
|
+
function buildCommentStatements(entries) {
|
|
179
|
+
const lines = [];
|
|
180
|
+
for (const entry of entries) {
|
|
181
|
+
const table = sqlIdentifier(entry.table);
|
|
182
|
+
if (entry.tableDescription) {
|
|
183
|
+
lines.push(`comment on table public.${table} is ${sqlStringLiteral(entry.tableDescription)};`);
|
|
184
|
+
}
|
|
185
|
+
for (const col of entry.columns) {
|
|
186
|
+
lines.push(
|
|
187
|
+
`comment on column public.${table}.${sqlIdentifier(col.name)} is ${sqlStringLiteral(col.description)};`
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
return lines.length > 0 ? `${lines.join("\n")}
|
|
192
|
+
` : "";
|
|
193
|
+
}
|
|
194
|
+
var ANONYMOUS_SIGN_IN_WARNING = `// NOTE: this project has anonymous sign-in enabled. An anonymous Supabase
|
|
195
|
+
// Auth session still gets role: authenticated (with an is_anonymous claim),
|
|
196
|
+
// NOT role: anon -- so it passes any RLS policy scoped to "authenticated"
|
|
197
|
+
// unless that policy explicitly excludes auth.jwt()->>'is_anonymous' = 'true'.
|
|
198
|
+
// Double-check your RLS policies (especially on tables granted writes below)
|
|
199
|
+
// account for this before trusting "authenticated" to mean "a real user".
|
|
200
|
+
`;
|
|
201
|
+
function buildFunctionTemplate(opts = {}) {
|
|
202
|
+
const lines = [];
|
|
203
|
+
if (opts.anonymousSignIn) lines.push(ANONYMOUS_SIGN_IN_WARNING);
|
|
204
|
+
lines.push(`import { aiReady } from "npm:@exposify/supabase";`, "");
|
|
205
|
+
lines.push("Deno.serve(aiReady({");
|
|
206
|
+
lines.push(
|
|
207
|
+
opts.writes && opts.writes.length > 0 ? ` writes: ${JSON.stringify(opts.writes)},` : ` // writes: ["orders.insert", "orders.update"],`
|
|
208
|
+
);
|
|
209
|
+
lines.push(
|
|
210
|
+
opts.rpc && opts.rpc.length > 0 ? ` rpc: ${JSON.stringify(opts.rpc)},` : ` // rpc: ["place_order"],`
|
|
211
|
+
);
|
|
212
|
+
lines.push(
|
|
213
|
+
" // functions: {",
|
|
214
|
+
' // refund: { description: "Refund an order", input: { type: "object", properties: {} } },',
|
|
215
|
+
" // },",
|
|
216
|
+
"}));",
|
|
217
|
+
""
|
|
218
|
+
);
|
|
219
|
+
return lines.join("\n");
|
|
220
|
+
}
|
|
221
|
+
|
|
133
222
|
// bin/cli.ts
|
|
134
223
|
var PACKAGE_ROOT = dirname(dirname(fileURLToPath(import.meta.url)));
|
|
135
224
|
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
225
|
async function exists(path) {
|
|
147
226
|
try {
|
|
148
227
|
await access(path);
|
|
@@ -159,15 +238,18 @@ function hasSupabaseCli() {
|
|
|
159
238
|
return false;
|
|
160
239
|
}
|
|
161
240
|
}
|
|
162
|
-
|
|
163
|
-
if (!process.stdin.isTTY) return
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
|
|
241
|
+
function openInteractiveReadline() {
|
|
242
|
+
if (!process.stdin.isTTY) return null;
|
|
243
|
+
return createInterface({ input: process.stdin, output: process.stdout });
|
|
244
|
+
}
|
|
245
|
+
async function promptYesNo(rl, question, defaultYes = false) {
|
|
246
|
+
const suffix = defaultYes ? "[Y/n]" : "[y/N]";
|
|
247
|
+
const answer = (await rl.question(`${question} ${suffix} `)).trim().toLowerCase();
|
|
248
|
+
if (answer === "") return defaultYes;
|
|
249
|
+
return answer === "y" || answer === "yes";
|
|
250
|
+
}
|
|
251
|
+
async function promptText(rl, question) {
|
|
252
|
+
return (await rl.question(question)).trim();
|
|
171
253
|
}
|
|
172
254
|
function printManualCommands(opts = {}) {
|
|
173
255
|
console.log("\nRun these to finish setup:");
|
|
@@ -177,66 +259,250 @@ function printManualCommands(opts = {}) {
|
|
|
177
259
|
steps.forEach((cmd, i) => console.log(` ${i + 1}. ${cmd}`));
|
|
178
260
|
console.log(` ${steps.length + 1}. Paste the function URL + a user JWT into an MCP client.`);
|
|
179
261
|
}
|
|
262
|
+
function pushAndDeploy() {
|
|
263
|
+
let pushed = false;
|
|
264
|
+
try {
|
|
265
|
+
console.log("\n$ supabase db push");
|
|
266
|
+
execFileSync("supabase", ["db", "push"], { stdio: "inherit" });
|
|
267
|
+
pushed = true;
|
|
268
|
+
console.log("\n$ supabase functions deploy mcp");
|
|
269
|
+
execFileSync("supabase", ["functions", "deploy", "mcp"], { stdio: "inherit" });
|
|
270
|
+
return { pushed: true, deployed: true };
|
|
271
|
+
} catch (err) {
|
|
272
|
+
return { pushed, deployed: false, error: err };
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
function resolveConnectionInfo() {
|
|
276
|
+
const url = process.env.SUPABASE_URL ?? argValue("--url");
|
|
277
|
+
const anonKey = process.env.SUPABASE_ANON_KEY ?? argValue("--key");
|
|
278
|
+
if (!url || !anonKey) return null;
|
|
279
|
+
return { url, anonKey };
|
|
280
|
+
}
|
|
281
|
+
async function tryIntrospect(conn) {
|
|
282
|
+
try {
|
|
283
|
+
const postgrestUrl = `${conn.url.replace(/\/+$/, "")}/rest/v1`;
|
|
284
|
+
const headers = new Headers({ authorization: `Bearer ${conn.anonKey}`, apikey: conn.anonKey });
|
|
285
|
+
const [openapi, metaRows] = await Promise.all([
|
|
286
|
+
fetchOpenApi(postgrestUrl, headers, fetch),
|
|
287
|
+
fetchExposifyMeta(postgrestUrl, headers, fetch)
|
|
288
|
+
]);
|
|
289
|
+
return { tables: introspectTables(openapi, metaRows), openapi };
|
|
290
|
+
} catch {
|
|
291
|
+
return null;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
async function tryDetectAnonymousSignIn(conn) {
|
|
295
|
+
try {
|
|
296
|
+
const url = `${conn.url.replace(/\/+$/, "")}/auth/v1/settings`;
|
|
297
|
+
const res = await fetch(url, { headers: { apikey: conn.anonKey, authorization: `Bearer ${conn.anonKey}` } });
|
|
298
|
+
if (!res.ok) return null;
|
|
299
|
+
return parseAnonymousSignInSetting(await res.json());
|
|
300
|
+
} catch {
|
|
301
|
+
return null;
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
async function askAnonymousSignIn(rl, conn) {
|
|
305
|
+
const detected = conn ? await tryDetectAnonymousSignIn(conn) : null;
|
|
306
|
+
if (detected !== null) {
|
|
307
|
+
console.log(`
|
|
308
|
+
Detected: anonymous sign-in is ${detected ? "enabled" : "disabled"} on this project.`);
|
|
309
|
+
const confirmed = await promptYesNo(rl, "Is that correct?", true);
|
|
310
|
+
if (confirmed) return detected;
|
|
311
|
+
}
|
|
312
|
+
return promptYesNo(rl, "\nDoes this Supabase project have anonymous sign-in enabled (Authentication \u2192 Sign In / Providers \u2192 Anonymous)?");
|
|
313
|
+
}
|
|
314
|
+
async function askWriteGrants(rl, tables) {
|
|
315
|
+
if (tables.length === 0) return [];
|
|
316
|
+
const configure = await promptYesNo(rl, `
|
|
317
|
+
Configure write access for ${tables.length} table(s)?`);
|
|
318
|
+
if (!configure) return [];
|
|
319
|
+
const grants = [];
|
|
320
|
+
for (const table of tables) {
|
|
321
|
+
const answer = await promptText(
|
|
322
|
+
rl,
|
|
323
|
+
` ${table.name} -- grant writes? (i=insert, u=update, d=delete, comma-separate, Enter=none): `
|
|
324
|
+
);
|
|
325
|
+
const ops = parseWriteOpsAnswer(answer);
|
|
326
|
+
if (ops.length > 0) grants.push({ table: table.name, ops });
|
|
327
|
+
}
|
|
328
|
+
return grants;
|
|
329
|
+
}
|
|
330
|
+
async function askRpcSelection(rl, candidates) {
|
|
331
|
+
if (candidates.length === 0) return [];
|
|
332
|
+
console.log(`
|
|
333
|
+
Available Postgres functions: ${candidates.join(", ")}`);
|
|
334
|
+
const answer = await promptText(rl, "Expose any as tools? (comma-separate, Enter=none): ");
|
|
335
|
+
return parseSelectionAnswer(answer, candidates);
|
|
336
|
+
}
|
|
337
|
+
async function askDocumentation(rl, tables) {
|
|
338
|
+
const missing = tablesMissingDocs(tables).filter((t) => t.missingTableComment || t.columnsWithoutComments.length > 0);
|
|
339
|
+
if (missing.length === 0) return [];
|
|
340
|
+
const configure = await promptYesNo(
|
|
341
|
+
rl,
|
|
342
|
+
`
|
|
343
|
+
${missing.length} table(s) are missing documentation MCP tools would otherwise show agents -- write some now?`
|
|
344
|
+
);
|
|
345
|
+
if (!configure) return [];
|
|
346
|
+
const entries = [];
|
|
347
|
+
for (const t of missing) {
|
|
348
|
+
const entry = { table: t.name, columns: [] };
|
|
349
|
+
if (t.missingTableComment) {
|
|
350
|
+
const desc = await promptText(rl, ` ${t.name} -- what is this table for? (Enter to skip): `);
|
|
351
|
+
if (desc) entry.tableDescription = desc;
|
|
352
|
+
}
|
|
353
|
+
if (t.columnsWithoutComments.length > 0) {
|
|
354
|
+
const addColumns = await promptYesNo(
|
|
355
|
+
rl,
|
|
356
|
+
` Add descriptions for ${t.name}'s ${t.columnsWithoutComments.length} undocumented column(s)?`
|
|
357
|
+
);
|
|
358
|
+
if (addColumns) {
|
|
359
|
+
for (const col of t.columnsWithoutComments) {
|
|
360
|
+
const desc = await promptText(rl, ` ${t.name}.${col}: `);
|
|
361
|
+
if (desc) entry.columns.push({ name: col, description: desc });
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
if (entry.tableDescription || entry.columns.length > 0) entries.push(entry);
|
|
366
|
+
}
|
|
367
|
+
return entries;
|
|
368
|
+
}
|
|
369
|
+
async function writeCommentsMigration(entries) {
|
|
370
|
+
const sql = buildCommentStatements(entries);
|
|
371
|
+
if (!sql) return null;
|
|
372
|
+
const migrationsDir = "supabase/migrations";
|
|
373
|
+
await mkdir(migrationsDir, { recursive: true });
|
|
374
|
+
const path = join(migrationsDir, `${timestamp()}_exposify_comments.sql`);
|
|
375
|
+
await writeFile(path, sql);
|
|
376
|
+
return path;
|
|
377
|
+
}
|
|
180
378
|
async function cmdInit() {
|
|
181
|
-
const migrationSql = await readFile(join(PACKAGE_ROOT, "sql/
|
|
379
|
+
const migrationSql = await readFile(join(PACKAGE_ROOT, "sql/exposify_meta.sql"), "utf8");
|
|
182
380
|
const migrationsDir = "supabase/migrations";
|
|
183
381
|
await mkdir(migrationsDir, { recursive: true });
|
|
184
|
-
const migrationPath = join(migrationsDir, `${timestamp()}
|
|
382
|
+
const migrationPath = join(migrationsDir, `${timestamp()}_exposify_meta.sql`);
|
|
185
383
|
await writeFile(migrationPath, migrationSql);
|
|
186
384
|
console.log(`Wrote ${migrationPath}`);
|
|
187
385
|
await mkdir(FUNCTION_DIR, { recursive: true });
|
|
188
386
|
const functionPath = join(FUNCTION_DIR, "index.ts");
|
|
189
|
-
|
|
190
|
-
console.log(`Skipped ${functionPath} (already exists)`);
|
|
191
|
-
} else {
|
|
192
|
-
await writeFile(functionPath, FUNCTION_TEMPLATE);
|
|
193
|
-
console.log(`Wrote ${functionPath}`);
|
|
194
|
-
}
|
|
387
|
+
const functionExists = await exists(functionPath);
|
|
195
388
|
await mkdir(join(FUNCTION_DIR, "docs"), { recursive: true });
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
if (!connect) {
|
|
389
|
+
const rl = openInteractiveReadline();
|
|
390
|
+
if (!rl) {
|
|
391
|
+
if (!functionExists) {
|
|
392
|
+
await writeFile(functionPath, buildFunctionTemplate());
|
|
393
|
+
console.log(`Wrote ${functionPath}`);
|
|
394
|
+
} else {
|
|
395
|
+
console.log(`Skipped ${functionPath} (already exists)`);
|
|
396
|
+
}
|
|
205
397
|
printManualCommands();
|
|
206
398
|
return;
|
|
207
399
|
}
|
|
208
|
-
let pushed = false;
|
|
209
400
|
try {
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
401
|
+
const conn = resolveConnectionInfo();
|
|
402
|
+
const anonymousSignIn = await askAnonymousSignIn(rl, conn);
|
|
403
|
+
let introspection = conn ? await tryIntrospect(conn) : null;
|
|
404
|
+
let alreadyPushed = false;
|
|
405
|
+
let alreadyDeployed = false;
|
|
406
|
+
if (!introspection) {
|
|
407
|
+
if (!functionExists) {
|
|
408
|
+
await writeFile(functionPath, buildFunctionTemplate({ anonymousSignIn }));
|
|
409
|
+
console.log(`Wrote ${functionPath}`);
|
|
410
|
+
} else {
|
|
411
|
+
console.log(`Skipped ${functionPath} (already exists)`);
|
|
412
|
+
}
|
|
413
|
+
if (!hasSupabaseCli()) {
|
|
414
|
+
console.log("\nSupabase CLI not found on PATH -- install it to run the next steps automatically next time.");
|
|
415
|
+
printManualCommands();
|
|
416
|
+
return;
|
|
417
|
+
}
|
|
418
|
+
const connect = await promptYesNo(
|
|
419
|
+
rl,
|
|
420
|
+
"\nApply the migration and deploy the function to whatever Supabase project is currently linked, now?"
|
|
421
|
+
);
|
|
422
|
+
if (!connect) {
|
|
423
|
+
printManualCommands();
|
|
424
|
+
return;
|
|
425
|
+
}
|
|
426
|
+
const result2 = pushAndDeploy();
|
|
427
|
+
alreadyPushed = result2.pushed;
|
|
428
|
+
alreadyDeployed = result2.deployed;
|
|
429
|
+
if (!result2.deployed) {
|
|
430
|
+
console.error(`
|
|
431
|
+
Supabase CLI command failed: ${result2.error instanceof Error ? result2.error.message : result2.error}`);
|
|
432
|
+
printManualCommands({ alreadyPushed });
|
|
433
|
+
return;
|
|
434
|
+
}
|
|
435
|
+
introspection = conn ? await tryIntrospect(conn) : null;
|
|
436
|
+
if (!introspection) {
|
|
437
|
+
console.log(
|
|
438
|
+
"\nDeployed, but couldn't reach the project to introspect tables (no SUPABASE_URL/SUPABASE_ANON_KEY or --url/--key available). Re-run `exposify-supabase init` with those set to configure writes, RPCs, and documentation interactively."
|
|
439
|
+
);
|
|
440
|
+
console.log("\nDone. Paste the function URL + a user JWT into an MCP client to try it.");
|
|
441
|
+
return;
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
const grants = await askWriteGrants(rl, introspection.tables);
|
|
445
|
+
const rpcCandidates = rpcCandidatesFromOpenApi(introspection.openapi);
|
|
446
|
+
const rpc = await askRpcSelection(rl, rpcCandidates);
|
|
447
|
+
const docs = await askDocumentation(rl, introspection.tables);
|
|
448
|
+
const writes = flattenWriteGrants(grants);
|
|
449
|
+
let wroteFunctionFile = false;
|
|
450
|
+
if (!functionExists) {
|
|
451
|
+
await writeFile(functionPath, buildFunctionTemplate({ writes, rpc, anonymousSignIn }));
|
|
452
|
+
console.log(`
|
|
453
|
+
Wrote ${functionPath} with your selections.`);
|
|
454
|
+
wroteFunctionFile = true;
|
|
455
|
+
} else if (writes.length > 0 || rpc.length > 0) {
|
|
456
|
+
console.log(`
|
|
457
|
+
${functionPath} already exists -- left untouched. Merge this into your aiReady() config:`);
|
|
458
|
+
console.log(buildFunctionTemplate({ writes, rpc, anonymousSignIn }));
|
|
459
|
+
}
|
|
460
|
+
const commentsPath = await writeCommentsMigration(docs);
|
|
461
|
+
if (commentsPath) console.log(`Wrote ${commentsPath}`);
|
|
462
|
+
const needsDeploy = wroteFunctionFile || commentsPath !== null;
|
|
463
|
+
if (!needsDeploy) {
|
|
464
|
+
console.log("\nNothing new to deploy.");
|
|
465
|
+
return;
|
|
466
|
+
}
|
|
467
|
+
if (!hasSupabaseCli()) {
|
|
468
|
+
console.log("\nSupabase CLI not found on PATH.");
|
|
469
|
+
printManualCommands({ alreadyPushed: commentsPath === null, alreadyDeployed: !wroteFunctionFile });
|
|
470
|
+
return;
|
|
471
|
+
}
|
|
472
|
+
const redeploy = await promptYesNo(rl, "\nApply/deploy these changes now?", true);
|
|
473
|
+
if (!redeploy) {
|
|
474
|
+
printManualCommands({ alreadyPushed: commentsPath === null, alreadyDeployed: !wroteFunctionFile });
|
|
475
|
+
return;
|
|
476
|
+
}
|
|
477
|
+
const result = pushAndDeploy();
|
|
478
|
+
if (!result.deployed) {
|
|
479
|
+
console.error(`
|
|
480
|
+
Supabase CLI command failed: ${result.error instanceof Error ? result.error.message : result.error}`);
|
|
481
|
+
printManualCommands({ alreadyPushed: result.pushed });
|
|
482
|
+
return;
|
|
483
|
+
}
|
|
215
484
|
console.log("\nDone. Paste the function URL + a user JWT into an MCP client to try it.");
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
Supabase CLI command failed: ${err instanceof Error ? err.message : err}`);
|
|
219
|
-
printManualCommands({ alreadyPushed: pushed });
|
|
485
|
+
} finally {
|
|
486
|
+
rl.close();
|
|
220
487
|
}
|
|
221
488
|
}
|
|
222
489
|
async function cmdGenerate() {
|
|
223
|
-
const
|
|
224
|
-
|
|
225
|
-
if (!supabaseUrl || !serviceKey) {
|
|
490
|
+
const conn = resolveConnectionInfo();
|
|
491
|
+
if (!conn) {
|
|
226
492
|
console.error(
|
|
227
|
-
"generate needs SUPABASE_URL and
|
|
493
|
+
"generate needs SUPABASE_URL and SUPABASE_ANON_KEY (env vars, or --url/--key flags) pointing at a running local or linked project (e.g. `supabase start`)."
|
|
228
494
|
);
|
|
229
495
|
process.exitCode = 1;
|
|
230
496
|
return;
|
|
231
497
|
}
|
|
232
|
-
const postgrestUrl = `${
|
|
233
|
-
const headers = new Headers({ authorization: `Bearer ${
|
|
498
|
+
const postgrestUrl = `${conn.url.replace(/\/+$/, "")}/rest/v1`;
|
|
499
|
+
const headers = new Headers({ authorization: `Bearer ${conn.anonKey}`, apikey: conn.anonKey });
|
|
234
500
|
const [openapi, metaRows] = await Promise.all([
|
|
235
501
|
fetchOpenApi(postgrestUrl, headers, fetch),
|
|
236
|
-
|
|
502
|
+
fetchExposifyMeta(postgrestUrl, headers, fetch)
|
|
237
503
|
]);
|
|
238
504
|
const manifest = buildManifest(openapi, metaRows, (/* @__PURE__ */ new Date()).toISOString());
|
|
239
|
-
const outPath = join(FUNCTION_DIR, "
|
|
505
|
+
const outPath = join(FUNCTION_DIR, "exposify.manifest.json");
|
|
240
506
|
await mkdir(dirname(outPath), { recursive: true });
|
|
241
507
|
await writeFile(outPath, JSON.stringify(manifest, null, 2));
|
|
242
508
|
console.log(`Wrote ${outPath} (${manifest.tables.length} table(s))`);
|
|
@@ -254,10 +520,13 @@ async function main() {
|
|
|
254
520
|
const [, , cmd] = process.argv;
|
|
255
521
|
if (cmd === "init") return cmdInit();
|
|
256
522
|
if (cmd === "generate") return cmdGenerate();
|
|
257
|
-
console.log("Usage:
|
|
258
|
-
console.log(" init Scaffold the
|
|
259
|
-
console.log("
|
|
260
|
-
console.log("
|
|
523
|
+
console.log("Usage: exposify-supabase <init|generate> [--url <url>] [--key <anon-key>]");
|
|
524
|
+
console.log(" init Scaffold the exposify_meta() migration + supabase/functions/mcp/index.ts.");
|
|
525
|
+
console.log(" Interactively offers to run `supabase db push` + `functions deploy`; if a");
|
|
526
|
+
console.log(" project is reachable (SUPABASE_URL/SUPABASE_ANON_KEY env vars, or --url/--key),");
|
|
527
|
+
console.log(" also walks through anonymous sign-in, per-table writes, RPC exposure, and");
|
|
528
|
+
console.log(" documentation for tables/columns missing a comment.");
|
|
529
|
+
console.log(" generate Build exposify.manifest.json from a running local/linked project");
|
|
261
530
|
process.exitCode = cmd ? 1 : 0;
|
|
262
531
|
}
|
|
263
532
|
main().catch((err) => {
|
package/dist/index.cjs
CHANGED
|
@@ -21,11 +21,11 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
21
21
|
var index_exports = {};
|
|
22
22
|
__export(index_exports, {
|
|
23
23
|
MANIFEST_VERSION: () => MANIFEST_VERSION,
|
|
24
|
-
|
|
24
|
+
MissingExposifyMetaError: () => MissingExposifyMetaError,
|
|
25
25
|
aiReady: () => aiReady,
|
|
26
26
|
buildManifest: () => buildManifest,
|
|
27
27
|
columnsFromOpenApi: () => columnsFromOpenApi,
|
|
28
|
-
|
|
28
|
+
fetchExposifyMeta: () => fetchExposifyMeta,
|
|
29
29
|
fetchOpenApi: () => fetchOpenApi,
|
|
30
30
|
introspectTables: () => introspectTables,
|
|
31
31
|
loadManifest: () => loadManifest,
|
|
@@ -103,7 +103,7 @@ function warnOnce(message) {
|
|
|
103
103
|
if (warned.has(message)) return;
|
|
104
104
|
warned.add(message);
|
|
105
105
|
try {
|
|
106
|
-
console.warn(`[
|
|
106
|
+
console.warn(`[exposify-supabase] ${message}`);
|
|
107
107
|
} catch {
|
|
108
108
|
}
|
|
109
109
|
}
|
|
@@ -129,6 +129,7 @@ function buildPostgrestHeaders(incoming, anonKey) {
|
|
|
129
129
|
if (value) headers.set(name, value);
|
|
130
130
|
}
|
|
131
131
|
if (!headers.has("apikey")) headers.set("apikey", anonKey);
|
|
132
|
+
if (!headers.has("authorization")) headers.set("authorization", `Bearer ${anonKey}`);
|
|
132
133
|
return headers;
|
|
133
134
|
}
|
|
134
135
|
function createPostgrestClient(postgrestUrl, headers, fetchImpl) {
|
|
@@ -172,31 +173,31 @@ async function fetchOpenApi(postgrestUrl, headers, fetchImpl) {
|
|
|
172
173
|
}
|
|
173
174
|
|
|
174
175
|
// src/meta.ts
|
|
175
|
-
var
|
|
176
|
+
var MissingExposifyMetaError = class extends Error {
|
|
176
177
|
constructor(cause) {
|
|
177
178
|
super(cause);
|
|
178
|
-
this.name = "
|
|
179
|
+
this.name = "MissingExposifyMetaError";
|
|
179
180
|
}
|
|
180
181
|
};
|
|
181
|
-
async function
|
|
182
|
-
const res = await fetchImpl(`${postgrestUrl}/rpc/
|
|
182
|
+
async function fetchExposifyMeta(postgrestUrl, headers, fetchImpl) {
|
|
183
|
+
const res = await fetchImpl(`${postgrestUrl}/rpc/exposify_meta`, {
|
|
183
184
|
method: "POST",
|
|
184
185
|
headers: { ...Object.fromEntries(headers), "content-type": "application/json" },
|
|
185
186
|
body: "{}"
|
|
186
187
|
});
|
|
187
188
|
if (res.status === 404) {
|
|
188
|
-
throw new
|
|
189
|
-
"
|
|
189
|
+
throw new MissingExposifyMetaError(
|
|
190
|
+
"exposify_meta() is not reachable at /rpc/exposify_meta (404). Run `npx exposify-supabase init` (or apply sql/exposify_meta.sql directly) and `supabase db push`, then redeploy the function."
|
|
190
191
|
);
|
|
191
192
|
}
|
|
192
193
|
if (!res.ok) {
|
|
193
194
|
const body = await res.text().catch(() => "");
|
|
194
195
|
if (/PGRST202|function .* does not exist/i.test(body)) {
|
|
195
|
-
throw new
|
|
196
|
-
"
|
|
196
|
+
throw new MissingExposifyMetaError(
|
|
197
|
+
"exposify_meta() is missing from the database (PostgREST error PGRST202). Apply the migration in sql/exposify_meta.sql via `supabase db push`, then redeploy the function."
|
|
197
198
|
);
|
|
198
199
|
}
|
|
199
|
-
throw new Error(`
|
|
200
|
+
throw new Error(`exposify_meta() call failed: HTTP ${res.status} ${body}`.trim());
|
|
200
201
|
}
|
|
201
202
|
return await res.json();
|
|
202
203
|
}
|
|
@@ -227,7 +228,7 @@ function columnsFromOpenApi(doc, table) {
|
|
|
227
228
|
// ON COLUMN` into `description`, then appends its own "Note:\nThis is a
|
|
228
229
|
// Primary/Foreign Key...<pk/|fk/>" boilerplate) -- strip that boilerplate
|
|
229
230
|
// so a column with no real comment ends up `undefined`, not the
|
|
230
|
-
// boilerplate text.
|
|
231
|
+
// boilerplate text. exposify_meta()'s copy wins when both exist anyway
|
|
231
232
|
// -- see introspect.ts's merge.
|
|
232
233
|
description: prop.description?.replace(/\n*Note:[\s\S]*$/, "").trim() || void 0
|
|
233
234
|
}));
|
|
@@ -262,7 +263,7 @@ function introspectTables(openapi, metaRows) {
|
|
|
262
263
|
if (!fromOpenApi) continue;
|
|
263
264
|
columns.push({
|
|
264
265
|
...fromOpenApi,
|
|
265
|
-
//
|
|
266
|
+
// exposify_meta()'s comment wins over OpenAPI's mirrored copy (spec:
|
|
266
267
|
// "Column comments → param descriptions" sourced from the migration).
|
|
267
268
|
description: row.column_comment ?? fromOpenApi.description
|
|
268
269
|
});
|
|
@@ -305,7 +306,7 @@ async function resolveTableSchemas(deps) {
|
|
|
305
306
|
try {
|
|
306
307
|
const [openapi, metaRows] = await Promise.all([
|
|
307
308
|
fetchOpenApi(deps.postgrestUrl, deps.headers, deps.fetchImpl),
|
|
308
|
-
|
|
309
|
+
fetchExposifyMeta(deps.postgrestUrl, deps.headers, deps.fetchImpl)
|
|
309
310
|
]);
|
|
310
311
|
return { tables: introspectTables(openapi, metaRows), mode: "runtime", warnings, openapi };
|
|
311
312
|
} catch (err) {
|
|
@@ -315,7 +316,7 @@ async function resolveTableSchemas(deps) {
|
|
|
315
316
|
if (manifest) {
|
|
316
317
|
warnings.push({
|
|
317
318
|
rung: "manifest",
|
|
318
|
-
reason: "runtime introspection failed; serving tools from the bundled
|
|
319
|
+
reason: "runtime introspection failed; serving tools from the bundled exposify.manifest.json instead"
|
|
319
320
|
});
|
|
320
321
|
return { tables: manifest.tables, mode: "manifest", warnings };
|
|
321
322
|
}
|
|
@@ -602,9 +603,9 @@ ${h.chunk.text}`).join("\n\n---\n\n") : "No matching documentation found.";
|
|
|
602
603
|
function registerDiagnosticsResource(server, warnings) {
|
|
603
604
|
if (warnings.length === 0) return;
|
|
604
605
|
server.registerResource(
|
|
605
|
-
"
|
|
606
|
-
"
|
|
607
|
-
{ title: "
|
|
606
|
+
"exposify-diagnostics",
|
|
607
|
+
"exposify://diagnostics",
|
|
608
|
+
{ title: "exposify-supabase diagnostics", mimeType: "application/json" },
|
|
608
609
|
async (uri) => ({ contents: [{ uri: uri.href, text: JSON.stringify(warnings, null, 2) }] })
|
|
609
610
|
);
|
|
610
611
|
}
|
|
@@ -659,7 +660,7 @@ function aiReady(options = {}) {
|
|
|
659
660
|
manifest: options.manifest
|
|
660
661
|
}).then((ladder) => {
|
|
661
662
|
for (const w of ladder.warnings) {
|
|
662
|
-
console.error(`[
|
|
663
|
+
console.error(`[exposify-supabase] degradation ladder: ${w.rung}: ${w.reason}`);
|
|
663
664
|
}
|
|
664
665
|
return ladder;
|
|
665
666
|
});
|
|
@@ -687,7 +688,7 @@ function aiReady(options = {}) {
|
|
|
687
688
|
return { status: res.status, bodyText: await res.text() };
|
|
688
689
|
};
|
|
689
690
|
return buildSupabaseMcpServer({
|
|
690
|
-
appName: "
|
|
691
|
+
appName: "exposify-supabase",
|
|
691
692
|
appVersion: "0.0.0",
|
|
692
693
|
tables: ladder.tables,
|
|
693
694
|
cfg,
|
|
@@ -706,11 +707,11 @@ function aiReady(options = {}) {
|
|
|
706
707
|
// Annotate the CommonJS export names for ESM import in node:
|
|
707
708
|
0 && (module.exports = {
|
|
708
709
|
MANIFEST_VERSION,
|
|
709
|
-
|
|
710
|
+
MissingExposifyMetaError,
|
|
710
711
|
aiReady,
|
|
711
712
|
buildManifest,
|
|
712
713
|
columnsFromOpenApi,
|
|
713
|
-
|
|
714
|
+
fetchExposifyMeta,
|
|
714
715
|
fetchOpenApi,
|
|
715
716
|
introspectTables,
|
|
716
717
|
loadManifest,
|
package/dist/index.d.cts
CHANGED
|
@@ -7,18 +7,18 @@ interface ColumnSchema {
|
|
|
7
7
|
/** OpenAPI `format`, e.g. "uuid", "timestamp with time zone", "bigint". */
|
|
8
8
|
format?: string;
|
|
9
9
|
required: boolean;
|
|
10
|
-
/** From `
|
|
10
|
+
/** From `exposify_meta()`'s `column_comment` (a `COMMENT ON COLUMN`), not OpenAPI. */
|
|
11
11
|
description?: string;
|
|
12
12
|
isPrimaryKey?: boolean;
|
|
13
13
|
}
|
|
14
14
|
interface TableSchema {
|
|
15
15
|
name: string;
|
|
16
16
|
rlsEnabled: boolean;
|
|
17
|
-
/** From `
|
|
17
|
+
/** From `exposify_meta()`'s `table_comment` (a `COMMENT ON TABLE`). */
|
|
18
18
|
description?: string;
|
|
19
19
|
columns: ColumnSchema[];
|
|
20
20
|
}
|
|
21
|
-
/** One row of `
|
|
21
|
+
/** One row of `exposify_meta()` -- the migration returns one row per column. */
|
|
22
22
|
interface MetaRow {
|
|
23
23
|
table_name: string;
|
|
24
24
|
rls_enabled: boolean;
|
|
@@ -62,7 +62,7 @@ interface AgentifySupabaseOptions {
|
|
|
62
62
|
anonKey?: string;
|
|
63
63
|
/** Injectable for tests; defaults to global `fetch`. */
|
|
64
64
|
fetchImpl?: typeof fetch;
|
|
65
|
-
/** Bundled fallback manifest (from `
|
|
65
|
+
/** Bundled fallback manifest (from `exposify-supabase generate`), used when runtime introspection fails. */
|
|
66
66
|
manifest?: unknown;
|
|
67
67
|
rateLimit?: number;
|
|
68
68
|
rateWindowMs?: number;
|
|
@@ -98,7 +98,7 @@ interface PostgrestColumnProperty {
|
|
|
98
98
|
/**
|
|
99
99
|
* Extracts column schemas for one table from a PostgREST OpenAPI document.
|
|
100
100
|
* Returns `[]` for a table PostgREST never exposed (not in `definitions` --
|
|
101
|
-
* e.g. it exists at the catalog level per `
|
|
101
|
+
* e.g. it exists at the catalog level per `exposify_meta()` but PostgREST's
|
|
102
102
|
* role lacks USAGE/SELECT grants on it), which the caller treats the same as
|
|
103
103
|
* "RLS-less": never shown to the agent.
|
|
104
104
|
*/
|
|
@@ -113,27 +113,27 @@ declare function columnsFromOpenApi(doc: PostgrestOpenApiDoc, table: string): Co
|
|
|
113
113
|
declare function rpcArgSchema(doc: PostgrestOpenApiDoc, fn: string): JsonSchemaObject | null;
|
|
114
114
|
|
|
115
115
|
declare const MANIFEST_VERSION = 1;
|
|
116
|
-
interface
|
|
116
|
+
interface ExposifyManifest {
|
|
117
117
|
version: typeof MANIFEST_VERSION;
|
|
118
118
|
generatedAt: string;
|
|
119
119
|
tables: TableSchema[];
|
|
120
120
|
}
|
|
121
|
-
/** Built by `npx
|
|
122
|
-
declare function buildManifest(openapi: PostgrestOpenApiDoc, metaRows: MetaRow[], generatedAt: string):
|
|
121
|
+
/** Built by `npx exposify-supabase generate` against a local/linked project. */
|
|
122
|
+
declare function buildManifest(openapi: PostgrestOpenApiDoc, metaRows: MetaRow[], generatedAt: string): ExposifyManifest;
|
|
123
123
|
/**
|
|
124
124
|
* Defensive load: a manifest is bundled JSON shipped alongside the function
|
|
125
125
|
* and can go stale relative to the deployed schema, or simply be malformed
|
|
126
126
|
* (hand-edited, wrong version). Returns `null` -- never throws -- so the
|
|
127
127
|
* degradation ladder can fall through to the next rung instead of crashing.
|
|
128
128
|
*/
|
|
129
|
-
declare function loadManifest(raw: unknown):
|
|
129
|
+
declare function loadManifest(raw: unknown): ExposifyManifest | null;
|
|
130
130
|
|
|
131
131
|
/**
|
|
132
|
-
* Merges PostgREST's OpenAPI document (column types) with `
|
|
132
|
+
* Merges PostgREST's OpenAPI document (column types) with `exposify_meta()`
|
|
133
133
|
* rows (RLS status + comments) into the DB tool surface.
|
|
134
134
|
*
|
|
135
135
|
* DB exposure policy (spec, locked): RLS-gated, reads auto. A table only
|
|
136
|
-
* appears here if `
|
|
136
|
+
* appears here if `exposify_meta()` reports `rls_enabled: true` for it AND
|
|
137
137
|
* PostgREST actually exposes it in its OpenAPI `definitions` (the role
|
|
138
138
|
* calling this has grants on it) -- a table failing either check never
|
|
139
139
|
* appears, silently, exactly like a table PostgREST doesn't know about at
|
|
@@ -142,25 +142,26 @@ declare function loadManifest(raw: unknown): AgentifyManifest | null;
|
|
|
142
142
|
*/
|
|
143
143
|
declare function introspectTables(openapi: PostgrestOpenApiDoc, metaRows: MetaRow[]): TableSchema[];
|
|
144
144
|
|
|
145
|
-
declare class
|
|
145
|
+
declare class MissingExposifyMetaError extends Error {
|
|
146
146
|
constructor(cause: string);
|
|
147
147
|
}
|
|
148
148
|
/**
|
|
149
|
-
* Calls the `
|
|
150
|
-
* endpoint. Requires the migration from `sql/
|
|
149
|
+
* Calls the `exposify_meta()` Postgres function via PostgREST's RPC
|
|
150
|
+
* endpoint. Requires the migration from `sql/exposify_meta.sql` to have been
|
|
151
151
|
* applied; a 404 (unknown route) or PostgREST's `PGRST202` (function not
|
|
152
152
|
* found in schema cache) both mean "migration missing" and are translated
|
|
153
153
|
* into a self-diagnosing error (spec: "a missing `agentify_meta()` migration
|
|
154
|
-
* produces a self-diagnosing message naming the exact fix"
|
|
154
|
+
* produces a self-diagnosing message naming the exact fix" -- function
|
|
155
|
+
* renamed to `exposify_meta()` post-spec, see the decisions journal).
|
|
155
156
|
*/
|
|
156
|
-
declare function
|
|
157
|
+
declare function fetchExposifyMeta(postgrestUrl: string, headers: Headers, fetchImpl: typeof fetch): Promise<MetaRow[]>;
|
|
157
158
|
|
|
158
159
|
/** PostgREST serves its OpenAPI document at the API root when asked for it explicitly. */
|
|
159
160
|
declare function fetchOpenApi(postgrestUrl: string, headers: Headers, fetchImpl: typeof fetch): Promise<PostgrestOpenApiDoc>;
|
|
160
161
|
|
|
161
162
|
/**
|
|
162
163
|
* Builds the `Deno.serve`-compatible MCP handler. Everything expensive
|
|
163
|
-
* (OpenAPI fetch, `
|
|
164
|
+
* (OpenAPI fetch, `exposify_meta()` call, doc loading/chunking) happens
|
|
164
165
|
* lazily on first request and is cached in-memory for the life of the edge
|
|
165
166
|
* instance -- "cold start" in the spec's sense, not literally module load,
|
|
166
167
|
* since env vars and the incoming JWT aren't available before the first
|
|
@@ -168,4 +169,4 @@ declare function fetchOpenApi(postgrestUrl: string, headers: Headers, fetchImpl:
|
|
|
168
169
|
*/
|
|
169
170
|
declare function aiReady(options?: AgentifySupabaseOptions): WebMcpHandler;
|
|
170
171
|
|
|
171
|
-
export { type
|
|
172
|
+
export { type AgentifySupabaseOptions, type ExposifyManifest, MANIFEST_VERSION, MissingExposifyMetaError, type PostgrestOpenApiDoc, aiReady, buildManifest, columnsFromOpenApi, fetchExposifyMeta, fetchOpenApi, introspectTables, loadManifest, rpcArgSchema };
|
package/dist/index.d.ts
CHANGED
|
@@ -7,18 +7,18 @@ interface ColumnSchema {
|
|
|
7
7
|
/** OpenAPI `format`, e.g. "uuid", "timestamp with time zone", "bigint". */
|
|
8
8
|
format?: string;
|
|
9
9
|
required: boolean;
|
|
10
|
-
/** From `
|
|
10
|
+
/** From `exposify_meta()`'s `column_comment` (a `COMMENT ON COLUMN`), not OpenAPI. */
|
|
11
11
|
description?: string;
|
|
12
12
|
isPrimaryKey?: boolean;
|
|
13
13
|
}
|
|
14
14
|
interface TableSchema {
|
|
15
15
|
name: string;
|
|
16
16
|
rlsEnabled: boolean;
|
|
17
|
-
/** From `
|
|
17
|
+
/** From `exposify_meta()`'s `table_comment` (a `COMMENT ON TABLE`). */
|
|
18
18
|
description?: string;
|
|
19
19
|
columns: ColumnSchema[];
|
|
20
20
|
}
|
|
21
|
-
/** One row of `
|
|
21
|
+
/** One row of `exposify_meta()` -- the migration returns one row per column. */
|
|
22
22
|
interface MetaRow {
|
|
23
23
|
table_name: string;
|
|
24
24
|
rls_enabled: boolean;
|
|
@@ -62,7 +62,7 @@ interface AgentifySupabaseOptions {
|
|
|
62
62
|
anonKey?: string;
|
|
63
63
|
/** Injectable for tests; defaults to global `fetch`. */
|
|
64
64
|
fetchImpl?: typeof fetch;
|
|
65
|
-
/** Bundled fallback manifest (from `
|
|
65
|
+
/** Bundled fallback manifest (from `exposify-supabase generate`), used when runtime introspection fails. */
|
|
66
66
|
manifest?: unknown;
|
|
67
67
|
rateLimit?: number;
|
|
68
68
|
rateWindowMs?: number;
|
|
@@ -98,7 +98,7 @@ interface PostgrestColumnProperty {
|
|
|
98
98
|
/**
|
|
99
99
|
* Extracts column schemas for one table from a PostgREST OpenAPI document.
|
|
100
100
|
* Returns `[]` for a table PostgREST never exposed (not in `definitions` --
|
|
101
|
-
* e.g. it exists at the catalog level per `
|
|
101
|
+
* e.g. it exists at the catalog level per `exposify_meta()` but PostgREST's
|
|
102
102
|
* role lacks USAGE/SELECT grants on it), which the caller treats the same as
|
|
103
103
|
* "RLS-less": never shown to the agent.
|
|
104
104
|
*/
|
|
@@ -113,27 +113,27 @@ declare function columnsFromOpenApi(doc: PostgrestOpenApiDoc, table: string): Co
|
|
|
113
113
|
declare function rpcArgSchema(doc: PostgrestOpenApiDoc, fn: string): JsonSchemaObject | null;
|
|
114
114
|
|
|
115
115
|
declare const MANIFEST_VERSION = 1;
|
|
116
|
-
interface
|
|
116
|
+
interface ExposifyManifest {
|
|
117
117
|
version: typeof MANIFEST_VERSION;
|
|
118
118
|
generatedAt: string;
|
|
119
119
|
tables: TableSchema[];
|
|
120
120
|
}
|
|
121
|
-
/** Built by `npx
|
|
122
|
-
declare function buildManifest(openapi: PostgrestOpenApiDoc, metaRows: MetaRow[], generatedAt: string):
|
|
121
|
+
/** Built by `npx exposify-supabase generate` against a local/linked project. */
|
|
122
|
+
declare function buildManifest(openapi: PostgrestOpenApiDoc, metaRows: MetaRow[], generatedAt: string): ExposifyManifest;
|
|
123
123
|
/**
|
|
124
124
|
* Defensive load: a manifest is bundled JSON shipped alongside the function
|
|
125
125
|
* and can go stale relative to the deployed schema, or simply be malformed
|
|
126
126
|
* (hand-edited, wrong version). Returns `null` -- never throws -- so the
|
|
127
127
|
* degradation ladder can fall through to the next rung instead of crashing.
|
|
128
128
|
*/
|
|
129
|
-
declare function loadManifest(raw: unknown):
|
|
129
|
+
declare function loadManifest(raw: unknown): ExposifyManifest | null;
|
|
130
130
|
|
|
131
131
|
/**
|
|
132
|
-
* Merges PostgREST's OpenAPI document (column types) with `
|
|
132
|
+
* Merges PostgREST's OpenAPI document (column types) with `exposify_meta()`
|
|
133
133
|
* rows (RLS status + comments) into the DB tool surface.
|
|
134
134
|
*
|
|
135
135
|
* DB exposure policy (spec, locked): RLS-gated, reads auto. A table only
|
|
136
|
-
* appears here if `
|
|
136
|
+
* appears here if `exposify_meta()` reports `rls_enabled: true` for it AND
|
|
137
137
|
* PostgREST actually exposes it in its OpenAPI `definitions` (the role
|
|
138
138
|
* calling this has grants on it) -- a table failing either check never
|
|
139
139
|
* appears, silently, exactly like a table PostgREST doesn't know about at
|
|
@@ -142,25 +142,26 @@ declare function loadManifest(raw: unknown): AgentifyManifest | null;
|
|
|
142
142
|
*/
|
|
143
143
|
declare function introspectTables(openapi: PostgrestOpenApiDoc, metaRows: MetaRow[]): TableSchema[];
|
|
144
144
|
|
|
145
|
-
declare class
|
|
145
|
+
declare class MissingExposifyMetaError extends Error {
|
|
146
146
|
constructor(cause: string);
|
|
147
147
|
}
|
|
148
148
|
/**
|
|
149
|
-
* Calls the `
|
|
150
|
-
* endpoint. Requires the migration from `sql/
|
|
149
|
+
* Calls the `exposify_meta()` Postgres function via PostgREST's RPC
|
|
150
|
+
* endpoint. Requires the migration from `sql/exposify_meta.sql` to have been
|
|
151
151
|
* applied; a 404 (unknown route) or PostgREST's `PGRST202` (function not
|
|
152
152
|
* found in schema cache) both mean "migration missing" and are translated
|
|
153
153
|
* into a self-diagnosing error (spec: "a missing `agentify_meta()` migration
|
|
154
|
-
* produces a self-diagnosing message naming the exact fix"
|
|
154
|
+
* produces a self-diagnosing message naming the exact fix" -- function
|
|
155
|
+
* renamed to `exposify_meta()` post-spec, see the decisions journal).
|
|
155
156
|
*/
|
|
156
|
-
declare function
|
|
157
|
+
declare function fetchExposifyMeta(postgrestUrl: string, headers: Headers, fetchImpl: typeof fetch): Promise<MetaRow[]>;
|
|
157
158
|
|
|
158
159
|
/** PostgREST serves its OpenAPI document at the API root when asked for it explicitly. */
|
|
159
160
|
declare function fetchOpenApi(postgrestUrl: string, headers: Headers, fetchImpl: typeof fetch): Promise<PostgrestOpenApiDoc>;
|
|
160
161
|
|
|
161
162
|
/**
|
|
162
163
|
* Builds the `Deno.serve`-compatible MCP handler. Everything expensive
|
|
163
|
-
* (OpenAPI fetch, `
|
|
164
|
+
* (OpenAPI fetch, `exposify_meta()` call, doc loading/chunking) happens
|
|
164
165
|
* lazily on first request and is cached in-memory for the life of the edge
|
|
165
166
|
* instance -- "cold start" in the spec's sense, not literally module load,
|
|
166
167
|
* since env vars and the incoming JWT aren't available before the first
|
|
@@ -168,4 +169,4 @@ declare function fetchOpenApi(postgrestUrl: string, headers: Headers, fetchImpl:
|
|
|
168
169
|
*/
|
|
169
170
|
declare function aiReady(options?: AgentifySupabaseOptions): WebMcpHandler;
|
|
170
171
|
|
|
171
|
-
export { type
|
|
172
|
+
export { type AgentifySupabaseOptions, type ExposifyManifest, MANIFEST_VERSION, MissingExposifyMetaError, type PostgrestOpenApiDoc, aiReady, buildManifest, columnsFromOpenApi, fetchExposifyMeta, fetchOpenApi, introspectTables, loadManifest, rpcArgSchema };
|
package/dist/index.js
CHANGED
|
@@ -70,7 +70,7 @@ function warnOnce(message) {
|
|
|
70
70
|
if (warned.has(message)) return;
|
|
71
71
|
warned.add(message);
|
|
72
72
|
try {
|
|
73
|
-
console.warn(`[
|
|
73
|
+
console.warn(`[exposify-supabase] ${message}`);
|
|
74
74
|
} catch {
|
|
75
75
|
}
|
|
76
76
|
}
|
|
@@ -96,6 +96,7 @@ function buildPostgrestHeaders(incoming, anonKey) {
|
|
|
96
96
|
if (value) headers.set(name, value);
|
|
97
97
|
}
|
|
98
98
|
if (!headers.has("apikey")) headers.set("apikey", anonKey);
|
|
99
|
+
if (!headers.has("authorization")) headers.set("authorization", `Bearer ${anonKey}`);
|
|
99
100
|
return headers;
|
|
100
101
|
}
|
|
101
102
|
function createPostgrestClient(postgrestUrl, headers, fetchImpl) {
|
|
@@ -139,31 +140,31 @@ async function fetchOpenApi(postgrestUrl, headers, fetchImpl) {
|
|
|
139
140
|
}
|
|
140
141
|
|
|
141
142
|
// src/meta.ts
|
|
142
|
-
var
|
|
143
|
+
var MissingExposifyMetaError = class extends Error {
|
|
143
144
|
constructor(cause) {
|
|
144
145
|
super(cause);
|
|
145
|
-
this.name = "
|
|
146
|
+
this.name = "MissingExposifyMetaError";
|
|
146
147
|
}
|
|
147
148
|
};
|
|
148
|
-
async function
|
|
149
|
-
const res = await fetchImpl(`${postgrestUrl}/rpc/
|
|
149
|
+
async function fetchExposifyMeta(postgrestUrl, headers, fetchImpl) {
|
|
150
|
+
const res = await fetchImpl(`${postgrestUrl}/rpc/exposify_meta`, {
|
|
150
151
|
method: "POST",
|
|
151
152
|
headers: { ...Object.fromEntries(headers), "content-type": "application/json" },
|
|
152
153
|
body: "{}"
|
|
153
154
|
});
|
|
154
155
|
if (res.status === 404) {
|
|
155
|
-
throw new
|
|
156
|
-
"
|
|
156
|
+
throw new MissingExposifyMetaError(
|
|
157
|
+
"exposify_meta() is not reachable at /rpc/exposify_meta (404). Run `npx exposify-supabase init` (or apply sql/exposify_meta.sql directly) and `supabase db push`, then redeploy the function."
|
|
157
158
|
);
|
|
158
159
|
}
|
|
159
160
|
if (!res.ok) {
|
|
160
161
|
const body = await res.text().catch(() => "");
|
|
161
162
|
if (/PGRST202|function .* does not exist/i.test(body)) {
|
|
162
|
-
throw new
|
|
163
|
-
"
|
|
163
|
+
throw new MissingExposifyMetaError(
|
|
164
|
+
"exposify_meta() is missing from the database (PostgREST error PGRST202). Apply the migration in sql/exposify_meta.sql via `supabase db push`, then redeploy the function."
|
|
164
165
|
);
|
|
165
166
|
}
|
|
166
|
-
throw new Error(`
|
|
167
|
+
throw new Error(`exposify_meta() call failed: HTTP ${res.status} ${body}`.trim());
|
|
167
168
|
}
|
|
168
169
|
return await res.json();
|
|
169
170
|
}
|
|
@@ -194,7 +195,7 @@ function columnsFromOpenApi(doc, table) {
|
|
|
194
195
|
// ON COLUMN` into `description`, then appends its own "Note:\nThis is a
|
|
195
196
|
// Primary/Foreign Key...<pk/|fk/>" boilerplate) -- strip that boilerplate
|
|
196
197
|
// so a column with no real comment ends up `undefined`, not the
|
|
197
|
-
// boilerplate text.
|
|
198
|
+
// boilerplate text. exposify_meta()'s copy wins when both exist anyway
|
|
198
199
|
// -- see introspect.ts's merge.
|
|
199
200
|
description: prop.description?.replace(/\n*Note:[\s\S]*$/, "").trim() || void 0
|
|
200
201
|
}));
|
|
@@ -229,7 +230,7 @@ function introspectTables(openapi, metaRows) {
|
|
|
229
230
|
if (!fromOpenApi) continue;
|
|
230
231
|
columns.push({
|
|
231
232
|
...fromOpenApi,
|
|
232
|
-
//
|
|
233
|
+
// exposify_meta()'s comment wins over OpenAPI's mirrored copy (spec:
|
|
233
234
|
// "Column comments → param descriptions" sourced from the migration).
|
|
234
235
|
description: row.column_comment ?? fromOpenApi.description
|
|
235
236
|
});
|
|
@@ -272,7 +273,7 @@ async function resolveTableSchemas(deps) {
|
|
|
272
273
|
try {
|
|
273
274
|
const [openapi, metaRows] = await Promise.all([
|
|
274
275
|
fetchOpenApi(deps.postgrestUrl, deps.headers, deps.fetchImpl),
|
|
275
|
-
|
|
276
|
+
fetchExposifyMeta(deps.postgrestUrl, deps.headers, deps.fetchImpl)
|
|
276
277
|
]);
|
|
277
278
|
return { tables: introspectTables(openapi, metaRows), mode: "runtime", warnings, openapi };
|
|
278
279
|
} catch (err) {
|
|
@@ -282,7 +283,7 @@ async function resolveTableSchemas(deps) {
|
|
|
282
283
|
if (manifest) {
|
|
283
284
|
warnings.push({
|
|
284
285
|
rung: "manifest",
|
|
285
|
-
reason: "runtime introspection failed; serving tools from the bundled
|
|
286
|
+
reason: "runtime introspection failed; serving tools from the bundled exposify.manifest.json instead"
|
|
286
287
|
});
|
|
287
288
|
return { tables: manifest.tables, mode: "manifest", warnings };
|
|
288
289
|
}
|
|
@@ -572,9 +573,9 @@ ${h.chunk.text}`).join("\n\n---\n\n") : "No matching documentation found.";
|
|
|
572
573
|
function registerDiagnosticsResource(server, warnings) {
|
|
573
574
|
if (warnings.length === 0) return;
|
|
574
575
|
server.registerResource(
|
|
575
|
-
"
|
|
576
|
-
"
|
|
577
|
-
{ title: "
|
|
576
|
+
"exposify-diagnostics",
|
|
577
|
+
"exposify://diagnostics",
|
|
578
|
+
{ title: "exposify-supabase diagnostics", mimeType: "application/json" },
|
|
578
579
|
async (uri) => ({ contents: [{ uri: uri.href, text: JSON.stringify(warnings, null, 2) }] })
|
|
579
580
|
);
|
|
580
581
|
}
|
|
@@ -629,7 +630,7 @@ function aiReady(options = {}) {
|
|
|
629
630
|
manifest: options.manifest
|
|
630
631
|
}).then((ladder) => {
|
|
631
632
|
for (const w of ladder.warnings) {
|
|
632
|
-
console.error(`[
|
|
633
|
+
console.error(`[exposify-supabase] degradation ladder: ${w.rung}: ${w.reason}`);
|
|
633
634
|
}
|
|
634
635
|
return ladder;
|
|
635
636
|
});
|
|
@@ -657,7 +658,7 @@ function aiReady(options = {}) {
|
|
|
657
658
|
return { status: res.status, bodyText: await res.text() };
|
|
658
659
|
};
|
|
659
660
|
return buildSupabaseMcpServer({
|
|
660
|
-
appName: "
|
|
661
|
+
appName: "exposify-supabase",
|
|
661
662
|
appVersion: "0.0.0",
|
|
662
663
|
tables: ladder.tables,
|
|
663
664
|
cfg,
|
|
@@ -675,11 +676,11 @@ function aiReady(options = {}) {
|
|
|
675
676
|
}
|
|
676
677
|
export {
|
|
677
678
|
MANIFEST_VERSION,
|
|
678
|
-
|
|
679
|
+
MissingExposifyMetaError,
|
|
679
680
|
aiReady,
|
|
680
681
|
buildManifest,
|
|
681
682
|
columnsFromOpenApi,
|
|
682
|
-
|
|
683
|
+
fetchExposifyMeta,
|
|
683
684
|
fetchOpenApi,
|
|
684
685
|
introspectTables,
|
|
685
686
|
loadManifest,
|
package/package.json
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@exposify/supabase",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.cjs",
|
|
7
7
|
"module": "./dist/index.js",
|
|
8
8
|
"types": "./dist/index.d.ts",
|
|
9
9
|
"bin": {
|
|
10
|
-
"
|
|
10
|
+
"exposify-supabase": "./dist/cli.js"
|
|
11
11
|
},
|
|
12
12
|
"exports": {
|
|
13
13
|
".": {
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
"prepack": "yarn build"
|
|
28
28
|
},
|
|
29
29
|
"dependencies": {
|
|
30
|
-
"@exposify/core": "0.
|
|
30
|
+
"@exposify/core": "0.2.0",
|
|
31
31
|
"@modelcontextprotocol/sdk": "^1.12.0",
|
|
32
32
|
"zod": "^3.24.0"
|
|
33
33
|
},
|
|
@@ -39,5 +39,14 @@
|
|
|
39
39
|
},
|
|
40
40
|
"publishConfig": {
|
|
41
41
|
"access": "public"
|
|
42
|
+
},
|
|
43
|
+
"repository": {
|
|
44
|
+
"type": "git",
|
|
45
|
+
"url": "git+https://github.com/ayadagain/agentready.git",
|
|
46
|
+
"directory": "packages/supabase"
|
|
47
|
+
},
|
|
48
|
+
"homepage": "https://github.com/ayadagain/agentready/tree/main/packages/supabase#readme",
|
|
49
|
+
"bugs": {
|
|
50
|
+
"url": "https://github.com/ayadagain/agentready/issues"
|
|
42
51
|
}
|
|
43
52
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
--
|
|
1
|
+
-- exposify-supabase: schema introspection function.
|
|
2
2
|
--
|
|
3
|
-
-- Exposes, per PostgREST's RPC convention (POST /rpc/
|
|
3
|
+
-- Exposes, per PostgREST's RPC convention (POST /rpc/exposify_meta), one row
|
|
4
4
|
-- per column of every ordinary table in the `public` schema: whether Row
|
|
5
5
|
-- Level Security is enabled on its table, and the table/column comments an
|
|
6
6
|
-- owner wrote via `COMMENT ON TABLE` / `COMMENT ON COLUMN`.
|
|
@@ -12,7 +12,15 @@
|
|
|
12
12
|
-- necessarily SELECT from would otherwise be blocked; the function only ever
|
|
13
13
|
-- returns catalog metadata, never row data, so this does not leak anything
|
|
14
14
|
-- RLS would otherwise protect.
|
|
15
|
-
|
|
15
|
+
--
|
|
16
|
+
-- Renamed from agentify_meta() -- if you already applied that migration to a
|
|
17
|
+
-- live project, this file alone will NOT rename it there (Postgres tracks
|
|
18
|
+
-- migrations by filename, not by diffing function bodies). Apply this
|
|
19
|
+
-- instead on an already-deployed project:
|
|
20
|
+
-- alter function public.agentify_meta() rename to exposify_meta();
|
|
21
|
+
-- or drop the old one after confirming nothing else calls it:
|
|
22
|
+
-- drop function public.agentify_meta();
|
|
23
|
+
create or replace function public.exposify_meta()
|
|
16
24
|
returns table (
|
|
17
25
|
table_name text,
|
|
18
26
|
rls_enabled boolean,
|
|
@@ -43,4 +51,4 @@ $$;
|
|
|
43
51
|
-- PostgREST's `anon`/`authenticated` roles must be able to call this RPC;
|
|
44
52
|
-- the SECURITY DEFINER body above is what makes that safe (catalog metadata
|
|
45
53
|
-- only, never row data).
|
|
46
|
-
grant execute on function public.
|
|
54
|
+
grant execute on function public.exposify_meta() to anon, authenticated;
|