odysseus-dev/odysseus · error · HTTPException
Invalid OAuth {field_name}: path must stay under {base}
Error message
Invalid OAuth {field_name}: path must stay under {base} What it means
Raised when an OAuth file path (keys_file / token_file) supplied in an MCP server's oauth_file or oauth_config resolves outside the mcp_oauth base directory. The code expands ~, joins relative paths onto the base, resolves symlinks, then calls Path.relative_to(base); on ValueError it rejects the path with 400. This is a path-traversal containment guard for the OAuth credential store.
Source
Thrown at routes/mcp/mcp_routes.py:44
return Path(MCP_OAUTH_DIR).resolve(strict=False)
def _resolve_mcp_oauth_path(raw_path, field_name: str) -> str:
"""Resolve an MCP OAuth path and keep it under DATA_DIR/mcp_oauth."""
raw = str(raw_path or "").strip()
if not raw:
return ""
base = _mcp_oauth_base_dir()
path = Path(os.path.expanduser(raw))
if not path.is_absolute():
path = base / path
resolved = path.resolve(strict=False)
try:
resolved.relative_to(base)
except ValueError as exc:
raise HTTPException(
400,
f"Invalid OAuth {field_name}: path must stay under {base}",
) from exc
return str(resolved)
def _sanitize_mcp_oauth_config(oauth_cfg):
"""Return an OAuth config copy with file paths confined to mcp_oauth."""
if not oauth_cfg:
return oauth_cfg
if not isinstance(oauth_cfg, dict):
return {}
sanitized = dict(oauth_cfg)
for field_name in ("keys_file", "token_file"):
if sanitized.get(field_name):
sanitized[field_name] = _resolve_mcp_oauth_path(
sanitized[field_name],
field_name,View on GitHub (pinned to f9235ebbf1)
Solutions
- Place the keys/token files inside the directory returned by _mcp_oauth_base_dir() and reference them by bare filename or path relative to it.
- If the file lives elsewhere, copy it into the mcp_oauth base dir and update the config to the relative name.
- Verify with Path(p).resolve().relative_to(base) locally before submitting the config.
Example fix
# before
oauth_file = "/home/me/creds/client_secret.json"
# after
import shutil, pathlib
base = pathlib.Path(_mcp_oauth_base_dir())
shutil.copy("/home/me/creds/client_secret.json", base / "client_secret.json")
oauth_file = "client_secret.json" # relative to base Defensive patterns
Strategy: validation
Validate before calling
from pathlib import Path
def oauth_path_ok(raw: str, base: Path) -> bool:
p = Path(raw).expanduser()
if not p.is_absolute():
p = base / p
try:
p.resolve(strict=False).relative_to(base.resolve(strict=False))
return True
except ValueError:
return False Try / catch
On 400 from server registration, check the detail for 'path must stay under' and fix the path rather than retrying.
Prevention
- Keep all OAuth credentials in the managed mcp_oauth directory.
- Never ship absolute credential paths in configs shared across machines.
- Avoid symlinks under the base dir — resolve() follows them and can escape.
When it happens
Trigger: POSTing/PATCHing an MCP server with oauth_file="/etc/google/keys.json" (absolute path outside base), a relative path with traversal like "../../secrets/keys.json", or a symlink inside the base that resolves elsewhere.
Common situations: Reusing an absolute path from a different machine or setup guide; pointing at a keys file in the project root or home directory instead of the managed mcp_oauth directory; symlinked dotfiles whose realpath escapes the base.
Related errors
- OAuth keys file not found
- HTTP ${r.status}
- Server has no OAuth config
- Invalid OAuth keys file format
- OAuth keys/token file not configured
AI-assisted analysis of odysseus-dev/odysseus@f9235ebbf1 (2026-08-14).
Data as JSON: /api/errors/9728161a9d8e75f2.
Report an issue: GitHub.