mirror of
https://github.com/supabase/supabase.git
synced 2026-10-05 09:25:06 +03:00
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:
1 parent
d0794add43
commit
30b02aa0b7
14 files changed
+185
-23
No files matched your search
@@ -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"
|
||||
|
||||
@@ -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 }
|
||||
@@ -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}',
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
Reference in new issue
Block a user