quickoauth
← All guides

Add OAuth to your MCP server with Stytch Connected Apps

Stytch as the authorization server for an MCP connector: magic-link signup, the Connected Apps DCR + CIMD toggles, the project-domain metadata trap, and validating Stytch JWTs in a Worker. Executed live, screenshots included.

Updated

On this page

Stytch’s Connected Apps feature turns a Stytch project into an OAuth 2.0/OIDC identity provider, exactly what an MCP server needs in front of it. We signed up fresh, enabled it, registered a client the way Claude would, and validated a real Stytch-issued JWT in a Cloudflare Worker. The testbed lives at https://qo-test-stytch.rough-disk-9b56.workers.dev/mcp.

stytch.com/start-now: enter your email, click the magic link it sends, create a workspace. No card, no phone number.

Stytch signup

Workspace creation form

You land in a dashboard with a default project and a Test environment.

Stytch dashboard

2. Enable Connected Apps

Sidebar → Connected Apps → Settings tab:

  • Authorization URL: a page your app hosts where Stytch’s IdentityProvider UI component renders login + consent. Stytch delegates the authorize step to you, like Hydra does; unlike Auth0, there is no hosted consent page.
  • Enable dynamic client registration: turn it on. This is what lets Claude, ChatGPT, and Grok register themselves.
  • Enable CIMD: client ID metadata documents, the mechanism ChatGPT prefers over DCR.

Connected Apps settings

3. Use the project domain, not the legacy API path

Every Stytch project has two metadata surfaces, and only one of them is MCP-ready:

legacy path (test.stytch.com/v1/public/{project_id}) project domain (https://….customers.stytch.dev)
/.well-known/openid-configuration ✅ ✅
/.well-known/oauth-authorization-server ❌ 404 ✅
registration_endpoint in metadata ❌ missing (even with DCR on!) ✅ /v1/oauth2/register
issuer stytch.com/project-test-… (not a URL) the domain itself ✅
refresh_token in grant types ❌ ✅

Find your project domain under Project Settings → Project Domain (ours: orchid-legend-8092.customers.stytch.dev). Advertise the project domain in your protected-resource metadata’s authorization_servers; an MCP client pointed at the legacy path will never find the registration endpoint.

Verify DCR works before wiring anything else:

curl -X POST https://YOUR-DOMAIN.customers.stytch.dev/v1/oauth2/register \
  -H 'content-type: application/json' -d '{
  "client_name": "probe",
  "redirect_uris": ["https://claude.ai/api/mcp/auth_callback"],
  "token_endpoint_auth_method": "none"
}'
# → 201 with connected-app-test-… client_id, no null fields

4. Validate Stytch tokens in your MCP server

Stytch access tokens are RS256 JWTs against the project JWKS (https://YOUR-DOMAIN.customers.stytch.dev/.well-known/jwks.json), but two claims differ from what audience-checking middleware expects:

  • iss on tokens is the legacy string stytch.com/project-test-… (not the project domain). Accept both forms.
  • aud is the project or client ID, never your MCP URL. Don’t enforce aud === resource; validate issuer + signature + expiry, and use scopes for authorization.

Everything else is the standard contract: serve /.well-known/oauth-protected-resource naming the project domain, answer with 401 + WWW-Authenticate: Bearer … resource_metadata="…", reject bad tokens with 401.

5. Headless testing with an M2M client

For CI, mint real project JWTs without a browser using a machine-to-machine client (created with your project secret, then exchanged via the same /oauth2/token endpoint):

curl -X POST https://test.stytch.com/v1/m2m/clients \
  -u "$PROJECT_ID:$SECRET" -H 'content-type: application/json' \
  -d '{"client_name":"ci"}'

curl -X POST https://test.stytch.com/v1/public/$PROJECT_ID/oauth2/token \
  -d grant_type=client_credentials -d client_id=$M2M_ID -d client_secret=$M2M_SECRET

The token that comes back is signed by the same JWKS your resource server trusts; our RL suite uses exactly this to prove the testbed validates Stytch-issued JWTs end to end on every deploy.

Next steps

To finish the setup:

  1. Host the authorization page (IdentityProvider component) at the URL you configured, and add your app’s domain under Frontend SDK → authorized domains.
  2. Run the four-curl self-test from the connector checklist, but against the project domain for the metadata steps.
  3. Connect in claude.ai under Customize → Connectors, then repeat for ChatGPT and Grok per one server, three connectors.

The Stytch Connected Apps prompt (stytch-connected-apps-mcp) packages all of this for your agent. If registration fails, debugging connector failures has the probe order; for a contrast in trade-offs, see the Auth0 and Supabase walkthroughs.

Put it into practice

Stytch Connected Apps in front of an MCP server

Enable Connected Apps, dodge the project-domain metadata trap, host the authorization page, and validate Stytch JWTs — with M2M-based headless tests.

Preview prompt
Put Stytch Connected Apps in front of my remote MCP server at https://MY_DOMAIN/mcp [fill in]. My Stytch project ID is MY_PROJECT_ID and my project domain is MY_SUBDOMAIN.customers.stytch.dev [find both under Project Settings → API keys].

Dashboard settings (Connected Apps → Settings):
- Enable dynamic client registration (this is how Claude/ChatGPT/Grok self-register) and enable CIMD for ChatGPT.
- Set the Authorization URL to https://MY_DOMAIN/authorize — a page MY app hosts where the Stytch IdentityProvider UI component (frontend SDK, public token) renders login and consent. Stytch has no hosted consent page; that page is mine to serve.

CRITICAL metadata fact — use the PROJECT DOMAIN, not the legacy API path:
- https://MY_SUBDOMAIN.customers.stytch.dev/.well-known/oauth-authorization-server exists, has a URL issuer, includes registration_endpoint (/v1/oauth2/register) and refresh_token support.
- https://test.stytch.com/v1/public/MY_PROJECT_ID/... serves ONLY openid-configuration, with a non-URL issuer (stytch.com/project-…) and NO registration_endpoint even when DCR is enabled. An MCP client pointed there will fail to register.
So my protected-resource metadata must list authorization_servers = ["https://MY_SUBDOMAIN.customers.stytch.dev"].

Resource server behaviour:
- /.well-known/oauth-protected-resource with resource = https://MY_DOMAIN/mcp and the project domain as the authorization server.
- 401 + WWW-Authenticate Bearer challenge with resource_metadata on every unauthenticated request; 401 for invalid tokens.
- Validate JWTs against https://MY_SUBDOMAIN.customers.stytch.dev/.well-known/jwks.json (RS256). Accept iss as EITHER the project domain OR the legacy "stytch.com/project-…" string (tokens carry the legacy form). Do NOT require aud to equal the MCP URL — Stytch sets aud to the project/client ID; authorize via scopes instead.

Verification (run and show output):
- POST https://MY_SUBDOMAIN.customers.stytch.dev/v1/oauth2/register with redirect_uris ["https://claude.ai/api/mcp/auth_callback"] and token_endpoint_auth_method "none" → 201, connected-app-… client_id, no null fields.
- Headless token proof: create an M2M client (POST https://test.stytch.com/v1/m2m/clients with basic auth project_id:secret), exchange client_credentials at /v1/public/MY_PROJECT_ID/oauth2/token, and call my MCP endpoint with the resulting JWT; confirm a garbage token gets 401.

Keep the project secret in environment variables and out of every log line.

Use this site from your agent

Add this URL as a connector in Claude, ChatGPT, or Grok. No sign-in required.

https://quickoauth.com/mcp
Explore more guides →

Search prompts & guides

24 results