Files
supabase/apps/studio/components/interfaces/MCP/Secrets/McpSecretsForm.tsx
kemal.earthandAli Waseem 4aa34f556a feat(studio): mcp secrets interstitial polish (#50351)
## I have read the
[CONTRIBUTING.md](https://github.com/supabase/supabase/blob/master/CONTRIBUTING.md)
file.

YES

## What kind of change does this PR introduce?

- We made the Next step copy a bit more generic so it doesn't read like
it's pointing you back to Inspector UI.
- Added CTA on key stored screen to send you to Edge Function Secrets
directly.
- Tidies up footer area to always be centrally aligned across all
states.

### 1. Enable the feature flag

### 2. Preview states via URL

Mock mode is enabled automatically in local/staging. Navigate to
`/mcp/secrets` with a `state` query param:

http://localhost:8082/mcp/secrets?state=<state>

States that need no other params:

| `state` value    | What it shows                          |
| ----------------- | --------------------------------------- |
| `loading`          | Loading skeleton                        |
| `expired`          | Link expired                            |
| `cancelled`        | Request cancelled                       |
| `paused`           | Storing keys paused                     |
| `wrong-account`    | Signed in as the wrong account          |
| `error`            | Generic failure                         |

States that need a real project —ame=<KEY_NAME>`:

| `state` value      | What it sh           |
| -------------------- | ---------------------- |
| `form`                | The "st              |
| `stored`              | Success              |
| `stored-timeout`      | Successopped waiting |
| `already-stored` | Key was already stored, nothing to do |

Example:

http://localhost:8082/mcp/secretsJECT_REF&name=OPENAI_API_KEY

### 3. What to check

- [ ] `stored` / `already-stored`tions secrets"** button linking to
`/project/<ref>/functions/secrets
- [ ] States without a project re `paused`, `error`) don't show that
button
- [ ] Footer text is centered on
- [ ] `wrong-account` → **Switch its footer is centered
- [ ] The provider-dashboard link` state, use a `name`
like`OPENAI_API_KEY` or `RESEND_API_Khint) is centered too


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **New Features**
* Added a project-specific link to Edge Functions secrets from the MCP
setup screen when a project is available.
* Added a separator to distinguish the secrets link from the remaining
setup guidance.

* **Improvements**
* Updated completion guidance to tell users to return to their agent and
confirm the setup is finished.
* Standardized interstitial footer content with centered guidance and
consistent provider dashboard instructions.

* **Tests**
* Added coverage for displaying the project-specific secrets link and
hiding it when no project is associated.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Ali Waseem <waseema393@gmail.com>
2026-09-14 17:44:42 +01:00

187 lines
6.0 KiB
TypeScript

import { zodResolver } from '@hookform/resolvers/zod'
import { Eye, EyeOff, Lock } from 'lucide-react'
import { useState } from 'react'
import { useForm, useWatch } from 'react-hook-form'
import {
Button,
cn,
Form,
FormField,
FormInputGroupInput,
InputGroup,
InputGroupAddon,
InputGroupButton,
InputGroupInput,
Separator,
} from 'ui'
import { Admonition } from 'ui-patterns/Admonition'
import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
import z from 'zod'
import { InterstitialFooter, InterstitialShell } from '../InterstitialShell'
import type { SecretRequest } from './McpSecrets.types'
import {
getOverwriteWarning,
getSecretHelperText,
getSecretPrefixWarning,
} from './McpSecrets.utils'
import { McpSecretsDetails } from './McpSecretsDetails'
const FORM_ID = 'mcp-secrets-form'
const KEY_NAME_FIELD_ID = 'mcp-secrets-key-name'
const FormSchema = z.object({
secret: z.string().min(1, 'Enter the key value'),
})
type FormValues = z.infer<typeof FormSchema>
const defaultValues: FormValues = { secret: '' }
export const McpSecretsForm = ({
request,
isSaving,
onSave,
onCancel,
}: {
request: SecretRequest
isSaving: boolean
onSave: (secret: string) => void
onCancel: () => void
}) => {
const [isRevealed, setIsRevealed] = useState(false)
const form = useForm<FormValues>({ resolver: zodResolver(FormSchema), defaultValues })
const secret = useWatch({ control: form.control, name: 'secret' })
const { providerHint } = request
const prefixWarning = getSecretPrefixWarning(secret, providerHint)
const overwriteWarning = getOverwriteWarning(request)
return (
<InterstitialShell
title="Store an API key"
subtitle="Supabase is asking for this key on behalf of a tool call. It never passes through your AI client."
>
<McpSecretsDetails request={request} />
<Form {...form}>
<form
id={FORM_ID}
onSubmit={form.handleSubmit((values) => onSave(values.secret))}
className="flex flex-col gap-6"
>
<FormItemLayout isReactForm={false} label="Key name" name={KEY_NAME_FIELD_ID}>
<InputGroup>
<InputGroupInput
id={KEY_NAME_FIELD_ID}
name={KEY_NAME_FIELD_ID}
value={request.keyName}
readOnly
aria-readonly
/>
<InputGroupAddon align="inline-end" className="pr-2">
<Lock size={14} className="text-foreground-muted" aria-hidden />
</InputGroupAddon>
</InputGroup>
</FormItemLayout>
<div className="flex flex-col gap-2">
<FormField
control={form.control}
name="secret"
render={({ field }) => (
<FormItemLayout
label="Secret value"
description={getSecretHelperText(request.project)}
>
<InputGroup>
<FormInputGroupInput
{...field}
type={isRevealed ? 'text' : 'password'}
placeholder="Paste your key"
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
spellCheck={false}
disabled={isSaving}
data-1p-ignore
data-lpignore="true"
data-form-type="other"
data-bwignore
/>
<InputGroupAddon
align="inline-end"
className="pr-1 has-[>button]:mr-0 has-[>kbd]:mr-0"
>
<InputGroupButton
size="tiny"
variant="text"
type="button"
className="size-6 shrink-0 p-0"
aria-pressed={isRevealed}
aria-label={isRevealed ? 'Hide secret value' : 'Show secret value'}
icon={isRevealed ? <EyeOff /> : <Eye />}
onClick={() => setIsRevealed(!isRevealed)}
/>
</InputGroupAddon>
</InputGroup>
</FormItemLayout>
)}
/>
<p
role="status"
aria-live="polite"
className={cn('text-xs text-warning-600', !prefixWarning && 'sr-only')}
>
{prefixWarning}
</p>
</div>
<div className="flex flex-col gap-4">
<Admonition
type="default"
description="Only continue if you asked your AI client to store this secret."
className="mb-0"
/>
{overwriteWarning && (
<Admonition type="warning" description={overwriteWarning} className="mb-0" />
)}
</div>
<div className="flex flex-col gap-2">
<Button block variant="primary" type="submit" loading={isSaving} disabled={isSaving}>
{isSaving ? 'Saving...' : 'Save'}
</Button>
<Button block variant="text" type="button" disabled={isSaving} onClick={onCancel}>
Cancel
</Button>
</div>
</form>
</Form>
{providerHint && (
<>
<Separator />
<InterstitialFooter>
{`Don't have your key? Create one in the `}
{providerHint.dashboardUrl ? (
<a
href={providerHint.dashboardUrl}
target="_blank"
rel="noreferrer noopener"
className="text-foreground underline underline-offset-2"
>
{providerHint.name} dashboard
</a>
) : (
`${providerHint.name} dashboard`
)}
.
</InterstitialFooter>
</>
)}
</InterstitialShell>
)
}