Summary
devin mcp login (and the automatic login triggered by devin mcp add) fails for HTTP MCP servers whose authorization server advertises authorization_response_iss_parameter_supported: true in its RFC 8414 metadata.
The authorization server does include a correct iss parameter in the redirect back to http://127.0.0.1:8765/auth/callback (RFC 9207), but Devin CLI reports:
Failed to exchange authorization code for tokens: Authorization server response missing required issuer: expected http://127.0.0.1:4477
The token endpoint is never called. The same server works as soon as the metadata field is omitted, so the callback parsing seems to drop or never read the iss query parameter before the validation runs.
This matches the regression reported for other rmcp-based clients: openai/codex#32472 (closed as duplicate of openai/codex#31573) and kirodotdev/Kiro#10461.
Environment
- Devin CLI 3000.6.19 (e2b252e2), also the copy bundled in Devin Desktop 3.9.19 (
MCP <server>.log shows the same error)
- macOS 26.6.2, arm64
- Node.js 24 for the reproduction server below
Reproduction
A minimal OAuth + MCP server that behaves like a spec-compliant authorization server. It supports dynamic client registration, PKCE, redirects immediately with code, state and iss, and toggles the metadata flag with ADVERTISE_ISS.
server.mjs:
import http from "node:http";
import crypto from "node:crypto";
const PORT = 4477;
const ISSUER = `http://127.0.0.1:${PORT}`;
const ADVERTISE_ISS = process.env.ADVERTISE_ISS !== "0";
const json = (res, status, body, headers = {}) => {
res.writeHead(status, { "content-type": "application/json", ...headers });
res.end(JSON.stringify(body));
};
const readBody = (req) =>
new Promise((resolve) => {
let data = "";
req.on("data", (chunk) => (data += chunk));
req.on("end", () => resolve(data));
});
http
.createServer(async (req, res) => {
const url = new URL(req.url, ISSUER);
const body = await readBody(req);
console.log(req.method, url.pathname + url.search, body);
switch (url.pathname) {
case "/mcp": {
if (!req.headers.authorization) {
return json(res, 401, { jsonrpc: "2.0", id: null, error: { code: -32000, message: "unauthorized" } }, {
"www-authenticate": `Bearer resource_metadata="${ISSUER}/.well-known/oauth-protected-resource/mcp", scope="read write"`,
});
}
const id = JSON.parse(body || "{}").id ?? null;
if (id === null) return res.writeHead(202).end();
return json(res, 200, { jsonrpc: "2.0", id, result: { protocolVersion: "2025-06-18", capabilities: { tools: {} }, serverInfo: { name: "repro", version: "0" } } });
}
case "/.well-known/oauth-protected-resource/mcp":
return json(res, 200, { resource: `${ISSUER}/mcp`, authorization_servers: [ISSUER], scopes_supported: ["read", "write"] });
case "/.well-known/oauth-authorization-server":
return json(res, 200, {
issuer: ISSUER,
authorization_endpoint: `${ISSUER}/authorize`,
token_endpoint: `${ISSUER}/token`,
registration_endpoint: `${ISSUER}/register`,
response_types_supported: ["code"],
grant_types_supported: ["authorization_code", "refresh_token"],
token_endpoint_auth_methods_supported: ["none"],
code_challenge_methods_supported: ["S256"],
scopes_supported: ["read", "write"],
...(ADVERTISE_ISS ? { authorization_response_iss_parameter_supported: true } : {}),
});
case "/register": {
const { redirect_uris = [] } = JSON.parse(body || "{}");
return json(res, 201, { client_id: "repro-client", token_endpoint_auth_method: "none", redirect_uris });
}
case "/authorize": {
const redirect = new URL(url.searchParams.get("redirect_uri"));
redirect.searchParams.set("code", crypto.randomUUID());
redirect.searchParams.set("state", url.searchParams.get("state") ?? "");
redirect.searchParams.set("iss", ISSUER);
console.log("302 ->", redirect.href);
res.writeHead(302, { location: redirect.href });
return res.end();
}
case "/token":
return json(res, 200, { access_token: "repro-access-token", token_type: "Bearer", expires_in: 3600, refresh_token: "repro-refresh-token", scope: "read write" });
default:
return json(res, 404, { error: "not_found" });
}
})
.listen(PORT, "127.0.0.1", () => console.log("listening on", ISSUER, "advertise iss:", ADVERTISE_ISS));
Case A: flag advertised (fails)
ADVERTISE_ISS=1 node server.mjs &
devin mcp add --scope local repro http://127.0.0.1:4477/mcp # login starts automatically
CLI output:
Authorize 'repro' by opening this URL in your browser:
http://127.0.0.1:4477/authorize?response_type=code&client_id=repro-client&state=...&code_challenge=...&code_challenge_method=S256&redirect_uri=http%3A%2F%2F127.0.0.1%3A8765%2Fauth%2Fcallback&scope=read+write&resource=http%3A%2F%2F127.0.0.1%3A4477%2Fmcp
Waiting for authorization...
Warning: OAuth login failed: Failed to exchange authorization code for tokens: Authorization server response missing required issuer: expected http://127.0.0.1:4477
Server log. Note the iss parameter in the redirect and that /token is never requested:
GET /mcp
GET /.well-known/oauth-protected-resource/mcp
GET /.well-known/oauth-authorization-server
POST /register {"client_name":"Devin CLI","redirect_uris":["http://127.0.0.1:8765/auth/callback"],...}
GET /authorize?response_type=code&client_id=repro-client&state=CVxzl5AFYcKag7Ky0VQ9CQ&...
302 -> http://127.0.0.1:8765/auth/callback?code=19181a3e-...&state=CVxzl5AFYcKag7Ky0VQ9CQ&iss=http%3A%2F%2F127.0.0.1%3A4477
Case B: flag omitted (works)
ADVERTISE_ISS=0 node server.mjs &
devin mcp login repro
Successfully authenticated with 'repro'!
Server log now shows the token exchange:
302 -> http://127.0.0.1:8765/auth/callback?code=964725e8-...&state=Yql64_iK2KYyPQnqRRns4A&iss=http%3A%2F%2F127.0.0.1%3A4477
POST /token grant_type=authorization_code&code=964725e8-...&code_verifier=...&client_id=repro-client&redirect_uri=http%3A%2F%2F127.0.0.1%3A8765%2Fauth%2Fcallback&resource=http%3A%2F%2F127.0.0.1%3A4477%2Fmcp
Expected
When the metadata advertises authorization_response_iss_parameter_supported, the iss value from the callback query string should be compared with the metadata issuer and, since they match, the code exchange should proceed.
Impact
Authorization servers that set this flag by default (for example Better Auth's OAuth provider and Keycloak) cannot be used with Devin at all. devin mcp login has no option to relax issuer validation, so the only workaround today is for the server operator to strip a correct field from their discovery document.
Summary
devin mcp login(and the automatic login triggered bydevin mcp add) fails for HTTP MCP servers whose authorization server advertisesauthorization_response_iss_parameter_supported: truein its RFC 8414 metadata.The authorization server does include a correct
issparameter in the redirect back tohttp://127.0.0.1:8765/auth/callback(RFC 9207), but Devin CLI reports:The token endpoint is never called. The same server works as soon as the metadata field is omitted, so the callback parsing seems to drop or never read the
issquery parameter before the validation runs.This matches the regression reported for other rmcp-based clients: openai/codex#32472 (closed as duplicate of openai/codex#31573) and kirodotdev/Kiro#10461.
Environment
MCP <server>.logshows the same error)Reproduction
A minimal OAuth + MCP server that behaves like a spec-compliant authorization server. It supports dynamic client registration, PKCE, redirects immediately with
code,stateandiss, and toggles the metadata flag withADVERTISE_ISS.server.mjs:Case A: flag advertised (fails)
CLI output:
Server log. Note the
issparameter in the redirect and that/tokenis never requested:Case B: flag omitted (works)
ADVERTISE_ISS=0 node server.mjs & devin mcp login reproServer log now shows the token exchange:
Expected
When the metadata advertises
authorization_response_iss_parameter_supported, theissvalue from the callback query string should be compared with the metadataissuerand, since they match, the code exchange should proceed.Impact
Authorization servers that set this flag by default (for example Better Auth's OAuth provider and Keycloak) cannot be used with Devin at all.
devin mcp loginhas no option to relax issuer validation, so the only workaround today is for the server operator to strip a correct field from their discovery document.