Skip to content

Migrating to v3 ​

v3 narrows the library to one job — turning a browser authorization into a validated authorization code on a loopback redirect URI — and lets the MCP SDK own OAuth in oauth-callback/mcp. Requirements: Node.js 22+, Deno 2, or Bun 1.2+; @modelcontextprotocol/client 2.2+ for /mcp.

getAuthCode() ​

ts
// v2
const result = await getAuthCode({
  authorizationUrl: url, // with redirect_uri=http://localhost:3000/callback
  port: 3000,
  launch: true,
  timeout: 30000,
});
result.code;
result.state;

// v3
const { code, redirectUri, params } = await getAuthCode(url);
params.get("state");
v2v3
getAuthCode({ authorizationUrl })getAuthCode(url, options?), or a builder getAuthCode(({ redirectUri, state }) => url)
port, hostname, callbackPathredirectUri option, or the URL's redirect_uri; builder default http://127.0.0.1:0/callback
launch: truedefault
launch: falselaunch: (url) => console.log(url) — the final URL includes the generated state
launch: fn(url: string)launch: (url: URL) => …; a rejection now fails the flow
timeout default 30 sdefault 5 min; integer ms in [1, 2³¹−1]
result { code, state, …params }{ code, redirectUri, params: URLSearchParams }
OAuthError (error_description, error_uri)OAuthCallbackError (error, description, uri, params)
TimeoutError classDOMException with name === "TimeoutError"
abort → Error("Operation aborted")rejects with signal.reason
errorHtml placeholders static HTML, served verbatim (callback data is never rendered)
onRequestremoved
getRedirectUrl()removed — use the builder's redirectUri
mcp namespace, root store exportsremoved — import from oauth-callback/mcp

Behavior changes:

  • State is always present. A URL without state gets one appended; callbacks must echo it.
  • URLs are validated before launch. https: (or loopback http:), no fragment or credentials, response_type and response_mode, if present, must be code and query. Prebuilt PAR/JAR URLs are rejected — use the builder.
  • Default host is 127.0.0.1, not localhost (RFC 8252 §8.3). localhost redirect URIs still work.
  • Register the loopback redirect your provider expects, e.g. http://127.0.0.1/callback for providers that allow any loopback port (RFC 8252 §7.3), or a fixed http://127.0.0.1:8765/callback with redirectUri.

oauth-callback/mcp ​

ts
// v2
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
import { browserAuth, fileStore } from "oauth-callback/mcp";

const authProvider = browserAuth({
  port: 3000,
  scope: "read write",
  store: fileStore(),
});
try {
  await client.connect(
    new StreamableHTTPClientTransport(serverUrl, { authProvider }),
  );
} catch {
  await client.connect(
    new StreamableHTTPClientTransport(serverUrl, { authProvider }),
  );
}

// v3
import { Client } from "@modelcontextprotocol/client";
import { browserAuth, fileStore } from "oauth-callback/mcp";

const auth = browserAuth({
  serverUrl,
  redirectUri: "http://127.0.0.1:3000/callback",
  clientName: "My CLI",
  clientMetadata: { scope: "read write" },
  store: fileStore(path.join(os.homedir(), ".config/my-cli/mcp.json")),
});
await auth.connect(client);
v2v3
@modelcontextprotocol/sdk 1.x@modelcontextprotocol/client 2.2+
connect-retry boilerplateauth.connect(client); custom transports: auth.completeAuthorization(transport)
port, hostname, callbackPathredirectUri (required, fixed port)
—serverUrl (required; the provider and its store serve one server)
DCR client name fixedclientName (for DCR; or clientInformation)
clientId, clientSecretclientInformation: { client_id, client_secret?, issuer }
scopeclientMetadata: { scope }; the server's challenge and metadata take priority
authTimeouttimeout (one interactive authorization, browser through token exchange)
store: TokenStore, storeKeystore: CredentialStore (load()/save(text)), one per server
inMemoryStore()default
fileStore() → ~/.mcp/tokens.jsonfileStore(absolutePath), no default path
no refresh tokensrefresh handled by the SDK
onRequestremoved

v3 doesn't migrate v2 token files: point fileStore() at a new file (an old one makes connect() throw Stored MCP credentials have an unsupported format), or clear it with invalidateCredentials("all"). Users authorize once after upgrading. To opt into offline_access refresh tokens, declare clientMetadata: { grant_types: ["authorization_code", "refresh_token"] }.