Manufact

How to add Mixed Authentication to your MCP Server

Andrew Khadder
Andrew KhadderFounding Engineer
How to add Mixed Authentication to your MCP Server

Shipping your MCP server to ChatGPT and Claude users is easier than ever. Then a new user adds your app and hits a wall: before they see a single tool, your server asks them to create an account.

On your website, nobody signs up first. They read your homepage, scroll through features, check pricing, and maybe watch a demo before they click "Sign up." In ChatGPT or Claude, those pages don't exist. A user finds your app in a directory, reads a short description, clicks connect, and your server asks for an account.

Your tools have to do the job your landing page did. Mixed authentication lets users try them before they sign up. For example, in a shop app, anyone can browse the catalog and get recommendations, and the server asks for sign-in only at checkout.

We just shipped first-class support for mixed authentication in mcp-use 2.7.0 and I'll show you how to use it.

The API

Mixed authentication adds two things to the SDK: a mixedAuth flag on the server constructor and a securitySchemes field on each tool. The examples use WorkOS, and every mcp-use OAuth provider supports mixed authentication.

Turn on mixedAuth

With oauth enabled, every request to your server needs a token, including the first one. Turn on mixedAuth in the server constructor, and signed-out users can connect and see your tools. Each tool then decides whether it needs sign-in or not.

Requestoauth onlyoauth + mixedAuth
ConnectNeeds sign-inOpen
List toolsNeeds sign-inOpen
Call a toolNeeds sign-inFollows the tool's securitySchemes

Declare securitySchemes on each tool

securitySchemes describes who can call a tool and what sign-in, if any, the call needs. You build it from two scheme types:

  • { type: "noauth" } runs without a token.
  • { type: "oauth2", scopes } needs a signed-in user. scopes lists any extra permissions the tool needs, and [] means none.

Combine the two types and you get four access levels:

securitySchemesWho can call itctx.auth
[{ type: "noauth" }]Anyonepossibly undefined
[{ type: "noauth" }, { type: "oauth2", scopes }]Anyone, personalized when signed inpossibly undefined
[{ type: "oauth2", scopes }]Signed-in users with the extra scopesalways set
omittedAny signed-in useralways set

The shop server

Here's an example shop server built with mcp-use:

import { MCPServer } from "mcp-use";
import { oauthWorkOSProvider } from "mcp-use/oauth/workos";
import { z } from "zod";
 
const server = new MCPServer({
  name: "shop",
  version: "1.0.0",
  oauth: oauthWorkOSProvider({
    subdomain: process.env.WORKOS_SUBDOMAIN!,
    requiredScopes: ["shop"],
  }),
  mixedAuth: true,
});
 
// Public: anyone can browse.
server.tool(
  {
    name: "list_bestsellers",
    description: "List this week's bestselling products.",
    securitySchemes: [{ type: "noauth" }],
  },
  async () => ({
    content: [{ type: "text", text: await bestsellers() }],
  }),
);
 
// Optional: personalized when signed in, bestsellers otherwise.
server.tool(
  {
    name: "recommend_products",
    description: "Suggest products. Personalized when the user is signed in.",
    securitySchemes: [{ type: "noauth" }, { type: "oauth2", scopes: [] }],
  },
  async (_args, ctx) => {
    if (!ctx.auth) {
      return { content: [{ type: "text", text: await bestsellers() }] };
    }
    return {
      content: [{ type: "text", text: await picksFor(ctx.auth.user.id) }],
    };
  },
);
 
// Sign-in with an extra scope: refused before the callback runs.
server.tool(
  {
    name: "create_checkout",
    description: "Check out the selected products.",
    inputSchema: z.object({ productIds: z.array(z.string()).min(1) }),
    securitySchemes: [{ type: "oauth2", scopes: ["checkout"] }],
  },
  async ({ productIds }, ctx) => ({
    content: [
      { type: "text", text: await createCheckout(productIds, ctx.auth.user.id) },
    ],
  }),
);
 
// Omitted: sign-in with the provider's requiredScopes.
server.tool(
  { name: "order_history", description: "List the signed-in user's orders." },
  async (_args, ctx) => ({
    content: [{ type: "text", text: await ordersFor(ctx.auth.user.id) }],
  }),
);
 
export default server;

How mixed authentication works in ChatGPT and Claude

ChatGPT and Claude both support mixed authentication. Signed-out users can call public tools, and the host asks for sign-in when they call a protected one. The way that the clients trigger their authentication UI is handled differently.

ChatGPT

mcp-use's securitySchemes follows ChatGPT's API. ChatGPT reads securitySchemes from your tool list to learn which tools need sign-in. When a signed-out user calls one, the server returns a tool result marked isError with a sign-in challenge in _meta["mcp/www_authenticate"]. OpenAI's authentication docs cover the details.

Loading diagram...

Claude

Claude calls this lazy authentication. When a signed-out user calls a protected tool, the server fails the HTTP request with a 401 and a WWW-Authenticate header. Claude shows an inline Connect card, the user signs in through a popup, and Claude retries the same tool call with the new token.

The HTTP status is the whole signal. If the server answers with a successful response that wraps a tool error, Claude hands the error text to the model, and the user sees "please sign in" in the chat with no Connect card. For a signed-in user who lacks a permission, the server returns a 403 with insufficient_scope, and Claude asks the user to approve the extra scope.

Loading diagram...

Side by side

ChatGPTClaude
Starts sign-in from200 result with isError and _meta["mcp/www_authenticate"]HTTP 401, or 403 with insufficient_scope
Treats the other format asNot a sign-in triggerAn ordinary tool failure
Learns a tool needs sign-inFrom securitySchemes on tools/listWhen the call is refused
oauth2 scheme with no scopesIgnored, so set requiredScopes on the providerWorks

You declare each tool once and never write either format yourself. mcp-use checks the User-Agent header on every refused call, answers ChatGPT with the tool result, and answers every other client with the HTTP status.

Get started

Upgrade to mcp-use 2.7.0 and add mixedAuth: true to a server that already uses OAuth. Every tool keeps requiring sign-in until you give it securitySchemes, so you can open tools one at a time and leave the rest of the server as it is.

The mixed-oauth example runs a mixed-auth server locally, with a tool for every access level in this post. Its README walks through connecting it to ChatGPT and Claude to test the sign-in flow end to end. For the full reference, see the mixed authentication docs.

Share