mirror of
https://github.com/supabase/supabase.git
synced 2026-10-05 09:25:06 +03:00
## I have read the [CONTRIBUTING.md](https://github.com/supabase/supabase/blob/master/CONTRIBUTING.md) file. YES ## What kind of change does this PR introduce? Feature — a new UI Library block. Stacked on #49573 (already in main) Fixes AI-1064 ## What is the new behavior? Adds `headless-app-tanstack`: customers sign in, authorize an MCP client, and use the product through agent tool calls. It composes the existing Password-Based Auth, OAuth Consent, and MCP Server blocks. - `/agents` provides a copyable connection prompt, lists OAuth authorizations, and lets customers revoke access. - The shared MCP runtime exposes `whoami` plus example task CRUD tools. Tools use the caller's Supabase client, with database grants and RLS enforcing ownership. - A root-level `supabase/` directory supplies local Auth/OAuth configuration, a declarative tasks schema, and Edge Function files, including `.env.example`. - Docs cover local setup, signing keys, migrations, environment configuration, deployment, and extending the tools. `/example/headless-app` previews the sign-in, consent, connect, and connected states. Shared block fixes make a fresh install work: - Explicit public URL resolution fixes OAuth discovery in local Edge Runtime when middleware runtime detection fails. Both external OAuth access tokens and ordinary authenticated app session tokens remain supported; embedded agents do not need an additional consent flow. - Registry targets keep backend files outside `src/`, and generated consumer routes omit source-only TypeScript suppressions. - Signup respects `auth.email.enable_confirmations`; sign-in/signup preserve the return destination. Missing consent IDs retain the existing error state without serializing `null` into the URL. ## How to test Use the UI Library on **staging** and follow the block pages' instructions. 1. Open the **Headless App** block page for TanStack Start. Install it into a fresh app and follow the setup instructions through connecting an MCP client. 2. Sign up, open `/agents`, and use the connection prompt to authorize a client. Call `whoami`, then create, list, update, and delete a task. 3. Confirm the client appears on `/agents`. Revoke access and verify it disappears and token refresh fails. An existing access token can continue working until it expires. 4. Follow the **MCP Server** block page's embedded-agent instructions using an authenticated app session. Confirm tools work without another OAuth consent flow and `whoami` returns `client_id: null`. 5. With a second user, confirm each user can only access their own tasks. Check that signup behaves correctly for the configured email-confirmation setting. 6. Check the Headless App preview states and run the installed app's typecheck and production build. ## Validation performed Fresh local installation and browser/SDK verification passed: 26 live MCP/Data API checks, 10 Deno tests, and 7 connection-page component tests. Also passed UI Library typecheck, targeted lint, registry/Markdown builds, and fresh consumer typecheck/production build. Both OAuth and ordinary app session authentication were exercised. Hosted deployment and consuming the confirmation-email link were not tested. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - **New Features** - Added a TanStack Headless App example with sign-in, OAuth consent, MCP connection, and connected-agent screens. - Added task management tools for listing, creating, updating, and deleting tasks through MCP. - Added connected-agent management, including server URL and prompt copying, refresh, and access revocation. - Added a new Headless App registry block and documentation. - **Bug Fixes** - Preserved intended destinations through sign-up, email confirmation, and protected-route login redirects. - Improved OAuth discovery URL handling across forwarded-host deployments. - **Documentation** - Updated setup, environment, deployment, and Supabase CLI guidance for headless apps and MCP servers. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: Saxon Fletcher <SaxonF@users.noreply.github.com> Co-authored-by: Claude Opus 5 <noreply@anthropic.com> Co-authored-by: repro <repro@local> Co-authored-by: Raúl Barroso <code@raulb.dev>
116 lines
45 KiB
JSON
116 lines
45 KiB
JSON
{
|
||
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
|
||
"name": "headless-app-tanstack",
|
||
"type": "registry:block",
|
||
"title": "Headless App for TanStack Start",
|
||
"description": "A backend with an agent as the primary interface, combining auth, OAuth consent, and an MCP server.",
|
||
"dependencies": [
|
||
"@supabase/ssr@latest",
|
||
"@supabase/supabase-js@latest"
|
||
],
|
||
"registryDependencies": [
|
||
"button",
|
||
"https://supabase.com/library/r/password-based-auth-tanstack.json",
|
||
"https://supabase.com/library/r/oauth-consent-tanstack.json"
|
||
],
|
||
"files": [
|
||
{
|
||
"path": "registry/default/blocks/headless-app-tanstack/supabase/functions/mcp-server/tools/index.ts",
|
||
"content": "import type { McpServer } from 'npm:@modelcontextprotocol/server@2.0.0'\n\nimport { registerTaskTools } from './tasks.ts'\nimport type { ToolContext } from './types.ts'\nimport { registerWhoamiTool } from './whoami.ts'\n\nexport type { ToolContext } from './types.ts'\n\n// Add your product's tool modules here. The shared MCP runtime supplies the\n// authenticated context for each request.\nexport function registerTools(server: McpServer, context: ToolContext): void {\n registerWhoamiTool(server, context)\n registerTaskTools(server, context)\n}\n",
|
||
"type": "registry:file",
|
||
"target": "~/supabase/functions/mcp-server/tools/index.ts"
|
||
},
|
||
{
|
||
"path": "registry/default/blocks/headless-app-tanstack/supabase/functions/mcp-server/tools/tasks.ts",
|
||
"content": "import type { McpServer } from 'npm:@modelcontextprotocol/server@2.0.0'\nimport { z } from 'npm:zod@4.4.3'\n\nimport { errorResult, jsonResult, runtimeErrorResult } from './result.ts'\nimport type { ToolContext } from './types.ts'\n\nconst taskFields = 'id, title, closed, created_at'\nconst taskId = z.uuid().describe('The task ID returned by list_tasks or create_task.')\nconst taskTitle = z\n .string()\n .trim()\n .min(1)\n .max(200)\n .describe('A task title, 1–200 characters after trimming surrounding whitespace.')\nconst taskNotFound = 'Task not found or you do not have access.'\n\n// Use only the caller's client. Ownership comes from auth.uid() in the schema,\n// and RLS applies to reads and writes, including queries by a supplied task ID.\nexport function registerTaskTools(server: McpServer, { supabase }: ToolContext): void {\n server.registerTool(\n 'list_tasks',\n {\n description:\n 'List your tasks, newest first. Optionally filter by closed status. Pass next_offset as offset to fetch another page; null means there are no more tasks.',\n inputSchema: z.strictObject({\n closed: z.boolean().optional().describe('False for open tasks, true for closed tasks.'),\n limit: z.int().min(1).max(100).default(20).describe('Maximum tasks per page (1–100).'),\n offset: z.int().min(0).default(0).describe('Number of tasks to skip.'),\n }),\n annotations: {\n readOnlyHint: true,\n destructiveHint: false,\n idempotentHint: true,\n openWorldHint: false,\n },\n },\n async ({ closed, limit, offset }) => {\n try {\n let query = supabase\n .from('tasks')\n .select(taskFields)\n .order('created_at', { ascending: false })\n .order('id', { ascending: false })\n .range(offset, offset + limit)\n\n if (closed !== undefined) query = query.eq('closed', closed)\n\n const { data } = await query.throwOnError()\n return jsonResult({\n tasks: data.slice(0, limit),\n next_offset: data.length > limit ? offset + limit : null,\n })\n } catch (error) {\n return runtimeErrorResult(error)\n }\n }\n )\n\n server.registerTool(\n 'create_task',\n {\n description:\n 'Create an open task for yourself and return it. Each call creates a new task; do not retry blindly after a connection failure.',\n inputSchema: z.strictObject({ title: taskTitle }),\n annotations: {\n readOnlyHint: false,\n destructiveHint: false,\n idempotentHint: false,\n openWorldHint: false,\n },\n },\n async ({ title }) => {\n try {\n const { data } = await supabase\n .from('tasks')\n .insert({ title })\n .select(taskFields)\n .single()\n .throwOnError()\n\n return jsonResult({ task: data })\n } catch (error) {\n return runtimeErrorResult(error)\n }\n }\n )\n\n server.registerTool(\n 'update_task',\n {\n description:\n 'Rename, close, or reopen one of your tasks and return it. Supply title, closed, or both. Fields you omit keep their current values.',\n inputSchema: z\n .strictObject({\n id: taskId,\n title: taskTitle.optional(),\n closed: z.boolean().optional().describe('True to close the task; false to reopen it.'),\n })\n .refine(({ title, closed }) => title !== undefined || closed !== undefined, {\n message: 'Supply title or closed to update a task.',\n }),\n annotations: {\n readOnlyHint: false,\n destructiveHint: true,\n idempotentHint: true,\n openWorldHint: false,\n },\n },\n async ({ id, title, closed }) => {\n try {\n const changes = {\n ...(title !== undefined ? { title } : {}),\n ...(closed !== undefined ? { closed } : {}),\n }\n const { data } = await supabase\n .from('tasks')\n .update(changes)\n .eq('id', id)\n .select(taskFields)\n .maybeSingle()\n .throwOnError()\n\n return data ? jsonResult({ task: data }) : errorResult(taskNotFound)\n } catch (error) {\n return runtimeErrorResult(error)\n }\n }\n )\n\n server.registerTool(\n 'delete_task',\n {\n description:\n 'Permanently delete one of your tasks by ID. Use update_task with closed: true to keep a completed task instead.',\n inputSchema: z.strictObject({ id: taskId }),\n annotations: {\n readOnlyHint: false,\n destructiveHint: true,\n idempotentHint: true,\n openWorldHint: false,\n },\n },\n async ({ id }) => {\n try {\n const { data } = await supabase\n .from('tasks')\n .delete()\n .eq('id', id)\n .select('id')\n .maybeSingle()\n .throwOnError()\n\n return data ? jsonResult({ deleted: true, id: data.id }) : errorResult(taskNotFound)\n } catch (error) {\n return runtimeErrorResult(error)\n }\n }\n )\n}\n",
|
||
"type": "registry:file",
|
||
"target": "~/supabase/functions/mcp-server/tools/tasks.ts"
|
||
},
|
||
{
|
||
"path": "registry/default/blocks/headless-app-tanstack/routes/_protected/agents.tsx",
|
||
"content": "/// <reference types=\"vite/types/importMeta.d.ts\" />\nimport { createFileRoute } from '@tanstack/react-router'\n\nimport { ConnectedAgents } from '@/registry/default/blocks/headless-app-tanstack/components/connected-agents'\n\nconst PRODUCT_NAME = import.meta.env.VITE_PRODUCT_NAME?.trim() || 'Your product'\nconst MCP_SERVER_URL = `${import.meta.env.VITE_SUPABASE_URL}/functions/v1/mcp-server`\n\n// Nested under _protected, so the layout route redirects signed-out visitors\n// before this renders.\nexport const Route = createFileRoute('/_protected/agents')({\n component: AgentsPage,\n})\n\nfunction AgentsPage() {\n return (\n <main className=\"flex min-h-svh items-center justify-center p-6 md:p-10\">\n <ConnectedAgents\n className=\"w-full max-w-lg\"\n mcpServerUrl={MCP_SERVER_URL}\n productName={PRODUCT_NAME}\n />\n </main>\n )\n}\n",
|
||
"type": "registry:file",
|
||
"target": "routes/_protected/agents.tsx"
|
||
},
|
||
{
|
||
"path": "registry/default/blocks/headless-app-tanstack/components/connected-agents.tsx",
|
||
"content": "import { Check, Copy } from 'lucide-react'\nimport { useEffect, useState } from 'react'\n\nimport { cn } from '@/lib/utils'\nimport {\n useOAuthGrants,\n type OAuthGrant,\n} from '@/registry/default/blocks/headless-app-tanstack/hooks/use-oauth-grants'\nimport { Button } from '@/registry/default/components/ui/button'\n\nconst buildPrompt = (productName: string, mcpServerUrl: string) =>\n `Connect to ${productName} using this MCP server:\\n\\n${mcpServerUrl}\\n\\nUse your MCP connection setup to authorize access in my browser. Then call whoami to verify the connection and list the available tools.`\n\nconst formatDate = (value: string) =>\n new Date(value).toLocaleDateString(undefined, { day: 'numeric', month: 'short', year: 'numeric' })\n\nfunction ConnectAgent({\n productName,\n mcpServerUrl,\n}: {\n productName: string\n mcpServerUrl: string\n}) {\n const [copied, setCopied] = useState<'url' | 'prompt' | null>(null)\n const [error, setError] = useState<string | null>(null)\n\n useEffect(() => {\n if (!copied) return\n const timeout = window.setTimeout(() => setCopied(null), 2000)\n return () => window.clearTimeout(timeout)\n }, [copied])\n\n const copy = async (type: 'url' | 'prompt') => {\n setError(null)\n try {\n await navigator.clipboard.writeText(\n type === 'url' ? mcpServerUrl : buildPrompt(productName, mcpServerUrl)\n )\n setCopied(type)\n } catch {\n setCopied(null)\n setError('Unable to copy. Select and copy the server URL above.')\n }\n }\n\n return (\n <section className=\"flex flex-col gap-3\" aria-label=\"Connect an agent\">\n <h2 className=\"font-medium\">Connect an agent</h2>\n <p className=\"text-sm text-muted-foreground\">\n Add this server URL in your agent’s MCP settings, then sign in and approve access.\n </p>\n <div className=\"overflow-hidden rounded-lg border bg-muted\">\n <pre className=\"overflow-x-auto whitespace-pre-wrap break-all p-4 font-mono text-sm\">\n {mcpServerUrl}\n </pre>\n <div className=\"flex flex-wrap justify-end gap-2 border-t p-2\">\n <Button type=\"button\" size=\"sm\" variant=\"outline\" onClick={() => void copy('prompt')}>\n {copied === 'prompt' ? 'Prompt copied' : 'Copy prompt'}\n </Button>\n <Button type=\"button\" size=\"sm\" variant=\"outline\" onClick={() => void copy('url')}>\n {copied === 'url' ? <Check className=\"size-4\" /> : <Copy className=\"size-4\" />}\n {copied === 'url' ? 'URL copied' : 'Copy URL'}\n </Button>\n </div>\n </div>\n <p className=\"text-xs text-muted-foreground\">\n Use the prompt if your agent supports adding MCP servers through chat.\n </p>\n {/* Always mounted so screen readers announce the change. */}\n <span role=\"status\" className=\"sr-only\">\n {copied ? 'Copied to clipboard' : ''}\n </span>\n {error && (\n <p role=\"alert\" className=\"text-sm text-destructive\">\n {error}\n </p>\n )}\n </section>\n )\n}\n\nfunction GrantRow({\n grant,\n isRevoking,\n disabled,\n onRevoke,\n}: {\n grant: OAuthGrant\n isRevoking: boolean\n disabled: boolean\n onRevoke: () => void\n}) {\n return (\n <li className=\"flex items-center gap-4 p-4\">\n <div className=\"flex min-w-0 flex-1 flex-col gap-1\">\n <span className=\"truncate font-medium\">{grant.client.name}</span>\n <span className=\"truncate text-muted-foreground\">\n Authorized {formatDate(grant.granted_at)}\n {grant.scopes.length > 0 && ` · ${grant.scopes.join(', ')}`}\n </span>\n </div>\n <Button type=\"button\" size=\"sm\" variant=\"outline\" disabled={disabled} onClick={onRevoke}>\n {isRevoking ? 'Revoking access...' : 'Revoke access'}\n </Button>\n </li>\n )\n}\n\nexport interface ConnectedAgentsViewProps extends React.ComponentPropsWithoutRef<'div'> {\n mcpServerUrl: string\n productName?: string\n grants?: OAuthGrant[] | null\n isLoading?: boolean\n error?: string | null\n revokingClientId?: string | null\n onRefresh?: () => void\n onRevoke?: (clientId: string) => void\n}\n\nexport function ConnectedAgentsView({\n mcpServerUrl,\n productName = 'this app',\n grants = null,\n isLoading = false,\n error = null,\n revokingClientId = null,\n onRefresh,\n onRevoke,\n className,\n ...props\n}: ConnectedAgentsViewProps) {\n return (\n <div className={cn('flex flex-col gap-6', className)} {...props}>\n <div className=\"flex flex-col space-y-1.5\">\n <h1 className=\"text-2xl font-semibold leading-none tracking-tight\">Connected agents</h1>\n <p className=\"text-sm text-muted-foreground\">\n Authorize agents to use {productName} on your behalf.\n </p>\n </div>\n\n <ConnectAgent productName={productName} mcpServerUrl={mcpServerUrl} />\n\n <section className=\"flex flex-col gap-3\" aria-label=\"Authorized agents\">\n <div className=\"flex items-center justify-between gap-4\">\n <h2 className=\"font-medium\">Authorized agents</h2>\n {onRefresh && (\n <Button\n type=\"button\"\n size=\"sm\"\n variant=\"outline\"\n disabled={isLoading || revokingClientId !== null}\n onClick={onRefresh}\n >\n {isLoading ? 'Refreshing...' : 'Refresh'}\n </Button>\n )}\n </div>\n {/* Always mounted so screen readers announce the change. */}\n <p role=\"status\" className=\"text-sm text-muted-foreground\">\n {isLoading ? 'Loading connected agents...' : ''}\n </p>\n {grants && grants.length > 0 && (\n <ul className=\"divide-y rounded-lg border bg-muted text-sm\">\n {grants.map((grant) => (\n <GrantRow\n key={grant.client.id}\n grant={grant}\n isRevoking={revokingClientId === grant.client.id}\n disabled={isLoading || revokingClientId !== null || !onRevoke}\n onRevoke={() => onRevoke?.(grant.client.id)}\n />\n ))}\n </ul>\n )}\n {!isLoading && !error && grants?.length === 0 && (\n <p className=\"text-sm text-muted-foreground\">\n No agents authorized yet. Connect an agent using the server URL above.\n </p>\n )}\n {error && (\n <p role=\"alert\" className=\"text-sm text-destructive\">\n {error}\n </p>\n )}\n {grants && grants.length > 0 && (\n <p className=\"text-xs text-muted-foreground\">\n Revoking access prevents an agent from renewing its session. Its current access token\n may work until it expires.\n </p>\n )}\n </section>\n </div>\n )\n}\n\ninterface ConnectedAgentsProps extends React.ComponentPropsWithoutRef<'div'> {\n mcpServerUrl: string\n productName?: string\n}\n\nexport function ConnectedAgents(props: ConnectedAgentsProps) {\n const { grants, error, isLoading, revokingClientId, refresh, revoke } = useOAuthGrants()\n\n return (\n <ConnectedAgentsView\n grants={grants}\n error={error}\n isLoading={isLoading}\n revokingClientId={revokingClientId}\n onRefresh={() => void refresh()}\n onRevoke={(clientId) => void revoke(clientId)}\n {...props}\n />\n )\n}\n",
|
||
"type": "registry:component"
|
||
},
|
||
{
|
||
"path": "registry/default/blocks/headless-app-tanstack/hooks/use-oauth-grants.ts",
|
||
"content": "import type { OAuthGrant } from '@supabase/supabase-js'\nimport { useCallback, useEffect, useRef, useState } from 'react'\n\nimport { createClient } from '@/registry/default/clients/tanstack/lib/supabase/client'\n\n// Revocation invalidates refresh tokens. Already-issued access tokens can still\n// be accepted by the MCP server until they expire.\nconst useOAuthGrants = () => {\n const [grants, setGrants] = useState<OAuthGrant[] | null>(null)\n const [error, setError] = useState<string | null>(null)\n const [isLoading, setIsLoading] = useState(true)\n const [revokingClientId, setRevokingClientId] = useState<string | null>(null)\n const mounted = useRef(false)\n const requestId = useRef(0)\n const isRevoking = useRef(false)\n\n const refresh = useCallback(async () => {\n if (!mounted.current || isRevoking.current) return\n const id = ++requestId.current\n setIsLoading(true)\n setError(null)\n\n try {\n const { data, error } = await createClient().auth.oauth.listGrants()\n if (error) throw error\n if (mounted.current && id === requestId.current) setGrants(data)\n } catch (error) {\n if (mounted.current && id === requestId.current) {\n setError(\n `Unable to load connected agents. ${error instanceof Error ? error.message : 'Try refreshing the list.'}`\n )\n }\n } finally {\n if (mounted.current && id === requestId.current) setIsLoading(false)\n }\n }, [])\n\n useEffect(() => {\n mounted.current = true\n const onFocus = () => void refresh()\n void refresh()\n window.addEventListener('focus', onFocus)\n return () => {\n mounted.current = false\n window.removeEventListener('focus', onFocus)\n }\n }, [refresh])\n\n const revoke = useCallback(async (clientId: string) => {\n if (!mounted.current || isRevoking.current) return\n isRevoking.current = true\n // A list request started before revocation must not restore the removed grant.\n const id = ++requestId.current\n setIsLoading(false)\n setRevokingClientId(clientId)\n setError(null)\n\n try {\n const { error } = await createClient().auth.oauth.revokeGrant({ clientId })\n if (error) throw error\n if (mounted.current && id === requestId.current) {\n setGrants((current) => current?.filter((grant) => grant.client.id !== clientId) ?? null)\n }\n } catch (error) {\n if (mounted.current && id === requestId.current) {\n setError(\n `Unable to revoke access. ${error instanceof Error ? error.message : 'Try revoking access again.'}`\n )\n }\n } finally {\n isRevoking.current = false\n if (mounted.current && id === requestId.current) setRevokingClientId(null)\n }\n }, [])\n\n return { grants, error, isLoading, revokingClientId, refresh, revoke }\n}\n\ntype UseOAuthGrantsReturn = ReturnType<typeof useOAuthGrants>\n\nexport { useOAuthGrants, type OAuthGrant, type UseOAuthGrantsReturn }\n",
|
||
"type": "registry:hook"
|
||
},
|
||
{
|
||
"path": "registry/default/blocks/headless-app-tanstack/supabase/config.toml",
|
||
"content": "# Supabase configuration for a headless app: password auth for the product\n# session, an OAuth 2.1 server for external agents, and the MCP server that both\n# call. Values below are for local development. Set production Auth URLs and\n# enable email confirmations before running `supabase config push`.\n\nproject_id = \"headless-app\"\n\n[api]\nenabled = true\nport = 54321\nschemas = [\"public\", \"graphql_public\"]\nextra_search_path = [\"public\", \"extensions\"]\nmax_rows = 1000\n\n[db]\nport = 54322\n# Used to diff ./schemas into a migration.\nshadow_port = 54320\nmajor_version = 17\n\n# One directory per schema, then one file per object, in dependency order.\n[db.migrations]\nschema_paths = [\n \"./schemas/*/tables/*.sql\",\n \"./schemas/*/views/*.sql\",\n \"./schemas/*/functions/*.sql\",\n]\n\n[studio]\nenabled = true\nport = 54323\n\n[inbucket]\nenabled = true\nport = 54324\n\n[auth]\nenabled = true\n# Origin that serves this app, including /oauth/consent. Use HTTPS in production.\nsite_url = \"http://localhost:3000\"\nadditional_redirect_urls = [\"http://localhost:3000/**\"]\njwt_expiry = 3600\nenable_signup = true\n\n[auth.email]\nenable_signup = true\n# Confirmations are off so local sign-ups can reach the app immediately.\nenable_confirmations = false\n\n# External MCP clients authorize here. Auth sends the user to\n# authorization_url_path, which the OAuth Consent block serves.\n[auth.oauth_server]\nenabled = true\nauthorization_url_path = \"/oauth/consent\"\n# Lets any compatible client register itself. Set to false to register clients\n# yourself.\nallow_dynamic_registration = true\n\n# The MCP server verifies user access tokens itself, so skip the gateway check.\n[functions.mcp-server]\nverify_jwt = false\n",
|
||
"type": "registry:file",
|
||
"target": "~/supabase/config.toml"
|
||
},
|
||
{
|
||
"path": "registry/default/blocks/headless-app-tanstack/supabase/schemas/public/tables/tasks.sql",
|
||
"content": "-- Declarative schema. This file is the source of truth for the tasks table:\n-- edit it, then run `supabase db diff -f <name>` to generate the migration.\n-- Changes made in Studio or the SQL editor are not picked up by the diff.\n\ncreate table public.tasks (\n id uuid primary key default gen_random_uuid(),\n user_id uuid not null default auth.uid() references auth.users (id) on delete cascade,\n title text not null constraint tasks_title_length check (\n char_length(title) between 1 and 200 and title ~ '[^[:space:]]'\n ),\n closed boolean not null default false,\n created_at timestamptz not null default now()\n);\n\n-- Support the ownership policies and the tools' newest-first ordering.\ncreate index tasks_user_id_created_at_id_idx on public.tasks (user_id, created_at desc, id desc);\n\n-- Agents call the MCP server with the user's access token, so every tool runs\n-- under these policies. A tool cannot reach another user's rows.\nalter table public.tasks enable row level security;\n\n-- Expose task operations to signed-in users even when automatic API grants are disabled.\ngrant select, insert, update, delete on table public.tasks to authenticated;\n\ncreate policy \"Users can view their own tasks\"\non public.tasks\nfor select\nto authenticated\nusing ((select auth.uid()) = user_id);\n\ncreate policy \"Users can create their own tasks\"\non public.tasks\nfor insert\nto authenticated\nwith check ((select auth.uid()) = user_id);\n\ncreate policy \"Users can update their own tasks\"\non public.tasks\nfor update\nto authenticated\nusing ((select auth.uid()) = user_id)\nwith check ((select auth.uid()) = user_id);\n\ncreate policy \"Users can delete their own tasks\"\non public.tasks\nfor delete\nto authenticated\nusing ((select auth.uid()) = user_id);\n",
|
||
"type": "registry:file",
|
||
"target": "~/supabase/schemas/public/tables/tasks.sql"
|
||
},
|
||
{
|
||
"path": "registry/default/blocks/mcp-server/supabase/functions/mcp-server/index.ts",
|
||
"content": "import 'jsr:@supabase/functions-js@2.108.2/edge-runtime.d.ts'\n\nimport { createMcpHandler, McpServer } from 'npm:@modelcontextprotocol/server@2.0.0'\nimport { pipeline } from 'npm:@supabase/middleware@0.5.0'\nimport {\n withOAuthProtectedResource,\n withSupabase,\n type SupabaseContext,\n} from 'npm:@supabase/server@1.6.0'\n\nimport { registerTools, type ToolContext } from './tools/index.ts'\n\n// An MCP server as a single Supabase Edge Function, composed as a pipeline:\n//\n// withOAuthProtectedResource OAuth discovery for external MCP clients. Runs\n// before the auth gate so unauthenticated clients\n// can fetch the RFC 9728 metadata, and adds the\n// WWW-Authenticate challenge to the gate's 401.\n// withSupabase Verifies the user access token and builds an\n// RLS-scoped client, so both embedded product\n// agents and external OAuth clients act as the\n// signed-in user.\n// handleMcp MCP transport and tools (./tools/index.ts).\n//\n// On Supabase Edge Functions the public URLs in the OAuth metadata are derived\n// automatically, locally and hosted. Off Edge Functions, pass `resourceServer`\n// and `authorizationServer` to withOAuthProtectedResource.\n\nfunction readTextEnv(name: string, fallback: string): string {\n return Deno.env.get(name)?.trim() || fallback\n}\n\nconst SERVER_NAME = readTextEnv('MCP_SERVER_NAME', 'supabase-mcp')\nconst SERVER_DESCRIPTION = readTextEnv(\n 'MCP_SERVER_DESCRIPTION',\n 'MCP access to this Supabase project for the signed-in user.'\n)\n\nconst SERVER_INSTRUCTIONS =\n `${SERVER_DESCRIPTION} ` +\n 'Every tool runs as the signed-in Supabase user, so role grants and Row Level Security apply. ' +\n \"Call tools/list to discover what this project exposes, and read a tool's description and \" +\n 'annotations before calling it — some tools have side effects.'\n\nconst CORS_HEADERS: Record<string, string> = {\n 'Access-Control-Allow-Origin': '*',\n 'Access-Control-Allow-Methods': 'GET, POST, DELETE, OPTIONS',\n 'Access-Control-Allow-Headers':\n 'Authorization, Content-Type, Accept, Mcp-Protocol-Version, Mcp-Session-Id, Mcp-Method, Mcp-Name',\n 'Access-Control-Expose-Headers': 'WWW-Authenticate, Mcp-Session-Id',\n}\n\nfunction createServer(context: ToolContext): McpServer {\n const server = new McpServer(\n { name: SERVER_NAME, version: '1.0.0' },\n { instructions: SERVER_INSTRUCTIONS }\n )\n\n registerTools(server, context)\n return server\n}\n\nasync function handleMcp(request: Request, ctx: SupabaseContext): Promise<Response> {\n // The server and its tools are bound to this caller for exactly one request.\n const handler = createMcpHandler(\n () =>\n createServer({\n supabase: ctx.supabase,\n // auth: 'user' guarantees both claim shapes before this handler runs.\n userClaims: ctx.userClaims!,\n jwtClaims: ctx.jwtClaims!,\n }),\n { onerror: (error) => console.error('MCP request failed', error) }\n )\n\n return handler.fetch(request)\n}\n\n// The handler is passed inline so TypeScript infers its context from the entries.\n// Passing `handleMcp` directly collapses the inferred context to `object`.\nDeno.serve(\n pipeline(\n [withOAuthProtectedResource(), withSupabase({ auth: 'user', cors: { headers: CORS_HEADERS } })],\n (request, ctx) => handleMcp(request, ctx)\n )\n)\n",
|
||
"type": "registry:file",
|
||
"target": "~/supabase/functions/mcp-server/index.ts"
|
||
},
|
||
{
|
||
"path": "registry/default/blocks/mcp-server/supabase/functions/mcp-server/deno.json",
|
||
"content": "{\n \"nodeModulesDir\": \"none\",\n \"compilerOptions\": {\n \"strict\": true\n },\n \"tasks\": {\n \"check\": \"deno check index.ts\"\n }\n}\n",
|
||
"type": "registry:file",
|
||
"target": "~/supabase/functions/mcp-server/deno.json"
|
||
},
|
||
{
|
||
"path": "registry/default/blocks/mcp-server/supabase/functions/mcp-server/deno.lock",
|
||
"content": "{\n \"version\": \"5\",\n \"specifiers\": {\n \"jsr:@supabase/functions-js@2.108.2\": \"2.108.2\",\n \"npm:@modelcontextprotocol/server@2.0.0\": \"2.0.0\",\n \"npm:@supabase/middleware@0.5.0\": \"0.5.0\",\n \"npm:@supabase/server@1.6.0\": \"1.6.0_@supabase+supabase-js@2.108.2\",\n \"npm:@supabase/supabase-js@2.108.2\": \"2.108.2\",\n \"npm:openai@^4.52.5\": \"4.104.0_zod@4.4.3\",\n \"npm:zod@4.4.3\": \"4.4.3\"\n },\n \"jsr\": {\n \"@supabase/functions-js@2.108.2\": {\n \"integrity\": \"39665d68e1cb721b0714ed1f55c075fba16f8d992672b51458660b3c1ec77c8f\",\n \"dependencies\": [\n \"npm:openai\"\n ]\n }\n },\n \"npm\": {\n \"@modelcontextprotocol/core@2.0.0\": {\n \"integrity\": \"sha512-pJCEwGG7Lfr/+PQp9ZTwKXNeO5wzbfKL7H3MYpCorM4oFBoQrdjnBgEoqG+RjhsvS1FKrDbKux+M1HhlnGWqcA==\",\n \"dependencies\": [\n \"zod\"\n ]\n },\n \"@modelcontextprotocol/server@2.0.0\": {\n \"integrity\": \"sha512-YhHWdHfpFMQfd0prsEnxKeS3Qz3ytIGmsS0sth4KDjnacIT7hxk6hXHkJ9KysxlkvTM+WZAtQbbcUhdoP4Hvtw==\",\n \"dependencies\": [\n \"@modelcontextprotocol/core\",\n \"zod\"\n ]\n },\n \"@supabase/auth-js@2.108.2\": {\n \"integrity\": \"sha512-tNaQmBgodDZwgB40mRwVbxFy8IDYwjdpcZ0BYrWiwlULCSQoJj4QoG4zgJT7QRPXcqipefNOzvO/qAu4dF98ag==\",\n \"dependencies\": [\n \"tslib\"\n ]\n },\n \"@supabase/functions-js@2.108.2\": {\n \"integrity\": \"sha512-RNUX8EiBy3iLwAX19jtRzLyePnl11/fHcgwDHLnpKcDSXt/5qBnh3LUwAtIjT21Q66QsmNUR2esrHziLCpNubw==\",\n \"dependencies\": [\n \"tslib\"\n ]\n },\n \"@supabase/middleware@0.5.0\": {\n \"integrity\": \"sha512-OjukUo+5p14zxTuylf2zVg1hZCHWKLO6VrZhtVeQQw09yrVo3GAduvFJPiFDhTAz/Du1ZfEzAR+s6aRAWD5wzQ==\",\n \"dependencies\": [\n \"std-env\"\n ]\n },\n \"@supabase/phoenix@0.4.5\": {\n \"integrity\": \"sha512-aAn9H9ovVyeApKy11OWOrrOGq8DV68yWeH4ud2lN9fzn4aO8Zb5GLL9m1pUg9nLqIcT+ZDfAcsZe0E/nqdv2lw==\"\n },\n \"@supabase/postgrest-js@2.108.2\": {\n \"integrity\": \"sha512-GQ28/Y8hk3CFmkb3kXH1h/AQx6JIYSQfO0CJMRVBcEKZoNy6C45cXAZ4fcJvRC5Id0cs6xnkUV0+c0rIocigsw==\",\n \"dependencies\": [\n \"tslib\"\n ]\n },\n \"@supabase/realtime-js@2.108.2\": {\n \"integrity\": \"sha512-aAGxCSUemZvQIibnCdvNvgaKib28I4rfrNjKbQ9cG1uBLwUsI7hVpGXgEbypCCDhLjQlDTAiJlu7rgljYUT73g==\",\n \"dependencies\": [\n \"@supabase/phoenix\",\n \"tslib\"\n ]\n },\n \"@supabase/server@1.6.0_@supabase+supabase-js@2.108.2\": {\n \"integrity\": \"sha512-LtUkzUqUGip6I2+kvSmA04u3s+npwLseer2yomBnUdQ1zyJPtlgjAqbwsNuGC1DT/FHi1BvvJluI7dZVV2XdTw==\",\n \"dependencies\": [\n \"@supabase/middleware\",\n \"@supabase/supabase-js\",\n \"jose\"\n ]\n },\n \"@supabase/storage-js@2.108.2\": {\n \"integrity\": \"sha512-TVZPQxXGxY2+A6yTtm77zUHsh70lBhYUEaJL8RQC+BghcX/ygiMG/rmXrNVBce30/WAeNPa8FiG8HbqlGeV05g==\",\n \"dependencies\": [\n \"iceberg-js\",\n \"tslib\"\n ]\n },\n \"@supabase/supabase-js@2.108.2\": {\n \"integrity\": \"sha512-hFhnPveb5JQg4a0QYicM0swT253YHMdfeRAl2BKHOlI5VAzuHxUGSr8RbwNLYNPauWOgQMS1H8sz8bvYlgwUfQ==\",\n \"dependencies\": [\n \"@supabase/auth-js\",\n \"@supabase/functions-js\",\n \"@supabase/postgrest-js\",\n \"@supabase/realtime-js\",\n \"@supabase/storage-js\"\n ]\n },\n \"@types/node-fetch@2.6.13\": {\n \"integrity\": \"sha512-QGpRVpzSaUs30JBSGPjOg4Uveu384erbHBoT1zeONvyCfwQxIkUshLAOqN/k9EjGviPRmWTTe6aH2qySWKTVSw==\",\n \"dependencies\": [\n \"@types/node\",\n \"form-data\"\n ]\n },\n \"@types/node@18.19.130\": {\n \"integrity\": \"sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg==\",\n \"dependencies\": [\n \"undici-types\"\n ]\n },\n \"abort-controller@3.0.0\": {\n \"integrity\": \"sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==\",\n \"dependencies\": [\n \"event-target-shim\"\n ]\n },\n \"agentkeepalive@4.6.0\": {\n \"integrity\": \"sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==\",\n \"dependencies\": [\n \"humanize-ms\"\n ]\n },\n \"asynckit@0.4.0\": {\n \"integrity\": \"sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==\"\n },\n \"call-bind-apply-helpers@1.0.2\": {\n \"integrity\": \"sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==\",\n \"dependencies\": [\n \"es-errors\",\n \"function-bind\"\n ]\n },\n \"combined-stream@1.0.8\": {\n \"integrity\": \"sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==\",\n \"dependencies\": [\n \"delayed-stream\"\n ]\n },\n \"delayed-stream@1.0.0\": {\n \"integrity\": \"sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==\"\n },\n \"dunder-proto@1.0.1\": {\n \"integrity\": \"sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==\",\n \"dependencies\": [\n \"call-bind-apply-helpers\",\n \"es-errors\",\n \"gopd\"\n ]\n },\n \"es-define-property@1.0.1\": {\n \"integrity\": \"sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==\"\n },\n \"es-errors@1.3.0\": {\n \"integrity\": \"sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==\"\n },\n \"es-object-atoms@1.1.2\": {\n \"integrity\": \"sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==\",\n \"dependencies\": [\n \"es-errors\"\n ]\n },\n \"es-set-tostringtag@2.1.0\": {\n \"integrity\": \"sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==\",\n \"dependencies\": [\n \"es-errors\",\n \"get-intrinsic\",\n \"has-tostringtag\",\n \"hasown\"\n ]\n },\n \"event-target-shim@5.0.1\": {\n \"integrity\": \"sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==\"\n },\n \"form-data-encoder@1.7.2\": {\n \"integrity\": \"sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A==\"\n },\n \"form-data@4.0.6\": {\n \"integrity\": \"sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==\",\n \"dependencies\": [\n \"asynckit\",\n \"combined-stream\",\n \"es-set-tostringtag\",\n \"hasown\",\n \"mime-types\"\n ]\n },\n \"formdata-node@4.4.1\": {\n \"integrity\": \"sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ==\",\n \"dependencies\": [\n \"node-domexception\",\n \"web-streams-polyfill\"\n ]\n },\n \"function-bind@1.1.2\": {\n \"integrity\": \"sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==\"\n },\n \"get-intrinsic@1.3.0\": {\n \"integrity\": \"sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==\",\n \"dependencies\": [\n \"call-bind-apply-helpers\",\n \"es-define-property\",\n \"es-errors\",\n \"es-object-atoms\",\n \"function-bind\",\n \"get-proto\",\n \"gopd\",\n \"has-symbols\",\n \"hasown\",\n \"math-intrinsics\"\n ]\n },\n \"get-proto@1.0.1\": {\n \"integrity\": \"sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==\",\n \"dependencies\": [\n \"dunder-proto\",\n \"es-object-atoms\"\n ]\n },\n \"gopd@1.2.0\": {\n \"integrity\": \"sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==\"\n },\n \"has-symbols@1.1.0\": {\n \"integrity\": \"sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==\"\n },\n \"has-tostringtag@1.0.2\": {\n \"integrity\": \"sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==\",\n \"dependencies\": [\n \"has-symbols\"\n ]\n },\n \"hasown@2.0.4\": {\n \"integrity\": \"sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==\",\n \"dependencies\": [\n \"function-bind\"\n ]\n },\n \"humanize-ms@1.2.1\": {\n \"integrity\": \"sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==\",\n \"dependencies\": [\n \"ms\"\n ]\n },\n \"iceberg-js@0.8.1\": {\n \"integrity\": \"sha512-1dhVQZXhcHje7798IVM+xoo/1ZdVfzOMIc8/rgVSijRK38EDqOJoGula9N/8ZI5RD8QTxNQtK/Gozpr+qUqRRA==\"\n },\n \"jose@6.2.10\": {\n \"integrity\": \"sha512-iiW7J9qRFlGxvCOIBDBDxFePQSn7ZMAnrYGhrrOo6siO/MIqwfyilLR27pkfDgUk+raLuzADS8A3S/KLBisc0g==\"\n },\n \"math-intrinsics@1.1.0\": {\n \"integrity\": \"sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==\"\n },\n \"mime-db@1.52.0\": {\n \"integrity\": \"sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==\"\n },\n \"mime-types@2.1.35\": {\n \"integrity\": \"sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==\",\n \"dependencies\": [\n \"mime-db\"\n ]\n },\n \"ms@2.1.3\": {\n \"integrity\": \"sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==\"\n },\n \"node-domexception@1.0.0\": {\n \"integrity\": \"sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==\",\n \"deprecated\": true\n },\n \"node-fetch@2.7.0\": {\n \"integrity\": \"sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==\",\n \"dependencies\": [\n \"whatwg-url\"\n ]\n },\n \"openai@4.104.0_zod@4.4.3\": {\n \"integrity\": \"sha512-p99EFNsA/yX6UhVO93f5kJsDRLAg+CTA2RBqdHK4RtK8u5IJw32Hyb2dTGKbnnFmnuoBv5r7Z2CURI9sGZpSuA==\",\n \"dependencies\": [\n \"@types/node\",\n \"@types/node-fetch\",\n \"abort-controller\",\n \"agentkeepalive\",\n \"form-data-encoder\",\n \"formdata-node\",\n \"node-fetch\",\n \"zod\"\n ],\n \"optionalPeers\": [\n \"zod\"\n ],\n \"bin\": true\n },\n \"std-env@4.2.0\": {\n \"integrity\": \"sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw==\"\n },\n \"tr46@0.0.3\": {\n \"integrity\": \"sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==\"\n },\n \"tslib@2.8.1\": {\n \"integrity\": \"sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==\"\n },\n \"undici-types@5.26.5\": {\n \"integrity\": \"sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==\"\n },\n \"web-streams-polyfill@4.0.0-beta.3\": {\n \"integrity\": \"sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug==\"\n },\n \"webidl-conversions@3.0.1\": {\n \"integrity\": \"sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==\"\n },\n \"whatwg-url@5.0.0\": {\n \"integrity\": \"sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==\",\n \"dependencies\": [\n \"tr46\",\n \"webidl-conversions\"\n ]\n },\n \"zod@4.4.3\": {\n \"integrity\": \"sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==\"\n }\n }\n}\n",
|
||
"type": "registry:file",
|
||
"target": "~/supabase/functions/mcp-server/deno.lock"
|
||
},
|
||
{
|
||
"path": "registry/default/blocks/mcp-server/supabase/functions/mcp-server/.env.example",
|
||
"content": "# Copy this file to supabase/functions/.env before serving locally:\n# cp supabase/functions/mcp-server/.env.example supabase/functions/.env\n# supabase functions serve mcp-server --env-file supabase/functions/.env\n\n# Deploy these values after linking your project:\n# supabase secrets set --env-file supabase/functions/.env\n# Supabase provides the project URL and API keys automatically.\n\n# Keep the protocol-level server name short and project-specific.\nMCP_SERVER_NAME=supabase-mcp\nMCP_SERVER_DESCRIPTION=\"MCP access to this Supabase project for the signed-in user.\"\n",
|
||
"type": "registry:file",
|
||
"target": "~/supabase/functions/mcp-server/.env.example"
|
||
},
|
||
{
|
||
"path": "registry/default/blocks/mcp-server/supabase/functions/mcp-server/tools/types.ts",
|
||
"content": "import type { SupabaseContext } from 'npm:@supabase/server@1.6.0'\nimport type { SupabaseClient } from 'npm:@supabase/supabase-js@2.108.2'\n\n// Only expose the user-scoped client and verified identity to tools. Keeping\n// supabaseAdmin out of this type makes bypassing RLS an explicit design choice.\nexport type ToolContext = {\n supabase: SupabaseClient\n userClaims: NonNullable<SupabaseContext['userClaims']>\n jwtClaims: NonNullable<SupabaseContext['jwtClaims']>\n}\n",
|
||
"type": "registry:file",
|
||
"target": "~/supabase/functions/mcp-server/tools/types.ts"
|
||
},
|
||
{
|
||
"path": "registry/default/blocks/mcp-server/supabase/functions/mcp-server/tools/result.ts",
|
||
"content": "import type { CallToolResult } from 'npm:@modelcontextprotocol/server@2.0.0'\n\n// Shared helpers for building MCP tool results, so every tool returns the same\n// shape and signals failure the same way.\n\n/**\n * A successful structured result with a JSON text fallback for older clients.\n */\nexport function jsonResult(value: unknown): CallToolResult {\n return {\n content: [{ type: 'text', text: JSON.stringify(value) ?? 'null' }],\n structuredContent: value ?? null,\n }\n}\n\n/**\n * A failed result. The message goes back to the model so it can correct itself,\n * so keep it actionable — and free of credentials, claims, and stack traces.\n */\nexport function errorResult(message: string): CallToolResult {\n return {\n isError: true,\n content: [{ type: 'text', text: message }],\n }\n}\n\nfunction readString(value: unknown, key: string): string | null {\n if (!value || typeof value !== 'object' || !(key in value)) return null\n const property = (value as Record<string, unknown>)[key]\n return typeof property === 'string' && property ? property : null\n}\n\n/**\n * Turn an unknown thrown value into a safe MCP error. Supabase API errors often\n * carry a `code` and `hint`, both of which help a model fix its next call.\n */\nexport function runtimeErrorResult(error: unknown): CallToolResult {\n const message = error instanceof Error ? error.message : String(error)\n const code = readString(error, 'code')\n const hint = readString(error, 'hint')\n\n return errorResult(\n [code ? `[${code}]` : null, message, hint ? `Hint: ${hint}` : null].filter(Boolean).join(' ')\n )\n}\n",
|
||
"type": "registry:file",
|
||
"target": "~/supabase/functions/mcp-server/tools/result.ts"
|
||
},
|
||
{
|
||
"path": "registry/default/blocks/mcp-server/supabase/functions/mcp-server/tools/whoami.ts",
|
||
"content": "import type { McpServer } from 'npm:@modelcontextprotocol/server@2.0.0'\n\nimport { jsonResult } from './result.ts'\nimport type { ToolContext } from './types.ts'\n\n// Answers from verified claims, demonstrating that every tool runs as the\n// signed-in user. client_id is present for OAuth tokens and null for ordinary\n// product sessions.\nexport function registerWhoamiTool(\n server: McpServer,\n { userClaims, jwtClaims }: ToolContext\n): void {\n const clientId =\n typeof jwtClaims?.client_id === 'string' && jwtClaims.client_id ? jwtClaims.client_id : null\n\n server.registerTool(\n 'whoami',\n {\n description: \"Return the signed-in user's identity and OAuth client id, when present.\",\n annotations: {\n readOnlyHint: true,\n destructiveHint: false,\n openWorldHint: false,\n },\n },\n () =>\n jsonResult({\n id: userClaims.id,\n email: userClaims.email ?? null,\n role: userClaims.role ?? null,\n client_id: clientId,\n })\n )\n}\n",
|
||
"type": "registry:file",
|
||
"target": "~/supabase/functions/mcp-server/tools/whoami.ts"
|
||
},
|
||
{
|
||
"path": "registry/default/clients/tanstack/lib/supabase/client.ts",
|
||
"content": "/// <reference types=\"vite/types/importMeta.d.ts\" />\nimport { createBrowserClient } from '@supabase/ssr'\n\nexport function createClient() {\n return createBrowserClient(\n import.meta.env.VITE_SUPABASE_URL!,\n import.meta.env.VITE_SUPABASE_PUBLISHABLE_KEY!\n )\n}\n",
|
||
"type": "registry:lib"
|
||
},
|
||
{
|
||
"path": "registry/default/clients/tanstack/lib/supabase/server.ts",
|
||
"content": "import { createServerClient } from '@supabase/ssr'\nimport { getCookies, setCookie } from '@tanstack/react-start/server'\n\nexport function createClient() {\n return createServerClient(\n process.env.VITE_SUPABASE_URL!,\n process.env.VITE_SUPABASE_PUBLISHABLE_KEY!,\n {\n cookies: {\n getAll() {\n return Object.entries(getCookies()).map(\n ([name, value]) =>\n ({\n name,\n value,\n }) as { name: string; value: string }\n )\n },\n setAll(cookies) {\n cookies.forEach((cookie) => {\n setCookie(cookie.name, cookie.value)\n })\n },\n },\n }\n )\n}\n",
|
||
"type": "registry:lib"
|
||
}
|
||
],
|
||
"envVars": {
|
||
"VITE_PRODUCT_NAME": "Your product",
|
||
"VITE_SUPABASE_URL": "",
|
||
"VITE_SUPABASE_PUBLISHABLE_KEY": ""
|
||
},
|
||
"docs": "Follow https://supabase.com/library/docs/tanstack/headless-app for setup. Merge `supabase/config.toml` with existing project settings. Open `/agents` to connect your agent and approve access, then try the example task tools.\n\nYou'll need to set the following environment variables in your project: `VITE_SUPABASE_URL` and `VITE_SUPABASE_PUBLISHABLE_KEY`."
|
||
} |