MCP Connect Auth for MCP servers
MCP Connect Auth lets you manage an MCP server listing on MCPBundles and offer standards-compliant MCP client OAuth without building your own authorization server. MCPBundles provisions a tenant authorization server per listing slug; your upstream JSON-RPC stays unchanged. Public directory publish to MCPBundles Skills is optional — you can connect clients via your vendor origin (Path A) or the MCPBundles bundle URL (Path B) without listing on /skills.
Overview
| Role | Who serves it | URL pattern |
|---|---|---|
| Authorization server (AS) | MCPBundles (per listing) | Tenant AS under Connect Auth API |
| Resource server (RS) | Vendor origin or MCPBundles bundle proxy | Your MCP base URL or https://mcp.mcpbundles.com/bundle/{slug}/ |
Two connect paths share one tenant AS:
| Path | Vendor MCP code | Client connects to | Best for |
|---|---|---|---|
| Path B (pilot default) | Federation only — no pip/npm on MCP server | https://mcp.mcpbundles.com/bundle/{slug}/ | Fastest setup; full proxy telemetry |
| Path A | mcpbundles-mcp-connect / @mcpbundles/mcp-connect-auth on origin | Your vendor MCP URL | Custom hosting; origin client telemetry |
Traffic diagram (Path B):
sequenceDiagram
participant Client as MCP client
participant Bundle as mcp.mcpbundles.com/bundle
participant AS as Connect Auth tenant AS
participant App as Your app (federation)
Client->>Bundle: MCP initialize / tools
Client->>AS: OAuth DCR + PKCE
Client->>AS: Authorize (resource = bundle URL)
AS->>App: Federation sign-in
App->>AS: federation/complete
AS->>Client: JWT access token
Client->>Bundle: Bearer JWT
Quickstart (~30 min)
Prerequisites
- MCP server listing on MCPBundles (MCP protocol, OAuth-capable upstream)
- MCP Connect Auth enabled in maintainer settings (at create time or on an existing listing; public
/skillspublish is optional) - Federation sign-in URL configured in maintainer Connect Auth settings
- Federation secret in your app environment (server-side only)
Path B — bundle URL only
- Enable MCP Connect Auth in the Connect Auth section of your listing maintainer dashboard (open dashboard)
- Configure federation URL + secret there
- Point MCP clients at
https://mcp.mcpbundles.com/bundle/{slug}/ - Smoke locally:
mcpbundles connect doctor --listing {slug} --env staging --surface bundle
No pip install on your MCP server.
Path A — vendor origin
from mcpbundles_mcp_connect import mcpbundles_fastmcp
mcp = mcpbundles_fastmcp(
"My Server",
listing_slug="{slug}",
base_url="https://mcp.example.com",
)
Or explicitly:
from fastmcp import FastMCP
from mcpbundles_mcp_connect.fastmcp import McpbundlesConnectProvider
provider = McpbundlesConnectProvider(listing_slug="{slug}", base_url="https://mcp.example.com")
mcp = FastMCP("My Server", auth=provider, middleware=provider.default_middleware())
Run doctor with origin surface:
mcpbundles connect doctor --listing {slug} --surface origin --base-url https://mcp.example.com
Protected resource metadata (RFC 9728)
- Bundle URL: MCPBundles serves protected-resource metadata on
mcp.mcpbundles.comfor/bundle/{slug}/ - Origin: Your server (via Connect Auth client library) serves metadata pointing at the tenant AS
- Clients discover the tenant AS from
authorization_serversin protected-resource JSON
Alternate well-known paths are supported on the bundle host (see platform routers.py patterns).
Federation
When an MCP client starts OAuth against the tenant AS, MCPBundles redirects the user to your sign-in URL. After authentication, your backend calls:
POST /connect-auth/tenants/{slug}/v1/federation/complete
with the federation secret, state, and subject (and optional organization_id, email, roles).
The federation secret is shown once on create/rotate — store it in your app secrets, never in client code.
Use the client libraries when possible:
- Python:
complete_federation()frommcpbundles-mcp-connect - Node:
completeFederation()from@mcpbundles/mcp-connect-auth
What vendors do not implement
MCPBundles operates the tenant authorization server. You do not need to run DCR, authorize, or token endpoints on your origin for Path B. For Path A, the Connect Auth provider forwards AS metadata to the tenant AS — you still do not operate DCR yourself.
Access tokens
Connect Auth access tokens are ES256 JWTs issued by the tenant AS. These are MCP client OAuth tokens, not MCPBundles hub opaque tokens and not your vendor API keys on the MCP wire.
| Claim | Purpose |
|---|---|
sub | Stable subject from your federation complete call |
client_id | DCR-registered MCP host (Cursor, Claude, …) — distinct from the human sub |
scope | Space-separated OAuth scopes granted to the MCP client |
aud | Allowed MCP resource URL(s) — origin and/or bundle proxy |
iss | Tenant authorization server issuer |
exp / iat | Short-lived access token TTL |
organization_id | Optional tenant/org context from federation |
email | Optional primary email when your complete call includes it |
roles | Optional role names your app passes at complete (see below) |
Refresh and revoke flows are tenant-scoped. On Path A, your MCP server validates the JWT and reads identity from sub, optional organization_id, email, and roles, plus OAuth metadata from client_id and scope. See Tool callback identity for framework mapping.
What roles are (and are not)
roles are pass-through hints from your federation complete call — whatever your app sends after sign-in. Connect Auth does not maintain a platform RBAC store, does not assign roles, and does not enforce them on tool calls.
For sensitive tools, enforce authorization in your MCP server (or call back to your app API using sub) rather than trusting JWT roles alone. Treat roles like convenience claims for logging, UI, or coarse checks; treat sub + your database as the source of truth for permissions.
Tool callback identity
Connect Auth access tokens carry both who signed in (federation subject) and which MCP client is calling (DCR client). Framework providers map those into different callback surfaces — the same split WorkOS AuthKit uses in mcp-use and FastMCP.
Required on every valid tool call
| Field | JWT claim | Semantics |
|---|---|---|
| User id | sub | Federated end-user subject from your federation complete call |
| Client id | client_id | DCR-registered MCP host — not the human |
| Scopes | scope | Space-separated OAuth scopes |
| Audience | aud | origin_resource and/or bundle_proxy_resource |
Optional
| Field | JWT claim | Semantics |
|---|---|---|
| Organization id | organization_id | Tenant/org context from federation |
email | Primary email when your complete call includes it | |
| Roles | roles | Role names your complete call includes — pass-through only; not platform-enforced RBAC |
Rules
client_idnever belongs on the user object — use framework OAuth metadata for the MCP client.- Do not treat
subas the OAuth client — Connect Auth always mintsclient_id; MCPBundles verifiers reject tokens that omit it. - Organization scoping uses
organization_idon the identity side, notclient_id. rolesare hints, not authorization — Connect Auth mints whatever your app sends atcomplete; enforce sensitive access in your MCP server or app API usingsubas the stable key.
Framework mapping
| Intent | mcp-use | FastMCP | Node (@mcpbundles/mcp-connect-auth) |
|---|---|---|---|
| User id | ctx.auth.user.id | token.subject or token.claims["sub"] | token.subject |
| Organization | ctx.auth.user.organizationId | token.claims.get("organization_id") | token.organizationId |
ctx.auth.user.email | token.claims.get("email") | token.email | |
| Roles | ctx.auth.user.roles | token.claims.get("roles") | token.roles |
| MCP client | ctx.auth.clientId | token.client_id | token.clientId |
| Scopes | ctx.auth.scopes | token.scopes | token.scopes |
| Expiry | ctx.auth.expiresAt | token.expires_at | token.expiresAt |
| Resource | ctx.auth.resource | Audience in token.claims["aud"] | token.audience |
| Raw claims | ctx.auth.payload | token.claims | token.rawClaims |
| Canonical JSON | build from ctx.auth | connect_auth_callback_identity(token) | connectAuthCallbackIdentity(token) |
Both official examples expose the same get-user-info tool shape:
{
"user": { "id": "…", "organizationId": "…", "email": "…", "roles": ["…"] },
"auth": {
"clientId": "…",
"scopes": ["…"],
"expiresAt": 1234567890,
"resource": "https://…/mcp"
}
}
There is no user.clientId.
Switching FastMCP ↔ mcp-use
You can change MCP server framework without rewriting identity logic:
- Keep the same listing slug, public MCP origin URL, and Connect Auth public-config.
- Map fields using the table above — do not copy
client_idonto your user model in TypeScript. - Use the shared golden JWT fixtures when writing provider tests:
product/mcp-connect-auth/fixtures/golden_access_token.json(also vendored in upstream fork test trees).
Golden vectors include a signed ES256 token, public JWKS, and expected user_id, organization_id, client_id, and scopes. Third-party provider authors can copy the fixture into their test suite and assert the same logical identity surface as the official FastMCP and mcp-use providers.
Framework providers:
- Python / FastMCP:
McpbundlesConnectProvider+connect_auth_callback_identity()— FastMCP docs or interimmcpbundles-mcp-connecton PyPI - TypeScript / mcp-use:
oauthMcpbundlesProvider— mcp-use provider docs - Node middleware:
@mcpbundles/mcp-connect-auth+connectAuthCallbackIdentity()
Validate tokens on your MCP server (Path A)
When clients connect to your origin, your server must validate Bearer JWTs and return 401 with WWW-Authenticate and protected-resource Link headers when missing or invalid.
Path B (bundle URL only) skips origin validation — MCPBundles validates JWTs at the bundle proxy.
# Path A — FastMCP with Connect Auth provider
from fastmcp import FastMCP
from mcpbundles_mcp_connect.fastmcp import McpbundlesConnectProvider
provider = McpbundlesConnectProvider(listing_slug="{slug}", base_url="https://mcp.example.com")
mcp = FastMCP("My Server", auth=provider, middleware=provider.default_middleware())
Dynamic client registration (DCR)
MCP clients register against the tenant AS, not your vendor origin:
POST …/o/registerGET …/o/authorize(PKCE S256)POST …/o/token
CIMD and PKCE policy are platform-owned. If DCR fails for a specific client, check the maintainer Connections panel and run mcpbundles connect doctor — do not add a parallel DCR endpoint on your server.
Connect Readiness and doctor
Maintainer dashboard shows Connect Readiness (green / yellow / red). Locally, use the same probe ladder:
mcpbundles connect doctor --listing {slug} --env staging --surface bundle --json
Probe order: public-config → JWKS → tenant AS metadata → bundle protected-resource → (origin probes if --surface origin).
Origin telemetry vs bundle proxy telemetry
| Source | How data arrives | Maintainer UI |
|---|---|---|
| Bundle proxy (Path B) | Platform OAuth + proxy initialize | Connections panel — zero vendor code |
| Origin (Path A) | McpbundlesHandshakeMiddleware POST to ingest URL | Connections panel (mount=connect_origin) |
This is not FastMCP OpenTelemetry — OTEL is for your ops traces; Connections is MCP client cohort analytics.
Privacy: We store client name/version, protocol negotiation, and capability summaries from initialize. We do not store tool arguments or arbitrary JSON-RPC payloads.
Testing on staging
- Create or use a staging listing with MCP Connect Auth enabled
mcpbundles connect doctor --listing {slug} --env staging --surface bundle- Connect from a certified MCP host using the bundle URL
- Confirm Connections and readiness in maintainer UI
Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
public-config 404 | Connect Auth not enabled or wrong slug | Enable MCP Connect Auth in maintainer settings |
| Bundle protected-resource missing tenant AS | Hosting flag not MCPBUNDLES_CONNECT | Update listing auth hosting |
| Origin AS forward mismatch | Provider not installed on origin | Path A: install mcpbundles-mcp-connect |
| Federation complete fails | Wrong secret or expired state | Rotate secret; retry authorize flow |
Full probe output: mcpbundles connect doctor --listing {slug} --json
Operations
| Responsibility | Owner |
|---|---|
| DCR, PKCE, CIMD, client quirks | MCPBundles tenant AS |
| MCP tools and upstream API | Vendor |
| Federation secret leak | Vendor — rotate in maintainer UI |
| Bundle proxy JWT validation | MCPBundles |
| Origin JWT validation | Vendor (Path A only) |
Custom issuer CNAME is not supported in v1. Status and incidents: MCPBundles status.
FAQ
How is this different from API keys on my MCP server?
MCP client OAuth uses DCR + PKCE against a standards-compliant authorization server. API keys are a separate concern — Connect Auth issues JWT access tokens for MCP clients only.
Do I need pip/npm if I use the bundle URL?
No. Path B is federation + bundle URL only — no pip/npm on your MCP server.
Can I use my own OAuth provider for app login?
Yes. Connect Auth federates to your sign-in URL. It replaces building MCP-client OAuth (DCR, protected-resource, tenant AS), not your entire identity stack.
What happens when I revoke a federation secret?
Rotate in the maintainer panel, update your app env, and complete new authorize flows. Active sessions expire with access token TTL.
WorkOS / Scalekit — same thing?
No. Those products target application login. Connect Auth targets MCP client OAuth for MCP server listings you manage on MCPBundles.
Platform publishing
Published listings with Connect Auth can expose:
- Claude web connector install links
- ChatGPT custom connector URLs
- MCP Registry export JSON from listing metadata
Connect Readiness should be green before submitting to external directories. See the publishing guide.
Related
- Publishing MCP servers
- MCPBundles CLI
- PyPI:
mcpbundles-mcp-connect— FastMCP provider (see Tool callback identity) - npm:
@mcpbundles/mcp-connect-auth - TypeScript: mcp-use
oauthMcpbundlesProvider— same identity contract as FastMCP