{"record":{"id":"f971fc509b965ba2","repo":"odysseus-dev/odysseus","slug":"oauth-keys-token-file-not-configured","errorCode":null,"errorMessage":"OAuth keys/token file not configured","messagePattern":"OAuth keys/token file not configured","errorType":"http","errorClass":"HTTPException","httpStatus":400,"severity":"error","filePath":"routes/mcp/mcp_routes.py","lineNumber":537,"sourceCode":"            ))\n\n        return await _exchange_and_connect(server_id, code, request)\n\n    async def _exchange_and_connect(server_id: str, code: str, request: Request):\n        \"\"\"Exchange auth code for tokens and connect the MCP server.\"\"\"\n        db = SessionLocal()\n        try:\n            srv = db.query(McpServer).filter(McpServer.id == server_id).first()\n            if not srv:\n                return HTMLResponse(_oauth_result_page(\"Error\", \"Server not found.\"), status_code=404)\n            if not srv.oauth_config:\n                return HTMLResponse(_oauth_result_page(\"Error\", \"No OAuth config.\"), status_code=400)\n\n            oauth_cfg = _sanitize_mcp_oauth_config(json.loads(srv.oauth_config))\n            keys_file = oauth_cfg.get(\"keys_file\", \"\")\n            token_file = oauth_cfg.get(\"token_file\", \"\")\n            if not keys_file or not token_file:\n                raise HTTPException(400, \"OAuth keys/token file not configured\")\n\n            with open(keys_file, encoding=\"utf-8\") as f:\n                keys_data = json.load(f)\n            keys = keys_data.get(\"installed\") or keys_data.get(\"web\")\n            client_id = keys[\"client_id\"]\n            client_secret = keys[\"client_secret\"]\n\n            redirect_uri = _mcp_oauth_redirect_uri()\n\n            async with httpx.AsyncClient() as client:\n                resp = await client.post(\n                    \"https://oauth2.googleapis.com/token\",\n                    data={\n                        \"code\": code,\n                        \"client_id\": client_id,\n                        \"client_secret\": client_secret,\n                        \"redirect_uri\": redirect_uri,\n                        \"grant_type\": \"authorization_code\",","sourceCodeStart":519,"sourceCodeEnd":555,"githubUrl":"https://github.com/odysseus-dev/odysseus/blob/f9235ebbf13f693a6fd29ce70b097f6ec83705bf/routes/mcp/mcp_routes.py#L519-L555","documentation":"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.","triggerScenarios":"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.","commonSituations":"Configs written before the token_file requirement; partial OAuth configs that only define where credentials are read from, not where tokens are written.","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."],"exampleFix":"# before\noauth_config = {\"keys_file\": \"secret.json\"}\n\n# after\noauth_config = {\"keys_file\": \"secret.json\", \"token_file\": \"token.json\"}","handlingStrategy":"validation","validationCode":"def oauth_config_complete(oauth_cfg: dict) -> bool:\n    return bool(oauth_cfg.get(\"keys_file\")) and bool(oauth_cfg.get(\"token_file\"))","typeGuard":null,"tryCatchPattern":"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).","preventionTips":["Always set both keys_file and token_file in oauth_config.","Validate config completeness before sending the user to Google."],"tags":["mcp","oauth","token-exchange","config"],"backgroundTag":null,"analyzedSha":"f9235ebbf13f693a6fd29ce70b097f6ec83705bf","analyzedAt":"2026-08-14T21:47:48.359Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}