\n )\n}\n",
+ "type": "registry:component"
+ },
+ {
+ "path": "registry/default/blocks/assistant/lib/supabase/server.ts",
+ "content": "import { createServerClient } from '@supabase/ssr'\nimport { cookies } from 'next/headers'\n\n/**\n * If using Fluid compute: Don't put this client in a global variable. Always create a new client within each\n * function when using it.\n */\nexport async function createClient() {\n const cookieStore = await cookies()\n\n return createServerClient(\n process.env.NEXT_PUBLIC_SUPABASE_URL!,\n process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,\n {\n cookies: {\n getAll() {\n return cookieStore.getAll()\n },\n setAll(cookiesToSet) {\n try {\n cookiesToSet.forEach(({ name, value, options }) =>\n cookieStore.set(name, value, options)\n )\n } catch {\n // The `setAll` method was called from a Server Component.\n // This can be ignored if you have middleware refreshing\n // user sessions.\n }\n },\n },\n }\n )\n}\n",
+ "type": "registry:file",
+ "target": "lib/supabase/server.ts"
+ },
+ {
+ "path": "registry/default/blocks/assistant/lib/supabase/client.ts",
+ "content": "import { createBrowserClient } from '@supabase/ssr'\n\nexport function createClient() {\n return createBrowserClient(\n process.env.NEXT_PUBLIC_SUPABASE_URL!,\n process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!\n )\n}\n",
+ "type": "registry:file",
+ "target": "lib/supabase/client.ts"
+ },
+ {
+ "path": "registry/default/blocks/assistant/lib/supabase/middleware.ts",
+ "content": "import { createServerClient } from '@supabase/ssr'\nimport { NextResponse, type NextRequest } from 'next/server'\n\nexport async function updateSession(request: NextRequest) {\n let supabaseResponse = NextResponse.next({\n request,\n })\n\n const supabase = createServerClient(\n process.env.NEXT_PUBLIC_SUPABASE_URL!,\n process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,\n {\n cookies: {\n getAll() {\n return request.cookies.getAll()\n },\n setAll(cookiesToSet) {\n cookiesToSet.forEach(({ name, value, options }) => request.cookies.set(name, value))\n supabaseResponse = NextResponse.next({\n request,\n })\n cookiesToSet.forEach(({ name, value, options }) =>\n supabaseResponse.cookies.set(name, value, options)\n )\n },\n },\n }\n )\n\n // IMPORTANT: Avoid writing any logic between createServerClient and\n // supabase.auth.getUser(). A simple mistake could make it very hard to debug\n // issues with users being randomly logged out.\n\n const {\n data: { user },\n } = await supabase.auth.getUser()\n\n // IMPORTANT: You *must* return the supabaseResponse object as it is.\n // If you're creating a new response object with NextResponse.next() make sure to:\n // 1. Pass the request in it, like so:\n // const myNewResponse = NextResponse.next({ request })\n // 2. Copy over the cookies, like so:\n // myNewResponse.cookies.setAll(supabaseResponse.cookies.getAll())\n // 3. Change the myNewResponse object to fit your needs, but avoid changing\n // the cookies!\n // 4. Finally:\n // return myNewResponse\n // If this is not done, you may be causing the browser and server to go out\n // of sync and terminate the user's session prematurely.\n\n return supabaseResponse\n}\n",
+ "type": "registry:file",
+ "target": "lib/supabase/middleware.ts"
+ },
+ {
+ "path": "registry/default/blocks/assistant/supabase/functions/mcp-server/index.ts",
+ "content": "// Follow this setup guide to integrate the Deno language server with your editor:\n// https://deno.land/manual/getting_started/setup_your_environment\n// This enables autocomplete, go to definition, etc.\n//\n// Setup type definitions for built-in Supabase Runtime APIs\n/// \n\nimport { Hono } from \"hono\";\nimport { McpServer, StreamableHttpTransport } from \"mcp-lite\";\nimport { z } from \"zod\";\n\n// We create two Hono instances:\n// 1. `app` is the root handler for the Supabase Edge Function (must match the function name, e.g. /mcp-server)\n// 2. `mcpApp` handles the MCP protocol and health endpoints, mounted under the function route\n// This pattern is required because Supabase Edge Functions route all requests to //*\n\nconst mcp = new McpServer({\n name: \"supabase-assistant-mcp-server\",\n version: \"1.0.0\",\n schemaAdapter: (schema: unknown) => z.toJSONSchema(schema as z.ZodType),\n});\n\n// Temporary placeholder tool while wiring up the rest of the MCP server.\nmcp.tool(\"queryDatabase\", {\n description:\n \"Example placeholder tool that just echoes a string back to the caller.\",\n inputSchema: z.object({\n message: z\n .string()\n .default(\"Hello from the Supabase MCP server!\")\n .describe(\"Optional message you would like the tool to return\"),\n }),\n handler: async ({ message }: { message: string }) => {\n return {\n content: [\n {\n type: \"text\",\n text: `Example tool response: ${message}`,\n },\n ],\n };\n },\n});\n\nconst transport = new StreamableHttpTransport();\nconst httpHandler = transport.bind(mcp);\n\nconst app = new Hono();\nconst mcpApp = new Hono();\n\nmcpApp.get(\"/\", (c) => {\n return c.json({\n message: \"MCP Server on Supabase Edge Functions\",\n endpoints: {\n mcp: \"/mcp\",\n health: \"/health\",\n },\n });\n});\n\nmcpApp.get(\"/health\", (c) => {\n return c.json({\n message: \"Service is up and running\",\n });\n});\n\nmcpApp.all(\"/mcp\", async (c) => {\n const response = await httpHandler(c.req.raw);\n return response;\n});\n\n// Mount the MCP app at /mcp-server (matches the function name)\napp.route(\"/mcp-server\", mcpApp);\n\nexport default app;\n\n/* To invoke locally:\n\n 1. Run `supabase start` (see: https://supabase.com/docs/reference/cli/supabase-start)\n 2. Make an HTTP request:\n\n curl -i --location --request POST 'http://127.0.0.1:54321/functions/v1/mcp-server/mcp' \\\n --header 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6ImFub24iLCJleHAiOjE5ODM4MTI5OTZ9.CRXP1A7WOeoJeXxjNni43kdQwgnWNReilDMblYTn_I0' \\\n --header 'Content-Type: application/json' \\\n --data '{\"jsonrpc\":\"2.0\",\"method\":\"tools/list\",\"id\":1}'\n\n 3. Test the health endpoint:\n\n curl 'http://127.0.0.1:54321/functions/v1/mcp-server/health'\n\n*/\n",
+ "type": "registry:file",
+ "target": "supabase/functions/mcp-server/index.ts"
+ },
+ {
+ "path": "registry/default/blocks/assistant/supabase/functions/mcp-server/deno.json",
+ "content": "{\n \"compilerOptions\": {\n \"lib\": [\"deno.window\", \"deno.ns\"],\n \"strict\": true\n },\n \"imports\": {\n \"hono\": \"npm:hono@^4.6.14\",\n \"mcp-lite\": \"npm:mcp-lite@0.8.2\",\n \"zod\": \"npm:zod@^4.1.12\",\n \"@supabase/supabase-js\": \"npm:@supabase/supabase-js@^2\"\n }\n}\n",
+ "type": "registry:file",
+ "target": "supabase/functions/mcp-server/deno.json"
+ },
+ {
+ "path": "registry/default/blocks/assistant/supabase/functions/chat/index.ts",
+ "content": "import { experimental_createMCPClient as createMCPClient } from '@ai-sdk/mcp'\nimport { openai } from '@ai-sdk/openai'\nimport { streamText, type CoreMessage } from 'ai'\nimport { z } from 'zod'\n\ntype ChatRequestBody = {\n messages?: CoreMessage[]\n model?: string\n}\n\nconst queryDatabaseInputSchema = z.object({\n table: z.string().describe('The name of the table to query'),\n columns: z\n .string()\n .optional()\n .describe(\"Columns to select (comma-separated). Defaults to '*' for all columns.\"),\n filters: z\n .array(\n z.object({\n column: z.string().describe('The column to filter on'),\n operator: z\n .enum(['eq', 'neq', 'gt', 'gte', 'lt', 'lte', 'like', 'ilike', 'in', 'is'])\n .describe('The filter operator'),\n value: z.any().describe('The value to filter by'),\n })\n )\n .optional()\n .describe('Array of filter conditions to apply'),\n limit: z.number().optional().describe('Maximum number of rows to return'),\n orderBy: z\n .object({\n column: z.string().describe('Column to order by'),\n ascending: z.boolean().optional().describe('Sort order (default: true)'),\n })\n .optional()\n .describe('Ordering options'),\n explanation: z.string().optional().describe('Optional explanation of what the query does'),\n})\n\nconst supabaseUrl = Deno.env.get('SUPABASE_URL')?.replace(/\\/$/, '')\nconst defaultMcpServerUrl = supabaseUrl ? `${supabaseUrl}/functions/v1/mcp-server` : undefined\n\nconst mcpServerUrl =\n Deno.env.get('MCP_SERVER_URL') ??\n Deno.env.get('NEXT_PUBLIC_MCP_SERVER_URL') ??\n defaultMcpServerUrl ??\n 'http://localhost:54321/functions/v1/mcp-server'\n\nconst corsHeaders: Record = {\n 'Access-Control-Allow-Origin': '*',\n 'Access-Control-Allow-Headers': 'authorization, content-type',\n 'Access-Control-Allow-Methods': 'POST, OPTIONS',\n}\n\nconst systemPrompt = `You are a helpful AI assistant with access to a database.\nYou can help users query their database using natural language.\nUse the queryDatabase tool to fetch data from tables with filters and options.\nAlways provide clear explanations of what the query does.\nThe database respects Row Level Security (RLS), so users can only access their own data.\nWhen building queries:\n- Use the table parameter to specify which table to query\n- Use filters array to add conditions (eq, neq, gt, gte, lt, lte, like, ilike, in, is)\n- Use limit to control the number of results\n- Use orderBy to sort results by a specific column`\n\nDeno.serve(async (req) => {\n if (req.method === 'OPTIONS') {\n return new Response('ok', { headers: corsHeaders })\n }\n\n if (req.method !== 'POST') {\n return new Response('Method Not Allowed', {\n status: 405,\n headers: corsHeaders,\n })\n }\n\n const authHeader = req.headers.get('Authorization')\n if (!authHeader) {\n return new Response(JSON.stringify({ error: 'Unauthorized' }), {\n status: 401,\n headers: { ...corsHeaders, 'Content-Type': 'application/json' },\n })\n }\n\n let body: ChatRequestBody\n try {\n body = await req.json()\n } catch {\n return new Response(JSON.stringify({ error: 'Invalid JSON payload' }), {\n status: 400,\n headers: { ...corsHeaders, 'Content-Type': 'application/json' },\n })\n }\n\n if (!body.messages || !Array.isArray(body.messages)) {\n return new Response(JSON.stringify({ error: 'Request must include a messages array' }), {\n status: 400,\n headers: { ...corsHeaders, 'Content-Type': 'application/json' },\n })\n }\n\n const modelId = body.model && typeof body.model === 'string' ? body.model : 'gpt-4o'\n\n let mcpClient: Awaited> | null = null\n let clientClosed = false\n\n const closeMcpClient = async () => {\n if (!mcpClient || clientClosed) return\n clientClosed = true\n try {\n await mcpClient.close()\n } catch (closeError) {\n console.error('Failed to close MCP client:', closeError)\n }\n }\n\n try {\n mcpClient = await createMCPClient({\n transport: {\n type: 'http',\n url: mcpServerUrl,\n headers: {\n Authorization: authHeader,\n },\n },\n })\n\n const tools = await mcpClient.tools({\n schemas: {\n queryDatabase: {\n inputSchema: queryDatabaseInputSchema,\n },\n },\n })\n\n const result = await streamText({\n model: openai(modelId),\n messages: body.messages,\n tools,\n system: systemPrompt,\n onFinish: closeMcpClient,\n })\n\n return result.toTextStreamResponse({ headers: corsHeaders })\n } catch (error) {\n await closeMcpClient()\n console.error('Assistant chat error:', error)\n return new Response(\n JSON.stringify({\n error: 'Failed to process chat request',\n details: error instanceof Error ? error.message : String(error),\n }),\n {\n status: 500,\n headers: { ...corsHeaders, 'Content-Type': 'application/json' },\n }\n )\n }\n})\n",
+ "type": "registry:file",
+ "target": "supabase/functions/chat/index.ts"
+ },
+ {
+ "path": "registry/default/blocks/assistant/supabase/functions/chat/deno.json",
+ "content": "{\n \"compilerOptions\": {\n \"lib\": [\"deno.window\", \"deno.ns\"],\n \"strict\": true\n },\n \"imports\": {\n \"hono\": \"npm:hono@^4.6.14\",\n \"zod\": \"npm:zod@^4.1.12\",\n \"ai\": \"npm:ai@^5.0.90\",\n \"@ai-sdk/openai\": \"npm:@ai-sdk/openai@^2.0.32\",\n \"@ai-sdk/mcp\": \"npm:@ai-sdk/mcp@latest\"\n }\n}\n",
+ "type": "registry:file",
+ "target": "supabase/functions/chat/deno.json"
+ },
+ {
+ "path": "registry/default/blocks/assistant/README.md",
+ "content": "# AI Assistant Block\n\nAn AI-powered chatbot widget with database query capabilities using the Model Context Protocol (MCP).\n\n## Features\n\n- Fixed bottom-right chat widget with minimize/expand controls\n- Natural language database queries using AI\n- Row Level Security (RLS) enforcement\n- Streaming responses with real-time feedback\n- Tool execution visualization\n- Secure SQL query execution with validation\n- Rich AI SDK Elements (Conversation, Messages, Prompt Input, Model Selector, Suggestions, Sources, Reasoning) for a modern assistant UX\n\n## Architecture\n\nThe block consists of four main pieces that work together end to end:\n\n1. **Assistant Widget** (`components/assistant-widget.tsx`): React component providing the chat UI, built with AI SDK Elements\n2. **Chat Edge Function** (`supabase/functions/chat/`): Supabase Edge Function that streams chat completions, bootstraps the MCP client, and is invoked directly from the browser\n3. **Hono MCP Server** (`supabase/functions/mcp-server/`): Supabase Edge Function exposing MCP tools (e.g. `queryDatabase`) via the `mcp-lite` transport\n4. **Supabase Client Utilities** (`lib/supabase/`): Server and browser helpers for authenticated Supabase access\n\n## Prerequisites\n\n- Supabase project with authentication enabled\n- OpenAI API key (or another MCP-compatible LLM provider)\n- Supabase CLI installed\n- Docker (for Supabase local dev)\n- Node.js and npm/pnpm installed\n- Deno 2.x (Supabase Edge Functions runtime)\n\n## Setup Instructions\n\n### 1. Install the Block\n\n```bash\nnpx shadcn@latest add https://ui.supabase.com/r/assistant\n```\n\n### 2. Install UI + AI SDK Elements\n\nThe assistant widget depends on both core UI primitives and the AI SDK Element registry entries. Install them (or make sure they already exist in your project) using the shadcn CLI:\n\n```bash\nnpx shadcn@latest add button input scroll-area \\\n @ai-elements/conversation \\\n @ai-elements/message \\\n @ai-elements/prompt-input \\\n @ai-elements/model-selector \\\n @ai-elements/reasoning \\\n @ai-elements/sources \\\n @ai-elements/suggestion\n```\n\nEach AI Element ships ready-to-style JSX + Tailwind so you can compose advanced assistant surfaces without rebuilding primitives.\n\n### 3. Set Up Environment Variables\n\nAdd the following to your `.env.local`:\n\n```env\n# OpenAI API Key\nOPENAI_API_KEY=your_openai_api_key\n\n# Supabase Configuration\nNEXT_PUBLIC_SUPABASE_URL=your_supabase_url\nNEXT_PUBLIC_SUPABASE_ANON_KEY=your_supabase_anon_key\n\n# Chat Edge Function (used by the browser)\nNEXT_PUBLIC_ASSISTANT_CHAT_URL=https://your-project.supabase.co/functions/v1/chat\n\n# MCP Server URL (consumed by the edge functions)\nMCP_SERVER_URL=https://your-project.supabase.co/functions/v1/mcp-server\n```\n\n### 4. Build and Deploy the Hono MCP Server\n\nSupabase Edge Functions route every request to `//*`, so the MCP server needs the \"two Hono apps\" pattern outlined in the [Supabase mcp-lite guide](https://supabase.com/docs/guides/functions/examples/mcp-server-mcp-lite):\n\n1. **Scaffold (optional):** `npm create mcp-lite@latest` and choose the **Supabase Edge Functions (MCP server)** template to bootstrap `supabase/functions/mcp-server`.\n2. **Import map:** Ensure `supabase/functions/mcp-server/deno.json` includes the dependencies the block uses:\n\n```jsonc\n{\n \"compilerOptions\": {\n \"lib\": [\"deno.window\", \"deno.ns\"],\n \"strict\": true\n },\n \"imports\": {\n \"hono\": \"npm:hono@^4.6.14\",\n \"mcp-lite\": \"npm:mcp-lite@0.8.2\",\n \"zod\": \"npm:zod@^4.1.12\",\n \"@supabase/supabase-js\": \"npm:@supabase/supabase-js@^2.48.0\"\n }\n}\n```\n\n3. **Hono entrypoint:** Update `supabase/functions/mcp-server/index.ts` to mount the MCP transport behind `/mcp-server/mcp`:\n\n```ts\nimport { Hono } from \"hono\";\nimport { McpServer, StreamableHttpTransport } from \"mcp-lite\";\nimport { createClient } from \"@supabase/supabase-js\";\nimport { z } from \"zod\";\n\nconst mcp = new McpServer({\n name: \"supabase-assistant-mcp-server\",\n version: \"1.0.0\",\n schemaAdapter: (schema) => z.toJSONSchema(schema as z.ZodType),\n});\n\n// ...define queryDatabase and other tools that create an authenticated Supabase client\n\nconst transport = new StreamableHttpTransport();\nconst httpHandler = transport.bind(mcp);\n\nconst mcpApp = new Hono();\nmcpApp.get(\"/\", (c) =>\n c.json({\n message: \"MCP Server on Supabase Edge Functions\",\n endpoints: { mcp: \"/mcp\", health: \"/health\" },\n })\n);\nmcpApp.get(\"/health\", (c) => c.json({ status: \"ok\" }));\nmcpApp.all(\"/mcp\", (c) => httpHandler(c.req.raw));\n\nconst app = new Hono();\napp.route(\"/mcp-server\", mcpApp);\n\nDeno.serve((req) => app.fetch(req));\n```\n\nThis structure keeps Supabase's root handler (`/mcp-server`) separate from your MCP protocol endpoints while still serving the `StreamableHttpTransport`.\n\n4. **Serve locally:**\n\n```bash\nsupabase start\nsupabase functions serve --no-verify-jwt mcp-server\n```\n\nYour MCP endpoint will be available at `http://localhost:54321/functions/v1/mcp-server/mcp`.\n\n5. **Deploy globally:**\n\n```bash\nsupabase functions deploy --no-verify-jwt mcp-server\n```\n\n### 5. Build and Deploy the Chat Edge Function\n\nThe chat edge function (located at `supabase/functions/chat/`) replaces the `/api/chat` Next.js route and is what the browser calls directly.\n\n1. Confirm the provided `deno.json` contains the AI + MCP dependencies required by the block.\n2. Run `supabase start` if you haven't already.\n3. Serve **both** edge functions in separate terminals so the MCP server and chat handler run side by side:\n\n```bash\nsupabase functions serve --env-file .env.local --no-verify-jwt mcp-server\nsupabase functions serve --env-file .env.local --no-verify-jwt chat\n```\n\n4. For local development, set `NEXT_PUBLIC_ASSISTANT_CHAT_URL=http://127.0.0.1:54321/functions/v1/chat`. In production, point it to `https://.supabase.co/functions/v1/chat`.\n5. Ensure `MCP_SERVER_URL` is available to the chat function (either via the Supabase dashboard or an `.env` file passed to `supabase functions serve`) so it can reach the MCP server endpoint.\n\n### 6. Add the Widget to Your App\n\n```tsx\nimport { AssistantWidget } from \"@/components/assistant-widget\";\n\nexport default function Layout({ children }) {\n return (\n
\n {children}\n \n
\n );\n}\n```\n\n## Local Development & Testing\n\n- Start Supabase locally: `supabase start`\n- Serve and iterate on the MCP function: `supabase functions serve --no-verify-jwt mcp-server`\n- Run the chat edge function alongside it: `supabase functions serve --no-verify-jwt chat`\n- Test with Claude Code: `claude mcp add my-mcp-server -t http http://localhost:54321/functions/v1/mcp-server/mcp`\n- Inspect traffic using the MCP inspector: `npx @modelcontextprotocol/inspector`\n- When everything looks good, deploy via `supabase functions deploy --no-verify-jwt mcp-server`\n\n## Widget UI with AI SDK Elements\n\nThe block ships with AI SDK Element primitives (Conversation, Message, PromptInput, ModelSelector, etc.) located under `components/ai-elements`. Combine them to build an opinionated assistant surface, like the following demo:\n\n```tsx\n\"use client\";\n\nimport {\n MessageBranch,\n MessageBranchContent,\n MessageBranchNext,\n MessageBranchPage,\n MessageBranchPrevious,\n MessageBranchSelector,\n} from \"@/components/ai-elements/message\";\nimport {\n Conversation,\n ConversationContent,\n ConversationScrollButton,\n} from \"@/components/ai-elements/conversation\";\nimport { Message, MessageContent } from \"@/components/ai-elements/message\";\nimport {\n PromptInput,\n PromptInputActionAddAttachments,\n PromptInputActionMenu,\n PromptInputActionMenuContent,\n PromptInputActionMenuTrigger,\n PromptInputAttachment,\n PromptInputAttachments,\n PromptInputBody,\n PromptInputButton,\n PromptInputFooter,\n PromptInputHeader,\n type PromptInputMessage,\n PromptInputSubmit,\n PromptInputTextarea,\n PromptInputTools,\n} from \"@/components/ai-elements/prompt-input\";\nimport {\n ModelSelector,\n ModelSelectorContent,\n ModelSelectorEmpty,\n ModelSelectorGroup,\n ModelSelectorInput,\n ModelSelectorItem,\n ModelSelectorList,\n ModelSelectorLogo,\n ModelSelectorLogoGroup,\n ModelSelectorName,\n ModelSelectorTrigger,\n} from \"@/components/ai-elements/model-selector\";\nimport {\n Reasoning,\n ReasoningContent,\n ReasoningTrigger,\n} from \"@/components/ai-elements/reasoning\";\nimport { MessageResponse } from \"@/components/ai-elements/message\";\nimport {\n Source,\n Sources,\n SourcesContent,\n SourcesTrigger,\n} from \"@/components/ai-elements/sources\";\nimport { Suggestion, Suggestions } from \"@/components/ai-elements/suggestion\";\nimport type { ToolUIPart } from \"ai\";\nimport { CheckIcon, GlobeIcon, MicIcon } from \"lucide-react\";\nimport { nanoid } from \"nanoid\";\nimport { useCallback, useState } from \"react\";\nimport { toast } from \"sonner\";\n\n// ...rest of the Example component from the block's source (see components/assistant-widget.tsx)\n```\n\nThe complete example (available in this block) demonstrates:\n\n- Branching messages with sources, reasoning traces, and MCP tool output\n- Scrollable conversation history with streaming responses\n- Prompt composer with attachments, microphone toggle, and model selector\n- Reusable suggestions to quickly seed the assistant\n\nUse the snippet as a starting point for your own assistant surface or trim it down for a lighter footprint.\n\n## Usage\n\n### For End Users\n\n1. Click the chat icon in the bottom-right corner to open the assistant\n2. Type natural language queries about your data\n3. The AI will translate your questions into SQL and execute them via the `queryDatabase` MCP tool\n4. Results are displayed in the chat interface with tool execution feedback\n5. Minimize or close the widget as needed\n\n### Example Queries\n\n- \"Show me all my tasks\"\n- \"How many orders did I place last month?\"\n- \"List my recent transactions\"\n- \"What's the total revenue from my sales?\"\n\n## Security Considerations\n\n### Query Builder Approach\n\nThe MCP server uses Supabase's query builder for safe data access:\n\n- Uses the `queryDatabase` tool with structured parameters (table, filters, orderBy, limit)\n- No raw SQL injection risks\n- Queries execute with the authenticated user's context\n- All operations go through Supabase's security layer\n\n### Row Level Security (RLS)\n\nAll queries respect Supabase RLS policies:\n\n- The authenticated user's token is passed to the MCP server\n- Queries run with user context\n- Users can only access data allowed by RLS policies\n\n### Authentication\n\n- Local development uses `--no-verify-jwt` for speed; production deployments should enforce authentication per the [MCP authorization spec](https://modelcontextprotocol.io/spec)\n- Never expose production data without adding proper auth and auditing\n\n#### MCP middleware helpers\n\n`mcp-lite` ships with Hono-style middleware so you can enforce authentication, logging, and rate limiting *before* a tool executes. This keeps your auth logic centralized instead of duplicating it inside each tool handler:\n\n```ts\n// Logging\nmcp.use(async ({ req }, next) => {\n const start = Date.now();\n await next();\n console.log(`${req.method} took ${Date.now() - start}ms`);\n});\n\n// Authentication\nmcp.use(async (ctx, next) => {\n const token = ctx.req.header(\"Authorization\");\n if (!token) throw new Error(\"Unauthorized\");\n\n const user = await validateToken(token);\n ctx.state.user = user;\n await next();\n});\n\n// Rate limiting (example)\nmcp.use(async (ctx, next) => {\n const userId = ctx.state.user?.id;\n if (await isRateLimited(userId)) {\n throw new Error(\"Rate limit exceeded\");\n }\n await next();\n});\n```\n\nInside your tools you can then rely on `ctx.state.user` (or whatever you set) rather than re-validating headers manually. See the [`mcp-lite` middleware docs](https://github.com/modelcontextprotocol/mcp-lite#middleware) for more patterns.\n\n### Best Practices\n\n1. **Enable RLS on all tables:** Ensure all tables have RLS enabled\n2. **Create appropriate policies:** Define policies that match your security requirements\n3. **Monitor edge function logs:** Check logs for suspicious queries or errors\n4. **Rate limiting:** Consider adding rate limiting to prevent abuse\n5. **Query complexity:** Set statement timeouts in Postgres to prevent long-running queries\n6. **Validate tool inputs:** Sanitize and validate every argument received by MCP tools\n\n## Customization\n\n### Change the AI Model\n\nEdit `supabase/functions/chat/index.ts`:\n\n```ts\nconst result = await streamText({\n model: openai(\"gpt-4o\"), // Change to another model (OpenAI, Anthropic, Google, etc.)\n // ... rest of config\n});\n```\n\n### Customize the System Prompt\n\nEdit the `system` parameter in `supabase/functions/chat/index.ts` to change the assistant's behavior.\n\n### Style the Widget\n\nAI SDK Elements are standard React components, so you can:\n\n- Override Tailwind classes\n- Reorder or remove sections (e.g., hide the model selector)\n- Inject custom suggestion lists or streaming loaders\n\n### Add More MCP Tools\n\nExtend the MCP server with additional tools in `supabase/functions/mcp-server/index.ts` following the same Hono pattern:\n\n```ts\nmcp.tool(\"myCustomTool\", {\n description: \"Description of what this tool does\",\n inputSchema: z.object({\n param1: z.string(),\n param2: z.number().optional(),\n }),\n handler: async (args, { req }) => {\n const supabaseClient = createClient(\n Deno.env.get(\"SUPABASE_URL\") ?? \"\",\n Deno.env.get(\"SUPABASE_ANON_KEY\") ?? \"\",\n {\n global: {\n headers: { Authorization: req.headers.get(\"Authorization\")! },\n },\n }\n );\n\n // Tool implementation\n return {\n content: [{ type: \"text\", text: \"Result\" }],\n };\n },\n});\n```\n\n## Troubleshooting\n\n### \"Unauthorized\" Error\n\n- Ensure the user is authenticated before using the widget\n- Check that `SUPABASE_URL` and `SUPABASE_ANON_KEY` are set correctly\n- Verify the Authorization header is forwarded to the MCP server\n\n### \"Query execution failed\"\n\n- Verify the table exists and is accessible\n- Check that RLS policies allow the user to access the data\n- Verify the column names in filters are correct\n- Check edge function logs: `supabase functions logs mcp-server`\n\n### \"Function not found\" or 404\n\n- Verify the chat function is deployed at `/functions/v1/chat` and `NEXT_PUBLIC_ASSISTANT_CHAT_URL` points to it\n- Make sure the Hono app is mounted at `/mcp-server`\n- Confirm you are calling `/functions/v1/mcp-server/mcp`\n- Restart `supabase functions serve` after changing import maps\n",
+ "type": "registry:file",
+ "target": "README.assistant.md"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/apps/ui-library/public/r/infinite-query-hook.json b/apps/ui-library/public/r/infinite-query-hook.json
index d795b7b6e1a..5ed44d9e635 100644
--- a/apps/ui-library/public/r/infinite-query-hook.json
+++ b/apps/ui-library/public/r/infinite-query-hook.json
@@ -12,7 +12,7 @@
"files": [
{
"path": "registry/default/blocks/infinite-query-hook/hooks/use-infinite-query.ts",
- "content": "'use client'\n\nimport { createClient } from '@/registry/default/fixtures/lib/supabase/client'\nimport { PostgrestQueryBuilder } from '@supabase/postgrest-js'\nimport { type SupabaseClient } from '@supabase/supabase-js'\nimport { useEffect, useRef, useSyncExternalStore } from 'react'\n\nconst supabase = createClient()\n\n// The following types are used to make the hook type-safe. It extracts the database type from the supabase client.\ntype SupabaseClientType = typeof supabase\n\n// Utility type to check if the type is any\ntype IfAny = 0 extends 1 & T ? Y : N\n\n// Extracts the database type from the supabase client. If the supabase client doesn't have a type, it will fallback properly.\ntype Database =\n SupabaseClientType extends SupabaseClient\n ? IfAny<\n U,\n {\n public: {\n Tables: Record\n Views: Record\n Functions: Record\n }\n },\n U\n >\n : {\n public: {\n Tables: Record\n Views: Record\n Functions: Record\n }\n }\n\n// Change this to the database schema you want to use\ntype DatabaseSchema = Database['public']\n\n// Extracts the table names from the database type\ntype SupabaseTableName = keyof DatabaseSchema['Tables']\n\n// Extracts the table definition from the database type\ntype SupabaseTableData = DatabaseSchema['Tables'][T]['Row']\n\ntype SupabaseSelectBuilder = ReturnType<\n PostgrestQueryBuilder['select']\n>\n\n// A function that modifies the query. Can be used to sort, filter, etc. If .range is used, it will be overwritten.\ntype SupabaseQueryHandler = (\n query: SupabaseSelectBuilder\n) => SupabaseSelectBuilder\n\ninterface UseInfiniteQueryProps {\n // The table name to query\n tableName: T\n // The columns to select, defaults to `*`\n columns?: string\n // The number of items to fetch per page, defaults to `20`\n pageSize?: number\n // A function that modifies the query. Can be used to sort, filter, etc. If .range is used, it will be overwritten.\n trailingQuery?: SupabaseQueryHandler\n}\n\ninterface StoreState {\n data: TData[]\n count: number\n isSuccess: boolean\n isLoading: boolean\n isFetching: boolean\n error: Error | null\n hasInitialFetch: boolean\n}\n\ntype Listener = () => void\n\nfunction createStore, T extends SupabaseTableName>(\n props: UseInfiniteQueryProps\n) {\n const { tableName, columns = '*', pageSize = 20, trailingQuery } = props\n\n let state: StoreState = {\n data: [],\n count: 0,\n isSuccess: false,\n isLoading: false,\n isFetching: false,\n error: null,\n hasInitialFetch: false,\n }\n\n const listeners = new Set()\n\n const notify = () => {\n listeners.forEach((listener) => listener())\n }\n\n const setState = (newState: Partial>) => {\n state = { ...state, ...newState }\n notify()\n }\n\n const fetchPage = async (skip: number) => {\n if (state.hasInitialFetch && (state.isFetching || state.count <= state.data.length)) return\n\n setState({ isFetching: true })\n\n let query = supabase\n .from(tableName)\n .select(columns, { count: 'exact' }) as unknown as SupabaseSelectBuilder\n\n if (trailingQuery) {\n query = trailingQuery(query)\n }\n const { data: newData, count, error } = await query.range(skip, skip + pageSize - 1)\n\n if (error) {\n console.error('An unexpected error occurred:', error)\n setState({ error })\n } else {\n setState({\n data: [...state.data, ...(newData as TData[])],\n count: count || 0,\n isSuccess: true,\n error: null,\n })\n }\n setState({ isFetching: false })\n }\n\n const fetchNextPage = async () => {\n if (state.isFetching) return\n await fetchPage(state.data.length)\n }\n\n const initialize = async () => {\n setState({ isLoading: true, isSuccess: false, data: [] })\n await fetchNextPage()\n setState({ isLoading: false, hasInitialFetch: true })\n }\n\n return {\n getState: () => state,\n subscribe: (listener: Listener) => {\n listeners.add(listener)\n return () => listeners.delete(listener)\n },\n fetchNextPage,\n initialize,\n }\n}\n\n// Empty initial state to avoid hydration errors.\nconst initialState: any = {\n data: [],\n count: 0,\n isSuccess: false,\n isLoading: false,\n isFetching: false,\n error: null,\n hasInitialFetch: false,\n}\n\nfunction useInfiniteQuery<\n TData extends SupabaseTableData,\n T extends SupabaseTableName = SupabaseTableName,\n>(props: UseInfiniteQueryProps) {\n const storeRef = useRef(createStore(props))\n\n const state = useSyncExternalStore(\n storeRef.current.subscribe,\n () => storeRef.current.getState(),\n () => initialState as StoreState\n )\n\n useEffect(() => {\n // Recreate store if props change\n if (\n storeRef.current.getState().hasInitialFetch &&\n (props.tableName !== props.tableName ||\n props.columns !== props.columns ||\n props.pageSize !== props.pageSize)\n ) {\n storeRef.current = createStore(props)\n }\n\n if (!state.hasInitialFetch && typeof window !== 'undefined') {\n storeRef.current.initialize()\n }\n }, [props.tableName, props.columns, props.pageSize, state.hasInitialFetch])\n\n return {\n data: state.data,\n count: state.count,\n isSuccess: state.isSuccess,\n isLoading: state.isLoading,\n isFetching: state.isFetching,\n error: state.error,\n hasMore: state.count > state.data.length,\n fetchNextPage: storeRef.current.fetchNextPage,\n }\n}\n\nexport {\n useInfiniteQuery,\n type SupabaseQueryHandler,\n type SupabaseTableData,\n type SupabaseTableName,\n type UseInfiniteQueryProps,\n}\n",
+ "content": "'use client'\n\nimport { createClient } from '@/registry/default/fixtures/lib/supabase/client'\nimport { PostgrestQueryBuilder, type PostgrestClientOptions } from '@supabase/postgrest-js'\nimport { type SupabaseClient } from '@supabase/supabase-js'\nimport { useEffect, useRef, useSyncExternalStore } from 'react'\n\nconst supabase = createClient()\n\n// The following types are used to make the hook type-safe. It extracts the database type from the supabase client.\ntype SupabaseClientType = typeof supabase\n\n// Utility type to check if the type is any\ntype IfAny = 0 extends 1 & T ? Y : N\n\n// Extracts the database type from the supabase client. If the supabase client doesn't have a type, it will fallback properly.\ntype Database =\n SupabaseClientType extends SupabaseClient\n ? IfAny<\n U,\n {\n public: {\n Tables: Record\n Views: Record\n Functions: Record\n }\n },\n U\n >\n : {\n public: {\n Tables: Record\n Views: Record\n Functions: Record\n }\n }\n\n// Change this to the database schema you want to use\ntype DatabaseSchema = Database['public']\n\n// Extracts the table names from the database type\ntype SupabaseTableName = keyof DatabaseSchema['Tables']\n\n// Extracts the table definition from the database type\ntype SupabaseTableData = DatabaseSchema['Tables'][T]['Row']\n\n// Default client options for PostgrestQueryBuilder\ntype DefaultClientOptions = PostgrestClientOptions\n\ntype SupabaseSelectBuilder = ReturnType<\n PostgrestQueryBuilder<\n DefaultClientOptions,\n DatabaseSchema,\n DatabaseSchema['Tables'][T],\n T\n >['select']\n>\n\n// A function that modifies the query. Can be used to sort, filter, etc. If .range is used, it will be overwritten.\ntype SupabaseQueryHandler = (\n query: SupabaseSelectBuilder\n) => SupabaseSelectBuilder\n\ninterface UseInfiniteQueryProps {\n // The table name to query\n tableName: T\n // The columns to select, defaults to `*`\n columns?: string\n // The number of items to fetch per page, defaults to `20`\n pageSize?: number\n // A function that modifies the query. Can be used to sort, filter, etc. If .range is used, it will be overwritten.\n trailingQuery?: SupabaseQueryHandler\n}\n\ninterface StoreState {\n data: TData[]\n count: number\n isSuccess: boolean\n isLoading: boolean\n isFetching: boolean\n error: Error | null\n hasInitialFetch: boolean\n}\n\ntype Listener = () => void\n\nfunction createStore, T extends SupabaseTableName>(\n props: UseInfiniteQueryProps\n) {\n const { tableName, columns = '*', pageSize = 20, trailingQuery } = props\n\n let state: StoreState = {\n data: [],\n count: 0,\n isSuccess: false,\n isLoading: false,\n isFetching: false,\n error: null,\n hasInitialFetch: false,\n }\n\n const listeners = new Set()\n\n const notify = () => {\n listeners.forEach((listener) => listener())\n }\n\n const setState = (newState: Partial>) => {\n state = { ...state, ...newState }\n notify()\n }\n\n const fetchPage = async (skip: number) => {\n if (state.hasInitialFetch && (state.isFetching || state.count <= state.data.length)) return\n\n setState({ isFetching: true })\n\n let query = supabase\n .from(tableName)\n .select(columns, { count: 'exact' }) as unknown as SupabaseSelectBuilder\n\n if (trailingQuery) {\n query = trailingQuery(query)\n }\n const { data: newData, count, error } = await query.range(skip, skip + pageSize - 1)\n\n if (error) {\n console.error('An unexpected error occurred:', error)\n setState({ error })\n } else {\n setState({\n data: [...state.data, ...(newData as TData[])],\n count: count || 0,\n isSuccess: true,\n error: null,\n })\n }\n setState({ isFetching: false })\n }\n\n const fetchNextPage = async () => {\n if (state.isFetching) return\n await fetchPage(state.data.length)\n }\n\n const initialize = async () => {\n setState({ isLoading: true, isSuccess: false, data: [] })\n await fetchNextPage()\n setState({ isLoading: false, hasInitialFetch: true })\n }\n\n return {\n getState: () => state,\n subscribe: (listener: Listener) => {\n listeners.add(listener)\n return () => listeners.delete(listener)\n },\n fetchNextPage,\n initialize,\n }\n}\n\n// Empty initial state to avoid hydration errors.\nconst initialState: any = {\n data: [],\n count: 0,\n isSuccess: false,\n isLoading: false,\n isFetching: false,\n error: null,\n hasInitialFetch: false,\n}\n\nfunction useInfiniteQuery<\n TData extends SupabaseTableData,\n T extends SupabaseTableName = SupabaseTableName,\n>(props: UseInfiniteQueryProps) {\n const storeRef = useRef(createStore(props))\n\n const state = useSyncExternalStore(\n storeRef.current.subscribe,\n () => storeRef.current.getState(),\n () => initialState as StoreState\n )\n\n useEffect(() => {\n // Recreate store if props change\n if (\n storeRef.current.getState().hasInitialFetch &&\n (props.tableName !== props.tableName ||\n props.columns !== props.columns ||\n props.pageSize !== props.pageSize)\n ) {\n storeRef.current = createStore(props)\n }\n\n if (!state.hasInitialFetch && typeof window !== 'undefined') {\n storeRef.current.initialize()\n }\n }, [props.tableName, props.columns, props.pageSize, state.hasInitialFetch])\n\n return {\n data: state.data,\n count: state.count,\n isSuccess: state.isSuccess,\n isLoading: state.isLoading,\n isFetching: state.isFetching,\n error: state.error,\n hasMore: state.count > state.data.length,\n fetchNextPage: storeRef.current.fetchNextPage,\n }\n}\n\nexport {\n useInfiniteQuery,\n type SupabaseQueryHandler,\n type SupabaseTableData,\n type SupabaseTableName,\n type UseInfiniteQueryProps,\n}\n",
"type": "registry:hook"
}
]
diff --git a/apps/ui-library/public/r/registry.json b/apps/ui-library/public/r/registry.json
index f541beefd3d..4e25aa1d464 100644
--- a/apps/ui-library/public/r/registry.json
+++ b/apps/ui-library/public/r/registry.json
@@ -1417,6 +1417,82 @@
}
]
},
+ {
+ "name": "assistant",
+ "type": "registry:block",
+ "title": "AI Assistant with Database Query",
+ "description": "AI chatbot widget with database query capabilities through MCP (Model Context Protocol)",
+ "registryDependencies": [
+ "button",
+ "input",
+ "scroll-area",
+ "@ai-elements/conversation",
+ "@ai-elements/message",
+ "@ai-elements/prompt-input",
+ "@ai-elements/model-selector",
+ "@ai-elements/reasoning",
+ "@ai-elements/sources",
+ "@ai-elements/suggestion"
+ ],
+ "dependencies": [
+ "ai@latest",
+ "@ai-sdk/react@latest",
+ "@ai-sdk/openai@latest",
+ "@ai-sdk/mcp@latest",
+ "@supabase/ssr@latest",
+ "@supabase/supabase-js@latest",
+ "zod@latest",
+ "nanoid@latest",
+ "sonner@latest",
+ "lucide-react@latest"
+ ],
+ "files": [
+ {
+ "path": "registry/default/blocks/assistant/components/assistant-widget.tsx",
+ "type": "registry:component"
+ },
+ {
+ "path": "registry/default/blocks/assistant/lib/supabase/server.ts",
+ "type": "registry:file",
+ "target": "lib/supabase/server.ts"
+ },
+ {
+ "path": "registry/default/blocks/assistant/lib/supabase/client.ts",
+ "type": "registry:file",
+ "target": "lib/supabase/client.ts"
+ },
+ {
+ "path": "registry/default/blocks/assistant/lib/supabase/middleware.ts",
+ "type": "registry:file",
+ "target": "lib/supabase/middleware.ts"
+ },
+ {
+ "path": "registry/default/blocks/assistant/supabase/functions/mcp-server/index.ts",
+ "type": "registry:file",
+ "target": "supabase/functions/mcp-server/index.ts"
+ },
+ {
+ "path": "registry/default/blocks/assistant/supabase/functions/mcp-server/deno.json",
+ "type": "registry:file",
+ "target": "supabase/functions/mcp-server/deno.json"
+ },
+ {
+ "path": "registry/default/blocks/assistant/supabase/functions/chat/index.ts",
+ "type": "registry:file",
+ "target": "supabase/functions/chat/index.ts"
+ },
+ {
+ "path": "registry/default/blocks/assistant/supabase/functions/chat/deno.json",
+ "type": "registry:file",
+ "target": "supabase/functions/chat/deno.json"
+ },
+ {
+ "path": "registry/default/blocks/assistant/README.md",
+ "type": "registry:file",
+ "target": "README.assistant.md"
+ }
+ ]
+ },
{
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
"name": "supabase-client-nextjs",
diff --git a/apps/ui-library/registry/blocks.ts b/apps/ui-library/registry/blocks.ts
index fc993d76be1..668dc24e343 100644
--- a/apps/ui-library/registry/blocks.ts
+++ b/apps/ui-library/registry/blocks.ts
@@ -5,6 +5,7 @@ import { registryItemAppend } from './utils'
import currentUserAvatar from './default/blocks/current-user-avatar/registry-item.json' with { type: 'json' }
import dropzone from './default/blocks/dropzone/registry-item.json' with { type: 'json' }
import infiniteQueryHook from './default/blocks/infinite-query-hook/registry-item.json' with { type: 'json' }
+import assistant from './default/blocks/assistant/registry-item.json' with { type: 'json' }
import realtimeAvatarStack from './default/blocks/realtime-avatar-stack/registry-item.json' with { type: 'json' }
import realtimeChat from './default/blocks/realtime-chat/registry-item.json' with { type: 'json' }
import realtimeCursor from './default/blocks/realtime-cursor/registry-item.json' with { type: 'json' }
@@ -54,4 +55,5 @@ export const blocks = [
...combine(realtimeChat as RegistryItem),
// infinite query hook is intentionally not combined with the clients since it depends on clients having database types.
infiniteQueryHook as RegistryItem,
+ assistant as RegistryItem,
] as RegistryItem[]
diff --git a/apps/ui-library/registry/default/blocks/assistant/README.md b/apps/ui-library/registry/default/blocks/assistant/README.md
index 90fe59db920..2b9f99db4ae 100644
--- a/apps/ui-library/registry/default/blocks/assistant/README.md
+++ b/apps/ui-library/registry/default/blocks/assistant/README.md
@@ -17,7 +17,7 @@ An AI-powered chatbot widget with database query capabilities using the Model Co
The block consists of four main pieces that work together end to end:
1. **Assistant Widget** (`components/assistant-widget.tsx`): React component providing the chat UI, built with AI SDK Elements
-2. **Chat API Route** (`app/api/chat/route.ts`): Next.js API route handling chat requests with MCP integration
+2. **Chat Edge Function** (`supabase/functions/chat/`): Supabase Edge Function that streams chat completions, bootstraps the MCP client, and is invoked directly from the browser
3. **Hono MCP Server** (`supabase/functions/mcp-server/`): Supabase Edge Function exposing MCP tools (e.g. `queryDatabase`) via the `mcp-lite` transport
4. **Supabase Client Utilities** (`lib/supabase/`): Server and browser helpers for authenticated Supabase access
@@ -67,8 +67,11 @@ OPENAI_API_KEY=your_openai_api_key
NEXT_PUBLIC_SUPABASE_URL=your_supabase_url
NEXT_PUBLIC_SUPABASE_ANON_KEY=your_supabase_anon_key
-# MCP Server URL (update after deploying edge function)
-NEXT_PUBLIC_MCP_SERVER_URL=https://your-project.supabase.co/functions/v1/mcp-server/mcp
+# Chat Edge Function (used by the browser)
+NEXT_PUBLIC_ASSISTANT_CHAT_URL=https://your-project.supabase.co/functions/v1/chat
+
+# MCP Server URL (consumed by the edge functions)
+MCP_SERVER_URL=https://your-project.supabase.co/functions/v1/mcp-server
```
### 4. Build and Deploy the Hono MCP Server
@@ -145,7 +148,23 @@ Your MCP endpoint will be available at `http://localhost:54321/functions/v1/mcp-
supabase functions deploy --no-verify-jwt mcp-server
```
-### 5. Add the Widget to Your App
+### 5. Build and Deploy the Chat Edge Function
+
+The chat edge function (located at `supabase/functions/chat/`) replaces the `/api/chat` Next.js route and is what the browser calls directly.
+
+1. Confirm the provided `deno.json` contains the AI + MCP dependencies required by the block.
+2. Run `supabase start` if you haven't already.
+3. Serve **both** edge functions in separate terminals so the MCP server and chat handler run side by side:
+
+```bash
+supabase functions serve --env-file .env.local --no-verify-jwt mcp-server
+supabase functions serve --env-file .env.local --no-verify-jwt chat
+```
+
+4. For local development, set `NEXT_PUBLIC_ASSISTANT_CHAT_URL=http://127.0.0.1:54321/functions/v1/chat`. In production, point it to `https://.supabase.co/functions/v1/chat`.
+5. Ensure `MCP_SERVER_URL` is available to the chat function (either via the Supabase dashboard or an `.env` file passed to `supabase functions serve`) so it can reach the MCP server endpoint.
+
+### 6. Add the Widget to Your App
```tsx
import { AssistantWidget } from "@/components/assistant-widget";
@@ -164,6 +183,7 @@ export default function Layout({ children }) {
- Start Supabase locally: `supabase start`
- Serve and iterate on the MCP function: `supabase functions serve --no-verify-jwt mcp-server`
+- Run the chat edge function alongside it: `supabase functions serve --no-verify-jwt chat`
- Test with Claude Code: `claude mcp add my-mcp-server -t http http://localhost:54321/functions/v1/mcp-server/mcp`
- Inspect traffic using the MCP inspector: `npx @modelcontextprotocol/inspector`
- When everything looks good, deploy via `supabase functions deploy --no-verify-jwt mcp-server`
@@ -338,7 +358,7 @@ Inside your tools you can then rely on `ctx.state.user` (or whatever you set) ra
### Change the AI Model
-Edit `app/api/chat/route.ts`:
+Edit `supabase/functions/chat/index.ts`:
```ts
const result = await streamText({
@@ -349,7 +369,7 @@ const result = await streamText({
### Customize the System Prompt
-Edit the `system` parameter in `app/api/chat/route.ts` to change the assistant's behavior.
+Edit the `system` parameter in `supabase/functions/chat/index.ts` to change the assistant's behavior.
### Style the Widget
@@ -406,6 +426,7 @@ mcp.tool("myCustomTool", {
### "Function not found" or 404
+- Verify the chat function is deployed at `/functions/v1/chat` and `NEXT_PUBLIC_ASSISTANT_CHAT_URL` points to it
- Make sure the Hono app is mounted at `/mcp-server`
- Confirm you are calling `/functions/v1/mcp-server/mcp`
- Restart `supabase functions serve` after changing import maps
diff --git a/apps/ui-library/registry/default/blocks/assistant/app/api/chat/route.ts b/apps/ui-library/registry/default/blocks/assistant/app/api/chat/route.ts
deleted file mode 100644
index 1d049fe8d6a..00000000000
--- a/apps/ui-library/registry/default/blocks/assistant/app/api/chat/route.ts
+++ /dev/null
@@ -1,113 +0,0 @@
-import { openai } from "@ai-sdk/openai";
-import { streamText } from "ai";
-import { experimental_createMCPClient as createMCPClient } from "@ai-sdk/mcp";
-import { z } from "zod";
-import { createClient } from "@/registry/default/blocks/assistant/lib/supabase/server";
-
-export const maxDuration = 60;
-
-export async function POST(req: Request) {
- try {
- const { messages } = await req.json();
-
- // Get Supabase auth session
- const supabase = await createClient();
-
- const {
- data: { session },
- } = await supabase.auth.getSession();
-
- if (!session) {
- return new Response("Unauthorized", { status: 401 });
- }
-
- // Initialize MCP client with auth token
- const mcpServerUrl =
- process.env.NEXT_PUBLIC_MCP_SERVER_URL ||
- "http://localhost:54321/functions/v1/mcp-server";
-
- const mcpClient = await createMCPClient({
- transport: {
- type: "http",
- url: mcpServerUrl,
- headers: {
- Authorization: `Bearer ${session.access_token}`,
- },
- },
- });
-
- // Define MCP tools with schemas for type safety
- const tools = await mcpClient.tools({
- schemas: {
- queryDatabase: {
- inputSchema: z.object({
- table: z.string().describe("The name of the table to query"),
- columns: z
- .string()
- .optional()
- .describe("Columns to select (comma-separated). Defaults to '*' for all columns."),
- filters: z
- .array(
- z.object({
- column: z.string().describe("The column to filter on"),
- operator: z.enum(["eq", "neq", "gt", "gte", "lt", "lte", "like", "ilike", "in", "is"]).describe("The filter operator"),
- value: z.any().describe("The value to filter by"),
- })
- )
- .optional()
- .describe("Array of filter conditions to apply"),
- limit: z
- .number()
- .optional()
- .describe("Maximum number of rows to return"),
- orderBy: z
- .object({
- column: z.string().describe("Column to order by"),
- ascending: z.boolean().optional().describe("Sort order (default: true)"),
- })
- .optional()
- .describe("Ordering options"),
- explanation: z
- .string()
- .optional()
- .describe("Optional explanation of what the query does"),
- }),
- },
- },
- });
-
- // Stream the AI response with MCP tools
- const result = await streamText({
- model: openai("gpt-4o"),
- messages,
- tools,
- system: `You are a helpful AI assistant with access to a database.
-You can help users query their database using natural language.
-Use the queryDatabase tool to fetch data from tables with filters and options.
-Always provide clear explanations of what the query does.
-The database respects Row Level Security (RLS), so users can only access their own data.
-When building queries:
-- Use the table parameter to specify which table to query
-- Use filters array to add conditions (eq, neq, gt, gte, lt, lte, like, ilike, in, is)
-- Use limit to control the number of results
-- Use orderBy to sort results by a specific column`,
- onFinish: async () => {
- await mcpClient.close();
- },
- });
-
- return result.toDataStreamResponse();
- } catch (error) {
- console.error("Chat API error:", error);
- return new Response(
- JSON.stringify({
- error: "Failed to process chat request",
- details: error instanceof Error ? error.message : String(error),
- }),
- {
- status: 500,
- headers: { "Content-Type": "application/json" },
- }
- );
- }
-}
diff --git a/apps/ui-library/registry/default/blocks/assistant/components/assistant-widget.tsx b/apps/ui-library/registry/default/blocks/assistant/components/assistant-widget.tsx
index cd307ba614f..de61e6763fe 100644
--- a/apps/ui-library/registry/default/blocks/assistant/components/assistant-widget.tsx
+++ b/apps/ui-library/registry/default/blocks/assistant/components/assistant-widget.tsx
@@ -1,20 +1,36 @@
-"use client";
+'use client'
-import { useEffect, useMemo, useState } from "react";
-import { useChat } from "@ai-sdk/react";
-import type { Message as ChatMessage } from "ai";
+import { cn } from '@/lib/utils'
+import { createClient } from '@/registry/default/blocks/assistant/lib/supabase/client'
import {
Conversation,
ConversationContent,
ConversationScrollButton,
-} from "@/components/ai-elements/conversation";
+} from '@/registry/default/components/ai-elements/conversation'
import {
+ Message,
MessageBranch,
MessageBranchContent,
- Message,
+ MessageBranchNext,
+ MessageBranchPage,
+ MessageBranchPrevious,
+ MessageBranchSelector,
MessageContent,
MessageResponse,
-} from "@/components/ai-elements/message";
+} from '@/registry/default/components/ai-elements/message'
+import {
+ ModelSelector,
+ ModelSelectorContent,
+ ModelSelectorEmpty,
+ ModelSelectorGroup,
+ ModelSelectorInput,
+ ModelSelectorItem,
+ ModelSelectorList,
+ ModelSelectorLogo,
+ ModelSelectorLogoGroup,
+ ModelSelectorName,
+ ModelSelectorTrigger,
+} from '@/registry/default/components/ai-elements/model-selector'
import {
PromptInput,
PromptInputActionAddAttachments,
@@ -31,215 +47,250 @@ import {
PromptInputSubmit,
PromptInputTextarea,
PromptInputTools,
-} from "@/components/ai-elements/prompt-input";
-import {
- ModelSelector,
- ModelSelectorContent,
- ModelSelectorEmpty,
- ModelSelectorGroup,
- ModelSelectorInput,
- ModelSelectorItem,
- ModelSelectorList,
- ModelSelectorLogo,
- ModelSelectorLogoGroup,
- ModelSelectorName,
- ModelSelectorTrigger,
-} from "@/components/ai-elements/model-selector";
-import {
- Sources,
- SourcesContent,
- SourcesTrigger,
- Source,
-} from "@/components/ai-elements/sources";
+} from '@/registry/default/components/ai-elements/prompt-input'
import {
Reasoning,
ReasoningContent,
ReasoningTrigger,
-} from "@/components/ai-elements/reasoning";
-import { Suggestions, Suggestion } from "@/components/ai-elements/suggestion";
-import { Button } from "@/components/ui/button";
-import { cn } from "@/lib/utils";
+} from '@/registry/default/components/ai-elements/reasoning'
import {
- CheckIcon,
- GlobeIcon,
- MessageSquare,
- MicIcon,
- Minimize2,
- X,
- Loader2,
-} from "lucide-react";
-import { toast } from "sonner";
+ Source,
+ Sources,
+ SourcesContent,
+ SourcesTrigger,
+} from '@/registry/default/components/ai-elements/sources'
+import { Suggestion, Suggestions } from '@/registry/default/components/ai-elements/suggestion'
+import { Button } from '@/registry/default/components/ui/button'
+import { useChat } from '@ai-sdk/react'
+import { DefaultChatTransport } from 'ai'
+import { GlobeIcon, MessageSquare, MicIcon, Minimize2, X } from 'lucide-react'
+import { useCallback, useMemo, useState } from 'react'
+import { toast } from 'sonner'
const suggestions = [
- "Show me my recent orders",
- "How many active users do we have?",
- "List all tasks due this week",
+ 'Show me my recent orders',
+ 'How many active users do we have?',
+ 'List all tasks due this week',
"Summarize today's signups",
-];
+]
const models = [
{
- id: "gpt-4o",
- name: "GPT-4o",
- chef: "OpenAI",
- chefSlug: "openai",
- providers: ["openai", "azure"],
+ id: 'gpt-4o',
+ name: 'GPT-4o',
+ chef: 'OpenAI',
+ chefSlug: 'openai',
+ providers: ['openai', 'azure'],
},
{
- id: "gpt-4o-mini",
- name: "GPT-4o Mini",
- chef: "OpenAI",
- chefSlug: "openai",
- providers: ["openai", "azure"],
+ id: 'gpt-4o-mini',
+ name: 'GPT-4o Mini',
+ chef: 'OpenAI',
+ chefSlug: 'openai',
+ providers: ['openai', 'azure'],
},
{
- id: "claude-sonnet-4-20250514",
- name: "Claude 4 Sonnet",
- chef: "Anthropic",
- chefSlug: "anthropic",
- providers: ["anthropic", "azure", "google", "amazon-bedrock"],
+ id: 'claude-sonnet-4-20250514',
+ name: 'Claude 4 Sonnet',
+ chef: 'Anthropic',
+ chefSlug: 'anthropic',
+ providers: ['anthropic', 'azure', 'google', 'amazon-bedrock'],
},
-];
+ {
+ id: 'gemini-2.0-flash-exp',
+ name: 'Gemini 2.0 Flash',
+ chef: 'Google',
+ chefSlug: 'google',
+ providers: ['google'],
+ },
+]
-type ComposerStatus = "ready" | "submitted" | "streaming" | "error";
+type TextPart = {
+ type: 'text'
+ text: string
+}
-type MessageMetadata = {
- sources?: { href: string; title: string }[];
- reasoning?: {
- content: string;
- duration: number;
- };
-};
+type SourcePart = {
+ type: 'source-url'
+ url: string
+ title?: string
+}
-type ToolInvocationItem = NonNullable[number];
+type ReasoningPart = {
+ type: 'reasoning'
+ text: string
+}
-const getMessageText = (content: ChatMessage["content"]) => {
- if (typeof content === "string") {
- return content;
- }
+type FormattedMessage = {
+ key: string
+ from: 'user' | 'assistant'
+ sources?: { href: string; title: string }[]
+ versions: { id: string; content: string }[]
+ reasoning?: { content: string; duration?: number }
+}
- return content
- .map((part) => (part.type === "text" ? part.text : ""))
- .join("")
- .trim();
-};
+const SUPABASE_URL = process.env.NEXT_PUBLIC_SUPABASE_URL?.replace(/\/$/, '')
+const DEFAULT_CHAT_API = SUPABASE_URL ? `${SUPABASE_URL}/functions/v1/chat` : '/functions/v1/chat'
+const CHAT_API_URL = 'https://idglwaxxhycmeyjvbbbr.supabase.co/functions/v1/chat'
export function AssistantWidget() {
- const [isOpen, setIsOpen] = useState(false);
- const [isMinimized, setIsMinimized] = useState(false);
- const [text, setText] = useState("");
- const [status, setStatus] = useState("ready");
- const [model, setModel] = useState(models[0].id);
- const [modelSelectorOpen, setModelSelectorOpen] = useState(false);
- const [useWebSearch, setUseWebSearch] = useState(false);
- const [useMicrophone, setUseMicrophone] = useState(false);
+ const supabase = useMemo(() => createClient(), [])
+ const getAuthHeaders = useCallback(async () => {
+ let {
+ data: { session },
+ error: sessionError,
+ } = await supabase.auth.getSession()
- const { messages, append, isLoading, error } = useChat({
- api: "/api/chat",
+ if (sessionError) {
+ throw sessionError
+ }
+
+ if (!session) {
+ const { data, error } = await supabase.auth.signInAnonymously()
+
+ if (error || !data.session) {
+ throw error ?? new Error('Unable to authenticate anonymously.')
+ }
+
+ session = data.session
+ }
+
+ return {
+ Authorization: `Bearer ${session.access_token}`,
+ }
+ }, [supabase])
+
+ const [isOpen, setIsOpen] = useState(false)
+ const [isMinimized, setIsMinimized] = useState(false)
+ const [text, setText] = useState('')
+ const [model, setModel] = useState(models[0].id)
+ const [modelSelectorOpen, setModelSelectorOpen] = useState(false)
+ const [useWebSearch, setUseWebSearch] = useState(false)
+ const [useMicrophone, setUseMicrophone] = useState(false)
+
+ const { messages, sendMessage, status, error } = useChat({
+ transport: new DefaultChatTransport({
+ api: CHAT_API_URL,
+ }),
onError: (chatError) => {
- toast.error("Failed to send message", {
+ toast.error('Failed to send message', {
description: chatError.message,
- });
- setStatus("error");
+ })
},
- });
+ })
- useEffect(() => {
- if (isLoading) {
- setStatus("streaming");
- return;
- }
+ const selectedModelData = models.find((entry) => entry.id === model)
- if (!isLoading && status === "streaming") {
- setStatus("ready");
- }
- }, [isLoading, status]);
+ const formattedMessages = useMemo(() => {
+ return messages.map((message) => {
+ const parts = message.parts ?? []
- useEffect(() => {
- if (!error && status === "error") {
- setStatus("ready");
- }
- }, [error, status]);
+ const textParts =
+ parts.filter((part): part is TextPart => part?.type === 'text') ??
+ (Array.isArray(message.content)
+ ? message.content.filter(
+ (part): part is TextPart => typeof part === 'object' && part.type === 'text'
+ )
+ : [])
- const selectedModel = useMemo(
- () => models.find((entry) => entry.id === model),
- [model]
- );
+ const versions =
+ textParts.length > 0
+ ? textParts.map((part, index) => ({
+ id: `${message.id}-${index}`,
+ content: part.text,
+ }))
+ : [
+ {
+ id: `${message.id}-content`,
+ content:
+ typeof message.content === 'string'
+ ? message.content
+ : message.content
+ ?.map((part) =>
+ typeof part === 'string' ? part : part.type === 'text' ? part.text : ''
+ )
+ .join('')
+ .trim() ?? '',
+ },
+ ]
+
+ const sources = parts
+ .filter((part): part is SourcePart => part?.type === 'source-url')
+ .map((part) => ({
+ href: part.url,
+ title: part.title ?? part.url,
+ }))
+
+ const reasoningPart = parts.find((part): part is ReasoningPart => part?.type === 'reasoning')
+
+ return {
+ key: message.id,
+ from: message.role === 'assistant' ? 'assistant' : 'user',
+ sources,
+ versions,
+ reasoning: reasoningPart
+ ? {
+ content: reasoningPart.text,
+ }
+ : undefined,
+ }
+ })
+ }, [messages])
const handleSubmit = async (message: PromptInputMessage) => {
- const hasText = Boolean(message.text?.trim());
- const hasAttachments = Boolean(message.files?.length);
+ const hasText = Boolean(message.text?.trim())
+ const hasAttachments = Boolean(message.files?.length)
- if (!(hasText || hasAttachments) || isLoading) {
- return;
+ if (!(hasText || hasAttachments) || status === 'streaming') {
+ return
}
- setStatus("submitted");
-
if (message.files?.length) {
- toast.success("Files attached", {
+ toast.success('Files attached', {
description: `${message.files.length} file(s) attached to message`,
- });
+ })
}
try {
- await append({
- role: "user",
- content: message.text?.trim() ?? "Sent with attachments",
- metadata: {
- model,
- useWebSearch,
- useMicrophone,
+ const headers = await getAuthHeaders()
+ await sendMessage(
+ {
+ text: message.text?.trim() ?? 'Sent with attachments',
+ files: message.files,
},
- });
- } catch (submissionError) {
- console.error(submissionError);
- setStatus("error");
- return;
+ {
+ headers,
+ body: {
+ model,
+ webSearch: useWebSearch,
+ },
+ }
+ )
+ setText('')
+ } catch (sendError) {
+ toast.error('Failed to send message', {
+ description: sendError instanceof Error ? sendError.message : 'Unknown error occurred.',
+ })
}
-
- setText("");
- };
+ }
const handleSuggestionClick = async (suggestion: string) => {
- if (isLoading) return;
- setStatus("submitted");
+ if (status === 'streaming' || status === 'submitted') return
try {
- await append({ role: "user", content: suggestion });
- } catch (suggestionError) {
- console.error(suggestionError);
- setStatus("error");
+ const headers = await getAuthHeaders()
+ await sendMessage(
+ { text: suggestion },
+ {
+ headers,
+ body: { model, webSearch: useWebSearch },
+ }
+ )
+ } catch (sendError) {
+ toast.error('Failed to send message', {
+ description: sendError instanceof Error ? sendError.message : 'Unknown error occurred.',
+ })
}
- };
-
- const renderToolInvocation = (tool: ToolInvocationItem) => {
- if (!tool) return null;
-
- return (
-