A library built on top of the MCP TypeScript SDK that makes it easier to implement MCP with auth into your MCP client and/or server.
It's a protocol that enables AI applications like Claude, ChatGPT, Cursor, etc to ask you for permission to access some of your private information that normally you'd need to sign in with your account to access. For example, your emails, or your private github repositories, etc.
This allows you to provide AI applications with context and abilities that it would normally not have access to. You could, for example, have an AI application use some code in a private repo as context to answer your questions, or have it write and send an email on your behalf, after you review it. It's kind of like this:
We think this is valuable because it enables people to use AI to access a bunch of extra information that it wasn't able to access before, and does so in a safe way where you as the user are in control over what it has access to and what it can do. We're excited to see what new AI use cases become possible as MCP adoption grows, and we built this library to try to help make it easier for people to integrate MCP into their applications.
There are two parties involved in MCP:
- The client, which is the one that wants to get access to another service. In the above example would be Claude, which wants to get access to Gmail.
- The server, which is the one that has something that a client wants access to. In the above example, this would be Gmail. This is sometimes referred to as the "resource server" or "MCP server".
This library has tools for both of these parties, so step one is to be clear on whether you are building a client or server. We'll address each of these use cases separately.
NOTE: In web development, the terms "client" and "server" are often used to refer to the frontend (browser) and backend (web server). This is not the case in this situation, so try not to confuse them!
For detailed implementation guides and examples specific to your framework, see:
- Express.js Integration - Complete guide for building MCP servers with Express.js
- Hono Integration - Complete guide for building MCP servers with Hono and edge runtimes
- Next.js Integration - Complete guide for building both MCP servers and clients with Next.js
If you are building a server that you'd like to introduce MCP support for, you will want to use the @clerk/mcp-tools/server import path.
In order for the most up to date authentication flow in the MCP spec to work correctly, your server will need to expose a static metadata file called "protected resource metadata", which is defined by RFC 9728.
This library exposes a tool that can quickly generate such a metadata file for you. Here's an example of how to use it:
import { generateProtectedResourceMetadata } from '@clerk/mcp-tools/server';
const result = generateProtectedResourceMetadata({
resourceUrl: 'https://myapp.com/current-route',
authServerUrl: 'https://auth.example.com',
});You will want to set up a route at .well-known/oauth-protected-resource and make sure to return this result from that route on your server.
If you are using Clerk for authentication in your app, we have a helper that makes this easier:
import { generateClerkProtectedResourceMetadata } from '@clerk/mcp-tools/server';
const result = generateClerkProtectedResourceMetadata({
publishableKey: process.env.CLERK_PUBLISHABLE_KEY,
resourceUrl: 'https://myapp.com/current-route',
});For framework-specific implementations of protected resource metadata handlers, see:
NOTE: This is not yet fully implemented
An older version of the MCP spec required MCP servers to publish a separate "authorization server metadata" document, defined by RFC 8414. Most deployments no longer need this endpoint when they publish protected resource metadata and use an authorization service with its own metadata endpoint. You may still need it for a custom or embedded authorization server or an older MCP client.
If you use Clerk for authentication, fetch the metadata from your Clerk Frontend API and return it from your authorization server metadata endpoint.
import { fetchClerkAuthorizationServerMetadata } from '@clerk/mcp-tools/server';
// Return this result from <your-app>/.well-known/oauth-authorization-server
const result = await fetchClerkAuthorizationServerMetadata({
publishableKey: process.env.CLERK_PUBLISHABLE_KEY,
});For framework-specific implementations, see:
To create an MCP endpoint that handles the actual MCP protocol communication, you'll need to use framework-specific adapters, since each framework has its own way of handling request and response objects, which are critical parts of implementing the MCP protocol:
- Express.js: Use the
streamableHttpHandlerfrom@clerk/mcp-tools/express- see Express.js guide - Hono: Use the
streamableHttpHandlerfrom@clerk/mcp-tools/hono- see Hono guide - Next.js: Use Vercel's MCP adapter with Next.js route handlers - see Next.js guide
These adapters handle the MCP protocol details and integrate with your authentication system.
When a tool needs an OAuth scope that the access token lacks, the MCP server can answer 403 with WWW-Authenticate: Bearer error="insufficient_scope", .... MCP clients answer that by asking the user to approve more scopes. MCP SDK 2.1 or later (@modelcontextprotocol/server, which Vercel's mcp-handler 2.x also uses) sends this challenge before the tool runs, from the scopeChallenge option of registerTool. requireAdditionalScopes from @clerk/mcp-tools/server (also exported from @clerk/mcp-tools/next) builds that option from the scopes the tool needs:
import { requireAdditionalScopes } from '@clerk/mcp-tools/server';
server.registerTool(
'list_projects',
{
title: 'List projects',
description: 'Lists the projects of the signed-in user.',
inputSchema: {},
scopeChallenge: requireAdditionalScopes('projects:read'),
},
async () => {
// ...
},
);- Pass
resourceMetadataUrlon theauthInfoyou give the SDK (for example{ ...authInfo, resourceMetadataUrl }). Without it, the challenge has noresource_metadata. - The challenge lists the granted scopes followed by the missing ones. Some MCP clients request exactly the challenged scopes, so listing only the missing ones could drop access that other tools need. The SDK's own
requireScopeshelper lists the required scopes only. - Enable every scope your tools need for the OAuth application. Its scope settings and the user's approval decide what a token can get.
scopes_supportedin your protected resource metadata only guides what clients request: MCP clients request the scopes named in a challenge, and otherwise usually the wholescopes_supportedlist on first connection. A401names scopes only when your server adds them, asmcp-handler'swithMcpAuthdoes from itsrequiredScopes. Listing only baseline scopes inscopes_supportedencourages clients to request extra scopes through step-up, though a client can still request them up front. When the401doesn't name scopes, listing a scope there makes most clients request it on first connection, which is usually the only way clients without step-up support ask for it. - The SDK sends the challenge only over HTTP (
createMcpHandleror the Streamable HTTP transport) and for requests with auth info. Elsewhere, including stdio, the tool runs without it. - MCP SDK 1.x has no
scopeChallengeoption, and TypeScript rejects it there. ThestreamableHttpHandleradapters in@clerk/mcp-tools/expressand@clerk/mcp-tools/honorun SDK 1.x, so a tool behind them must check its scopes itself. requireAdditionalScopesthrows aClerk error:message when it is given no scopes or a scope that is not a valid OAuth scope token.createBrowserSessionToolsets this option from itsrequiredScopesfor you, and leaves it out whenrequiredScopesis empty.
Agents that use your MCP server sometimes get blocked, need a workaround, or find a tool confusing. @clerk/mcp-tools/feedback adds a submit_feedback tool that agents can call to report it. Reports are stored on your Clerk instance and shown on the instance's Feedback tab in the Clerk Dashboard.
import { submitFeedbackTool } from '@clerk/mcp-tools/feedback';
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
function createServer() {
const server = new McpServer({ name: 'my-app', version: '1.0.0' });
server.registerTool(...submitFeedbackTool({ secretKey: process.env.CLERK_SECRET_KEY! }));
// ...register your own tools
return server;
}The tool works with both major versions of the MCP SDK: @modelcontextprotocol/sdk 1.x (McpServer from @modelcontextprotocol/sdk/server/mcp.js) and @modelcontextprotocol/server 2.x (McpServer from @modelcontextprotocol/server). The registration line is the same. With SDK 2.x, install zod 4 (SDK 2.x requires it), so the tool's input schema type-checks against registerTool.
secretKeymust be the secret key (sk_...) of the Clerk instance the reports belong to.submitFeedbackToolthrows if it's empty, so a missing environment variable fails at startup.- Agents describe the task, what kind of problem it was (
missing,broken,confusing, oridea), how the task ended, and what they tried and what went wrong. - Each report names your server as its source: the request's host by default, or pass
server: { name, version }to label it yourself. - Each report names the agent from the client info its MCP client sends with the request, or otherwise from the HTTP
User-Agentheader. Generic HTTP libraries and browsers (such asnodeorMozilla) aren't used as names, and clients that identify themselves only when they connect (common over stdio) appear without a name. - When the request was authenticated with
verifyClerkToken(for example throughmcpAuthClerk), the signed-in user's ID and the OAuth client ID are attached to each report.verifyClerkTokenmarks its result withextra.verifiedBy: 'clerk'(check it withisClerkAuthInfofrom@clerk/mcp-tools/server); identity from any other verifier is not sent. - Agents file reports without asking the user first. A report contains the agent's summary of what the user asked for, the tool calls it made, and any errors, and is linked to the user's ID when one is attached. Cover this in the privacy information you give your users.
- Clerk removes secrets it detects in reports and accepts up to 100 reports per instance per hour.
- On a public MCP endpoint without authentication, anyone can call this tool, submit misleading reports, and exhaust the instance's 100-report-per-hour limit. Protect it with authentication or other access controls where possible.
- If a report can't be stored, the tool tells the agent not to retry and logs the response status with
console.error. Requests to Clerk time out after 10 seconds. - This module requires
zod(3.25 or later, or 4), which the MCP SDK already needs.
When an agent drives a browser for your user and lands on your app's sign-in page, @clerk/mcp-tools/create-browser-session lets it get a one-time link that opens your app already signed in as that user. The create_browser_session tool takes the user from the verified MCP access token, never from tool input.
The link starts a session as the user, so the access token check in front of this tool must be strict. Verify the token with authenticateRequest and an audience equal to your MCP server's resource URL. Here is a Next.js setup on @modelcontextprotocol/server 2.1 or later, so that the SDK can send the step-up challenge. It replaces the mcp-adapter route from the Next.js guide. The tool also works behind the mcp-adapter route when that route checks the audience, but without step-up.
First, serve the protected resource metadata at the path form of the MCP endpoint, with resource set to the same URL the route uses as its audience. MCP clients send that resource when they request a token, and Clerk puts it in the token's aud, so the two must match. The guide's root metadata route advertises only the origin, and every token would then fail the audience check. Enable the scopes the tool requires for the OAuth application. List them in scopes_supported too: a client without step-up support requests that list on its first connection, so that is the only way it asks for them. See Step-up for your own tools:
// app/.well-known/oauth-protected-resource/mcp/route.ts
import {
metadataCorsOptionsRequestHandler,
protectedResourceHandlerClerk,
} from '@clerk/mcp-tools/next';
// List every scope the server's tools need, here the `requiredScopes` of the tool below.
const handler = protectedResourceHandlerClerk({
resource: 'https://app.example.com/mcp',
scopes_supported: ['browser_session'],
});
const corsHandler = metadataCorsOptionsRequestHandler();
export { handler as GET, corsHandler as OPTIONS };Then the MCP route, here on @modelcontextprotocol/server 2.1 or later. createBrowserSessionTool sets the SDK's scopeChallenge registration option, so the SDK sends the step-up challenge before the tool runs, and the route needs no scope code:
// app/mcp/route.ts
import { verifyClerkToken } from '@clerk/mcp-tools/next';
import {
browserSessionInstructions,
createBrowserSessionTool,
} from '@clerk/mcp-tools/create-browser-session';
import { clerkClient } from '@clerk/nextjs/server';
import { createMcpHandler, McpServer } from '@modelcontextprotocol/server';
// The OAuth resource this server accepts. Tokens must carry it in their `aud` claim.
const MCP_RESOURCE_URL = 'https://app.example.com/mcp';
const RESOURCE_METADATA_URL = 'https://app.example.com/.well-known/oauth-protected-resource/mcp';
const APP_URL = 'https://app.example.com/dashboard';
// A custom scope for this tool only. Not a scope that most clients get, such as `profile`: see the notes below.
const BROWSER_SESSION_SCOPES = ['browser_session'] as const;
// Built once at module scope, so a bad configuration fails at startup instead of on every request.
const browserSessionTool = createBrowserSessionTool({
secretKey: process.env.CLERK_SECRET_KEY!,
destinationUrl: APP_URL,
requiredScopes: BROWSER_SESSION_SCOPES,
});
const mcpHandler = createMcpHandler(() => {
const server = new McpServer(
{ name: 'my-app', version: '1.0.0' },
{ instructions: browserSessionInstructions(APP_URL) },
);
server.registerTool(...browserSessionTool);
// ...register your own tools
return server;
});
async function handler(req: Request) {
const token = req.headers.get('authorization')?.match(/^Bearer (.+)$/i)?.[1];
const clerk = await clerkClient();
const requestState = await clerk.authenticateRequest(req, {
acceptsToken: 'oauth_token',
audience: MCP_RESOURCE_URL,
});
const authInfo = verifyClerkToken(requestState.toAuth(), token);
if (!authInfo) {
return new Response('Unauthorized', {
status: 401,
headers: {
'WWW-Authenticate': `Bearer resource_metadata="${RESOURCE_METADATA_URL}"`,
},
});
}
// The SDK takes the challenge's `resource_metadata` from `resourceMetadataUrl`, which `verifyClerkToken` does not set.
return mcpHandler.fetch(req, {
authInfo: { ...authInfo, resourceMetadataUrl: RESOURCE_METADATA_URL },
});
}
export { handler as GET, handler as POST, handler as DELETE };- Pass
browserSessionInstructions(destinationUrl)as the serverinstructions. Some MCP clients, such as Codex, show agents only the server instructions until the agent searches for a tool, so without them an agent may open your app in a browser and stop at the sign-in page. - Check the audience. Without an
audiencecheck, any OAuth access token that your Clerk instance issued, to any OAuth application and for any purpose, can call this tool. Your Clerk instance must issue theaudclaim in its OAuth access tokens. Require@clerk/backend3.19.0 (@clerk/nextjs7.9.5,@clerk/express2.1.70,@clerk/hono0.1.80) or later (see clerk/javascript#9724). Before those versions, a JWT access token without anaudclaim and an opaque OAuth access token both pass theaudiencecheck, and only a JWT with a differentaudis rejected. ThemcpAuthClerkhelpers for Express and Hono do not check the audience, so add that check yourself before you register this tool behind them. To confirm that the check works, send a token that was issued to another OAuth application of the same instance, or for another resource: the MCP route must answer401. - Authenticate requests with
verifyClerkToken, and setrequiredScopesto the OAuth scopes the access token must carry.requiredScopesis required.[]is accepted, but then every token that passes the audience check can create a link. - Require a dedicated scope for this tool, such as a custom scope named
browser_session, not a scope that nearly every client gets, such asprofileoremail. The link signs a browser in to your app as the user, so the scope is how the user approves that for a given MCP client. With a common scope, every MCP client the user connects can open your app as them.restricteddoes not narrow this for your own backend: only Clerk's Frontend API enforces it, and your backend sees an ordinary signed-in session unless it checks for an agent session itself. Create the scope as a custom OAuth scope on your Clerk instance, enable it for the OAuth applications that may use the tool, and keep it out of the scopes that clients get by default, so a client gets it only when the user approves it, for example through the step-up challenge. - Step-up authorization (a
403challenge that makes the MCP client ask the user to approve more scopes) needs@modelcontextprotocol/server2.1 or later. The tool sets the SDK'sscopeChallengeregistration option, and you must passresourceMetadataUrlonauthInfo, as above; without it the challenge has noresource_metadata. The challenge asks for the token's granted scopes plus the missing ones, so the new grant does not drop access that other tools need. The SDK's ownrequireScopeshelper challenges for the listed scopes only. - On
@modelcontextprotocol/sdk1.x and@modelcontextprotocol/serverversions before 2.1 the tool still registers and works, but there is no step-up. On@modelcontextprotocol/serverversions before 2.1 the tool also logs a one-timeClerk warning:. A token without the required scopes gets the tool's own error, which tells the agent to ask the user to reconnect the MCP server and approve those scopes. The tool refuses before it calls Clerk.createBrowserSessionToolalso throws aClerk error:message when a scope is not a valid OAuth scope token or looks like a Clerk key (sk_...,pk_...). - Your MCP server and your app must use the same Clerk instance.
secretKeyis that instance's secret key (sk_...), anddestinationUrlmust be on a domain of that instance. - The agent can pass an absolute
pathsuch as/settings(with optional query and hash). It replaces the path ofdestinationUrl, and the tool refuses anything that would leave the app. - The link is a bearer credential: anyone who has it before it is used or expires can sign in as the user. The tool never puts it in error messages, but its text result contains the link so that the agent can open it. Never log it, and treat MCP client and gateway logs of tool results as credentials.
restricted(defaulttrue) limits the session the link starts. Clerk's Frontend API refuses these writes from that session: touch, switching the active session or organization, sign-out, writes to the user's own profile and organizations, reverification, and OAuth consent and approval. The one exception is refreshing its own session token. Clerk enforces this restriction, not your app's own backend: your backend sees an ordinary signed-in session unless it checks for an agent session itself. Passrestricted: falsefor an ordinary session. When Clerk does not confirm the restriction in its response, for example on a Clerk Backend API that does not support it yet, the tool revokes the new session link on a best-effort basis, refuses to return it, and fails with the error coderestricted_not_applied. The log line names theagent_task_idwhen it is a well-formed ID. A malformed ID is never logged. It is still revoked when it can safely go in the revoke request's path; otherwise the log line saysrevoke_skipped, and the unused link expires on its own.- Optional settings:
sessionMaxDurationInSecondssets the maximum length, in seconds, of the session the link starts. Clerk's default is 30 minutes, and a larger value makes the session longer.agentName,apiUrl, andfetchare also available. Requests to Clerk time out after 10 seconds. - To create a link outside of an MCP tool, use
createBrowserSession({ authInfo, path, taskDescription, organizationId }, options)from the same module. It resolves to{ url, agentTaskId, redirectUrl }.organizationIdis optional and sets the organization the session starts in. Withrestricted, it is also the only organization the session can use. Without it, Clerk uses the user's last active organization. When you pass it, the link is refused withrestricted_not_appliedunless Clerk confirms that organization, and it fails withorganization_rejectedwhen the user does not belong to the organization, or when the instance requires one and none applies. UsevalidateBrowserSessionOptions(options)to check the options at startup. When it throws aBrowserSessionError,error.codesays why (for exampleinsufficient_scope, with the scopes inerror.missingScopes, orrestricted_not_applied), andbrowserSessionErrorMessage(error)returns the message the tool gives the agent. - The tool works with
@modelcontextprotocol/sdk1.x and@modelcontextprotocol/server2.x, with the same registration line. This module requireszod(3.25 or later, or 4), which the MCP SDK already needs; with SDK 2.x, installzod4.
The first step to building MCP compatibility into your AI application is allowing your users to connect with an MCP service. This can be kicked off simply with the URL of an MCP-compatible server, like https://example.com/mcp. Normally, your app would implement a text field where the user can enter an MCP endpoint, or have a pre-built integration where clicking a button would trigger an MCP connection flow with a baked-in endpoint URL.
The process of actually making the MCP connection using the SDK, however, is fairly arduous, so we expose some tools that can help make this easier.
Here's how you can create an MCP client using the core utilities:
import { createDynamicallyRegisteredMcpClient } from '@clerk/mcp-tools/client';
import { createRedisStore } from '@clerk/mcp-tools/stores/redis';
// Create a persistent store (use appropriate store for your environment)
const store = createRedisStore({ url: process.env.REDIS_URL });
export async function initializeMCPConnection(mcpEndpoint: string) {
const { connect, sessionId } = createDynamicallyRegisteredMcpClient({
mcpEndpoint,
oauthScopes: 'openid profile email',
oauthRedirectUrl: 'https://yourapp.com/oauth_callback',
oauthClientUri: 'https://yourapp.com',
mcpClientName: 'My App MCP Client',
mcpClientVersion: '0.0.1',
redirect: (url: string) => {
// Implement redirect logic for your framework
window.location.href = url;
},
store,
});
// Connect to the MCP endpoint
await connect();
return { sessionId };
}After the user completes the OAuth flow, you'll need to handle the callback:
import { completeAuthWithCode } from '@clerk/mcp-tools/client';
export async function handleOAuthCallback(code: string, state: string) {
const result = await completeAuthWithCode({
state,
code,
store,
});
// OAuth flow is now complete
return result;
}Once authentication is complete, you can call MCP tools:
import { getClientBySessionId } from '@clerk/mcp-tools/client';
export async function callMCPTool(sessionId: string, toolName: string, args: any) {
const { client, connect } = getClientBySessionId({
sessionId,
store,
});
await connect();
const toolResponse = await client.callTool({
name: toolName,
arguments: args,
});
return toolResponse;
}For complete framework-specific implementations with working examples, see:
In order to implement MCP functionality in a client, persistent storage is required. This is because:
- The MCP flow operates across a minimum of three distinct server endpoints (initialization of MCP client, OAuth callback, MCP request), and these server endpoints could be deployed to distinct serverless/edge functions without a shared memory pool.
- Since the MCP connection is intended to be long-running, it must maintain a "session". Relying on in-memory storage for long-running sessions is generally a very bad idea ™️, since it would bloat memory requirements indefinitely as the app scales, and any sort of clearing of memory like a server restart would immediately invalidate all sessions.
As such, each of the client functions require that you pass in a store adapter. There are several built-in store adapters available:
import fsStore from '@clerk/mcp-tools/stores/fs';This uses a temporary file and is fast, easy, and adequate for local development and testing. However, it's not suitable for production since the file could be deleted at any time.
For production environments, use one of these persistent stores:
import { createRedisStore } from '@clerk/mcp-tools/stores/redis';
const store = createRedisStore({
url: process.env.REDIS_URL,
});import { createPostgresStore } from '@clerk/mcp-tools/stores/postgres';
const store = createPostgresStore({
connectionString: process.env.DATABASE_URL,
});import { createSqliteStore } from '@clerk/mcp-tools/stores/sqlite';
const store = createSqliteStore({
filename: './mcp-sessions.db',
});If you wish to use a different kind of store, you can implement your own by complying with this simple interface:
type JsonSerializable =
| null
| boolean
| number
| string
| JsonSerializable[]
| { [key: string]: JsonSerializable };
interface McpClientStore {
write: (key: string, data: JsonSerializable) => Promise<void>;
read: (key: string) => Promise<JsonSerializable>;
}The built-in stores have a few extra methods that may be useful, but only read and write are required for this to work.
The above examples are more of a guide for how to implement the tools, but for those digging deeper, this section covers each tool that is exposed out of this package, what it can take in as arguments, and what it will return.
-
createKnownCredentialsMcpClient-
Description: If dynamic client registration is not desirable, and your interface can collect a client id and secret from an existing OAuth client, you can create a MCP client with this function. Though it does increase friction in the user experience, we recommend allowing MCP services that do not enable dynamic client registration, since it comes with several security/fraud risks that not every provider wants to take on.
-
Arguments:
interface CreateKnownCredentialsMcpClientParams { /** * OAuth client id, expected to be collected via user input */ clientId: string; /** * OAuth client secret, expected to be collected via user input */ clientSecret: string; /** * OAuth redirect URL - after the user consents, this route will get * back the authorization code and state. */ oauthRedirectUrl: string; /** * OAuth scopes that you'd like to request access to */ oauthScopes?: string; /** * The endpoint of the MCP service, expected to be collected via user input */ mcpEndpoint: string; /** * Name passed to the client created by the MCP SDK * @see https://github.com/modelcontextprotocol/typescript-sdk?tab=readme-ov-file#writing-mcp-clients */ mcpClientName: string; /** * Version number passed to the client created by the MCP SDK * @see https://github.com/modelcontextprotocol/typescript-sdk?tab=readme-ov-file#writing-mcp-clients */ mcpClientVersion: string; /** * A function that, when called with a url, will redirect to the given url */ redirect: (url: string) => void; /** * A persistent store for auth data * @see https://github.com/clerk/mcp-tools?tab=readme-ov-file#stores */ store: McpClientStore; }
-
Return Type:
interface McpClientReturnType { /** * Represents a session associated with the connected MCP service endpoint. */ sessionId: string; /** * Calling this function will initialize a connect to the MCP service. */ connect: () => void; /** * Lower level primitive, likely not necessary for use * @see https://github.com/modelcontextprotocol/typescript-sdk/blob/main/src/client/streamableHttp.ts#L119 */ transport: StreamableHTTPClientTransport; /** * Lower level primitive, likely not necessary for use * @see https://github.com/modelcontextprotocol/typescript-sdk/blob/main/src/client/index.ts#L81 */ client: Client; /** * Lower level primitive, likely not necessary for use * @see https://github.com/modelcontextprotocol/typescript-sdk/blob/main/src/client/auth.ts#L13 */ authProvider: OAuthClientProvider; }
-
-
createDynamicallyRegisteredMcpClient-
Description: Creates a new MCP client given only an MCP endpoint url. Registers an OAuth client with the authorization server on-demand via OAuth 2.0 Dynamic Client Registration Protocol.
-
Arguments:
interface CreateDynamicallyRegisteredMcpClientParams { /** * The endpoint of the MCP service, expected to be collected via user input */ mcpEndpoint: string; /** * OAuth redirect URL - after the user consents, this route will get * back the authorization code and state. */ oauthRedirectUrl: string; /** * The name of the OAuth client to be created with the authorization server */ oauthClientName?: string; /** * The URI of the OAuth client to be created with the authorization server */ oauthClientUri?: string; /** * OAuth scopes that you'd like to request access to */ oauthScopes?: string; /** * Whether the OAuth client is public or confidential * @see https://datatracker.ietf.org/doc/html/rfc6749#section-2.1 */ oauthPublicClient?: boolean; /** * Name passed to the client created by the MCP SDK * @see https://github.com/modelcontextprotocol/typescript-sdk?tab=readme-ov-file#writing-mcp-clients */ mcpClientName: string; /** * Version number passed to the client created by the MCP SDK * @see https://github.com/modelcontextprotocol/typescript-sdk?tab=readme-ov-file#writing-mcp-clients */ mcpClientVersion: string; /** * A function that, when called with a url, will redirect to the given url */ redirect: (url: string) => void; /** * A persistent store for auth data * @see https://github.com/clerk/mcp-tools?tab=readme-ov-file#stores */ store: McpClientStore; }
-
Return Type:
interface McpClientReturnType { /** * Represents a session associated with the connected MCP service endpoint. */ sessionId: string; /** * Calling this function will initialize a connect to the MCP service. */ connect: () => void; /** * Lower level primitive, likely not necessary for use * @see https://github.com/modelcontextprotocol/typescript-sdk/blob/main/src/client/streamableHttp.ts#L119 */ transport: StreamableHTTPClientTransport; /** * Lower level primitive, likely not necessary for use * @see https://github.com/modelcontextprotocol/typescript-sdk/blob/main/src/client/index.ts#L81 */ client: Client; /** * Lower level primitive, likely not necessary for use * @see https://github.com/modelcontextprotocol/typescript-sdk/blob/main/src/client/auth.ts#L13 */ authProvider: OAuthClientProvider; }
-
-
getClientBySessionId- Description: Given an existing session id, constructs a MCP client that matches the information used to create the client/session initially. Intended to be used in OAuth callback routes and any subsequent MCP calls once the service has been initialized.
- Arguments:
interface GetClientBySessionIdParams { /** * The session id to retrieve the client details for */ sessionId: string; /** * A persistent store for auth data * @see https://github.com/clerk/mcp-tools?tab=readme-ov-file#stores */ store: McpClientStore; /** * If using this function in the OAuth callback route, pass in the state to * ensure that PKCE can run correctly. */ state?: string; }
-
Return Type:
interface McpClientReturnType { /** * Represents a session associated with the connected MCP service endpoint. */ sessionId: string; /** * Calling this function will initialize a connect to the MCP service. */ connect: () => void; /** * Lower level primitive, likely not necessary for use * @see https://github.com/modelcontextprotocol/typescript-sdk/blob/main/src/client/streamableHttp.ts#L119 */ transport: StreamableHTTPClientTransport; /** * Lower level primitive, likely not necessary for use * @see https://github.com/modelcontextprotocol/typescript-sdk/blob/main/src/client/index.ts#L81 */ client: Client; /** * Lower level primitive, likely not necessary for use * @see https://github.com/modelcontextprotocol/typescript-sdk/blob/main/src/client/auth.ts#L13 */ authProvider: OAuthClientProvider; }
-
completeAuthWithCode-
Description: Designed to be used in the OAuth callback route. Passing in the code, state, and your store will finish the auth process
-
Arguments:
interface CompleteAuthWithCodeParams { /** * The authorization code returned from the auth provider via querystring. * @see https://datatracker.ietf.org/doc/html/rfc6749#section-4.1 */ code: string; /** * The state returned from the auth provider via querystring. * @see https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.1 */ state: string; /** * A persistent store for auth data * @see https://github.com/clerk/mcp-tools?tab=readme-ov-file#stores */ store: McpClientStore; }
-
Return Type:
interface CompleteAuthWithCodeReturnType { /** * Lower level primitive, likely not necessary for use * @see https://github.com/modelcontextprotocol/typescript-sdk/blob/main/src/client/streamableHttp.ts#L119 */ transport: StreamableHTTPClientTransport; /** * Represents a session associated with the connected MCP service endpoint. */ sessionId: string; }
-
-
generateProtectedResourceMetadata-
Description: Generates OAuth 2.0 Protected Resource Metadata as defined by RFC 9728. This metadata helps OAuth clients understand how to authenticate with your resource server.
-
Arguments:
interface GenerateProtectedResourceMetadataParams { /** * URL of the OAuth authorization server */ authServerUrl: string; /** * URL of the resource server that the metadata applies to */ resourceUrl: string; /** * Additional properties to include in the metadata */ properties?: Record<string, any>; }
-
Return Type:
{ resource: string; authorization_servers: string[]; token_types_supported: string[]; token_introspection_endpoint: string; token_introspection_endpoint_auth_methods_supported: string[]; jwks_uri: string; authorization_data_types_supported: string[]; authorization_data_locations_supported: string[]; key_challenges_supported: Array<{ challenge_type: string; challenge_algs: string[]; }>; [key: string]: any; // Additional properties if provided }
-
-
generateClerkProtectedResourceMetadata-
Description: Generates OAuth 2.0 Protected Resource Metadata specifically configured for Clerk authentication. This is a convenience wrapper around
generateProtectedResourceMetadatathat automatically derives the auth server URL from your Clerk publishable key. -
Arguments:
interface GenerateClerkProtectedResourceMetadataParams { /** * Your Clerk publishable key (e.g., pk_test_... or pk_live_...) */ publishableKey: string; /** * URL of the resource server that the metadata applies to */ resourceUrl: string; }
-
Return Type: Same as
generateProtectedResourceMetadata
-
-
fetchClerkAuthorizationServerMetadata-
Description: Fetches OAuth 2.0 Authorization Server Metadata from Clerk's servers based on your publishable key. This returns the actual metadata from Clerk's authorization server rather than generating it locally.
-
Arguments:
interface FetchClerkAuthorizationServerMetadataParams { /** * Your Clerk publishable key (e.g., pk_test_... or pk_live_...) */ publishableKey: string; }
-
Return Type:
Promise<{ issuer: string; authorization_endpoint: string; token_endpoint: string; userinfo_endpoint: string; jwks_uri: string; registration_endpoint?: string; scopes_supported: string[]; response_types_supported: string[]; response_modes_supported: string[]; grant_types_supported: string[]; subject_types_supported: string[]; id_token_signing_alg_values_supported: string[]; token_endpoint_auth_methods_supported: string[]; claims_supported: string[]; code_challenge_methods_supported: string[]; [key: string]: any; // Additional metadata from Clerk }>;
-
-
isClerkAuthInfo- Description: Returns
truewhen anAuthInfoobject came fromverifyClerkToken, which marks its result withextra.verifiedBy: 'clerk'and a non-emptyextra.userId. Use it to tell Clerk user and client ids apart from ones produced by a custom verifier. It is a type guard: after the check, TypeScript knowsauthInfo.extra.userIdis a string. - Params:
authInfo: AuthInfo | undefined - Returns:
authInfo is ClerkAuthInfo(ClerkAuthInfois also exported)
- Description: Returns
-
requireAdditionalScopes- Description: Builds the
scopeChallengeregistration option of MCP SDK 2.1 or later for a tool that needsrequiredScopes. Before the tool runs, the SDK answers a token that lacks some of them with a403step-up challenge for the granted scopes followed by the missing ones. A request without auth info is left to the tool. SDK 1.x has no such option. See Step-up for your own tools. - Params:
...requiredScopes: string[], one or more scopes as separate arguments - Returns: the function to pass as the
scopeChallengeoption,(context: { authInfo?: AuthInfo }) => { scopes: readonly [string, ...string[]] } | undefined
- Description: Builds the
-
corsHeaders-
Description: Pre-configured CORS headers for OAuth metadata endpoints. These headers allow cross-origin requests to your OAuth metadata endpoints, which is necessary for web-based MCP clients.
-
Value:
{ "Access-Control-Allow-Origin": "*"; "Access-Control-Allow-Methods": "GET, OPTIONS"; "Access-Control-Allow-Headers": "*"; "Access-Control-Max-Age": "86400"; }
-
For detailed documentation on server utilities and framework-specific implementations, see:
For framework-specific utilities and handlers, see:
- Express.js: See Express.js reference documentation
- Hono: See Hono reference documentation
- Next.js: See Next.js reference documentation
