Agent connections
The User MCP and Admin MCP of the Next.js + Supabase starter: endpoints on the web and admin origins, tools, consent flow and where the code lives.
This kit exposes two remote MCP servers, one per origin: the User MCP on the web app and the Admin MCP on the admin app. They use the Supabase Auth session you already have; nothing else is installed on the user's side. For the cross-kit overview read user and admin MCPs, and for the client walkthrough read connect an agent.
Where users manage connections
| Connection | App and settings page | Live demo server |
|---|---|---|
| User | Web app, /mcp/connections/user | https://nextjs-supabase.usenextbase.com/mcp/user |
| Admin | Admin app, /mcp/connections/admin | https://nextjs-supabase-admin.usenextbase.com/mcp/admin |
The web app's sidebar links to Agent connections (sidebar-platform-nav.tsx); the admin app's sidebar links to Admin agent connections (sidebar-admin-panel-nav.tsx). Each page shows the server address to copy, the account's active connections for that server and a Disconnect button. The admin page requires an application administrator session on the admin origin.
Each app serves only its own audience. apps/web/src/mcp/adapter.ts passes audiences: ["user"] and apps/admin/src/mcp/adapter.ts passes audiences: ["admin"], so /mcp/admin on the web origin and /mcp/user on the admin origin return 404. For your own deployment the addresses are the web app's NEXT_PUBLIC_SITE_URL followed by /mcp/user and the admin app's NEXT_PUBLIC_SITE_URL followed by /mcp/admin. The settings pages print the exact values.
Tools and permissions
Both servers are read-only in v5. Every tool carries readOnlyHint: true and destructiveHint: false.
| Server | Tool | Scope | What it returns |
|---|---|---|---|
| User | account_context | user:read | The signed-in user's id and email, and the workspace approved for this connection |
| User | projects_list | user:read | Up to limit projects (1 to 50, default 20) in the approved workspace, newest first |
| Admin | admin_users_list | admin:read | Up to limit user profiles (id, full_name, created_at) |
| Admin | admin_workspaces_list | admin:read | Up to limit workspaces (id, name, slug, created_at) |
A user connection is bound to one workspace chosen during consent. Only workspaces where the account is owner or admin, or has the view_projects permission, are offered. projects_list runs the SQL function mcp_projects, which rechecks the live session and the membership in the same statement as the read.
Admin tools call mcp_live_actor on every request and fail with "Administrator access required" if the user_roles row is gone. A user token is never accepted at /mcp/admin, even for an administrator.
OAuth and consent
Each origin is its own authorization server. The issuer is {NEXT_PUBLIC_SITE_URL}/oauth of the app that serves the resource. Discovery documents on each origin:
/.well-known/oauth-protected-resource/mcp/user(web) or/.well-known/oauth-protected-resource/mcp/admin(admin)/.well-known/oauth-authorization-server/oauth
Endpoints are /oauth/register, /oauth/authorize, /oauth/token and /oauth/revoke. Public clients only, with dynamic registration, authorization code and mandatory PKCE S256. Callback URLs must be HTTPS, except loopback HTTP on localhost, 127.0.0.1 or ::1.
The consent page is /mcp/authorize. If you are not signed in, the login page receives next=/mcp/authorize?... and the app sends you back through /auth/callback to the same pending request. The page shows the client name (marked unverified), callback address, server and scope, and for user connections a workspace selector. If the account has no eligible workspace, approval is disabled until you create or join one.
Lifetimes from src/mcp/core.ts: pending consent 10 minutes, authorization codes 5 minutes, access tokens 15 minutes, refresh for at most 30 days and never past the original Supabase session. Refresh tokens rotate; reuse revokes the whole connection. The UI limits each account to 20 active connections.
Browser clients are accepted from the configured app origin, https://claude.ai and https://chatgpt.com. Desktop and server clients without an Origin header are accepted too.
Where the code lives
The MCP module is duplicated in both apps; keep the copies in sync when you change the protocol core.
| File | Role |
|---|---|
apps/web/src/mcp/core.ts and apps/admin/src/mcp/core.ts | Protocol, OAuth endpoints, consent and connection pages, tool registration. Shared with the other Postgres kits |
apps/web/src/mcp/supabase-adapter.ts and apps/admin/src/mcp/supabase-adapter.ts | Supabase-specific actor checks, workspace lookup and the tool implementations |
apps/web/src/mcp/supabase-store.ts and apps/admin/src/mcp/supabase-store.ts | Storage of registrations, pending consents and credentials in public.mcp_records |
apps/web/src/mcp/adapter.ts (audiences: ["user"]) and apps/admin/src/mcp/adapter.ts (audiences: ["admin"]) | Build the handler from each app's NEXT_PUBLIC_SITE_URL, NEXT_PUBLIC_SUPABASE_URL and SUPABASE_SECRET_KEY |
src/app/mcp/[...path]/route.ts, src/app/oauth/[...path]/route.ts, src/app/.well-known/... in each app | Thin Next.js route handlers that forward to handleMcp |
apps/database/supabase/migrations/20261014114351_user_admin_mcp.sql | mcp_records table and the mcp_live_actor and mcp_projects functions, shared by both apps |
apps/web/src/mcp/core.test.ts | Vitest coverage for protocol, PKCE, CSRF, replay, expiry, demotion and revocation. The admin copy has no tests of its own |
apps/web/src/mcp/supabase-store.integration.test.ts | Storage tests against a local database, enabled by the MCP_TEST_SUPABASE_* variables |
The migration grants mcp_records and both functions to service_role only. anon and authenticated cannot read credentials or call the bridge functions. The repository's own write-up is docs/mcp-connections.md.
Adding a tool safely
- Add the tool in the
audience === "user"branch ofcreateMcpHandlerin the web app'score.ts, or in the admin branch of the admin app'score.ts, with the sameannotationsobject and a boundedlimitinput. - Implement the data access in that app's
supabase-adapter.ts. CallliveActor(actor)first, and for workspace data pass the actor's user id and session id into a SQL function that checks membership inside the query, likemcp_projectsdoes. - Never trust
userIdorworkspaceIdarguments from the client. The grant's workspace and the live session are the only source of permission. - If the tool writes data, add a new scope.
requiredScopecurrently only knowsuser:readandadmin:read, and the consent page labels everything "Read access only". Changing that needs new consent copy and tests. - Extend
apps/web/src/mcp/core.test.ts, runpnpm test, and run the integration test against a local database. Mirror protocol changes into the admin copy.
See tools and permissions for the shared rules, and troubleshooting for 401 and 403 cases.
Authentication
How Supabase Auth, email verification, social providers, the admin role, the separate admin app and RLS work in the Next.js + Supabase starter.
Project structure
The monorepo layout of the Next.js + Supabase starter, including the separate admin app, and where to make each kind of change.