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.
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:
- Two different people add the same MCP server URL to their clients.
- Each completes sign-in on first connect (no secrets in client config).
- Each calls
save_noteandlist_my_notes. - 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 server | Stays with each person |
|---|---|
| MCP server URL you share | Sign-in through Scalekit |
SCALEKIT_CLIENT_SECRET and service credentials in .env | Bearer token on each request |
| Your tool logic and data rules | Identity (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
- HTTP transport: Shared MCP servers use Streamable HTTP, not stdio. See installation.
- Install the plugin:
pnpm add @xmcp-dev/scalekit(or npm/yarn/bun). Full API: Scalekit integration. - Environment variables: Server
.envonly:
- Middleware: Export
scalekitProviderfromsrc/middleware.ts. MatchBASE_URLto the URL you registered in Scalekit. - Scope by
userId: Key reads and writes ongetSession().userId, not shared env credentials in the client.
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:
| Check | Stops |
|---|---|
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.
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:
- Alice signs in, saves a note, lists notes, and sees only hers.
- Bob uses the same URL with a different account and does not see Alice's note.
- (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
| Symptom | Fix |
|---|---|
401 on every tool call | Finish the browser OAuth flow; reconnect the client |
Session not initialized | Confirm src/middleware.ts exports scalekitProvider |
| DCR / client registration error | Set SCALEKIT_RESOURCE_ID; enable dynamic client registration |
| Token validation fails | Align BASE_URL, Scalekit server URL, and client URL |
| Empty sign-in screen | Enable at least one auth method in the Scalekit dashboard |
| Both users see the same data | Scope 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
localhostfor 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.