From 7f68b00a74c30872561da34face2f0d4076033cb Mon Sep 17 00:00:00 2001 From: Saxon Fletcher Date: Fri, 21 Nov 2025 15:25:00 +1000 Subject: [PATCH] remove files --- .../content/docs/assistant-chat.mdx | 9 - apps/ui-library/public/r/assistant.json | 103 ++++- apps/ui-library/public/r/registry.json | 77 +++- .../default/blocks/assistant/README.md | 432 ------------------ .../blocks/assistant/registry-item.json | 5 - 5 files changed, 168 insertions(+), 458 deletions(-) delete mode 100644 apps/ui-library/registry/default/blocks/assistant/README.md diff --git a/apps/ui-library/content/docs/assistant-chat.mdx b/apps/ui-library/content/docs/assistant-chat.mdx index 6fe01416d6f..e56aa72e516 100644 --- a/apps/ui-library/content/docs/assistant-chat.mdx +++ b/apps/ui-library/content/docs/assistant-chat.mdx @@ -46,15 +46,6 @@ The bundle contains four key pieces: Store these values in `.env.local` for the app and in a `.env` file that you pass to `supabase functions serve`. -## Running locally - -1. Start Supabase: `supabase start` -2. Serve the MCP server: `supabase functions serve --no-verify-jwt --env-file .env.local mcp-server` -3. Serve the chat bridge: `supabase functions serve --no-verify-jwt --env-file .env.local chat` -4. Set `NEXT_PUBLIC_ASSISTANT_CHAT_URL=http://127.0.0.1:54321/functions/v1/chat` so the widget hits your local function. - -Both functions need to be running so that the Edge Function can call into the MCP server while streaming results back to the browser. - ## Adding the widget Place the widget close to your root layout so it is loaded once per page. The assistant automatically renders a floating toggle button and enforces Supabase authentication before sending requests. diff --git a/apps/ui-library/public/r/assistant.json b/apps/ui-library/public/r/assistant.json index 35752823355..160b8b2ce1d 100644 --- a/apps/ui-library/public/r/assistant.json +++ b/apps/ui-library/public/r/assistant.json @@ -14,12 +14,15 @@ "zod@latest", "nanoid@latest", "sonner@latest", - "lucide-react@latest" + "lucide-react@latest", + "recharts@latest" ], "registryDependencies": [ "button", "input", + "popover", "scroll-area", + "chart", "@ai-elements/conversation", "@ai-elements/message", "@ai-elements/prompt-input", @@ -31,7 +34,17 @@ "files": [ { "path": "registry/default/blocks/assistant/components/assistant-widget.tsx", - "content": "'use client'\n\nimport { cn } from '@/lib/utils'\nimport {\n Conversation,\n ConversationContent,\n ConversationScrollButton,\n} from '@/registry/default/components/ai-elements/conversation'\nimport {\n Message,\n MessageBranch,\n MessageBranchContent,\n MessageContent,\n MessageResponse,\n} from '@/registry/default/components/ai-elements/message'\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 '@/registry/default/components/ai-elements/model-selector'\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 '@/registry/default/components/ai-elements/prompt-input'\nimport {\n Reasoning,\n ReasoningContent,\n ReasoningTrigger,\n} from '@/registry/default/components/ai-elements/reasoning'\nimport {\n Source,\n Sources,\n SourcesContent,\n SourcesTrigger,\n} from '@/registry/default/components/ai-elements/sources'\nimport { Suggestion, Suggestions } from '@/registry/default/components/ai-elements/suggestion'\nimport { Button } from '@/registry/default/components/ui/button'\nimport { useChat } from '@ai-sdk/react'\nimport type { Message as ChatMessage } from 'ai'\nimport { CheckIcon, GlobeIcon, Loader2, MessageSquare, MicIcon, Minimize2, X } from 'lucide-react'\nimport { useCallback, useEffect, useMemo, useState } from 'react'\nimport { toast } from 'sonner'\nimport { createClient } from '@/registry/default/blocks/assistant/lib/supabase/client'\n\nconst suggestions = [\n 'Show me my recent orders',\n 'How many active users do we have?',\n 'List all tasks due this week',\n \"Summarize today's signups\",\n]\n\nconst models = [\n {\n id: 'gpt-4o',\n name: 'GPT-4o',\n chef: 'OpenAI',\n chefSlug: 'openai',\n providers: ['openai', 'azure'],\n },\n {\n id: 'gpt-4o-mini',\n name: 'GPT-4o Mini',\n chef: 'OpenAI',\n chefSlug: 'openai',\n providers: ['openai', 'azure'],\n },\n {\n id: 'claude-sonnet-4-20250514',\n name: 'Claude 4 Sonnet',\n chef: 'Anthropic',\n chefSlug: 'anthropic',\n providers: ['anthropic', 'azure', 'google', 'amazon-bedrock'],\n },\n]\n\ntype ComposerStatus = 'ready' | 'submitted' | 'streaming' | 'error'\n\ntype MessageMetadata = {\n sources?: { href: string; title: string }[]\n reasoning?: {\n content: string\n duration: number\n }\n}\n\ntype ToolInvocationItem = NonNullable[number]\n\nconst getMessageText = (content: ChatMessage['content']) => {\n if (typeof content === 'string') {\n return content\n }\n\n return content\n .map((part) => (part.type === 'text' ? part.text : ''))\n .join('')\n .trim()\n}\n\nconst SUPABASE_URL = process.env.NEXT_PUBLIC_SUPABASE_URL?.replace(/\\/$/, '')\nconst DEFAULT_CHAT_API = SUPABASE_URL ? `${SUPABASE_URL}/functions/v1/chat` : '/functions/v1/chat'\nconst CHAT_API_URL = process.env.NEXT_PUBLIC_ASSISTANT_CHAT_URL ?? DEFAULT_CHAT_API\n\nexport function AssistantWidget() {\n const supabase = useMemo(() => createClient(), [])\n const fetchWithAuth = useCallback(\n async (\n input: Parameters[0],\n init?: Parameters[1]\n ): Promise => {\n const {\n data: { session },\n error: sessionError,\n } = await supabase.auth.getSession()\n\n if (sessionError || !session) {\n throw new Error('You must be signed in to chat with the assistant.')\n }\n\n const headers = new Headers(init?.headers ?? {})\n headers.set('Authorization', `Bearer ${session.access_token}`)\n\n return fetch(input, {\n ...init,\n headers,\n })\n },\n [supabase]\n )\n const [isOpen, setIsOpen] = useState(false)\n const [isMinimized, setIsMinimized] = useState(false)\n const [text, setText] = useState('')\n const [status, setStatus] = useState('ready')\n const [model, setModel] = useState(models[0].id)\n const [modelSelectorOpen, setModelSelectorOpen] = useState(false)\n const [useWebSearch, setUseWebSearch] = useState(false)\n const [useMicrophone, setUseMicrophone] = useState(false)\n\n const { messages, append, isLoading, error } = useChat({\n api: CHAT_API_URL,\n fetch: fetchWithAuth,\n onError: (chatError) => {\n toast.error('Failed to send message', {\n description: chatError.message,\n })\n setStatus('error')\n },\n })\n\n useEffect(() => {\n if (isLoading) {\n setStatus('streaming')\n return\n }\n\n if (!isLoading && status === 'streaming') {\n setStatus('ready')\n }\n }, [isLoading, status])\n\n useEffect(() => {\n if (!error && status === 'error') {\n setStatus('ready')\n }\n }, [error, status])\n\n const selectedModel = useMemo(() => models.find((entry) => entry.id === model), [model])\n\n const handleSubmit = async (message: PromptInputMessage) => {\n const hasText = Boolean(message.text?.trim())\n const hasAttachments = Boolean(message.files?.length)\n\n if (!(hasText || hasAttachments) || isLoading) {\n return\n }\n\n setStatus('submitted')\n\n if (message.files?.length) {\n toast.success('Files attached', {\n description: `${message.files.length} file(s) attached to message`,\n })\n }\n\n try {\n await append({\n role: 'user',\n content: message.text?.trim() ?? 'Sent with attachments',\n metadata: {\n model,\n useWebSearch,\n useMicrophone,\n },\n })\n } catch (submissionError) {\n console.error(submissionError)\n setStatus('error')\n return\n }\n\n setText('')\n }\n\n const handleSuggestionClick = async (suggestion: string) => {\n if (isLoading) return\n setStatus('submitted')\n try {\n await append({ role: 'user', content: suggestion })\n } catch (suggestionError) {\n console.error(suggestionError)\n setStatus('error')\n }\n }\n\n const renderToolInvocation = (tool: ToolInvocationItem) => {\n if (!tool) return null\n\n return (\n
\n
{tool.toolName}
\n {tool.state === 'call' && (\n
\n \n Running…\n
\n )}\n {tool.state === 'result' && (\n
\n {typeof tool.result === 'string' ? tool.result : JSON.stringify(tool.result, null, 2)}\n
\n )}\n {tool.state === 'error' &&
{tool.error}
}\n
\n )\n }\n\n if (!isOpen) {\n return (\n setIsOpen(true)}\n className=\"fixed bottom-6 right-6 z-50 flex h-14 w-14 items-center justify-center rounded-full bg-primary text-primary-foreground shadow-lg transition-all hover:bg-primary/90\"\n aria-label=\"Open AI Assistant\"\n >\n \n \n )\n }\n\n return (\n \n
\n
\n \n
\n

AI Assistant

\n

Powered by MCP

\n
\n
\n
\n setIsMinimized((prev) => !prev)}\n >\n \n \n \n
\n
\n\n {!isMinimized && (\n <>\n
\n \n \n {messages.length === 0 && (\n
\n

Hi! I'm your AI assistant.

\n

I can help you query your data using natural language.

\n
\n )}\n {messages.map((message) => {\n const content = getMessageText(message.content)\n const hasTools = Boolean(message.toolInvocations?.length)\n const metadata = message.metadata as MessageMetadata | undefined\n\n return (\n \n \n \n
\n {metadata?.sources?.length ? (\n \n \n \n {metadata.sources.map(\n (source: { href: string; title: string }) => (\n \n )\n )}\n \n \n ) : null}\n\n {metadata?.reasoning && (\n \n \n {metadata.reasoning.content}\n \n )}\n\n \n {content}\n \n\n {hasTools && (\n
\n {message.toolInvocations?.map((tool) => renderToolInvocation(tool))}\n
\n )}\n
\n
\n
\n
\n )\n })}\n {isLoading && (\n
\n \n
\n )}\n
\n \n
\n
\n\n
\n \n {suggestions.map((suggestion) => (\n handleSuggestionClick(suggestion)}\n />\n ))}\n \n
\n \n \n \n {(attachment) => }\n \n \n \n setText(event.target.value)}\n placeholder=\"Ask me anything about your Supabase data...\"\n />\n \n \n \n \n \n \n \n \n \n setUseMicrophone((prev) => !prev)}\n variant={useMicrophone ? 'default' : 'ghost'}\n type=\"button\"\n >\n \n Toggle microphone\n \n setUseWebSearch((prev) => !prev)}\n variant={useWebSearch ? 'default' : 'ghost'}\n type=\"button\"\n >\n \n Toggle web search\n \n \n \n \n {selectedModel?.chefSlug && (\n \n )}\n {selectedModel?.name && (\n {selectedModel.name}\n )}\n \n \n \n \n \n No models found.\n {['OpenAI', 'Anthropic'].map((chef) => (\n \n {models\n .filter((entry) => entry.chef === chef)\n .map((entry) => (\n {\n setModel(entry.id)\n setModelSelectorOpen(false)\n }}\n >\n \n {entry.name}\n \n {entry.providers.map((provider) => (\n \n ))}\n \n {model === entry.id ? (\n \n ) : (\n
\n )}\n \n ))}\n \n ))}\n \n \n \n \n \n \n \n {error &&

{error.message}

}\n
\n
\n \n )}\n
\n )\n}\n", + "content": "'use client'\nimport { createClient } from '@/registry/default/blocks/assistant/lib/supabase/client'\nimport {\n Conversation,\n ConversationContent,\n ConversationScrollButton,\n} from '@/registry/default/components/ai-elements/conversation'\nimport {\n Message,\n MessageContent,\n MessageResponse,\n} from '@/registry/default/components/ai-elements/message'\nimport {\n ModelSelector,\n ModelSelectorContent,\n ModelSelectorEmpty,\n ModelSelectorGroup,\n ModelSelectorInput,\n ModelSelectorItem,\n ModelSelectorList,\n ModelSelectorLogo,\n ModelSelectorName,\n ModelSelectorTrigger,\n} from '@/registry/default/components/ai-elements/model-selector'\nimport {\n PromptInput,\n PromptInputBody,\n PromptInputButton,\n PromptInputFooter,\n type PromptInputMessage,\n PromptInputSubmit,\n PromptInputTextarea,\n PromptInputTools,\n} from '@/registry/default/components/ai-elements/prompt-input'\nimport { Suggestion, Suggestions } from '@/registry/default/components/ai-elements/suggestion'\nimport { Button } from '@/registry/default/components/ui/button'\nimport { Popover, PopoverContent, PopoverTrigger } from '@/registry/default/components/ui/popover'\nimport { useChat } from '@ai-sdk/react'\nimport { DefaultChatTransport } from 'ai'\nimport { CheckIcon, MessageCircle } from 'lucide-react'\nimport { useCallback, useState } from 'react'\nimport { toast } from 'sonner'\nimport { Chart, type ChartProps } from './parts/chart'\nimport { Row, type RowItem } from './parts/row'\n\nconst CHAT_API_URL = process.env.NEXT_PUBLIC_CHAT_API_URL\n\nconst models = [\n {\n id: 'gpt-5',\n name: 'GPT-5',\n chef: 'OpenAI',\n chefSlug: 'openai',\n providers: ['openai', 'azure'],\n },\n {\n id: 'gpt-5-mini',\n name: 'GPT-5 Mini',\n chef: 'OpenAI',\n chefSlug: 'openai',\n providers: ['openai', 'azure'],\n },\n {\n id: 'claude-opus-4-20250514',\n name: 'Claude 4 Opus',\n chef: 'Anthropic',\n chefSlug: 'anthropic',\n providers: ['anthropic', 'azure', 'google', 'amazon-bedrock'],\n },\n {\n id: 'claude-sonnet-4-20250514',\n name: 'Claude 4 Sonnet',\n chef: 'Anthropic',\n chefSlug: 'anthropic',\n providers: ['anthropic', 'azure', 'google', 'amazon-bedrock'],\n },\n {\n id: 'gemini-2.0-flash-exp',\n name: 'Gemini 2.0 Flash',\n chef: 'Google',\n chefSlug: 'google',\n providers: ['google'],\n },\n]\n\nconst suggestions = [\n 'How many tasks have I completed this week',\n 'Show me active tasks',\n 'Mark all tasks as complete',\n 'Help me plan my day',\n]\n\ntype RenderRowToolInput = {\n rows: RowItem[]\n}\n\ntype RenderChartToolInput = ChartProps\n\ninterface AssistantWidgetProps {\n /**\n * Override the trigger positioning if you need to embed the widget in a scoped container.\n */\n triggerClassName?: string\n}\n\nexport const AssistantWidget = ({\n triggerClassName = 'fixed bottom-6 right-6',\n}: AssistantWidgetProps = {}) => {\n const [model, setModel] = useState(models[1].id)\n const [modelSelectorOpen, setModelSelectorOpen] = useState(false)\n const [text, setText] = useState('')\n const [useWebSearch, setUseWebSearch] = useState(false)\n const [useMicrophone, setUseMicrophone] = useState(false)\n\n const getAuthHeaders = useCallback(async () => {\n const supabase = createClient()\n const {\n data: { session },\n error,\n } = await supabase.auth.getSession()\n\n if (error) {\n throw error\n }\n\n if (!session) {\n throw new Error('No active session found. Please sign in.')\n }\n\n return {\n Authorization: `Bearer ${session.access_token}`,\n }\n }, [])\n\n const { messages, sendMessage, status, error } = useChat({\n transport: new DefaultChatTransport({\n api: CHAT_API_URL,\n headers: async () => {\n try {\n return await getAuthHeaders()\n } catch (error) {\n console.error('Authentication error:', error)\n toast.error('Failed to authenticate')\n return {}\n }\n },\n body: () => ({\n model,\n useWebSearch,\n }),\n }),\n onError: (error) => {\n console.error('Chat error:', error)\n toast.error('Failed to send message', {\n description: error.message,\n })\n },\n onFinish: () => {\n // Successfully finished streaming\n },\n })\n\n const selectedModelData = models.find((m) => m.id === model)\n\n const handleSubmit = useCallback(\n (message: PromptInputMessage) => {\n const hasText = Boolean(message.text)\n const hasAttachments = Boolean(message.files?.length)\n\n if (!(hasText || hasAttachments)) {\n return\n }\n\n if (message.files?.length) {\n toast.success('Files attached', {\n description: `${message.files.length} file(s) attached to message`,\n })\n }\n\n sendMessage({ text: message.text || 'Sent with attachments' })\n setText('')\n },\n [sendMessage]\n )\n\n const handleSuggestionClick = useCallback(\n (suggestion: string) => {\n sendMessage({ text: suggestion })\n },\n [sendMessage]\n )\n\n return (\n \n \n \n \n \n \n\n \n \n \n {messages.map(({ role, parts }, index) => (\n \n \n {parts.map((part, i) => {\n switch (part.type) {\n case 'text':\n return {part.text}\n case 'tool-renderRow': {\n const payload = (part.input ?? part.output) as\n | RenderRowToolInput\n | undefined\n if (!payload?.rows?.length) return null\n\n return (\n {\n if (prompt) {\n sendMessage({ text: prompt })\n }\n }}\n rows={payload.rows}\n />\n )\n }\n case 'tool-renderChart': {\n console.log('part', part)\n const payload = (part.input ?? part.output) as\n | RenderChartToolInput\n | undefined\n if (!payload) return null\n\n return \n }\n default:\n return null\n }\n })}\n \n \n ))}\n \n \n \n \n {suggestions.map((suggestion) => (\n handleSuggestionClick(suggestion)}\n suggestion={suggestion}\n />\n ))}\n \n
\n \n \n setText(event.target.value)} value={text} />\n \n \n \n \n \n \n {selectedModelData?.chefSlug && (\n \n )}\n {selectedModelData?.name && (\n {selectedModelData.name}\n )}\n \n \n \n \n \n No models found.\n {['OpenAI', 'Anthropic', 'Google'].map((chef) => (\n \n {models\n .filter((m) => m.chef === chef)\n .map((m) => (\n {\n setModel(m.id)\n setModelSelectorOpen(false)\n }}\n value={m.id}\n >\n \n {m.name}\n {model === m.id ? (\n \n ) : (\n
\n )}\n \n ))}\n \n ))}\n \n \n \n \n \n \n \n
\n \n \n )\n}\n\nexport default AssistantWidget\n", + "type": "registry:component" + }, + { + "path": "registry/default/blocks/assistant/components/parts/row.tsx", + "content": "'use client'\n\nimport { Button } from '@/registry/default/components/ui/button'\nimport {\n DropdownMenu,\n DropdownMenuContent,\n DropdownMenuItem,\n DropdownMenuTrigger,\n} from '@/registry/default/components/ui/dropdown-menu'\nimport { MoreHorizontal } from 'lucide-react'\n\nexport type RowAction = {\n label: string\n prompt: string\n}\n\nexport type RowItem = {\n primaryText: string\n secondaryText?: string\n actions?: RowAction[]\n}\n\nexport type RowProps = {\n rows: RowItem[]\n onActionSelect?: (prompt: string) => void\n}\n\nexport function Row({ rows, onActionSelect }: RowProps) {\n if (!rows.length) return null\n\n return (\n
\n {rows.map((row, index) => {\n const hasActions = Boolean(row.actions?.length)\n\n return (\n \n
\n

{row.primaryText}

\n {row.secondaryText ? (\n

{row.secondaryText}

\n ) : null}\n
\n {hasActions ? (\n \n \n \n \n \n \n \n {row.actions?.map((action) => (\n {\n event.preventDefault()\n if (action.prompt) {\n onActionSelect?.(action.prompt)\n }\n }}\n >\n {action.label}\n \n ))}\n \n \n ) : null}\n
\n )\n })}\n
\n )\n}\n", + "type": "registry:component" + }, + { + "path": "registry/default/blocks/assistant/components/parts/chart.tsx", + "content": "'use client'\n\nimport {\n Card,\n CardContent,\n CardDescription,\n CardFooter,\n CardHeader,\n CardTitle,\n} from '@/registry/default/components/ui/card'\n\nexport type ChartDataPoint = Record\n\nexport type ChartProps = {\n primaryText?: string\n secondaryText?: string\n tertiaryText?: string\n data?: ChartDataPoint[]\n xAxis?: string\n yAxis?: string\n}\n\nexport function Chart({\n primaryText,\n secondaryText,\n tertiaryText,\n data,\n xAxis,\n yAxis,\n}: ChartProps) {\n // Don't render if we don't have the minimum required data\n const hasMinimumData = Boolean(data && data.length > 0 && xAxis && yAxis)\n\n // Calculate max value for normalization\n const maxValue =\n hasMinimumData && data && yAxis\n ? Math.max(...data.map((point) => Number(point[yAxis]) || 0))\n : 0\n\n return (\n \n \n {primaryText ? {primaryText} : null}\n {secondaryText ? {secondaryText} : null}\n \n {hasMinimumData && data && xAxis && yAxis ? (\n \n
\n {data.map((point, index) => {\n const value = Number(point[yAxis]) || 0\n const heightPercentage = maxValue > 0 ? (value / maxValue) * 100 : 0\n const label = String(point[xAxis] || '').slice(0, 3)\n\n return (\n
\n
\n \n
\n
{label}
\n
\n )\n })}\n
\n
\n ) : (\n \n
\n Loading chart data...\n
\n
\n )}\n {tertiaryText ? (\n \n
{tertiaryText}
\n
\n ) : null}\n
\n )\n}\n", "type": "registry:component" }, { @@ -54,33 +67,105 @@ }, { "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", + "content": "import { Hono } from 'hono'\nimport { McpServer, StreamableHttpTransport } from 'mcp-lite'\nimport { z } from 'zod'\nimport { corsHeaders } from '../_shared/cors.ts'\nimport { createClient } from 'jsr:@supabase/supabase-js@2'\nimport { toolRegistry } from './tools/registry.ts'\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// Auth Header Management\n// StreamableHttpTransport doesn't expose HTTP request headers to the MCP context (ctx.request.headers is undefined).\n// We use a module-level variable to pass the auth header from the Hono handler to MCP middleware.\n// This is safe because Supabase Edge Functions process requests sequentially, not concurrently.\n// The header is set before processing and cleared in a finally block to prevent leaks.\n// Alternative: Use InMemorySessionAdapter with custom session context, but adds unnecessary complexity.\nlet currentAuthHeader: string | null = null\n\n// Helper function to create authenticated Supabase client\nfunction createAuthenticatedClient(authHeader: string) {\n return createClient(\n Deno.env.get('SUPABASE_URL')!,\n Deno.env.get('SUPABASE_ANON_KEY')!,\n {\n global: {\n headers: {\n Authorization: authHeader,\n },\n },\n }\n )\n}\n\n// Middleware to inject auth header into MCP context state\nmcp.use(async (ctx: any, next: any) => {\n // Try to get auth header from ctx.request.headers (doesn't work with StreamableHttpTransport)\n // Fall back to the stored currentAuthHeader set by the Hono handler\n const authHeader = ctx.request?.headers?.get?.('Authorization') || currentAuthHeader\n\n if (authHeader) {\n // Make auth header available to all tool handlers via ctx.state\n ctx.state.authHeader = authHeader\n }\n\n await next()\n})\n\ntoolRegistry.forEach((tool) => {\n mcp.tool(tool.name, {\n description: tool.description,\n inputSchema: tool.inputSchema,\n handler: async (input: unknown, ctx: any) => {\n const authHeader = ctx.state?.authHeader\n\n if (!authHeader) {\n return {\n content: [\n {\n type: 'text',\n text: tool.authErrorMessage,\n },\n ],\n isError: true,\n }\n }\n\n try {\n const supabase = createAuthenticatedClient(authHeader)\n return await tool.run(supabase, input)\n } catch (err) {\n return {\n content: [\n {\n type: 'text',\n text: `Unexpected error: ${err instanceof Error ? err.message : String(err)}`,\n },\n ],\n isError: true,\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\n// Handle OPTIONS requests for CORS\nmcpApp.use('*', async (c, next) => {\n if (c.req.method === 'OPTIONS') {\n return new Response('ok', { headers: corsHeaders })\n }\n await next()\n})\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\n// Handle both POST (for regular MCP requests) and GET (for SSE connections)\nconst handleMcpRequest = async (c: any) => {\n // Validate Authorization header at the Hono level\n // (StreamableHttpTransport doesn't expose HTTP headers to MCP context)\n const authHeader = c.req.header('Authorization')\n\n if (!authHeader) {\n return c.json({ error: 'Unauthorized' }, { status: 401, headers: corsHeaders })\n }\n\n // Store auth header so MCP middleware can inject it into ctx.state for tool handlers\n currentAuthHeader = authHeader\n\n try {\n const response = await httpHandler(c.req.raw)\n return response\n } finally {\n // Clean up auth header after request completes to prevent leaks\n currentAuthHeader = null\n }\n}\n\nmcpApp.post('/mcp', handleMcpRequest)\nmcpApp.get('/mcp', handleMcpRequest)\n\n// Mount the MCP app at /mcp-server (matches the function name)\napp.route('/mcp-server', mcpApp)\n\nexport default app\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", + "content": "{\n \"compilerOptions\": {\n \"lib\": [\"deno.window\", \"deno.ns\"],\n \"strict\": true\n },\n \"imports\": {\n \"hono\": \"npm:hono@4.10.6\",\n \"mcp-lite\": \"npm:mcp-lite@0.10.0\",\n \"zod\": \"npm:zod@^4.1.12\",\n \"@supabase/supabase-js\": \"npm:@supabase/supabase-js@^2\"\n },\n \"verify_jwt\": false\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", + "content": "import { experimental_createMCPClient as createMCPClient } from '@ai-sdk/mcp'\nimport { openai } from '@ai-sdk/openai'\nimport { convertToModelMessages, stepCountIs, streamText } from 'ai'\nimport { corsHeaders } from '../_shared/cors.ts'\nimport { mcpServerRegistry } from './mcp-servers/registry.ts'\nimport { systemPrompt } from './prompt.ts'\nimport { renderTools } from './render-tools.ts'\n\ntype ChatRequestBody = {\n messages?: unknown[]\n model?: string\n}\n\nDeno.serve(async (req) => {\n if (req.method === 'OPTIONS') {\n return new Response('ok', { headers: corsHeaders })\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 normalizedMessages\n try {\n normalizedMessages = convertToModelMessages(body.messages as any)\n } catch (conversionError) {\n console.error('Assistant chat message normalization error:', conversionError)\n return new Response(JSON.stringify({ error: 'Invalid message format' }), {\n status: 400,\n headers: { ...corsHeaders, 'Content-Type': 'application/json' },\n })\n }\n\n // Create MCP clients from registry definitions\n let tools = { ...renderTools }\n const mcpClients: Array<{ id: string; client: Awaited> }> = []\n\n try {\n for (const server of mcpServerRegistry) {\n try {\n const transport = await server.createTransport({ authHeader })\n if (!transport) continue\n\n const client = await createMCPClient({ transport })\n mcpClients.push({ id: server.id, client })\n\n const remoteTools = await client.tools()\n tools = { ...tools, ...remoteTools }\n } catch (serverError) {\n console.error(`Failed to initialize MCP server \"${server.id}\":`, serverError)\n }\n }\n } catch (mcpError) {\n console.error('Unexpected MCP registry initialization error:', mcpError)\n }\n\n const closeMcpClients = async () => {\n await Promise.all(\n mcpClients.map(async ({ client }) => {\n try {\n await client.close()\n } catch (closeError) {\n console.error('Error closing MCP client:', closeError)\n }\n })\n )\n }\n\n try {\n const result = await streamText({\n model: openai(modelId),\n messages: normalizedMessages,\n stopWhen: stepCountIs(5),\n system: systemPrompt,\n tools,\n onFinish: async () => {\n await closeMcpClients()\n },\n })\n\n return result.toUIMessageStreamResponse({\n headers: corsHeaders,\n sendReasoning: true,\n sendSources: true,\n })\n } catch (error) {\n console.error('Assistant chat error:', error)\n\n await closeMcpClients()\n\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/mcp-servers/types.ts", + "content": "import type { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js'\n\nexport type MCPServerContext = {\n authHeader: string\n}\n\nexport type MCPServerDefinition = {\n id: string\n name: string\n description?: string\n createTransport: (\n context: MCPServerContext\n ) => Promise | StreamableHTTPClientTransport | null\n}\n", + "type": "registry:file", + "target": "supabase/functions/chat/mcp-servers/types.ts" + }, + { + "path": "registry/default/blocks/assistant/supabase/functions/chat/mcp-servers/supabase.ts", + "content": "import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js'\nimport type { MCPServerDefinition } from './types.ts'\n\nexport const supabaseMcpServer: MCPServerDefinition = {\n id: 'supabase-edge',\n name: 'Supabase Edge MCP Server',\n description: 'Local MCP server that provides Supabase task tools.',\n createTransport: ({ authHeader }) => {\n const supabaseUrl = Deno.env.get('SUPABASE_URL')\n if (!supabaseUrl) {\n console.warn('SUPABASE_URL is not configured. Skipping Supabase MCP server registration.')\n return null\n }\n\n const normalizedUrl = supabaseUrl.replace(/\\/$/, '')\n const mcpServerUrl = `${normalizedUrl}/functions/v1/mcp-server/mcp`\n\n return new StreamableHTTPClientTransport(new URL(mcpServerUrl), {\n requestInit: {\n headers: {\n Authorization: authHeader,\n },\n },\n })\n },\n}\n", + "type": "registry:file", + "target": "supabase/functions/chat/mcp-servers/supabase.ts" + }, + { + "path": "registry/default/blocks/assistant/supabase/functions/chat/mcp-servers/registry.ts", + "content": "import type { MCPServerDefinition } from './types.ts'\nimport { supabaseMcpServer } from './supabase.ts'\n\nexport const mcpServerRegistry: MCPServerDefinition[] = [supabaseMcpServer]\n", + "type": "registry:file", + "target": "supabase/functions/chat/mcp-servers/registry.ts" + }, + { + "path": "registry/default/blocks/assistant/supabase/functions/chat/prompt.ts", + "content": "const currentDate = new Date().toISOString().split('T')[0]\n\nexport const systemPrompt = `Today is ${currentDate}. You are Supabase Tasks, an AI assistant focused on helping users plan, prioritize, and manage their work. Keep conversations grounded in their task lists, suggest concrete next actions, and clarify requirements before acting.`\n", + "type": "registry:file", + "target": "supabase/functions/chat/prompt.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", + "content": "{\n \"compilerOptions\": {\n \"lib\": [\"deno.window\", \"deno.ns\"],\n \"strict\": true\n },\n \"imports\": {\n \"hono\": \"npm:hono@4.10.6\",\n \"zod\": \"npm:zod@^4.1.12\",\n \"ai\": \"npm:ai@^5.0.93\",\n \"@ai-sdk/openai\": \"npm:@ai-sdk/openai@^2.0.68\",\n \"@ai-sdk/mcp\": \"npm:@ai-sdk/mcp@latest\",\n \"@modelcontextprotocol/sdk/client/streamableHttp.js\": \"npm:@modelcontextprotocol/sdk@^1.22.0/client/streamableHttp.js\"\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", + "path": "registry/default/blocks/assistant/supabase/functions/_shared/cors.ts", + "content": "export const corsHeaders = {\n 'Access-Control-Allow-Origin': '*',\n 'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type',\n 'Access-Control-Allow-Methods': 'POST, OPTIONS',\n}\n", "type": "registry:file", - "target": "README.assistant.md" + "target": "supabase/functions/_shared/cors.ts" + }, + { + "path": "registry/default/blocks/assistant/supabase/functions/chat/render-tools.ts", + "content": "import { tool } from 'ai'\nimport { z } from 'zod'\n\nconst rowSchema = z.object({\n primaryText: z.string().describe('Primary label for the row such as the task title.'),\n secondaryText: z\n .string()\n .optional()\n .describe('Secondary information such as due dates or task metadata.'),\n actions: z\n .array(\n z.object({\n label: z.string().describe('Text shown in the action menu, e.g., \"Delete task\".'),\n prompt: z\n .string()\n .describe('Prompt to send back to the assistant when the action is selected.'),\n })\n )\n .optional()\n .describe('Optional list of quick actions the user can trigger for this row.'),\n})\n\nconst chartDataPointSchema = z\n .object({})\n .catchall(\n z\n .union([z.string(), z.number()])\n .describe(\n 'Value for this property in the data point. Strings are typically labels, numbers are metrics.'\n )\n )\n .describe('Data point object containing axis and value fields.')\n\nconst chartSchema = z.object({\n primaryText: z\n .string()\n .describe('Primary title shown at the top of the chart card. e.g. Tasks completed'),\n secondaryText: z\n .string()\n .optional()\n .describe('Optional short description shown under the title. e.g. Last 7 days'),\n tertiaryText: z\n .string()\n .optional()\n .describe(\n 'Optional supporting text shown beneath the chart. e.g. Increase of 12% week over week'\n ),\n data: z.array(chartDataPointSchema).min(1).describe('Data points to plot on the chart.'),\n xAxis: z\n .string()\n .describe('Key inside each data point to use for the X-axis labels (e.g., \"month\").'),\n yAxis: z\n .string()\n .describe('Key inside each data point to use for the bar height values (e.g., \"desktop\").'),\n})\n\nconst renderRowTool = tool({\n description:\n 'Render a task row to summarize Supabase records, including follow-up actions the user can take.',\n inputSchema: z.object({\n rows: z.array(rowSchema).min(1).describe('Rows to display to the user.'),\n }),\n execute: async ({ rows }) => {\n return {\n success: true,\n message: 'Rows have been shown to the user',\n }\n },\n})\n\nconst renderChartTool = tool({\n description: 'Render a bar chart summarizing Supabase metrics for the user.',\n inputSchema: chartSchema,\n execute: async () => {\n return {\n success: true,\n message: 'Chart has been shown to the user',\n }\n },\n})\n\nexport const renderTools = {\n renderRow: renderRowTool,\n renderChart: renderChartTool,\n}\n", + "type": "registry:file", + "target": "supabase/functions/chat/render-tools.ts" + }, + { + "path": "registry/default/blocks/assistant/supabase/functions/mcp-server/tools/count-tasks.ts", + "content": "import { z } from 'zod'\nimport type { ToolDefinition } from './types.ts'\n\ntype CountTasksInput = {\n createdAtStart?: string\n createdAtEnd?: string\n completedAtStart?: string\n completedAtEnd?: string\n}\n\nexport const countTasksTool: ToolDefinition = {\n name: 'countTasks',\n description:\n 'Count tasks that were created and/or completed between the provided date ranges. Dates should be ISO strings (YYYY-MM-DD or full timestamp).',\n authErrorMessage: 'Error: Authentication required to count tasks.',\n inputSchema: z.object({\n createdAtStart: z\n .string()\n .optional()\n .describe('Lower bound for created_at (inclusive). Accepts ISO dates or timestamps.'),\n createdAtEnd: z\n .string()\n .optional()\n .describe('Upper bound for created_at (inclusive). Accepts ISO dates or timestamps.'),\n completedAtStart: z\n .string()\n .optional()\n .describe('Lower bound for completed_at (inclusive). Accepts ISO dates or timestamps.'),\n completedAtEnd: z\n .string()\n .optional()\n .describe('Upper bound for completed_at (inclusive). Accepts ISO dates or timestamps.'),\n }),\n run: async (supabase, { createdAtStart, createdAtEnd, completedAtStart, completedAtEnd }) => {\n try {\n const { data, error } = await supabase.rpc('count_tasks', {\n created_at_start: createdAtStart ?? null,\n created_at_end: createdAtEnd ?? null,\n completed_at_start: completedAtStart ?? null,\n completed_at_end: completedAtEnd ?? null,\n })\n\n if (error) {\n return {\n content: [\n {\n type: 'text',\n text: `Error counting tasks: ${error.message}`,\n },\n ],\n isError: true,\n }\n }\n\n const total = Array.isArray(data)\n ? (data[0]?.total as number | undefined) ?? 0\n : (data as number | null) ?? 0\n\n return {\n content: [\n {\n type: 'text',\n text: `Found ${total} task(s) matching the provided filters.`,\n },\n ],\n }\n } catch (err) {\n return {\n content: [\n {\n type: 'text',\n text: `Unexpected error: ${err instanceof Error ? err.message : String(err)}`,\n },\n ],\n isError: true,\n }\n }\n },\n}\n", + "type": "registry:file", + "target": "supabase/functions/mcp-server/tools/count-tasks.ts" + }, + { + "path": "registry/default/blocks/assistant/supabase/functions/mcp-server/tools/create-task.ts", + "content": "import { z } from 'zod'\nimport type { ToolDefinition } from './types.ts'\n\ntype CreateTaskInput = {\n title: string\n due_date?: string\n}\n\nexport const createTaskTool: ToolDefinition = {\n name: 'createTask',\n description: 'Create a task for the authenticated user. Optionally provide a due date (YYYY-MM-DD).',\n authErrorMessage: 'Error: Authentication required to create tasks.',\n inputSchema: z.object({\n title: z\n .string()\n .min(1, 'Title must not be empty')\n .describe('Task title to display in the list.'),\n due_date: z\n .string()\n .optional()\n .describe('Optional due date in YYYY-MM-DD format.'),\n }),\n run: async (supabase, { title, due_date }) => {\n try {\n const {\n data: { user },\n error: userError,\n } = await supabase.auth.getUser()\n\n if (userError || !user) {\n return {\n content: [\n {\n type: 'text',\n text: `Error retrieving authenticated user: ${userError?.message ?? 'Not found'}`,\n },\n ],\n isError: true,\n }\n }\n\n const { data, error } = await supabase\n .from('tasks')\n .insert({\n title,\n due_at: due_date ?? null,\n assignee_id: user.id,\n })\n .select('id, title, due_at, assignee_id')\n .single()\n\n if (error || !data) {\n return {\n content: [\n {\n type: 'text',\n text: `Error creating task: ${error?.message ?? 'Unknown error'}`,\n },\n ],\n isError: true,\n }\n }\n\n const dueDateText = data.due_at ? `Due ${data.due_at}` : 'No due date set'\n\n return {\n content: [\n {\n type: 'text',\n text: `Created task [${data.id}] \"${data.title}\" for user ${data.assignee_id}. ${dueDateText}.`,\n },\n ],\n }\n } catch (err) {\n return {\n content: [\n {\n type: 'text',\n text: `Unexpected error: ${err instanceof Error ? err.message : String(err)}`,\n },\n ],\n isError: true,\n }\n }\n },\n}\n", + "type": "registry:file", + "target": "supabase/functions/mcp-server/tools/create-task.ts" + }, + { + "path": "registry/default/blocks/assistant/supabase/functions/mcp-server/tools/delete-task.ts", + "content": "import { z } from 'zod'\nimport type { ToolDefinition } from './types.ts'\n\ntype DeleteTaskInput = {\n id: number\n}\n\nexport const deleteTaskTool: ToolDefinition = {\n name: 'deleteTask',\n description: 'Delete a task using its numeric id.',\n authErrorMessage: 'Error: Authentication required to delete tasks.',\n inputSchema: z.object({\n id: z.number().int().describe('Task id to delete.'),\n }),\n run: async (supabase, { id }) => {\n try {\n const { data, error } = await supabase\n .from('tasks')\n .delete()\n .eq('id', id)\n .select('id, title')\n .single()\n\n if (error || !data) {\n return {\n content: [\n {\n type: 'text',\n text: `Error deleting task: ${error?.message ?? 'Task not found or already deleted.'}`,\n },\n ],\n isError: true,\n }\n }\n\n return {\n content: [\n {\n type: 'text',\n text: `Deleted task [${data.id}] \"${data.title ?? 'Untitled'}\" successfully.`,\n },\n ],\n }\n } catch (err) {\n return {\n content: [\n {\n type: 'text',\n text: `Unexpected error: ${err instanceof Error ? err.message : String(err)}`,\n },\n ],\n isError: true,\n }\n }\n },\n}\n", + "type": "registry:file", + "target": "supabase/functions/mcp-server/tools/delete-task.ts" + }, + { + "path": "registry/default/blocks/assistant/supabase/functions/mcp-server/tools/get-user-tasks.ts", + "content": "import { z } from 'zod'\nimport type { ToolDefinition } from './types.ts'\n\ntype GetUserTasksInput = {\n due_date?: string\n title_search?: string\n}\n\nexport const getUserTasksTool: ToolDefinition = {\n name: 'getUserTasks',\n description:\n 'Fetch tasks for the authenticated user with optional filtering by due date and title search.',\n authErrorMessage: 'Error: Authentication required to fetch tasks.',\n inputSchema: z.object({\n due_date: z\n .string()\n .optional()\n .describe('Optional due date filter in YYYY-MM-DD format. Returns tasks with this exact due date.'),\n title_search: z\n .string()\n .optional()\n .describe('Optional title search term. Returns tasks where the title contains this text (case-insensitive).'),\n }),\n run: async (supabase, { due_date, title_search }) => {\n try {\n let query = supabase\n .from('tasks')\n .select('id, created_at, title, assignee_id, due_at')\n .order('due_at', { ascending: true, nullsFirst: false })\n\n if (due_date) {\n query = query.eq('due_at', due_date)\n }\n\n if (title_search) {\n query = query.ilike('title', `%${title_search}%`)\n }\n\n const { data, error } = await query\n\n if (error) {\n return {\n content: [\n {\n type: 'text',\n text: `Error fetching tasks: ${error.message}`,\n },\n ],\n isError: true,\n }\n }\n\n const taskCount = data?.length || 0\n const tasksText =\n data && taskCount > 0\n ? data\n .map((task) => {\n const dueDate = task.due_at ? `Due: ${task.due_at}` : 'No due date'\n return `- [${task.id}] ${task.title || 'Untitled'} (${dueDate})`\n })\n .join('\\n')\n : 'No tasks found.'\n\n return {\n content: [\n {\n type: 'text',\n text: `Found ${taskCount} task(s):\\n\\n${tasksText}`,\n },\n ],\n }\n } catch (err) {\n return {\n content: [\n {\n type: 'text',\n text: `Unexpected error: ${err instanceof Error ? err.message : String(err)}`,\n },\n ],\n isError: true,\n }\n }\n },\n}\n", + "type": "registry:file", + "target": "supabase/functions/mcp-server/tools/get-user-tasks.ts" + }, + { + "path": "registry/default/blocks/assistant/supabase/functions/mcp-server/tools/registry.ts", + "content": "import { createTaskTool } from './create-task.ts'\nimport { deleteTaskTool } from './delete-task.ts'\nimport { getUserTasksTool } from './get-user-tasks.ts'\nimport { countTasksTool } from './count-tasks.ts'\nimport { updateTaskTool } from './update-task.ts'\nimport type { ToolDefinition } from './types.ts'\n\nexport const toolRegistry: ToolDefinition[] = [\n getUserTasksTool,\n createTaskTool,\n updateTaskTool,\n deleteTaskTool,\n countTasksTool,\n]\n", + "type": "registry:file", + "target": "supabase/functions/mcp-server/tools/registry.ts" + }, + { + "path": "registry/default/blocks/assistant/supabase/functions/mcp-server/tools/types.ts", + "content": "import type { SupabaseClient } from 'jsr:@supabase/supabase-js@2'\nimport type { ZodTypeAny } from 'zod'\n\nexport interface ToolDefinition {\n name: string\n description: string\n inputSchema: ZodTypeAny\n authErrorMessage: string\n run: (supabase: SupabaseClient, input: Input) => Promise\n}\n", + "type": "registry:file", + "target": "supabase/functions/mcp-server/tools/types.ts" + }, + { + "path": "registry/default/blocks/assistant/supabase/functions/mcp-server/tools/update-task.ts", + "content": "import { z } from 'zod'\nimport type { ToolDefinition } from './types.ts'\n\ntype UpdateTaskInput = {\n id: number\n title?: string\n due_date?: string\n completed_at?: string\n}\n\nexport const updateTaskTool: ToolDefinition = {\n name: 'updateTask',\n description: 'Update a task title, due date, and/or completion date by id.',\n authErrorMessage: 'Error: Authentication required to update tasks.',\n inputSchema: z\n .object({\n id: z.number().int().describe('Task id to update.'),\n title: z.string().optional().describe('New title for the task.'),\n due_date: z\n .string()\n .optional()\n .describe('New due date in YYYY-MM-DD format. Set to empty string to clear.'),\n completed_at: z\n .string()\n .optional()\n .describe(\n 'Completion date in YYYY-MM-DD or ISO timestamp format. Set to empty string to clear.'\n ),\n })\n .refine(\n (data) =>\n data.title !== undefined || data.due_date !== undefined || data.completed_at !== undefined,\n {\n message: 'Provide at least a title, due_date, or completed_at to update.',\n path: ['title'],\n }\n ),\n run: async (supabase, { id, title, due_date, completed_at }) => {\n try {\n const updatePayload: Record = {}\n\n if (title !== undefined) {\n updatePayload.title = title\n }\n\n if (due_date !== undefined) {\n updatePayload.due_at = due_date || null\n }\n\n if (completed_at !== undefined) {\n updatePayload.completed_at = completed_at || null\n }\n\n const { data, error } = await supabase\n .from('tasks')\n .update(updatePayload)\n .eq('id', id)\n .select('id, title, due_at, completed_at, assignee_id')\n .single()\n\n if (error || !data) {\n return {\n content: [\n {\n type: 'text',\n text: `Error updating task: ${error?.message ?? 'Task not found.'}`,\n },\n ],\n isError: true,\n }\n }\n\n const dueDateText = data.due_at ? `Due ${data.due_at}` : 'No due date set'\n const completedText = data.completed_at\n ? `Completed on ${data.completed_at}`\n : 'Not completed'\n\n return {\n content: [\n {\n type: 'text',\n text: `Updated task [${data.id}] \"${data.title}\". ${dueDateText}. ${completedText}.`,\n },\n ],\n }\n } catch (err) {\n return {\n content: [\n {\n type: 'text',\n text: `Unexpected error: ${err instanceof Error ? err.message : String(err)}`,\n },\n ],\n isError: true,\n }\n }\n },\n}\n", + "type": "registry:file", + "target": "supabase/functions/mcp-server/tools/update-task.ts" } ] } \ No newline at end of file diff --git a/apps/ui-library/public/r/registry.json b/apps/ui-library/public/r/registry.json index 4141bd73841..6949b55f643 100644 --- a/apps/ui-library/public/r/registry.json +++ b/apps/ui-library/public/r/registry.json @@ -1425,7 +1425,9 @@ "registryDependencies": [ "button", "input", + "popover", "scroll-area", + "chart", "@ai-elements/conversation", "@ai-elements/message", "@ai-elements/prompt-input", @@ -1444,13 +1446,22 @@ "zod@latest", "nanoid@latest", "sonner@latest", - "lucide-react@latest" + "lucide-react@latest", + "recharts@latest" ], "files": [ { "path": "registry/default/blocks/assistant/components/assistant-widget.tsx", "type": "registry:component" }, + { + "path": "registry/default/blocks/assistant/components/parts/row.tsx", + "type": "registry:component" + }, + { + "path": "registry/default/blocks/assistant/components/parts/chart.tsx", + "type": "registry:component" + }, { "path": "registry/default/blocks/assistant/lib/supabase/server.ts", "type": "registry:file", @@ -1481,15 +1492,75 @@ "type": "registry:file", "target": "supabase/functions/chat/index.ts" }, + { + "path": "registry/default/blocks/assistant/supabase/functions/chat/mcp-servers/types.ts", + "type": "registry:file", + "target": "supabase/functions/chat/mcp-servers/types.ts" + }, + { + "path": "registry/default/blocks/assistant/supabase/functions/chat/mcp-servers/supabase.ts", + "type": "registry:file", + "target": "supabase/functions/chat/mcp-servers/supabase.ts" + }, + { + "path": "registry/default/blocks/assistant/supabase/functions/chat/mcp-servers/registry.ts", + "type": "registry:file", + "target": "supabase/functions/chat/mcp-servers/registry.ts" + }, + { + "path": "registry/default/blocks/assistant/supabase/functions/chat/prompt.ts", + "type": "registry:file", + "target": "supabase/functions/chat/prompt.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", + "path": "registry/default/blocks/assistant/supabase/functions/_shared/cors.ts", "type": "registry:file", - "target": "README.assistant.md" + "target": "supabase/functions/_shared/cors.ts" + }, + { + "path": "registry/default/blocks/assistant/supabase/functions/chat/render-tools.ts", + "type": "registry:file", + "target": "supabase/functions/chat/render-tools.ts" + }, + { + "path": "registry/default/blocks/assistant/supabase/functions/mcp-server/tools/count-tasks.ts", + "type": "registry:file", + "target": "supabase/functions/mcp-server/tools/count-tasks.ts" + }, + { + "path": "registry/default/blocks/assistant/supabase/functions/mcp-server/tools/create-task.ts", + "type": "registry:file", + "target": "supabase/functions/mcp-server/tools/create-task.ts" + }, + { + "path": "registry/default/blocks/assistant/supabase/functions/mcp-server/tools/delete-task.ts", + "type": "registry:file", + "target": "supabase/functions/mcp-server/tools/delete-task.ts" + }, + { + "path": "registry/default/blocks/assistant/supabase/functions/mcp-server/tools/get-user-tasks.ts", + "type": "registry:file", + "target": "supabase/functions/mcp-server/tools/get-user-tasks.ts" + }, + { + "path": "registry/default/blocks/assistant/supabase/functions/mcp-server/tools/registry.ts", + "type": "registry:file", + "target": "supabase/functions/mcp-server/tools/registry.ts" + }, + { + "path": "registry/default/blocks/assistant/supabase/functions/mcp-server/tools/types.ts", + "type": "registry:file", + "target": "supabase/functions/mcp-server/tools/types.ts" + }, + { + "path": "registry/default/blocks/assistant/supabase/functions/mcp-server/tools/update-task.ts", + "type": "registry:file", + "target": "supabase/functions/mcp-server/tools/update-task.ts" } ] }, diff --git a/apps/ui-library/registry/default/blocks/assistant/README.md b/apps/ui-library/registry/default/blocks/assistant/README.md deleted file mode 100644 index 2b9f99db4ae..00000000000 --- a/apps/ui-library/registry/default/blocks/assistant/README.md +++ /dev/null @@ -1,432 +0,0 @@ -# AI Assistant Block - -An AI-powered chatbot widget with database query capabilities using the Model Context Protocol (MCP). - -## Features - -- Fixed bottom-right chat widget with minimize/expand controls -- Natural language database queries using AI -- Row Level Security (RLS) enforcement -- Streaming responses with real-time feedback -- Tool execution visualization -- Secure SQL query execution with validation -- Rich AI SDK Elements (Conversation, Messages, Prompt Input, Model Selector, Suggestions, Sources, Reasoning) for a modern assistant UX - -## Architecture - -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 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 - -## Prerequisites - -- Supabase project with authentication enabled -- OpenAI API key (or another MCP-compatible LLM provider) -- Supabase CLI installed -- Docker (for Supabase local dev) -- Node.js and npm/pnpm installed -- Deno 2.x (Supabase Edge Functions runtime) - -## Setup Instructions - -### 1. Install the Block - -```bash -npx shadcn@latest add https://ui.supabase.com/r/assistant -``` - -### 2. Install UI + AI SDK Elements - -The 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: - -```bash -npx shadcn@latest add 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 -``` - -Each AI Element ships ready-to-style JSX + Tailwind so you can compose advanced assistant surfaces without rebuilding primitives. - -### 3. Set Up Environment Variables - -Add the following to your `.env.local`: - -```env -# OpenAI API Key -OPENAI_API_KEY=your_openai_api_key - -# Supabase Configuration -NEXT_PUBLIC_SUPABASE_URL=your_supabase_url -NEXT_PUBLIC_SUPABASE_ANON_KEY=your_supabase_anon_key - -# 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 - -Supabase 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): - -1. **Scaffold (optional):** `npm create mcp-lite@latest` and choose the **Supabase Edge Functions (MCP server)** template to bootstrap `supabase/functions/mcp-server`. -2. **Import map:** Ensure `supabase/functions/mcp-server/deno.json` includes the dependencies the block uses: - -```jsonc -{ - "compilerOptions": { - "lib": ["deno.window", "deno.ns"], - "strict": true - }, - "imports": { - "hono": "npm:hono@^4.6.14", - "mcp-lite": "npm:mcp-lite@0.8.2", - "zod": "npm:zod@^4.1.12", - "@supabase/supabase-js": "npm:@supabase/supabase-js@^2.48.0" - } -} -``` - -3. **Hono entrypoint:** Update `supabase/functions/mcp-server/index.ts` to mount the MCP transport behind `/mcp-server/mcp`: - -```ts -import { Hono } from "hono"; -import { McpServer, StreamableHttpTransport } from "mcp-lite"; -import { createClient } from "@supabase/supabase-js"; -import { z } from "zod"; - -const mcp = new McpServer({ - name: "supabase-assistant-mcp-server", - version: "1.0.0", - schemaAdapter: (schema) => z.toJSONSchema(schema as z.ZodType), -}); - -// ...define queryDatabase and other tools that create an authenticated Supabase client - -const transport = new StreamableHttpTransport(); -const httpHandler = transport.bind(mcp); - -const mcpApp = new Hono(); -mcpApp.get("/", (c) => - c.json({ - message: "MCP Server on Supabase Edge Functions", - endpoints: { mcp: "/mcp", health: "/health" }, - }) -); -mcpApp.get("/health", (c) => c.json({ status: "ok" })); -mcpApp.all("/mcp", (c) => httpHandler(c.req.raw)); - -const app = new Hono(); -app.route("/mcp-server", mcpApp); - -Deno.serve((req) => app.fetch(req)); -``` - -This structure keeps Supabase's root handler (`/mcp-server`) separate from your MCP protocol endpoints while still serving the `StreamableHttpTransport`. - -4. **Serve locally:** - -```bash -supabase start -supabase functions serve --no-verify-jwt mcp-server -``` - -Your MCP endpoint will be available at `http://localhost:54321/functions/v1/mcp-server/mcp`. - -5. **Deploy globally:** - -```bash -supabase functions deploy --no-verify-jwt mcp-server -``` - -### 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"; - -export default function Layout({ children }) { - return ( -
- {children} - -
- ); -} -``` - -## Local Development & Testing - -- 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` - -## Widget UI with AI SDK Elements - -The 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: - -```tsx -"use client"; - -import { - MessageBranch, - MessageBranchContent, - MessageBranchNext, - MessageBranchPage, - MessageBranchPrevious, - MessageBranchSelector, -} from "@/components/ai-elements/message"; -import { - Conversation, - ConversationContent, - ConversationScrollButton, -} from "@/components/ai-elements/conversation"; -import { Message, MessageContent } from "@/components/ai-elements/message"; -import { - PromptInput, - PromptInputActionAddAttachments, - PromptInputActionMenu, - PromptInputActionMenuContent, - PromptInputActionMenuTrigger, - PromptInputAttachment, - PromptInputAttachments, - PromptInputBody, - PromptInputButton, - PromptInputFooter, - PromptInputHeader, - type PromptInputMessage, - 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 { - Reasoning, - ReasoningContent, - ReasoningTrigger, -} from "@/components/ai-elements/reasoning"; -import { MessageResponse } from "@/components/ai-elements/message"; -import { - Source, - Sources, - SourcesContent, - SourcesTrigger, -} from "@/components/ai-elements/sources"; -import { Suggestion, Suggestions } from "@/components/ai-elements/suggestion"; -import type { ToolUIPart } from "ai"; -import { CheckIcon, GlobeIcon, MicIcon } from "lucide-react"; -import { nanoid } from "nanoid"; -import { useCallback, useState } from "react"; -import { toast } from "sonner"; - -// ...rest of the Example component from the block's source (see components/assistant-widget.tsx) -``` - -The complete example (available in this block) demonstrates: - -- Branching messages with sources, reasoning traces, and MCP tool output -- Scrollable conversation history with streaming responses -- Prompt composer with attachments, microphone toggle, and model selector -- Reusable suggestions to quickly seed the assistant - -Use the snippet as a starting point for your own assistant surface or trim it down for a lighter footprint. - -## Usage - -### For End Users - -1. Click the chat icon in the bottom-right corner to open the assistant -2. Type natural language queries about your data -3. The AI will translate your questions into SQL and execute them via the `queryDatabase` MCP tool -4. Results are displayed in the chat interface with tool execution feedback -5. Minimize or close the widget as needed - -### Example Queries - -- "Show me all my tasks" -- "How many orders did I place last month?" -- "List my recent transactions" -- "What's the total revenue from my sales?" - -## Security Considerations - -### Query Builder Approach - -The MCP server uses Supabase's query builder for safe data access: - -- Uses the `queryDatabase` tool with structured parameters (table, filters, orderBy, limit) -- No raw SQL injection risks -- Queries execute with the authenticated user's context -- All operations go through Supabase's security layer - -### Row Level Security (RLS) - -All queries respect Supabase RLS policies: - -- The authenticated user's token is passed to the MCP server -- Queries run with user context -- Users can only access data allowed by RLS policies - -### Authentication - -- Local development uses `--no-verify-jwt` for speed; production deployments should enforce authentication per the [MCP authorization spec](https://modelcontextprotocol.io/spec) -- Never expose production data without adding proper auth and auditing - -#### MCP middleware helpers - -`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: - -```ts -// Logging -mcp.use(async ({ req }, next) => { - const start = Date.now(); - await next(); - console.log(`${req.method} took ${Date.now() - start}ms`); -}); - -// Authentication -mcp.use(async (ctx, next) => { - const token = ctx.req.header("Authorization"); - if (!token) throw new Error("Unauthorized"); - - const user = await validateToken(token); - ctx.state.user = user; - await next(); -}); - -// Rate limiting (example) -mcp.use(async (ctx, next) => { - const userId = ctx.state.user?.id; - if (await isRateLimited(userId)) { - throw new Error("Rate limit exceeded"); - } - await next(); -}); -``` - -Inside 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. - -### Best Practices - -1. **Enable RLS on all tables:** Ensure all tables have RLS enabled -2. **Create appropriate policies:** Define policies that match your security requirements -3. **Monitor edge function logs:** Check logs for suspicious queries or errors -4. **Rate limiting:** Consider adding rate limiting to prevent abuse -5. **Query complexity:** Set statement timeouts in Postgres to prevent long-running queries -6. **Validate tool inputs:** Sanitize and validate every argument received by MCP tools - -## Customization - -### Change the AI Model - -Edit `supabase/functions/chat/index.ts`: - -```ts -const result = await streamText({ - model: openai("gpt-4o"), // Change to another model (OpenAI, Anthropic, Google, etc.) - // ... rest of config -}); -``` - -### Customize the System Prompt - -Edit the `system` parameter in `supabase/functions/chat/index.ts` to change the assistant's behavior. - -### Style the Widget - -AI SDK Elements are standard React components, so you can: - -- Override Tailwind classes -- Reorder or remove sections (e.g., hide the model selector) -- Inject custom suggestion lists or streaming loaders - -### Add More MCP Tools - -Extend the MCP server with additional tools in `supabase/functions/mcp-server/index.ts` following the same Hono pattern: - -```ts -mcp.tool("myCustomTool", { - description: "Description of what this tool does", - inputSchema: z.object({ - param1: z.string(), - param2: z.number().optional(), - }), - handler: async (args, { req }) => { - const supabaseClient = createClient( - Deno.env.get("SUPABASE_URL") ?? "", - Deno.env.get("SUPABASE_ANON_KEY") ?? "", - { - global: { - headers: { Authorization: req.headers.get("Authorization")! }, - }, - } - ); - - // Tool implementation - return { - content: [{ type: "text", text: "Result" }], - }; - }, -}); -``` - -## Troubleshooting - -### "Unauthorized" Error - -- Ensure the user is authenticated before using the widget -- Check that `SUPABASE_URL` and `SUPABASE_ANON_KEY` are set correctly -- Verify the Authorization header is forwarded to the MCP server - -### "Query execution failed" - -- Verify the table exists and is accessible -- Check that RLS policies allow the user to access the data -- Verify the column names in filters are correct -- Check edge function logs: `supabase functions logs mcp-server` - -### "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/registry-item.json b/apps/ui-library/registry/default/blocks/assistant/registry-item.json index 8bae6db7ceb..de5bc8190bf 100644 --- a/apps/ui-library/registry/default/blocks/assistant/registry-item.json +++ b/apps/ui-library/registry/default/blocks/assistant/registry-item.json @@ -98,11 +98,6 @@ "type": "registry:file", "target": "supabase/functions/chat/deno.json" }, - { - "path": "registry/default/blocks/assistant/README.md", - "type": "registry:file", - "target": "README.assistant.md" - }, { "path": "registry/default/blocks/assistant/supabase/functions/_shared/cors.ts", "type": "registry:file",