Files
supabase/apps/studio/components/interfaces/MCP/Secrets/McpSecrets.utils.ts
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

149 lines
4.9 KiB
TypeScript

import dayjs from 'dayjs'
import relativeTime from 'dayjs/plugin/relativeTime'
import utc from 'dayjs/plugin/utc'
import type { InterstitialTerminalCopy } from '../InterstitialTerminalScreen'
import type {
SecretProviderHint,
SecretRequest,
SecretsOutcomeState,
SecretsState,
} from './McpSecrets.types'
dayjs.extend(relativeTime)
dayjs.extend(utc)
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.'
const UNVERIFIED_KEY_FOOTER =
"Supabase doesn't verify keys. You can view or replace this one in Edge Functions secrets."
export function getSecretsCopy(state: SecretsOutcomeState): InterstitialTerminalCopy {
switch (state.status) {
case 'stored': {
const { keyName, project } = state.request
const savedSentence = `${keyName} is saved for ${project}.`
if (state.timedOut) {
return {
title: 'Key stored',
subtitle: `${savedSentence} This took longer than your client waits, so it may have stopped listening.`,
calloutTitle: 'Next step',
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,
}
}
return {
title: 'Key stored',
subtitle: savedSentence,
calloutTitle: 'Next step',
calloutBody: RETURN_TO_CLIENT_STEP,
footer: UNVERIFIED_KEY_FOOTER,
projectRef: state.request.ref,
}
}
case 'already-stored':
return {
title: 'This key is already stored',
subtitle: `${state.request.keyName} was saved for ${state.request.project}. Nothing further to do here.`,
calloutTitle: 'Next step',
calloutBody: RETURN_TO_CLIENT_STEP,
footer: CLOSE_TAB_FOOTER,
projectRef: state.request.ref,
}
case 'expired':
return {
title: 'This link has expired',
subtitle: 'Nothing was stored. Your key is still safe where you copied it from.',
calloutTitle: 'Next step',
calloutBody: "Ask your agent to store your API key again. You'll get a fresh link.",
footer: CLOSE_TAB_FOOTER,
}
case 'cancelled':
return {
title: 'This request was cancelled',
subtitle: 'Nothing was stored.',
calloutTitle: 'Next step',
calloutBody: 'Ask your agent to run the tool again if you still need to store the key.',
footer: CLOSE_TAB_FOOTER,
}
case 'paused':
return {
title: 'Storing keys is paused',
subtitle: 'Supabase has turned this off for now. Nothing was stored.',
calloutTitle: 'Next step',
calloutBody: 'Try again later, or set the key in Edge Functions secrets instead.',
footer: CLOSE_TAB_FOOTER,
}
case 'error':
return {
title: "Couldn't complete this request",
subtitle: 'Nothing was stored.',
calloutTitle: 'Next step',
calloutBody:
'Ask your agent to run the tool again, or set the key in Edge Functions secrets instead.',
footer: CLOSE_TAB_FOOTER,
}
}
}
export function getSecretsAnnouncement(state: SecretsState | undefined): string {
if (state === undefined) return ''
switch (state.status) {
case 'loading':
return 'Loading request details'
case 'form':
return `Ready to save ${state.request.keyName} for ${state.request.project}`
case 'wrong-account':
return 'This account cannot access the request'
default: {
const { title, subtitle } = getSecretsCopy(state)
return `${title}. ${subtitle}`
}
}
}
export function getSecretHelperText(project: string) {
return `Stored encrypted for ${project}. Anyone with write access to this project can use it. Remove it any time from Edge Functions secrets.`
}
export function getOverwriteWarning(request: SecretRequest) {
const { existingSecret, keyName } = request
if (existingSecret === undefined) return undefined
const updatedAt = existingSecret.updatedAt
const age = updatedAt === undefined ? undefined : formatSecretAge(updatedAt)
return age === undefined
? `${keyName} already exists. Storing will replace it.`
: `${keyName} already exists — updated ${age} ago. Storing will replace it.`
}
function formatSecretAge(updatedAt: string) {
const isUnixMicro = !Number.isNaN(Number(updatedAt)) && updatedAt.length === 16
const parsed = isUnixMicro ? dayjs.unix(Number(updatedAt) / 1000 / 1000) : dayjs.utc(updatedAt)
return parsed.isValid() ? parsed.fromNow(true) : undefined
}
export function getSecretPrefixWarning(
value: string,
providerHint: SecretProviderHint | undefined
) {
const prefix = providerHint?.prefix
if (!prefix || value.length === 0 || value.startsWith(prefix)) return undefined
return `${providerHint.name} keys usually start with ${prefix}. You can still save this one.`
}