diff --git a/apps/docs/internals/generate-guides-markdown.ts b/apps/docs/internals/generate-guides-markdown.ts index 2d639b35d10..41944582ca4 100644 --- a/apps/docs/internals/generate-guides-markdown.ts +++ b/apps/docs/internals/generate-guides-markdown.ts @@ -10,6 +10,7 @@ import { toMarkdown } from 'mdast-util-to-markdown' import { gfm } from 'micromark-extension-gfm' import { mdxjs } from 'micromark-extension-mdxjs' import { parse as parseToml } from 'smol-toml' +import { mcpConfigPanelMarkdown as McpConfigPanel } from 'ui-patterns/McpUrlBuilder/McpConfigPanel.md' import { addBaseUrlPrefix } from './internal-links' import { Admonition } from './markdown-schema/Admonition' @@ -153,6 +154,7 @@ const SCHEMA: ComponentSchema = { ComputeDiskLimitsTable, ErrorCodes, Link, + McpConfigPanel, Price, GlassPanel: Panel, IconPanel: Panel, diff --git a/apps/docs/package.json b/apps/docs/package.json index dbc8d74b787..aa57262e3d9 100644 --- a/apps/docs/package.json +++ b/apps/docs/package.json @@ -140,6 +140,7 @@ "@graphql-codegen/cli": "5.0.5", "@graphql-codegen/typescript": "4.1.6", "@graphql-codegen/typescript-resolvers": "4.5.0", + "@redocly/cli": "^2.26.0", "@supabase/supa-mdx-lint": "0.3.2", "@testing-library/react": "^16.0.0", "@types/common-tags": "^1.8.4", @@ -154,7 +155,6 @@ "@types/react-copy-to-clipboard": "^5.0.4", "@types/react-dom": "catalog:", "@types/unist": "^2.0.6", - "@redocly/cli": "^2.26.0", "amaro": "^1.1.5", "api-types": "workspace:*", "cheerio": "^1.0.0-rc.12", diff --git a/packages/ui-patterns/package.json b/packages/ui-patterns/package.json index 5bb970e0e70..2f843672147 100644 --- a/packages/ui-patterns/package.json +++ b/packages/ui-patterns/package.json @@ -458,6 +458,10 @@ "import": "./src/McpUrlBuilder/McpConfigPanel.tsx", "types": "./src/McpUrlBuilder/McpConfigPanel.tsx" }, + "./McpUrlBuilder/McpConfigPanel.md": { + "import": "./src/McpUrlBuilder/McpConfigPanel.md.tsx", + "types": "./src/McpUrlBuilder/McpConfigPanel.md.tsx" + }, "./McpUrlBuilder/components/ClientSelectDropdown": { "import": "./src/McpUrlBuilder/components/ClientSelectDropdown.tsx", "types": "./src/McpUrlBuilder/components/ClientSelectDropdown.tsx" @@ -474,9 +478,17 @@ "import": "./src/McpUrlBuilder/components/McpConfigurationOptions.tsx", "types": "./src/McpUrlBuilder/components/McpConfigurationOptions.tsx" }, - "./McpUrlBuilder/constants": { - "import": "./src/McpUrlBuilder/constants.tsx", - "types": "./src/McpUrlBuilder/constants.tsx" + "./McpUrlBuilder/clients.data": { + "import": "./src/McpUrlBuilder/clients.data.ts", + "types": "./src/McpUrlBuilder/clients.data.ts" + }, + "./McpUrlBuilder/utils/serializeMcpConfig": { + "import": "./src/McpUrlBuilder/utils/serializeMcpConfig.ts", + "types": "./src/McpUrlBuilder/utils/serializeMcpConfig.ts" + }, + "./McpUrlBuilder/mcpClients": { + "import": "./src/McpUrlBuilder/mcpClients.tsx", + "types": "./src/McpUrlBuilder/mcpClients.tsx" }, "./McpUrlBuilder": { "import": "./src/McpUrlBuilder/index.ts", @@ -790,7 +802,6 @@ "dependencies": { "@hookform/resolvers": "^3.1.1", "@monaco-editor/react": "catalog:", - "@std/toml": "jsr:^1.0.11", "@supabase/sql-to-rest": "^0.1.6", "@supabase/supabase-js": "catalog:", "@tanstack/react-table": "catalog:", @@ -808,6 +819,7 @@ "lodash": "catalog:", "lucide-react": "*", "mdast": "^3.0.0", + "mdast-jsx": "^0.1.1", "mermaid": "^11.12.1", "monaco-editor": "*", "next-themes": "catalog:", @@ -828,6 +840,7 @@ "remark": "^15.0.1", "remark-gfm": "^4.0.0", "scroll-into-view-if-needed": "^3.1.0", + "smol-toml": "^1.6.1", "sonner": "^1.5.0", "sql-formatter": "^15.0.0", "sse.js": "^2.2.0", diff --git a/packages/ui-patterns/src/CodeBlock/CodeBlock.tsx b/packages/ui-patterns/src/CodeBlock/CodeBlock.tsx index f74cbcf0f54..43e2b58ea2e 100644 --- a/packages/ui-patterns/src/CodeBlock/CodeBlock.tsx +++ b/packages/ui-patterns/src/CodeBlock/CodeBlock.tsx @@ -31,27 +31,34 @@ import { Button, cn, copyToClipboard } from 'ui' import { monokaiCustomTheme } from './CodeBlock.utils' -export type CodeBlockLang = - | 'js' - | 'jsx' - | 'sql' - | 'py' - | 'bash' - | 'ts' - | 'dart' - | 'json' - | 'csharp' - | 'kotlin' - | 'curl' - | 'http' - | 'php' - | 'python' - | 'go' - | 'pgsql' - | 'swift' - | 'yaml' - | 'toml' - | 'html' +const codeBlockLangs = [ + 'js', + 'jsx', + 'sql', + 'py', + 'bash', + 'ts', + 'dart', + 'json', + 'csharp', + 'kotlin', + 'curl', + 'http', + 'php', + 'python', + 'go', + 'pgsql', + 'swift', + 'yaml', + 'toml', + 'html', +] as const + +export type CodeBlockLang = (typeof codeBlockLangs)[number] + +export function isCodeBlockLang(lang: string): lang is CodeBlockLang { + return (codeBlockLangs as readonly string[]).includes(lang) +} export interface CodeBlockProps { title?: ReactNode diff --git a/packages/ui-patterns/src/McpUrlBuilder/McpConfigPanel.md.tsx b/packages/ui-patterns/src/McpUrlBuilder/McpConfigPanel.md.tsx new file mode 100644 index 00000000000..87b23d3fe41 --- /dev/null +++ b/packages/ui-patterns/src/McpUrlBuilder/McpConfigPanel.md.tsx @@ -0,0 +1,166 @@ +/** @jsxRuntime automatic */ +/** @jsxImportSource mdast-jsx */ +import type { Blockquote, Content, Paragraph } from 'mdast' +import { remark } from 'remark' +import remarkGfm from 'remark-gfm' + +import { + HOSTED_MCP_URL as HOSTED_URL, + DEFAULT_MCP_URL_NON_PLATFORM as LOCAL_URL, + MCP_CLIENT_DATA, + MCP_CLIENT_GROUPS, +} from './clients.data' +import type { McpClientData } from './clients.data' +import { + calloutVariant, + MCP_CLIENT_INSTRUCTIONS, + type McpCalloutVariant, + type McpInstructionContent, +} from './clients.instructions.md' +import { buildClientConfig } from './utils/getMcpUrl' +import { serializeMcpConfig } from './utils/serializeMcpConfig' + +// Flatten the `as const` group tuples into the union of client keys. +type McpClientKey = (typeof MCP_CLIENT_GROUPS)[number]['keys'][number] + +// The docs document the hosted platform, so instructions render their hosted variant. +const IS_PLATFORM = true + +const CALLOUT_LABELS: Record = { warning: 'Warning', note: 'Note' } + +/** Prefixes a callout blockquote's first paragraph with a bold `Warning:`/`Note:` label. */ +function labelCallout(block: Blockquote, variant: McpCalloutVariant): Blockquote { + const [first, ...rest] = block.children + if (!first || first.type !== 'paragraph') return block + const labeled: Paragraph = { + ...first, + children: [ + { type: 'strong', children: [{ type: 'text', value: `${CALLOUT_LABELS[variant]}:` }] }, + { type: 'text', value: ' ' }, + ...first.children, + ], + } + return { ...block, children: [labeled, ...rest] } +} + +/** + * Adapts a shared instruction tree (see `clients.instructions.md.tsx`) for markdown: + * drops illustrative `image` nodes (the dashboard renders these from a bundled + * local asset; markdown has no published URL to reference, and the prose covers + * the steps) and gives callouts a textual `Warning:`/`Note:` label, since + * markdown has no styling to lean on. + */ +function instructionToMarkdown(tree: McpInstructionContent): Content[] { + const blocks = tree.type === 'root' ? (tree.children as Content[]) : [tree as Content] + const out: Content[] = [] + for (const block of blocks) { + if (block.type === 'image') continue + if (block.type === 'blockquote') { + const variant = calloutVariant(block.data) + if (variant) { + out.push(labelCallout(block, variant)) + continue + } + } + out.push(block) + } + return out +} + +/** One client's section: a bold lead-in, then its install/config/connector steps and auth. */ +function Client({ client }: { client: McpClientData }) { + const instructions = MCP_CLIENT_INSTRUCTIONS[client.key] + const primary = instructions?.primary?.({ isPlatform: IS_PLATFORM, url: HOSTED_URL }) + const alternate = instructions?.alternate?.({ isPlatform: IS_PLATFORM, url: HOSTED_URL }) + const config = client.configFile + ? { + file: client.configFile, + ...serializeMcpConfig(client.configFile, buildClientConfig(HOSTED_URL, client)), + } + : null + + return ( + <> + + {client.label} + + + {instructions?.deepLinkDescription && instructionToMarkdown(instructions.deepLinkDescription)} + + {primary && instructionToMarkdown(primary)} + + {config ? ( + <> + + {primary ? 'Alternatively, add' : 'Add'} this configuration to{' '} + : + + + + ) : !primary && client.externalDocsUrl ? ( + + Available as a connector. Install it from the{' '} + {client.label} directory. + + ) : null} + + {alternate && instructionToMarkdown(alternate)} + + ) +} + +/** + * Static markdown rendering of `` for the docs `.md` build. + * Co-located with the React component and built from the same shared data and + * instruction trees, so the markdown and the dashboard's Connect panel can't + * drift. Authored with the mdast-jsx runtime; consumed by the docs markdown + * pipeline. + */ +export function McpConfigPanel() { + const clientsByKey = new Map(MCP_CLIENT_DATA.map((c) => [c.key, c])) + + return ( + <> + + The hosted Supabase MCP server is available at . If + you're developing locally with the Supabase CLI, use {' '} + instead. + + + Find your client below and add the configuration shown. You can scope the server by + appending URL query parameters: to limit it to a + single project, to allow only read queries, and{' '} + to enable specific tool groups. + + + {MCP_CLIENT_GROUPS.map((group) => ( + <> + {group.heading} + {group.keys.map((key: McpClientKey) => { + const client = clientsByKey.get(key) + return client ? : null + })} + + ))} + + + Authentication + + + Some MCP clients automatically prompt you to log in during setup, while others require + manual authentication steps. Either way, a browser window opens where you log in to your + Supabase account and grant the MCP client access to your organization. + + + A personal access token (PAT) was previously required, but is no longer needed. + + + ) +} + +export function mcpConfigPanelMarkdown(): string { + const processor = remark().use(remarkGfm) + return processor + .stringify(McpConfigPanel() as unknown as Parameters[0]) + .trim() +} diff --git a/packages/ui-patterns/src/McpUrlBuilder/McpConfigPanel.tsx b/packages/ui-patterns/src/McpUrlBuilder/McpConfigPanel.tsx index dfeb537ac2f..1136ea1936e 100644 --- a/packages/ui-patterns/src/McpUrlBuilder/McpConfigPanel.tsx +++ b/packages/ui-patterns/src/McpUrlBuilder/McpConfigPanel.tsx @@ -5,15 +5,15 @@ import { cn, Separator } from 'ui' import { CodeBlock } from 'ui-patterns/CodeBlock' import { InfoTooltip } from '../info-tooltip' -import { ClientSelectDropdown } from './components/ClientSelectDropdown' -import { McpConfigurationDisplay } from './components/McpConfigurationDisplay' -import { McpConfigurationOptions } from './components/McpConfigurationOptions' import { FEATURE_GROUPS_NON_PLATFORM, FEATURE_GROUPS_PLATFORM, MCP_CLIENT_GROUPS, - MCP_CLIENTS, -} from './constants' +} from './clients.data' +import { ClientSelectDropdown } from './components/ClientSelectDropdown' +import { McpConfigurationDisplay } from './components/McpConfigurationDisplay' +import { McpConfigurationOptions } from './components/McpConfigurationOptions' +import { MCP_CLIENTS } from './mcpClients' import type { McpClient, McpOnCopyCallback } from './types' import { getMcpUrl } from './utils/getMcpUrl' diff --git a/packages/ui-patterns/src/McpUrlBuilder/clients.data.ts b/packages/ui-patterns/src/McpUrlBuilder/clients.data.ts new file mode 100644 index 00000000000..06e5dd64f0b --- /dev/null +++ b/packages/ui-patterns/src/McpUrlBuilder/clients.data.ts @@ -0,0 +1,390 @@ +import type { + AntigravityMcpConfig, + ClaudeCodeMcpConfig, + CodexMcpConfig, + CopilotMcpConfig, + FactoryMcpConfig, + GeminiMcpConfig, + GooseMcpConfig, + McpClientBaseConfig, + McpClientConfig, + McpClientDeepLinkOptions, + McpFeatureGroup, + OpenCodeMcpConfig, + VSCodeMcpConfig, + WindsurfMcpConfig, +} from './types' +import { getMcpUrl } from './types' + +export const FEATURE_GROUPS_PLATFORM: McpFeatureGroup[] = [ + { + id: 'docs', + name: 'Documentation', + description: 'Access Supabase documentation and guides', + }, + { + id: 'account', + name: 'Account', + description: 'Manage account settings and preferences', + }, + { + id: 'database', + name: 'Database', + description: 'Query and manage database schema and data', + }, + { + id: 'debugging', + name: 'Debugging', + description: 'Debug and troubleshoot issues', + }, + { + id: 'development', + name: 'Development', + description: 'Development tools and utilities', + }, + { + id: 'functions', + name: 'Functions', + description: 'Manage and deploy Edge Functions', + }, + { + id: 'branching', + name: 'Branching', + description: 'Manage database branches', + }, + { + id: 'storage', + name: 'Storage', + description: 'Manage files and storage buckets', + }, +] + +export const FEATURE_GROUPS_NON_PLATFORM = FEATURE_GROUPS_PLATFORM.filter((group) => + ['docs', 'database', 'development', 'debugging'].includes(group.id) +) + +/** React-free subset of McpClient - safe for Node/build scripts. */ +export interface McpClientData { + key: string + label: string + icon?: string + /** When true, use -icon-dark.svg in dark theme; otherwise the same -icon.svg is used for both themes. */ + hasDistinctDarkIcon?: boolean + configFile?: string + externalDocsUrl?: string + transformConfig?: (config: McpClientBaseConfig) => McpClientConfig + generateDeepLink?: (config: McpClientConfig, options?: McpClientDeepLinkOptions) => string | null +} + +/** All MCP clients - React-free data only (no JSX fields). */ +export const MCP_CLIENT_DATA: McpClientData[] = [ + { + key: 'claude-code', + label: 'Claude Code', + icon: 'claude', + configFile: '.mcp.json', + externalDocsUrl: 'https://code.claude.com/docs/en/mcp', + transformConfig: (config): ClaudeCodeMcpConfig => { + return { + mcpServers: { + supabase: { + type: 'http', + url: config.mcpServers.supabase.url, + }, + }, + } + }, + }, + { + key: 'cursor', + label: 'Cursor', + icon: 'cursor', + hasDistinctDarkIcon: true, + configFile: '.cursor/mcp.json', + externalDocsUrl: 'https://docs.cursor.com/context/mcp', + generateDeepLink: (config) => { + const name = 'supabase' + const mcpUrl = getMcpUrl(config) + const serverConfig = { + url: mcpUrl, + } + const base64Config = btoa(JSON.stringify(serverConfig)) + return `cursor://anysphere.cursor-deeplink/mcp/install?name=${name}&config=${encodeURIComponent(base64Config)}` + }, + }, + { + key: 'vscode', + label: 'VS Code', + icon: 'vscode', + configFile: '.vscode/mcp.json', + externalDocsUrl: 'https://code.visualstudio.com/docs/copilot/chat/mcp-servers', + transformConfig: (config): VSCodeMcpConfig => { + return { + servers: { + supabase: { + type: 'http', + url: config.mcpServers.supabase.url, + }, + }, + } + }, + generateDeepLink: (_config) => { + const config = _config as VSCodeMcpConfig + const mcpConfig = { name: 'supabase', ...config.servers.supabase } + + return `vscode:mcp/install?${encodeURIComponent(JSON.stringify(mcpConfig))}` + }, + }, + { + key: 'codex', + label: 'Codex', + icon: 'openai', + hasDistinctDarkIcon: true, + configFile: '~/.codex/config.toml', + externalDocsUrl: 'https://developers.openai.com/codex/mcp/', + transformConfig: (config): CodexMcpConfig => { + return { + mcp_servers: { + supabase: { + url: config.mcpServers.supabase.url, + }, + }, + } + }, + }, + { + key: 'gemini-cli', + label: 'Gemini CLI', + icon: 'gemini-cli', + configFile: '.gemini/settings.json', + externalDocsUrl: 'https://geminicli.com/docs/tools/mcp-server/', + transformConfig: (config): GeminiMcpConfig => { + return { + mcpServers: { + supabase: { + httpUrl: config.mcpServers.supabase.url, + }, + }, + } + }, + }, + { + key: 'copilot-cli', + label: 'GitHub Copilot', + icon: 'copilot', + hasDistinctDarkIcon: true, + configFile: '~/.copilot/mcp-config.json', + externalDocsUrl: + 'https://docs.github.com/en/copilot/how-tos/copilot-cli/customize-copilot/add-mcp-servers', + transformConfig: (config): CopilotMcpConfig => { + return { + mcpServers: { + supabase: { + type: 'http', + url: config.mcpServers.supabase.url, + }, + }, + } + }, + }, + { + key: 'antigravity', + label: 'Antigravity', + icon: 'antigravity', + configFile: '~/.gemini/antigravity/mcp_config.json', + externalDocsUrl: 'https://antigravity.google/docs/mcp', + transformConfig: (config): AntigravityMcpConfig => { + return { + mcpServers: { + supabase: { + serverUrl: config.mcpServers.supabase.url, + }, + }, + } + }, + }, + { + key: 'windsurf', + label: 'Windsurf', + icon: 'windsurf', + hasDistinctDarkIcon: true, + configFile: '~/.codeium/windsurf/mcp_config.json', + externalDocsUrl: '', + transformConfig: (config): WindsurfMcpConfig => { + return { + mcpServers: { + supabase: { + command: 'npx', + args: ['-y', 'mcp-remote', config.mcpServers.supabase.url], + }, + }, + } + }, + }, + { + key: 'goose', + label: 'Goose', + icon: 'goose', + hasDistinctDarkIcon: true, + configFile: '~/.config/goose/config.yaml', + externalDocsUrl: 'https://block.github.io/goose/docs/category/getting-started', + transformConfig: (config): GooseMcpConfig => { + return { + extensions: { + supabase: { + available_tools: [], + bundled: null, + description: + 'Connect your Supabase projects to AI assistants. Manage tables, query data, deploy Edge Functions, and interact with your Supabase backend directly from your MCP client.', + enabled: true, + env_keys: [], + envs: {}, + headers: {}, + name: 'Supabase', + timeout: 300, + type: 'streamable_http', + uri: config.mcpServers.supabase.url, + }, + }, + } + }, + generateDeepLink: (config) => { + const name = 'supabase' + const mcpUrl = getMcpUrl(config) + return `goose://extension?type=streamable_http&url=${encodeURIComponent(mcpUrl)}&id=supabase&name=${name}&description=${encodeURIComponent('Connect your Supabase projects to AI assistants. Manage tables, query data, deploy Edge Functions, and interact with your Supabase backend directly from your MCP client.')}` + }, + }, + { + key: 'factory', + label: 'Factory', + icon: 'factory', + hasDistinctDarkIcon: true, + configFile: '~/.factory/mcp.json', + externalDocsUrl: 'https://docs.factory.ai/cli/configuration/mcp.md', + transformConfig: (config): FactoryMcpConfig => { + return { + mcpServers: { + supabase: { + type: 'http', + url: config.mcpServers.supabase.url, + }, + }, + } + }, + }, + { + key: 'opencode', + label: 'OpenCode', + icon: 'opencode', + hasDistinctDarkIcon: true, + configFile: '~/.config/opencode/opencode.json', + externalDocsUrl: 'https://opencode.ai/docs/mcp-servers/', + transformConfig: (config): OpenCodeMcpConfig => { + const mcpUrl = getMcpUrl(config) + return { + $schema: 'https://opencode.ai/config.json', + mcp: { + supabase: { + type: 'remote', + url: mcpUrl, + enabled: true, + }, + }, + } + }, + }, + { + key: 'kiro', + label: 'Kiro', + icon: 'kiro', + configFile: '~/.kiro/settings/mcp.json', + externalDocsUrl: 'https://kiro.dev/docs/mcp/', + generateDeepLink: (_config, options) => { + const power = options?.isPlatform ? 'supabase-hosted' : 'supabase-local' + return `https://kiro.dev/launch/powers/${power}` + }, + }, + { + key: 'claude-ai', + label: 'Claude.ai', + icon: 'claude', + externalDocsUrl: 'https://claude.com/docs/connectors/overview', + generateDeepLink: () => + 'https://claude.ai/directory/connectors/11ca66fc-1e98-49d5-ab9b-7cb4672a8f10', + }, + { + key: 'chatgpt', + label: 'ChatGPT', + icon: 'openai', + hasDistinctDarkIcon: true, + externalDocsUrl: 'https://chatgpt.com/features/apps/', + generateDeepLink: () => + 'https://chatgpt.com/apps/supabase/asdk_app_69d3e5ee6a708191baa733f7b8931995', + }, +] + +/** Terminal commands for clients that can be configured via CLI. */ +export interface McpCliCommands { + /** Builds the install command for a given MCP server URL. */ + install?: (url: string) => string + /** Command to authenticate the configured server. */ + authenticate?: string +} + +/** + * CLI install/auth commands keyed by client key. Single source of truth shared + * by the dashboard's Connect panel and the generated markdown docs, so the two + * never drift. Clients absent here are configured via file only. + */ +export const MCP_CLI_COMMANDS: Record = { + 'claude-code': { + install: (url) => `claude mcp add --scope project --transport http supabase "${url}"`, + authenticate: 'claude /mcp', + }, + codex: { + install: (url) => `codex mcp add supabase --url "${url}"`, + authenticate: 'codex mcp login supabase', + }, + 'gemini-cli': { + install: (url) => `gemini mcp add -t http supabase "${url}"`, + authenticate: '/mcp auth supabase', + }, + 'copilot-cli': { + install: (url) => `copilot mcp add --transport http supabase "${url}"`, + authenticate: 'copilot -i /mcp', + }, + goose: { + install: (url) => `goose session --with-streamable-http-extension "${url}"`, + }, + factory: { + install: (url) => `droid mcp add supabase "${url}" --type http`, + }, + opencode: { + authenticate: 'opencode mcp auth supabase', + }, +} + +export const MCP_CLIENT_GROUPS = [ + { + heading: 'AI Agent CLI', + keys: ['claude-code', 'codex', 'gemini-cli', 'copilot-cli', 'opencode', 'factory'], + }, + { + heading: 'Web Clients', + keys: ['claude-ai', 'chatgpt', 'goose'], + }, + { + heading: 'IDE', + keys: ['cursor', 'vscode', 'antigravity', 'kiro', 'windsurf'], + }, +] as const + +export const DEFAULT_MCP_URL_PLATFORM = 'http://localhost:8080/mcp' +export const DEFAULT_MCP_URL_NON_PLATFORM = 'http://localhost:54321/mcp' + +/** + * Production hosted MCP server URL. The dashboard resolves the hosted URL + * per-environment via `NEXT_PUBLIC_MCP_URL`; this is the canonical value the + * docs document (and what to fall back to in production). + */ +export const HOSTED_MCP_URL = 'https://mcp.supabase.com/mcp' diff --git a/packages/ui-patterns/src/McpUrlBuilder/clients.instructions.md.tsx b/packages/ui-patterns/src/McpUrlBuilder/clients.instructions.md.tsx new file mode 100644 index 00000000000..be200091d4e --- /dev/null +++ b/packages/ui-patterns/src/McpUrlBuilder/clients.instructions.md.tsx @@ -0,0 +1,239 @@ +/** @jsxRuntime automatic */ +/** @jsxImportSource mdast-jsx */ +import type { Content, Root } from 'mdast' + +import { MCP_CLI_COMMANDS } from './clients.data' + +/** + * Per-client setup instructions, authored once as mdast (via the mdast-jsx + * runtime) so a single source feeds both surfaces: + * + * - the dashboard's Connect panel, via the React adapter in + * `components/InstructionBlocks.tsx` (mdast -> React), and + * - the generated markdown docs, via `McpConfigPanel.md.tsx` (mdast -> markdown). + * + * Both render the same tree, so the prose can't drift. Editing a client is a + * content-only change here. This module stays React-free (it emits plain mdast + * objects), so the Node markdown build can import it. + */ + +/** A single mdast node, or a Fragment-produced `root` wrapping several. */ +export type McpInstructionContent = Root | Content + +export interface InstructionOptions { + /** Hosted platform (docs) vs self-hosted/local. Switches a few steps. */ + isPlatform: boolean + /** Resolved MCP server URL, interpolated into command snippets. */ + url: string +} + +export interface McpClientInstructions { + /** Steps shown before the config-file block (e.g. CLI install). */ + primary?: (options: InstructionOptions) => McpInstructionContent + /** Steps shown after the config-file block (e.g. authentication). */ + alternate?: (options: InstructionOptions) => McpInstructionContent + /** Inline description shown above a deep-link/connector install. */ + deepLinkDescription?: McpInstructionContent +} + +export type McpCalloutVariant = 'warning' | 'note' + +/** + * Reads the callout variant off a blockquote's `data`. Callouts are authored as + * `
`; the variant lives in `data` + * (in-memory only - it does not survive markdown serialization), so each adapter + * materializes it: the dashboard styles the blockquote, the markdown adapter + * prefixes a label. + */ +export const calloutVariant = (data: unknown): McpCalloutVariant | undefined => + (data as { callout?: McpCalloutVariant } | undefined)?.callout + +const GEMINI_EXTENSION_URL = 'https://github.com/supabase-community/gemini-extension' + +export const MCP_CLIENT_INSTRUCTIONS: Record = { + 'claude-code': { + primary: ({ url }) => ( + <> + Add the MCP server to your project config using the command line: + + + ), + alternate: () => ( + <> + + After configuring the MCP server, you need to authenticate. In a regular terminal (not the + IDE extension) run: + + + + Select the "supabase" server, then "Authenticate" to begin the authentication flow. + + + ), + }, + codex: { + primary: ({ url }) => ( + <> + Add the Supabase MCP server to Codex: + + + ), + alternate: () => ( + <> + Authenticate with the MCP server: + + + Finally, run inside Codex to verify authentication. + + + ), + }, + 'gemini-cli': { + primary: ({ isPlatform, url }) => ( + <> +
+ + Ensure you are running Gemini CLI version or higher. + +
+ {isPlatform ? ( + <> + + Install the Supabase extension for Gemini CLI. + This bundles the Supabase MCP server connection,{' '} + agent skills, and other + context. + + + Or add just the MCP server to Gemini CLI: + + ) : ( + Add the Supabase MCP server to Gemini CLI: + )} + + + ), + alternate: () => ( + <> + + After installation, start the Gemini CLI and run the following command to authenticate the + server: + + + + ), + }, + 'copilot-cli': { + primary: ({ url }) => ( + <> + + Add the MCP server to your GitHub Copilot config using the command line: + + + + ), + alternate: () => ( + <> + After configuring the MCP server, authenticate by running: + + + Follow the on-screen instructions to complete the authentication flow. + + + ), + }, + antigravity: { + alternate: () => ( + <> + + After saving the config, restart Antigravity. It will prompt you to complete the OAuth + flow to authenticate with Supabase. + + + To edit the config from within Antigravity, click the ··· menu at the top + of the Agent pane > MCP Servers >{' '} + Manage MCP Servers > View raw config. From the Manage + MCP Servers page you can also Refresh server configs and enable/disable + servers. + + + If you run into authentication issues, open Agent Settings with Cmd+,{' '} + (Mac) or Ctrl+, (Windows/Linux), navigate to the{' '} + Customizations tab, and click the Authenticate button + next to the Supabase server. + + Antigravity MCP server settings showing the Authenticate button next to the Supabase server + + ), + }, + windsurf: { + primary: () => ( +
+ + Ensure you are running Windsurf version or higher. + +
+ ), + alternate: () => ( + + Windsurf does not currently support remote MCP servers over HTTP transport. You need to use + the mcp-remote package as a proxy. + + ), + }, + goose: { + primary: ({ url }) => ( + <> + Start a Goose session with the Supabase extension: + + + ), + alternate: () => ( + + For more details, see{' '} + + Using Extensions + {' '} + in Goose. + + ), + }, + factory: { + primary: ({ url }) => ( + <> + Add Supabase MCP server to Factory: + + + ), + alternate: () => ( + + Restart Factory or type within droid to complete OAuth + authentication flow. + + ), + }, + opencode: { + alternate: () => ( + <> + + After adding the configuration, run the following command to authenticate: + + + + This will open your browser to complete the OAuth authentication flow. + + + ), + }, + kiro: { + deepLinkDescription: ( + + Install the Supabase power for Kiro. This + bundles the Supabase MCP server and steering files for best practices. + + ), + }, +} diff --git a/packages/ui-patterns/src/McpUrlBuilder/components/InstructionBlocks.tsx b/packages/ui-patterns/src/McpUrlBuilder/components/InstructionBlocks.tsx new file mode 100644 index 00000000000..50031722b3e --- /dev/null +++ b/packages/ui-patterns/src/McpUrlBuilder/components/InstructionBlocks.tsx @@ -0,0 +1,135 @@ +'use client' + +import type { Content } from 'mdast' +import Image, { type StaticImageData } from 'next/image' +import { Fragment, type ReactNode } from 'react' +import { cn } from 'ui' +import { CodeBlock, isCodeBlockLang } from 'ui-patterns/CodeBlock' + +import antigravityAuthenticateScreenshot from '../assets/antigravity-authenticate-screenshot.png' +import { calloutVariant, type McpInstructionContent } from '../clients.instructions.md' +import type { McpOnCopyCallback } from '../types' + +/** + * Local image assets referenced by `image` nodes, keyed by the node's `url` + * (used as an asset id, not a real URL). Kept here so the React-free + * instructions module stays importable by the Node markdown build, which can't + * resolve static png imports. + */ +const INSTRUCTION_ASSETS: Record = { + 'antigravity-auth': antigravityAuthenticateScreenshot, +} + +/** + * Per-node-type renderers, keyed by the mdast node's `type`. The mapped type + * narrows each renderer's `node` to that type (e.g. `link` receives a `Link`). + * A node type with no renderer (and its subtree) is skipped. Renderers don't deal + * with React keys - `renderList` applies those. + */ +type Renderers = { + [K in Content['type']]?: (node: Extract, children: ReactNode[]) => ReactNode +} + +/** Recursively renders an mdast node (see `clients.instructions.md.tsx`) as React. */ +function renderNode(node: Content, renderers: Renderers): ReactNode { + // The mapped type guarantees renderers[node.type] matches node; the index + // can't prove it to the compiler, so widen at this single dispatch point. + const renderer = renderers[node.type] as + | ((node: Content, children: ReactNode[]) => ReactNode) + | undefined + if (!renderer) return null + const children = 'children' in node ? renderList(node.children as Content[], renderers) : [] + return renderer(node, children) +} + +/** Renders a list of sibling nodes, applying React keys so renderers don't have to. */ +function renderList(nodes: Content[], renderers: Renderers): ReactNode[] { + return nodes.map((node, i) => {renderNode(node, renderers)}) +} + +/** Builds the per-node-type renderer map for rendering mdast as React. Commands + * become copyable `CodeBlock`s, callouts colored text, and `image` nodes resolve + * their asset id to a bundled screenshot. */ +function reactRenderers(onCopy: (type?: McpOnCopyCallback) => void): Renderers { + const renderers: Renderers = { + text: (node) => node.value, + inlineCode: (node) => {node.value}, + strong: (_node, children) => {children}, + emphasis: (_node, children) => {children}, + link: (node, children) => ( + + {children} + + ), + paragraph: (_node, children) =>

{children}

, + code: (node) => ( + onCopy('command')} + /> + ), + image: (node) => { + const src = INSTRUCTION_ASSETS[node.url] + return src ? ( + {node.alt + ) : null + }, + blockquote: (node) => { + // Callouts render as colored inline text (no quote chrome). The variant + // lives in `data`; children render inline so the color isn't overridden. + const variant = calloutVariant(node.data) + return ( +

+ {renderList(node.children as Content[], { + ...renderers, + paragraph: (_, children) => children, + })} +

+ ) + }, + } + return renderers +} + +/** A node, or the children of a Fragment-produced `root`. */ +const toNodes = (tree: McpInstructionContent): Content[] => + tree.type === 'root' ? (tree.children as Content[]) : [tree as Content] + +interface InstructionContentProps { + /** Instruction tree from `MCP_CLIENT_INSTRUCTIONS` (commands pre-resolved). */ + tree: McpInstructionContent + onCopy: (type?: McpOnCopyCallback) => void +} + +/** + * Renders an instruction tree (see `clients.instructions.md.tsx`) as React, for + * every surface that shows the live component (Studio's Connect sheet, the docs + * website). The generated `.md` docs render the same tree via their own mdast + * adapter, so the two stay in sync. + */ +export function InstructionContent({ tree, onCopy }: InstructionContentProps) { + return
{renderList(toNodes(tree), reactRenderers(onCopy))}
+} + +/** Renders a tree's text inline, for the deep-link description label. */ +export function InlineContent({ tree }: { tree: McpInstructionContent }) { + const renderers: Renderers = { + ...reactRenderers(() => {}), + paragraph: (_, children) => children, + } + return renderList(toNodes(tree), renderers) +} diff --git a/packages/ui-patterns/src/McpUrlBuilder/components/McpConfigurationDisplay.tsx b/packages/ui-patterns/src/McpUrlBuilder/components/McpConfigurationDisplay.tsx index bc60bc489a2..0a8feb04d6f 100644 --- a/packages/ui-patterns/src/McpUrlBuilder/components/McpConfigurationDisplay.tsx +++ b/packages/ui-patterns/src/McpUrlBuilder/components/McpConfigurationDisplay.tsx @@ -1,7 +1,5 @@ 'use client' -import { stringify as stringifyToml } from '@std/toml/stringify' -import yaml from 'js-yaml' import { ExternalLink } from 'lucide-react' import Image from 'next/image' import { Button, cn } from 'ui' @@ -9,6 +7,7 @@ import { CodeBlock, type CodeBlockLang } from 'ui-patterns/CodeBlock' import type { McpClient, McpClientConfig, McpOnCopyCallback } from '../types' import { getMcpButtonData } from '../utils/getMcpButtonData' +import { serializeMcpConfig } from '../utils/serializeMcpConfig' interface McpConfigurationDisplayProps { selectedClient: McpClient @@ -20,8 +19,6 @@ interface McpConfigurationDisplayProps { isPlatform?: boolean } -type ConfigFormat = CodeBlockLang | 'toml' - export function McpConfigurationDisplay({ selectedClient, clientConfig, @@ -38,33 +35,10 @@ export function McpConfigurationDisplay({ isPlatform, }) - // Extract file extension and determine format - const fileExtension = selectedClient.configFile?.split('.').pop()?.toLowerCase() - // If the file extension is not 'json', 'yaml', or 'toml', default to 'txt' - let configFormat: ConfigFormat | undefined - if (['json', 'yaml', 'toml'].includes(fileExtension ?? '')) { - configFormat = fileExtension as ConfigFormat - } - - // Serialize config based on format - let configValue: string - switch (configFormat) { - case 'yaml': - configValue = yaml.dump(clientConfig, { indent: 2, lineWidth: -1 }) - break - case 'toml': - configValue = stringifyToml(clientConfig as Record).trim() - break - case 'json': - configValue = JSON.stringify(clientConfig, null, 2) - break - default: - configValue = String(clientConfig) - } - - // Toml will default to undefined display language + const { lang, value: configValue } = serializeMcpConfig(selectedClient.configFile, clientConfig) + // TOML has no CodeBlock highlighter, so render it without a language. const displayLanguage: CodeBlockLang | undefined = - configFormat === 'toml' ? undefined : configFormat + lang === 'toml' ? undefined : (lang as CodeBlockLang) return (
diff --git a/packages/ui-patterns/src/McpUrlBuilder/constants.tsx b/packages/ui-patterns/src/McpUrlBuilder/constants.tsx deleted file mode 100644 index f9eeb166c06..00000000000 --- a/packages/ui-patterns/src/McpUrlBuilder/constants.tsx +++ /dev/null @@ -1,638 +0,0 @@ -import Image from 'next/image' -import { CodeBlock } from 'ui-patterns/CodeBlock' - -import antigravityAuthenticateScreenshot from './assets/antigravity-authenticate-screenshot.png' -import type { - AntigravityMcpConfig, - ClaudeCodeMcpConfig, - CodexMcpConfig, - CopilotMcpConfig, - FactoryMcpConfig, - GeminiMcpConfig, - GooseMcpConfig, - McpClient, - McpFeatureGroup, - OpenCodeMcpConfig, - VSCodeMcpConfig, - WindsurfMcpConfig, -} from './types' -import { getMcpUrl } from './types' - -export const FEATURE_GROUPS_PLATFORM: McpFeatureGroup[] = [ - { - id: 'docs', - name: 'Documentation', - description: 'Access Supabase documentation and guides', - }, - { - id: 'account', - name: 'Account', - description: 'Manage account settings and preferences', - }, - { - id: 'database', - name: 'Database', - description: 'Query and manage database schema and data', - }, - { - id: 'debugging', - name: 'Debugging', - description: 'Debug and troubleshoot issues', - }, - { - id: 'development', - name: 'Development', - description: 'Development tools and utilities', - }, - { - id: 'functions', - name: 'Functions', - description: 'Manage and deploy Edge Functions', - }, - { - id: 'branching', - name: 'Branching', - description: 'Manage database branches', - }, - { - id: 'storage', - name: 'Storage', - description: 'Manage files and storage buckets', - }, -] - -export const FEATURE_GROUPS_NON_PLATFORM = FEATURE_GROUPS_PLATFORM.filter((group) => - ['docs', 'database', 'development', 'debugging'].includes(group.id) -) - -/** Only set hasDistinctDarkIcon: true when the client has a separate -icon-dark.svg that looks different. Otherwise the same -icon.svg is used for both themes. */ -export const MCP_CLIENTS: McpClient[] = [ - { - key: 'claude-code', - label: 'Claude Code', - icon: 'claude', - configFile: '.mcp.json', - externalDocsUrl: 'https://code.claude.com/docs/en/mcp', - transformConfig: (config): ClaudeCodeMcpConfig => { - return { - mcpServers: { - supabase: { - type: 'http', - url: config.mcpServers.supabase.url, - }, - }, - } - }, - primaryInstructions: (_config, onCopy) => { - const config = _config as ClaudeCodeMcpConfig - const command = `claude mcp add --scope project --transport http supabase "${config.mcpServers.supabase.url}"` - return ( -
-

- Add the MCP server to your project config using the command line: -

- onCopy('command')} - /> -
- ) - }, - alternateInstructions: (_config, onCopy) => ( -
-

- After configuring the MCP server, you need to authenticate. In a regular terminal (not the - IDE extension) run: -

- onCopy('command')} - /> -

- Select the "supabase" server, then "Authenticate" to begin the authentication flow. -

-
- ), - }, - { - key: 'cursor', - label: 'Cursor', - icon: 'cursor', - hasDistinctDarkIcon: true, - configFile: '.cursor/mcp.json', - externalDocsUrl: 'https://docs.cursor.com/context/mcp', - generateDeepLink: (config) => { - const name = 'supabase' - const mcpUrl = getMcpUrl(config) - const serverConfig = { - url: mcpUrl, - } - const base64Config = Buffer.from(JSON.stringify(serverConfig)).toString('base64') - return `cursor://anysphere.cursor-deeplink/mcp/install?name=${name}&config=${encodeURIComponent(base64Config)}` - }, - }, - { - key: 'vscode', - label: 'VS Code', - icon: 'vscode', - configFile: '.vscode/mcp.json', - externalDocsUrl: 'https://code.visualstudio.com/docs/copilot/chat/mcp-servers', - transformConfig: (config): VSCodeMcpConfig => { - return { - servers: { - supabase: { - type: 'http', - url: config.mcpServers.supabase.url, - }, - }, - } - }, - generateDeepLink: (_config) => { - const config = _config as VSCodeMcpConfig - const mcpConfig = { name: 'supabase', ...config.servers.supabase } - - return `vscode:mcp/install?${encodeURIComponent(JSON.stringify(mcpConfig))}` - }, - }, - { - key: 'codex', - label: 'Codex', - icon: 'openai', - hasDistinctDarkIcon: true, - configFile: '~/.codex/config.toml', - externalDocsUrl: 'https://developers.openai.com/codex/mcp/', - transformConfig: (config): CodexMcpConfig => { - return { - mcp_servers: { - supabase: { - url: config.mcpServers.supabase.url, - }, - }, - } - }, - primaryInstructions: (config, onCopy) => { - const mcpUrl = getMcpUrl(config) - const command = `codex mcp add supabase --url "${mcpUrl}"` - return ( -
-

Add the Supabase MCP server to Codex:

- onCopy('command')} - /> -
- ) - }, - alternateInstructions: (_config, onCopy) => ( -
-

Authenticate with the MCP server:

- onCopy('command')} - /> -

- Finally, run /mcp inside Codex to verify authentication. -

-
- ), - }, - { - key: 'gemini-cli', - label: 'Gemini CLI', - icon: 'gemini-cli', - configFile: '.gemini/settings.json', - externalDocsUrl: 'https://geminicli.com/docs/tools/mcp-server/', - transformConfig: (config): GeminiMcpConfig => { - return { - mcpServers: { - supabase: { - httpUrl: config.mcpServers.supabase.url, - }, - }, - } - }, - primaryInstructions: (config, onCopy, options) => { - const mcpUrl = getMcpUrl(config) - const mcpCommand = `gemini mcp add -t http supabase ${mcpUrl}` - return ( -
-

- Ensure you are running Gemini CLI version 0.20.2 or higher. -

- {options?.isPlatform && ( - <> -

- Install the Supabase{' '} - - extension - {' '} - for Gemini CLI. This bundles the Supabase MCP server connection,{' '} - - agent skills - - , and other context. -

- onCopy('command')} - /> -

- Or add just the MCP server to Gemini CLI: -

- - )} - {!options?.isPlatform && ( -

- Add the Supabase MCP server to Gemini CLI: -

- )} - onCopy('command')} - /> -
- ) - }, - alternateInstructions: (_config, onCopy) => { - return ( -
-

- After installation, start the Gemini CLI and run the following command to authenticate - the server: -

- onCopy('command')} - /> -
- ) - }, - }, - { - key: 'copilot-cli', - label: 'GitHub Copilot', - icon: 'copilot', - hasDistinctDarkIcon: true, - configFile: '~/.copilot/mcp-config.json', - externalDocsUrl: - 'https://docs.github.com/en/copilot/how-tos/copilot-cli/customize-copilot/add-mcp-servers', - transformConfig: (config): CopilotMcpConfig => { - return { - mcpServers: { - supabase: { - type: 'http', - url: config.mcpServers.supabase.url, - }, - }, - } - }, - primaryInstructions: (_config, onCopy) => { - const config = _config as CopilotMcpConfig - const command = `copilot mcp add --transport http supabase "${config.mcpServers.supabase.url}"` - return ( -
-

- Add the MCP server to your GitHub Copilot config using the command line: -

- onCopy('command')} - /> -
- ) - }, - alternateInstructions: (_config, onCopy) => ( -
-

- After configuring the MCP server, authenticate by running: -

- onCopy('command')} - /> -

- Follow the on-screen instructions to complete the authentication flow. -

-
- ), - }, - { - key: 'antigravity', - label: 'Antigravity', - icon: 'antigravity', - configFile: '~/.gemini/antigravity/mcp_config.json', - externalDocsUrl: 'https://antigravity.google/docs/mcp', - transformConfig: (config): AntigravityMcpConfig => { - return { - mcpServers: { - supabase: { - serverUrl: config.mcpServers.supabase.url, - }, - }, - } - }, - alternateInstructions: (_config, _onCopy) => ( -
-

- After saving the config, restart Antigravity. It will prompt you to complete the OAuth - flow to authenticate with Supabase. -

-

- To edit the config from within Antigravity, click the ··· menu at the top - of the Agent pane > MCP Servers >{' '} - Manage MCP Servers > View raw config. From the Manage - MCP Servers page you can also Refresh server configs and enable/disable - servers. -

-

- If you run into authentication issues, open Agent Settings with Cmd+,{' '} - (Mac) or Ctrl+, (Windows/Linux), navigate to the{' '} - Customizations tab, and click the Authenticate button - next to the Supabase server. -

- Antigravity MCP server settings showing the Authenticate button next to the Supabase server -
- ), - }, - { - key: 'windsurf', - label: 'Windsurf', - icon: 'windsurf', - hasDistinctDarkIcon: true, - configFile: '~/.codeium/windsurf/mcp_config.json', - externalDocsUrl: '', - transformConfig: (config): WindsurfMcpConfig => { - return { - mcpServers: { - supabase: { - command: 'npx', - args: ['-y', 'mcp-remote', config.mcpServers.supabase.url], - }, - }, - } - }, - primaryInstructions: (_config, _onCopy) => ( -

- Ensure you are running Windsurf version 0.1.37 or higher. -

- ), - alternateInstructions: (_config, _onCopy) => ( -

- Windsurf does not currently support remote MCP servers over HTTP transport. You need to use - the mcp-remote package as a proxy. -

- ), - }, - { - key: 'goose', - label: 'Goose', - icon: 'goose', - hasDistinctDarkIcon: true, - configFile: '~/.config/goose/config.yaml', - externalDocsUrl: 'https://block.github.io/goose/docs/category/getting-started', - transformConfig: (config): GooseMcpConfig => { - return { - extensions: { - supabase: { - available_tools: [], - bundled: null, - description: - 'Connect your Supabase projects to AI assistants. Manage tables, query data, deploy Edge Functions, and interact with your Supabase backend directly from your MCP client.', - enabled: true, - env_keys: [], - envs: {}, - headers: {}, - name: 'Supabase', - timeout: 300, - type: 'streamable_http', - uri: config.mcpServers.supabase.url, - }, - }, - } - }, - generateDeepLink: (config) => { - const name = 'supabase' - const mcpUrl = getMcpUrl(config) - return `goose://extension?type=streamable_http&url=${encodeURIComponent(mcpUrl)}&id=supabase&name=${name}&description=${encodeURIComponent('Connect your Supabase projects to AI assistants. Manage tables, query data, deploy Edge Functions, and interact with your Supabase backend directly from your MCP client.')}` - }, - primaryInstructions: (config, onCopy) => { - const mcpUrl = getMcpUrl(config) - const command = `goose session --with-streamable-http-extension "${mcpUrl}"` - return ( -
-

- Start a Goose session with the Supabase extension: -

- onCopy('command')} - /> -
- ) - }, - alternateInstructions: (_config, _onCopy) => ( -
-

- For more details, see{' '} - - Using Extensions - {' '} - in Goose. -

-
- ), - }, - { - key: 'factory', - label: 'Factory', - icon: 'factory', - hasDistinctDarkIcon: true, - configFile: '~/.factory/mcp.json', - externalDocsUrl: 'https://docs.factory.ai/cli/configuration/mcp.md', - transformConfig: (config): FactoryMcpConfig => { - return { - mcpServers: { - supabase: { - type: 'http', - url: config.mcpServers.supabase.url, - }, - }, - } - }, - primaryInstructions: (config, onCopy) => { - const mcpUrl = getMcpUrl(config) - const command = `droid mcp add supabase ${mcpUrl} --type http` - return ( -
-

Add Supabase MCP server to Factory:

- onCopy('command')} - /> -
- ) - }, - alternateInstructions: (_config, _onCopy) => ( -
-

- Restart Factory or type /mcp within droid to complete OAuth authentication - flow. -

-
- ), - }, - { - key: 'opencode', - label: 'OpenCode', - icon: 'opencode', - hasDistinctDarkIcon: true, - configFile: '~/.config/opencode/opencode.json', - externalDocsUrl: 'https://opencode.ai/docs/mcp-servers/', - transformConfig: (config): OpenCodeMcpConfig => { - const mcpUrl = getMcpUrl(config) - return { - $schema: 'https://opencode.ai/config.json', - mcp: { - supabase: { - type: 'remote', - url: mcpUrl, - enabled: true, - }, - }, - } - }, - alternateInstructions: (_config, onCopy) => ( -
-

- After adding the configuration, run the following command to authenticate: -

- onCopy('command')} - /> -

- This will open your browser to complete the OAuth authentication flow. -

-
- ), - }, - { - key: 'kiro', - label: 'Kiro', - icon: 'kiro', - configFile: '~/.kiro/settings/mcp.json', - externalDocsUrl: 'https://kiro.dev/docs/mcp/', - generateDeepLink: (_config, options) => { - const power = options?.isPlatform ? 'supabase-hosted' : 'supabase-local' - return `https://kiro.dev/launch/powers/${power}` - }, - deepLinkDescription: ( - <> - Install the Supabase{' '} - - power - {' '} - for Kiro. This bundles the Supabase MCP server and steering files for best practices. - - ), - }, - { - key: 'claude-ai', - label: 'Claude.ai', - icon: 'claude', - externalDocsUrl: 'https://claude.com/docs/connectors/overview', - generateDeepLink: () => - 'https://claude.ai/directory/connectors/11ca66fc-1e98-49d5-ab9b-7cb4672a8f10', - }, - { - key: 'chatgpt', - label: 'ChatGPT', - icon: 'openai', - hasDistinctDarkIcon: true, - externalDocsUrl: 'https://chatgpt.com/features/apps/', - generateDeepLink: () => - 'https://chatgpt.com/apps/supabase/asdk_app_69d3e5ee6a708191baa733f7b8931995', - }, -] - -export const MCP_CLIENT_GROUPS = [ - { - heading: 'AI Agent CLI', - keys: ['claude-code', 'codex', 'gemini-cli', 'copilot-cli', 'opencode', 'factory'], - }, - { - heading: 'Web Clients', - keys: ['claude-ai', 'chatgpt', 'goose'], - }, - { - heading: 'IDE', - keys: ['cursor', 'vscode', 'antigravity', 'kiro', 'windsurf'], - }, -] as const - -export const DEFAULT_MCP_URL_PLATFORM = 'http://localhost:8080/mcp' -export const DEFAULT_MCP_URL_NON_PLATFORM = 'http://localhost:54321/mcp' diff --git a/packages/ui-patterns/src/McpUrlBuilder/index.ts b/packages/ui-patterns/src/McpUrlBuilder/index.ts index 0cfa9cc7d5e..fe8d3b96800 100644 --- a/packages/ui-patterns/src/McpUrlBuilder/index.ts +++ b/packages/ui-patterns/src/McpUrlBuilder/index.ts @@ -7,9 +7,11 @@ export { DEFAULT_MCP_URL_NON_PLATFORM, FEATURE_GROUPS_PLATFORM, FEATURE_GROUPS_NON_PLATFORM, - MCP_CLIENTS, MCP_CLIENT_GROUPS, -} from './constants' + MCP_CLIENT_DATA, +} from './clients.data' +export type { McpClientData } from './clients.data' +export { MCP_CLIENTS } from './mcpClients' export { getMcpUrl } from './utils/getMcpUrl' export { getMcpClientIconSrc } from './utils/getMcpIconSrc' export { createMcpCopyHandler, type McpCopyType } from './utils/createMcpCopyHandler' diff --git a/packages/ui-patterns/src/McpUrlBuilder/mcpClients.tsx b/packages/ui-patterns/src/McpUrlBuilder/mcpClients.tsx new file mode 100644 index 00000000000..fc1b7ec7ea1 --- /dev/null +++ b/packages/ui-patterns/src/McpUrlBuilder/mcpClients.tsx @@ -0,0 +1,43 @@ +import { MCP_CLIENT_DATA } from './clients.data' +import { MCP_CLIENT_INSTRUCTIONS } from './clients.instructions.md' +import { InlineContent, InstructionContent } from './components/InstructionBlocks' +import type { McpClient } from './types' +import { getMcpUrl } from './types' + +/** + * The client list for the React surfaces: the React-free `MCP_CLIENT_DATA` joined with the + * per-client instruction trees from `MCP_CLIENT_INSTRUCTIONS`, rendered to UI by + * the `InstructionContent`/`InlineContent` adapters. The markdown docs render the + * same trees via their own adapter, so the two surfaces can't drift. + */ +export const MCP_CLIENTS: McpClient[] = MCP_CLIENT_DATA.map((data) => { + const instructions = MCP_CLIENT_INSTRUCTIONS[data.key] + return { + ...data, + ...(instructions?.primary && { + primaryInstructions: (config, onCopy, options) => ( + + ), + }), + ...(instructions?.alternate && { + alternateInstructions: (config, onCopy, options) => ( + + ), + }), + ...(instructions?.deepLinkDescription && { + deepLinkDescription: , + }), + } +}) diff --git a/packages/ui-patterns/src/McpUrlBuilder/utils/getMcpUrl.ts b/packages/ui-patterns/src/McpUrlBuilder/utils/getMcpUrl.ts index e7ff07dad69..6a4a09e9d86 100644 --- a/packages/ui-patterns/src/McpUrlBuilder/utils/getMcpUrl.ts +++ b/packages/ui-patterns/src/McpUrlBuilder/utils/getMcpUrl.ts @@ -1,5 +1,18 @@ -import { DEFAULT_MCP_URL_NON_PLATFORM, DEFAULT_MCP_URL_PLATFORM } from '../constants' -import type { McpClient, McpClientConfig } from '../types' +import { DEFAULT_MCP_URL_NON_PLATFORM, DEFAULT_MCP_URL_PLATFORM } from '../clients.data' +import type { McpClient, McpClientBaseConfig, McpClientConfig } from '../types' + +/** + * Builds the client config object for a given MCP server URL, applying the + * client's format transform when it has one. Shared by `getMcpUrl` (dashboard) + * and the generated markdown docs, so the config shape lives in one place. + */ +export function buildClientConfig( + url: string, + client?: Pick +): McpClientConfig { + const base: McpClientBaseConfig = { mcpServers: { supabase: { url } } } + return client?.transformConfig ? client.transformConfig(base) : base +} interface GetMcpUrlOptions { projectRef?: string @@ -36,21 +49,9 @@ export function getMcpUrl({ } const mcpUrl = url.toString() - let clientConfig: McpClientConfig = { - mcpServers: { - supabase: { - url: mcpUrl, - }, - }, - } - // Apply client-specific transformation if available - if (selectedClient?.transformConfig) { - clientConfig = selectedClient.transformConfig(clientConfig) - } - return { mcpUrl, - clientConfig, + clientConfig: buildClientConfig(mcpUrl, selectedClient), } } diff --git a/packages/ui-patterns/src/McpUrlBuilder/utils/serializeMcpConfig.ts b/packages/ui-patterns/src/McpUrlBuilder/utils/serializeMcpConfig.ts new file mode 100644 index 00000000000..e1540b41d5d --- /dev/null +++ b/packages/ui-patterns/src/McpUrlBuilder/utils/serializeMcpConfig.ts @@ -0,0 +1,28 @@ +import yaml from 'js-yaml' +import { stringify as stringifyToml } from 'smol-toml' + +import type { McpClientConfig } from '../types' + +/** + * Serializes a client's MCP config to a code-block string, choosing the format + * (and the fence language) from the config file's extension. Shared by the + * dashboard's Connect panel and the generated markdown docs so the two never + * drift. + */ +export function serializeMcpConfig( + configFile: string | undefined, + config: McpClientConfig +): { lang: 'json' | 'yaml' | 'toml'; value: string } { + switch (configFile?.split('.').pop()?.toLowerCase()) { + case 'yaml': + case 'yml': + return { lang: 'yaml', value: yaml.dump(config, { indent: 2, lineWidth: -1 }).trim() } + case 'toml': + return { + lang: 'toml', + value: stringifyToml(config as unknown as Record).trim(), + } + default: + return { lang: 'json', value: JSON.stringify(config, null, 2) } + } +} diff --git a/packages/ui-patterns/src/types/assets.d.ts b/packages/ui-patterns/src/types/assets.d.ts index 31d6fe63736..da4204b01c9 100644 --- a/packages/ui-patterns/src/types/assets.d.ts +++ b/packages/ui-patterns/src/types/assets.d.ts @@ -9,6 +9,6 @@ declare module '*.svg' { } declare module '*.png' { - const content: string + const content: import('next/image').StaticImageData export default content } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 55287cc350f..bb806ad544f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -2530,9 +2530,6 @@ importers: '@monaco-editor/react': specifier: 'catalog:' version: 4.7.0(monaco-editor@0.52.2)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@std/toml': - specifier: jsr:^1.0.11 - version: '@jsr/std__toml@1.0.11' '@supabase/sql-to-rest': specifier: ^0.1.6 version: 0.1.6(encoding@0.1.13)(supports-color@8.1.1) @@ -2584,6 +2581,9 @@ importers: mdast: specifier: ^3.0.0 version: 3.0.0 + mdast-jsx: + specifier: ^0.1.1 + version: 0.1.1 mermaid: specifier: ^11.12.1 version: 11.15.0(supports-color@8.1.1) @@ -2647,6 +2647,9 @@ importers: scroll-into-view-if-needed: specifier: ^3.1.0 version: 3.1.0 + smol-toml: + specifier: ^1.6.1 + version: 1.6.1 sonner: specifier: ^1.5.0 version: 1.5.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6) @@ -4529,15 +4532,9 @@ packages: peerDependencies: tslib: '2' - '@jsr/std__collections@1.1.3': - resolution: {integrity: sha512-jGG6mv3IjOyxm6PyT1YVbLyAlZL+Gow6LOpBw+84qb1nkdJY0+t6bi7ICEqAwUz87cNjBS0P+yZQ5HHclJhsfw==, tarball: https://npm.jsr.io/~/11/@jsr/std__collections/1.1.3.tgz} - '@jsr/std__path@1.0.8': resolution: {integrity: sha512-eNBGlh/8ZVkMxtFH4bwIzlAeKoHYk5in4wrBZhi20zMdOiuX4QozP4+19mIXBT2lzHDjhuVLyECbhFeR304iDg==, tarball: https://npm.jsr.io/~/11/@jsr/std__path/1.0.8.tgz} - '@jsr/std__toml@1.0.11': - resolution: {integrity: sha512-+LZAizoPAPwMDmRZr86/xPrbO8E0Oq0BJ70mr1fNMobJ/6X6v15Jk16ZClFGukWgzBMd8ggiMWnzp24+N3VNZg==, tarball: https://npm.jsr.io/~/11/@jsr/std__toml/1.0.11.tgz} - '@kwsites/file-exists@1.1.1': resolution: {integrity: sha512-m9/5YGR18lIwxSFDwfE3oA7bWuq9kdau6ugN4H2rJeyhFQZcG9AgSHkQtSD15a8WvTgfz9aikZMrKPHvbpqFiw==} @@ -13005,6 +13002,9 @@ packages: math-random@1.0.4: resolution: {integrity: sha512-rUxjysqif/BZQH2yhd5Aaq7vXMSx9NdEsQcyA07uEzIvxgI7zIr33gGsh+RU0/XjmQpCW7RsVof1vlkvQVCK5A==} + mdast-jsx@0.1.1: + resolution: {integrity: sha512-7EVQNn9tWVIOaSHh0hj0mqPiAwuZ30SSsIazRUqKpU6Nj6Xud4LEjaO+k+8eptwCNF8jKT52lPiNhugaged0WA==} + mdast-util-definitions@5.1.2: resolution: {integrity: sha512-8SVPMuHqlPME/z3gqVwWY4zVXn8lqKv/pAhC57FuJ40ImXyBpmO5ukh98zB2v7Blql2FiHjHv9LVztSIqjY+MA==} @@ -20029,14 +20029,8 @@ snapshots: dependencies: tslib: 2.8.1 - '@jsr/std__collections@1.1.3': {} - '@jsr/std__path@1.0.8': {} - '@jsr/std__toml@1.0.11': - dependencies: - '@jsr/std__collections': 1.1.3 - '@kwsites/file-exists@1.1.1(supports-color@8.1.1)': dependencies: debug: 4.4.3(supports-color@8.1.1) @@ -29514,6 +29508,10 @@ snapshots: math-random@1.0.4: {} + mdast-jsx@0.1.1: + dependencies: + '@types/mdast': 3.0.15 + mdast-util-definitions@5.1.2: dependencies: '@types/mdast': 3.0.15 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 87e6cf33e35..cc4403f2981 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -60,6 +60,9 @@ minimumReleaseAgeExclude: - '@ai-sdk/*' - '@supabase/*' - '@supabase-labs/*' + # First-party, published from supabase-community/mdast-jsx. + - mdast-jsx + # The following are excluded to fix vulnerablities. - react-use onlyBuiltDependencies: