From 30b02aa0b7e380d6bfb84890c9b53acbd082dbc7 Mon Sep 17 00:00:00 2001 From: Chris Chinchilla Date: Tue, 7 Jul 2026 10:04:56 +0200 Subject: [PATCH] docs: Allow for custom MCP server URLs (#47218) ## I have read the [CONTRIBUTING.md](https://github.com/supabase/supabase/blob/master/CONTRIBUTING.md) file. YES ## Summary by CodeRabbit * **New Features** * Added new public MCP base URL environment variables for hosted and self-hosted setups. * Introduced reusable MDX components to render custom MCP configuration content. * **Documentation** * Updated the MCP guide to reference shared MCP server template values for examples. * Swapped the CI configuration example for a component-rendered snippet for consistency. * **Bug Fixes** * Improved self-hosted MCP base URL fallback so it prefers the new non-platform URL when no custom API URL is provided. --------- Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- apps/docs/.env.development | 4 ++ apps/docs/components/CustomContent.tsx | 59 +++++++++++++++++++ apps/docs/components/McpCiConfigBlock.tsx | 16 +++++ .../docs/components/McpCiConfigBlock.utils.ts | 19 ++++++ apps/docs/content/guides/ai-tools/mcp.mdx | 18 +----- apps/docs/features/docs/MdxBase.shared.tsx | 4 ++ apps/docs/features/ui/McpConfigPanel.tsx | 4 ++ .../internals/generate-guides-markdown.ts | 6 +- .../markdown-schema/CustomContent.ts | 24 ++++++++ .../markdown-schema/McpCiConfigBlock.ts | 9 +++ .../lib/custom-content/CustomContent.types.ts | 4 ++ .../lib/custom-content/custom-content.json | 4 ++ .../src/McpUrlBuilder/McpConfigPanel.tsx | 9 ++- .../src/McpUrlBuilder/utils/getMcpUrl.ts | 28 +++++++-- 14 files changed, 185 insertions(+), 23 deletions(-) create mode 100644 apps/docs/components/CustomContent.tsx create mode 100644 apps/docs/components/McpCiConfigBlock.tsx create mode 100644 apps/docs/components/McpCiConfigBlock.utils.ts create mode 100644 apps/docs/internals/markdown-schema/CustomContent.ts create mode 100644 apps/docs/internals/markdown-schema/McpCiConfigBlock.ts diff --git a/apps/docs/.env.development b/apps/docs/.env.development index f5e09eed460..56120419134 100644 --- a/apps/docs/.env.development +++ b/apps/docs/.env.development @@ -9,6 +9,10 @@ NEXT_PUBLIC_BASE_PATH="/docs" # Setting this to true requires certain secret keys NEXT_PUBLIC_IS_PLATFORM="false" +# Base URL for the hosted MCP server. Overrides the DEFAULT_MCP_URL_PLATFORM +# fallback in @ui-patterns/McpUrlBuilder. +NEXT_PUBLIC_MCP_URL="http://localhost:8080/mcp" + # Supabase project containing integration information NEXT_PUBLIC_MISC_URL="https://obuldanrptloktxcffvn.supabase.co" NEXT_PUBLIC_MISC_ANON_KEY="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Im9idWxkYW5ycHRsb2t0eGNmZnZuIiwicm9sZSI6ImFub24iLCJpYXQiOjE3MTg2MTQ2ODUsImV4cCI6MjAzNDE5MDY4NX0.NFt49g6DFkc1X5khCzN5p01iAVo2TMxlx88cY1V0E2M" diff --git a/apps/docs/components/CustomContent.tsx b/apps/docs/components/CustomContent.tsx new file mode 100644 index 00000000000..ed6c727b293 --- /dev/null +++ b/apps/docs/components/CustomContent.tsx @@ -0,0 +1,59 @@ +import { + getCustomContent, + type CustomContent as CustomContentKey, +} from '~/lib/custom-content/getCustomContent' +import type { ReactNode } from 'react' + +import { resolveSharedDataPath } from './SharedData.utils' + +type ValueFor = ReturnType< + typeof getCustomContent<[T]> +>[keyof ReturnType>] + +/** + * A wrapper component to access values from `custom-content.json` within MDX + * files. Mirrors the `getCustomContent` helper used in TSX code, and follows + * the same `data`/path-or-render-function pattern as `SharedData`. + * + * @param data - The `custom-content.json` key to read, e.g. `navigation:logo`. + * @param children - How to access the selected value. If it is a render + * function, it takes the value as a param. If it is a + * string, it takes a path through the value, formatted like + * `a[0].b.c`. If omitted, the value itself is rendered. + * + * @example Render a value inline + * + * + * @example Address a nested field with a path + * light + * + * @example Use a render function for richer output + * + * {(logo) => } + * + */ +function CustomContent({ + data, + children, +}: { + data: T + children?: ((value: ValueFor) => ReactNode) | string +}) { + const result = getCustomContent([data]) + const value = Object.values(result)[0] as ValueFor + + if (typeof children === 'function') { + return children(value) + } + if (typeof children === 'string') { + return resolveSharedDataPath(value, children) as ReactNode + } + + if (value != null && typeof value === 'object') { + return JSON.stringify(value) as unknown as ReactNode + } + + return (value ?? null) as ReactNode +} + +export { CustomContent } diff --git a/apps/docs/components/McpCiConfigBlock.tsx b/apps/docs/components/McpCiConfigBlock.tsx new file mode 100644 index 00000000000..81e4c1d730d --- /dev/null +++ b/apps/docs/components/McpCiConfigBlock.tsx @@ -0,0 +1,16 @@ +import { CodeBlock } from '~/features/ui/CodeBlock/CodeBlock' +import { getCustomContent } from '~/lib/custom-content/getCustomContent' + +import { buildMcpCiConfig } from './McpCiConfigBlock.utils' + +/** + * Renders the example CI MCP server configuration with the remote MCP server + * URL pulled from `custom-content.json` (`mcp:servers`). Fenced code blocks in + * MDX render verbatim, so a dynamic value has to be injected via a component. + */ +export function McpCiConfigBlock() { + const { mcpServers } = getCustomContent(['mcp:servers']) + const config = buildMcpCiConfig(mcpServers?.remote) + + return +} diff --git a/apps/docs/components/McpCiConfigBlock.utils.ts b/apps/docs/components/McpCiConfigBlock.utils.ts new file mode 100644 index 00000000000..d13c7274d6d --- /dev/null +++ b/apps/docs/components/McpCiConfigBlock.utils.ts @@ -0,0 +1,19 @@ +/** + * Builds the example CI MCP server configuration. Pure and dependency-free so + * it can be shared between the React `` component (Next.js + * bundle) and the build-time markdown-schema handler without pulling in + * `CodeBlock`'s heavier dependencies (Shiki, Twoslash) into the build script. + */ +export function buildMcpCiConfig(remoteUrl = 'https://mcp.supabase.com/mcp') { + return { + mcpServers: { + supabase: { + type: 'http', + url: `${remoteUrl}?project_ref=\${SUPABASE_PROJECT_REF}`, + headers: { + Authorization: 'Bearer ${SUPABASE_ACCESS_TOKEN}', + }, + }, + }, + } +} diff --git a/apps/docs/content/guides/ai-tools/mcp.mdx b/apps/docs/content/guides/ai-tools/mcp.mdx index 33126fc0707..6dbc5d37bfb 100644 --- a/apps/docs/content/guides/ai-tools/mcp.mdx +++ b/apps/docs/content/guides/ai-tools/mcp.mdx @@ -112,11 +112,11 @@ The [configuration panel above](#step-2-configure-your-ai-tool) can set these op | `project_ref=` | Scope to a specific project (disables account tools) | `?project_ref=abc123` | | `features=` | Enable only specific tool groups (comma-separated) | `?features=database,docs` | -Parameters can be combined: `https://mcp.supabase.com/mcp?project_ref=abc123&read_only=true` +Parameters can be combined: remote?project_ref=abc123&read_only=true -When using [Supabase CLI](/docs/guides/cli) for local development, the MCP server is available at `http://localhost:54321/mcp`. +When using [Supabase CLI](/docs/guides/cli) for local development, the MCP server is available at local. @@ -139,19 +139,7 @@ To authenticate the MCP server in a CI environment, you can create a personal ac 1. Pass the token to the `Authorization` header in your MCP server configuration. For example if you are using [Claude Code](https://docs.claude.com/en/docs/claude-code/github-actions), your MCP server configuration might look like this: - ```json - { - "mcpServers": { - "supabase": { - "type": "http", - "url": "https://mcp.supabase.com/mcp?project_ref=${SUPABASE_PROJECT_REF}", - "headers": { - "Authorization": "Bearer ${SUPABASE_ACCESS_TOKEN}" - } - } - } - } - ``` + The above example assumes you have environment variables `SUPABASE_ACCESS_TOKEN` and `SUPABASE_PROJECT_REF` set in your CI environment. diff --git a/apps/docs/features/docs/MdxBase.shared.tsx b/apps/docs/features/docs/MdxBase.shared.tsx index 9df8413a927..7bd5a9d1b12 100644 --- a/apps/docs/features/docs/MdxBase.shared.tsx +++ b/apps/docs/features/docs/MdxBase.shared.tsx @@ -5,9 +5,11 @@ import AuthProviders from '~/components/AuthProviders' import { AuthSmsProviderConfig } from '~/components/AuthSmsProviderConfig' import ButtonCard from '~/components/ButtonCard' import { ComputeDiskLimitsTable } from '~/components/ComputeDiskLimitsTable' +import { CustomContent } from '~/components/CustomContent' import { ContentListings } from '~/components/ContentListings' import { Extensions } from '~/components/Extensions' import Image, { type ImageProps } from '~/components/Image' +import { McpCiConfigBlock } from '~/components/McpCiConfigBlock' import { Mermaid } from '~/components/Mermaid' import { MetricsStackCards } from '~/components/MetricsStackCards' import { NavData } from '~/components/NavData' @@ -76,6 +78,7 @@ const components = { CodeSampleDummy, CodeSampleWrapper, ComputeDiskLimitsTable, + CustomContent, ContentListings, ErrorCodes, Extensions, @@ -86,6 +89,7 @@ const components = { IconX: X, Image: (props: ImageProps) => , Link, + McpCiConfigBlock, McpConfigPanel, Mermaid, MetricsStackCards, diff --git a/apps/docs/features/ui/McpConfigPanel.tsx b/apps/docs/features/ui/McpConfigPanel.tsx index a02e50443c0..b4de568600e 100644 --- a/apps/docs/features/ui/McpConfigPanel.tsx +++ b/apps/docs/features/ui/McpConfigPanel.tsx @@ -2,6 +2,7 @@ import { useDebounce } from '~/hooks/useDebounce' import { useIntersectionObserver } from '~/hooks/useIntersectionObserver' +import { getCustomContent } from '~/lib/custom-content/getCustomContent' import { useProjectsInfiniteQuery } from '~/lib/fetch/projects-infinite' import { useSendTelemetryEvent } from '~/lib/telemetry' import { useIsLoggedIn, useIsUserLoading } from 'common' @@ -268,6 +269,7 @@ export function McpConfigPanel() { const [selectedClient, setSelectedClient] = useState(null) const { resolvedTheme } = useTheme() const sendTelemetryEvent = useSendTelemetryEvent() + const { mcpServers } = getCustomContent(['mcp:servers']) const isPlatform = selectedPlatform === 'hosted' const project = isPlatform ? selectedProject : null @@ -326,6 +328,8 @@ export function McpConfigPanel() { projectRef={project?.ref} theme={resolvedTheme as 'light' | 'dark'} isPlatform={isPlatform} + platformUrl={mcpServers?.remote} + nonPlatformUrl={mcpServers?.local} onCopyCallback={handleCopy} onInstallCallback={handleInstall} onClientSelect={setSelectedClient} diff --git a/apps/docs/internals/generate-guides-markdown.ts b/apps/docs/internals/generate-guides-markdown.ts index 41944582ca4..be6a75bf2b6 100644 --- a/apps/docs/internals/generate-guides-markdown.ts +++ b/apps/docs/internals/generate-guides-markdown.ts @@ -16,9 +16,11 @@ import { addBaseUrlPrefix } from './internal-links' import { Admonition } from './markdown-schema/Admonition' import { AuthProviders } from './markdown-schema/AuthProviders' import { ComputeDiskLimitsTable } from './markdown-schema/ComputeDiskLimitsTable' +import { ContentListings } from './markdown-schema/ContentListings' +import { CustomContent } from './markdown-schema/CustomContent' import { ErrorCodes } from './markdown-schema/ErrorCodes' import { Link } from './markdown-schema/Link' -import { ContentListings } from './markdown-schema/ContentListings' +import { McpCiConfigBlock } from './markdown-schema/McpCiConfigBlock' import { MetricsStackCards } from './markdown-schema/MetricsStackCards' import { NavData } from './markdown-schema/NavData' import { Panel } from './markdown-schema/Panel' @@ -152,8 +154,10 @@ const SCHEMA: ComponentSchema = { Admonition, AuthProviders, ComputeDiskLimitsTable, + CustomContent, ErrorCodes, Link, + McpCiConfigBlock, McpConfigPanel, Price, GlassPanel: Panel, diff --git a/apps/docs/internals/markdown-schema/CustomContent.ts b/apps/docs/internals/markdown-schema/CustomContent.ts new file mode 100644 index 00000000000..d710384f772 --- /dev/null +++ b/apps/docs/internals/markdown-schema/CustomContent.ts @@ -0,0 +1,24 @@ +import { resolveSharedDataPath } from '~/components/SharedData.utils' +import { + getCustomContent, + type CustomContent as CustomContentKey, +} from '~/lib/custom-content/getCustomContent' + +export const CustomContent = ({ + props, + children, +}: { + props: Record + children: string +}): string => { + const key = String(props.data ?? '') as CustomContentKey + const result = getCustomContent([key]) + const value = Object.values(result)[0] + if (value == null) return '' + + const path = children.trim() + if (!path) return typeof value === 'string' ? value : JSON.stringify(value) + + const resolved = resolveSharedDataPath(value, path) + return resolved != null ? String(resolved) : '' +} diff --git a/apps/docs/internals/markdown-schema/McpCiConfigBlock.ts b/apps/docs/internals/markdown-schema/McpCiConfigBlock.ts new file mode 100644 index 00000000000..4d969af3bb2 --- /dev/null +++ b/apps/docs/internals/markdown-schema/McpCiConfigBlock.ts @@ -0,0 +1,9 @@ +import { buildMcpCiConfig } from '~/components/McpCiConfigBlock.utils' +import { getCustomContent } from '~/lib/custom-content/getCustomContent' + +export const McpCiConfigBlock = (): string => { + const { mcpServers } = getCustomContent(['mcp:servers']) + const config = buildMcpCiConfig(mcpServers?.remote) + + return '```json\n' + JSON.stringify(config, null, 2) + '\n```' +} diff --git a/apps/docs/lib/custom-content/CustomContent.types.ts b/apps/docs/lib/custom-content/CustomContent.types.ts index a936dfac862..455e38578ed 100644 --- a/apps/docs/lib/custom-content/CustomContent.types.ts +++ b/apps/docs/lib/custom-content/CustomContent.types.ts @@ -3,6 +3,10 @@ export type CustomContentTypes = { metadataApplicationName: string metadataTitle: string cliProfile: string + mcpServers: { + local: string + remote: string + } navigationLogo: { light: string dark: string diff --git a/apps/docs/lib/custom-content/custom-content.json b/apps/docs/lib/custom-content/custom-content.json index 54504e90117..2849811e6c1 100644 --- a/apps/docs/lib/custom-content/custom-content.json +++ b/apps/docs/lib/custom-content/custom-content.json @@ -5,5 +5,9 @@ "navigation:logo": { "light": "/docs/supabase-light.svg", "dark": "/docs/supabase-dark.svg" + }, + "mcp:servers": { + "local": "http://localhost:54321/mcp", + "remote": "https://mcp.supabase.com/mcp" } } diff --git a/packages/ui-patterns/src/McpUrlBuilder/McpConfigPanel.tsx b/packages/ui-patterns/src/McpUrlBuilder/McpConfigPanel.tsx index 1136ea1936e..31893e1ee57 100644 --- a/packages/ui-patterns/src/McpUrlBuilder/McpConfigPanel.tsx +++ b/packages/ui-patterns/src/McpUrlBuilder/McpConfigPanel.tsx @@ -25,7 +25,6 @@ const CLIENT_GROUPS = MCP_CLIENT_GROUPS.map((group) => ({ })) export interface McpConfigPanelProps { - baseUrl?: string projectRef?: string initialSelectedClient?: McpClient onClientSelect?: (client: McpClient) => void @@ -35,6 +34,10 @@ export interface McpConfigPanelProps { className?: string isPlatform: boolean // For docs this is controlled by state, for studio by environment variable apiUrl?: string + /** Overrides the NEXT_PUBLIC_MCP_URL/DEFAULT_MCP_URL_PLATFORM fallback for the hosted MCP server */ + platformUrl?: string + /** Overrides the DEFAULT_MCP_URL_NON_PLATFORM fallback for the self-hosted MCP server (used when apiUrl is unset) */ + nonPlatformUrl?: string } export function McpConfigPanel({ @@ -47,6 +50,8 @@ export function McpConfigPanel({ theme = 'dark', isPlatform, apiUrl, + platformUrl, + nonPlatformUrl, }: McpConfigPanelProps) { const [readonly, setReadonly] = useState(false) const [selectedFeatures, setSelectedFeatures] = useState([]) @@ -63,6 +68,8 @@ export function McpConfigPanel({ projectRef, isPlatform, apiUrl, + platformUrl, + nonPlatformUrl, readonly, features: selectedFeaturesSupported, selectedClient, diff --git a/packages/ui-patterns/src/McpUrlBuilder/utils/getMcpUrl.ts b/packages/ui-patterns/src/McpUrlBuilder/utils/getMcpUrl.ts index 6a4a09e9d86..de25cca6723 100644 --- a/packages/ui-patterns/src/McpUrlBuilder/utils/getMcpUrl.ts +++ b/packages/ui-patterns/src/McpUrlBuilder/utils/getMcpUrl.ts @@ -21,6 +21,10 @@ interface GetMcpUrlOptions { selectedClient?: McpClient isPlatform: boolean apiUrl?: string + /** Overrides the NEXT_PUBLIC_MCP_URL/DEFAULT_MCP_URL_PLATFORM fallback for the hosted MCP server */ + platformUrl?: string + /** Overrides the DEFAULT_MCP_URL_NON_PLATFORM fallback for the self-hosted MCP server (used when apiUrl is unset) */ + nonPlatformUrl?: string } interface GetMcpUrlReturn { @@ -32,12 +36,14 @@ export function getMcpUrl({ projectRef, isPlatform, apiUrl, + platformUrl, + nonPlatformUrl, readonly = false, features = [], selectedClient, }: GetMcpUrlOptions): GetMcpUrlReturn { // Generate the MCP URL based on current configuration - const url = new URL(getMcpUrlBase({ isPlatform, apiUrl })) + const url = new URL(getMcpUrlBase({ isPlatform, apiUrl, platformUrl, nonPlatformUrl })) if (projectRef && isPlatform) { url.searchParams.set('project_ref', projectRef) } @@ -58,12 +64,22 @@ export function getMcpUrl({ /** * Assembles base `/mcp` endpoint URL for the given environment */ -function getMcpUrlBase({ isPlatform, apiUrl }: { isPlatform: boolean; apiUrl?: string }) { - // Hosted platform uses environment variable with fallback +function getMcpUrlBase({ + isPlatform, + apiUrl, + platformUrl, + nonPlatformUrl, +}: { + isPlatform: boolean + apiUrl?: string + platformUrl?: string + nonPlatformUrl?: string +}) { + // Hosted platform uses an explicit override, then environment variable, with fallback if (isPlatform) { - return process.env.NEXT_PUBLIC_MCP_URL ?? DEFAULT_MCP_URL_PLATFORM + return platformUrl ?? 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 + // Self-hosted uses API URL, then an explicit override, with fallback + return apiUrl ? `${apiUrl}/mcp` : (nonPlatformUrl ?? DEFAULT_MCP_URL_NON_PLATFORM) }