Roll out to a team

Roll out secure team access to your xmcp server using Scalekit OAuth 2.1. Understand the team-notes pattern, wire it into your project, and verify per-user isolation.

For the complete documentation index, see llms.txt. Markdown variants of every page are available by appending .md to the URL.

You built an MCP server your team wants in their own MCP client, like Cursor, Claude Code, or others. The fastest way to share it is also the riskiest: drop API_KEY, database URLs, and service tokens into a shared mcp.json, commit it, and paste the same block into every laptop.

It only takes one person rotating a key, one onboarding call, or the realization that every tool runs as a shared service account before problems surface. Audit logs mix up who did what, and a single leaked config can expose credentials.

Per-user OAuth on the server fixes this. xmcp runs auth in middleware before tools execute; the Scalekit plugin handles sign-in, token validation, and session identity. You share one MCP URL. Each user signs in individually. Secrets stay in server .env, not in client config.

By the end of this guide, you will know how two people connect to the same MCP server URL, sign in individually, and each see only their own notes.

How team access works on one URL

The pattern below uses a small team notes MCP server as the example. You deploy it once and share a single HTTP endpoint (for example https://notes.internal.example/mcp).

Each user adds only that URL to Cursor, Claude Code, or another MCP client. On the first tool call, the client opens browser sign-in. No API keys or service tokens go into client config.

After sign-in, the server knows who is calling. Tools such as save_note and list_my_notes read identity from the session and scope data to that person. Alice and Bob use the same URL, but each works in a private notes space. Call whoami when you need to inspect userId (JWT sub) during rollout.

Not limited to one internal team

The same pattern works when you publish one MCP server URL broadly. Different B2B customers can each connect their own teams through Scalekit sign-in, with identity and data scoped per user (and per organization when you need it).

Four checks before you share the URL

A multi-user rollout succeeds when all of this is true:

  1. Two different people add the same MCP server URL to their clients.
  2. Each completes sign-in on first connect (no secrets in client config).
  3. Each calls save_note and list_my_notes.
  4. Each person sees only their notes.

Per-user OAuth moves the trust boundary to your xmcp server

Shared credentials put the trust boundary in the MCP client. Per-user OAuth moves it to the server.

Stays on the serverStays with each person
MCP server URL you shareSign-in through Scalekit
SCALEKIT_CLIENT_SECRET and service credentials in .envBearer token on each request
Your tool logic and data rulesIdentity (userId) and optional authorization (permissions) in the token

xmcp exposes middleware so auth runs before tools execute. The @xmcp-dev/scalekit plugin handles OAuth discovery, token validation, and getSession() inside tools. Users add one URL; the server decides who they are.

If you would rather wire OAuth yourself, you can, but you still need discovery endpoints, dynamic client registration, PKCE, and JWT validation on every /mcp request. The authentication guide shows what MCP clients expect.

Wire per-user auth into your xmcp server

Bring the team-notes shape into your project.

Wire Scalekit middleware

  1. HTTP transport: Shared MCP servers use Streamable HTTP, not stdio. See installation.
  2. Install the plugin: pnpm add @xmcp-dev/scalekit (or npm/yarn/bun). Full API: Scalekit integration.
  3. Environment variables: Server .env only:
  1. Middleware: Export scalekitProvider from src/middleware.ts. Match BASE_URL to the URL you registered in Scalekit.
  2. Scope by userId: Key reads and writes on getSession().userId, not shared env credentials in the client.
Keep secrets on the server

Never put SCALEKIT_CLIENT_SECRET or other service credentials in Cursor, Claude, or any MCP client config. Users connect with the server URL only.

Configure sign-in for users

Enable the authentication methods your team will use in the Scalekit dashboard. Scalekit supports social logins, passwordless (OTP and magic links), Enterprise SSO, and bring your own auth.

Share the URL

Each user adds only the MCP server URL to their client. For Cursor, Claude Desktop, and other clients, see connecting to your server. For production, replace localhost with your deployed address.

When identity is not enough: RBAC

userId scoping answers whose data is this? It does not answer should this person use this tool? That second question is authorization.

A viewer might list their own notes but must not call save_note. Scalekit can put roles and permissions in the access token your middleware already validates:

In tool code, check permissions before mutating state:

Gate before write
CheckStops
listNotes(session.userId)Bob reading Alice's rows
hasPermission(session, "notes:write")Carol saving when she is read-only
hasPermission(session, "notes:read")Dave using note tools at all

Define roles in Scalekit (create roles and permissions), assign them to members, and enforce authorization in tools. No extra API call per request.

OAuth scopes vs RBAC permissions

session.scopes (openid, profile, email) identify the user. RBAC permissions (notes:write) control what tools they may run. Use both layers when you need isolation and policy.

Try it out

Bootstrap the Scalekit Authentication template and run the checks below to verify per-user isolation end-to-end. When the wizard prompts you to choose a transport, select http.

Try per-user isolation

Run these checks against the template or your deployment:

  1. Alice signs in, saves a note, lists notes, and sees only hers.
  2. Bob uses the same URL with a different account and does not see Alice's note.
  3. (Optional) A read-only role can list but cannot save.

Success means per-user identity works on one shared URL. A permission error on step 3 means authorization is doing its job.

Troubleshooting

SymptomFix
401 on every tool callFinish the browser OAuth flow; reconnect the client
Session not initializedConfirm src/middleware.ts exports scalekitProvider
DCR / client registration errorSet SCALEKIT_RESOURCE_ID; enable dynamic client registration
Token validation failsAlign BASE_URL, Scalekit server URL, and client URL
Empty sign-in screenEnable at least one auth method in the Scalekit dashboard
Both users see the same dataScope storage with getSession().userId, not shared client env vars

More plugin errors: Scalekit integration troubleshooting.

What to do next

  • Production: Deploy the server and swap localhost for your public URL. See deployment.
  • OAuth internals: Authentication guide for discovery, PKCE, and token lifecycle.
  • Plugin reference: Scalekit integration for getClient(), scopes, and advanced configuration.

On this page

One framework to rule them all