odysseus-dev/odysseus · error · HTTPException

Invalid OAuth keys file format

Error message

Invalid OAuth keys file format

What it means

On GET /oauth/authorize/{server_id}, the keys file exists and parses as JSON, but neither an "installed" nor a "web" top-level key is present, so the credentials shape is unrecognized and the route returns 400. Google client-secret files for desktop apps use "installed" and for web apps use "web"; anything else (a token file, a service-account key, or a hand-written file) fails here.

Source

Thrown at routes/mcp/mcp_routes.py:449

        require_admin(request)
        db = SessionLocal()
        try:
            srv = db.query(McpServer).filter(McpServer.id == server_id).first()
            if not srv:
                raise HTTPException(404, "Server not found")
            if not srv.oauth_config:
                raise HTTPException(400, "Server has no OAuth config")

            oauth_cfg = _sanitize_mcp_oauth_config(json.loads(srv.oauth_config))
            keys_file = oauth_cfg.get("keys_file", "")
            if not keys_file or not os.path.exists(keys_file):
                raise HTTPException(400, "OAuth keys file not found")

            with open(keys_file, encoding="utf-8") as f:
                keys_data = json.load(f)
            keys = keys_data.get("installed") or keys_data.get("web")
            if not keys:
                raise HTTPException(400, "Invalid OAuth keys file format")

            client_id = keys["client_id"]
            scopes = oauth_cfg.get("scopes", [])

            # For Desktop App creds, default to localhost — the user will
            # paste the resulting URL back if they're on a different device.
            redirect_uri = _mcp_oauth_redirect_uri()

            params = {
                "client_id": client_id,
                "redirect_uri": redirect_uri,
                "response_type": "code",
                "scope": " ".join(scopes),
                "access_type": "offline",
                "prompt": "consent",
                "state": server_id,
            }
            auth_url = "https://accounts.google.com/o/oauth2/v2/auth?" + urllib.parse.urlencode(params)

View on GitHub (pinned to f9235ebbf1)

Solutions

  1. Download the OAuth client JSON from Google Cloud Console (Credentials → OAuth client ID) and use that file as keys_file.
  2. Open the file and confirm it starts with {"installed": {...}} or {"web": {...}}.
  3. If it is a token file or service-account key, get the correct client-credentials file — this flow only supports the installed/web shapes.

Example fix

# before (token file, wrong shape)
{"access_token": "...", "token_type": "Bearer"}

# after (client credentials file)
{"installed": {"client_id": "...", "client_secret": "...", "redirect_uris": ["http://localhost"]}}
Defensive patterns

Strategy: type-guard

Validate before calling

import json
def keys_file_shape_ok(path: str) -> bool:
    try:
        with open(path, encoding="utf-8") as f:
            d = json.load(f)
        return isinstance(d, dict) and ("installed" in d or "web" in d)
    except (OSError, json.JSONDecodeError):
        return False

Type guard

def is_google_client_creds(d: unknown): boolean {
  return typeof d === "object" && d !== null &&
    ("installed" in d || "web" in d);
}

Prevention

When it happens

Trigger: Uploading an OAuth *token* JSON instead of the client-secret JSON; a service-account key file (type: service_account); a truncated or edited credentials file missing the wrapper object; wrong file downloaded from a different Google Cloud flow.

Common situations: Grabbing token.json written by a previous flow rather than client_secret_*.apps.googleusercontent.com.json; mixing up web vs desktop credential downloads with another format; copying only the inner {client_id,...} object without the "installed" wrapper.

Related errors


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