Skip to content

[codex] Implement hosted MCP OAuth for ChatGPT#8508

Merged
Git-on-my-level merged 3 commits into
mainfrom
codex/mcp-oauth-chatgpt-8506
Jun 28, 2026
Merged

[codex] Implement hosted MCP OAuth for ChatGPT#8508
Git-on-my-level merged 3 commits into
mainfrom
codex/mcp-oauth-chatgpt-8506

Conversation

@Git-on-my-level

@Git-on-my-level Git-on-my-level commented Jun 28, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • Implements hosted MCP OAuth for ChatGPT Apps using a predefined OAuth client, Omi/Firebase login consent, PKCE S256 authorization-code exchange, scoped access tokens, rotating refresh tokens, and durable Firestore grants/tokens.
  • Adds OAuth protected-resource metadata at both discovery paths, authorization-server metadata for code + refresh grants, resource-bound token validation, scope-filtered MCP tool listing/calls, and MCP auth challenges for missing scopes.
  • Keeps legacy omi_mcp_... API-key auth working while preventing OAuth tokens from falling through legacy full-access helpers.
  • Adds signed-in grant listing/revocation endpoints so users can disconnect hosted MCP OAuth grants.
  • Documents the new hosted MCP OAuth env vars.

Closes #8506

Validation

  • make setup
  • backend/test-preflight.sh (passes; optional dependency/env warnings only)
  • python3 -m py_compile backend/database/mcp_oauth.py backend/routers/mcp_sse.py backend/routers/mcp.py backend/tests/unit/test_mcp_oauth.py backend/tests/unit/test_mcp_data_endpoints.py
  • black --line-length 120 --skip-string-normalization backend/database/mcp_oauth.py backend/routers/mcp_sse.py backend/routers/mcp.py backend/tests/unit/test_mcp_oauth.py backend/tests/unit/test_mcp_data_endpoints.py
  • git diff --check
  • pytest backend/tests/unit/test_mcp_oauth.py backend/tests/unit/test_mcp_data_endpoints.py -q (28 passed)
  • python3 backend/scripts/scan_async_blockers.py (no changed MCP files flagged; existing unrelated blockers remain elsewhere)
  • Subagent review completed; fixed browser redirect, atomic consume/rotate, and query-preserving redirect findings.
  • Oracle review completed; fixed scope mismatch, replay reactivation, atomic exchange, resource binding, async dispatch, template escaping, shared config, PKCE validation, and legacy-helper findings.

Known unrelated failure

  • backend/test.sh reaches existing tests/unit/test_speaker_sample.py failures because utils.speaker_sample does not expose deepgram_prerecorded_from_bytes for those monkeypatches; this is outside the MCP OAuth diff.

Review in cubic

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: e4b046cb45

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

return urlunsplit((parts.scheme, parts.netloc, parts.path, urlencode(params), parts.fragment))


@router.get("/authorize", response_class=HTMLResponse, tags=["mcp"])

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Use non-conflicting MCP OAuth paths

I checked backend/main.py: auth.router is included before mcp_sse.router, and backend/routers/auth.py already registers GET /authorize and POST /token. Starlette matches the earlier auth routes first, so ChatGPT's OAuth discovery will hit auth_authorize (422 for missing provider) and auth_token (looks for a main-app Redis auth code) instead of these MCP handlers, making the hosted MCP OAuth flow unusable until these endpoints are moved or the routing is disambiguated.

Useful? React with 👍 / 👎.

Comment thread backend/database/mcp_oauth.py Outdated
Comment thread backend/tests/unit/test_mcp_oauth.py

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No issues found across 7 files

Confidence score: 5/5

  • Automated review surfaced no issues in the provided summaries.
  • No files require special attention.

Re-trigger cubic

Git-on-my-level and others added 2 commits June 29, 2026 03:26
Addresses review feedback:
- _now() returns datetime.now(timezone.utc) instead of naive utcnow(),
  preventing TypeError when comparing persisted Firestore timestamps
  (which decode as tz-aware UTC) against _now() in auth-code exchange,
  access-token validation, and refresh-token rotation.
- list_user_grants sort fallback uses tz-aware datetime.min to stay
  consistent with persisted aware timestamps.
- Add test_mcp_oauth.py to backend/test.sh per AGENTS.md requirement
  so the OAuth suite runs in CI.
@Git-on-my-level Git-on-my-level force-pushed the codex/mcp-oauth-chatgpt-8506 branch from 49f4dab to d90aa27 Compare June 28, 2026 20:31
@Git-on-my-level

Copy link
Copy Markdown
Collaborator Author

Review feedback addressed

Thread Comment Verdict Action
#1 (P1 route conflict) Codex: /authorize and /token shadowed by auth.router Not reproduced — false positive Did not change routing (see analysis below)
#2 (P1 timezone) _now() returns naive datetime, breaks Firestore comparisons Confirmed real bug ✅ Fixed
#3 (P2 test runner) test_mcp_oauth.py missing from test.sh Confirmed ✅ Fixed

Thread #1 — no route conflict exists (d90aa27)

Codex claimed auth.router registers root-level GET /authorize and POST /token that shadow the MCP endpoints. Verified against the actual code:

  • auth.router = APIRouter(prefix="/v1/auth", ...) → its routes are /v1/auth/authorize and /v1/auth/token, not root paths.
  • oauth.router uses /v1/oauth/authorize.
  • mcp_sse.router = APIRouter() (no prefix) → its /authorize and /token are at root with no collision.

backend/main.py include order is therefore irrelevant — the prefixes already disambiguate. No routing change needed.

Thread #2 — timezone-aware UTC datetimes (d90aa27)

_now() returned datetime.utcnow() (naive). Firestore decodes Timestamp fields as timezone-aware UTC datetimes on read-back. Every expiry comparison (expires_at <= _now()) in auth-code consume/exchange, access-token validation, and refresh-token rotation would raise TypeError: can't compare offset-naive and offset-aware datetimes against real persisted records.

Changed _now() to datetime.now(timezone.utc). Also made the list_user_grants sort fallback tz-aware (datetime.min.replace(tzinfo=timezone.utc)) to stay consistent with persisted aware timestamps.

Thread #3 — OAuth tests added to CI runner (d90aa27)

Added pytest tests/unit/test_mcp_oauth.py -v to backend/test.sh alongside the other MCP test files, per backend/AGENTS.md's requirement that new test files must be registered in test.sh or they won't run in CI.

Verification

  • python3 -m py_compile backend/database/mcp_oauth.py — OK
  • black --line-length 120 --skip-string-normalization --check backend/database/mcp_oauth.py — OK
  • pytest backend/tests/unit/test_mcp_oauth.py -v4 passed (auth-code exchange + reuse rejection, refresh-token rotation + replay revocation, grant revocation, PKCE malformed-value rejection)
  • git diff --check — clean

@Git-on-my-level

Copy link
Copy Markdown
Collaborator Author

I looked into this and I don't think there's an actual route conflict here.

auth.router is declared with prefix="/v1/auth" (backend/routers/auth.py:24-27), so its GET /authorize and POST /token decorators resolve to /v1/auth/authorize and /v1/auth/token — not the root paths.

mcp_sse.router has no prefix, so its handlers resolve to /authorize and /token at the root. These are distinct paths, so Starlette never has to choose between them.

The /v1/oauth/authorize + /v1/oauth/token routes in oauth.py are a third, separate set of paths.

I verified this statically across every router under backend/routers/mcp_sse.py is the only router registering root-level GET /authorize, POST /authorize, and POST /token. Effective paths registered for authorize/token across routers:

Method Effective path Router (prefix)
GET /authorize mcp_sse (no prefix)
POST /authorize mcp_sse (no prefix)
POST /token mcp_sse (no prefix)
GET /v1/auth/authorize auth (/v1/auth)
POST /v1/auth/token auth (/v1/auth)
GET /v1/oauth/authorize oauth (no prefix)
POST /v1/oauth/token oauth (no prefix)

No (method, effective_path) collisions exist between the MCP OAuth routes and the main-app auth routes, so the discovery → /authorize/token flow will reach the MCP handlers as intended. No change needed.

@Git-on-my-level

Copy link
Copy Markdown
Collaborator Author

Verified this concern against the actual route registration — there is no conflict, so no change is needed.

Evidence:

  • backend/routers/auth.py line 27: router = APIRouter(prefix="/v1/auth", ...). FastAPI always applies the router prefix; it is never stripped regardless of include order.
  • Therefore auth.router's @router.get("/authorize") (auth.py:206) resolves to GET /v1/auth/authorize, and @router.post("/token") (auth.py:330) resolves to POST /v1/auth/token.
  • backend/routers/mcp_sse.py line 54: router = APIRouter() (no prefix), so its @router.get("/authorize") (mcp_sse.py:1265) and @router.post("/token") (mcp_sse.py:1342) live at the root paths GET /authorize and POST /token.
  • backend/routers/oauth.py registers full-path /v1/oauth/authorize and /v1/oauth/token — also distinct.

The authorization-server metadata advertises authorization_endpoint = https://api.omi.me/authorize and token_endpoint = https://api.omi.me/token (root paths, mcp_sse.py:63-64). These root paths uniquely match the MCP handlers; the auth router's prefixed paths (/v1/auth/...) are different URLs and do not intercept them.

Leaving this thread open since no code change is warranted.

@Git-on-my-level Git-on-my-level merged commit 280dcb9 into main Jun 28, 2026
4 checks passed
@Git-on-my-level Git-on-my-level deleted the codex/mcp-oauth-chatgpt-8506 branch June 28, 2026 21:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Implement full ChatGPT Apps OAuth 2.1 flow for hosted MCP

1 participant