Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/CODEOWNERS
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
/packages/shared-data/pricing.ts @roryw10 @supabase/billing
/packages/shared-data/plans.ts @roryw10 @supabase/billing
/packages/common/telemetry-constants.ts @supabase/growth-eng
/packages/pg-meta @supabase/postgres

/apps/studio/ @supabase/Dashboard

Expand Down
51 changes: 51 additions & 0 deletions apps/docs/content/guides/getting-started/mcp.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,57 @@ Choose your Supabase platform, project, and MCP client and follow the installati

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.

## Manual authentication

By default the hosted Supabase MCP server uses [dynamic client registration](https://modelcontextprotocol.io/specification/2025-06-18/basic/authorization#dynamic-client-registration) to authenticate with your Supabase org. This means that you don't need to manually create a personal access token (PAT) or OAuth app to use the server.

There are some situations where you might want to manually authenticate the MCP server instead:

1. You are using Supabase MCP in a CI environment where browser-based OAuth flows are not possible
2. Your MCP client does not support dynamic client registration and instead requires an OAuth client ID and secret

### CI environment

To authenticate the MCP server in a CI environment, you can create a personal access token (PAT) with the necessary scopes and pass it as a header to the MCP server.

1. Remember to never connect the MCP server to production data. Supabase MCP is only designed for development and testing purposes. See [Security risks](#security-risks).

1. Navigate to your Supabase [access tokens](/dashboard/account/tokens) and generate a new token. Name the token based on its purpose, e.g. "Example App MCP CI token".

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.

Note that not every MCP client supports custom headers, so check your client's documentation for details.

### Manual OAuth app

If your MCP client requires an OAuth client ID and secret (e.g. Azure API Center), you can manually create an OAuth app in your Supabase account and pass the credentials to the MCP client.

1. Remember to never connect the MCP server to production data. Supabase MCP is only designed for development and testing purposes. See [Security risks](#security-risks).

1. Navigate to your Supabase organization's [OAuth apps](/dashboard/org/_/apps) and add a new application. Name the app based on its purpose, e.g. "Example App MCP".

Your client should provide you the website URL and callback URL that it expects for the OAuth app. Use these values when creating the OAuth app in Supabase.

Grant write access to all of the available scopes. In the future, the MCP server will support more fine-grained scopes, but for now all scopes are required.

1. After creating the OAuth app, copy the client ID and client secret to your MCP client.

## Security risks

Connecting any data source to an LLM carries inherent risks, especially when it stores sensitive data. Supabase is no exception, so it's important to discuss what risks you should be aware of and extra precautions you can take to lower them.
Expand Down
24 changes: 14 additions & 10 deletions apps/studio/components/grid/components/menu/RowContextMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -75,16 +75,20 @@ export const RowContextMenu = ({ rows }: RowContextMenuProps) => {
<Copy size={12} />
<span className="ml-2 text-xs">Copy row</span>
</Item>
<DialogSectionSeparator className="my-1.5" />
<Item onClick={onEditRowClick} hidden={!snap.editable} data="edit">
<Edit size={12} />
<span className="ml-2 text-xs">Edit row</span>
</Item>
<DialogSectionSeparator className="my-1.5" />
<Item onClick={onDeleteRow} hidden={!snap.editable} data="delete">
<Trash size={12} />
<span className="ml-2 text-xs">Delete row</span>
</Item>
{snap.editable && (
<>
<DialogSectionSeparator className="my-1.5" />
<Item onClick={onEditRowClick} data="edit">
<Edit size={12} />
<span className="ml-2 text-xs">Edit row</span>
</Item>
<DialogSectionSeparator className="my-1.5" />
<Item onClick={onDeleteRow} data="delete">
<Trash size={12} />
<span className="ml-2 text-xs">Delete row</span>
</Item>
</>
)}
</Menu>
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,15 @@ import { X } from 'lucide-react'

import { SheetClose, SheetHeader, SheetTitle, cn } from 'ui'

interface CreateFunctionHeaderProps {
selectedFunction?: string
isDuplicating?: boolean
}

export const CreateFunctionHeader = ({
selectedFunction,
assistantVisible,
setAssistantVisible,
}: {
selectedFunction?: string
assistantVisible: boolean
setAssistantVisible: (v: boolean) => void
}) => {
isDuplicating,
}: CreateFunctionHeaderProps) => {
return (
<SheetHeader className="py-3 flex flex-row justify-between items-center border-b-0">
<div className="flex flex-row gap-3 items-center max-w-[75%]">
Expand All @@ -27,33 +27,12 @@ export const CreateFunctionHeader = ({
</SheetClose>
<SheetTitle className="truncate">
{selectedFunction !== undefined
? `Edit '${selectedFunction}' function`
? isDuplicating
? `Duplicate function`
: `Edit '${selectedFunction}' function`
: 'Add a new function'}
</SheetTitle>
</div>
{/* <Tooltip>
<TooltipTrigger asChild>
<button
aria-expanded={assistantVisible}
aria-controls="ai-chat-assistant"
className={cn(
!assistantVisible ? 'text-foreground-lighter' : 'text-light',
'hover:text-foreground',
'transition'
)}
onClick={() => setAssistantVisible(!assistantVisible)}
>
{!assistantVisible ? (
<PanelLeftClose size={19} strokeWidth={1} />
) : (
<PanelRightClose size={19} strokeWidth={1} />
)}
</button>
</TooltipTrigger>
<TooltipContent side="left">
{assistantVisible ? 'Hide' : 'Show'} tools
</TooltipContent>
</Tooltip> */}
</SheetHeader>
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,9 @@ const FORM_ID = 'create-function-sidepanel'

interface CreateFunctionProps {
func?: DatabaseFunction
isDuplicating?: boolean
visible: boolean
setVisible: (value: boolean) => void
onClose: () => void
}

const FormSchema = z.object({
Expand All @@ -68,15 +69,13 @@ const FormSchema = z.object({
.optional(),
})

const CreateFunction = ({ func, visible, setVisible }: CreateFunctionProps) => {
const CreateFunction = ({ func, visible, isDuplicating = false, onClose }: CreateFunctionProps) => {
const { data: project } = useSelectedProjectQuery()
const [isClosingPanel, setIsClosingPanel] = useState(false)
const [advancedSettingsShown, setAdvancedSettingsShown] = useState(false)
// For now, there's no AI assistant for functions
const [assistantVisible, setAssistantVisible] = useState(false)
const [focusedEditor, setFocusedEditor] = useState(false)

const isEditing = !!func?.id
const isEditing = !isDuplicating && !!func?.id

const form = useForm<z.infer<typeof FormSchema>>({
resolver: zodResolver(FormSchema),
Expand All @@ -89,7 +88,7 @@ const CreateFunction = ({ func, visible, setVisible }: CreateFunctionProps) => {
useDatabaseFunctionUpdateMutation()

function isClosingSidePanel() {
form.formState.isDirty ? setIsClosingPanel(true) : setVisible(!visible)
form.formState.isDirty ? setIsClosingPanel(true) : onClose()
}

const onSubmit: SubmitHandler<z.infer<typeof FormSchema>> = async (data) => {
Expand All @@ -111,7 +110,7 @@ const CreateFunction = ({ func, visible, setVisible }: CreateFunctionProps) => {
{
onSuccess: () => {
toast.success(`Successfully updated function ${data.name}`)
setVisible(!visible)
onClose()
},
}
)
Expand All @@ -125,7 +124,7 @@ const CreateFunction = ({ func, visible, setVisible }: CreateFunctionProps) => {
{
onSuccess: () => {
toast.success(`Successfully created function ${data.name}`)
setVisible(!visible)
onClose()
},
}
)
Expand Down Expand Up @@ -155,19 +154,11 @@ const CreateFunction = ({ func, visible, setVisible }: CreateFunctionProps) => {
<Sheet open={visible} onOpenChange={() => isClosingSidePanel()}>
<SheetContent
showClose={false}
size={assistantVisible ? 'lg' : 'default'}
className={cn(
// 'bg-surface-200',
'p-0 flex flex-row gap-0',
assistantVisible ? '!min-w-screen lg:!min-w-[1200px]' : '!min-w-screen lg:!min-w-[600px]'
)}
size={'default'}
className={'p-0 flex flex-row gap-0 !min-w-screen lg:!min-w-[600px]'}
>
<div className={cn('flex flex-col grow w-full', assistantVisible && 'w-[60%]')}>
<CreateFunctionHeader
selectedFunction={func?.name}
assistantVisible={assistantVisible}
setAssistantVisible={setAssistantVisible}
/>
<div className="flex flex-col grow w-full">
<CreateFunctionHeader selectedFunction={func?.name} isDuplicating={isDuplicating} />
<Separator />
<Form_Shadcn_ {...form}>
<form
Expand Down Expand Up @@ -405,19 +396,14 @@ const CreateFunction = ({ func, visible, setVisible }: CreateFunctionProps) => {
</Button>
</SheetFooter>
</div>
{assistantVisible ? (
<div className="border-l shadow-[rgba(0,0,0,0.13)_-4px_0px_6px_0px] z-10 w-[50%] bg-studio">
{/* This is where the AI assistant would be added */}
</div>
) : null}
<ConfirmationModal
visible={isClosingPanel}
title="Discard changes"
confirmLabel="Discard"
onCancel={() => setIsClosingPanel(false)}
onConfirm={() => {
setIsClosingPanel(false)
setVisible(!visible)
onClose()
}}
>
<p className="text-sm text-foreground-light">
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { PermissionAction } from '@supabase/shared-types/out/constants'
import { includes, noop, sortBy } from 'lodash'
import { Edit, Edit2, FileText, MoreVertical, Trash } from 'lucide-react'
import { Copy, Edit, Edit2, FileText, MoreVertical, Trash } from 'lucide-react'
import { useRouter } from 'next/router'

import { ButtonTooltip } from 'components/ui/ButtonTooltip'
Expand All @@ -23,6 +23,7 @@ interface FunctionListProps {
schema: string
filterString: string
isLocked: boolean
duplicateFunction: (fn: any) => void
editFunction: (fn: any) => void
deleteFunction: (fn: any) => void
}
Expand All @@ -31,6 +32,7 @@ const FunctionList = ({
schema,
filterString,
isLocked,
duplicateFunction = noop,
editFunction = noop,
deleteFunction = noop,
}: FunctionListProps) => {
Expand Down Expand Up @@ -132,6 +134,7 @@ const FunctionList = ({
<p>Client API docs</p>
</DropdownMenuItem>
)}
<DropdownMenuSeparator />
<DropdownMenuItem className="space-x-2" onClick={() => editFunction(x)}>
<Edit2 size={14} />
<p>Edit function</p>
Expand Down Expand Up @@ -169,6 +172,13 @@ const FunctionList = ({
<Edit size={14} />
<p>Edit function with Assistant</p>
</DropdownMenuItem>
<DropdownMenuItem
className="space-x-2"
onClick={() => duplicateFunction(x)}
>
<Copy size={14} />
<p>Duplicate function</p>
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem className="space-x-2" onClick={() => deleteFunction(x)}>
<Trash size={14} className="text-destructive" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import FunctionList from './FunctionList'

interface FunctionsListProps {
createFunction: () => void
duplicateFunction: (fn: PostgresFunction) => void
editFunction: (fn: PostgresFunction) => void
deleteFunction: (fn: PostgresFunction) => void
}
Expand All @@ -40,6 +41,7 @@ const FunctionsList = ({
createFunction = noop,
editFunction = noop,
deleteFunction = noop,
duplicateFunction = noop,
}: FunctionsListProps) => {
const router = useRouter()
const { search } = useParams()
Expand Down Expand Up @@ -199,6 +201,7 @@ const FunctionsList = ({
schema={selectedSchema}
filterString={filterString}
isLocked={isSchemaLocked}
duplicateFunction={duplicateFunction}
editFunction={editFunction}
deleteFunction={deleteFunction}
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -192,14 +192,14 @@ export const EdgeFunctionTesterSheet = ({ visible, onClose }: EdgeFunctionTester

// Construct custom headers
const customHeaders: Record<string, string> = {}
headerFields.forEach(({ key, value }) => {
values.headers.forEach(({ key, value }) => {
if (key && value) {
customHeaders[key] = value
}
})

// Construct query parameters
const queryString = queryParamFields
const queryString = values.queryParams
.filter(({ key, value }) => key && value)
.map(({ key, value }) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`)
.join('&')
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,15 @@ import {
} from 'ui'
import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
import type { SupportFormValues } from './SupportForm.schema'
import { DASHBOARD_LOG_CATEGORIES, getSanitizedBreadcrumbs } from './dashboard-logs'
import { DASHBOARD_LOG_CATEGORIES } from './dashboard-logs'

interface DashboardLogsToggleProps {
form: UseFormReturn<SupportFormValues>
sanitizedLog: unknown[]
}

export function DashboardLogsToggle({ form }: DashboardLogsToggleProps) {
const sanitizedLogJson = useMemo(() => JSON.stringify(getSanitizedBreadcrumbs(), null, 2), [])
export function DashboardLogsToggle({ form, sanitizedLog }: DashboardLogsToggleProps) {
const sanitizedLogJson = useMemo(() => JSON.stringify(sanitizedLog, null, 2), [sanitizedLog])

const [isPreviewOpen, setIsPreviewOpen] = useState(false)

Expand Down
Loading
Loading