diff --git a/.github/workflows/update-js-libs.yml b/.github/workflows/update-js-libs.yml new file mode 100644 index 0000000000000..4715581686c97 --- /dev/null +++ b/.github/workflows/update-js-libs.yml @@ -0,0 +1,76 @@ +name: Update JS Libraries + +on: + workflow_dispatch: + inputs: + version: + description: 'Version to update to' + required: true + type: string + source: + description: 'Source of the update' + required: false + type: string + default: 'manual' + +permissions: + pull-requests: write + contents: write + +jobs: + update-js-libs: + runs-on: blacksmith-4vcpu-ubuntu-2404 + + steps: + - uses: actions/checkout@v4 + with: + ref: master + + - uses: pnpm/action-setup@v4 + name: Install pnpm + with: + run_install: false + + - name: Use Node.js + uses: actions/setup-node@v4 + with: + node-version-file: '.nvmrc' + cache: 'pnpm' + + - name: Update @supabase/*-js packages in pnpm-workspace.yaml + run: | + # Update @supabase/supabase-js + sed -i "s/'@supabase\/supabase-js': .*/'@supabase\/supabase-js': ^${{ github.event.inputs.version }}/" pnpm-workspace.yaml + + # Update @supabase/auth-js + sed -i "s/'@supabase\/auth-js': .*/'@supabase\/auth-js': ^${{ github.event.inputs.version }}/" pnpm-workspace.yaml + + # Update @supabase/realtime-js + sed -i "s/'@supabase\/realtime-js': .*/'@supabase\/realtime-js': ^${{ github.event.inputs.version }}/" pnpm-workspace.yaml + + echo "Updated pnpm-workspace.yaml:" + cat pnpm-workspace.yaml + + - name: Install dependencies + run: pnpm i + + - name: Create pull request + uses: peter-evans/create-pull-request@v6 + with: + token: ${{ secrets.GITHUB_TOKEN }} + commit-message: 'feat: update @supabase/*-js libraries to v${{ github.event.inputs.version }}' + title: 'feat: update @supabase/*-js libraries to v${{ github.event.inputs.version }}' + body: | + This PR updates @supabase/*-js libraries to version ${{ github.event.inputs.version }}. + + **Source**: ${{ github.event.inputs.source }} + + **Changes**: + - Updated @supabase/supabase-js to ^${{ github.event.inputs.version }} + - Updated @supabase/auth-js to ^${{ github.event.inputs.version }} + - Updated @supabase/realtime-js to ^${{ github.event.inputs.version }} + - Refreshed pnpm-lock.yaml + + This PR was created automatically. + branch: 'gha/auto-update-js-libs-v${{ github.event.inputs.version }}' + base: 'master' diff --git a/apps/docs/content/_partials/mcp_supabase_config.mdx b/apps/docs/content/_partials/mcp_supabase_config.mdx deleted file mode 100644 index 871a25462b2af..0000000000000 --- a/apps/docs/content/_partials/mcp_supabase_config.mdx +++ /dev/null @@ -1,156 +0,0 @@ - - - - - ```json - { - "mcpServers": { - "supabase": { - "command": "npx", - "args": [ - "-y", - "@supabase/mcp-server-supabase", - "--read-only", - "--project-ref=" - ], - "env": { - "SUPABASE_ACCESS_TOKEN": "" - } - } - } - } - ``` - - Replace `` with your project ref, and `` with your personal access token. - - - - - - ```json - { - "mcpServers": { - "supabase": { - "command": "cmd", - "args": [ - "/c", - "npx", - "-y", - "@supabase/mcp-server-supabase", - "--read-only", - "--project-ref=" - ], - "env": { - "SUPABASE_ACCESS_TOKEN": "" - } - } - } - } - ``` - -Or, if using `pnpm` instead of `npm` - - ```json - { - "mcpServers": { - "supabase": { - "command": "cmd", - "args": [ - "/c", - "pnpm", - "dlx", - "@supabase/mcp-server-supabase", - "--read-only", - "--project-ref=" - ], - "env": { - "SUPABASE_ACCESS_TOKEN": "" - } - } - } - } - ``` - Replace `` with your project ref, and `` with your personal access token. - - - - Make sure that `node` and `npx` are available in your system `PATH`. Assuming `node` is installed, you can get the path by running: - - ```shell - npm config get prefix - ``` - - Then add it to your system `PATH` by running: - - ```shell - setx PATH "%PATH%;" - ``` - - Replacing `` with the path you got from the previous command. - - Finally restart {{ .app }} for the changes to take effect. - - - - - - - - ```json - { - "mcpServers": { - "supabase": { - "command": "wsl", - "args": [ - "npx", - "-y", - "@supabase/mcp-server-supabase", - "--read-only", - "--project-ref=" - ], - "env": { - "SUPABASE_ACCESS_TOKEN": "" - } - } - } - } - ``` - - Replace `` with your project ref, and `` with your personal access token. - - This assumes you have Windows Subsystem for Linux (WSL) enabled and `node`/`npx` are installed within the WSL environment. - - - - - - ```json - { - "mcpServers": { - "supabase": { - "command": "npx", - "args": [ - "-y", - "@supabase/mcp-server-supabase", - "--read-only", - "--project-ref=" - ], - "env": { - "SUPABASE_ACCESS_TOKEN": "" - } - } - } - } - ``` - - Replace `` with your project ref, and `` with your personal access token. - - - - diff --git a/apps/docs/content/_partials/mcp_supabase_vscode_config.mdx b/apps/docs/content/_partials/mcp_supabase_vscode_config.mdx deleted file mode 100644 index 0117cb7d3e3aa..0000000000000 --- a/apps/docs/content/_partials/mcp_supabase_vscode_config.mdx +++ /dev/null @@ -1,143 +0,0 @@ - - - - - ```json - { - "inputs": [ - { - "type": "promptString", - "id": "supabase-access-token", - "description": "Supabase personal access token", - "password": true - } - ], - "servers": { - "supabase": { - "command": "npx", - "args": ["-y", "@supabase/mcp-server-supabase@latest", "--read-only", "--project-ref="], - "env": { - "SUPABASE_ACCESS_TOKEN": "${input:supabase-access-token}" - } - } - } - } - ``` - - Replace `` with your project ref. - - - - - - ```json - { - "inputs": [ - { - "type": "promptString", - "id": "supabase-access-token", - "description": "Supabase personal access token", - "password": true - } - ], - "servers": { - "supabase": { - "command": "cmd", - "args": ["/c", "npx", "-y", "@supabase/mcp-server-supabase@latest", "--read-only", "--project-ref="], - "env": { - "SUPABASE_ACCESS_TOKEN": "${input:supabase-access-token}" - } - } - } - } - ``` - - Replace `` with your project ref. - - - - Make sure that `node` and `npx` are available in your system `PATH`. Assuming `node` is installed, you can get the path by running: - - ```shell - npm config get prefix - ``` - - Then add it to your system `PATH` by running: - - ```shell - setx PATH "%PATH%;" - ``` - - Replacing `` with the path you got from the previous command. - - Finally restart VS Code for the changes to take effect. - - - - - - - - ```json - { - "inputs": [ - { - "type": "promptString", - "id": "supabase-access-token", - "description": "Supabase personal access token", - "password": true - } - ], - "servers": { - "supabase": { - "command": "wsl", - "args": ["npx", "-y", "@supabase/mcp-server-supabase@latest", "--read-only", "--project-ref="], - "env": { - "SUPABASE_ACCESS_TOKEN": "${input:supabase-access-token}" - } - } - } - } - ``` - - Replace `` with your project ref. - - This assumes you have Windows Subsystem for Linux (WSL) enabled and `node`/`npx` are installed within the WSL environment. - - - - - - ```json - { - "inputs": [ - { - "type": "promptString", - "id": "supabase-access-token", - "description": "Supabase personal access token", - "password": true - } - ], - "servers": { - "supabase": { - "command": "npx", - "args": ["-y", "@supabase/mcp-server-supabase@latest", "--read-only", "--project-ref="], - "env": { - "SUPABASE_ACCESS_TOKEN": "${input:supabase-access-token}" - } - } - } - } - ``` - - Replace `` with your project ref. - - - - diff --git a/apps/docs/content/guides/getting-started/mcp.mdx b/apps/docs/content/guides/getting-started/mcp.mdx index b5667aba713d6..e3dbd33fba978 100644 --- a/apps/docs/content/guides/getting-started/mcp.mdx +++ b/apps/docs/content/guides/getting-started/mcp.mdx @@ -6,211 +6,31 @@ description: 'Connect your AI tools to Supabase using MCP' sidebar_label: 'Model context protocol (MCP)' --- -The [Model Context Protocol](https://modelcontextprotocol.io/introduction) (MCP) is a standard for connecting Large Language Models (LLMs) to platforms like Supabase. This guide covers how to connect Supabase to the following AI tools using MCP: +The [Model Context Protocol](https://modelcontextprotocol.io/introduction) (MCP) is a standard for connecting Large Language Models (LLMs) to platforms like Supabase. Once connected, your AI assistants can interact with and query your Supabase projects on your behalf. -- [Cursor](#cursor) -- [Windsurf](#windsurf) (Codium) -- [Visual Studio Code](#visual-studio-code-copilot) (Copilot) -- [Cline](#cline) (VS Code extension) -- [Claude desktop](#claude-desktop) -- [Claude code](#claude-code) -- [Amp](#amp) +## Remote MCP installation -Once connected, your AI assistants can interact with and query your Supabase projects on your behalf. - -## Step 1: Create an access token - -First, go to your [Supabase settings](/dashboard/account/tokens) and create an access token to authenticate the MCP server with your Supabase account. Give it a name that describes its purpose, like "Cursor MCP Server". - -## Step 2: Follow our security best practices +### Step 1: Follow our security best practices Before running the MCP server, we recommend you read our [security best practices](#security-risks) to understand the risks of connecting an LLM to your Supabase projects and how to mitigate them. -## Step 3: Configure your AI tool - -MCP compatible tools connect to Supabase using the [Supabase MCP server](https://github.com/supabase-community/supabase-mcp). - -Follow the instructions for your AI tool to connect the Supabase MCP server. The configuration below uses read-only, project-scoped mode by default. We recommend these settings to prevent the agent from making unintended changes to your database. - - - -Read-only mode applies only to database operations. Write operations on project-management tools, -such as `create_project`, are still available. - - - -### Cursor - -1. Open [Cursor](https://www.cursor.com/) and create a `.cursor` directory in your project root if it doesn't exist. -2. Create a `.cursor/mcp.json` file if it doesn't exist and open it. -3. Add the following configuration: - - <$Partial path="mcp_supabase_config.mdx" variables={{ "app": "Cursor" }} /> - -4. Save the configuration file. -5. Open Cursor and navigate to **Settings > Cursor Settings > MCP & Integrations**. You should see a green active status after the server is successfully connected. - -### Windsurf +### Step 2: Configure your AI tool -1. Open [Windsurf](https://docs.codeium.com/windsurf) and open the Cascade assistant. -2. Tap on the box (**Customizations**) icon, then the **Configure** icon in the top right of the panel to open the configuration file. -3. Add the following configuration: +Choose your Supabase platform, project, and MCP client and follow the installation instructions: - <$Partial path="mcp_supabase_config.mdx" variables={{ "app": "Windsurf" }} /> + -4. Save the configuration file and reload by tapping **Refresh** in the Cascade assistant. -5. You should see a green active status after the server is successfully connected. + -### Visual Studio Code (Copilot) +Your MCP client will automatically prompt you to login to Supabase during setup. This will open a browser window where you can login to your Supabase account and grant access to the MCP client. Be sure to choose the organization that contains the project you wish to work with. In the future, we'll offer more fine grain control over these permissions. -
- [![Install with NPX in VS - Code](https://img.shields.io/badge/VS_Code-NPM-0098FF?style=flat-square&logo=visualstudiocode&logoColor=white)](https://insiders.vscode.dev/redirect/mcp/install?name=supabase&inputs=%5B%7B%22type%22%3A%22promptString%22%2C%22id%22%3A%22supabase-access-token%22%2C%22description%22%3A%22Supabase%20personal%20access%20token%22%2C%22password%22%3Atrue%7D%5D&config=%7B%22command%22%3A%22npx%22%2C%22args%22%3A%5B%22-y%22%2C%22%40supabase%2Fmcp-server-supabase%40latest%22%2C%22--readonly%22%2C%22--project-ref%3D%24SUPABASE_MCP_PROJECT_REF%22%5D%2C%22env%22%3A%7B%22SUPABASE_ACCESS_TOKEN%22%3A%22%24%7Binput%3Asupabase-access-token%7D%22%2C%22SUPABASE_MCP_PROJECT_REF%22%3A%22%24%7Binput%3Asupabase-project-ref%7D%22%7D%7D) - [![Install with NPX in VS Code - Insiders](https://img.shields.io/badge/VS_Code_Insiders-NPM-24bfa5?style=flat-square&logo=visualstudiocode&logoColor=white)](https://insiders.vscode.dev/redirect/mcp/install?name=supabase&inputs=%5B%7B%22type%22%3A%22promptString%22%2C%22id%22%3A%22supabase-access-token%22%2C%22description%22%3A%22Supabase%20personal%20access%20token%22%2C%22password%22%3Atrue%7D%5D&config=%7B%22command%22%3A%22npx%22%2C%22args%22%3A%5B%22-y%22%2C%22%40supabase%2Fmcp-server-supabase%40latest%22%2C%22--readonly%22%2C%22--project-ref%3D%24SUPABASE_MCP_PROJECT_REF%22%5D%2C%22env%22%3A%7B%22SUPABASE_ACCESS_TOKEN%22%3A%22%24%7Binput%3Asupabase-access-token%7D%22%2C%22SUPABASE_MCP_PROJECT_REF%22%3A%22%24%7Binput%3Asupabase-project-ref%7D%22%7D%7D&quality=insiders) -
- -1. Open [VS Code](https://code.visualstudio.com/) and create a `.vscode` directory in your project root if it doesn't exist. -2. Create a `.vscode/mcp.json` file if it doesn't exist and open it. -3. Add the following configuration: - - <$Partial path="mcp_supabase_vscode_config.mdx" /> - -4. Save the configuration file and click the **Start** button that appears inline above the Supabase server definition. VS Code prompts you to enter your personal access token. Enter the token that you created earlier. -5. Open Copilot chat and switch to "Agent" mode. You should see a tool icon that you can tap to confirm the MCP tools are available. - - - -For more info on using MCP in VS Code, read the [Copilot documentation](https://code.visualstudio.com/docs/copilot/chat/mcp-servers). +Previously Supabase MCP required you to generate a personal access token (PAT), but this is no longer required. -### Cline - -1. Open the [Cline](https://github.com/cline/cline) extension in VS Code and tap the **MCP Servers** icon. -2. Tap **MCP Servers**, open the **Installed** tab, then click "Configure MCP Servers" to open the configuration file. -3. Add the following configuration: - - <$Partial path="mcp_supabase_config.mdx" variables={{ "app": "VS Code" }} /> - -4. Save the configuration file. Cline should automatically reload the configuration. -5. You should see a green active status after the server is successfully connected. - -### Claude desktop - -1. Open [Claude desktop](https://claude.ai/download) and navigate to **Settings**. -2. Under the **Developer** tab, tap **Edit Config** to open the configuration file. -3. Add the following configuration: - - <$Partial path="mcp_supabase_config.mdx" variables={{ "app": "Claude desktop" }} /> - -4. Save the configuration file and restart Claude desktop. -5. From the new chat screen, you should see a settings (Search and tools) icon appear with the new MCP server available. - -### Claude code - -You can add the Supabase MCP server to Claude Code in two ways: - -#### Option 1: Project-scoped server (via .mcp.json file) - -1. Create a `.mcp.json` file in your project root if it doesn't exist. -2. Add the following configuration: - - <$Partial path="mcp_supabase_config.mdx" variables={{ "app": "Claude code" }} /> - -3. Save the configuration file. -4. Restart [Claude code](https://claude.ai/code) to apply the new configuration. - -#### Option 2: Locally-scoped server (via CLI command) - -You can also add the Supabase MCP server as a locally-scoped server, which is only available to you in the current project: - -1. Run the following command in your terminal: - - ```bash - claude mcp add supabase -s local -e SUPABASE_ACCESS_TOKEN=your_token_here -- npx -y @supabase/mcp-server-supabase@latest - ``` - -Locally-scoped servers take precedence over project-scoped servers with the same name and are stored in your project-specific user settings. - -### Amp - -You can add the Supabase MCP server to [Amp](https://ampcode.com) in two ways: - -#### Option 1: VSCode settings.json - -1. Open VSCode's `settings.json` file. -2. Add the following configuration: - - ```json - { - "amp.mcpServers": { - "supabase": { - "command": "npx", - "args": [ - "-y", - "@supabase/mcp-server-supabase@latest", - "--read-only", - "--project-ref=" - ], - "env": { - "SUPABASE_ACCESS_TOKEN": "" - } - } - } - } - ``` - - Replace `project-ref` and `personal-access-token` with your project ref and personal access token. - -3. Save the configuration file. -4. Restart VS Code to apply the new configuration. - -#### Option 2: Amp CLI - -1. Edit `~/.config/amp/settings.json` -2. Add the following configuration: - - ```json - { - "amp.mcpServers": { - "supabase": { - "command": "npx", - "args": [ - "-y", - "@supabase/mcp-server-supabase", - "--read-only", - "--project-ref=" - ], - "env": { - "SUPABASE_ACCESS_TOKEN": "" - } - } - } - } - ``` - - Replace `project-ref` and `personal-access-token` with your project ref and personal access token. - -3. Save the configuration file. -4. Restart Amp to apply the new configuration. - -### Qodo Gen - -1. Open [Qodo Gen](https://docs.qodo.ai/qodo-documentation/qodo-gen) chat panel in VSCode or IntelliJ. -2. Click **Connect more tools**. -3. Click **+ Add new MCP**. -4. Add the following configuration: - - <$Partial path="mcp_supabase_config.mdx" variables={{ "app": "Qodo Gen" }} /> - -5. Click **Save**. - ### Next steps -Your AI tool is now connected to Supabase using MCP. Try asking your AI assistant to create a new project, create a table, or fetch project config. - -For a full list of tools available, see the [GitHub README](https://github.com/supabase-community/supabase-mcp#tools). If you experience any issues, [submit an bug report](https://github.com/supabase-community/supabase-mcp/issues/new?template=1.Bug_report.md). +Your AI tool is now connected to your Supabase project or account using remote MCP. Try asking the AI tool to query your database using natural language commands. ## Security risks diff --git a/apps/docs/content/guides/self-hosting.mdx b/apps/docs/content/guides/self-hosting.mdx index b1b63d1196838..ffa04358aa431 100644 --- a/apps/docs/content/guides/self-hosting.mdx +++ b/apps/docs/content/guides/self-hosting.mdx @@ -51,11 +51,6 @@ export const community = [ description: 'Helm charts to deploy a Supabase on Kubernetes.', href: 'https://github.com/supabase-community/supabase-kubernetes', }, - { - name: 'Terraform', - description: 'A community-driven Terraform Provider for Supabase.', - href: 'https://github.com/supabase-community/supabase-terraform', - }, { name: 'Traefik', description: 'A self-hosted Supabase setup with Traefik as a reverse proxy.', @@ -74,11 +69,6 @@ The following third-party providers have shown consistent support for the self-h
{[ - { - name: 'Digital Ocean', - description: 'Deploys using Terraform.', - href: 'https://docs.digitalocean.com/developer-center/hosting-supabase-on-digitalocean/', - }, { name: 'StackGres', description: 'Deploys using Kubernetes.', diff --git a/apps/docs/features/docs/MdxBase.shared.tsx b/apps/docs/features/docs/MdxBase.shared.tsx index 7a5bb363be174..2e1fef8edc4b4 100644 --- a/apps/docs/features/docs/MdxBase.shared.tsx +++ b/apps/docs/features/docs/MdxBase.shared.tsx @@ -1,11 +1,11 @@ import { ArrowDown, Check, X } from 'lucide-react' import Link from 'next/link' import { Button, Image } from 'ui' +import { Heading } from 'ui/src/components/CustomHTMLElements' import { Admonition } from 'ui-patterns/admonition' import { GlassPanel } from 'ui-patterns/GlassPanel' import { IconPanel } from 'ui-patterns/IconPanel' import SqlToRest from 'ui-patterns/SqlToRest' -import { Heading } from 'ui/src/components/CustomHTMLElements' import { AiPromptsIndex } from '~/app/guides/getting-started/ai-prompts/[slug]/AiPromptsIndex' import { AppleSecretGenerator } from '~/components/AppleSecretGenerator' import AuthProviders from '~/components/AuthProviders' @@ -29,6 +29,7 @@ import InfoTooltip from '~/features/ui/InfoTooltip' import { ShowUntil } from '~/features/ui/ShowUntil' import { TabPanel, Tabs } from '~/features/ui/Tabs' import { ErrorCodes } from '../ui/ErrorCodes' +import { McpConfigPanel } from '../ui/McpConfigPanel' const components = { Accordion, @@ -53,6 +54,7 @@ const components = { Image: (props: any) => , JwtGenerator, Link, + McpConfigPanel, NamedCodeBlock, NavData, ProjectConfigVariables, diff --git a/apps/docs/features/docs/Reference.sections.tsx b/apps/docs/features/docs/Reference.sections.tsx index 2da5829adc0cf..ca9197dab149e 100644 --- a/apps/docs/features/docs/Reference.sections.tsx +++ b/apps/docs/features/docs/Reference.sections.tsx @@ -4,7 +4,7 @@ import { Tabs_Shadcn_, TabsContent_Shadcn_, TabsList_Shadcn_, TabsTrigger_Shadcn import { isFeatureEnabled } from 'common' import ApiSchema from '~/components/ApiSchema' -import { REFERENCES } from '~/content/navigation.references' +import { clientSdkIds, REFERENCES } from '~/content/navigation.references' import { MDXRemoteRefs, getRefMarkdown } from '~/features/docs/Reference.mdx' import type { MethodTypes } from '~/features/docs/Reference.typeSpec' import { @@ -43,7 +43,7 @@ async function RefSections({ libraryId, version }: RefSectionsProps) { flattenedSections = trimIntro(flattenedSections) } - if (!isFeatureEnabled('sdk:auth')) { + if (!isFeatureEnabled('sdk:auth') && clientSdkIds.includes(libraryId)) { flattenedSections = flattenedSections?.filter( (section) => 'product' in section && section.product !== 'auth' && section.product !== 'auth-admin' diff --git a/apps/docs/features/ui/McpConfigPanel.tsx b/apps/docs/features/ui/McpConfigPanel.tsx new file mode 100644 index 0000000000000..96411cd929994 --- /dev/null +++ b/apps/docs/features/ui/McpConfigPanel.tsx @@ -0,0 +1,220 @@ +'use client' + +import { useIsLoggedIn, useIsUserLoading } from 'common' +import { Check, ChevronDown } from 'lucide-react' +import { useTheme } from 'next-themes' +import Link from 'next/link' +import { useState } from 'react' +import { + Button, + cn, + Command_Shadcn_, + CommandEmpty_Shadcn_, + CommandGroup_Shadcn_, + CommandInput_Shadcn_, + CommandItem_Shadcn_, + CommandList_Shadcn_, + Popover_Shadcn_, + PopoverContent_Shadcn_, + PopoverTrigger_Shadcn_, +} from 'ui' +import { McpConfigPanel as McpConfigPanelBase } from 'ui-patterns/McpUrlBuilder' +import { useProjectsQuery } from '~/lib/fetch/projects' + +type PlatformType = (typeof PLATFORMS)[number]['value'] + +const PLATFORMS = [ + { value: 'hosted', label: 'Hosted' }, + { value: 'local', label: 'CLI' }, +] as const satisfies Array<{ value: string; label: string }> + +function ProjectSelector({ + className, + selectedProject, + onProjectSelect, +}: { + className?: string + selectedProject?: { ref: string; name: string } | null + onProjectSelect?: (project: { ref: string; name: string } | null) => void +}) { + const isUserLoading = useIsUserLoading() + const isLoggedIn = useIsLoggedIn() + const { data: projects, isLoading, isError } = useProjectsQuery({ enabled: isLoggedIn }) + + const [open, setOpen] = useState(false) + + return ( + +
+ + Project + + + {!isUserLoading && !isLoggedIn ? ( + + ) : ( + + + + )} +
+ + + + + No results found. + + {projects?.map((project) => ( + { + onProjectSelect?.(project.ref === selectedProject?.ref ? null : project) + setOpen(false) + }} + className="flex gap-2 items-center" + > + {project.name} + + + ))} + + + + +
+ ) +} + +function PlatformSelector({ + className, + selectedPlatform, + onPlatformSelect, +}: { + className?: string + selectedPlatform: PlatformType + onPlatformSelect?: (platform: PlatformType) => void +}) { + const [open, setOpen] = useState(false) + + return ( + +
+ + Platform + + + + + +
+ + + + + {PLATFORMS.map((platform) => ( + { + onPlatformSelect?.(platform.value) + setOpen(false) + }} + className="flex gap-2 items-center" + > + {platform.label} + + + ))} + + + + +
+ ) +} + +export function McpConfigPanel() { + const [selectedProject, setSelectedProject] = useState<{ ref: string; name: string } | null>(null) + const [selectedPlatform, setSelectedPlatform] = useState<'hosted' | 'local'>('hosted') + const { theme } = useTheme() + + const isPlatform = selectedPlatform === 'hosted' + const project = isPlatform ? selectedProject : null + + return ( +
+
+ + {isPlatform && ( + + )} +
+

+ {isPlatform + ? 'Scope the MCP server to a project. If no project is selected, all projects will be accessible.' + : 'Project selection is only available for the hosted platform.'} +

+ +
+ ) +} diff --git a/apps/docs/public/img/mcp-clients/claude-dark-icon.svg b/apps/docs/public/img/mcp-clients/claude-dark-icon.svg new file mode 100644 index 0000000000000..b68b27324e2cc --- /dev/null +++ b/apps/docs/public/img/mcp-clients/claude-dark-icon.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + diff --git a/apps/docs/public/img/mcp-clients/claude-icon.svg b/apps/docs/public/img/mcp-clients/claude-icon.svg new file mode 100644 index 0000000000000..135a8b9f33f30 --- /dev/null +++ b/apps/docs/public/img/mcp-clients/claude-icon.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/apps/docs/public/img/mcp-clients/cursor-dark-icon.svg b/apps/docs/public/img/mcp-clients/cursor-dark-icon.svg new file mode 100644 index 0000000000000..48f85eb4d5ae0 --- /dev/null +++ b/apps/docs/public/img/mcp-clients/cursor-dark-icon.svg @@ -0,0 +1 @@ + diff --git a/apps/docs/public/img/mcp-clients/cursor-icon.svg b/apps/docs/public/img/mcp-clients/cursor-icon.svg new file mode 100644 index 0000000000000..48f85eb4d5ae0 --- /dev/null +++ b/apps/docs/public/img/mcp-clients/cursor-icon.svg @@ -0,0 +1 @@ + diff --git a/apps/docs/public/img/mcp-clients/vscode-dark-icon.svg b/apps/docs/public/img/mcp-clients/vscode-dark-icon.svg new file mode 100644 index 0000000000000..c453e633f349c --- /dev/null +++ b/apps/docs/public/img/mcp-clients/vscode-dark-icon.svg @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/docs/public/img/mcp-clients/vscode-icon.svg b/apps/docs/public/img/mcp-clients/vscode-icon.svg new file mode 100644 index 0000000000000..c453e633f349c --- /dev/null +++ b/apps/docs/public/img/mcp-clients/vscode-icon.svg @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/docs/public/img/mcp-clients/windsurf-dark-icon.svg b/apps/docs/public/img/mcp-clients/windsurf-dark-icon.svg new file mode 100644 index 0000000000000..2e4e4e4921d0d --- /dev/null +++ b/apps/docs/public/img/mcp-clients/windsurf-dark-icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/apps/docs/public/img/mcp-clients/windsurf-icon.svg b/apps/docs/public/img/mcp-clients/windsurf-icon.svg new file mode 100644 index 0000000000000..386f8c035dc15 --- /dev/null +++ b/apps/docs/public/img/mcp-clients/windsurf-icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/apps/docs/turbo.json b/apps/docs/turbo.json index d980120ebdc86..5e22a06e3d6bc 100644 --- a/apps/docs/turbo.json +++ b/apps/docs/turbo.json @@ -36,6 +36,7 @@ "NEXT_PUBLIC_GOOGLE_TAG_MANAGER_ID", "NEXT_PUBLIC_GOTRUE_URL", "NEXT_PUBLIC_SUPABASE_ANON_KEY", + "NEXT_PUBLIC_MCP_URL", // These envs are technically passthrough env vars because they're only used on the server side of Nextjs "ASSET_CDN_S3_ENDPOINT", "AWS_ACCESS_KEY_ID", diff --git a/apps/studio/components/interfaces/Connect/Connect.constants.ts b/apps/studio/components/interfaces/Connect/Connect.constants.ts index 33ff40fa88b6f..6636fdc76ca12 100644 --- a/apps/studio/components/interfaces/Connect/Connect.constants.ts +++ b/apps/studio/components/interfaces/Connect/Connect.constants.ts @@ -350,6 +350,7 @@ export const CONNECTION_TYPES = [ { key: 'frameworks', label: 'App Frameworks', obj: FRAMEWORKS }, { key: 'mobiles', label: 'Mobile Frameworks', obj: MOBILES }, { key: 'orms', label: 'ORMs', obj: ORMS }, + { key: 'mcp', label: 'MCP', obj: [] }, ] export const PGBOUNCER_ENABLED_BUT_NO_IPV4_ADDON_TEXT = diff --git a/apps/studio/components/interfaces/Connect/Connect.tsx b/apps/studio/components/interfaces/Connect/Connect.tsx index 7a6f1e2133358..5f207ea614ce6 100644 --- a/apps/studio/components/interfaces/Connect/Connect.tsx +++ b/apps/studio/components/interfaces/Connect/Connect.tsx @@ -5,6 +5,7 @@ import { parseAsBoolean, parseAsString, useQueryState } from 'nuqs' import { useEffect, useMemo, useState } from 'react' import { DatabaseConnectionString } from 'components/interfaces/Connect/DatabaseConnectionString' +import { McpTabContent } from 'components/interfaces/Connect/McpTabContent' import { ButtonTooltip } from 'components/ui/ButtonTooltip' import Panel from 'components/ui/Panel' import { getKeys, useAPIKeysQuery } from 'data/api-keys/api-keys-query' @@ -357,6 +358,18 @@ export const Connect = () => { ) } + if (type.key === 'mcp') { + return ( + + + + ) + } + return ( void label: string items: ConnectionType[] + iconFolder?: string + supportsDarkMode?: boolean } export const ConnectDropdown = ({ @@ -30,6 +32,8 @@ export const ConnectDropdown = ({ label, items, + iconFolder, + supportsDarkMode, }: ConnectDropdownProps) => { const [open, setOpen] = useState(false) @@ -54,7 +58,15 @@ export const ConnectDropdown = ({ iconRight={} >
- {selectedItem?.icon ? : } + {selectedItem?.icon ? ( + + ) : ( + + )} {selectedItem?.label}
@@ -76,7 +88,15 @@ export const ConnectDropdown = ({ }} className="flex gap-2 items-center" > - {item.icon ? : } + {item.icon ? ( + + ) : ( + + )} {item.label} { +export const ConnectionIcon = ({ icon, iconFolder, supportsDarkMode }: ConnectionIconProps) => { const { resolvedTheme } = useTheme() - const imageFolder = ['ionic-angular'].includes(icon) ? 'icons/frameworks' : 'libraries' + const imageFolder = + iconFolder || (['ionic-angular'].includes(icon) ? 'icons/frameworks' : 'libraries') + const imageExtension = imageFolder === 'icons/frameworks' ? '' : '-icon' + + const shouldUseDarkMode = + supportsDarkMode || + ['expo', 'nextjs', 'prisma', 'drizzle', 'astro', 'remix'].includes(icon.toLowerCase()) + const iconImgSrc = icon.startsWith('http') ? icon : `${BASE_PATH}/img/${imageFolder}/${icon.toLowerCase()}${ - ['expo', 'nextjs', 'prisma', 'drizzle', 'astro', 'remix'].includes(icon.toLowerCase()) - ? resolvedTheme?.includes('dark') - ? '-dark' - : '' - : '' + shouldUseDarkMode && resolvedTheme?.includes('dark') ? '-dark' : '' }${imageExtension}.svg` return ( diff --git a/apps/studio/components/interfaces/Connect/McpTabContent.tsx b/apps/studio/components/interfaces/Connect/McpTabContent.tsx new file mode 100644 index 0000000000000..bbb9beeb64024 --- /dev/null +++ b/apps/studio/components/interfaces/Connect/McpTabContent.tsx @@ -0,0 +1,50 @@ +import { IS_PLATFORM, useParams } from 'common' +import Panel from 'components/ui/Panel' +import ShimmeringLoader from 'components/ui/ShimmeringLoader' +import { BASE_PATH } from 'lib/constants' +import { useTheme } from 'next-themes' +import { McpConfigPanel } from 'ui-patterns/McpUrlBuilder' +import type { projectKeys } from './Connect.types' + +export const McpTabContent = ({ projectKeys }: { projectKeys: projectKeys }) => { + const { ref: projectRef } = useParams() + + return ( + + {projectRef ? ( + + ) : ( + + )} + + ) +} + +const McpTabContentInnerLoading = () => { + return ( +
+ + +
+ ) +} + +const McpTabContentInnerLoaded = ({ + projectRef, + projectKeys, +}: { + projectRef: string + projectKeys: projectKeys +}) => { + const { resolvedTheme } = useTheme() + + return ( + + ) +} diff --git a/apps/studio/components/ui/Charts/ComposedChart.utils.tsx b/apps/studio/components/ui/Charts/ComposedChart.utils.tsx index abddab6bd6491..b5c8134bf45c3 100644 --- a/apps/studio/components/ui/Charts/ComposedChart.utils.tsx +++ b/apps/studio/components/ui/Charts/ComposedChart.utils.tsx @@ -214,12 +214,9 @@ const CustomTooltip = ({ >

{dayjs(timestamp).format(DateTimeFormats.FULL_SECONDS)}

- {payload - .reverse() - .filter((entry: any) => entry.value !== 0) - .map((entry: any, index: number) => ( - - ))} + {payload.reverse().map((entry: any, index: number) => ( + + ))} {active && showTotal && (
Total diff --git a/apps/studio/public/img/mcp-clients/claude-dark-icon.svg b/apps/studio/public/img/mcp-clients/claude-dark-icon.svg new file mode 100644 index 0000000000000..b68b27324e2cc --- /dev/null +++ b/apps/studio/public/img/mcp-clients/claude-dark-icon.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + diff --git a/apps/studio/public/img/mcp-clients/claude-icon.svg b/apps/studio/public/img/mcp-clients/claude-icon.svg new file mode 100644 index 0000000000000..135a8b9f33f30 --- /dev/null +++ b/apps/studio/public/img/mcp-clients/claude-icon.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/apps/studio/public/img/mcp-clients/cursor-dark-icon.svg b/apps/studio/public/img/mcp-clients/cursor-dark-icon.svg new file mode 100644 index 0000000000000..48f85eb4d5ae0 --- /dev/null +++ b/apps/studio/public/img/mcp-clients/cursor-dark-icon.svg @@ -0,0 +1 @@ + diff --git a/apps/studio/public/img/mcp-clients/cursor-icon.svg b/apps/studio/public/img/mcp-clients/cursor-icon.svg new file mode 100644 index 0000000000000..48f85eb4d5ae0 --- /dev/null +++ b/apps/studio/public/img/mcp-clients/cursor-icon.svg @@ -0,0 +1 @@ + diff --git a/apps/studio/public/img/mcp-clients/vscode-dark-icon.svg b/apps/studio/public/img/mcp-clients/vscode-dark-icon.svg new file mode 100644 index 0000000000000..c453e633f349c --- /dev/null +++ b/apps/studio/public/img/mcp-clients/vscode-dark-icon.svg @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/studio/public/img/mcp-clients/vscode-icon.svg b/apps/studio/public/img/mcp-clients/vscode-icon.svg new file mode 100644 index 0000000000000..c453e633f349c --- /dev/null +++ b/apps/studio/public/img/mcp-clients/vscode-icon.svg @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/studio/public/img/mcp-clients/windsurf-dark-icon.svg b/apps/studio/public/img/mcp-clients/windsurf-dark-icon.svg new file mode 100644 index 0000000000000..2e4e4e4921d0d --- /dev/null +++ b/apps/studio/public/img/mcp-clients/windsurf-dark-icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/apps/studio/public/img/mcp-clients/windsurf-icon.svg b/apps/studio/public/img/mcp-clients/windsurf-icon.svg new file mode 100644 index 0000000000000..386f8c035dc15 --- /dev/null +++ b/apps/studio/public/img/mcp-clients/windsurf-icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/ui-patterns/index.tsx b/packages/ui-patterns/index.tsx index 9685d8fff4f72..f33a01ebe8d70 100644 --- a/packages/ui-patterns/index.tsx +++ b/packages/ui-patterns/index.tsx @@ -11,6 +11,7 @@ export * from './src/ComputeBadge' export * from './src/FilterBar' export * from './src/GlassPanel' export * from './src/InnerSideMenu' +export * from './src/McpUrlBuilder' export * from './src/PopupFrame' export * from './src/ShimmeringLoader' export * from './src/TimestampInfo' diff --git a/packages/ui-patterns/package.json b/packages/ui-patterns/package.json index fd886e8c6027b..5df8c48b9e3fa 100644 --- a/packages/ui-patterns/package.json +++ b/packages/ui-patterns/package.json @@ -314,6 +314,10 @@ "import": "./src/LogsBarChart/index.tsx", "types": "./src/LogsBarChart/index.tsx" }, + "./McpUrlBuilder": { + "import": "./src/McpUrlBuilder/index.ts", + "types": "./src/McpUrlBuilder/index.ts" + }, "./MobileSheetNav/MobileSheetNav": { "import": "./src/MobileSheetNav/MobileSheetNav.tsx", "types": "./src/MobileSheetNav/MobileSheetNav.tsx" diff --git a/packages/ui-patterns/src/McpUrlBuilder/McpConfigPanel.tsx b/packages/ui-patterns/src/McpUrlBuilder/McpConfigPanel.tsx new file mode 100644 index 0000000000000..709130225f38a --- /dev/null +++ b/packages/ui-patterns/src/McpUrlBuilder/McpConfigPanel.tsx @@ -0,0 +1,110 @@ +'use client' + +import React, { useMemo, useState } from 'react' +import { cn, Separator } from 'ui' + +import { ClientSelectDropdown } from './components/ClientSelectDropdown' +import { McpConfigurationDisplay } from './components/McpConfigurationDisplay' +import { McpConfigurationOptions } from './components/McpConfigurationOptions' +import { FEATURE_GROUPS_PLATFORM, FEATURE_GROUPS_NON_PLATFORM, MCP_CLIENTS } from './constants' +import type { McpClient } from './types' +import { getMcpUrl } from './utils/getMcpUrl' + +export interface McpConfigPanelProps { + basePath: string + baseUrl?: string + projectRef?: string + initialSelectedClient?: McpClient + onClientSelect?: (client: McpClient) => void + theme?: 'light' | 'dark' + className?: string + isPlatform: boolean // For docs this is controlled by state, for studio by environment variable + apiUrl?: string +} + +export function McpConfigPanel({ + basePath, + projectRef, + initialSelectedClient, + onClientSelect, + className, + theme = 'dark', + isPlatform, + apiUrl, +}: McpConfigPanelProps) { + const [readonly, setReadonly] = useState(false) + const [selectedFeatures, setSelectedFeatures] = useState([]) + const [selectedClient, setSelectedClient] = useState(initialSelectedClient ?? MCP_CLIENTS[0]) + + const supportedFeatures = isPlatform ? FEATURE_GROUPS_PLATFORM : FEATURE_GROUPS_NON_PLATFORM + const selectedFeaturesSupported = useMemo(() => { + return selectedFeatures.filter((feature) => + supportedFeatures.some((group) => group.id === feature) + ) + }, [selectedFeatures, supportedFeatures]) + + const { clientConfig } = getMcpUrl({ + projectRef, + isPlatform, + apiUrl, + readonly, + features: selectedFeaturesSupported, + selectedClient, + }) + + const handleClientChange = (clientKey: string) => { + const client = MCP_CLIENTS.find((c) => c.key === clientKey) + if (client) { + setSelectedClient(client) + } + } + React.useEffect(() => { + onClientSelect?.(selectedClient) + }, [selectedClient, onClientSelect]) + + const innerPanelSpacing = 'px-4 py-3' + + return ( +
+
+

Options

+ + +
+
+ +

+ Configure your MCP client to connect with your Supabase project +

+
+
+
+

Installation

+
+ + +
+
+ ) +} diff --git a/packages/ui-patterns/src/McpUrlBuilder/components/ClientSelectDropdown.tsx b/packages/ui-patterns/src/McpUrlBuilder/components/ClientSelectDropdown.tsx new file mode 100644 index 0000000000000..7c1111c2b78aa --- /dev/null +++ b/packages/ui-patterns/src/McpUrlBuilder/components/ClientSelectDropdown.tsx @@ -0,0 +1,124 @@ +'use client' + +import { Bot, Check, ChevronDown } from 'lucide-react' +import { useState } from 'react' +import { + Button, + CommandEmpty_Shadcn_, + CommandGroup_Shadcn_, + CommandInput_Shadcn_, + CommandItem_Shadcn_, + CommandList_Shadcn_, + Command_Shadcn_, + PopoverContent_Shadcn_, + PopoverTrigger_Shadcn_, + Popover_Shadcn_, + cn, +} from 'ui' +import type { McpClient } from '../types' +import { ConnectionIcon } from './ConnectionIcon' + +interface ClientSelectDropdownProps { + basePath: string + theme?: 'light' | 'dark' + label?: string + clients: McpClient[] + selectedClient: McpClient + onClientChange: (clientKey: string) => void +} + +export const ClientSelectDropdown = ({ + basePath, + theme = 'light', + label = 'Client', + clients, + selectedClient, + onClientChange, +}: ClientSelectDropdownProps) => { + const [open, setOpen] = useState(false) + + function onSelectClient(key: string) { + onClientChange(key) + setOpen(false) + } + + return ( + +
+ + {label} + + + + +
+ + + + + No results found. + + {clients.map((client) => ( + { + onSelectClient(client.key) + setOpen(false) + }} + className="flex gap-2 items-center" + > + {client.icon ? ( + + ) : ( + + )} + {client.label} + + + ))} + + + + +
+ ) +} diff --git a/packages/ui-patterns/src/McpUrlBuilder/components/ConnectionIcon.tsx b/packages/ui-patterns/src/McpUrlBuilder/components/ConnectionIcon.tsx new file mode 100644 index 0000000000000..ec4d367e42cfb --- /dev/null +++ b/packages/ui-patterns/src/McpUrlBuilder/components/ConnectionIcon.tsx @@ -0,0 +1,26 @@ +interface ConnectionIconProps { + basePath: string + theme?: 'light' | 'dark' + supportsDarkMode?: boolean + iconFolder: string + connection: string +} + +export const ConnectionIcon = ({ + basePath, + theme = 'dark', + supportsDarkMode, + iconFolder, + connection, +}: ConnectionIconProps) => { + return ( + {`${connection} + ) +} diff --git a/packages/ui-patterns/src/McpUrlBuilder/components/McpConfigurationDisplay.tsx b/packages/ui-patterns/src/McpUrlBuilder/components/McpConfigurationDisplay.tsx new file mode 100644 index 0000000000000..5ea38d01fe644 --- /dev/null +++ b/packages/ui-patterns/src/McpUrlBuilder/components/McpConfigurationDisplay.tsx @@ -0,0 +1,101 @@ +'use client' + +import { ExternalLink } from 'lucide-react' +import Image from 'next/image' +import { Button, cn } from 'ui' +import { CodeBlock } from 'ui/src/components/CodeBlock' +import type { McpClient, McpClientConfig } from '../types' +import { getMcpButtonData } from '../utils/getMcpButtonData' + +interface McpConfigurationDisplayProps { + selectedClient: McpClient + clientConfig: McpClientConfig + className?: string + theme?: 'light' | 'dark' + basePath: string +} + +export function McpConfigurationDisplay({ + selectedClient, + clientConfig, + className, + theme = 'dark', + basePath, +}: McpConfigurationDisplayProps) { + const mcpButtonData = getMcpButtonData({ + basePath, + theme, + client: selectedClient, + clientConfig, + }) + + return ( +
+ {mcpButtonData && ( + <> +
Install in one click:
+ + + )} + + {selectedClient.configFile && ( +
+ {mcpButtonData ? 'Or add' : 'Add'} this configuration to{' '} + {selectedClient.configFile}: +
+ )} + + + + {selectedClient.alternateInstructions && selectedClient.alternateInstructions(clientConfig)} + + {(selectedClient.docsUrl || selectedClient.externalDocsUrl) && ( +
+ Need help? + {selectedClient.docsUrl && ( + + View setup guide + + + )} + {selectedClient.externalDocsUrl && ( + + View {selectedClient.label} docs + + + )} +
+ )} +
+ ) +} diff --git a/packages/ui-patterns/src/McpUrlBuilder/components/McpConfigurationOptions.tsx b/packages/ui-patterns/src/McpUrlBuilder/components/McpConfigurationOptions.tsx new file mode 100644 index 0000000000000..7d71e8a8f91ab --- /dev/null +++ b/packages/ui-patterns/src/McpUrlBuilder/components/McpConfigurationOptions.tsx @@ -0,0 +1,84 @@ +'use client' + +import { cn, Switch } from 'ui' +import { Label } from 'ui/src/components/shadcn/ui/label' +import { InfoTooltip } from '../../info-tooltip' +import { + MultiSelector, + MultiSelectorContent, + MultiSelectorItem, + MultiSelectorList, + MultiSelectorTrigger, +} from '../../multi-select' +import type { McpFeatureGroup } from '../types' + +interface McpConfigurationOptionsProps { + isPlatform: boolean + readonly: boolean + onReadonlyChange: (readonly: boolean) => void + selectedFeatures: string[] + onFeaturesChange: (features: string[]) => void + featureGroups: McpFeatureGroup[] + className?: string +} + +export function McpConfigurationOptions({ + isPlatform, + readonly, + onReadonlyChange, + selectedFeatures, + onFeaturesChange, + featureGroups, + className, +}: McpConfigurationOptionsProps) { + return ( +
+ {/* Readonly Mode */} + {isPlatform && ( +
+
+ + Only allow read operations on your database +
+
+ +
+
+ )} + + {/* Feature Groups */} +
+
+ + + Only enable a subset of features. Helps keep the number of tools within MCP client + limits + +
+ + + + + + {featureGroups.map((feature) => ( + +
+ {feature.name} + {feature.description} +
+
+ ))} +
+
+
+
+
+ ) +} diff --git a/packages/ui-patterns/src/McpUrlBuilder/constants.tsx b/packages/ui-patterns/src/McpUrlBuilder/constants.tsx new file mode 100644 index 0000000000000..67f6e7c8875a6 --- /dev/null +++ b/packages/ui-patterns/src/McpUrlBuilder/constants.tsx @@ -0,0 +1,180 @@ +import { CodeBlock } from 'ui/src/components/CodeBlock' +import type { + ClaudeCodeMcpConfig, + McpClient, + McpFeatureGroup, + OtherMcpConfig, + VSCodeMcpConfig, + WindsurfMcpConfig, +} from './types' + +export const FEATURE_GROUPS_PLATFORM: McpFeatureGroup[] = [ + { + id: 'docs', + name: 'Documentation', + description: 'Access project 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'].includes(group.id) +) + +export const MCP_CLIENTS: McpClient[] = [ + { + key: 'cursor', + label: 'Cursor', + icon: 'cursor', + configFile: '.cursor/mcp.json', + externalDocsUrl: 'https://docs.cursor.com/context/mcp', + generateDeepLink: (config) => { + const name = 'supabase' + const base64Config = Buffer.from(JSON.stringify(config.mcpServers.supabase)).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 { + mcpServers: { + supabase: { + type: 'http', + url: config.mcpServers.supabase.url, + }, + }, + } + }, + generateDeepLink: (_config) => { + const config = _config as VSCodeMcpConfig + const mcpConfig = { name: 'supabase', ...config.mcpServers.supabase } + + return `vscode:mcp/install?${encodeURIComponent(JSON.stringify(mcpConfig))}` + }, + }, + { + key: 'windsurf', + label: 'Windsurf', + icon: 'windsurf', + configFile: '~/.codeium/windsurf/mcp_config.json', + externalDocsUrl: '', + transformConfig: (config): WindsurfMcpConfig => { + return { + mcpServers: { + supabase: { + command: 'npx', + args: ['-y', 'mcp-remote', config.mcpServers.supabase.url], + }, + }, + } + }, + alternateInstructions: () => ( +

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

+ ), + }, + { + key: 'claude-code', + label: 'Claude Code', + icon: 'claude', + configFile: '~/.claude.json', + externalDocsUrl: 'https://docs.anthropic.com/en/docs/claude-code/mcp', + transformConfig: (config): ClaudeCodeMcpConfig => { + return { + mcpServers: { + supabase: { + type: 'http', + url: config.mcpServers.supabase.url, + }, + }, + } + }, + alternateInstructions: (_config) => { + const config = _config as ClaudeCodeMcpConfig + const command = `claude mcp add --transport http supabase "${config.mcpServers.supabase.url}"` + return ( +
+

+ Alternatively, add the MCP server using the command line: +

+ +
+ ) + }, + }, + { + key: 'other', + label: 'Other', + transformConfig: (config): OtherMcpConfig => { + return { + mcpServers: { + supabase: { + type: 'http', + url: config.mcpServers.supabase.url, + }, + }, + } + }, + alternateInstructions: (_config) => { + return ( +

+ These generic MCP settings may work with other MCP clients, but there are no guarantees, + due to differences between clients. Refer to your specific client docs for where to input + the configuration. +

+ ) + }, + }, +] + +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 new file mode 100644 index 0000000000000..a15b032d88468 --- /dev/null +++ b/packages/ui-patterns/src/McpUrlBuilder/index.ts @@ -0,0 +1,19 @@ +export { ClientSelectDropdown } from './components/ClientSelectDropdown' +export { ConnectionIcon } from './components/ConnectionIcon' +export { McpConfigurationDisplay } from './components/McpConfigurationDisplay' +export { McpConfigurationOptions } from './components/McpConfigurationOptions' +export { + DEFAULT_MCP_URL_PLATFORM, + DEFAULT_MCP_URL_NON_PLATFORM, + FEATURE_GROUPS_PLATFORM, + FEATURE_GROUPS_NON_PLATFORM, + MCP_CLIENTS, +} from './constants' +export { getMcpUrl } from './utils/getMcpUrl' +export { McpConfigPanel, type McpConfigPanelProps } from './McpConfigPanel' +export type { + McpClient, + McpClientBaseConfig as McpClientConfig, + McpFeatureGroup, + McpUrlBuilderConfig, +} from './types' diff --git a/packages/ui-patterns/src/McpUrlBuilder/types.ts b/packages/ui-patterns/src/McpUrlBuilder/types.ts new file mode 100644 index 0000000000000..7b6ae93001244 --- /dev/null +++ b/packages/ui-patterns/src/McpUrlBuilder/types.ts @@ -0,0 +1,88 @@ +export interface McpFeatureGroup { + id: string + name: string + description: string +} + +export interface McpClient { + key: string + label: string + icon?: string + docsUrl?: string + externalDocsUrl?: string + configFile?: string + generateDeepLink?: (config: McpClientConfig) => string | null + transformConfig?: (config: McpClientBaseConfig) => McpClientConfig + alternateInstructions?: (config: McpClientConfig) => React.ReactNode +} + +export interface McpUrlBuilderConfig { + projectRef: string + readonly?: boolean + features?: string[] +} + +export interface McpClientBaseConfig { + mcpServers: { + supabase: { + url: string + } + } +} + +export interface CursorMcpConfig extends McpClientBaseConfig {} + +export interface VSCodeMcpConfig extends McpClientBaseConfig { + mcpServers: { + supabase: { + type: 'http' + url: string + } + } +} + +export interface WindsurfMcpConfig { + mcpServers: { + supabase: { + command: 'npx' + args: ['-y', 'mcp-remote', string] + } + } +} + +export interface ClaudeCodeMcpConfig extends McpClientBaseConfig { + mcpServers: { + supabase: { + type: 'http' + url: string + } + } +} + +export interface ClaudeDesktopMcpConfig extends McpClientBaseConfig { + mcpServers: { + supabase: { + type: 'http' + url: string + } + } +} + +export interface OtherMcpConfig extends McpClientBaseConfig { + mcpServers: { + supabase: { + type: 'http' + url: string + } + } +} + +// Union of all possible config types +export type McpClientConfig = + | ClaudeCodeMcpConfig + | ClaudeDesktopMcpConfig + | CursorMcpConfig + | McpClientBaseConfig + | OtherMcpConfig + | VSCodeMcpConfig + | WindsurfMcpConfig diff --git a/packages/ui-patterns/src/McpUrlBuilder/utils/getMcpButtonData.ts b/packages/ui-patterns/src/McpUrlBuilder/utils/getMcpButtonData.ts new file mode 100644 index 0000000000000..b53d07c582890 --- /dev/null +++ b/packages/ui-patterns/src/McpUrlBuilder/utils/getMcpButtonData.ts @@ -0,0 +1,31 @@ +import type { McpClient, McpClientConfig } from '../types' + +interface GetMcpButtonDataOptions { + basePath: string + theme?: 'light' | 'dark' + client: McpClient + clientConfig: McpClientConfig +} + +export function getMcpButtonData({ + basePath, + theme, + client, + clientConfig, +}: GetMcpButtonDataOptions) { + if (client.generateDeepLink) { + const deepLink = client.generateDeepLink(clientConfig) + if (!deepLink) return null + + const imageSrc = `${basePath}/img/mcp-clients/${client.icon}${ + theme === 'dark' ? '-dark' : '' + }-icon.svg` + + return { + deepLink, + imageSrc, + label: client.label, + } + } + return null +} diff --git a/packages/ui-patterns/src/McpUrlBuilder/utils/getMcpUrl.ts b/packages/ui-patterns/src/McpUrlBuilder/utils/getMcpUrl.ts new file mode 100644 index 0000000000000..94dc41f8cb7f7 --- /dev/null +++ b/packages/ui-patterns/src/McpUrlBuilder/utils/getMcpUrl.ts @@ -0,0 +1,68 @@ +import { DEFAULT_MCP_URL_NON_PLATFORM, DEFAULT_MCP_URL_PLATFORM } from '../constants' +import type { McpClient, McpClientConfig } from '../types' + +interface GetMcpUrlOptions { + projectRef?: string + readonly?: boolean + features?: string[] + selectedClient?: McpClient + isPlatform: boolean + apiUrl?: string +} + +interface GetMcpUrlReturn { + mcpUrl: string + clientConfig: McpClientConfig +} + +export function getMcpUrl({ + projectRef, + isPlatform, + apiUrl, + readonly = false, + features = [], + selectedClient, +}: GetMcpUrlOptions): GetMcpUrlReturn { + // Generate the MCP URL based on current configuration + const url = new URL(getMcpUrlBase({ isPlatform, apiUrl })) + if (projectRef && isPlatform) { + url.searchParams.set('project_ref', projectRef) + } + if (readonly && isPlatform) { + url.searchParams.set('read_only', 'true') + } + if (features.length > 0) { + url.searchParams.set('features', features.join(',')) + } + 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, + } +} + +/** + * Assembles base `/mcp` endpoint URL for the given environment + */ +function getMcpUrlBase({ isPlatform, apiUrl }: { isPlatform: boolean; apiUrl?: string }) { + // Hosted platform uses environment variable with fallback + if (isPlatform) { + return process.env.NEXT_PUBLIC_MCP_URL ?? DEFAULT_MCP_URL_PLATFORM + } + + // Self-hosted uses API URL with fallback + return apiUrl ? `${apiUrl}/mcp` : DEFAULT_MCP_URL_NON_PLATFORM +} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 78798e906acf3..397ad1df060e7 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -26,3 +26,6 @@ minimumReleaseAgeExclude: - '@ai-sdk/*' - '@supabase/mcp-server-supabase' - '@supabase/mcp-utils' + - '@supabase/auth-js' + - '@supabase/supabase-js' + - '@supabase/realtime-js' diff --git a/turbo.json b/turbo.json index 028863f609c95..a7c33bf553390 100644 --- a/turbo.json +++ b/turbo.json @@ -78,6 +78,7 @@ "NEXT_PUBLIC_GOOGLE_TAG_MANAGER_ID", "NEXT_PUBLIC_VERCEL_ENV", "NEXT_PUBLIC_USERCENTRICS_RULESET_ID", + "NEXT_PUBLIC_MCP_URL", // These envs are technically passthrough env vars because they're only used on the server side of Nextjs "PLATFORM_PG_META_URL", "STUDIO_PG_META_URL",