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>
This commit is contained in:
kemal.earthandAli Waseem authored and GitHub committed 2026-09-14 17:44:42 +01:00
1 parent b0de9dd7a6
commit 4aa34f556a
8 files changed
+56 -27

No files matched your search

@@ -1,5 +1,5 @@
import type { ReactNode } from 'react'
import { cn, Skeleton } from 'ui'
import { Skeleton } from 'ui'
import { InterstitialLayout, SupabaseLogo } from '@/components/layouts/InterstitialLayout'
@@ -36,19 +36,6 @@ export const InterstitialShellSkeleton = ({ children }: { children: ReactNode })
</InterstitialShell>
)
export const InterstitialFooter = ({
children,
align = 'center',
}: {
children: ReactNode
align?: 'center' | 'start'
}) => (
<p
className={cn(
'text-xs text-foreground-light',
align === 'center' ? 'text-center' : 'text-left'
)}
>
{children}
</p>
export const InterstitialFooter = ({ children }: { children: ReactNode }) => (
<p className="text-center text-xs text-foreground-light">{children}</p>
)
@@ -0,0 +1,32 @@
import { screen } from '@testing-library/react'
import { describe, expect, it } from 'vitest'
import { InterstitialTerminalScreen } from './InterstitialTerminalScreen'
import { customRender } from '@/tests/lib/custom-render'
const baseCopy = {
title: 'Key stored',
subtitle: 'RESEND_API_KEY is saved for billing-staging.',
calloutTitle: 'Next step',
calloutBody: 'Head back to your agent and let it know you finished.',
footer: 'You can close this tab.',
}
describe('InterstitialTerminalScreen', () => {
it('links to the project Edge Functions secrets page when a project ref is given', () => {
customRender(<InterstitialTerminalScreen {...baseCopy} projectRef="abcdefghijklmnopqrst" />)
expect(screen.getByRole('link', { name: 'Go to Edge Functions secrets' })).toHaveAttribute(
'href',
'/project/abcdefghijklmnopqrst/functions/secrets'
)
})
it('omits the Edge Functions secrets link when there is no project to point at', () => {
customRender(<InterstitialTerminalScreen {...baseCopy} />)
expect(
screen.queryByRole('link', { name: 'Go to Edge Functions secrets' })
).not.toBeInTheDocument()
})
})
@@ -1,3 +1,4 @@
import { Button, Separator } from 'ui'
import { Admonition } from 'ui-patterns/Admonition'
import { InterstitialFooter, InterstitialShell } from './InterstitialShell'
@@ -8,6 +9,7 @@ export type InterstitialTerminalCopy = {
calloutTitle: string
calloutBody: string
footer: string
projectRef?: string
}
export const InterstitialTerminalScreen = ({
@@ -16,9 +18,19 @@ export const InterstitialTerminalScreen = ({
calloutTitle,
calloutBody,
footer,
projectRef,
}: InterstitialTerminalCopy) => (
<InterstitialShell title={title} subtitle={subtitle}>
<Admonition type="note" title={calloutTitle} description={calloutBody} className="mb-0" />
{projectRef && (
<Button block variant="default" asChild>
<a href={`/project/${projectRef}/functions/secrets`}>Go to Edge Functions secrets</a>
</Button>
)}
<Separator />
<InterstitialFooter>{footer}</InterstitialFooter>
</InterstitialShell>
)
@@ -2,8 +2,6 @@ export const MCP_SECRETS_ROUTE = '/mcp/secrets'
export const MCP_SECRETS_FLAG = 'McpElicitURLMode'
export const UNKNOWN_CLIENT_LABEL = 'your AI client'
export const SECRETS_TOOL_NAME = 'create_edge_function_secret'
export const MAX_SECRET_NAME_LENGTH = 256
@@ -35,9 +35,7 @@ describe('getSecretsCopy', () => {
it('names the generic client, because v1 never learns which one sent the user', () => {
const copy = getSecretsCopy({ status: 'stored', request, timedOut: false })
expect(copy.calloutBody).toBe(
`Go back to your AI client and choose "I've completed it" to finish the tool call.`
)
expect(copy.calloutBody).toBe(`Head back to your agent and let it know you're finished.`)
})
it('sends the user back to the agent instead of the client on timeout', () => {
@@ -3,7 +3,6 @@ import relativeTime from 'dayjs/plugin/relativeTime'
import utc from 'dayjs/plugin/utc'
import type { InterstitialTerminalCopy } from '../InterstitialTerminalScreen'
import { UNKNOWN_CLIENT_LABEL } from './McpSecrets.constants'
import type {
SecretProviderHint,
SecretRequest,
@@ -14,7 +13,7 @@ import type {
dayjs.extend(relativeTime)
dayjs.extend(utc)
const RETURN_TO_CLIENT_STEP = `Go back to ${UNKNOWN_CLIENT_LABEL} and choose "I've completed it" to finish the tool call.`
const RETURN_TO_CLIENT_STEP = `Head back to your agent and let it know you're finished.`
const CLOSE_TAB_FOOTER = 'You can close this tab.'
@@ -35,6 +34,7 @@ export function getSecretsCopy(state: SecretsOutcomeState): InterstitialTerminal
calloutBody:
'Ask your agent to store the key again. It will find the saved key and finish without sending you back here.',
footer: UNVERIFIED_KEY_FOOTER,
projectRef: state.request.ref,
}
}
@@ -44,6 +44,7 @@ export function getSecretsCopy(state: SecretsOutcomeState): InterstitialTerminal
calloutTitle: 'Next step',
calloutBody: RETURN_TO_CLIENT_STEP,
footer: UNVERIFIED_KEY_FOOTER,
projectRef: state.request.ref,
}
}
@@ -54,6 +55,7 @@ export function getSecretsCopy(state: SecretsOutcomeState): InterstitialTerminal
calloutTitle: 'Next step',
calloutBody: RETURN_TO_CLIENT_STEP,
footer: CLOSE_TAB_FOOTER,
projectRef: state.request.ref,
}
case 'expired':
@@ -18,7 +18,7 @@ import { Admonition } from 'ui-patterns/Admonition'
import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
import z from 'zod'
import { InterstitialShell } from '../InterstitialShell'
import { InterstitialFooter, InterstitialShell } from '../InterstitialShell'
import type { SecretRequest } from './McpSecrets.types'
import {
getOverwriteWarning,
@@ -163,7 +163,7 @@ export const McpSecretsForm = ({
{providerHint && (
<>
<Separator />
<p className="text-xs text-foreground-light">
<InterstitialFooter>
{`Don't have your key? Create one in the `}
{providerHint.dashboardUrl ? (
<a
@@ -178,7 +178,7 @@ export const McpSecretsForm = ({
`${providerHint.name} dashboard`
)}
.
</p>
</InterstitialFooter>
</>
)}
</InterstitialShell>
@@ -27,7 +27,7 @@ export const McpSecretsWrongAccount = ({
<Separator />
<InterstitialFooter align="start">
<InterstitialFooter>
After switching, ask your agent to run the tool again. Nothing has been stored.
</InterstitialFooter>
</InterstitialShell>