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

Agent connections

The User and Admin MCP servers in the Next.js + Cloudflare D1 starter: endpoints, tools, OAuth behaviour and where the code lives.

This kit ships two remote MCP servers. The web Worker serves the User MCP and the admin Worker serves the Admin MCP. Both use the existing Better Auth login, store their OAuth records in D1 and are read-only in v5. The shared pages cover the general flow: user and admin MCPs, connect an agent and tools and permissions.

The mcp_records migration (apps/database/migrations/0010_famous_groot.sql) is part of the normal migration set and must be applied before the /mcp routes serve traffic. pnpm db:migrate applies it locally.

Endpoints

ConnectionServed bySettings pageServer URLLive demo
Userweb Worker (apps/web)<web origin>/mcp/connections/user<web origin>/mcp/userhttps://nextjs-d1.usenextbase.com/mcp/user
Adminadmin Worker (apps/admin)<admin origin>/mcp/connections/admin<admin origin>/mcp/adminhttps://nextjs-d1-admin.usenextbase.com/mcp/admin

Each origin is that Worker's NEXT_PUBLIC_SITE_URL: http://localhost:3000 for the web app and http://localhost:3001 for the admin app during development. The web sidebar has an Agent connections link for every user, and the admin sidebar has an Admin agent connections link. Both open in a new tab because the connection pages are rendered by the MCP handler, not by the Next.js app shell.

The same handler runs in both Workers. ADMIN_APP=true makes it register only the admin audience, so the web origin never answers /mcp/admin and the admin origin never answers /mcp/user.

Opening a server URL in a browser without a token returns a 401 with a WWW-Authenticate challenge pointing at the resource metadata. That is expected.

Tools

ServerToolScopeWhat it returns
Useraccount_contextuser:readThe signed-in user's ID and email plus the approved workspace
Userprojects_listuser:readProjects in the approved workspace only, newest first
Adminadmin_users_listadmin:readID, name, email and role for users across the app
Adminadmin_workspaces_listadmin:readID, name and slug for workspaces across the app

List tools accept a limit between 1 and 50, defaulting to 20. Every tool is annotated read-only, non-destructive and idempotent. There are no write tools in this kit; projects_create exists only in the Convex kits.

Each call reloads the live actor from D1: the session must still exist and be unexpired, the user must be verified and not banned, and the session must not be an impersonation session. projects_list additionally re-checks the caller's membership and projects.read permission in the approved workspace. The admin tools re-check user.role = 'admin' on every request and refuse to run unless ADMIN_APP is true, so removing the role ends access immediately.

Consent and workspace selection

The consent page lists only workspaces where the account has projects.read. If there are none, the page says to create or join a workspace first and offers no approve button. Administrators who connect to the User MCP get a normal user connection; their token doesn't work at the admin endpoint, which is on a different origin anyway.

Consent and disconnect forms require same-origin submission and a session-bound CSRF value. The consent page disallows framing. Client names are self-reported by the client and are marked unverified; check the callback address and the server shown on the page.

Each account can have at most 20 active connections. Disconnect from the settings page when you no longer need one.

OAuth details for this kit

  • Discovery, per origin: /.well-known/oauth-protected-resource/mcp/user on the web origin, /.well-known/oauth-protected-resource/mcp/admin on the admin origin, and /.well-known/oauth-authorization-server/oauth on both. The issuer is <origin>/oauth of the Worker you connect to.
  • Endpoints on each origin: /oauth/register, /oauth/authorize, /oauth/token, /oauth/revoke. Consent is at /mcp/authorize.
  • Public clients with dynamic registration, authorization code, mandatory PKCE S256, exact callback and resource matching. Confidential clients and client ID metadata documents are not supported.
  • Callback URLs must be HTTPS, except HTTP loopback on localhost, 127.0.0.1 and ::1.
  • Lifetimes: pending consent 10 minutes, authorization codes 5 minutes, access tokens 15 minutes, refresh up to 30 days but never longer than the native session that approved the connection.
  • Refresh tokens rotate. Reusing an old one revokes the whole connection family.
  • Browser origins allowed: that Worker's NEXT_PUBLIC_SITE_URL, https://claude.ai and https://chatgpt.com. Clients without an Origin header are accepted.

If a user isn't signed in when consent opens, they are sent to /login?next=/mcp/authorize?request_id=... on the same origin and returned to the same request after signing in. Both login pages do a full-document navigation for /mcp/ destinations so the consent page loads outside the app shell.

Where the code lives

core.ts
adapter.ts
d1-store.ts
core.test.ts
adapter.integration.test.ts
  • core.ts is the provider-agnostic protocol: OAuth endpoints, consent HTML, token handling, rate limits, the connection pages and tool registration with the official @modelcontextprotocol/sdk (Streamable HTTP, stateless JSON responses).
  • adapter.ts plugs in this kit: it resolves the native Better Auth session, loads the live actor and workspaces from D1 with the shared can() permission check, picks the audience from ADMIN_APP, and implements the four tool queries.
  • d1-store.ts stores registrations, pending consents and credentials in the mcp_records table using insert-or-nothing and compare-and-set updates, so concurrent consent, code and refresh transitions can't be duplicated.
  • The route files in both apps export the same handleMcp handler for GET, POST, DELETE and OPTIONS. The admin app has no copy of the MCP code; its tsconfig.json maps @/* to ./src/* and then ../web/src/*, so it imports apps/web/src/mcp/adapter.ts directly.

Adding a tool safely

  1. Add the data access in adapter.ts and keep the permission check inside it. Load the actor with liveActor and check the workspace membership or admin role in the query itself, as projects and adminUsers do. Admin queries should also require env.ADMIN_APP === "true".
  2. Register the tool in core.ts inside the correct audience branch. User tokens never see admin tools and vice versa.
  3. Treat tool arguments as untrusted. A workspaceId in the input is not proof of access; use auth.grant.workspace for user tools.
  4. If the tool writes data, don't attach it to user:read or admin:read. Introduce an explicit write scope, add consent wording for it and keep the read-only annotations accurate.
  5. Add cases to core.test.ts and adapter.integration.test.ts. The integration test creates isolated D1 databases with Miniflare and exercises real Better Auth sign-up and login.
  6. Run pnpm verify:local before deploying, and apply any new migration with pnpm db:migrate:production before shipping the Workers.

Authentication

How Better Auth is configured in the Next.js + Cloudflare D1 starter, how admins are assigned, where the admin app lives, and what protects application data.

Project structure

The five apps in the Next.js + Cloudflare D1 monorepo, the key config files, and where to make each kind of change.

On this page

EndpointsToolsConsent and workspace selectionOAuth details for this kitWhere the code livesAdding a tool safely