mirror of
https://github.com/supabase/supabase.git
synced 2026-10-06 09:55:06 +03:00
## 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? Feature — a new UI Library block. Stacked on #49573 (already in main) Fixes AI-1064 ## What is the new behavior? Adds `headless-app-tanstack`: customers sign in, authorize an MCP client, and use the product through agent tool calls. It composes the existing Password-Based Auth, OAuth Consent, and MCP Server blocks. - `/agents` provides a copyable connection prompt, lists OAuth authorizations, and lets customers revoke access. - The shared MCP runtime exposes `whoami` plus example task CRUD tools. Tools use the caller's Supabase client, with database grants and RLS enforcing ownership. - A root-level `supabase/` directory supplies local Auth/OAuth configuration, a declarative tasks schema, and Edge Function files, including `.env.example`. - Docs cover local setup, signing keys, migrations, environment configuration, deployment, and extending the tools. `/example/headless-app` previews the sign-in, consent, connect, and connected states. Shared block fixes make a fresh install work: - Explicit public URL resolution fixes OAuth discovery in local Edge Runtime when middleware runtime detection fails. Both external OAuth access tokens and ordinary authenticated app session tokens remain supported; embedded agents do not need an additional consent flow. - Registry targets keep backend files outside `src/`, and generated consumer routes omit source-only TypeScript suppressions. - Signup respects `auth.email.enable_confirmations`; sign-in/signup preserve the return destination. Missing consent IDs retain the existing error state without serializing `null` into the URL. ## How to test Use the UI Library on **staging** and follow the block pages' instructions. 1. Open the **Headless App** block page for TanStack Start. Install it into a fresh app and follow the setup instructions through connecting an MCP client. 2. Sign up, open `/agents`, and use the connection prompt to authorize a client. Call `whoami`, then create, list, update, and delete a task. 3. Confirm the client appears on `/agents`. Revoke access and verify it disappears and token refresh fails. An existing access token can continue working until it expires. 4. Follow the **MCP Server** block page's embedded-agent instructions using an authenticated app session. Confirm tools work without another OAuth consent flow and `whoami` returns `client_id: null`. 5. With a second user, confirm each user can only access their own tasks. Check that signup behaves correctly for the configured email-confirmation setting. 6. Check the Headless App preview states and run the installed app's typecheck and production build. ## Validation performed Fresh local installation and browser/SDK verification passed: 26 live MCP/Data API checks, 10 Deno tests, and 7 connection-page component tests. Also passed UI Library typecheck, targeted lint, registry/Markdown builds, and fresh consumer typecheck/production build. Both OAuth and ordinary app session authentication were exercised. Hosted deployment and consuming the confirmation-email link were not tested. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - **New Features** - Added a TanStack Headless App example with sign-in, OAuth consent, MCP connection, and connected-agent screens. - Added task management tools for listing, creating, updating, and deleting tasks through MCP. - Added connected-agent management, including server URL and prompt copying, refresh, and access revocation. - Added a new Headless App registry block and documentation. - **Bug Fixes** - Preserved intended destinations through sign-up, email confirmation, and protected-route login redirects. - Improved OAuth discovery URL handling across forwarded-host deployments. - **Documentation** - Updated setup, environment, deployment, and Supabase CLI guidance for headless apps and MCP servers. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: Saxon Fletcher <SaxonF@users.noreply.github.com> Co-authored-by: Claude Opus 5 <noreply@anthropic.com> Co-authored-by: repro <repro@local> Co-authored-by: Raúl Barroso <code@raulb.dev>
87 lines
2.4 KiB
TypeScript
87 lines
2.4 KiB
TypeScript
import * as fs from 'fs'
|
|
|
|
export interface RegistryNode {
|
|
name: string
|
|
path: string
|
|
originalPath: string
|
|
type: 'directory' | 'file'
|
|
children?: RegistryNode[]
|
|
content?: string
|
|
}
|
|
|
|
interface RegistryFile {
|
|
path: string
|
|
target?: string
|
|
type: string
|
|
content: string
|
|
}
|
|
|
|
const DEFAULT_PATHS = {
|
|
component: '/components',
|
|
hook: '/hooks',
|
|
util: '/lib',
|
|
} as const
|
|
|
|
/**
|
|
* Converts a flat registry array into a hierarchical file tree structure
|
|
*/
|
|
export function generateRegistryTree(registryPath: string): RegistryNode[] {
|
|
const registry = JSON.parse(fs.readFileSync(registryPath, 'utf-8')) as { files: RegistryFile[] }
|
|
const tree: RegistryNode[] = []
|
|
|
|
const sortedRegistry = [...registry.files].sort((a, b) => a.path.localeCompare(b.path))
|
|
|
|
for (const file of sortedRegistry) {
|
|
const itemPath = (file.target || getDefaultPath(file)).replace(/^~\//, '')
|
|
const pathParts = itemPath.split('/').filter(Boolean)
|
|
let currentLevel = tree
|
|
|
|
for (let i = 0; i < pathParts.length; i++) {
|
|
const part = pathParts[i]
|
|
const isLast = i === pathParts.length - 1
|
|
const path = '/' + pathParts.slice(0, i + 1).join('/')
|
|
|
|
let node = currentLevel.find((n) => n.name === part)
|
|
|
|
// Remove any paths in the file content that point to the block directory.
|
|
const content = file.content
|
|
.replaceAll(/@\/registry\/default\/blocks\/.+?\//gi, '@/')
|
|
.replaceAll(/@\/registry\/default\/fixtures\//gi, '@/')
|
|
.replaceAll(/@\/registry\/default\//gi, '@/')
|
|
.replaceAll(/@\/clients\/.+?\//gi, '@/')
|
|
|
|
if (!node) {
|
|
node = {
|
|
name: part,
|
|
path,
|
|
originalPath: file.path,
|
|
type: isLast ? 'file' : 'directory',
|
|
...(isLast ? { content } : { children: [] }),
|
|
}
|
|
currentLevel.push(node)
|
|
}
|
|
|
|
if (!isLast) {
|
|
node.children = node.children || []
|
|
currentLevel = node.children
|
|
}
|
|
}
|
|
}
|
|
|
|
return tree
|
|
}
|
|
|
|
/**
|
|
* Determines the default path for an item based on its type
|
|
*/
|
|
function getDefaultPath(item: RegistryFile): string {
|
|
const type = item.type.toLowerCase() || ''
|
|
const basePath = DEFAULT_PATHS[type as keyof typeof DEFAULT_PATHS] || ''
|
|
// clean all paths that start with paths specific to this repo organization
|
|
const filePath = item.path
|
|
.replace(/registry\/default\/blocks\/.+?\//, '')
|
|
.replace(/registry\/default\/clients\/.+?\//, '')
|
|
|
|
return `${basePath}/${filePath}`
|
|
}
|