Code Mode functions.exec drops mcp/www_authenticate from nested MCP tool results
What version of the Codex App are you using?
Codex Desktop 26.727.51351 (bundle 6119, com.openai.codex)
Bundled/local CLI:
codex-cli 0.146.0-alpha.9.2
What subscription do you have?
Not relevant to the reproduction.
What platform is your computer?
macOS 26.5.1 (build 25F80), arm64
Which model were you using?
GPT-5.6 Sol
What issue are you seeing?
In Code Mode, when an MCP tool is invoked through the nested tools.* API inside functions.exec, Codex does not surface the standard OAuth reauthorization UI from the tool result's _meta["mcp/www_authenticate"].
The nested MCP call returns a valid CallToolResult similar to:
{
"isError": true,
"content": [
{
"type": "text",
"text": "OAuth token must include \"example:write\"."
}
],
"_meta": {
"mcp/www_authenticate": [
"Bearer resource_metadata=\"https://example.com/.well-known/oauth-protected-resource\", scope=\"example:read example:write\", error=\"insufficient_scope\", error_description=\"OAuth token must include \\\"example:write\\\".\""
]
}
}
The tool also declares its OAuth securitySchemes in tools/list.
The complete result, including the challenge, is visible to JavaScript inside functions.exec. However, the outer tool result only exposes whatever text/image items the JavaScript explicitly emits. No native OAuth card appears and no browser authorization flow starts.
A direct fallback works:
codex mcp login <server> --scopes example:read,example:write
That command opens the browser and completes authorization successfully, confirming that the MCP server's OAuth flow and requested scopes are valid.
What steps can reproduce the bug?
- Configure a Streamable HTTP MCP server using OAuth.
- Authorize it with an initial limited scope such as
example:read. - Expose a tool requiring a newly added scope such as
example:write. - Have the tool return
isError: trueplus_meta["mcp/www_authenticate"]with:
resource_metadata- the union of existing and missing scopes
error="insufficient_scope"
- Invoke that MCP tool from Code Mode through
functions.exec:
const result = await tools.mcp__example__write_operation({});
text(JSON.stringify({
isError: result.isError,
meta: result._meta,
content: result.content
}));
- Observe that the challenge is present in
result._meta, but Codex does not show an OAuth card or start reauthorization. - Run
codex mcp login <server> --scopes example:read,example:writemanually and observe that browser authorization succeeds.
What is the expected behavior?
Nested MCP calls made through functions.exec should preserve host-handled MCP metadata.
When a nested CallToolResult contains _meta["mcp/www_authenticate"], Codex should either:
- intercept and propagate the challenge to the same native OAuth/reauthorization flow used for direct MCP calls before returning control to JavaScript; or
- attach the nested challenge to the outer
functions.execresult so the Desktop host can show the authorization UI.
After successful authorization, the current conversation should reload the updated MCP credential and retry or allow the call to be retried without restarting Codex.
What is the actual behavior?
- The nested JavaScript receives the full challenge.
- The outer Codex UI receives no auth elicitation.
- No browser opens.
- The user only sees the textual missing-scope error if the agent emits it.
- Manual
codex mcp loginsucceeds, but the already-open conversation can continue using its cached old token until Codex is restarted.
Additional information
This is related to, but distinct from, #20518:
- #20518 covers HTTP
403 insufficient_scopeand theWWW-Authenticateresponse header in the MCP transport. - This report covers an in-protocol MCP tool result where JSON-RPC succeeds and the runtime challenge is returned in
CallToolResult._meta["mcp/www_authenticate"], but Code Mode'sfunctions.execnesting boundary does not propagate it to the Desktop auth UI.
It may also overlap with the credential-refresh/UI concerns in #17265 and #23453, but the minimal failure here occurs specifically at the nested tool-result metadata boundary.