Nextbase Docs
Nextbase Docs
HomeBlogWelcome to Nextbase v5!Choose your starterGetting startedLocal Supabase per agent
Next.js + NeonInstallationEnvironment variablesAuthenticationAgent connectionsProject structureDeployment
AuthenticationWorkspaces and the admin area
Building with agentsEmails with Resend
TroubleshootingNextbase v5
The 8 starter kitsNext.js + Neon

Agent connections

The User MCP and Admin MCP servers in the Next.js + Neon starter: endpoints on the web and admin origins, tools, OAuth behaviour and how to add a tool.

This kit ships two remote MCP servers, one in each Next.js app. They use your existing Better Auth login, so an agent never gets a password or a database credential. For the general flow, read user and admin MCPs and connect an agent.

Two connections on two origins

The User MCP reads account context and projects in one selected workspace and is served by the web app. The Admin MCP lists users and workspaces across the app and is served by the admin app. Both are read-only in v5.

ConnectionServed bySettings pageLive demo server
Userapps/web/mcp/connections/user on the web originhttps://nextjs-neon.usenextbase.com/mcp/user
Adminapps/admin/mcp/connections/admin on the admin originhttps://nextjs-neon-admin.usenextbase.com/mcp/admin

The settings pages are linked as Agent connections in the web app sidebar and Admin agent connections in the admin app sidebar. Each page shows the server address to copy, lists that account's connections and offers Disconnect. The admin page only opens for administrators signed in on the admin host.

Each app answers only its own audience. The web routes call handleMcp(request), which defaults to the user audience, and the admin routes call handleMcp(request, "admin"); a request for the other audience returns 404. So /mcp/admin and /mcp/connections/admin don't exist on the web origin, and /mcp/user doesn't exist on the admin origin.

Your own deployment has its own addresses. Each server's origin is the NEXT_PUBLIC_APP_URL of the app serving it: the User MCP is <web NEXT_PUBLIC_APP_URL>/mcp/user and the Admin MCP is <admin NEXT_PUBLIC_APP_URL>/mcp/admin. Locally that is http://localhost:3000/mcp/user and http://localhost:3001/mcp/admin.

Connecting

Copy the server address into your agent's remote MCP settings and choose OAuth. The client must support Streamable HTTP, public dynamic client registration, the authorization code flow and PKCE S256.

The agent is sent to /oauth/authorize on the same origin, which redirects to the consent page at /mcp/authorize?request_id=.... If you aren't signed in, you go to /login?next=... on that origin and return to the same request; for the admin server that is the admin sign-in page with email and password. The consent page shows the client name (marked as unverified, because the client supplies it), the callback address, the server and the permission. For a user connection you pick one workspace; if you have none, create or join one first. Choose Allow read access or Cancel.

Ask the agent to list projects in that workspace to confirm it works. An empty list is valid until you add a project.

Tools and permissions

Scopes are exactly user:read for the user server and admin:read for the admin server. Every tool is registered with readOnlyHint: true. List tools accept a limit between 1 and 50, default 20.

ServerToolScopeWhat it returns
Useraccount_contextuser:readThe signed-in account and the approved workspace
Userprojects_listuser:readProjects in the approved workspace: id, name, slug, description, status, updated_at
Adminadmin_users_listadmin:readUsers across the app: id, name, email, role
Adminadmin_workspaces_listadmin:readWorkspaces across the app: id, name, slug

Permission is re-checked on every call. liveActor in apps/web/src/mcp/adapter.ts reloads the session and requires that it hasn't expired, isn't an impersonation session, and that the user is verified and not banned. projects_list filters by the granted workspace and re-checks membership in SQL. Admin tools re-check users.role = 'admin' before every query. A user token is rejected at the admin endpoint and the other way round, and because the two servers live on different origins their tokens are also bound to different resources.

Being an administrator doesn't widen a user connection to other workspaces. Connect to the admin server on the admin origin separately when you need application-wide data.

OAuth details specific to this kit

  • Issuer: <NEXT_PUBLIC_APP_URL>/oauth of the app you connect to, with metadata at /.well-known/oauth-authorization-server/oauth on that origin. The web app advertises only user:read and the admin app only admin:read in scopes_supported.
  • Resource metadata: /.well-known/oauth-protected-resource/mcp/user on the web origin and /.well-known/oauth-protected-resource/mcp/admin on the admin origin.
  • Endpoints on each origin: /oauth/register, /oauth/authorize, /oauth/token, /oauth/revoke.
  • Public clients only (token_endpoint_auth_method=none). Confidential clients and client ID metadata documents aren't supported.
  • Callback URLs must be HTTPS, except HTTP loopback callbacks on localhost, 127.0.0.1 or ::1.
  • Lifetimes: pending consent 10 minutes, authorization codes 5 minutes, access tokens 15 minutes, refresh up to 30 days but never longer than the sign-in session that approved the connection.
  • Refresh tokens rotate. Reusing an old one revokes the whole connection.
  • Up to 20 active connections per account.
  • Browser requests are accepted from the serving app's own origin, https://claude.ai and https://chatgpt.com. Desktop and server clients without an Origin header are accepted too.
  • Opening /mcp/user on the web origin or /mcp/admin on the admin origin without a token returns a 401 with a WWW-Authenticate challenge pointing at the resource metadata. That is expected.

Signing out, disconnecting, losing workspace membership, being banned or losing the admin role ends delegated access. Reconnect from a new sign-in when you need to.

Where the code lives

The implementation is in the web app and shared with the admin app through its @/* alias, which falls back to apps/web/src.

PathRole
apps/web/src/mcp/core.tsProtocol, OAuth, consent and connection pages, audience isolation, the Adapter interface and the tool registry
apps/web/src/mcp/adapter.tshandleMcp(request, audience) and the Postgres adapter: parameterized SQL on the shared pool, session revalidation, workspace lookups
apps/web/src/mcp/pg-store.tsCredential storage in the mcp_records table with compare-and-set updates
apps/web/src/app/mcp/[...path]/route.ts, apps/web/src/app/oauth/[...path]/route.ts and apps/web/src/app/.well-known/Mount the handler for the user audience on the web origin
apps/admin/src/app/mcp/[...path]/route.ts, apps/admin/src/app/oauth/[...path]/route.ts and apps/admin/src/app/.well-known/Mount the same handler with the admin audience on the admin origin
apps/web/src/components/sidebar-platform-nav.tsx and apps/admin/src/components/sidebar-admin-panel-nav.tsxThe sidebar links to the two connection pages
apps/web/src/mcp/core.test.tsVitest suite with an in-memory store covering consent, CSRF, PKCE, replay, expiry, membership loss and demotion
apps/web/src/mcp/pg-store.integration.test.tsStore test against a loopback Postgres given by MCP_TEST_PG_URL
apps/database/drizzle-postgres/0002_*.sql and 0003_*.sqlThe mcp_records table, its indexes and the guarded grant to an app_runtime role
docs/mcp-connections.mdDesign notes in the repository

Secrets in mcp_records are stored as SHA-256 hashes. Expired rows are cleaned up in bounded batches as traffic arrives, so there is no always-on poller.

Adding a tool safely

The repository ships an mcp-tool skill in .agents/skills/mcp-tool/SKILL.md that walks a coding agent through this. The short version:

  1. Add a method to the Adapter interface in core.ts. User tools take the actor and the granted workspace; admin tools take the actor only.
  2. Implement it in adapter.ts. User queries filter by workspace.id and re-check membership in SQL, like projects does. Admin queries start by confirming liveActor(actor)?.admin. Select explicit columns, never secrets or mcp_records, and bound results with a limit.
  3. Register it with server.registerTool inside the right audience branch only, with a zod inputSchema and the shared annotations. Use limitSchema for list tools. Name user tools <noun>_<verb> and prefix admin tools with admin_. Admin tools are served by the admin app automatically, because its routes pass the admin audience to the same handler.
  4. Extend core.test.ts: the fake adapter, the tools/list expectations, a tools/call test, and negative tests for the wrong audience, invalid input, membership loss and admin demotion.
  5. Add the tool to the table in docs/mcp-connections.md.

Write tools are a design change, not a new method. They need a new scope, consent copy, readOnlyHint: false, the existing policy asserts from apps/web/src/lib/domain/, and audit and outbox rows via domainEventValues. Keep the authenticated identity on the server: a workspaceId argument from the client is never proof of access.

Verify with pnpm --filter web exec vitest run src/mcp/core.test.ts, then pnpm lint && pnpm typecheck && pnpm test. For a manual check, run pnpm local:start and connect an OAuth MCP client to http://localhost:3000/mcp/user; for the admin server, also run pnpm --filter admin dev and connect to http://localhost:3001/mcp/admin.

Authentication

How Better Auth is configured in the Next.js + Neon starter, how sessions, the separate admin app and impersonation work, and what protects your data.

Project structure

The Next.js + Neon monorepo at a glance and where to make changes for routes, the admin app, data, schema, auth, emails, jobs, MCP and tests.

On this page

Two connections on two originsConnectingTools and permissionsOAuth details specific to this kitWhere the code livesAdding a tool safely