CodeGym /Courses /ChatGPT Apps /Testing login and access via MCP Jam: None, Bearer, OAuth...

Testing login and access via MCP Jam: None, Bearer, OAuth with credentials, Default OAuth

ChatGPT Apps
Level 10 , Lesson 4
Available

1. MCP Jam as a lab for authorization

MCP Jam is not “just another weird tool,” but your lab bench that can play the role of an MCP client. Essentially, it is an emulator of ChatGPT’s behaviour when working with an MCP server: it can read .well-known/oauth-protected-resource, run the OAuth flow, attach tokens to requests, and show you exactly what went wrong.

A very important practical point: if you achieve a successful Default OAuth flow in MCP Jam, you are about 80% ready to integrate with a real ChatGPT App. Everything ChatGPT does during account linking, Jam can already do — just with more transparent logs and buttons.

In the previous lecture, we configured basic authorization for our training MCP server GiftGenius: we chose a token validation strategy (JWT or introspection), implemented .well-known/oauth-protected-resource and middleware to protect tools. Now let’s see how all of this behaves in MCP Jam in different authorization modes.

Our goal in this lecture is to learn to:

  • intentionally switch authorization modes in Jam (None, Bearer, OAuth with credentials, Default OAuth);
  • understand what exactly Jam sends to the MCP server in each mode;
  • diagnose which part of the system is broken: the MCP Server, the Auth Server, or the metadata;
  • verify that protected tools work only with a token, while open ones also work without it.

2. Our training MCP server: what we are testing

To avoid speaking abstractly, let’s briefly recall the context. We continue with our training app GiftGenius — a ChatGPT App that helps choose gifts and shows users their orders and wishlists.

On the MCP server side we already have:

  • an open tool, for example search_gifts — it can be called anonymously;
  • a protected tool, for example list_user_orders — it should work only for an authenticated user and require the mcp:tools scope.

The server can:

  • publish .well-known/oauth-protected-resource;
  • validate a token (JWT or via introspection — you chose one approach in the previous lecture);
  • extract sub (user id), scope, aud from the token and pass them to tool handlers.

A typical token verification middleware in Node.js/TypeScript might look like this:

// middleware/auth.ts
export function requireScope(requiredScope: string) {
  return async (req: any, res: any, next: () => void) => {
    const header = req.headers["authorization"];
    if (!header?.startsWith("Bearer ")) {
      res
        .status(401)
        .set(
          "WWW-Authenticate",
          `Bearer realm="mcp", resource_metadata="${process.env.BASE_URL}/.well-known/oauth-protected-resource", scope="${requiredScope}"`
        )
        .json({ error: "unauthorized" });
      return;
    }

    // here you validate the token (signature, exp, aud, scope...)
    // and put the result into req.user
    next();
  };
}

This middleware will be used before protected MCP tools. If there is no token, we return 401 and a proper WWW-Authenticate with resource_metadata, as required by the MCP Authorization specification. You already did a deep dive into token validation and helper function implementations in the previous lecture; here we treat that as a given.

3. Authorization modes in MCP Jam: overview

MCP Jam has several authorization modes for connecting to an MCP server. They correspond to typical OAuth patterns: from no token at all to a full Authorization Code + PKCE.

Briefly:

  1. None (No Auth) — Jam does not add the Authorization header at all. This is anonymous access. Suitable for open MCP servers and for verifying that closed resources correctly reject with 401 and WWW-Authenticate.
  2. Bearer Token — Jam adds Authorization: Bearer <token>, which you paste into the UI manually. Suitable for quick checks: you already obtained a token somewhere (curl, Keycloak UI), and you want to test the MCP resource’s behaviour.
  3. OAuth with credentials (Client Credentials) — Jam obtains a token itself via client_credentials from the Auth Server using the provided Client ID and Secret. This is the “confidential client” mode, more like server‑to‑server authorization without user involvement.
  4. Default OAuth (Authorization Code + PKCE) — the main mode for ChatGPT‑like clients (a public client without a secret). Jam reads resource_metadata, discovers the Auth Server, opens the browser to /authorize, performs the PKCE flow, and obtains a user token.

For clarity, let’s condense this into a table.

Mode in Jam What Jam sends Who obtains the token Typical scenario
None No Authorization Nobody Anonymous tools, 401 check
Bearer Token Bearer <manual> You (curl, IdP UI) Testing Resource Server logic
OAuth with cred. Bearer <client token> Jam via client_credentials Service/admin tools
Default OAuth Bearer <user token> Jam via Authorization Code+PKCE User login like in ChatGPT

Now let’s go through each mode and see how to run our GiftGenius MCP server through it.

4. None mode: verify the server rejects correctly

Let’s start with the most primitive mode: no authorization.

In MCP Jam, select your server (for example, http://localhost:4000/mcp) and in the connection settings choose the None authorization mode.

What happens:

  • Jam establishes the MCP connection;
  • when invoking a tool it does not add the Authorization header;
  • you can call any open tools (for example, search_gifts);
  • when calling a protected tool (for example, list_user_orders), your server should respond with 401 Unauthorized.

It is important that for this 401 the server adds a proper WWW-Authenticate. Here is an example response with additional realm and scope fields, close to what OpenAI and the MCP Authorization spec recommend:

HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer realm="mcp",
  resource_metadata="https://giftgenius.example.com/.well-known/oauth-protected-resource",
  scope="mcp:tools"
Content-Type: application/json

{"error": "unauthorized"}

Seeing such a response, Jam understands that the resource is protected, where to fetch metadata from (resource_metadata), and which scopes are expected. In None mode, it will just show you an error, but in Default OAuth mode, it will automatically follow the provided resource_metadata and launch the OAuth flow.

From a debugging standpoint, in None mode you verify that:

  • open tools work without a token at all;
  • protected tools never execute anonymously;
  • the WWW-Authenticate header matches the spec (includes Bearer and resource_metadata).

It might sound like a trivial check, but lots of problems start with 401 being returned without WWW-Authenticate or with an incorrect parameter in it (for example, legacy resource_metadata_uri instead of the current resource_metadata).

5. Bearer Token mode: a quick test of Resource Server logic

The next step is a mode where you already have a valid token (obtained outside Jam) and you want to test exactly the Resource Server logic: whether it accepts/rejects this token correctly, works with the scope and audience correctly, and maps sub to your service’s user.

In MCP Jam, switch the mode to Bearer Token and paste into the token field something like:

eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...

Jam will now add the following header to every MCP request:

Authorization: Bearer eyJhbGciOi...

Your MCP server receives the request, passes through the requireScope("mcp:tools") middleware, decodes the JWT, and checks claims. A typical verification snippet can be written in simplified form as:

// auth/verifyToken.ts
import jwt from "jsonwebtoken";

export function verifyToken(header: string) {
  const token = header.replace("Bearer ", "");
  const payload = jwt.verify(token, process.env.JWT_PUBLIC_KEY!);
  // here you can check aud, scope, etc.
  return payload as { sub: string; scope?: string };
}

And used in the middleware:

// inside requireScope
const payload = verifyToken(header);
if (!payload.scope?.includes(requiredScope)) {
  res.status(403).json({ error: "insufficient_scope" });
  return;
}
(req as any).user = { id: payload.sub };
next();

In Bearer mode you can experiment:

  • insert a token without the required scope and make sure the server returns 403/401;
  • insert a token with an incorrect aud and see that the server rejects it;
  • insert an expired token to verify the invalid_token error.

This is a local “stress test” mode for Resource Server logic without UI login and PKCE. Everything you check here will apply one-to-one to the tokens that ChatGPT or Jam will obtain in Default OAuth mode.

6. OAuth with credentials (Client Credentials) mode: a token “on behalf of the application”

Now a less common but useful mode to understand: OAuth with credentials, that is the client_credentials grant. In Jam you specify:

  • Client ID
  • Client Secret
  • the required scopes (for example, mcp:tools)

Jam performs a request to your Auth Server’s token_endpoint roughly like this:

POST /oauth2/token
Content-Type: application/x-www-form-urlencoded

grant_type=client_credentials&
client_id=<ID>&
client_secret=<SECRET>&
scope=mcp:tools

The Auth Server issues a token where sub usually refers to the client itself (for example, sub = "mcp-jam-test-client"), not a specific user. Jam starts using this token as a regular Bearer.

What this can be useful for in the MCP world:

  • service/admin tools not tied to a specific user (for example, log export, health check, tech support);
  • verifying that the MCP server can distinguish user tokens from “client” tokens, if your business logic relies on that.

In the context of ChatGPT Apps this mode is usually not used, because ChatGPT as a public client does not store secrets (and a public client by definition should not have a client_secret). But in Jam it helps illustrate the difference between:

  • “I simply supplied a ready-made token” (Bearer mode);
  • “Jam fetched a token itself using client credentials” (OAuth with credentials).

On the training server you can, for example, add a special MCP tool admin_list_all_orders that is available only with a token whose grant_type is client_credentials and an appropriate role. This is not a mandatory part of today’s lecture, but a useful experiment.

7. Default OAuth mode: full Authorization Code + PKCE, like ChatGPT

Now the star of the show: Default OAuth. This mode is closest to what ChatGPT does when linking your App’s account. The client reads resource_metadata, goes to the Auth Server, opens the login page for the user, obtains an authorization code, and exchanges it for an access token using Authorization Code + PKCE S256.

Let’s break down the sequence of steps. For clarity — a diagram.

sequenceDiagram
    participant Jam as MCP Jam (Client)
    participant RS as MCP Server (Resource)
    participant PRM as /.well-known/oauth-protected-resource
    participant AS as Auth Server (Keycloak/Auth0)
    
    Jam->>RS: Call protected tool (no token)
    RS-->>Jam: 401 + WWW-Authenticate (resource_metadata=PRM)
    Jam->>PRM: GET /.well-known/oauth-protected-resource
    PRM-->>Jam: JSON with resource, authorization_servers, scopes_supported...
    Jam->>AS: GET /authorize?client_id=...&code_challenge=...&scope=...
    Note right of AS: User logs in and gives consent
    AS-->>Jam: redirect with authorization_code
    Jam->>AS: POST /token (code + code_verifier)
    AS-->>Jam: { access_token, scope, expires_in, ... }
    Jam->>RS: Call tool with Authorization: Bearer <access_token>
    RS-->>Jam: Successful tool result

What you need to verify in this mode:

  1. A correct 401/WWW-Authenticate response from the MCP server. If the server does not return resource_metadata, or returns an incorrect URL, Jam will not be able to read the PRM and start the OAuth flow.
  2. A valid .well-known/oauth-protected-resource document. It must contain correct resource, authorization_servers, scopes_supported, etc., so that Jam can figure out where to get tokens and which scopes to request.
  3. Proper Auth Server configuration.
    • Authorization Code Flow with PKCE S256 is enabled.
    • The Client ID matches what is expected in the PRM (or is registered via DCR — Dynamic Client Registration).
    • The Redirect URI in the Auth Server exactly matches the one Jam uses.
  4. PKCE S256. Jam forms a code_challenge and expects the Auth Server to support the S256 method. If PKCE is disabled or only “plain” is supported, the flow will break.
  5. Scopes and audience. The Auth Server must issue a token with the required aud and the requested scopes (mcp:tools, etc.), and the MCP server must verify them.

As a result of a successful Default OAuth you will get:

  • in Jam — a connection to the MCP server where the protected tool list_user_orders returns correct data specifically for the user you logged in as on the Auth Server;
  • in the Auth Server logs — a successful authorize + token exchange;
  • in the MCP server logs — successful token validation and extraction of sub.

For debugging it often helps to add a simple logger in the tool handler, to ensure you indeed see the userId from the token:

// inside the MCP tool handler list_user_orders
export async function listUserOrders(args: any, context: any) {
  const user = context.user as { id: string };
  console.log("[MCP] listUserOrders for user", user.id);
  // then you return this user's orders
}

8. Where things break: diagnostics by mode

Now let’s discuss how to understand from symptoms in MCP Jam where exactly the problem is: in the MCP server, the Auth Server, or the metadata. This section is a kind of diagnostic checklist by mode.

If in None mode:

You call a protected tool, and the server returns:

  • 200 OK and performs the action even without a token — then you do not have token verification in front of this tool. You need to add a middleware or scope checks.
  • 401, but without WWW-Authenticate or with a malformed resource_metadata — Jam will not know where to fetch the metadata from and will not be able to start Default OAuth. Fix the header per the example above.

If in Bearer Token mode:

  • Jam consistently gets 401/403 even with a token you are sure is valid when called directly (via curl or Postman). Most likely something is wrong in the Resource Server logic: incorrect aud/scope check or the wrong public key for JWT signature verification.
  • If the Bearer token works in Jam, but then does not work in Default OAuth — then the problem is not in the MCP server, but in the Auth Server or PRM: the token obtained via Default OAuth differs in scope/aud from the one you tested manually.

If in OAuth with credentials mode:

  • If Jam cannot obtain a token (error at the /token step) — look for the cause in the client configuration on the Auth Server (incorrect secret, disallowed client_credentials, or a forbidden scope).
  • If there is a token but the MCP server rejects it — your server might be expecting a user sub (user email/ID), while the token contains only the client identifier. Or the aud/scope do not match what is expected.

If in Default OAuth mode:

This is the scenario richest in pitfalls. Common problems:

  • Incorrect redirect URIs. The Auth Server complains about invalid_redirect_uri or simply does not issue a code. Make sure Jam’s URI is added to the IdP client settings without extra slashes or typos.
  • Missing or unsupported PKCE. If the Auth Server requires PKCE and Jam (or an older version) does not send a code_challenge, or vice versa — Jam sends S256 and the IdP does not support this method, you will see invalid_request.
  • Mismatched scopes. In the PRM you declared mcp:tools, but the client is allowed only openid in the IdP, or conversely — Jam requests more scopes than the IdP is willing to issue.
  • Wrong audience (aud). The token is issued with an aud different from what the MCP server expects (for example, a URL of another resource). The server will rightly reject it.

It is very important to learn to look at logs in three places:

  • MCP Jam — errors while parsing PRM and during HTTP requests to the Auth Server;
  • Auth Server — logs of /authorize and /token will hint at what it is rejecting;
  • MCP server — reasons for token rejection (invalid_token, insufficient_scope, wrong_audience).

9. How this relates to a real ChatGPT App

Why are we spending so much time playing with Jam instead of jumping straight into ChatGPT’s Developer Mode? Because Jam is precisely a lab bench: it gives you control over authorization modes and reveals the entire inner workings of the flow.

When you run Default OAuth in Jam and bring it to success, you effectively confirm that:

  • .well-known/oauth-protected-resource on the MCP server is correct;
  • the Auth Server (Keycloak/Auth0/…) is configured properly;
  • roles, scopes, audience, and claims match expectations;
  • the MCP server can validate the token and bind it to the user.

ChatGPT connected to the same MCP server will do the same: read the PRM, go to the Auth Server, get a token, and start invoking tools with Authorization: Bearer.

The difference is that in ChatGPT you only see the final result (“account linked successfully” or “something went wrong”), whereas in Jam you see the entire protocol and can pinpoint step by step where exactly it went “wrong.”

10. Mini practice: step-by-step testing of our GiftGenius MCP server

Let’s put everything into a simple sequential scenario you can repeat in your project.

First, start your MCP server (for example, pnpm dev:mcp) and make sure:

  • it’s listening at http://localhost:4000/mcp (or your URL);
  • the /.well-known/oauth-protected-resource endpoint returns correct JSON;
  • the Auth Server (Keycloak) is running and has a configured public client for Jam/ChatGPT.

Then:

  1. None mode.
    Connect Jam to the MCP server without authorization. Verify that:
    • search_gifts works;
    • list_user_orders returns 401 with a proper WWW-Authenticate.
  2. Bearer Token mode.
    Obtain an access token via Keycloak (through the UI or curl). Paste it into Jam, call list_user_orders, and make sure that:
    • with a valid token the tool works and returns the orders of the specific user;
    • with a token lacking mcp:tools or with a different aud — the server returns an error.
  3. OAuth with credentials mode.
    If you have a confidential client: specify client_id and client_secret in Jam, set the required scope, call a technical tool (for example, admin_list_all_orders), and verify it works only with such a service token.
  4. Default OAuth mode.
    Enable Default OAuth, call list_user_orders. Jam will:
    • receive 401 + WWW-Authenticate,
    • read the PRM,
    • open the browser where you will log into Keycloak,
    • obtain a token via Authorization Code + PKCE,
    • call the MCP tool with the token, after which you will see your orders in the response.

If all four modes worked as expected — congratulations, you didn’t just “set up something with Keycloak,” you actually understand how to test and debug the entire auth flow.

11. Common mistakes when working with MCP Jam and testing authorization

In practice, these problems often show up as recurring error patterns. Below are several common “don’t do this” scenarios so you can recognize them by their symptoms.

Error #1: expecting a protected tool to work in None mode.
Sometimes a developer turns Jam on in None mode, calls list_user_orders, is surprised by a 401, and then “just in case” removes token verification on the server. As a result, the MCP tool starts working anonymously, which is categorically unacceptable for personal data and commerce scenarios. The None mode exists specifically to verify that the server correctly rejects requests without a token and returns WWW-Authenticate with resource_metadata.

Error #2: a missing or incorrect WWW-Authenticate header.
A very common case: the server returns 401 without WWW-Authenticate or with the legacy resource_metadata_uri parameter. In such cases, Jam (and ChatGPT) do not know where to get the Protected Resource Metadata from, and Default OAuth simply does not start. A minimally sufficient variant is WWW-Authenticate: Bearer resource_metadata="https://.../.well-known/oauth-protected-resource". The realm and scope fields remain optional; the key is to not forget the resource_metadata itself.

Error #3: testing only the Bearer mode and ignoring Default OAuth.
A developer manually gets a token, pastes it into Jam, sees that everything works, and considers the task done. But when it’s time to connect a real ChatGPT, it turns out that .well-known is incorrect, PKCE is not supported, the redirect URI does not match, and linking fails. Testing the Bearer mode is necessary but not sufficient. You must also run Default OAuth; otherwise, you will not verify half of the most important Auth Server and PRM settings.

Error #4: trying to use client_credentials where a user token is needed.
Sometimes in desperation a developer turns on OAuth with credentials in Jam and starts obtaining tokens via client_credentials, then uses them for user tools like list_user_orders. As a result, sub in the token is the client_id, not a real user, and the business logic starts behaving oddly (for example, showing “shared” data or crashing when trying to find a user with such an ID). For ChatGPT scenarios with real users you need Authorization Code + PKCE (Default OAuth), and client_credentials is suitable only for service tasks.

Error #5: inconsistent scopes and audience across PRM, Auth Server, and the MCP server.
In .well-known/oauth-protected-resource you declared the resource as https://giftgenius.example.com and supported scopes as ["mcp:tools"]. The Auth Server issues a token without aud, while the MCP server during verification expects strictly aud = "https://giftgenius.example.com" and the presence of mcp:tools. As a result, the token obtained via Default OAuth is rejected by the MCP server, and you waste half a day chasing “magic.” Always verify that the PRM, the client config in the IdP, and the checks in the MCP server’s middleware agree on audience and scope.

Error #6: using an outdated version of MCP Jam.
The MCP Authorization spec is evolving rapidly; new fields appear (resource_metadata, improved PKCE flow, helper debuggers). If your Jam version is old, it may not understand the latest fields or may work with deprecated parameter names. This leads to surreal bugs: you set everything up by the latest RFC, but Jam simply doesn’t know what to do with it. Before despairing, make sure that Jam is updated to the latest version.

1
Task
ChatGPT Apps, level 10, lesson 4
Locked
Bearer Token mode — dev-JWT + scope/aud/exp validation
Bearer Token mode — dev-JWT + scope/aud/exp validation
1
Task
ChatGPT Apps, level 10, lesson 4
Locked
Default OAuth + OAuth with credentials — two tools, two scopes, two Jam modes
Default OAuth + OAuth with credentials — two tools, two scopes, two Jam modes
1
Survey/quiz
Authentication and Access, level 10, lesson 4
Unavailable
Authentication and Access
Authentication and Access
Comments
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION