odysseus-dev/odysseus · error · HTTPException
OAuth keys/token file not configured
Error message
OAuth keys/token file not configured
What it means
Raised in the OAuth callback (token exchange) when the server's sanitized oauth_config lacks keys_file or token_file. Unlike the authorize step, which only requires keys_file, the token exchange must also persist a token_file, so a config with only keys_file fails here with 400 after the user completes Google consent.
Source
Thrown at routes/mcp/mcp_routes.py:537
))
return await _exchange_and_connect(server_id, code, request)
async def _exchange_and_connect(server_id: str, code: str, request: Request):
"""Exchange auth code for tokens and connect the MCP server."""
db = SessionLocal()
try:
srv = db.query(McpServer).filter(McpServer.id == server_id).first()
if not srv:
return HTMLResponse(_oauth_result_page("Error", "Server not found."), status_code=404)
if not srv.oauth_config:
return HTMLResponse(_oauth_result_page("Error", "No OAuth config."), status_code=400)
oauth_cfg = _sanitize_mcp_oauth_config(json.loads(srv.oauth_config))
keys_file = oauth_cfg.get("keys_file", "")
token_file = oauth_cfg.get("token_file", "")
if not keys_file or not token_file:
raise HTTPException(400, "OAuth keys/token file not configured")
with open(keys_file, encoding="utf-8") as f:
keys_data = json.load(f)
keys = keys_data.get("installed") or keys_data.get("web")
client_id = keys["client_id"]
client_secret = keys["client_secret"]
redirect_uri = _mcp_oauth_redirect_uri()
async with httpx.AsyncClient() as client:
resp = await client.post(
"https://oauth2.googleapis.com/token",
data={
"code": code,
"client_id": client_id,
"client_secret": client_secret,
"redirect_uri": redirect_uri,
"grant_type": "authorization_code",View on GitHub (pinned to f9235ebbf1)
Solutions
- Provide both paths in oauth_config, e.g. {"keys_file": "secret.json", "token_file": "token.json"}, both under the mcp_oauth base dir.
- Re-register the server with a complete oauth_file/config so the persisted row contains both fields.
- Validate the config shape (both keys present and files resolvable under base) before starting the flow, since failing here wastes a completed consent round-trip.
Example fix
# before
oauth_config = {"keys_file": "secret.json"}
# after
oauth_config = {"keys_file": "secret.json", "token_file": "token.json"} Defensive patterns
Strategy: validation
Validate before calling
def oauth_config_complete(oauth_cfg: dict) -> bool:
return bool(oauth_cfg.get("keys_file")) and bool(oauth_cfg.get("token_file")) Try / catch
The 400 happens after consent completed — catch it, add token_file to the config, re-register, and restart the flow (a new consent round-trip is required).
Prevention
- Always set both keys_file and token_file in oauth_config.
- Validate config completeness before sending the user to Google.
When it happens
Trigger: Registering a server with an oauth_file that supplies keys_file but no token_file path, completing the Google consent screen, and the redirect hitting the callback; token_file left empty in a hand-built oauth_config.
Common situations: Configs written before the token_file requirement; partial OAuth configs that only define where credentials are read from, not where tokens are written.
Related errors
- Server has no OAuth config
- OAuth keys file not found
- Invalid OAuth keys file format
- Unknown device-flow provider: ${provider}
- HTTP ${r.status}
AI-assisted analysis of odysseus-dev/odysseus@f9235ebbf1 (2026-08-14).
Data as JSON: /api/errors/f971fc509b965ba2.
Report an issue: GitHub.