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

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## 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.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
Chris ChinchillaandCopilot Autofix powered by AI authored and GitHub committed 2026-07-07 10:04:56 +02:00
1 parent d0794add43
commit 30b02aa0b7
14 files changed
+185 -23

No files matched your search

+4
View File
@@ -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"
+59
View File
@@ -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<T extends CustomContentKey> = ReturnType<
typeof getCustomContent<[T]>
>[keyof ReturnType<typeof getCustomContent<[T]>>]
/**
* 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
* <CustomContent data="metadata:title" />
*
* @example Address a nested field with a path
* <CustomContent data="navigation:logo">light</CustomContent>
*
* @example Use a render function for richer output
* <CustomContent data="navigation:logo">
* {(logo) => <img src={logo?.light} />}
* </CustomContent>
*/
function CustomContent<T extends CustomContentKey>({
data,
children,
}: {
data: T
children?: ((value: ValueFor<T>) => ReactNode) | string
}) {
const result = getCustomContent([data])
const value = Object.values(result)[0] as ValueFor<T>
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 }
+16
View File
@@ -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 <CodeBlock lang="json" contents={JSON.stringify(config, null, 2)} />
}
@@ -0,0 +1,19 @@
/**
* Builds the example CI MCP server configuration. Pure and dependency-free so
* it can be shared between the React `<McpCiConfigBlock>` 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}',
},
},
},
}
}
+3 -15
View File
@@ -112,11 +112,11 @@ The [configuration panel above](#step-2-configure-your-ai-tool) can set these op
| `project_ref=<id>` | Scope to a specific project (disables account tools) | `?project_ref=abc123` |
| `features=<groups>` | 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: <code><CustomContent data="mcp:servers">remote</CustomContent>?project_ref=abc123&read_only=true</code>
<Admonition type="tip">
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 <code><CustomContent data="mcp:servers">local</CustomContent></code>.
</Admonition>
@@ -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}"
}
}
}
}
```
<McpCiConfigBlock />
The above example assumes you have environment variables `SUPABASE_ACCESS_TOKEN` and `SUPABASE_PROJECT_REF` set in your CI environment.
@@ -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) => <Image className="rounded-md w-full" {...props} />,
Link,
McpCiConfigBlock,
McpConfigPanel,
Mermaid,
MetricsStackCards,
+4
View File
@@ -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<McpClient | null>(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}
@@ -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,
@@ -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<string, unknown>
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) : ''
}
@@ -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```'
}
@@ -3,6 +3,10 @@ export type CustomContentTypes = {
metadataApplicationName: string
metadataTitle: string
cliProfile: string
mcpServers: {
local: string
remote: string
}
navigationLogo: {
light: string
dark: string
@@ -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"
}
}
@@ -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<string[]>([])
@@ -63,6 +68,8 @@ export function McpConfigPanel({
projectRef,
isPlatform,
apiUrl,
platformUrl,
nonPlatformUrl,
readonly,
features: selectedFeaturesSupported,
selectedClient,
@@ -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)
}