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

Agent connections

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

This kit exposes two independent remote MCP servers, one per Worker: the web Worker serves the User MCP and the admin Worker serves the Admin MCP. Both use your existing Better Auth accounts and both are read-only in v5. The code ships on main in apps/web/src/mcp/, with the mcp_records table in migration 0025_user_admin_mcp.sql. Read user and admin MCPs for the concepts and connect an agent for the client-side steps.

Where users manage connections

ConnectionSettings pageLive demo server
User/mcp/connections/user on the web apphttps://tanstack-d1.usenextbase.com/mcp/user
Admin/mcp/connections/admin on the admin apphttps://tanstack-d1-admin.usenextbase.com/mcp/admin

"Agent connections" is the first item in the signed-in navigation of the web app and is also linked from /user/settings. Administrators open the admin origin for the Admin MCP. Each page shows the server address to copy, lists that account's connections for that server, and offers Disconnect.

Your own deployment uses its own origins: the User MCP is the web Worker's SITE_URL + /mcp/user and the Admin MCP is the admin Worker's SITE_URL + /mcp/admin. The settings pages print the exact value.

One audience per origin

apps/web/src/mcp/adapter.ts serves only the user audience when ADMIN_APP is unset and only the admin audience when ADMIN_APP is true, which is how the admin Worker is configured. Requesting the other audience returns 404: https://tanstack-d1.usenextbase.com/mcp/admin is a 404, while https://tanstack-d1-admin.usenextbase.com/mcp/admin answers with the expected 401 challenge.

Tools and permissions

ServerToolScopeWhat it returns
Useraccount_contextuser:readThe signed-in user's ID and email and the workspace approved for this connection.
Userprojects_listuser:readProjects in the approved workspace only, newest first. Optional limit from 1 to 50, default 20.
Adminadmin_users_listadmin:readA bounded list of users (id, name, email, role). Optional limit, same bounds.
Adminadmin_workspaces_listadmin:readA bounded list of workspaces that are not being deleted. Optional limit, same bounds.

Every tool is annotated read-only, non-destructive and idempotent. There is no projects_create in this kit. A user connection selects exactly one workspace; being an application administrator does not widen it, and a user token is rejected at the admin endpoint.

Before answering, each tool call reloads the live session: the session must exist and not be expired, must not be an impersonation session, and the user must be verified and not banned. projects_list additionally resolves the workspace through requireWorkspaceContext and checks workspace.read. The admin tools re-check role = 'admin' and ADMIN_APP === "true" on every call, so removing the role ends access immediately.

OAuth behaviour in this kit

Both Workers handle /mcp/*, /oauth/* and /.well-known/oauth-* before React Router sees the request. Each origin is its own authorization server; a token issued by one is not valid at the other.

  • Discovery: /.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 that Worker's SITE_URL + /oauth.
  • Endpoints: /oauth/register, /oauth/authorize, /oauth/token, /oauth/revoke, and the consent page at /mcp/authorize.
  • Public clients only, with dynamic client registration, authorization code and mandatory PKCE S256. Confidential clients and client ID metadata documents are not supported.
  • Callback URLs must be HTTPS, except HTTP loopback on localhost, 127.0.0.1 or ::1.
  • If you are not signed in, consent redirects to /login?next=/mcp/authorize?... on the same origin. Login, sign-up and email verification preserve that return path and load the consent page as a full document. On the admin origin that is the admin login page.
  • The consent screen shows the client name (marked unverified), callback address, server, permission and your email, and lets you pick a workspace for user access. If you have no workspace it asks you to create one first.
  • Pending consent lasts 10 minutes, codes 5 minutes, access tokens 15 minutes. A connection can refresh for up to 30 days but never longer than the sign-in session that approved it. Refresh tokens rotate and reuse revokes the whole family.
  • Browser origins allowed: that Worker's SITE_URL, https://claude.ai and https://chatgpt.com. Desktop and server clients without an Origin header are accepted.
  • Each account can hold up to 20 active connections. Consent and disconnect need a same-origin form post with a session-bound CSRF value.

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

Storage

OAuth registrations, pending consents and delegated credentials live in one additive table, mcp_records, created by apps/database/migrations/0025_user_admin_mcp.sql and mirrored in apps/database/src/schema/core.ts. Both Workers read and write the same table through the shared APP_DB. Secrets are stored only as SHA-256 hashes. pnpm db:migrate applies the migration locally and pnpm db:migrate:preview or pnpm db:migrate:production remotely.

Where the code lives

FileRole
apps/web/src/mcp/core.tsProvider-neutral protocol: discovery, OAuth endpoints, consent HTML, connection pages, token lifecycle and the MCP server with its tools. isMcpPath decides which paths it owns and the adapter's audiences decide which server an origin serves.
apps/web/src/mcp/adapter.tsThe D1 and Better Auth adapter: liveActor, workspaces, projects, adminUsers, adminWorkspaces and the native session lookup. Picks the audience from ADMIN_APP.
apps/web/src/mcp/d1-store.tsCompare-and-set storage over mcp_records.
apps/web/workers/app.tsRoutes /mcp/*, /oauth/* and /.well-known/oauth-* to handleMcp on the web Worker.
apps/admin/workers/app.tsImports the same handleMcp and isMcpPath so the admin Worker serves the Admin MCP.
apps/web/src/mcp/core.test.tsProtocol, PKCE, CSRF, replay, concurrency, expiry, membership and revocation tests.
apps/web/test/mcp.integration.test.tsReal D1 and Better Auth sign-up, login and delegated user and admin flows.
docs/mcp-connections.mdThe repository's own write-up of the design.

Adding a tool safely

  1. Register it in core.ts next to the existing server.registerTool calls for the right audience. User tokens never see admin tools and the reverse, so keep the audience split.
  2. Put the data access in adapter.ts and go through the same checks the website uses: requireWorkspaceContext and authorizeWorkspace for workspace data, a fresh liveActor admin check plus the ADMIN_APP guard for admin data.
  3. Treat tool arguments such as a workspace or user ID as untrusted. The authorized workspace comes from the grant, not from the agent.
  4. Keep results bounded. Reuse the limit schema (1 to 50).
  5. A write tool needs a new scope, consent wording that says so, and tests in core.test.ts and mcp.integration.test.ts before you rely on it. The shared tools and permissions page explains the scope model.

If a connection stops working after you sign out, sign in again on the same origin and reconnect from the settings page. See troubleshooting for the other common cases.

Authentication

How Better Auth, platform roles, the separate admin app and workspace capabilities protect the TanStack + Cloudflare D1 starter.

Project structure

The monorepo layout of the TanStack + Cloudflare D1 starter and where to make common changes.

On this page

Where users manage connectionsTools and permissionsOAuth behaviour in this kitStorageWhere the code livesAdding a tool safely