From 3057faa4e2e57fe1e38a205d35d4a1c89cc894fc Mon Sep 17 00:00:00 2001 From: Charis <26616127+charislam@users.noreply.github.com> Date: Wed, 1 Oct 2025 10:45:34 -0400 Subject: [PATCH 01/22] feat: MCP URL builder (#36867) * feat: MCP URL builder - Creates an MCP URL builder component that allows a user to choose options via a UI, then automatically builds the URL and config needed for various MCP clients - Adds this component to studio Config modal and docs MCP page * fix(mcp url builder): mcp-remote options mcp-remote handles OAuth, so no need to pass personal access token * fix(docs): mcp We are pushing the remote MCP server rather than the local one, so removing references to the local server from documentation. * fix(mcp url builder): ui messages - Clarify read-only mode - Storage is not enabled in the defautl configuration * fix: mcp url builder - Refactor unnecessary hook into a utility function - Fix background color in dark mode * feat: update remote MCP base URL * docs: update MCP getting started - Restores relevant content from prod - Adds callout about authentication - Adds next steps to remote MCP installation section * Update apps/docs/content/guides/getting-started/mcp.mdx Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> * feat: click selected project to unselect This follows the unselect pattern from shadcn combobox example https://ui.shadcn.com/docs/components/combobox * Update apps/docs/content/guides/getting-started/mcp.mdx Co-authored-by: Greg Richardson * Update apps/docs/content/guides/getting-started/mcp.mdx Co-authored-by: Greg Richardson * Update apps/docs/content/guides/getting-started/mcp.mdx Co-authored-by: Greg Richardson * fix: light mode connection icons * chore: format `mcp.mdx` * fix: VS Code URL handler "name" Based on docs the field is "name" instead of "id": - https://code.visualstudio.com/docs/copilot/customization/mcp-servers#_commandline-configuration - https://code.visualstudio.com/docs/copilot/customization/mcp-servers#_url-handler * refactor: `NEXT_PUBLIC_MCP_URL` environment variable * feat: move client selector below options * feat: "Add to" buttons left aligned about config file * feat: actionable login hint * refactor: remote claude desktop client option --------- Co-authored-by: Matt Rossman <22670878+mattrossman@users.noreply.github.com> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Greg Richardson Co-authored-by: Danny White <3104761+dnywh@users.noreply.github.com> --- .../content/_partials/mcp_supabase_config.mdx | 156 -------------- .../_partials/mcp_supabase_vscode_config.mdx | 143 ------------- .../content/guides/getting-started/mcp.mdx | 202 +----------------- apps/docs/features/docs/MdxBase.shared.tsx | 4 +- apps/docs/features/ui/McpConfigPanel.tsx | 134 ++++++++++++ .../img/mcp-clients/claude-dark-icon.svg | 16 ++ .../public/img/mcp-clients/claude-icon.svg | 16 ++ .../img/mcp-clients/cursor-dark-icon.svg | 1 + .../public/img/mcp-clients/cursor-icon.svg | 1 + .../img/mcp-clients/vscode-dark-icon.svg | 41 ++++ .../public/img/mcp-clients/vscode-icon.svg | 41 ++++ .../img/mcp-clients/windsurf-dark-icon.svg | 3 + .../public/img/mcp-clients/windsurf-icon.svg | 3 + apps/docs/turbo.json | 1 + .../interfaces/Connect/Connect.constants.ts | 1 + .../components/interfaces/Connect/Connect.tsx | 13 ++ .../interfaces/Connect/ConnectDropdown.tsx | 24 ++- .../interfaces/Connect/ConnectionIcon.tsx | 19 +- .../interfaces/Connect/McpTabContent.tsx | 41 ++++ .../img/mcp-clients/claude-dark-icon.svg | 16 ++ .../public/img/mcp-clients/claude-icon.svg | 16 ++ .../img/mcp-clients/cursor-dark-icon.svg | 1 + .../public/img/mcp-clients/cursor-icon.svg | 1 + .../img/mcp-clients/vscode-dark-icon.svg | 41 ++++ .../public/img/mcp-clients/vscode-icon.svg | 41 ++++ .../img/mcp-clients/windsurf-dark-icon.svg | 3 + .../public/img/mcp-clients/windsurf-icon.svg | 3 + packages/ui-patterns/index.tsx | 1 + packages/ui-patterns/package.json | 4 + .../src/McpUrlBuilder/McpConfigPanel.tsx | 98 +++++++++ .../components/ClientSelectDropdown.tsx | 124 +++++++++++ .../components/ConnectionIcon.tsx | 26 +++ .../components/McpConfigurationDisplay.tsx | 101 +++++++++ .../components/McpConfigurationOptions.tsx | 80 +++++++ .../src/McpUrlBuilder/constants.tsx | 175 +++++++++++++++ .../ui-patterns/src/McpUrlBuilder/index.ts | 13 ++ .../ui-patterns/src/McpUrlBuilder/types.ts | 88 ++++++++ .../McpUrlBuilder/utils/getMcpButtonData.ts | 31 +++ .../src/McpUrlBuilder/utils/getMcpUrl.ts | 53 +++++ turbo.json | 1 + 40 files changed, 1277 insertions(+), 500 deletions(-) delete mode 100644 apps/docs/content/_partials/mcp_supabase_config.mdx delete mode 100644 apps/docs/content/_partials/mcp_supabase_vscode_config.mdx create mode 100644 apps/docs/features/ui/McpConfigPanel.tsx create mode 100644 apps/docs/public/img/mcp-clients/claude-dark-icon.svg create mode 100644 apps/docs/public/img/mcp-clients/claude-icon.svg create mode 100644 apps/docs/public/img/mcp-clients/cursor-dark-icon.svg create mode 100644 apps/docs/public/img/mcp-clients/cursor-icon.svg create mode 100644 apps/docs/public/img/mcp-clients/vscode-dark-icon.svg create mode 100644 apps/docs/public/img/mcp-clients/vscode-icon.svg create mode 100644 apps/docs/public/img/mcp-clients/windsurf-dark-icon.svg create mode 100644 apps/docs/public/img/mcp-clients/windsurf-icon.svg create mode 100644 apps/studio/components/interfaces/Connect/McpTabContent.tsx create mode 100644 apps/studio/public/img/mcp-clients/claude-dark-icon.svg create mode 100644 apps/studio/public/img/mcp-clients/claude-icon.svg create mode 100644 apps/studio/public/img/mcp-clients/cursor-dark-icon.svg create mode 100644 apps/studio/public/img/mcp-clients/cursor-icon.svg create mode 100644 apps/studio/public/img/mcp-clients/vscode-dark-icon.svg create mode 100644 apps/studio/public/img/mcp-clients/vscode-icon.svg create mode 100644 apps/studio/public/img/mcp-clients/windsurf-dark-icon.svg create mode 100644 apps/studio/public/img/mcp-clients/windsurf-icon.svg create mode 100644 packages/ui-patterns/src/McpUrlBuilder/McpConfigPanel.tsx create mode 100644 packages/ui-patterns/src/McpUrlBuilder/components/ClientSelectDropdown.tsx create mode 100644 packages/ui-patterns/src/McpUrlBuilder/components/ConnectionIcon.tsx create mode 100644 packages/ui-patterns/src/McpUrlBuilder/components/McpConfigurationDisplay.tsx create mode 100644 packages/ui-patterns/src/McpUrlBuilder/components/McpConfigurationOptions.tsx create mode 100644 packages/ui-patterns/src/McpUrlBuilder/constants.tsx create mode 100644 packages/ui-patterns/src/McpUrlBuilder/index.ts create mode 100644 packages/ui-patterns/src/McpUrlBuilder/types.ts create mode 100644 packages/ui-patterns/src/McpUrlBuilder/utils/getMcpButtonData.ts create mode 100644 packages/ui-patterns/src/McpUrlBuilder/utils/getMcpUrl.ts 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 871a25462b2..00000000000 --- 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 0117cb7d3e3..00000000000 --- 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 b5667aba713..908680fe3d8 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 +### Step 2: Configure your AI tool -MCP compatible tools connect to Supabase using the [Supabase MCP server](https://github.com/supabase-community/supabase-mcp). +Choose your project and MCP client and follow the installation instructions: -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. +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. + +Previously Supabase MCP required you to generate a personal access token (PAT), but this is no longer required. -### 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 - -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: - - <$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) - -
- [![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). - - - -### 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/features/docs/MdxBase.shared.tsx b/apps/docs/features/docs/MdxBase.shared.tsx index 7a5bb363be1..2e1fef8edc4 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/ui/McpConfigPanel.tsx b/apps/docs/features/ui/McpConfigPanel.tsx new file mode 100644 index 00000000000..f13716a3ffd --- /dev/null +++ b/apps/docs/features/ui/McpConfigPanel.tsx @@ -0,0 +1,134 @@ +'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' + +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} + + + ))} + + + + +
+ ) +} + +export function McpConfigPanel() { + const [selectedProject, setSelectedProject] = useState<{ ref: string; name: string } | null>(null) + const { theme } = useTheme() + + return ( +
+ +

+ Scope the MCP server to a project. If no project is selected, all projects will be + accessible. +

+ +
+ ) +} 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 00000000000..b68b27324e2 --- /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 00000000000..135a8b9f33f --- /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 00000000000..48f85eb4d5a --- /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 00000000000..48f85eb4d5a --- /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 00000000000..c453e633f34 --- /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 00000000000..c453e633f34 --- /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 00000000000..2e4e4e4921d --- /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 00000000000..386f8c035dc --- /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 d980120ebdc..5e22a06e3d6 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 33ff40fa88b..6636fdc76ca 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 7a6f1e21333..cc9bad797c1 100644 --- a/apps/studio/components/interfaces/Connect/Connect.tsx +++ b/apps/studio/components/interfaces/Connect/Connect.tsx @@ -34,6 +34,7 @@ import { CONNECTION_TYPES, ConnectionType, FRAMEWORKS, MOBILES, ORMS } from './C import { getContentFilePath } from './Connect.utils' import { ConnectDropdown } from './ConnectDropdown' import { ConnectTabContent } from './ConnectTabContent' +import { McpTabContent } from 'components/interfaces/Connect/McpTabContent' export const Connect = () => { const { ref: projectRef } = useParams() @@ -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 00000000000..4178d58d271 --- /dev/null +++ b/apps/studio/components/interfaces/Connect/McpTabContent.tsx @@ -0,0 +1,41 @@ +import { 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' + +export const McpTabContent = () => { + const { ref: projectRef } = useParams() + + return ( + + {projectRef ? ( + + ) : ( + + )} + + ) +} + +const McpTabContentInnerLoading = () => { + return ( +
+ + +
+ ) +} + +const McpTabContentInnerLoaded = ({ projectRef }: { projectRef: string }) => { + const { resolvedTheme } = useTheme() + + return ( + + ) +} 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 00000000000..b68b27324e2 --- /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 00000000000..135a8b9f33f --- /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 00000000000..48f85eb4d5a --- /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 00000000000..48f85eb4d5a --- /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 00000000000..c453e633f34 --- /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 00000000000..c453e633f34 --- /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 00000000000..2e4e4e4921d --- /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 00000000000..386f8c035dc --- /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 9685d8fff4f..f33a01ebe8d 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 fd886e8c602..5df8c48b9e3 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 00000000000..680b64d34c1 --- /dev/null +++ b/packages/ui-patterns/src/McpUrlBuilder/McpConfigPanel.tsx @@ -0,0 +1,98 @@ +'use client' + +import React, { 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, MCP_CLIENTS } from './constants' +import { getMcpUrl } from './utils/getMcpUrl' +import type { McpClient } from './types' + +export interface McpConfigPanelProps { + basePath: string + baseUrl?: string + projectRef?: string + initialSelectedClient?: McpClient + onClientSelect?: (client: McpClient) => void + theme?: 'light' | 'dark' + className?: string +} + +export function McpConfigPanel({ + basePath, + baseUrl, + projectRef, + initialSelectedClient, + onClientSelect, + className, + theme = 'dark', +}: McpConfigPanelProps) { + const [readonly, setReadonly] = useState(false) + const [selectedFeatures, setSelectedFeatures] = useState([]) + const [selectedClient, setSelectedClient] = useState(initialSelectedClient ?? MCP_CLIENTS[0]) + + const { clientConfig } = getMcpUrl({ + baseUrl, + projectRef, + readonly, + features: selectedFeatures, + 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 00000000000..7c1111c2b78 --- /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 00000000000..ec4d367e42c --- /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 00000000000..5ea38d01fe6 --- /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 00000000000..93517aafc89 --- /dev/null +++ b/packages/ui-patterns/src/McpUrlBuilder/components/McpConfigurationOptions.tsx @@ -0,0 +1,80 @@ +'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 { + readonly: boolean + onReadonlyChange: (readonly: boolean) => void + selectedFeatures: string[] + onFeaturesChange: (features: string[]) => void + featureGroups: McpFeatureGroup[] + className?: string +} + +export function McpConfigurationOptions({ + readonly, + onReadonlyChange, + selectedFeatures, + onFeaturesChange, + featureGroups, + className, +}: McpConfigurationOptionsProps) { + return ( +
+ {/* Readonly Mode */} +
+
+ + 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 00000000000..bcd1730f27a --- /dev/null +++ b/packages/ui-patterns/src/McpUrlBuilder/constants.tsx @@ -0,0 +1,175 @@ +import { CodeBlock } from 'ui/src/components/CodeBlock' +import type { + ClaudeCodeMcpConfig, + McpClient, + McpFeatureGroup, + OtherMcpConfig, + VSCodeMcpConfig, + WindsurfMcpConfig, +} from './types' + +export const FEATURE_GROUPS: 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 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 MCP_URL = process.env.NEXT_PUBLIC_MCP_URL ?? 'http://localhost:8080/mcp' diff --git a/packages/ui-patterns/src/McpUrlBuilder/index.ts b/packages/ui-patterns/src/McpUrlBuilder/index.ts new file mode 100644 index 00000000000..a48c727c2df --- /dev/null +++ b/packages/ui-patterns/src/McpUrlBuilder/index.ts @@ -0,0 +1,13 @@ +export { ClientSelectDropdown } from './components/ClientSelectDropdown' +export { ConnectionIcon } from './components/ConnectionIcon' +export { McpConfigurationDisplay } from './components/McpConfigurationDisplay' +export { McpConfigurationOptions } from './components/McpConfigurationOptions' +export { MCP_URL, FEATURE_GROUPS, 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 00000000000..7b6ae930012 --- /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 00000000000..b53d07c5828 --- /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 00000000000..4f22c128e5d --- /dev/null +++ b/packages/ui-patterns/src/McpUrlBuilder/utils/getMcpUrl.ts @@ -0,0 +1,53 @@ +import { MCP_URL } from '../constants' +import type { McpClient, McpClientConfig } from '../types' + +interface GetMcpUrlOptions { + projectRef?: string + baseUrl?: string + readonly?: boolean + features?: string[] + selectedClient?: McpClient +} + +interface GetMcpUrlReturn { + mcpUrl: string + clientConfig: McpClientConfig +} + +export function getMcpUrl({ + projectRef, + baseUrl = MCP_URL, + readonly = false, + features = [], + selectedClient, +}: GetMcpUrlOptions): GetMcpUrlReturn { + // Generate the MCP URL based on current configuration + const url = new URL(baseUrl) + if (projectRef) { + url.searchParams.set('project_ref', projectRef) + } + if (readonly) { + 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, + } +} diff --git a/turbo.json b/turbo.json index 028863f609c..a7c33bf5533 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", From 84601211e6bb4c5f316ea604a2ae96490ee38fc8 Mon Sep 17 00:00:00 2001 From: Matt Rossman <22670878+mattrossman@users.noreply.github.com> Date: Wed, 1 Oct 2025 11:45:36 -0400 Subject: [PATCH 02/22] feat: local/self-hosted support in MCP URL builder (#38981) * feat: MCP URL builder - Creates an MCP URL builder component that allows a user to choose options via a UI, then automatically builds the URL and config needed for various MCP clients - Adds this component to studio Config modal and docs MCP page * fix(mcp url builder): mcp-remote options mcp-remote handles OAuth, so no need to pass personal access token * fix(docs): mcp We are pushing the remote MCP server rather than the local one, so removing references to the local server from documentation. * fix(mcp url builder): ui messages - Clarify read-only mode - Storage is not enabled in the defautl configuration * fix: mcp url builder - Refactor unnecessary hook into a utility function - Fix background color in dark mode * feat: update remote MCP base URL * docs: update MCP getting started - Restores relevant content from prod - Adds callout about authentication - Adds next steps to remote MCP installation section * Update apps/docs/content/guides/getting-started/mcp.mdx Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> * feat: click selected project to unselect This follows the unselect pattern from shadcn combobox example https://ui.shadcn.com/docs/components/combobox * Update apps/docs/content/guides/getting-started/mcp.mdx Co-authored-by: Greg Richardson * Update apps/docs/content/guides/getting-started/mcp.mdx Co-authored-by: Greg Richardson * Update apps/docs/content/guides/getting-started/mcp.mdx Co-authored-by: Greg Richardson * fix: light mode connection icons * chore: format `mcp.mdx` * fix: VS Code URL handler "name" Based on docs the field is "name" instead of "id": - https://code.visualstudio.com/docs/copilot/customization/mcp-servers#_commandline-configuration - https://code.visualstudio.com/docs/copilot/customization/mcp-servers#_url-handler * refactor: `NEXT_PUBLIC_MCP_URL` environment variable * feat: move client selector below options * feat: "Add to" buttons left aligned about config file * feat: actionable login hint * feat: condition supported feature groups on platform * feat: platform selector, mcp url for self-hosted * feat: update copy for platform selection * fix: base URL logic, copy * fix: form MCP url from apiUrl in self-hosted studio * chore: unused file * fix: don't populate apiUrl for docs * fix: omit `project_ref` for self-hosted * docs: tweak copy for CLI platform * chore: optional instead of nullable `apiUrl` * chore: type sharing / inference for platform type * feat: 'development' self-hosted feature group * feat: omit read-only switch/param for local/self-hosted * refactor: remove claude desktop client option Replay of https://github.com/supabase/supabase/pull/36867/commits/59670f6771bca8e81fa62c0d3115288b82bfbb5d --------- Co-authored-by: Charis Lam <26616127+charislam@users.noreply.github.com> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Greg Richardson Co-authored-by: Danny White <3104761+dnywh@users.noreply.github.com> --- .../content/guides/getting-started/mcp.mdx | 2 +- apps/docs/features/ui/McpConfigPanel.tsx | 102 ++++++++++++++++-- .../components/interfaces/Connect/Connect.tsx | 4 +- .../interfaces/Connect/McpTabContent.tsx | 17 ++- .../src/McpUrlBuilder/McpConfigPanel.tsx | 28 +++-- .../components/McpConfigurationOptions.tsx | 24 +++-- .../src/McpUrlBuilder/constants.tsx | 9 +- .../ui-patterns/src/McpUrlBuilder/index.ts | 8 +- .../src/McpUrlBuilder/utils/getMcpUrl.ts | 27 +++-- 9 files changed, 179 insertions(+), 42 deletions(-) diff --git a/apps/docs/content/guides/getting-started/mcp.mdx b/apps/docs/content/guides/getting-started/mcp.mdx index 908680fe3d8..e3dbd33fba9 100644 --- a/apps/docs/content/guides/getting-started/mcp.mdx +++ b/apps/docs/content/guides/getting-started/mcp.mdx @@ -16,7 +16,7 @@ Before running the MCP server, we recommend you read our [security best practice ### Step 2: Configure your AI tool -Choose your project and MCP client and follow the installation instructions: +Choose your Supabase platform, project, and MCP client and follow the installation instructions: diff --git a/apps/docs/features/ui/McpConfigPanel.tsx b/apps/docs/features/ui/McpConfigPanel.tsx index f13716a3ffd..96411cd9299 100644 --- a/apps/docs/features/ui/McpConfigPanel.tsx +++ b/apps/docs/features/ui/McpConfigPanel.tsx @@ -21,6 +21,13 @@ import { 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, @@ -108,26 +115,105 @@ function ProjectSelector({ ) } +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 && ( + + )} +

- Scope the MCP server to a project. If no project is selected, all projects will be - accessible. + {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/studio/components/interfaces/Connect/Connect.tsx b/apps/studio/components/interfaces/Connect/Connect.tsx index cc9bad797c1..5f207ea614c 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' @@ -34,7 +35,6 @@ import { CONNECTION_TYPES, ConnectionType, FRAMEWORKS, MOBILES, ORMS } from './C import { getContentFilePath } from './Connect.utils' import { ConnectDropdown } from './ConnectDropdown' import { ConnectTabContent } from './ConnectTabContent' -import { McpTabContent } from 'components/interfaces/Connect/McpTabContent' export const Connect = () => { const { ref: projectRef } = useParams() @@ -365,7 +365,7 @@ export const Connect = () => { value="mcp" className={cn(DIALOG_PADDING_X, DIALOG_PADDING_Y, '!mt-0')} > - +
) } diff --git a/apps/studio/components/interfaces/Connect/McpTabContent.tsx b/apps/studio/components/interfaces/Connect/McpTabContent.tsx index 4178d58d271..bbb9beeb640 100644 --- a/apps/studio/components/interfaces/Connect/McpTabContent.tsx +++ b/apps/studio/components/interfaces/Connect/McpTabContent.tsx @@ -1,17 +1,18 @@ -import { useParams } from 'common' +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 = () => { +export const McpTabContent = ({ projectKeys }: { projectKeys: projectKeys }) => { const { ref: projectRef } = useParams() return ( {projectRef ? ( - + ) : ( )} @@ -28,7 +29,13 @@ const McpTabContentInnerLoading = () => { ) } -const McpTabContentInnerLoaded = ({ projectRef }: { projectRef: string }) => { +const McpTabContentInnerLoaded = ({ + projectRef, + projectKeys, +}: { + projectRef: string + projectKeys: projectKeys +}) => { const { resolvedTheme } = useTheme() return ( @@ -36,6 +43,8 @@ const McpTabContentInnerLoaded = ({ projectRef }: { projectRef: string }) => { basePath={BASE_PATH} projectRef={projectRef} theme={resolvedTheme as 'light' | 'dark'} + isPlatform={IS_PLATFORM} + apiUrl={projectKeys.apiUrl ?? undefined} /> ) } diff --git a/packages/ui-patterns/src/McpUrlBuilder/McpConfigPanel.tsx b/packages/ui-patterns/src/McpUrlBuilder/McpConfigPanel.tsx index 680b64d34c1..709130225f3 100644 --- a/packages/ui-patterns/src/McpUrlBuilder/McpConfigPanel.tsx +++ b/packages/ui-patterns/src/McpUrlBuilder/McpConfigPanel.tsx @@ -1,14 +1,14 @@ 'use client' -import React, { useState } from 'react' +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, MCP_CLIENTS } from './constants' -import { getMcpUrl } from './utils/getMcpUrl' +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 @@ -18,26 +18,37 @@ export interface McpConfigPanelProps { 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, - baseUrl, 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({ - baseUrl, projectRef, + isPlatform, + apiUrl, readonly, - features: selectedFeatures, + features: selectedFeaturesSupported, selectedClient, }) @@ -60,11 +71,12 @@ export function McpConfigPanel({
diff --git a/packages/ui-patterns/src/McpUrlBuilder/components/McpConfigurationOptions.tsx b/packages/ui-patterns/src/McpUrlBuilder/components/McpConfigurationOptions.tsx index 93517aafc89..7d71e8a8f91 100644 --- a/packages/ui-patterns/src/McpUrlBuilder/components/McpConfigurationOptions.tsx +++ b/packages/ui-patterns/src/McpUrlBuilder/components/McpConfigurationOptions.tsx @@ -13,6 +13,7 @@ import { import type { McpFeatureGroup } from '../types' interface McpConfigurationOptionsProps { + isPlatform: boolean readonly: boolean onReadonlyChange: (readonly: boolean) => void selectedFeatures: string[] @@ -22,6 +23,7 @@ interface McpConfigurationOptionsProps { } export function McpConfigurationOptions({ + isPlatform, readonly, onReadonlyChange, selectedFeatures, @@ -32,17 +34,19 @@ export function McpConfigurationOptions({ return (
{/* Readonly Mode */} -
-
- - Only allow read operations on your database + {isPlatform && ( +
+
+ + Only allow read operations on your database +
+
+ +
-
- -
-
+ )} {/* Feature Groups */}
diff --git a/packages/ui-patterns/src/McpUrlBuilder/constants.tsx b/packages/ui-patterns/src/McpUrlBuilder/constants.tsx index bcd1730f27a..67f6e7c8875 100644 --- a/packages/ui-patterns/src/McpUrlBuilder/constants.tsx +++ b/packages/ui-patterns/src/McpUrlBuilder/constants.tsx @@ -8,7 +8,7 @@ import type { WindsurfMcpConfig, } from './types' -export const FEATURE_GROUPS: McpFeatureGroup[] = [ +export const FEATURE_GROUPS_PLATFORM: McpFeatureGroup[] = [ { id: 'docs', name: 'Documentation', @@ -51,6 +51,10 @@ export const FEATURE_GROUPS: McpFeatureGroup[] = [ }, ] +export const FEATURE_GROUPS_NON_PLATFORM = FEATURE_GROUPS_PLATFORM.filter((group) => + ['docs', 'database', 'development'].includes(group.id) +) + export const MCP_CLIENTS: McpClient[] = [ { key: 'cursor', @@ -172,4 +176,5 @@ export const MCP_CLIENTS: McpClient[] = [ }, ] -export const MCP_URL = process.env.NEXT_PUBLIC_MCP_URL ?? 'http://localhost:8080/mcp' +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 a48c727c2df..a15b032d884 100644 --- a/packages/ui-patterns/src/McpUrlBuilder/index.ts +++ b/packages/ui-patterns/src/McpUrlBuilder/index.ts @@ -2,7 +2,13 @@ export { ClientSelectDropdown } from './components/ClientSelectDropdown' export { ConnectionIcon } from './components/ConnectionIcon' export { McpConfigurationDisplay } from './components/McpConfigurationDisplay' export { McpConfigurationOptions } from './components/McpConfigurationOptions' -export { MCP_URL, FEATURE_GROUPS, MCP_CLIENTS } from './constants' +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 { diff --git a/packages/ui-patterns/src/McpUrlBuilder/utils/getMcpUrl.ts b/packages/ui-patterns/src/McpUrlBuilder/utils/getMcpUrl.ts index 4f22c128e5d..94dc41f8cb7 100644 --- a/packages/ui-patterns/src/McpUrlBuilder/utils/getMcpUrl.ts +++ b/packages/ui-patterns/src/McpUrlBuilder/utils/getMcpUrl.ts @@ -1,12 +1,13 @@ -import { MCP_URL } from '../constants' +import { DEFAULT_MCP_URL_NON_PLATFORM, DEFAULT_MCP_URL_PLATFORM } from '../constants' import type { McpClient, McpClientConfig } from '../types' interface GetMcpUrlOptions { projectRef?: string - baseUrl?: string readonly?: boolean features?: string[] selectedClient?: McpClient + isPlatform: boolean + apiUrl?: string } interface GetMcpUrlReturn { @@ -16,17 +17,18 @@ interface GetMcpUrlReturn { export function getMcpUrl({ projectRef, - baseUrl = MCP_URL, + isPlatform, + apiUrl, readonly = false, features = [], selectedClient, }: GetMcpUrlOptions): GetMcpUrlReturn { // Generate the MCP URL based on current configuration - const url = new URL(baseUrl) - if (projectRef) { + const url = new URL(getMcpUrlBase({ isPlatform, apiUrl })) + if (projectRef && isPlatform) { url.searchParams.set('project_ref', projectRef) } - if (readonly) { + if (readonly && isPlatform) { url.searchParams.set('read_only', 'true') } if (features.length > 0) { @@ -51,3 +53,16 @@ export function getMcpUrl({ 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 +} From 843234e153ad16e303b332e02852f268211530a7 Mon Sep 17 00:00:00 2001 From: Joshen Lim Date: Thu, 2 Oct 2025 01:26:14 +0800 Subject: [PATCH 03/22] Add flag to hide email and phone columns in auth users (#39151) * add flag to hide email and phone columns in auth users * fix: add authentication:show_email_phone_columns to required schema --------- Co-authored-by: Charis Lam <26616127+charislam@users.noreply.github.com> --- .../interfaces/Auth/Users/Users.utils.tsx | 8 ++++--- .../interfaces/Auth/Users/UsersV2.tsx | 22 +++++++++++++++---- .../enabled-features/enabled-features.json | 7 +++--- .../enabled-features.schema.json | 17 +++++++++----- 4 files changed, 38 insertions(+), 16 deletions(-) diff --git a/apps/studio/components/interfaces/Auth/Users/Users.utils.tsx b/apps/studio/components/interfaces/Auth/Users/Users.utils.tsx index 92c05be3e02..ca935717c1d 100644 --- a/apps/studio/components/interfaces/Auth/Users/Users.utils.tsx +++ b/apps/studio/components/interfaces/Auth/Users/Users.utils.tsx @@ -15,7 +15,7 @@ import { copyToClipboard, } from 'ui' import { PROVIDERS_SCHEMAS } from '../AuthProvidersFormValidation' -import { ColumnConfiguration, USERS_TABLE_COLUMNS } from './Users.constants' +import { ColumnConfiguration, UsersTableColumn } from './Users.constants' import { HeaderCell } from './UsersGridComponents' const GITHUB_AVATAR_URL = 'https://avatars.githubusercontent.com' @@ -250,21 +250,23 @@ export function getAvatarUrl(user: User): string | undefined { } export const formatUserColumns = ({ + columns, config, users, visibleColumns = [], setSortByValue, onSelectDeleteUser, }: { + columns: UsersTableColumn[] config: ColumnConfiguration[] users: User[] visibleColumns?: string[] setSortByValue: (val: string) => void onSelectDeleteUser: (user: User) => void }) => { - const columnOrder = config.map((c) => c.id) ?? USERS_TABLE_COLUMNS.map((c) => c.id) + const columnOrder = config.map((c) => c.id) ?? columns.map((c) => c.id) - let gridColumns = USERS_TABLE_COLUMNS.map((col) => { + let gridColumns = columns.map((col) => { const savedConfig = config.find((c) => c.id === col.id) const res: Column = { key: col.id, diff --git a/apps/studio/components/interfaces/Auth/Users/UsersV2.tsx b/apps/studio/components/interfaces/Auth/Users/UsersV2.tsx index d1564757727..6570110caf0 100644 --- a/apps/studio/components/interfaces/Auth/Users/UsersV2.tsx +++ b/apps/studio/components/interfaces/Auth/Users/UsersV2.tsx @@ -86,13 +86,25 @@ export const UsersV2 = () => { authenticationShowSortByEmail: showSortByEmail, authenticationShowSortByPhone: showSortByPhone, authenticationShowUserTypeFilter: showUserTypeFilter, + authenticationShowEmailPhoneColumns: showEmailPhoneColumns, } = useIsFeatureEnabled([ 'authentication:show_provider_filter', 'authentication:show_sort_by_email', 'authentication:show_sort_by_phone', 'authentication:show_user_type_filter', + 'authentication:show_email_phone_columns', ]) + const userTableColumns = useMemo(() => { + if (showEmailPhoneColumns) return USERS_TABLE_COLUMNS + else { + return USERS_TABLE_COLUMNS.filter((col) => { + if (col.id === 'email' || col.id === 'phone') return false + return true + }) + } + }, [showEmailPhoneColumns]) + const [columns, setColumns] = useState[]>([]) const [search, setSearch] = useState('') const [filter, setFilter] = useState('all') @@ -265,6 +277,7 @@ export const UsersV2 = () => { (isErrorStorage && (errorStorage as Error).message.includes('data is undefined'))) ) { const columns = formatUserColumns({ + columns: userTableColumns, config: columnConfiguration ?? [], users: users ?? [], visibleColumns: selectedColumns, @@ -272,7 +285,7 @@ export const UsersV2 = () => { onSelectDeleteUser: setSelectedUserToDelete, }) setColumns(columns) - if (columns.length < USERS_TABLE_COLUMNS.length) { + if (columns.length < userTableColumns.length) { setSelectedColumns(columns.filter((col) => col.key !== 'img').map((col) => col.key)) } } @@ -386,7 +399,7 @@ export const UsersV2 = () => { name={selectedColumns.length === 0 ? 'All columns' : 'Columns'} title="Select columns to show" buttonType={selectedColumns.length === 0 ? 'dashed' : 'default'} - options={USERS_TABLE_COLUMNS.slice(1)} // Ignore user image column + options={userTableColumns.slice(1)} // Ignore user image column labelKey="name" valueKey="id" labelClass="text-xs" @@ -401,19 +414,20 @@ export const UsersV2 = () => { let updatedConfig = (columnConfiguration ?? []).slice() if (value.length === 0) { - updatedConfig = USERS_TABLE_COLUMNS.map((c) => ({ id: c.id, width: c.width })) + updatedConfig = userTableColumns.map((c) => ({ id: c.id, width: c.width })) } else { value.forEach((col) => { const hasExisting = updatedConfig.find((c) => c.id === col) if (!hasExisting) updatedConfig.push({ id: col, - width: USERS_TABLE_COLUMNS.find((c) => c.id === col)?.width, + width: userTableColumns.find((c) => c.id === col)?.width, }) }) } const updatedColumns = formatUserColumns({ + columns: userTableColumns, config: updatedConfig, users: users ?? [], visibleColumns: value, diff --git a/packages/common/enabled-features/enabled-features.json b/packages/common/enabled-features/enabled-features.json index 3ecd75981cf..6bef25c14fc 100644 --- a/packages/common/enabled-features/enabled-features.json +++ b/packages/common/enabled-features/enabled-features.json @@ -14,14 +14,15 @@ "authentication:attack_protection": true, "authentication:advanced": true, - "authentication:show_providers": true, + + "authentication:show_email_phone_columns": true, "authentication:show_manual_linking": true, - "authentication:show_send_invitation": true, "authentication:show_provider_filter": true, + "authentication:show_providers": true, + "authentication:show_send_invitation": true, "authentication:show_sort_by_email": true, "authentication:show_sort_by_phone": true, "authentication:show_user_type_filter": true, - "billing:all": true, "dashboard_auth:sign_up": true, diff --git a/packages/common/enabled-features/enabled-features.schema.json b/packages/common/enabled-features/enabled-features.schema.json index e678d317abc..3ee59127865 100644 --- a/packages/common/enabled-features/enabled-features.schema.json +++ b/packages/common/enabled-features/enabled-features.schema.json @@ -80,6 +80,10 @@ "type": "boolean", "description": "Show the user type filter in the authentication users page" }, + "authentication:show_email_phone_columns": { + "type": "boolean", + "description": "Show the email and phone columns in the authentication users page" + }, "billing:all": { "type": "boolean", @@ -366,20 +370,21 @@ "ai:opt_in_level_schema", "ai:opt_in_level_schema_and_log", "ai:opt_in_level_schema_and_log_and_data", - "authentication:sign_in_providers", - "authentication:third_party_auth", - "authentication:rate_limits", + "authentication:advanced", + "authentication:attack_protection", "authentication:emails", "authentication:multi_factor", - "authentication:attack_protection", - "authentication:advanced", - "authentication:show_providers", + "authentication:rate_limits", + "authentication:show_email_phone_columns", "authentication:show_manual_linking", "authentication:show_provider_filter", + "authentication:show_providers", "authentication:show_send_invitation", "authentication:show_sort_by_email", "authentication:show_sort_by_phone", "authentication:show_user_type_filter", + "authentication:sign_in_providers", + "authentication:third_party_auth", "billing:all", "dashboard_auth:sign_up", "dashboard_auth:sign_in_with_github", From 579332fda3f6ef8047fb495a85723c150260a61d Mon Sep 17 00:00:00 2001 From: Joshen Lim Date: Thu, 2 Oct 2025 01:34:20 +0800 Subject: [PATCH 04/22] Add flag to disable wrappers UI (#39150) --- .../Landing/AvailableIntegrations.tsx | 8 ++-- .../Landing/useInstalledIntegrations.tsx | 46 ++++++++++++------- .../DatabaseLayout/Database.Commands.tsx | 21 +++++---- .../layouts/DatabaseLayout/DatabaseLayout.tsx | 10 ++-- .../DatabaseLayout/DatabaseMenu.utils.tsx | 20 +++++--- .../layouts/Integrations/layout.tsx | 33 +++++++++---- .../Integrations.Commands.tsx | 8 ++-- .../integrations/[id]/[pageId]/index.tsx | 9 +++- .../enabled-features/enabled-features.json | 2 +- .../enabled-features.schema.json | 6 +-- 10 files changed, 102 insertions(+), 61 deletions(-) diff --git a/apps/studio/components/interfaces/Integrations/Landing/AvailableIntegrations.tsx b/apps/studio/components/interfaces/Integrations/Landing/AvailableIntegrations.tsx index 2d740de982b..864c3338fa1 100644 --- a/apps/studio/components/interfaces/Integrations/Landing/AvailableIntegrations.tsx +++ b/apps/studio/components/interfaces/Integrations/Landing/AvailableIntegrations.tsx @@ -18,9 +18,7 @@ const CATEGORIES = [ ] as const export const AvailableIntegrations = () => { - const { integrationsShowStripeWrapper } = useIsFeatureEnabled([ - 'integrations:show_stripe_wrapper', - ]) + const { integrationsWrappers } = useIsFeatureEnabled(['integrations:wrappers']) const [selectedCategory, setSelectedCategory] = useQueryState( 'category', @@ -43,9 +41,9 @@ export const AvailableIntegrations = () => { const installedIds = installedIntegrations.map((i) => i.id) // available integrations for install - const availableIntegrations = integrationsShowStripeWrapper + const availableIntegrations = integrationsWrappers ? allIntegrations - : allIntegrations.filter((x) => x.id !== 'stripe_wrapper') + : allIntegrations.filter((x) => !x.id.endsWith('_wrapper')) const integrationsByCategory = selectedCategory === 'all' ? availableIntegrations diff --git a/apps/studio/components/interfaces/Integrations/Landing/useInstalledIntegrations.tsx b/apps/studio/components/interfaces/Integrations/Landing/useInstalledIntegrations.tsx index 9df420f1b13..0834121c2ea 100644 --- a/apps/studio/components/interfaces/Integrations/Landing/useInstalledIntegrations.tsx +++ b/apps/studio/components/interfaces/Integrations/Landing/useInstalledIntegrations.tsx @@ -3,6 +3,7 @@ import { useMemo } from 'react' import { useDatabaseExtensionsQuery } from 'data/database-extensions/database-extensions-query' import { useSchemasQuery } from 'data/database/schemas-query' import { useFDWsQuery } from 'data/fdw/fdws-query' +import { useIsFeatureEnabled } from 'hooks/misc/useIsFeatureEnabled' import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject' import { EMPTY_ARR } from 'lib/void' import { wrapperMetaComparator } from '../Wrappers/Wrappers.utils' @@ -10,6 +11,15 @@ import { INTEGRATIONS } from './Integrations.constants' export const useInstalledIntegrations = () => { const { data: project } = useSelectedProjectQuery() + const { integrationsWrappers } = useIsFeatureEnabled(['integrations:wrappers']) + + const allIntegrations = useMemo(() => { + if (integrationsWrappers) { + return INTEGRATIONS + } else { + return INTEGRATIONS.filter((integration) => !integration.id.endsWith('_wrapper')) + } + }, [integrationsWrappers]) const { data, @@ -47,28 +57,30 @@ export const useInstalledIntegrations = () => { const wrappers = useMemo(() => data ?? EMPTY_ARR, [data]) const installedIntegrations = useMemo(() => { - return INTEGRATIONS.filter((i) => { - // special handling for supabase webhooks - if (i.id === 'webhooks') { - return isHooksEnabled - } - if (i.type === 'wrapper') { - return wrappers.find((w) => wrapperMetaComparator(i.meta, w)) - } - if (i.type === 'postgres_extension') { - return i.requiredExtensions.every((extName) => { - const foundExtension = (extensions ?? []).find((ext) => ext.name === extName) - return !!foundExtension?.installed_version - }) - } - return false - }).sort((a, b) => a.name.localeCompare(b.name)) + return allIntegrations + .filter((i) => { + // special handling for supabase webhooks + if (i.id === 'webhooks') { + return isHooksEnabled + } + if (i.type === 'wrapper') { + return wrappers.find((w) => wrapperMetaComparator(i.meta, w)) + } + if (i.type === 'postgres_extension') { + return i.requiredExtensions.every((extName) => { + const foundExtension = (extensions ?? []).find((ext) => ext.name === extName) + return !!foundExtension?.installed_version + }) + } + return false + }) + .sort((a, b) => a.name.localeCompare(b.name)) }, [wrappers, extensions, isHooksEnabled]) // available integrations are all integrations that can be installed. If an integration can't be installed (needed // extensions are not available on this DB image), the UI will provide a tooltip explaining why. const availableIntegrations = useMemo( - () => INTEGRATIONS.sort((a, b) => a.name.localeCompare(b.name)), + () => allIntegrations.sort((a, b) => a.name.localeCompare(b.name)), [] ) diff --git a/apps/studio/components/layouts/DatabaseLayout/Database.Commands.tsx b/apps/studio/components/layouts/DatabaseLayout/Database.Commands.tsx index 3abd34af48c..ddfb6dd7f72 100644 --- a/apps/studio/components/layouts/DatabaseLayout/Database.Commands.tsx +++ b/apps/studio/components/layouts/DatabaseLayout/Database.Commands.tsx @@ -12,9 +12,10 @@ export function useDatabaseGotoCommands(options?: CommandOptions) { let { ref } = useParams() ref ||= '_' - const { databaseReplication, databaseRoles } = useIsFeatureEnabled([ + const { databaseReplication, databaseRoles, integrationsWrappers } = useIsFeatureEnabled([ 'database:replication', 'database:roles', + 'integrations:wrappers', ]) useRegisterCommands( @@ -102,13 +103,17 @@ export function useDatabaseGotoCommands(options?: CommandOptions) { route: `/project/${ref}/database/backups/scheduled`, defaultHidden: true, }, - { - id: 'nav-database-wrappers', - name: 'Wrappers', - value: 'Database: Wrappers', - route: `/project/${ref}/integrations/wrappers`, - defaultHidden: true, - }, + ...(integrationsWrappers + ? [ + { + id: 'nav-database-wrappers', + name: 'Wrappers', + value: 'Database: Wrappers', + route: `/project/${ref}/integrations?category=wrappers`, + defaultHidden: true, + } as IRouteCommand, + ] + : []), { id: 'nav-database-migrations', name: 'Migrations', diff --git a/apps/studio/components/layouts/DatabaseLayout/DatabaseLayout.tsx b/apps/studio/components/layouts/DatabaseLayout/DatabaseLayout.tsx index 4919aac04a7..59f74122a4f 100644 --- a/apps/studio/components/layouts/DatabaseLayout/DatabaseLayout.tsx +++ b/apps/studio/components/layouts/DatabaseLayout/DatabaseLayout.tsx @@ -33,10 +33,11 @@ const DatabaseProductMenu = () => { const columnLevelPrivileges = useIsColumnLevelPrivilegesEnabled() const enablePgReplicate = useFlag('enablePgReplicate') - const { databaseReplication: showPgReplicate, databaseRoles: showRoles } = useIsFeatureEnabled([ - 'database:replication', - 'database:roles', - ]) + const { + databaseReplication: showPgReplicate, + databaseRoles: showRoles, + integrationsWrappers: showWrappers, + } = useIsFeatureEnabled(['database:replication', 'database:roles', 'integrations:wrappers']) return ( <> @@ -49,6 +50,7 @@ const DatabaseProductMenu = () => { enablePgReplicate, showPgReplicate, showRoles, + showWrappers, })} /> diff --git a/apps/studio/components/layouts/DatabaseLayout/DatabaseMenu.utils.tsx b/apps/studio/components/layouts/DatabaseLayout/DatabaseMenu.utils.tsx index 8226871f54a..d551edcc0bd 100644 --- a/apps/studio/components/layouts/DatabaseLayout/DatabaseMenu.utils.tsx +++ b/apps/studio/components/layouts/DatabaseLayout/DatabaseMenu.utils.tsx @@ -13,6 +13,7 @@ export const generateDatabaseMenu = ( enablePgReplicate: boolean showPgReplicate: boolean showRoles: boolean + showWrappers: boolean } ): ProductMenuGroup[] => { const ref = project?.ref ?? 'default' @@ -23,6 +24,7 @@ export const generateDatabaseMenu = ( enablePgReplicate, showPgReplicate, showRoles, + showWrappers, } = flags || {} return [ @@ -134,13 +136,17 @@ export const generateDatabaseMenu = ( url: `/project/${ref}/database/migrations`, items: [], }, - { - name: 'Wrappers', - key: 'wrappers', - url: `/project/${ref}/integrations?category=wrapper`, - rightIcon: , - items: [], - }, + ...(showWrappers + ? [ + { + name: 'Wrappers', + key: 'wrappers', + url: `/project/${ref}/integrations?category=wrapper`, + rightIcon: , + items: [], + }, + ] + : []), ...(!!pgNetExtensionExists ? [ { diff --git a/apps/studio/components/layouts/Integrations/layout.tsx b/apps/studio/components/layouts/Integrations/layout.tsx index bef2d1ce074..87732d1a80d 100644 --- a/apps/studio/components/layouts/Integrations/layout.tsx +++ b/apps/studio/components/layouts/Integrations/layout.tsx @@ -1,13 +1,13 @@ import { useRouter } from 'next/router' import { PropsWithChildren } from 'react' -import { useFlag } from 'common' import { useInstalledIntegrations } from 'components/interfaces/Integrations/Landing/useInstalledIntegrations' import ProjectLayout from 'components/layouts/ProjectLayout/ProjectLayout' import AlertError from 'components/ui/AlertError' import { ProductMenu } from 'components/ui/ProductMenu' import { ProductMenuGroup } from 'components/ui/ProductMenu/ProductMenu.types' import ProductMenuItem from 'components/ui/ProductMenu/ProductMenuItem' +import { useIsFeatureEnabled } from 'hooks/misc/useIsFeatureEnabled' import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject' import { withAuth } from 'hooks/misc/withAuth' import { Menu, Separator } from 'ui' @@ -20,6 +20,7 @@ import { GenericSkeletonLoader } from 'ui-patterns' const IntegrationsLayout = ({ children }: PropsWithChildren) => { const router = useRouter() const { data: project } = useSelectedProjectQuery() + const { integrationsWrappers: showWrappers } = useIsFeatureEnabled(['integrations:wrappers']) const segments = router.asPath.split('/') // construct the page url to be used to determine the active state for the sidebar @@ -65,7 +66,7 @@ const IntegrationsLayout = ({ children }: PropsWithChildren) => { : 'integrations' : page } - menu={generateIntegrationsMenu({ projectRef: project?.ref })} + menu={generateIntegrationsMenu({ projectRef: project?.ref, flags: { showWrappers } })} />
@@ -110,7 +111,15 @@ const IntegrationsLayout = ({ children }: PropsWithChildren) => { // Wrap component with authentication HOC before exporting export default withAuth(IntegrationsLayout) -const generateIntegrationsMenu = ({ projectRef }: { projectRef?: string }): ProductMenuGroup[] => { +const generateIntegrationsMenu = ({ + projectRef, + flags, +}: { + projectRef?: string + flags?: { showWrappers: boolean } +}): ProductMenuGroup[] => { + const { showWrappers } = flags ?? {} + return [ { title: 'Explore', @@ -122,13 +131,17 @@ const generateIntegrationsMenu = ({ projectRef }: { projectRef?: string }): Prod pages: ['integrations'], items: [], }, - { - name: 'Wrappers', - key: 'integrations-wrapper', - url: `/project/${projectRef}/integrations?category=wrapper`, - pages: ['integrations?category=wrapper'], - items: [], - }, + ...(showWrappers + ? [ + { + name: 'Wrappers', + key: 'integrations-wrapper', + url: `/project/${projectRef}/integrations?category=wrapper`, + pages: ['integrations?category=wrapper'], + items: [], + }, + ] + : []), { name: 'Postgres Modules', key: 'integrations-postgres_extension', diff --git a/apps/studio/components/layouts/IntegrationsLayout/Integrations.Commands.tsx b/apps/studio/components/layouts/IntegrationsLayout/Integrations.Commands.tsx index 510407e9c72..6a775092ea8 100644 --- a/apps/studio/components/layouts/IntegrationsLayout/Integrations.Commands.tsx +++ b/apps/studio/components/layouts/IntegrationsLayout/Integrations.Commands.tsx @@ -13,13 +13,11 @@ export function useIntegrationsGotoCommands(options?: CommandOptions) { let { ref } = useParams() ref ||= '_' - const { integrationsShowStripeWrapper } = useIsFeatureEnabled([ - 'integrations:show_stripe_wrapper', - ]) + const { integrationsWrappers } = useIsFeatureEnabled(['integrations:wrappers']) - const allIntegrations = integrationsShowStripeWrapper + const allIntegrations = integrationsWrappers ? INTEGRATIONS - : INTEGRATIONS.filter((x) => x.id !== 'stripe_wrapper') + : INTEGRATIONS.filter((x) => !x.id.endsWith('_wrapper')) const getName = (integration: IntegrationDefinition) => { switch (integration.id) { diff --git a/apps/studio/pages/project/[ref]/integrations/[id]/[pageId]/index.tsx b/apps/studio/pages/project/[ref]/integrations/[id]/[pageId]/index.tsx index b0eb70e5efe..1b7ced2f8ee 100644 --- a/apps/studio/pages/project/[ref]/integrations/[id]/[pageId]/index.tsx +++ b/apps/studio/pages/project/[ref]/integrations/[id]/[pageId]/index.tsx @@ -3,9 +3,11 @@ import { INTEGRATIONS } from 'components/interfaces/Integrations/Landing/Integra import { useInstalledIntegrations } from 'components/interfaces/Integrations/Landing/useInstalledIntegrations' import DefaultLayout from 'components/layouts/DefaultLayout' import IntegrationsLayout from 'components/layouts/Integrations/layout' -import { PageLayout, NavigationItem } from 'components/layouts/PageLayout/PageLayout' +import { NavigationItem, PageLayout } from 'components/layouts/PageLayout/PageLayout' import { ScaffoldContainer, ScaffoldSection } from 'components/layouts/Scaffold' import { GenericSkeletonLoader } from 'components/ui/ShimmeringLoader' +import { UnknownInterface } from 'components/ui/UnknownInterface' +import { useIsFeatureEnabled } from 'hooks/misc/useIsFeatureEnabled' import { useRouter } from 'next/compat/router' import { useEffect, useMemo } from 'react' import { NextPageWithLayout } from 'types' @@ -14,6 +16,7 @@ import { Admonition } from 'ui-patterns' const IntegrationPage: NextPageWithLayout = () => { const router = useRouter() const { ref, id, pageId, childId } = useParams() + const { integrationsWrappers } = useIsFeatureEnabled(['integrations:wrappers']) const { installedIntegrations: installedIntegrations, isLoading: isIntegrationsLoading } = useInstalledIntegrations() @@ -117,6 +120,10 @@ const IntegrationPage: NextPageWithLayout = () => { return null } + if (!integrationsWrappers && id?.endsWith('_wrapper')) { + return + } + return ( Date: Thu, 2 Oct 2025 01:42:23 +0800 Subject: [PATCH 05/22] Add flag for metadata IP logs explorer template (#39152) * Add flag for metadata IP logs explorer template * Flip true * fix(log explorer): also remove metadata ip template from templates page when flag disabled --------- Co-authored-by: Charis Lam <26616127+charislam@users.noreply.github.com> --- .../pages/project/[ref]/logs/explorer/index.tsx | 11 +++++++++-- .../pages/project/[ref]/logs/explorer/templates.tsx | 10 ++++++++-- .../common/enabled-features/enabled-features.json | 1 + .../enabled-features/enabled-features.schema.json | 5 +++++ 4 files changed, 23 insertions(+), 4 deletions(-) diff --git a/apps/studio/pages/project/[ref]/logs/explorer/index.tsx b/apps/studio/pages/project/[ref]/logs/explorer/index.tsx index 79f7ff78c29..8fe0c6e02e1 100644 --- a/apps/studio/pages/project/[ref]/logs/explorer/index.tsx +++ b/apps/studio/pages/project/[ref]/logs/explorer/index.tsx @@ -3,7 +3,7 @@ import { useLocalStorage } from '@uidotdev/usehooks' import dayjs from 'dayjs' import { editor } from 'monaco-editor' import { useRouter } from 'next/router' -import { useEffect, useRef, useState } from 'react' +import { useEffect, useMemo, useRef, useState } from 'react' import { toast } from 'sonner' import { IS_PLATFORM, LOCAL_STORAGE_KEYS, useParams } from 'common' @@ -37,6 +37,7 @@ import { import useLogsQuery from 'hooks/analytics/useLogsQuery' import { useLogsUrlState } from 'hooks/analytics/useLogsUrlState' import { useCustomContent } from 'hooks/custom-content/useCustomContent' +import { useIsFeatureEnabled } from 'hooks/misc/useIsFeatureEnabled' import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization' import { useUpgradePrompt } from 'hooks/misc/useUpgradePrompt' import { uuidv4 } from 'lib/helpers' @@ -66,6 +67,12 @@ export const LogsExplorerPage: NextPageWithLayout = () => { const { ref, q, queryId } = useParams() const projectRef = ref as string const { data: organization } = useSelectedOrganizationQuery() + const { logsShowMetadataIpTemplate } = useIsFeatureEnabled(['logs:show_metadata_ip_template']) + + const allTemplates = useMemo(() => { + if (logsShowMetadataIpTemplate) return TEMPLATES + else return TEMPLATES.filter((x) => x.label !== 'Metadata IP') + }, [logsShowMetadataIpTemplate]) const editorRef = useRef() const [editorId] = useState(uuidv4()) @@ -301,7 +308,7 @@ export const LogsExplorerPage: NextPageWithLayout = () => { defaultTo={timestampEnd || ''} onDateChange={handleDateChange} onSelectSource={handleInsertSource} - templates={TEMPLATES.filter((template) => template.mode === 'custom')} + templates={allTemplates.filter((template) => template.mode === 'custom')} onSelectTemplate={onSelectTemplate} warnings={warnings} /> diff --git a/apps/studio/pages/project/[ref]/logs/explorer/templates.tsx b/apps/studio/pages/project/[ref]/logs/explorer/templates.tsx index 9c93527c570..15a52d0cbc4 100644 --- a/apps/studio/pages/project/[ref]/logs/explorer/templates.tsx +++ b/apps/studio/pages/project/[ref]/logs/explorer/templates.tsx @@ -15,17 +15,23 @@ import { Button, Popover, cn } from 'ui' export const LogsTemplatesPage: NextPageWithLayout = () => { const { ref: projectRef } = useParams() - const isTemplatesEnabled = useIsFeatureEnabled('logs:templates') + const { logsTemplates: isTemplatesEnabled, logsShowMetadataIpTemplate: showMetadataIpTemplate } = + useIsFeatureEnabled(['logs:templates', 'logs:show_metadata_ip_template']) if (!isTemplatesEnabled) { return } + const allTemplates = showMetadataIpTemplate + ? TEMPLATES + : TEMPLATES.filter((template) => template.label !== 'Metadata IP') + return (
- {TEMPLATES.sort((a, b) => a.label!.localeCompare(b.label!)) + {allTemplates + .sort((a, b) => a.label!.localeCompare(b.label!)) .filter((template) => template.mode === 'custom') .map((template, i) => { return