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

  1. Provide both paths in oauth_config, e.g. {"keys_file": "secret.json", "token_file": "token.json"}, both under the mcp_oauth base dir.
  2. Re-register the server with a complete oauth_file/config so the persisted row contains both fields.
  3. 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

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


AI-assisted analysis of odysseus-dev/odysseus@f9235ebbf1 (2026-08-14). Data as JSON: /api/errors/f971fc509b965ba2. Report an issue: GitHub.