modelcontextprotocol / modelcontextprotocol/typescript-sdk

[v2] Incompatibility with refresh_tokens from Azure when client_id=resource_id

Open
#2,718 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

v2
Dominant language
TypeScript
Stars
13.4k
Forks
2.2k
Avg merge
3d 15h
Merged PRs (30d)
4

Description

What happened?

I have some MCP servers which require access_tokens from login.microsoftonline.com, and for which the OAuth client ID and the OAuth resource ID are the same Azure app.

All AI agent harnesses can successfully authenticate to these MCP servers initially, but several fail to fetch new access_tokens when they expire using the refresh_token.

This is because some MCP client libraries (including this one) don't handle the scope parameter when using a refresh_token in the way that Azure requires. This leads to the following misleading error message:

$ node repro.mjs "${my_tenantid}" "${my_clientid}" "api://${my_clientid}/access_as_user"
...
InvalidRequestError: AADSTS90009: Application 'REDACTED'(REDACTED) is requesting a token for it
self. This scenario is supported only if resource is specified using the GUID based App Identifier. Trace ID: REDACTED Correlation ID: REDACTED Timestamp: 2026-08-26 01:06:39Z

One way to fix this error actually has nothing to do with whether an app is requesting a token for itself or not - it is to pass along the same scope parameter with the refresh request as was used in the original request.

How's the support for other harnesses?

Other related issues:

What did you expect?

No response

Code to reproduce
import { randomUUID } from "node:crypto";
import http from "node:http";
import { exchangeAuthorization, refreshAuthorization, startAuthorization } from "@modelcontextprotocol/client";

const [tenant, clientId, apiScope] = process.argv.slice(2);
if (!tenant || !clientId || !apiScope) {
  console.error('Usage: node repro.mjs TENANT_ID CLIENT_ID "API_SCOPE"');
  process.exit(2);
}

const root = `https://login.microsoftonline.com/${tenant}/oauth2/v2.0`;
const redirectUri = "http://localhost:53682/callback";
const scope = `openid profile offline_access ${apiScope}`;
const clientInformation = { client_id: clientId };
const metadata = {
  authorization_endpoint: `${root}/authorize`,
  token_endpoint: `${root}/token`,
  response_types_supported: ["code"],
  code_challenge_methods_supported: ["S256"],
};
const state = randomUUID();
const { authorizationUrl, codeVerifier } = await startAuthorization(root, {
  metadata, clientInformation, redirectUrl: redirectUri, scope, state,
});

const authorizationCode = await new Promise((resolve, reject) => {
  const server = http.createServer((request, response) => {
    const url = new URL(request.url, redirectUri);
    if (url.pathname !== "/callback") return response.writeHead(404).end();
    const code = url.searchParams.get("code");
    const error = url.searchParams.get("error");
    const valid = url.searchParams.get("state") === state;
    response.end(valid && code ? "Authorization complete." : "Authorization failed.");
    server.close();
    if (error) reject(new Error(error));
    else if (!valid || !code) reject(new Error("Invalid OAuth callback"));
    else resolve(code);
  });
  server.once("error", reject);
  server.listen(53682, () => console.log(`Open this URL:\n\n${authorizationUrl}\n`));
});

const trace = (label) => async (url, init) => {
  const body = new URLSearchParams(init.body.toString());
  console.log(`${label}:`, { grant_type: body.get("grant_type"), scope: body.get("scope") ?? "<redacted>" });
  return fetch(url, init);
};
const initial = await exchangeAuthorization(root, {
  metadata, clientInformation, authorizationCode, codeVerifier, redirectUri,
  addClientAuthentication: async (_headers, body) => {
    body.set("client_id", clientId);
    body.set("scope", scope);
  },
  fetchFn: trace("initial request"),
});
if (!initial.refresh_token) throw new Error("No refresh token returned");
console.log("Initial access and refresh tokens received.");

const refreshed = await refreshAuthorization(root, {
  metadata, clientInformation, refreshToken: initial.refresh_token,
  fetchFn: trace("refresh request"),
});
console.log("Refreshed access token received:", !!refreshed.access_token);
SDK version

1.29.0 and also 2.0.0

Area

Auth

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with the refreshAuthorization call in the provided repro.mjs example and trace how its refresh request is assembled. Run the reproduction against Azure, compare the initial and refresh request scopes, and verify that the refreshed access token succeeds when client_id and resource_id match.

Written by the indexing model from the issue text.

Assessment

Tech stack
typescript
Domain
authentication
Issue type
Bug
Difficulty
3/5
Estimated time
1-2 days
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
72/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.