@@ -300,74 +228,93 @@ export function ViewTokenSheet({ visible, tokenId, onClose }: ViewTokenSheetProp
description="A token only works with permissions you currently hold. Permissions marked below will be denied until your role includes them."
/>
)}
+
- {hasCapabilities && (
- <>
-
-
Management API endpoints enabled
- {capabilityGroups.length === 0 ? (
-
- No Management API endpoints are enabled by the selected capabilities.
-
- ) : (
- capabilityGroups.map(({ entry, mode, endpoints }) => (
-
-
- {entry.name}
-
- {PERMISSION_MODE_LABEL[mode]}
-
-
-
- {endpoints.map(([method, path]) => (
-
-
- {method}
-
- {path}
-
- ))}
-
-
- ))
- )}
+
+
Token summary
+
+
+
- Created
+ -
+ {token.created_at ? (
+
+ ) : (
+ Unknown
+ )}
+
-
-
-
MCP tools
- {mcpTools.length === 0 ? (
-
- No MCP tools are enabled by the selected capabilities.
-
- ) : (
-
- {mcpTools.map((tool) => (
-
- {tool}
-
- ))}
-
+
+
- Last used
+ -
+ {token.last_used_at ? (
+
+ ) : (
+ Never
+ )}
+
+
+
+
- Expires
+ -
+ {token.expires_at ? (
+
+ ) : (
+ Never
+ )}
+
+
+
+
- Resource access
+
-
+
+
+
+
- >
- )}
+
+
+
+
+
+
Capabilities
+ {capabilityTier === 'dense' && (
+
+ )}
+
+
+
>
)}
diff --git a/apps/studio/components/interfaces/Account/AccessTokens/hooks/useCapabilitySummary.ts b/apps/studio/components/interfaces/Account/AccessTokens/hooks/useCapabilitySummary.ts
index 76b78959d4a75..891a56cc2ef22 100644
--- a/apps/studio/components/interfaces/Account/AccessTokens/hooks/useCapabilitySummary.ts
+++ b/apps/studio/components/interfaces/Account/AccessTokens/hooks/useCapabilitySummary.ts
@@ -2,15 +2,16 @@ import { useMemo } from 'react'
import {
getEntryScopes,
- PERMISSION_CATALOG_BY_CATEGORY,
+ PERMISSION_CATALOG,
type PermissionCatalogEntry,
type PermissionMode,
type PermissionSelection,
} from '../AccessToken.permissions'
import {
getEnabledEndpointsForCapability,
- getEnabledMcpTools,
- PermissionScopeMap,
+ getEnabledMcpToolsForCapability,
+ type EnabledEndpoint,
+ type PermissionScopeMap,
} from '@/data/scoped-access-tokens/permission-scope-map-query'
interface UseCapabilitySummaryArgs {
@@ -19,49 +20,43 @@ interface UseCapabilitySummaryArgs {
permissionScopeMap: PermissionScopeMap | undefined
}
+export interface CapabilitySummaryEntry {
+ entry: PermissionCatalogEntry
+ mode: PermissionMode
+ endpoints: EnabledEndpoint[]
+ mcpTools: string[]
+}
+
/**
- * Selection-derived summary data for the token view sheet: selected entries grouped by catalog
- * category, the Management API endpoints each capability enables, and the enabled MCP tools.
+ * Selection-derived summary data for the token view sheet: every granted catalog entry paired with
+ * the Management API endpoints and MCP tools it enables.
*/
export const useCapabilitySummary = ({
selection,
grantedScopes,
permissionScopeMap,
}: UseCapabilitySummaryArgs) => {
- const activeByCategory = useMemo(
- () =>
- PERMISSION_CATALOG_BY_CATEGORY.map((category) => ({
- ...category,
- entries: category.entries
- .map((entry) => ({ entry, mode: selection[entry.key] ?? 'none' }))
- .filter(({ mode }) => mode !== 'none'),
- })).filter((category) => category.entries.length > 0),
- [selection]
- )
-
- const mcpTools = useMemo(
- () => getEnabledMcpTools({ grantedScopes, permissionScopeMap }),
- [grantedScopes, permissionScopeMap]
- )
+ const capabilities = useMemo(() => {
+ const result: CapabilitySummaryEntry[] = []
+ for (const entry of PERMISSION_CATALOG) {
+ const mode = selection[entry.key] ?? 'none'
+ if (mode === 'none') continue
- const capabilityGroups = useMemo(() => {
- const groups: { entry: PermissionCatalogEntry; mode: PermissionMode; endpoints: string[][] }[] =
- []
- for (const category of activeByCategory) {
- for (const { entry, mode } of category.entries) {
- const capabilityScopes = getEntryScopes(entry, mode)
- const endpoints = getEnabledEndpointsForCapability({
- capabilityScopes,
- allGrantedScopes: grantedScopes,
- permissionScopeMap,
- })
- if (endpoints.length > 0) {
- groups.push({ entry, mode, endpoints: endpoints.map((e) => [e.method, e.path]) })
- }
- }
+ const capabilityScopes = getEntryScopes(entry, mode)
+ const endpoints = getEnabledEndpointsForCapability({
+ capabilityScopes,
+ allGrantedScopes: grantedScopes,
+ permissionScopeMap,
+ })
+ const mcpTools = getEnabledMcpToolsForCapability({
+ capabilityScopes,
+ allGrantedScopes: grantedScopes,
+ permissionScopeMap,
+ })
+ result.push({ entry, mode, endpoints, mcpTools })
}
- return groups
- }, [activeByCategory, grantedScopes, permissionScopeMap])
+ return result
+ }, [selection, grantedScopes, permissionScopeMap])
- return { activeByCategory, mcpTools, capabilityGroups }
+ return { capabilities }
}
diff --git a/apps/studio/data/scoped-access-tokens/permission-scope-map-query.ts b/apps/studio/data/scoped-access-tokens/permission-scope-map-query.ts
index 604cc8d68ebf3..c662c8f3667f3 100644
--- a/apps/studio/data/scoped-access-tokens/permission-scope-map-query.ts
+++ b/apps/studio/data/scoped-access-tokens/permission-scope-map-query.ts
@@ -154,6 +154,34 @@ export const getEnabledEndpointsForCapability = ({
.map(([raw]) => splitEndpoint(raw))
}
+/**
+ * MCP-tool counterpart to getEnabledEndpointsForCapability: the MCP tools enabled by the complete
+ * granted-scope set that owe that to `capabilityScopes`, for grouping enabled tools under the
+ * capability that contributes them.
+ */
+export const getEnabledMcpToolsForCapability = ({
+ capabilityScopes,
+ allGrantedScopes,
+ permissionScopeMap,
+}: {
+ capabilityScopes: Iterable
+ allGrantedScopes: Iterable
+ permissionScopeMap: PermissionScopeMap | undefined
+}): string[] => {
+ if (permissionScopeMap == null) return []
+
+ const granted = new Set(allGrantedScopes)
+ const capability = new Set(capabilityScopes)
+ return Object.entries(permissionScopeMap.mcp_tools)
+ .filter(([, groups]) =>
+ groups.some(
+ (group) =>
+ group.some((scope) => capability.has(scope)) && group.every((scope) => granted.has(scope))
+ )
+ )
+ .map(([tool]) => tool)
+}
+
/**
* Informational lookup for the per-permission risk tooltip: the MCP tools associated with any of
* the given scopes. Unlike getEnabledMcpTools this is not conjunctive — it surfaces every tool that
From 2440b06cb7089bee69499fd1583b815a2f7d2917 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Cemal=20K=C4=B1l=C4=B1=C3=A7?=
Date: Tue, 18 Aug 2026 12:13:07 +0200
Subject: [PATCH 11/21] fix(docs/oauth-server): add `plain` for
code_challenge_method (#49180)
## 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?
docs update
## Summary by CodeRabbit
* **Documentation**
* Clarified that OAuth authorization requests support both `S256` and
`plain` code challenge methods.
* Recommends `S256` for improved security.
---------
Co-authored-by: Jeremias Menichelli
---
apps/docs/content/guides/auth/oauth-server/oauth-flows.mdx | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/apps/docs/content/guides/auth/oauth-server/oauth-flows.mdx b/apps/docs/content/guides/auth/oauth-server/oauth-flows.mdx
index 3820b37c0ce64..855391dca71f5 100644
--- a/apps/docs/content/guides/auth/oauth-server/oauth-flows.mdx
+++ b/apps/docs/content/guides/auth/oauth-server/oauth-flows.mdx
@@ -158,7 +158,7 @@ https://.supabase.co/auth/v1/oauth/authorize?
| `client_id` | The client ID from registration |
| `redirect_uri` | Must exactly match a registered redirect URI |
| `code_challenge` | The generated code challenge |
-| `code_challenge_method` | Must be `S256` (SHA-256) |
+| `code_challenge_method` | `S256` (SHA-256, recommended) or `plain` |
#### Optional parameters
From ce27b4ee5b7804e652637350ba501e015a56497b Mon Sep 17 00:00:00 2001
From: "kemal.earth" <606977+kemaldotearth@users.noreply.github.com>
Date: Tue, 18 Aug 2026 12:29:12 +0100
Subject: [PATCH 12/21] chore(studio): scoped pat mcp tool ui improvement
(#49188)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
## 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?
Follow on from view permissions sheet and review step tidy up to show a
clear list of available mcp tools.
## Summary by CodeRabbit
* **New Features**
* Added an “Available MCP tools” section to scoped token reviews and
token details.
* Displays enabled tools as badges, with a clear empty state when none
are available.
* **Improvements**
* Simplified capability cards to focus on enabled API endpoints.
* Removed per-permission MCP tool details and ungranted capability
listings.
* Updated endpoint count formatting for clearer singular and plural
labels.
* **Tests**
* Updated capability and token detail tests to reflect the new MCP tool
summary presentation.
---
.../AccessToken.permissions.test.ts | 62 -------------------
.../Scoped/Form/NewScopedTokenForm.tsx | 1 -
.../Scoped/Form/NewScopedTokenFormReview.tsx | 31 +++++++++-
.../Scoped/Form/PermissionRow.tsx | 12 +---
.../Scoped/Form/PermissionsAccordion.tsx | 4 --
.../AccessTokens/Scoped/Form/RiskMarker.tsx | 29 +--------
.../TokenCapabilities/CapabilityCard.tsx | 14 ++---
.../TokenCapabilities/CapabilityCardBody.tsx | 19 +-----
.../TokenCapabilities/DenseCapabilities.tsx | 24 +------
.../TokenCapabilities.utils.test.ts | 38 ------------
.../TokenCapabilities.utils.ts | 21 -------
.../Scoped/ViewTokenSheet.test.tsx | 13 ++--
.../AccessTokens/Scoped/ViewTokenSheet.tsx | 31 +++++++++-
.../hooks/useCapabilitySummary.ts | 11 +---
.../permission-scope-map-query.ts | 49 ---------------
15 files changed, 78 insertions(+), 281 deletions(-)
diff --git a/apps/studio/components/interfaces/Account/AccessTokens/AccessToken.permissions.test.ts b/apps/studio/components/interfaces/Account/AccessTokens/AccessToken.permissions.test.ts
index 7b7e03a3a274c..b40d3550382af 100644
--- a/apps/studio/components/interfaces/Account/AccessTokens/AccessToken.permissions.test.ts
+++ b/apps/studio/components/interfaces/Account/AccessTokens/AccessToken.permissions.test.ts
@@ -11,7 +11,6 @@ import {
getEnabledEndpoints,
getEnabledEndpointsForCapability,
getEnabledMcpTools,
- getEnabledMcpToolsForCapability,
normalizePermissionScopeMap,
type PermissionScopeMap,
} from '@/data/scoped-access-tokens/permission-scope-map-query'
@@ -310,64 +309,3 @@ describe('getEnabledEndpointsForCapability', () => {
).toEqual(['PUT /api/upgrade'])
})
})
-
-describe('getEnabledMcpToolsForCapability', () => {
- it('attributes a tool to each capability whose scope is in a fully-granted group', () => {
- const permissionScopeMap = scopeMap({
- mcp_tools: {
- list_branches: [['branching_development_read'], ['branching_production_read']],
- },
- })
- const allGrantedScopes = ['branching_development_read', 'branching_production_read']
-
- expect(
- getEnabledMcpToolsForCapability({
- capabilityScopes: ['branching_development_read'],
- allGrantedScopes,
- permissionScopeMap,
- })
- ).toEqual(['list_branches'])
- expect(
- getEnabledMcpToolsForCapability({
- capabilityScopes: ['branching_production_read'],
- allGrantedScopes,
- permissionScopeMap,
- })
- ).toEqual(['list_branches'])
- })
-
- it('does not attribute a tool to a capability whose own group is unsatisfied', () => {
- const enabled = getEnabledMcpToolsForCapability({
- capabilityScopes: ['branching_production_read'],
- allGrantedScopes: ['branching_development_read'],
- permissionScopeMap: scopeMap({
- mcp_tools: {
- list_branches: [['branching_development_read'], ['branching_production_read']],
- },
- }),
- })
-
- expect(enabled).toEqual([])
- })
-
- it('requires every scope of the capability group to be granted', () => {
- const permissionScopeMap = scopeMap({
- mcp_tools: { upgrade_project: [['project_admin_read', 'database_read']] },
- })
-
- expect(
- getEnabledMcpToolsForCapability({
- capabilityScopes: ['database_read'],
- allGrantedScopes: ['database_read'],
- permissionScopeMap,
- })
- ).toEqual([])
- expect(
- getEnabledMcpToolsForCapability({
- capabilityScopes: ['database_read'],
- allGrantedScopes: ['database_read', 'project_admin_read'],
- permissionScopeMap,
- })
- ).toEqual(['upgrade_project'])
- })
-})
diff --git a/apps/studio/components/interfaces/Account/AccessTokens/Scoped/Form/NewScopedTokenForm.tsx b/apps/studio/components/interfaces/Account/AccessTokens/Scoped/Form/NewScopedTokenForm.tsx
index bf066edfaccbe..7c18d08c332b0 100644
--- a/apps/studio/components/interfaces/Account/AccessTokens/Scoped/Form/NewScopedTokenForm.tsx
+++ b/apps/studio/components/interfaces/Account/AccessTokens/Scoped/Form/NewScopedTokenForm.tsx
@@ -170,7 +170,6 @@ export const NewScopedTokenForm = ({
{showMissingPermissionsWarning && (
diff --git a/apps/studio/components/interfaces/Account/AccessTokens/Scoped/Form/NewScopedTokenFormReview.tsx b/apps/studio/components/interfaces/Account/AccessTokens/Scoped/Form/NewScopedTokenFormReview.tsx
index 2894159cb00c3..6ef729763e966 100644
--- a/apps/studio/components/interfaces/Account/AccessTokens/Scoped/Form/NewScopedTokenFormReview.tsx
+++ b/apps/studio/components/interfaces/Account/AccessTokens/Scoped/Form/NewScopedTokenFormReview.tsx
@@ -1,6 +1,6 @@
import dayjs from 'dayjs'
import { useMemo, useState } from 'react'
-import { cn } from 'ui'
+import { Badge, cn } from 'ui'
import { Admonition } from 'ui-patterns/Admonition'
import { PERMISSION_MODE_LABEL, selectionToScopes } from '../../AccessToken.permissions'
@@ -26,7 +26,10 @@ import {
type CapabilityLevelFilter,
} from '../TokenCapabilities/TokenCapabilities.utils'
import { EXPIRY_OPTIONS, type TokenFormValues } from './NewScopedTokenForm.utils'
-import { PermissionScopeMap } from '@/data/scoped-access-tokens/permission-scope-map-query'
+import {
+ getEnabledMcpTools,
+ PermissionScopeMap,
+} from '@/data/scoped-access-tokens/permission-scope-map-query'
interface ReviewStepProps {
values: TokenFormValues
@@ -97,6 +100,11 @@ export const NewScopedTokenFormReview = ({
const capabilityTier = getCapabilityDensityTier(capabilities.length)
const [levelFilter, setLevelFilter] = useState('all')
+ const enabledMcpTools = useMemo(
+ () => getEnabledMcpTools({ grantedScopes, permissionScopeMap }).sort(),
+ [grantedScopes, permissionScopeMap]
+ )
+
const { containerRef: pillsRef, isWrapped: isResourceAccessWrapped } = useResourceAccessWrap()
return (
@@ -176,6 +184,25 @@ export const NewScopedTokenFormReview = ({
levelFilter={levelFilter}
/>