Agent connections
The User MCP and Admin MCP of the Next.js + Convex starter: endpoints, tools, OAuth consent and how to add a tool.
This kit serves two MCP servers from the Convex backend, not from either Next.js app. Each is its own convex-mcp-gateway component instance (mcpUser and mcpAdmin in apps/database/convex/convex.config.ts) with its own tool registry and OAuth resource. Read user and admin MCPs for the shared concepts and connect an agent for the client-side steps.
Where users manage connections
| Connection | Settings page | Live demo settings | Live demo server |
|---|---|---|---|
| User | /user/settings/connections in the web app | https://nextjs-convex.usenextbase.com/user/settings/connections | https://dynamic-grasshopper-80.convex.site/mcp/user |
| Admin | /app-admin/connections in the admin app | https://nextjs-convex-admin.usenextbase.com/app-admin/connections | https://dynamic-grasshopper-80.convex.site/mcp/admin |
Both pages render McpConnections (one copy in each app's src/components/) and read api.mcp.connections.settings, which returns the exact endpoint for the current deployment, the account's connections and recent activity. Revoke a connection from the same page. The admin page requires a current platformRoles admin role and lives on the admin origin, not in apps/web.
Your own deployment has its own address: CONVEX_SITE_URL followed by /mcp/user or /mcp/admin. Neither app origin is the MCP address. Copy the URL shown in connection settings rather than guessing it.
Endpoints on the backend
All of these live on the .convex.site origin and are mounted by mountMcp in apps/database/convex/mcp/http.ts:
| Endpoint | Purpose |
|---|---|
/mcp/user, /mcp/admin | Streamable HTTP MCP servers (POST, GET, DELETE, OPTIONS) |
/.well-known/oauth-protected-resource/mcp/user and .../mcp/admin | Resource metadata, pointing at the issuer |
/.well-known/oauth-authorization-server/oauth | Issuer metadata (the issuer is CONVEX_SITE_URL/oauth) |
/oauth/register | Dynamic public-client registration |
/oauth/authorize | Starts a consent request and redirects to the consent page of the web app (user) or the admin app (admin) |
/oauth/token | Authorization code and refresh token exchange |
/oauth/revoke | Token revocation |
Opening an MCP URL without a token returns an authentication challenge. That is expected.
Tools and permissions
From apps/database/convex/mcp/tools.ts:
| Server | Tool | Scope | Access | What it does |
|---|---|---|---|---|
| User | account_context | user:read | Read | Signed-in account, the one authorized workspace and the granted scopes |
| User | projects_list | user:read | Read | Active projects in the authorized workspace, paginated with cursor and limit (1 to 50, default 20) |
| User | projects_create | user:write | Write | Creates a project through createProjectForActor, the same helper the website uses. Needs at least the member role |
| Admin | admin_users_list | admin:read | Read | User profiles (id, email, name, disabled and deleted flags). Never credentials or sessions |
| Admin | admin_workspaces_list | admin:read | Read | Workspaces (id, name, slug) |
The gateway only lets a tool run when the token's scopes include the tool's metadata.scope. Each handler then calls verifiedCaller, which rechecks on every call that the connection is unrevoked and unexpired, that the approving Convex Auth session still exists, that the account is active, and that the user still has the required workspace role or admin role. A token issued for /mcp/user is rejected by /mcp/admin and the other way round, even for an administrator.
The caller argument is injected by the gateway and hidden from tool schemas; clients cannot supply it. Successful write calls are recorded in mcpActivity.
Consent behaviour in this kit
- The agent registers itself at
/oauth/register(public client, PKCE S256,authorization_codeandrefresh_tokengrants only) and requests/oauth/authorizewith the exact resource URL. - The backend stores a pending request (valid 10 minutes) and redirects to
/mcp/authorize?request_id=...on the consent origin. For the user server that origin isMCP_APP_URL(falling back toAPP_URL, thenSITE_URL), which is the web app. For the admin server it isADMIN_SITE_URLfirst, so administrator consent happens in the admin app. If you are not signed in, the consent page sends you to that app's/login?next=/mcp/authorize?...and the middleware brings you back. - The consent page (
src/components/mcp-consent.tsxin each app) shows the client name as unverified, the callback URL and the requested scopes. For the user server you pick one workspace;user:write(create projects) is an optional checkbox and is reset when you change workspaces. The admin server offers onlyadmin:readand shows an error if you are not an administrator. If an admin request ever reaches the web app's consent page, it shows a link that continues the request in the admin app atNEXT_PUBLIC_ADMIN_APP_URL. - Approval creates an
mcpConnectionsrow bound to your user and session, valid for 30 days, and redirects to the client with a one-time code (5 minutes). Each account can hold 20 active connections. - Access tokens (
nb_mcp_...) last 15 minutes. Refresh tokens rotate once; reusing one revokes the connection. Raw codes and tokens are stored only as hashes.
Browser clients are accepted from MCP_APP_URL (or its APP_URL and SITE_URL fallbacks), https://claude.ai and https://chatgpt.com. Desktop and server clients without an Origin header also work. Confidential clients and client ID metadata documents are not supported. An hourly cron (convex/crons.ts) prunes expired credentials and 90-day-old activity.
For the end-to-end walkthrough with an agent client, follow connect an agent. For the tool catalogue across kits, see tools and permissions.
Where the code lives
| File | Role |
|---|---|
apps/database/convex/mcp/tools.ts | Tool handlers and the userTools and adminTools registries |
apps/database/convex/mcp/http.ts | Mounts both gateways, OAuth routes and discovery documents |
apps/database/convex/mcp/oauth.ts | Registration, authorize, exchange and revoke mutations |
apps/database/convex/mcp/connections.ts | verifiedCaller, record, consent and the settings query |
apps/database/convex/mcp/actor.ts | Shared read helpers (listProjects, listUsers, listWorkspaces) |
apps/database/convex/mcp/protocol.ts | Scopes, resource and issuer URLs, PKCE and hashing |
apps/database/convex/mcp/schema.ts | mcpClients, mcpRequests, mcpConnections, mcpCodes, mcpTokens, mcpActivity |
apps/database/convex/mcp/cleanup.ts | The hourly prune |
apps/database/convex/mcp.test.ts | Scope, consent, replay, audience and role tests |
apps/web/src/app/mcp/authorize/ and apps/web/src/components/mcp-consent.tsx | User consent page |
apps/admin/src/app/mcp/authorize/ and apps/admin/src/components/mcp-consent.tsx | Admin consent page |
apps/web/src/components/mcp-connections.tsx, apps/admin/src/components/mcp-connections.tsx | Settings pages |
docs/mcp-connections.md | The repository's own reference |
Adding a tool safely
The repository ships an mcp-tool skill in .agents/skills/mcp-tool/SKILL.md that walks through this. In short:
- Write the handler in
mcp/tools.tsas aninternalQueryorinternalMutation, never a public function. The first line isverifiedCaller(ctx, caller, "user", "user:read")for reads, orverifiedCaller(ctx, caller, "user", "user:write", true)for writes (thetruerequires at leastmember). Admin tools use"admin", "admin:read". - Scope data to
grant.workspaceId. ReusepageArgsandbounded(limit)for lists, validate free-text sizes, and call the same exported helper the website uses for writes, thenawait record(ctx, grant, "<tool_name>"). - Register it in
userToolsoradminToolswithdefineMcpQueryordefineMcpMutation,identityArg: "caller"and ametadata.scopethat matches the handler's check. - Add tests in
mcp.test.tsfor the happy path, the missing scope, the downgraded role, cross-workspace access and a forged caller. Runpnpm --filter database testandpnpm --filter database typecheck. - Update
docs/mcp-connections.md. A new scope also needsscopesForinprotocol.ts,scopes_supportedinhttp.tsand the labels in both copies ofmcp-consent.tsx.
Never publish MCP test fixtures or synthetic identity helpers as public Convex functions.
Authentication
How Convex Auth works in the Next.js + Convex starter: providers, sessions, the admin app and role, and how functions are protected.
Project structure
The Next.js + Convex monorepo layout and where to make changes to routes, data, schema, auth, the admin app, emails, jobs, MCP and tests.