This commit is contained in:
Saxon Fletcher committed 2026-07-31 11:23:06 +10:00
1 parent 5c1a3e6886
commit 1263f8924f
15 files changed
+324 -37

No files matched your search

@@ -0,0 +1,33 @@
import fs from 'fs/promises'
import path from 'path'
import { NextRequest, NextResponse } from 'next/server'
import type { RegistryItem } from 'shadcn/schema'
async function readRegistryItem(): Promise<RegistryItem> {
const filePath = path.join(process.cwd(), 'public/r/mcp-headless-app.json')
const content = await fs.readFile(filePath, 'utf-8')
return JSON.parse(content) as RegistryItem
}
export async function GET(request: NextRequest) {
try {
const registryItem = await readRegistryItem()
const basePath = process.env.NEXT_PUBLIC_BASE_PATH ?? ''
const registryItemUrl = (name: string) =>
new URL(`${basePath}/r/${name}.json`, request.nextUrl.origin).toString()
return NextResponse.json({
...registryItem,
registryDependencies: [
registryItemUrl('mcp-auth-html'),
registryItemUrl('mcp-server'),
registryItemUrl('mcp-tools-postgrest'),
],
} satisfies RegistryItem)
} catch (error) {
console.error('Failed to load the Headless MCP App registry item:', error)
return NextResponse.json({ error: 'Failed to load registry item.' }, { status: 500 })
}
}
+3 -2
View File
@@ -7,14 +7,15 @@ const Command = dynamic(() => import('./command').then((mod) => mod.Command), {
interface BlockItemProps {
name: string
installArgs?: string
}
export const BlockItem = ({ name }: BlockItemProps) => {
export const BlockItem = ({ name, installArgs }: BlockItemProps) => {
const framework = name.includes('vue') || name.includes('nuxtjs') ? 'vue' : 'react'
return (
<div className="mt-4">
<Command name={name} highlight framework={framework} />
<Command name={name} highlight framework={framework} installArgs={installArgs} />
</div>
)
}
+11 -18
View File
@@ -10,6 +10,7 @@ import { useLocalStorage } from './use-local-storage'
interface CommandCopyProps {
name: string
highlight?: boolean
installArgs?: string
// For Vue, we need to use the `shadcn-vue` package instead of `shadcn`
framework?: 'react' | 'vue'
}
@@ -46,7 +47,7 @@ const getComponentPath = (name: string) => {
}
}
export function Command({ name, highlight, framework = 'react' }: CommandCopyProps) {
export function Command({ name, highlight, framework = 'react', installArgs }: CommandCopyProps) {
const pathname = usePathname()
const [value, setValue] = useLocalStorage<InstallationTab>(LOCAL_STORAGE_KEY, 'prompt')
@@ -56,30 +57,22 @@ export function Command({ name, highlight, framework = 'react' }: CommandCopyPro
const commands: Record<PackageManager, string> =
framework === 'vue'
? {
npm: `npx shadcn-vue@latest add ${baseUrl}${componentPath}`,
pnpm: `pnpm dlx shadcn-vue@latest add ${baseUrl}${componentPath}`,
yarn: `yarn dlx shadcn-vue@latest add ${baseUrl}${componentPath}`,
bun: `bunx --bun shadcn-vue@latest add ${baseUrl}${componentPath}`,
npm: `npx shadcn-vue@latest add ${baseUrl}${componentPath}${installArgs ? ` ${installArgs}` : ''}`,
pnpm: `pnpm dlx shadcn-vue@latest add ${baseUrl}${componentPath}${installArgs ? ` ${installArgs}` : ''}`,
yarn: `yarn dlx shadcn-vue@latest add ${baseUrl}${componentPath}${installArgs ? ` ${installArgs}` : ''}`,
bun: `bunx --bun shadcn-vue@latest add ${baseUrl}${componentPath}${installArgs ? ` ${installArgs}` : ''}`,
}
: {
npm: `npx shadcn@latest add ${baseUrl}${componentPath}`,
pnpm: `pnpm dlx shadcn@latest add ${baseUrl}${componentPath}`,
yarn: `yarn dlx shadcn@latest add ${baseUrl}${componentPath}`,
bun: `bunx --bun shadcn@latest add ${baseUrl}${componentPath}`,
npm: `npx shadcn@latest add ${baseUrl}${componentPath}${installArgs ? ` ${installArgs}` : ''}`,
pnpm: `pnpm dlx shadcn@latest add ${baseUrl}${componentPath}${installArgs ? ` ${installArgs}` : ''}`,
yarn: `yarn dlx shadcn@latest add ${baseUrl}${componentPath}${installArgs ? ` ${installArgs}` : ''}`,
bun: `bunx --bun shadcn@latest add ${baseUrl}${componentPath}${installArgs ? ` ${installArgs}` : ''}`,
}
const basePath = process.env.NEXT_PUBLIC_BASE_PATH ?? ''
const currentPath = pathname?.startsWith(basePath) ? pathname : `${basePath}${pathname ?? ''}`
const pageUrl = `${getPageBaseUrl()}${currentPath}`
const prompt = [
`Read the installation instructions at ${pageUrl} before making changes.`,
'',
`Then install the ${name} block in the current project with:`,
'',
commands.npm,
'',
'Follow the remaining setup and configuration steps on that page.',
].join('\n')
const prompt = `Read the installation instructions at ${pageUrl} before making changes. Then install the ${name} block in the current project with: ${commands.npm}. Follow the remaining setup and configuration steps on that page.`
const tabs: InstallationTab[] = ['prompt', ...Object.keys(commands)] as InstallationTab[]
return (
+1 -1
View File
@@ -76,7 +76,7 @@ function SideNavigation() {
</div>
<div className="pb-6">
<div className="font-mono uppercase text-xs text-foreground-lighter/75 mb-2 px-6 tracking-widest">
Blocks
UI Blocks
</div>
<div className="space-y-0.5">
{/* Render items based on component definitions */}
+15 -8
View File
@@ -39,15 +39,8 @@ export const platformBlocks: SidebarNavGroup = {
// Blocks for apps with no front-end of their own. These ship plain HTML rather
// than components, so they take no framework and are not framework-switchable.
export const headlessBlocks: SidebarNavGroup = {
title: 'Headless',
title: 'MCP',
items: [
{
title: 'MCP Auth',
href: '/docs/headless/mcp-auth',
items: [],
new: true,
commandItemLabel: 'MCP Auth',
},
{
title: 'MCP Server',
href: '/docs/headless/mcp-server',
@@ -62,6 +55,20 @@ export const headlessBlocks: SidebarNavGroup = {
new: true,
commandItemLabel: 'MCP Tools: PostgREST',
},
{
title: 'MCP Auth',
href: '/docs/headless/mcp-auth',
items: [],
new: true,
commandItemLabel: 'MCP Auth',
},
{
title: 'Headless MCP App',
href: '/docs/headless/mcp-headless-app',
items: [],
new: true,
commandItemLabel: 'Headless MCP App',
},
],
}
@@ -0,0 +1,122 @@
---
title: Headless MCP App
description: A complete MCP app with OAuth screens, an Edge Function, and database tools
---
Everything needed for a production MCP app in one install: static browser
screens for OAuth, an MCP server Edge Function, and PostgREST tools generated
from your database schema. This block owns the toolset manifest that enables the
installed PostgREST toolset and a starter `supabase/config.toml`.
## Installation
<BlockItem
name="mcp-headless-app"
description="Complete headless MCP app with PostgREST database tools"
installArgs="--yes --overwrite"
/>
The app composes [MCP Auth](./mcp-auth), [MCP Server](./mcp-server), and
[MCP Tools: PostgREST](./mcp-tools-postgrest) as registry dependencies. It also
installs `supabase/functions/mcp-server/tools/index.ts`, with the PostgREST
toolset registered already, so no source edits are needed after installation.
The server dependency creates the same file first; the displayed `--overwrite`
flag deliberately replaces it with this complete manifest. It only overwrites
that generated toolset manifest and the starter `supabase/config.toml`.
<Callout type="caution">
This is a starter block for a new project. The install command overwrites both
`supabase/functions/mcp-server/tools/index.ts` and `supabase/config.toml`. For
an existing project, install the individual MCP blocks and merge their
configuration deliberately instead.
</Callout>
## Folder structure
<RegistryBlock itemName="mcp-headless-app" />
## Configure and deploy
The installed `supabase/config.toml` already enables the OAuth server,
access-token hook, and `mcp-server` function. Update `project_id`, `site_url`,
and `additional_redirect_urls` with the static app's host, then set the static
site's `SUPABASE_URL` and `SUPABASE_PUBLISHABLE_KEY` in `config.js` as described
in [MCP Auth](./mcp-auth).
Once the static files are hosted and `site_url` points to that host, deploy the
database hook and Edge Function:
```bash
supabase db push
supabase config push
supabase functions deploy mcp-server
```
The server registers `echo`, `whoami`, and the PostgREST toolset. The latter
builds database tools from the caller's schema and permissions; see [MCP Tools:
PostgREST](./mcp-tools-postgrest) for its guardrails and RLS guidance.
## Local quickstart
Use three terminals to run the complete app locally. The starter
`supabase/config.toml` already targets `http://localhost:3000`; update it first
if you use another static-server origin.
Start Supabase, then copy the **API URL** and **publishable key** printed by
`supabase status` into `config.js`:
```js
export const SUPABASE_URL = 'API_URL_FROM_SUPABASE_STATUS'
export const SUPABASE_PUBLISHABLE_KEY = 'PUBLISHABLE_KEY_FROM_SUPABASE_STATUS'
```
```bash
# Terminal 1
supabase start
supabase status
# Terminal 2
npx serve . --listen 3000
# Terminal 3
cp supabase/functions/mcp-server/.env.example supabase/functions/.env
# Set MCP_PUBLIC_SUPABASE_URL to the API URL from `supabase status`.
supabase functions serve mcp-server --env-file supabase/functions/.env
```
Open `http://localhost:3000`. The static app uses the values in `config.js` to
connect to the local project and shows the MCP endpoint served by the function.
## Expose database tools safely
The PostgREST toolset only discovers tables and functions that the calling user
can access. For new tables, enable RLS and grant the `authenticated` role the
operations your tools need. Some projects require explicit grants before a new
table is reachable through the Data API.
```sql
create table public.todos (
id bigint generated always as identity primary key,
user_id uuid not null references auth.users,
title text not null,
done boolean not null default false
);
alter table public.todos enable row level security;
grant select, insert, update, delete on table public.todos to authenticated;
create policy "Users can manage their own todos"
on public.todos
for all
to authenticated
using ((select auth.uid()) = user_id)
with check ((select auth.uid()) = user_id);
```
The policy limits every operation to the caller's rows. Do not grant a role
access without an RLS policy that matches the intended ownership model. Views
over protected tables should use `security_invoker = true`; see [MCP Tools:
PostgREST](./mcp-tools-postgrest) for the full security model.
+5
View File
@@ -36,6 +36,11 @@ const nextConfig = {
destination: '/api/registry/mcp-tools-postgrest',
permanent: false,
},
{
source: '/r/mcp-headless-app.json',
destination: '/api/registry/mcp-headless-app',
permanent: false,
},
]
},
}
@@ -0,0 +1,27 @@
{
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
"name": "mcp-headless-app",
"type": "registry:item",
"title": "Headless MCP App",
"description": "A complete headless MCP app: static OAuth screens, an MCP Edge Function, and PostgREST database tools.",
"registryDependencies": [
"https://supabase.com/library/r/mcp-auth-html.json",
"https://supabase.com/library/r/mcp-server.json",
"https://supabase.com/library/r/mcp-tools-postgrest.json"
],
"files": [
{
"path": "registry/default/blocks/mcp-headless-app/supabase/config.toml",
"content": "# Headless MCP App configuration.\n#\n# This is a complete starter config for a new local Supabase project. Update\n# project_id and the two browser origins before deploying the static app.\n\n# Use a distinct value when multiple local Supabase projects run on this machine.\nproject_id = \"headless-mcp-app\"\n\n[api]\nenabled = true\n# `public` is where PostgREST discovers tables and functions for the MCP tools.\nschemas = [\"public\", \"graphql_public\"]\nextra_search_path = [\"public\", \"extensions\"]\n# New public entities are not auto-exposed. Grant the authenticated role the\n# required operations and enable RLS for every table used by MCP tools.\n# auto_expose_new_tables = true\n\n[auth]\nenabled = true\n# Replace both URLs with the origin where you host this block's static files.\nsite_url = \"http://localhost:3000\"\nadditional_redirect_urls = [\"http://localhost:3000/auth/\"]\n\n[auth.oauth_server]\nenabled = true\nauthorization_url_path = \"/oauth/consent/\"\nallow_dynamic_registration = true\n\n[auth.hook.custom_access_token]\nenabled = true\nuri = \"pg-functions://postgres/private/mcp_custom_access_token_hook\"\n\n[functions.mcp-server]\n# The function verifies OAuth and Supabase bearer tokens itself.\nverify_jwt = false\n",
"type": "registry:file",
"target": "supabase/config.toml"
},
{
"path": "registry/default/blocks/mcp-headless-app/supabase/functions/mcp-server/tools/index.ts",
"content": "import { echoToolset } from './echo.ts'\nimport { postgrestToolset } from './postgrest/index.ts'\nimport { registerToolset } from './registry.ts'\nimport { whoamiToolset } from './whoami.ts'\n\nexport type { ToolContext } from './types.ts'\n\n// The complete app includes the server's built-in tools and the PostgREST\n// toolset installed by its registry dependency.\nexport function registerToolsets(): void {\n registerToolset(echoToolset)\n registerToolset(whoamiToolset)\n registerToolset(postgrestToolset)\n}\n",
"type": "registry:file",
"target": "supabase/functions/mcp-server/tools/index.ts"
}
],
"docs": "Required setup: this starter block installs `supabase/config.toml` with OAuth, access-token-hook, and Edge Function settings. Fill in its `project_id`, `site_url`, and `additional_redirect_urls` for your static app host, then set `SUPABASE_URL` and `SUPABASE_PUBLISHABLE_KEY` in `config.js`. The `--yes --overwrite` install command intentionally replaces both this starter config and `supabase/functions/mcp-server/tools/index.ts`; merge those files manually if your project already has either one. Deploy with `supabase db push`, `supabase config push`, and `supabase functions deploy mcp-server`. The complete manifest already registers the PostgREST toolset; no manual source edit is needed. See the block documentation for the local quickstart and RLS/grant requirements."
}
@@ -27,5 +27,5 @@
"target": "supabase/functions/mcp-server/tools/postgrest/tools.ts"
}
],
"docs": "Required setup: register the installed PostgREST toolset in `supabase/functions/mcp-server/tools/index.ts`. Import `postgrestToolset` from `./postgrest/index.ts`, then call `registerToolset(postgrestToolset)` inside `registerToolsets()`. See the block documentation for the complete edit."
"docs": "Standalone-install note: register the installed PostgREST toolset in `supabase/functions/mcp-server/tools/index.ts`. Import `postgrestToolset` from `./postgrest/index.ts`, then call `registerToolset(postgrestToolset)` inside `registerToolsets()`. If you installed `mcp-headless-app`, its manifest already performs this registration; do not edit it. See the block documentation for the complete setup."
}
+25 -1
View File
@@ -1776,6 +1776,30 @@
}
]
},
{
"name": "mcp-headless-app",
"type": "registry:item",
"title": "Headless MCP App",
"description": "A complete headless MCP app: static OAuth screens, an MCP Edge Function, and PostgREST database tools.",
"registryDependencies": [
"https://supabase.com/library/r/mcp-auth-html.json",
"https://supabase.com/library/r/mcp-server.json",
"https://supabase.com/library/r/mcp-tools-postgrest.json"
],
"docs": "Required setup: this starter block installs `supabase/config.toml` with OAuth, access-token-hook, and Edge Function settings. Fill in its `project_id`, `site_url`, and `additional_redirect_urls` for your static app host, then set `SUPABASE_URL` and `SUPABASE_PUBLISHABLE_KEY` in `config.js`. The `--yes --overwrite` install command intentionally replaces both this starter config and `supabase/functions/mcp-server/tools/index.ts`; merge those files manually if your project already has either one. Deploy with `supabase db push`, `supabase config push`, and `supabase functions deploy mcp-server`. The complete manifest already registers the PostgREST toolset; no manual source edit is needed. See the block documentation for the local quickstart and RLS/grant requirements.",
"files": [
{
"path": "registry/default/blocks/mcp-headless-app/supabase/config.toml",
"type": "registry:file",
"target": "supabase/config.toml"
},
{
"path": "registry/default/blocks/mcp-headless-app/supabase/functions/mcp-server/tools/index.ts",
"type": "registry:file",
"target": "supabase/functions/mcp-server/tools/index.ts"
}
]
},
{
"name": "mcp-server",
"type": "registry:item",
@@ -1857,7 +1881,7 @@
"registryDependencies": [
"https://supabase.com/library/r/mcp-server.json"
],
"docs": "Required setup: register the installed PostgREST toolset in `supabase/functions/mcp-server/tools/index.ts`. Import `postgrestToolset` from `./postgrest/index.ts`, then call `registerToolset(postgrestToolset)` inside `registerToolsets()`. See the block documentation for the complete edit.",
"docs": "Standalone-install note: register the installed PostgREST toolset in `supabase/functions/mcp-server/tools/index.ts`. Import `postgrestToolset` from `./postgrest/index.ts`, then call `registerToolset(postgrestToolset)` inside `registerToolsets()`. If you installed `mcp-headless-app`, its manifest already performs this registration; do not edit it. See the block documentation for the complete setup.",
"files": [
{
"path": "registry/default/blocks/mcp-tools-postgrest/supabase/functions/mcp-server/tools/postgrest/index.ts",
+7 -5
View File
@@ -5,6 +5,7 @@ import currentUserAvatar from './default/blocks/current-user-avatar/registry-ite
import dropzone from './default/blocks/dropzone/registry-item.json' with { type: 'json' }
import infiniteQueryHook from './default/blocks/infinite-query-hook/registry-item.json' with { type: 'json' }
import mcpAuthHtml from './default/blocks/mcp-auth-html/registry-item.json' with { type: 'json' }
import mcpHeadlessApp from './default/blocks/mcp-headless-app/registry-item.json' with { type: 'json' }
import mcpServer from './default/blocks/mcp-server/registry-item.json' with { type: 'json' }
import mcpToolsPostgrest from './default/blocks/mcp-tools-postgrest/registry-item.json' with { type: 'json' }
import passwordBasedAuthNextjs from './default/blocks/password-based-auth-nextjs/registry-item.json' with { type: 'json' }
@@ -61,12 +62,13 @@ export const blocks = [
// infinite query hook is intentionally not combined with the clients since it depends on clients having database types.
infiniteQueryHook as RegistryItem,
// The headless MCP blocks take no client: mcp-auth-html is plain HTML, and the
// other two are Deno Edge Function code. Every file carries an explicit target,
// which is what lets `shadcn add` install them into a directory with no
// components.json. mcp-tools-postgrest declares mcp-server as a registry
// dependency, so installing it lands the framework first.
// The headless MCP blocks take no client: mcp-auth-html is plain HTML and the
// server and tool blocks are Deno Edge Function code. Every file carries an
// explicit target, which is what lets `shadcn add` install them into a
// directory with no components.json. mcp-tools-postgrest declares mcp-server
// as a registry dependency; mcp-headless-app composes all three.
mcpAuthHtml as RegistryItem,
mcpHeadlessApp as RegistryItem,
mcpServer as RegistryItem,
mcpToolsPostgrest as RegistryItem,
@@ -0,0 +1,24 @@
{
"name": "mcp-headless-app",
"type": "registry:item",
"title": "Headless MCP App",
"description": "A complete headless MCP app: static OAuth screens, an MCP Edge Function, and PostgREST database tools.",
"registryDependencies": [
"https://supabase.com/library/r/mcp-auth-html.json",
"https://supabase.com/library/r/mcp-server.json",
"https://supabase.com/library/r/mcp-tools-postgrest.json"
],
"docs": "Required setup: this starter block installs `supabase/config.toml` with OAuth, access-token-hook, and Edge Function settings. Fill in its `project_id`, `site_url`, and `additional_redirect_urls` for your static app host, then set `SUPABASE_URL` and `SUPABASE_PUBLISHABLE_KEY` in `config.js`. The `--yes --overwrite` install command intentionally replaces both this starter config and `supabase/functions/mcp-server/tools/index.ts`; merge those files manually if your project already has either one. Deploy with `supabase db push`, `supabase config push`, and `supabase functions deploy mcp-server`. The complete manifest already registers the PostgREST toolset; no manual source edit is needed. See the block documentation for the local quickstart and RLS/grant requirements.",
"files": [
{
"path": "registry/default/blocks/mcp-headless-app/supabase/config.toml",
"type": "registry:file",
"target": "supabase/config.toml"
},
{
"path": "registry/default/blocks/mcp-headless-app/supabase/functions/mcp-server/tools/index.ts",
"type": "registry:file",
"target": "supabase/functions/mcp-server/tools/index.ts"
}
]
}
@@ -0,0 +1,35 @@
# Headless MCP App configuration.
#
# This is a complete starter config for a new local Supabase project. Update
# project_id and the two browser origins before deploying the static app.
# Use a distinct value when multiple local Supabase projects run on this machine.
project_id = "headless-mcp-app"
[api]
enabled = true
# `public` is where PostgREST discovers tables and functions for the MCP tools.
schemas = ["public", "graphql_public"]
extra_search_path = ["public", "extensions"]
# New public entities are not auto-exposed. Grant the authenticated role the
# required operations and enable RLS for every table used by MCP tools.
# auto_expose_new_tables = true
[auth]
enabled = true
# Replace both URLs with the origin where you host this block's static files.
site_url = "http://localhost:3000"
additional_redirect_urls = ["http://localhost:3000/auth/"]
[auth.oauth_server]
enabled = true
authorization_url_path = "/oauth/consent/"
allow_dynamic_registration = true
[auth.hook.custom_access_token]
enabled = true
uri = "pg-functions://postgres/private/mcp_custom_access_token_hook"
[functions.mcp-server]
# The function verifies OAuth and Supabase bearer tokens itself.
verify_jwt = false
@@ -0,0 +1,14 @@
import { echoToolset } from './echo.ts'
import { postgrestToolset } from './postgrest/index.ts'
import { registerToolset } from './registry.ts'
import { whoamiToolset } from './whoami.ts'
export type { ToolContext } from './types.ts'
// The complete app includes the server's built-in tools and the PostgREST
// toolset installed by its registry dependency.
export function registerToolsets(): void {
registerToolset(echoToolset)
registerToolset(whoamiToolset)
registerToolset(postgrestToolset)
}
@@ -4,7 +4,7 @@
"title": "MCP Tools: PostgREST",
"description": "A toolset for the MCP server that generates database tools from your PostgREST schema: read and write tools per table, view, and function.",
"registryDependencies": ["https://supabase.com/library/r/mcp-server.json"],
"docs": "Required setup: register the installed PostgREST toolset in `supabase/functions/mcp-server/tools/index.ts`. Import `postgrestToolset` from `./postgrest/index.ts`, then call `registerToolset(postgrestToolset)` inside `registerToolsets()`. See the block documentation for the complete edit.",
"docs": "Standalone-install note: register the installed PostgREST toolset in `supabase/functions/mcp-server/tools/index.ts`. Import `postgrestToolset` from `./postgrest/index.ts`, then call `registerToolset(postgrestToolset)` inside `registerToolsets()`. If you installed `mcp-headless-app`, its manifest already performs this registration; do not edit it. See the block documentation for the complete setup.",
"files": [
{
"path": "registry/default/blocks/mcp-tools-postgrest/supabase/functions/mcp-server/tools/postgrest/index.ts",