Skip to main content

The Model Context Protocol

The Model Context Protocol (MCP) is a specification for how AI agents can communicate with, and discover the capabilities of, servers which implement specific functionality. Whereas an LLM may not natively have the capability to perform certain actions, such as sending email, if it can communicate with an MCP server that advertises this capability it is empowered to delegate the implementation of email sending to that server. Naturally, this type of interaction can extend beyond email to whatever services implementers wish to create. For the purposes of implementing authorization with Stytch, MCP Clients are a type of Connected App. MCP Clients request access to a member’s account through the authorization_code grant. In this guide, we’ll walk through the creation of a Remote (MCP) server which manages authorization using Stytch. We’ll discuss all the steps that go into a complete MCP Authorization flow. These steps are performed between a variety of actors - some are performed by the MCP Client, some by your MCP Server, and some by Stytch directly. At each step, we’ll call out which actor is responsible for what. For steps that need to be implemented by you, we’ll show code snippets in a variety of languages. To illustrate, we’ll follow along with the Stytch MCP Server, which uses Stytch under the hood.
This guide is for implementing MCP Authorization with the Stytch product. For a more general purpose guide on the role of OAuth within the MCP ecosystem, check out our Blog.

Pre-requisites

At a high level, your MCP Server is responsible for the following:
  1. Validating Stytch-issued access tokens and returning a specific error format on failure
  2. Serving a JSON document instructing MCP Clients to talk to Stytch
In addition, your application is responsible for hosting one of the following Stytch components to handle the OAuth Consent step:
  • <IdentityProvider /> React Component for Consumer applications
  • <B2BIdentityProvider /> React Component for B2B applications
If you are adding MCP to an existing project, this should be added to your main frontend codebase. If you are building a standalone MCP Server, this can be added to the MCP Server directly.

The Complete MCP Authorization Flow

1

Access Token Validation

This step is performed by your MCP Server.
When an MCP client first tries to connect to your MCP server, the client will send an Initialization request.Your MCP server should validate that the request contains a valid Stytch-issued access token JWT.If the request has no access token, or an invalid or expired access token, your MCP server must respond with a 401 Unauthorized status code, and a specially formatted WWW-Authenticate header. This header will direct the client to look up the Protected Resource Metadata (PRM) document as detailed in RFC 9728.Let’s try with the Stytch MCP Server, which uses Stytch under the hood.
# The MCP Client initializes a connection without any credentials
curl -D - "https://mcp.stytch.dev/mcp"
The response contains a WWW-Authenticate header with a resource_metadata attribute.
HTTP/2 401
www-authenticate: Bearer error="Unauthorized", error_description="Unauthorized",
  resource_metadata="https://mcp.stytch.dev/.well-known/oauth-protected-resource"

Unauthorized
Here are some examples of how to implement this check in a variety of languages and runtimes:
Node projects should use the Stytch backend SDK to perform token validation using the Introspect Access Token API method.
import * as stytch from "stytch";
import express from "express";

const app = express();

const client = new stytch.B2BClient({
   project_id: "PROJECT_ID",
   secret: "SECRET",
   custom_base_url: '${projectDomain}',
})

const authorizeTokenMiddleware = async (req, res, next) => {
   const wwwAuthValue = `Bearer error="Unauthorized", ` +
     `error_description="Unauthorized",` +
     `resource_metadata="${req.get("host")}/.well-known/oauth-protected-resource"`;

   const token = req.headers.authorization &&
     req.headers.authorization.split(' ')[1];
   if (!token) {
      res.setHeader('WWW-Authenticate', wwwAuthValue);
      return res.status(401).json({ error: 'Unauthorized' });
   }

   client.idp.introspectTokenLocal(token)
     .then(tokenData => {
        // Set the token data on the request for later use
        req.user = response;
        next();
     })
     .catch(err => {
        console.error('Error in middleware:', err);
        res.setHeader('WWW-Authenticate', wwwAuthValue);
        return res.status(401).json({ error: 'Unauthorized' });
     })
};

app.post('/mcp', authorizeTokenMiddleware, async (req, res) => {
   const server = new McpServer({ name: "Demo", version: "1.0.0" });

   // The server can now access the validated req.user within tool calls
   server.tool("whoami", {}, async () => ({
      content: [{
        type: "text",
         text: "You are " + JSON.stringify(req.user, null, 2),
      }]
   }));

   const transport = new StreamableHTTPServerTransport({
      sessionIdGenerator: undefined,
   });
   res.on('close', () => {
      transport.close();
      server.close();
   });
   try {
      await server.connect(transport);
      await transport.handleRequest(req, res, req.body);
   } catch (error) {
      console.error('Error handling MCP request:', error);
      res.status(500).json({
         jsonrpc: '2.0',
         error: {
            code: -32603,
            message: 'Internal server error',
         },
         id: null,
      });
   }
})
2

Protected Resource Metadata

This step is performed by your MCP Server.
The MCP Client will extract the resource_metadata field from the WWW-Authenticate header returned and issue a GET request to that URL to retrieve the Protected Resource Metadata (PRM) Document. The PRM Document is hosted by your MCP Server. Your MCP Server will return a JSON document containing information about how the client should request an access token.
# The MCP Client retrieves the PRM document
curl -s "https://mcp.stytch.dev/.well-known/oauth-protected-resource" | jq
Here’s what the Stytch MCP Server returns. Note that there are several Custom Scopes used by the Stytch MCP Server that have been eliminated for brevity.
{
  "resource": "https://mcp.stytch.dev",
  "authorization_servers": [
    "https://rustic-kilogram-6347.customers.stytch.com"
  ],
  "scopes_supported": ["openid", "email", "profile", "manage:project_data", ...]
}
Field NameMeaning
resourceThe resource identifier - a https:// URL. This should be the location of your MCP Server.
authorization_serversA JSON array containing a list of OAuth authorization server issuer URLs. This should be your Stytch PROJECT_DOMAIN.
scopes_supportedA JSON array of OAuth scopes that can be requested from your Stytch project. Some MCP clients will use the scopes returned in the scopes_supported array as the initial set of permissions to request.
Here are some examples of how to implement this check in a variety of languages and runtimes:
import express from "express";

const app = express();

app.get('/.well-known/oauth-protected-resource/:transport?', (req, res) => {
   return res.json({
      resource: req.get("host"),
      authorization_servers: [process.env.STYTCH_DOMAIN],
      scopes_supported: ["openid", "email", "profile"]
   })
})
3

Authorization Server Metadata

This step is performed by Stytch.
The MCP Client will extract the authorization_servers field from the PRM document and issue a GET request to the first authorization server’s Authorization Server Metadata (ASM) endpoint, as defined by RFC 8414.The ASM endpoint is hosted by Stytch, not by your server. However, the ASM endpoint needs to know the location where you host the
  • <IdentityProvider /> React Component for Consumer applications
  • <B2BIdentityProvider /> React Component for B2B applications
Configure this in the Connected Apps section of the Stytch Dashboard. Enter the URL where the component is mounted into the Authorization URL field. If you don’t have a value for this yet - put https://example.com - we’ll come back to it later.
curl -s "https://rustic-kilogram-6347.customers.stytch.com/.well-known/oauth-authorization-server" | jq
This will return a JSON document containing information about what functionality your Stytch project supports, and what endpoints will be used for the rest of the authorization process. For example, we see that the <B2BIdentityProvider /> or <IdentityProvider /> React component is hosted at https://stytch.com/oauth/authorize, which is communicated to the MCP Client as the authorization_endpoint.
{
  "authorization_endpoint": "https://stytch.com/oauth/authorize",
  "registration_endpoint": "https://rustic-kilogram-6347.customers.stytch.com/v1/oauth2/register",
  "token_endpoint": "https://rustic-kilogram-6347.customers.stytch.com/v1/oauth2/token",
  ...
}
Field NameMeaning
authorization_endpointURL of your Stytch Project’s authorization endpoint, hosted by you and configured in the Stytch Dashboard.
registration_endpointURL of your Stytch Project’s OAuth 2.0 Dynamic Client Registration endpoint, hosted by Stytch.
token_endpointURL of your Stytch Project’s token endpoint, hosted by Stytch.
4

Dynamic Client Registration

This step is performed by Stytch.
The MCP Client needs to be given a client_id by Stytch, in order to uniquely identify itself. There are two ways for this to happen:
  • Pre-registration: if the MCP Client already has a client_id, it may use that client_id and skip this step. Some MCP Clients will let users specify their own client_id. This is especially common in Enterprise environments where there are more limitations on what parties have access to sensitive data.
  • Dynamic registration: the MCP Client can send a Dynamic Client Registration (DCR) request to your Stytch Project’s DCR Endpoint( for B2B, for Consumer) and be granted a unique client_id on the fly.
Dynamic Client Registration is an opt-in feature, and must be enabled in the Connected Apps dashboard.
curl -s "https://rustic-kilogram-6347.customers.stytch.com/v1/oauth2/register" \
  -H 'Content-Type: application/json' \
  -d '{
    "client_name": "MCP Inspector",
    "redirect_uris": ["http://localhost:6274/oauth/callback"],
    "grant_types": ["authorization_code", "refresh_token"],
    "response_types": ["code"]
  }' | jq
Stytch will return a JSON blob with the client registration information. This will be a superset of the original registration metadata, and will contain a newly-issued client_id. All clients created through DCR will be Third Party Public clients within Stytch.
{
  "client_id": "connected-app-live-aa7562c6-67b6-4363-bbc0-643fed5990ec",
  "client_name": "MCP Inspector",
  "grant_types": ["authorization_code", "refresh_token"],
  "redirect_uris": ["http://localhost:6274/oauth/callback"],
  "response_types": ["code"],
  ...
}
5

Request Consent

This step is performed by the MCP Client.
The MCP client will now open a browser window to the authorization_endpoint previously returned in the ASM document. The MCP client will pass in various query parameters to tell us who it is and what data it is trying to access. The full set of parameters that may be passed are detailed in the OAuth 2.1 Authorization Request specification.
const base = "https://stytch.com/oauth/authorize";

const params = new URLSearchParams({
   // The client_id issued from Dynamic Client Registration in step 4
   client_id: "connected-app-live-aa7562c6-67b6-4363-bbc0-643fed5990ec",
   // Where the member should be redirected back with the code
   // This _must_ match one of the `redirect_uris` previously registered
   redirect_uri: "http://localhost:6274/oauth/callback",
   // The desired response - an authorization code
   response_type: "code",
   // The permissions being requested
   // Usually inferred from the `scopes_supported` field in the PRM
   scope: "openid email profile manage:project_data",
   // An opaque value used by the client to track login state
   // The AS will return this value in the callback
   state: "29499...",
   // A PKCE code challenge - a one-time secret created by the client to secure the request
   code_challenge: "meY9Iy...",
   code_challenge_method: "S256",
});

// Perform a full-page navigation
window.location.href = `${base}?${params.toString()}`;
6

Grant Consent

This step is performed by your main application.
The authorization_endpoint is the location of a web page hosting the Stytch <B2BIdentityProvider /> or <IdentityProvider /> React component. This component will parse the query parameters passed by the MCP Client in step 5, validate the authorization request, and prompt the member for consent. If the member consents to share their data with the MCP Client, the member will be redirected back to the redirect_uri owned by the client with a code.
import { B2BIdentityProvider, useStytchMember } from '@stytch/react/b2b';
import { useEffect } from 'react';

const OAuthAuthorizePage = () => {
  const { member } = useStytchMember();

  // The member must be logged in before they can consent to share data
  useEffect(() => {
    if (!member) window.location.href = '/login';
  }, [member]);

  return <B2BIdentityProvider />;
};
7

Exchange Authorization Code

This step is performed by Stytch.
The <B2BIdentityProvider /> or <IdentityProvider /> React component will redirect the member/user back to the client with an authorization code that the client exchanges for tokens. The client will call the token_endpoint listed in the ASM response - an endpoint hosted by Stytch.
curl -X POST "https://rustic-kilogram-6347.customers.stytch.com/v1/oauth2/token" \
  -d "grant_type=authorization_code" \
  -d "client_id=connected-app-live-aa7562c6-67b6-4363-bbc0-643fed5990ec" \
  -d "code=$CODE" \
  -d "code_verifier=$CODE_VERIFIER" \
  -d "redirect_uri=http://localhost:6274/oauth/callback"
{
  "access_token": "eyJhbGciOiJSUzI1NiIs...",
  "refresh_token": "def502...",
  "token_type": "Bearer",
  "expires_in": 3600
}
The access token can now be used by the MCP Client to make requests to the MCP server.
8

Complete

Finally, the MCP Client can make requests to your MCP Server using the access token embedded in the Authorization header:
curl -X POST "https://mcp.stytch.dev/mcp" \
  -H 'Authorization: Bearer $ACCESS_TOKEN'
The middleware we implemented in Step 1 will validate this token and process the request. Your MCP Server can now use the authenticated MCP Client’s information inside tool calls.

What’s Next

You should now have a working overview of all the steps that go in to setting up authorization for MCP servers.