better-supabase 0.0.0 → 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.
Files changed (154) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +88 -1
  3. package/bin/better-supabase.js +3 -0
  4. package/dist/casing-da0uRTqt.js +14 -0
  5. package/dist/cli/bin.d.ts +1 -0
  6. package/dist/cli/bin.js +15 -0
  7. package/dist/cli/index.d.ts +226 -0
  8. package/dist/cli/index.js +3 -0
  9. package/dist/cli-CoByWYja.js +6472 -0
  10. package/dist/client/index.d.ts +2 -0
  11. package/dist/client/index.js +112 -0
  12. package/dist/compiler-DL18ukPe.d.ts +14 -0
  13. package/dist/config/index.d.ts +3 -0
  14. package/dist/config/index.js +3 -0
  15. package/dist/config-D-aR2ZoN.js +276 -0
  16. package/dist/define-CTfWw-SS.d.ts +662 -0
  17. package/dist/define-QGkq04oe.js +1068 -0
  18. package/dist/edge/index.d.ts +43 -0
  19. package/dist/edge/index.js +103 -0
  20. package/dist/entitlements-BBg61DQZ.d.ts +20 -0
  21. package/dist/entitlements-BC_khwRy.js +17 -0
  22. package/dist/env/index.d.ts +2 -0
  23. package/dist/env/index.js +2 -0
  24. package/dist/env-BEToab0I.js +204 -0
  25. package/dist/errors-Bl3rkOzo.js +196 -0
  26. package/dist/events/index.d.ts +2 -0
  27. package/dist/events/index.js +2 -0
  28. package/dist/events-2OJsVG_d.d.ts +84 -0
  29. package/dist/events-BuzTgn2D.js +135 -0
  30. package/dist/executor-Bazhgsgq.d.ts +195 -0
  31. package/dist/executor-Bje4suQF.d.ts +25 -0
  32. package/dist/executor-Ceq60hPM.js +89 -0
  33. package/dist/hono/index.d.ts +45 -0
  34. package/dist/hono/index.js +70 -0
  35. package/dist/index-BD6wKthp.d.ts +597 -0
  36. package/dist/index-DQVvHzjW.d.ts +74 -0
  37. package/dist/index-DRid0qIv.d.ts +76 -0
  38. package/dist/index-Dup-nSe9.d.ts +117 -0
  39. package/dist/index-Dy6kuF0P.d.ts +71 -0
  40. package/dist/index-Hf8PP9sP.d.ts +116 -0
  41. package/dist/index-JXHBuS53.d.ts +144 -0
  42. package/dist/index-Nxo3TJcm.d.ts +208 -0
  43. package/dist/index-Sw43J8HG.d.ts +62 -0
  44. package/dist/index-cEeweTeM.d.ts +38 -0
  45. package/dist/index.d.ts +110 -0
  46. package/dist/index.js +42 -0
  47. package/dist/invalidate-DbxuL2jL.js +18 -0
  48. package/dist/jobs/index.d.ts +173 -0
  49. package/dist/jobs/index.js +477 -0
  50. package/dist/json-schema-BbwO2mZg.js +181 -0
  51. package/dist/lint/index.d.ts +75 -0
  52. package/dist/lint/index.js +167 -0
  53. package/dist/list/index.d.ts +2 -0
  54. package/dist/list/index.js +410 -0
  55. package/dist/live-6b9i72Ux.d.ts +180 -0
  56. package/dist/live-D8rV1Ldw.js +168 -0
  57. package/dist/mcp/index.d.ts +105 -0
  58. package/dist/mcp/index.js +330 -0
  59. package/dist/mfa-CpogP66z.js +521 -0
  60. package/dist/mfa-TVia554c.d.ts +44 -0
  61. package/dist/next/image/index.d.ts +34 -0
  62. package/dist/next/image/index.js +40 -0
  63. package/dist/next/index.d.ts +210 -0
  64. package/dist/next/index.js +426 -0
  65. package/dist/openapi/index.d.ts +2 -0
  66. package/dist/openapi/index.js +332 -0
  67. package/dist/orpc/index.d.ts +46 -0
  68. package/dist/orpc/index.js +50 -0
  69. package/dist/otel/index.d.ts +40 -0
  70. package/dist/otel/index.js +168 -0
  71. package/dist/path-Cv__-OTj.d.ts +17 -0
  72. package/dist/plugin-BqR6wKMB.js +23 -0
  73. package/dist/plugin-HvGqCurB.d.ts +118 -0
  74. package/dist/plugins/actor/index.d.ts +13 -0
  75. package/dist/plugins/actor/index.js +43 -0
  76. package/dist/plugins/rules/index.d.ts +88 -0
  77. package/dist/plugins/rules/index.js +174 -0
  78. package/dist/plugins/soft-delete/index.d.ts +38 -0
  79. package/dist/plugins/soft-delete/index.js +69 -0
  80. package/dist/plugins/tenant/index.d.ts +43 -0
  81. package/dist/plugins/tenant/index.js +73 -0
  82. package/dist/plugins/timestamps/index.d.ts +9 -0
  83. package/dist/plugins/timestamps/index.js +37 -0
  84. package/dist/plugins/validation/index.d.ts +24 -0
  85. package/dist/plugins/validation/index.js +59 -0
  86. package/dist/pool-BFeqzwQS.d.ts +37 -0
  87. package/dist/postgres/index.d.ts +28 -0
  88. package/dist/postgres/index.js +79 -0
  89. package/dist/postgrest-Dm-vhY6R.d.ts +43 -0
  90. package/dist/problem-DPu6Fh-t.js +97 -0
  91. package/dist/problem-DlgB0lYl.d.ts +47 -0
  92. package/dist/query/index.d.ts +2 -0
  93. package/dist/query/index.js +3 -0
  94. package/dist/query-ChvDbsDE.js +186 -0
  95. package/dist/react/index.d.ts +7 -0
  96. package/dist/react/index.js +5 -0
  97. package/dist/react/server.d.ts +13 -0
  98. package/dist/react/server.js +25 -0
  99. package/dist/react/session.d.ts +2 -0
  100. package/dist/react/session.js +3 -0
  101. package/dist/react-CrOvrnIP.js +241 -0
  102. package/dist/read-set-DhX2d04c.js +216 -0
  103. package/dist/realtime/index.d.ts +3 -0
  104. package/dist/realtime/index.js +253 -0
  105. package/dist/resolve-DW8h9NPm.d.ts +155 -0
  106. package/dist/resource-CYiu6Ygi.js +175 -0
  107. package/dist/resource-SkPTRJDD.d.ts +65 -0
  108. package/dist/respond-4-dhoV7x.js +380 -0
  109. package/dist/respond-PoTpy5DT.d.ts +169 -0
  110. package/dist/result-CTkCZ6JA.d.ts +198 -0
  111. package/dist/result-DWXatkd6.js +103 -0
  112. package/dist/scope-CDHcS7dE.js +90 -0
  113. package/dist/seed-CzecODcJ.js +101 -0
  114. package/dist/server/index.d.ts +42 -0
  115. package/dist/server/index.js +68 -0
  116. package/dist/session-C3osL21k.d.ts +21 -0
  117. package/dist/session-CD-3orO_.js +19 -0
  118. package/dist/shared-_6-uPL7E.js +57 -0
  119. package/dist/simplify-oHiDKRVQ.js +665 -0
  120. package/dist/spec-pins-CJPVghHy.d.ts +20 -0
  121. package/dist/spec-pins-t6sPaHvl.js +20 -0
  122. package/dist/sql-M8l3fnS0.js +260 -0
  123. package/dist/ssr/index.d.ts +5 -0
  124. package/dist/ssr/index.js +4 -0
  125. package/dist/standard-CyhRzctb.d.ts +7 -0
  126. package/dist/standard-FUi3Z3-7.js +21 -0
  127. package/dist/stats-3iy8u3_P.js +566 -0
  128. package/dist/storage/index.d.ts +4 -0
  129. package/dist/storage/index.js +2 -0
  130. package/dist/storage-BWXVUXGk.js +461 -0
  131. package/dist/tables-rZxRxNPW.js +59 -0
  132. package/dist/template-BeOGnynM.js +93 -0
  133. package/dist/template-mbvlysOo.d.ts +6 -0
  134. package/dist/testing/index.d.ts +294 -0
  135. package/dist/testing/index.js +751 -0
  136. package/dist/types-6cYK9pbJ.js +29 -0
  137. package/dist/types-C2fslP1z.d.ts +195 -0
  138. package/dist/version-Cqq1m3UY.js +4 -0
  139. package/dist/view-B0ltUmTi.d.ts +43 -0
  140. package/dist/view-B6F9QbG-.js +35 -0
  141. package/dist/webhooks/index.d.ts +152 -0
  142. package/dist/webhooks/index.js +2 -0
  143. package/dist/webhooks-DbMTJJX2.js +139 -0
  144. package/package.json +246 -17
  145. package/schemas/config-v1.json +328 -0
  146. package/schemas/doctor-report-v1.json +105 -0
  147. package/schemas/snapshot-v2.json +1002 -0
  148. package/skills/better-supabase/SKILL.md +84 -0
  149. package/skills/better-supabase/references/plugins.md +56 -0
  150. package/skills/better-supabase/references/troubleshooting.md +39 -0
  151. package/skills/better-supabase-api/SKILL.md +67 -0
  152. package/skills/better-supabase-api/references/adapters.md +108 -0
  153. package/skills/better-supabase-testing/SKILL.md +65 -0
  154. package/index.js +0 -1
@@ -0,0 +1,168 @@
1
+ import { REALTIME_SUBSCRIBE_STATES } from "@supabase/supabase-js";
2
+ //#region src/realtime/live.ts
3
+ /** Topic a `realtime-tables` trigger broadcasts on: `bs:t:<schema>.<table>[:<tenant>]`. */
4
+ function liveTopic(meta, table, tenant) {
5
+ const found = meta.tables[table];
6
+ if (!found) throw new TypeError(`better-supabase: unknown table "${table}"`);
7
+ const base = `bs:t:${found.schema}.${found.name}`;
8
+ if (!meta.realtime?.[table]?.tenant) return base;
9
+ if (tenant === void 0) throw new TypeError(`better-supabase: "${table}" broadcasts per tenant; pass \`tenant\` to watch it`);
10
+ return `${base}:${tenant}`;
11
+ }
12
+ const channels = /* @__PURE__ */ new WeakMap();
13
+ function join(client, topic, listener) {
14
+ let byTopic = channels.get(client);
15
+ if (!byTopic) {
16
+ byTopic = /* @__PURE__ */ new Map();
17
+ channels.set(client, byTopic);
18
+ }
19
+ let shared = byTopic.get(topic);
20
+ if (!shared) {
21
+ const channel = client.channel(topic, { config: { private: true } });
22
+ const listeners = /* @__PURE__ */ new Set();
23
+ channel.on("broadcast", { event: "change" }, () => {
24
+ for (const notify of listeners) notify();
25
+ });
26
+ let joined = false;
27
+ const ready = (async () => {
28
+ await client.realtime.setAuth();
29
+ await new Promise((resolve, reject) => {
30
+ channel.subscribe((status, error) => {
31
+ switch (status) {
32
+ case REALTIME_SUBSCRIBE_STATES.SUBSCRIBED:
33
+ if (joined) for (const notify of listeners) notify();
34
+ joined = true;
35
+ resolve();
36
+ return;
37
+ case REALTIME_SUBSCRIBE_STATES.CLOSED:
38
+ resolve();
39
+ return;
40
+ case REALTIME_SUBSCRIBE_STATES.CHANNEL_ERROR:
41
+ case REALTIME_SUBSCRIBE_STATES.TIMED_OUT:
42
+ reject(error ?? /* @__PURE__ */ new Error(`Realtime ${status.toLowerCase()} on ${topic}`));
43
+ return;
44
+ default: reject(/* @__PURE__ */ new Error(`Unknown realtime status ${String(status)}`));
45
+ }
46
+ });
47
+ });
48
+ })();
49
+ ready.catch(() => void 0);
50
+ shared = {
51
+ channel,
52
+ listeners,
53
+ ready
54
+ };
55
+ byTopic.set(topic, shared);
56
+ }
57
+ const current = shared;
58
+ current.listeners.add(listener);
59
+ let left = false;
60
+ return {
61
+ ready: current.ready,
62
+ leave: async () => {
63
+ if (left) return;
64
+ left = true;
65
+ current.listeners.delete(listener);
66
+ if (current.listeners.size > 0) return;
67
+ byTopic.delete(topic);
68
+ await client.removeChannel(current.channel);
69
+ }
70
+ };
71
+ }
72
+ /**
73
+ * Calls `onChange` when a table the spec reads changes, so you can refetch.
74
+ * Needs the `realtime-tables` SQL module and the tables in
75
+ * `realtime.tables`. Live queries on the same client share one channel per
76
+ * table, so any number of them can run at once.
77
+ *
78
+ * ```ts
79
+ * using live = liveQuery(sb, supabase, spec, { onChange: () => refetch() });
80
+ * ```
81
+ */
82
+ function liveQuery(sb, client, spec, options) {
83
+ const touched = Array.isArray(spec) ? spec : sb.tablesOf(spec);
84
+ const tables = touched.filter((table) => sb.meta.realtime?.[table]);
85
+ const unwatched = touched.filter((table) => !sb.meta.realtime?.[table]);
86
+ const debounceMs = options.debounceMs ?? 100;
87
+ const changed = /* @__PURE__ */ new Set();
88
+ let timer;
89
+ let closed = false;
90
+ const flush = () => {
91
+ timer = void 0;
92
+ if (closed || changed.size === 0) return;
93
+ const batch = [...changed];
94
+ changed.clear();
95
+ options.onChange(batch);
96
+ };
97
+ const memberships = tables.map((table) => join(client, liveTopic(sb.meta, table, options.tenant), () => {
98
+ changed.add(table);
99
+ if (timer !== void 0) clearTimeout(timer);
100
+ timer = setTimeout(flush, debounceMs);
101
+ }));
102
+ options.onStatus?.(tables.length === 0 ? "closed" : "joining");
103
+ const ready = Promise.all(memberships.map((member) => member.ready)).then(() => {
104
+ if (!closed && tables.length > 0) options.onStatus?.("subscribed");
105
+ }, (cause) => {
106
+ const error = cause instanceof Error ? cause : new Error(String(cause));
107
+ options.onStatus?.("error", error);
108
+ throw error;
109
+ });
110
+ ready.catch(() => void 0);
111
+ const unsubscribe = async () => {
112
+ if (closed) return;
113
+ closed = true;
114
+ if (timer !== void 0) clearTimeout(timer);
115
+ await Promise.all(memberships.map((member) => member.leave()));
116
+ options.onStatus?.("closed");
117
+ };
118
+ return {
119
+ tables,
120
+ unwatched,
121
+ ready,
122
+ unsubscribe,
123
+ [Symbol.dispose]: () => void unsubscribe(),
124
+ [Symbol.asyncDispose]: unsubscribe
125
+ };
126
+ }
127
+ /**
128
+ * Keeps a count current: runs the `count` spec (a HEAD request) now, after
129
+ * each debounced change to a table it reads, and after the channel rejoins.
130
+ * Responses that arrive out of order are dropped.
131
+ *
132
+ * ```ts
133
+ * using live = liveCount(sb, supabase, db, sb.spec.notes.count(), {
134
+ * onCount: (count) => render(count),
135
+ * });
136
+ * ```
137
+ */
138
+ function liveCount(sb, client, db, spec, options) {
139
+ let latest = 0;
140
+ let closed = false;
141
+ const refetch = () => {
142
+ const call = ++latest;
143
+ Promise.resolve(db.$run(spec)).then((result) => {
144
+ if (closed || call !== latest) return;
145
+ if (result.ok) options.onCount(result.data);
146
+ else options.onError?.(result.error);
147
+ });
148
+ };
149
+ const live = liveQuery(sb, client, spec, {
150
+ onChange: refetch,
151
+ ...options.tenant === void 0 ? {} : { tenant: options.tenant },
152
+ ...options.debounceMs === void 0 ? {} : { debounceMs: options.debounceMs },
153
+ ...options.onStatus ? { onStatus: options.onStatus } : {}
154
+ });
155
+ if (options.immediate ?? true) refetch();
156
+ const unsubscribe = async () => {
157
+ closed = true;
158
+ await live.unsubscribe();
159
+ };
160
+ return {
161
+ ...live,
162
+ unsubscribe,
163
+ [Symbol.dispose]: () => void unsubscribe(),
164
+ [Symbol.asyncDispose]: unsubscribe
165
+ };
166
+ }
167
+ //#endregion
168
+ export { liveQuery as n, liveTopic as r, liveCount as t };
@@ -0,0 +1,105 @@
1
+ import { k as TableKey, n as AnyModels, t as AnyFunctions } from "../types-C2fslP1z.js";
2
+ import { t as BetterSupabase } from "../define-CTfWw-SS.js";
3
+ import { d as ServerContext, f as ServerOptions, l as BetterServer, n as GuardOptions } from "../respond-PoTpy5DT.js";
4
+ import { i as ResourceRouteOptions } from "../resource-SkPTRJDD.js";
5
+ import { StandardSchemaV1 } from "@standard-schema/spec";
6
+ //#region src/mcp/index.d.ts
7
+ export declare const MCP_PROTOCOL_VERSION: string;
8
+ type Json = Readonly<Record<string, unknown>>;
9
+ /** MCP tool annotations (hints for clients, not guarantees). */
10
+ export interface ToolAnnotations {
11
+ readonly title?: string;
12
+ readonly readOnlyHint?: boolean;
13
+ readonly destructiveHint?: boolean;
14
+ readonly idempotentHint?: boolean;
15
+ readonly openWorldHint?: boolean;
16
+ }
17
+ /** A tool as listed by `tools/list`. */
18
+ export interface ToolInfo {
19
+ readonly name: string;
20
+ readonly title?: string;
21
+ readonly description?: string;
22
+ readonly inputSchema: Json;
23
+ readonly outputSchema?: Json;
24
+ readonly annotations?: ToolAnnotations;
25
+ }
26
+ export interface ToolContext<M extends AnyModels, F extends AnyFunctions, E> extends ServerContext<M, F, E> {
27
+ readonly request: Request;
28
+ readonly signal: AbortSignal;
29
+ }
30
+ export interface McpTool<M extends AnyModels, F extends AnyFunctions, E> {
31
+ readonly info: ToolInfo;
32
+ /** Validates arguments; its output is what `run` receives. */
33
+ readonly input?: StandardSchemaV1;
34
+ run(args: never, ctx: ToolContext<M, F, E>): unknown;
35
+ }
36
+ export interface ToolDefinition<M extends AnyModels, F extends AnyFunctions, E, I> {
37
+ readonly name: string;
38
+ readonly title?: string;
39
+ readonly description: string;
40
+ /**
41
+ * Validator for the arguments. Its JSON Schema comes from Standard JSON
42
+ * Schema (zod 4, arktype, valibot) unless `inputSchema` is given.
43
+ */
44
+ readonly input?: StandardSchemaV1<unknown, I>;
45
+ readonly inputSchema?: Json;
46
+ readonly outputSchema?: Json;
47
+ readonly annotations?: ToolAnnotations;
48
+ run(args: I, ctx: ToolContext<M, F, E>): unknown;
49
+ }
50
+ export type ToolResources<M extends AnyModels> = { readonly [T in TableKey<M>]?: ResourceRouteOptions<M, T> | true; };
51
+ export interface McpOptions<M extends AnyModels, F extends AnyFunctions, E> extends ServerOptions, GuardOptions {
52
+ readonly name: string;
53
+ readonly version: string;
54
+ readonly title?: string;
55
+ /** Sent to clients on `initialize`, e.g. how the tools fit together. */
56
+ readonly instructions?: string;
57
+ /** Tables exposed as tools: `<table>_list`, `_get`, `_create`, `_update`, `_delete`. */
58
+ readonly resources?: ToolResources<M>;
59
+ readonly tools?: readonly McpTool<M, F, E>[];
60
+ /**
61
+ * Canonical URL of this MCP server (RFC 9728 `resource`). Defaults to the
62
+ * request URL without query.
63
+ */
64
+ readonly resource?: string | ((request: Request) => string);
65
+ /** Defaults to Supabase Auth of `env.url` (`<url>/auth/v1`). */
66
+ readonly authorizationServers?: readonly string[];
67
+ /** Origins allowed to call the server (DNS rebinding protection). Defaults to any. */
68
+ readonly allowedOrigins?: readonly string[];
69
+ /** Custom jsonb types, as in `createOpenApi`. */
70
+ readonly json?: Readonly<Record<string, unknown>>;
71
+ /** Include internal error messages in tool results. Defaults to `NODE_ENV === 'development'`. */
72
+ readonly exposeErrors?: boolean;
73
+ }
74
+ export interface BetterMcp<M extends AnyModels, F extends AnyFunctions, E> extends BetterServer<M, F, E> {
75
+ /** Everything in one handler: the MCP endpoint and `/.well-known/oauth-protected-resource`. */
76
+ readonly fetch: (request: Request) => Promise<Response>;
77
+ /** The Streamable HTTP endpoint (POST JSON-RPC). */
78
+ readonly handler: (request: Request) => Promise<Response>;
79
+ /** RFC 9728 protected resource metadata. */
80
+ metadata(request: Request): Response;
81
+ readonly tools: readonly ToolInfo[];
82
+ /** Adds a tool typed against this app's repositories. */
83
+ tool<I = Record<string, unknown>>(definition: ToolDefinition<M, F, E, I>): this;
84
+ /** Calls a tool directly, e.g. from tests or another transport. */
85
+ call(name: string, args: unknown, ctx: ToolContext<M, F, E>): Promise<ToolResult>;
86
+ }
87
+ export interface ToolResult {
88
+ readonly content: readonly {
89
+ readonly type: "text";
90
+ readonly text: string;
91
+ }[];
92
+ readonly structuredContent?: Json;
93
+ readonly isError?: boolean;
94
+ }
95
+ /** A custom tool. Pass it in `createMcp({ tools: [...] })`. */
96
+ export declare function defineTool<M extends AnyModels, F extends AnyFunctions, E, I = Record<string, unknown>>(definition: ToolDefinition<M, F, E, I>): McpTool<M, F, E>;
97
+ /**
98
+ * An MCP server (Streamable HTTP, stateless) whose tools run as the caller:
99
+ * the Bearer token from the MCP client is verified and every query goes
100
+ * through RLS. Answers 401 with RFC 9728 metadata so clients can sign in with
101
+ * Supabase Auth's OAuth server.
102
+ */
103
+ export declare function createMcp<M extends AnyModels, D, F extends AnyFunctions, E>(sb: BetterSupabase<M, D, F, E>, options: McpOptions<M, F, E>): BetterMcp<M, F, E>;
104
+ //#endregion
105
+ export type { GuardOptions, ResourceRouteOptions };
@@ -0,0 +1,330 @@
1
+ import { n as dbError } from "../errors-Bl3rkOzo.js";
2
+ import { t as validate } from "../standard-FUi3Z3-7.js";
3
+ import { o as toProblem } from "../problem-DPu6Fh-t.js";
4
+ import { t as SPEC_PINS } from "../spec-pins-t6sPaHvl.js";
5
+ import { t as buildJsonSchema } from "../json-schema-BbwO2mZg.js";
6
+ import { a as settle, c as withExtra, n as guard, o as createServer, s as extendServer, t as defaultExpose } from "../respond-4-dhoV7x.js";
7
+ import { t as defineResource } from "../resource-CYiu6Ygi.js";
8
+ import { fromSupabaseUrl, resourceMetadataResponse, unauthorizedResponse } from "@supabase/server/oauth-protected-resource";
9
+ //#region src/mcp/index.ts
10
+ const MCP_PROTOCOL_VERSION = SPEC_PINS.mcp;
11
+ const WELL_KNOWN = "/.well-known/oauth-protected-resource";
12
+ const SUPPORTED_VERSIONS = /* @__PURE__ */ new Set([MCP_PROTOCOL_VERSION, "2025-03-26"]);
13
+ /** A custom tool. Pass it in `createMcp({ tools: [...] })`. */
14
+ function defineTool(definition) {
15
+ if (!/^[A-Za-z0-9_.-]{1,64}$/.test(definition.name)) throw new TypeError(`Invalid tool name "${definition.name}": use 1-64 letters, digits, "_", "-" or "."`);
16
+ const standard = definition.input?.["~standard"];
17
+ const inputSchema = definition.inputSchema ?? standard?.jsonSchema?.input({ target: "draft-2020-12" }) ?? (definition.input ? void 0 : {
18
+ type: "object",
19
+ properties: {}
20
+ });
21
+ if (!inputSchema) throw new TypeError(`Tool "${definition.name}": the input schema has no JSON Schema; pass inputSchema`);
22
+ const { $schema: _, ...schema } = inputSchema;
23
+ return {
24
+ info: {
25
+ name: definition.name,
26
+ ...definition.title ? { title: definition.title } : {},
27
+ description: definition.description,
28
+ inputSchema: schema,
29
+ ...definition.outputSchema ? { outputSchema: definition.outputSchema } : {},
30
+ ...definition.annotations ? { annotations: definition.annotations } : {}
31
+ },
32
+ ...definition.input ? { input: definition.input } : {},
33
+ run: (args, ctx) => definition.run(args, ctx)
34
+ };
35
+ }
36
+ const ANNOTATIONS = {
37
+ list: {
38
+ readOnlyHint: true,
39
+ openWorldHint: false
40
+ },
41
+ get: {
42
+ readOnlyHint: true,
43
+ openWorldHint: false
44
+ },
45
+ create: {
46
+ readOnlyHint: false,
47
+ destructiveHint: false,
48
+ openWorldHint: false
49
+ },
50
+ update: {
51
+ readOnlyHint: false,
52
+ destructiveHint: true,
53
+ idempotentHint: true,
54
+ openWorldHint: false
55
+ },
56
+ delete: {
57
+ readOnlyHint: false,
58
+ destructiveHint: true,
59
+ idempotentHint: true,
60
+ openWorldHint: false
61
+ }
62
+ };
63
+ function pageSchema(max) {
64
+ return {
65
+ type: "object",
66
+ properties: {
67
+ page: {
68
+ type: "integer",
69
+ minimum: 1,
70
+ default: 1
71
+ },
72
+ size: {
73
+ type: "integer",
74
+ minimum: 1,
75
+ maximum: max,
76
+ default: 50
77
+ }
78
+ },
79
+ additionalProperties: false
80
+ };
81
+ }
82
+ function tableTools(resource, defs) {
83
+ const { table, keyParam } = resource;
84
+ const row = defs[`${table}Row`] ?? { type: "object" };
85
+ const key = keyParam ? row["properties"]?.[keyParam] ?? { type: "string" } : void 0;
86
+ const keyInput = (more = {}) => ({
87
+ type: "object",
88
+ properties: {
89
+ [keyParam]: key,
90
+ ...more
91
+ },
92
+ required: [keyParam, ...Object.keys(more)],
93
+ additionalProperties: false
94
+ });
95
+ const page = {
96
+ type: "object",
97
+ properties: {
98
+ items: {
99
+ type: "array",
100
+ items: row
101
+ },
102
+ page: { type: "object" }
103
+ },
104
+ required: ["items", "page"]
105
+ };
106
+ const tools = [];
107
+ const add = (operation, description, inputSchema, outputSchema) => {
108
+ tools.push({
109
+ operation,
110
+ info: {
111
+ name: `${table}_${operation}`,
112
+ title: `${operation[0].toUpperCase()}${operation.slice(1)} ${table}`,
113
+ description,
114
+ inputSchema,
115
+ ...outputSchema ? { outputSchema } : {},
116
+ annotations: ANNOTATIONS[operation]
117
+ }
118
+ });
119
+ };
120
+ for (const operation of resource.operations) switch (operation) {
121
+ case "list": {
122
+ const { $schema: _, ...listSchema } = resource.list?.jsonSchema ?? pageSchema(resource.maxPageSize);
123
+ add("list", `List ${table} rows visible to the caller, one page at a time.`, listSchema, page);
124
+ break;
125
+ }
126
+ case "get":
127
+ if (keyParam) add("get", `Get one ${table} row by ${keyParam}.`, keyInput(), row);
128
+ break;
129
+ case "create":
130
+ add("create", `Create a ${table} row. Returns the created row.`, defs[`${table}Insert`] ?? { type: "object" }, row);
131
+ break;
132
+ case "update":
133
+ if (keyParam) add("update", `Update the columns in "patch" on one ${table} row.`, keyInput({ patch: defs[`${table}Update`] ?? { type: "object" } }), row);
134
+ break;
135
+ case "delete":
136
+ if (keyParam) add("delete", `Delete one ${table} row by ${keyParam}.`, keyInput());
137
+ break;
138
+ default: throw new TypeError(`Unknown operation ${String(operation)}`);
139
+ }
140
+ return tools;
141
+ }
142
+ const PARSE_ERROR = -32700;
143
+ const INVALID_REQUEST = -32600;
144
+ const METHOD_NOT_FOUND = -32601;
145
+ const INVALID_PARAMS = -32602;
146
+ function rpcError(id, code, message, status = 200) {
147
+ return Response.json({
148
+ jsonrpc: "2.0",
149
+ id: id ?? null,
150
+ error: {
151
+ code,
152
+ message
153
+ }
154
+ }, { status });
155
+ }
156
+ function isRpcRequest(value) {
157
+ return typeof value === "object" && value !== null && value["jsonrpc"] === "2.0" && typeof value["method"] === "string";
158
+ }
159
+ function textResult(value, isError = false) {
160
+ const structured = typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
161
+ return {
162
+ content: [{
163
+ type: "text",
164
+ text: typeof value === "string" ? value : JSON.stringify(value ?? null)
165
+ }],
166
+ ...structured ? { structuredContent: structured } : {},
167
+ ...isError ? { isError: true } : {}
168
+ };
169
+ }
170
+ function canonical(request) {
171
+ const url = new URL(request.url);
172
+ return `${url.origin}${url.pathname.replace(/\/$/, "")}`;
173
+ }
174
+ /**
175
+ * An MCP server (Streamable HTTP, stateless) whose tools run as the caller:
176
+ * the Bearer token from the MCP client is verified and every query goes
177
+ * through RLS. Answers 401 with RFC 9728 metadata so clients can sign in with
178
+ * Supabase Auth's OAuth server.
179
+ */
180
+ function createMcp(sb, options) {
181
+ const server = createServer(sb, options);
182
+ const expose = options.exposeErrors ?? defaultExpose();
183
+ const defs = buildJsonSchema({
184
+ meta: sb.meta,
185
+ config: { json: options.json ?? {} }
186
+ })["$defs"];
187
+ const registry = /* @__PURE__ */ new Map();
188
+ const register = (info, call) => {
189
+ if (registry.has(info.name)) throw new TypeError(`Duplicate MCP tool "${info.name}"`);
190
+ registry.set(info.name, {
191
+ info,
192
+ call
193
+ });
194
+ };
195
+ const failure = (error) => textResult(toProblem(error, { expose }), true);
196
+ for (const [table, raw] of Object.entries(options.resources ?? {})) {
197
+ if (!raw) continue;
198
+ const resource = defineResource(sb, table, raw === true ? {} : raw);
199
+ for (const { info, operation } of tableTools(resource, defs)) register(info, async (args, ctx) => {
200
+ const input = args ?? {};
201
+ const id = resource.keyParam ? input[resource.keyParam] : void 0;
202
+ const result = await resource.execute(ctx.db, operation, {
203
+ id,
204
+ query: input,
205
+ data: operation === "update" ? input["patch"] : input
206
+ });
207
+ if (!result.ok) return failure(result.error);
208
+ return textResult(operation === "delete" ? { deleted: true } : result.data);
209
+ });
210
+ }
211
+ const addTool = (tool) => {
212
+ register(tool.info, async (args, ctx) => {
213
+ let value = args ?? {};
214
+ if (tool.input) {
215
+ const parsed = await validate(tool.input, value, "arguments");
216
+ if (!parsed.ok) return failure(parsed.error);
217
+ value = parsed.data;
218
+ }
219
+ const outcome = await settle(() => tool.run(value, ctx));
220
+ return outcome.ok ? textResult(outcome.data) : failure(outcome.error);
221
+ });
222
+ };
223
+ for (const tool of options.tools ?? []) addTool(tool);
224
+ const listTools = () => [...registry.values()].map((entry) => entry.info);
225
+ const authorizationServers = () => options.authorizationServers ?? [fromSupabaseUrl(server.env.url)];
226
+ const resourceOf = (request) => {
227
+ if (typeof options.resource === "function") return options.resource(request);
228
+ if (options.resource) return options.resource;
229
+ const url = new URL(canonical(request));
230
+ return url.pathname.startsWith(WELL_KNOWN) ? `${url.origin}${url.pathname.slice(37)}` : url.href.replace(/\/$/, "");
231
+ };
232
+ const metadataUrl = (request) => {
233
+ const resource = new URL(resourceOf(request));
234
+ return `${resource.origin}${WELL_KNOWN}${resource.pathname.replace(/\/$/, "")}`;
235
+ };
236
+ const call = async (name, args, ctx) => {
237
+ const entry = registry.get(name);
238
+ if (!entry) return failure(dbError("not_found", `Unknown tool "${name}"`));
239
+ try {
240
+ return await entry.call(args, ctx);
241
+ } catch (cause) {
242
+ return failure(dbError("unexpected", expose && cause instanceof Error ? cause.message : "The tool failed"));
243
+ }
244
+ };
245
+ const handler = async (request) => {
246
+ const origin = request.headers.get("origin");
247
+ if (origin && options.allowedOrigins && !options.allowedOrigins.includes(origin)) return rpcError(null, INVALID_REQUEST, "Origin not allowed", 403);
248
+ if (request.method !== "POST") return new Response(null, {
249
+ status: 405,
250
+ headers: { allow: "POST" }
251
+ });
252
+ const version = request.headers.get("mcp-protocol-version");
253
+ if (version && !SUPPORTED_VERSIONS.has(version)) return rpcError(null, INVALID_REQUEST, `Unsupported MCP-Protocol-Version ${version}`, 400);
254
+ const ctx = await server.context(request);
255
+ const denied = guard(ctx.auth, options.allow, options.aal);
256
+ if (denied) {
257
+ if (denied.kind === "unauthorized") return unauthorizedResponse(request, { resourceMetadataUrl: metadataUrl(request) });
258
+ return rpcError(null, INVALID_REQUEST, denied.message, 403);
259
+ }
260
+ let message;
261
+ try {
262
+ message = await request.json();
263
+ } catch {
264
+ return rpcError(null, PARSE_ERROR, "Parse error", 400);
265
+ }
266
+ if (!isRpcRequest(message)) return rpcError(null, INVALID_REQUEST, "Expected one JSON-RPC 2.0 message", 400);
267
+ if (message.id === void 0) return new Response(null, { status: 202 });
268
+ const reply = (result) => Response.json({
269
+ jsonrpc: "2.0",
270
+ id: message.id,
271
+ result
272
+ });
273
+ switch (message.method) {
274
+ case "initialize": {
275
+ const requested = message.params?.["protocolVersion"];
276
+ return reply({
277
+ protocolVersion: typeof requested === "string" && SUPPORTED_VERSIONS.has(requested) ? requested : MCP_PROTOCOL_VERSION,
278
+ capabilities: { tools: { listChanged: false } },
279
+ serverInfo: {
280
+ name: options.name,
281
+ version: options.version,
282
+ ...options.title ? { title: options.title } : {}
283
+ },
284
+ ...options.instructions ? { instructions: options.instructions } : {}
285
+ });
286
+ }
287
+ case "ping": return reply({});
288
+ case "tools/list": return reply({ tools: listTools() });
289
+ case "tools/call": {
290
+ const name = message.params?.["name"];
291
+ if (typeof name !== "string" || !registry.has(name)) return rpcError(message.id, INVALID_PARAMS, `Unknown tool ${JSON.stringify(name)}`);
292
+ return reply(await call(name, message.params?.["arguments"], withExtra(ctx, {
293
+ request,
294
+ signal: request.signal
295
+ })));
296
+ }
297
+ default: return rpcError(message.id, METHOD_NOT_FOUND, `Method not found: ${message.method}`);
298
+ }
299
+ };
300
+ const metadata = (request) => resourceMetadataResponse(request, {
301
+ resource: resourceOf(request),
302
+ authorizationServers: [...authorizationServers()]
303
+ });
304
+ const mcp = extendServer(server, {
305
+ get tools() {
306
+ return listTools();
307
+ },
308
+ tool(definition) {
309
+ addTool(defineTool(definition));
310
+ return mcp;
311
+ },
312
+ call,
313
+ handler,
314
+ metadata,
315
+ fetch(request) {
316
+ const { pathname } = new URL(request.url);
317
+ if (pathname.startsWith(WELL_KNOWN)) return Promise.resolve(request.method === "OPTIONS" ? new Response(null, {
318
+ status: 204,
319
+ headers: {
320
+ "access-control-allow-origin": "*",
321
+ "access-control-allow-methods": "GET"
322
+ }
323
+ }) : metadata(request));
324
+ return handler(request);
325
+ }
326
+ });
327
+ return mcp;
328
+ }
329
+ //#endregion
330
+ export { MCP_PROTOCOL_VERSION, createMcp, defineTool };