Files
supabase/apps/ui-library/public/r/headless-app-tanstack.json
T
Katerina Skroumpelou 2013ebf417 docs: drop alpha labels and pin server and middleware imports to a major (#51031)
## Problem

`@supabase/middleware` ships as 1.0.0. The docs still label the
`pipeline` entry form of `withSupabase` alpha, and several snippets
import `npm:@supabase/server` and `npm:@supabase/middleware` with no
version or with a `^0.5.0` pin. A snippet without a version leaves
readers and tools to guess one, and a guessed version fails on deploy.

## Solution

- Removes the alpha wording from the middleware reference intro and
usage examples, the server frameworks partial, and the Bring your own
MCP guide. The `@supabase/server` 1.6.0 floor stays.
- Pins every `npm:@supabase/server` and `npm:@supabase/middleware`
import in the guides to a major range, `@1`, following the
`npm:@supabase/supabase-js@2` convention in Managing dependencies.
- Bumps the authenticated-mcp-server example to middleware `^1.0.0` and
server `^1.9.0`.

~~Blocked by supabase/middleware#49. The `@1` range resolves once 1.0.0
is on npm.~~




<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **Documentation**
* Updated authentication, API key, and MCP examples to use versioned
Supabase server and middleware packages.
* Clarified that pipeline and nested composition behave the same, and
that both require `@supabase/server` 1.6.0 or later.
* Removed alpha-status labels from `withSupabase` guidance while
retaining the 1.6.0 minimum-version requirement.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-09-30 17:27:44 +03:00

116 lines
44 KiB
JSON
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
"name": "headless-app-tanstack",
"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/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/tools/index.ts"
},
{
"path": "registry/default/blocks/headless-app-tanstack/supabase/functions/mcp/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/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`\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]\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/supabase/functions/mcp/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@1'\nimport {\n withOAuthProtectedResource,\n withSupabase,\n type SupabaseContext,\n} from 'npm:@supabase/server@1'\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/index.ts"
},
{
"path": "registry/default/blocks/mcp/supabase/functions/mcp/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/deno.json"
},
{
"path": "registry/default/blocks/mcp/supabase/functions/mcp/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@1\": \"1.0.0\",\n \"npm:@supabase/server@1\": \"1.9.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\"\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@1.0.0\": {\n \"integrity\": \"sha512-ecA+z/o5E9sB+quuupGt8d7QjrnD4BBwZr+Qthm0uj6CtL/+yvAUTDlknhGuTbUidvx5CxBHJ1R3H96UjDpgyg==\",\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.9.0_@supabase+supabase-js@2.108.2\": {\n \"integrity\": \"sha512-EhgfKFWjB6bCWy2UR2JdExnK+B8WBdwiG90+bLTt0GkdAuHZZSooNxOsn/fP3UlcXUNJ6xfAkYlvGzN8NUpz1w==\",\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.12\": {\n \"integrity\": \"sha512-9NiFmJEex0sy2Dk58j2UGBSHgUs2ypF9eZSu4L6vjOX3Dp96Sw1F3uL+H+D1sx02jZZdzUT0HgvCy59CuvXcWw==\"\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\": {\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 ],\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.6.2\": {\n \"integrity\": \"sha512-lh5RCAGFa1Cm2hjtNwLQhSs/AsqdWnTQaBER9fEwN/88pSh7KOtJavtBx/0VlkN/uFd61SwYmljLMDAsHlvzBQ==\"\n }\n }\n}\n",
"type": "registry:file",
"target": "~/supabase/functions/mcp/deno.lock"
},
{
"path": "registry/default/blocks/mcp/supabase/functions/mcp/.env.example",
"content": "# Copy this file to supabase/functions/.env before serving locally:\n# cp supabase/functions/mcp/.env.example supabase/functions/.env\n# supabase functions serve mcp --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/.env.example"
},
{
"path": "registry/default/blocks/mcp/supabase/functions/mcp/tools/types.ts",
"content": "import type { SupabaseContext } from 'npm:@supabase/server@1'\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/tools/types.ts"
},
{
"path": "registry/default/blocks/mcp/supabase/functions/mcp/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/tools/result.ts"
},
{
"path": "registry/default/blocks/mcp/supabase/functions/mcp/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/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`.",
"type": "registry:block"
}