Skip to content

[UI] M2M OAuth2 UI Flow #20794

Merged
ishaan-jaff merged 20 commits intomainfrom
litellm_mcp_oauth2_m2m_ui
Feb 10, 2026
Merged

[UI] M2M OAuth2 UI Flow #20794
ishaan-jaff merged 20 commits intomainfrom
litellm_mcp_oauth2_m2m_ui

Conversation

@ishaan-jaff
Copy link
Member

@ishaan-jaff ishaan-jaff commented Feb 10, 2026

[UI] M2M OAuth2 UI Flow

Add support for Adding Oauth2 M2M MCPs through LiteLLM Admin UI

Pre-Submission checklist

Please complete all items before asking a LiteLLM maintainer to review your PR

  • I have Added testing in the tests/litellm/ directory, Adding at least 1 test is a hard requirement - see details
  • My PR passes all unit tests on make test-unit
  • My PR's scope is as isolated as possible, it only solves 1 specific problem

CI (LiteLLM team)

CI status guideline:

  • 50-55 passing tests: main is stable with minor issues.
  • 45-49 passing tests: acceptable but needs attention
  • <= 40 passing tests: unstable; be careful with your merges and assess the risk.
  • Branch creation CI run
    Link:

  • CI run for the last commit
    Link:

  • Merge / cherry-pick CI run
    Links:

Type

🆕 New Feature
✅ Test

Changes

ishaan-jaff and others added 14 commits February 9, 2026 14:41
…docs

- Fix zero-TTL edge case: floor at MCP_OAUTH2_TOKEN_CACHE_MIN_TTL (10s)
- Make all MCP OAuth2 constants env-configurable via os.getenv()
- Move test file to follow 1:1 mapping convention (test_oauth2_token_cache.py)
- Add MCP OAuth doc page (mcp_oauth.md) with M2M and PKCE sections
- Update FAQ in mcp.md to reflect M2M support
- Add E2E test script and config

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@vercel
Copy link

vercel bot commented Feb 10, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
litellm Ready Ready Preview, Comment Feb 10, 2026 2:34am

Request Review

@greptile-apps
Copy link
Contributor

greptile-apps bot commented Feb 10, 2026

Greptile Overview

Greptile Summary

This PR adds a UI + proxy flow for MCP OAuth2, including a new machine-to-machine (client_credentials) mode where the proxy fetches and caches access tokens automatically.

Key backend changes:

  • _create_mcp_client in litellm/proxy/_experimental/mcp_server/mcp_server_manager.py is now async and resolves auth in one place via resolve_mcp_auth().
  • New litellm/proxy/_experimental/mcp_server/oauth2_token_cache.py implements an in-memory token cache with per-server locks and TTL based on expires_in.
  • OAuth2 servers are now split into “needs user token” vs “has client_credentials” using new MCPServer properties.

Key UI changes:

  • Adds a shared OAuthFormFields component and an “OAuth Flow Type” selector (Interactive PKCE vs M2M).
  • Updates connection testing logic to not require a user token for M2M.

Docs add a dedicated mcp_oauth.md page with diagrams and configuration guidance.

Issues needing fixes before merge:

  • The new OAuth2 e2e script doesn’t start the proxy / sets no PROXY_PID.
  • A localhost OAuth2 test server with hardcoded secrets was added to the default proxy_config.yaml.
  • A couple of UI-only mistakes (accidental validation message punctuation; leftover console.log).

Confidence Score: 2/5

  • This PR should not be merged until the broken e2e test script and default config credential/test-server additions are fixed.
  • Core logic changes look coherent and tests were updated for the async client creation, but the new e2e script will fail as committed, and shipping a localhost OAuth2 test server with hardcoded credentials in proxy_config.yaml is not acceptable for a default config/template. There are also minor UI cleanliness issues (debug validation message and console.log).
  • tests/mcp_tests/test_oauth2_e2e.sh; litellm/proxy/proxy_config.yaml; ui/litellm-dashboard/src/components/mcp_tools/create_mcp_server.tsx; ui/litellm-dashboard/src/components/mcp_tools/types.tsx

Important Files Changed

Filename Overview
litellm/proxy/_experimental/mcp_server/mcp_server_manager.py Makes _create_mcp_client async and centralizes auth resolution via resolve_mcp_auth; adjusts tool listing/health-check skips for user-token OAuth servers.
litellm/proxy/_experimental/mcp_server/oauth2_token_cache.py Adds in-memory OAuth2 client_credentials token cache with per-server locks and resolve_mcp_auth priority logic.
litellm/proxy/proxy_config.yaml Adds a localhost OAuth2 test MCP server with hardcoded client credentials to default proxy_config.yaml (should not ship as default).
tests/mcp_tests/test_oauth2_e2e.sh Adds an OAuth2 M2M e2e shell script but it does not start the proxy (will fail due to missing PROXY_PID/closed port).
ui/litellm-dashboard/src/components/mcp_tools/create_mcp_server.tsx Refactors OAuth UI section to use OAuthFormFields and adds flow-type selection; includes an accidental '!!!!!!!!!' validation message.
ui/litellm-dashboard/src/components/mcp_tools/types.tsx Adds OAUTH_FLOW constants but still contains a leftover console.log in handleTransport.

Sequence Diagram

sequenceDiagram
    participant UI as LiteLLM UI
    participant Proxy as LiteLLM Proxy
    participant Manager as MCPServerManager
    participant Cache as MCPOAuth2TokenCache
    participant Auth as OAuth2 Token URL
    participant MCP as MCP Server

    UI->>Proxy: POST /mcp-rest/test/tools/list (OAuth2 M2M config)
    Proxy->>Manager: _create_mcp_client(server, mcp_auth_header, extra_headers)
    Manager->>Cache: resolve_mcp_auth(server, mcp_auth_header)
    alt Token cached
        Cache-->>Manager: access_token
    else Cache miss
        Cache->>Auth: POST token_url (grant_type=client_credentials)
        Auth-->>Cache: {access_token, expires_in}
        Cache-->>Manager: access_token
    end
    Manager->>MCP: MCP request with Bearer access_token
    MCP-->>Manager: MCP response
    Manager-->>Proxy: tools list/response
    Proxy-->>UI: tools list/response
Loading

Copy link
Contributor

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

6 files reviewed, 4 comments

Edit Code Review Agent Settings | Greptile

Comment on lines +35 to +39
# ── 3. List tools ────────────────────────────────────────────────────────────
echo ""
echo "=== Request 1: List MCP tools ==="
curl -s http://localhost:$PROXY_PORT/mcp-rest/tools/list \
-H "Authorization: Bearer sk-1234" | python3 -m json.tool
Copy link
Contributor

Choose a reason for hiding this comment

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

Missing proxy startup

This e2e script never starts the LiteLLM proxy, but later calls http://localhost:$PROXY_PORT/... and tries to kill "$PROXY_PID" in cleanup. As written it will fail with PROXY_PID: unbound variable (due to set -u) and the curl calls will hit a closed port. Add the proxy launch step (using $CONFIG) and set PROXY_PID before any requests/cleanup.

Comment on lines 45 to +55
deepwiki:
transport: "http"
url: "https://mcp.deepwiki.com/mcp"
test_oauth2_server:
url: "http://localhost:8765/mcp"
transport: "http"
auth_type: "oauth2"
client_id: "test-client"
client_secret: "test-secret"
token_url: "http://localhost:8765/oauth/token"

Copy link
Contributor

Choose a reason for hiding this comment

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

Test server in default config

test_oauth2_server (localhost URL + hardcoded client_secret) is added to the repo’s default proxy_config.yaml. This config is commonly used as a template/example and can be deployed; shipping a test server entry with credentials is not appropriate for a default config. This should live in test-only config under tests/ or documentation instead.

@greptile-apps
Copy link
Contributor

greptile-apps bot commented Feb 10, 2026

Additional Comments (2)

ui/litellm-dashboard/src/components/mcp_tools/create_mcp_server.tsx
Debug-only validation text

The description field rule message is "Please enter a server description!!!!!!!!!". This user-facing string looks accidental and will show up in the UI; it should be replaced with a normal message or removed (field isn’t required).


ui/litellm-dashboard/src/components/mcp_tools/types.tsx
Leftover console.log

handleTransport still logs console.log(transport); which is noisy in production and will spam the browser console whenever transport is derived. This should be removed before merge.

@ishaan-jaff
Copy link
Member Author

@greptile review this again

@greptile-apps
Copy link
Contributor

greptile-apps bot commented Feb 10, 2026

Greptile Overview

Greptile Summary

This PR adds UI support for configuring OAuth2 MCP servers with explicit flow selection (interactive/PKCE vs M2M client_credentials), and updates the proxy’s MCP REST test helper to forward OAuth client credentials into the MCPServer model while dropping incoming Authorization headers for M2M so the proxy can fetch its own token.

It also adds unit tests for the new M2M behavior plus a shell E2E script/config intended to validate client_credentials token fetching and tool calls against a mock server.

Confidence Score: 2/5

  • This PR should not merge until the default proxy config and test harness issues are addressed.
  • Backend changes for M2M OAuth2 look directionally correct and are covered by new unit tests, but the PR currently ships a localhost OAuth2 test server (with credentials) in the repo’s default proxy_config.yaml, adds an E2E script that depends on python/python3 availability, and changes error responses to include raw exception strings.
  • litellm/proxy/proxy_config.yaml, tests/mcp_tests/test_oauth2_e2e.sh, litellm/proxy/_experimental/mcp_server/rest_endpoints.py

Important Files Changed

Filename Overview
litellm/proxy/_experimental/mcp_server/rest_endpoints.py Adds M2M OAuth2 credential extraction/forwarding and drops incoming auth headers for client_credentials; but now returns raw exception strings to callers (info leak).
litellm/proxy/proxy_config.yaml Adds a localhost OAuth2 MCP test server with hardcoded credentials to the default proxy config (should not ship in default config).
tests/mcp_tests/test_oauth2_e2e.sh Adds OAuth2 client_credentials E2E script, but it assumes python/python3 are installed and still lacks starting the proxy (already noted in prior PR thread).
tests/test_litellm/proxy/_experimental/mcp_server/test_rest_endpoints.py Adds unit tests covering M2M credential forwarding, dropping incoming Authorization for M2M, and catching BaseExceptionGroup.
ui/litellm-dashboard/src/components/mcp_tools/OAuthFormFields.tsx Introduces shared OAuth form fields including flow type selection and M2M-required fields; uses Tremor components in new code (Tremor deprecated in repo guidelines).
ui/litellm-dashboard/src/components/mcp_tools/types.tsx Adds OAUTH_FLOW constants but also leaves a console.log in handleTransport(), which should be removed.
ui/litellm-dashboard/src/hooks/useTestMCPConnection.tsx Skips requiring user OAuth token for M2M oauth_flow_type, but compares against hardcoded "m2m" string instead of OAUTH_FLOW constant (easy to drift).

Sequence Diagram

sequenceDiagram
  participant UI as Admin UI
  participant Proxy as LiteLLM Proxy (FastAPI)
  participant REST as /mcp-rest/test/*
  participant Manager as global_mcp_server_manager
  participant OAuth as OAuth2 Token Endpoint
  participant MCP as MCP Server

  UI->>Proxy: POST /mcp-rest/test/tools/list (NewMCPServerRequest)
  Proxy->>REST: _execute_with_mcp_client(request, operation)
  REST->>REST: _extract_credentials(request.credentials)
  REST->>REST: Build MCPServer model (client_id/secret/token_url/scopes)
  REST->>REST: If has_client_credentials => drop incoming oauth2_headers
  REST->>Manager: _create_mcp_client(server, extra_headers, stdio_env)
  alt M2M client_credentials
    Manager->>OAuth: POST token_url (grant_type=client_credentials)
    OAuth-->>Manager: access_token
  end
  Manager->>MCP: session.initialize / list tools
  MCP-->>Manager: tools list
  Manager-->>REST: client
  REST-->>Proxy: operation result (tools)
  Proxy-->>UI: tools response
Loading

Copy link
Contributor

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

7 files reviewed, 3 comments

Edit Code Review Agent Settings | Greptile

Comment on lines +591 to +598
except (KeyboardInterrupt, SystemExit):
raise
except BaseException as e:
verbose_logger.error("Error in MCP operation: %s", e, exc_info=True)
return {
"status": "error",
"message": "An internal error has occurred while testing the MCP server.",
"error": True,
"message": f"Failed to connect to MCP server: {e}",
Copy link
Contributor

Choose a reason for hiding this comment

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

Leaking internal exception

_execute_with_mcp_client() now returns the raw exception string in the API response ("message": f"Failed to connect to MCP server: {e}"). This can leak internal details (e.g., backend URLs, stack/context from upstream libs) to any caller of /mcp-rest/test/*. The previous behavior returned a generic message; consider restoring a generic user-facing message and keep details only in logs (or gate detailed errors behind a debug flag).

Comment on lines +25 to +31
python "$MOCK_SERVER" &
MOCK_PID=$!
sleep 2

# Quick smoke test on the token endpoint
TOKEN_RESP=$(curl -sf http://localhost:$MOCK_PORT/oauth/token \
-d "grant_type=client_credentials&client_id=test-client&client_secret=test-secret")
Copy link
Contributor

Choose a reason for hiding this comment

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

E2E script requires python

This script invokes python/python3 (python "$MOCK_SERVER", python3 -m json.tool, python3 -c ...), but the test environment may not have either available (and in this repo’s sandbox they’re absent). As written, the E2E test will fail immediately on systems without Python. Either ensure Python is a declared/installed test dependency in CI for this script, or rewrite the JSON parsing/printing to avoid Python entirely.

@greptile-apps
Copy link
Contributor

greptile-apps bot commented Feb 10, 2026

Additional Comments (1)

ui/litellm-dashboard/src/components/mcp_tools/types.tsx
Debug log in helper

handleTransport() includes console.log(transport), which will spam the browser console for any caller (and potentially leak config values in shared screenshots/log captures). This looks accidental and should be removed before merge.

@ishaan-jaff ishaan-jaff merged commit 36e0361 into main Feb 10, 2026
58 of 67 checks passed
krrishdholakia pushed a commit that referenced this pull request Feb 10, 2026
…logging_payload is missing (#20851)

* fix: Preserved nullable object fields by carrying schema properties

* Fix: _convert_schema_types

* Fix all mypy issues

* Add alert about email notifications

* fixing tests

* extending timeout for long running tests

* Text changes

* [Feat] MCP Oauth2 Fixes - Add support for MCP M2M Oauth2 support (#20788)

* add has_client_credentials

* MCPOAuth2TokenCache

* init MCP Oauth2 constants

* MCPOAuth2TokenCache

* resolve_mcp_auth

* test fixes

* docs fix

* address greptile review: min TTL, env-configurable constants, tests, docs

- Fix zero-TTL edge case: floor at MCP_OAUTH2_TOKEN_CACHE_MIN_TTL (10s)
- Make all MCP OAuth2 constants env-configurable via os.getenv()
- Move test file to follow 1:1 mapping convention (test_oauth2_token_cache.py)
- Add MCP OAuth doc page (mcp_oauth.md) with M2M and PKCE sections
- Update FAQ in mcp.md to reflect M2M support
- Add E2E test script and config

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix mypy lint

* fix oauth2

* remove old files

* docs fix

* address greptile comments

* fix: atomic lock creation + validate JSON response shape

- Use dict.setdefault() for atomic per-server lock creation
- Add isinstance(body, dict) check before accessing token response fields

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: replace asserts with proper guards, wrap HTTP errors with context

- Replace `assert` statements with `if/raise ValueError` (asserts can be
  disabled with python -O in production)
- Wrap `httpx.HTTPStatusError` to provide a clear error message with
  server_id and status code
- Add tests for HTTP error and non-dict JSON response error paths
- Remove unused imports

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>

* [UI] M2M OAuth2 UI Flow  (#20794)

* add has_client_credentials

* MCPOAuth2TokenCache

* init MCP Oauth2 constants

* MCPOAuth2TokenCache

* resolve_mcp_auth

* test fixes

* docs fix

* address greptile review: min TTL, env-configurable constants, tests, docs

- Fix zero-TTL edge case: floor at MCP_OAUTH2_TOKEN_CACHE_MIN_TTL (10s)
- Make all MCP OAuth2 constants env-configurable via os.getenv()
- Move test file to follow 1:1 mapping convention (test_oauth2_token_cache.py)
- Add MCP OAuth doc page (mcp_oauth.md) with M2M and PKCE sections
- Update FAQ in mcp.md to reflect M2M support
- Add E2E test script and config

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix mypy lint

* fix oauth2

* ui feat fixes

* test M2M

* test fix

* ui feats

* ui fixes

* ui fix client ID

* fix: backend endpoints

* docs fix

* fixes greptile

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>

* [Fix] prevent shared backend model key from being polluted by per-deployment custom pricing (#20679)

* bug: custom price override for models

* added associated test

* fix(mcp): resolve OAuth2 root endpoints returning "MCP server not found" (#20784)

When MCP SDK hits root-level /register, /authorize, /token without
server name prefix, auto-resolve to the single configured OAuth2
server. Also fix WWW-Authenticate header to use correct public URL
behind reverse proxy.

* Add support for langchain_aws via litellm passthrough

* fix(proxy): return early instead of raising ValueError when standard_logging_payload is missing

The `_PROXY_VirtualKeyModelMaxBudgetLimiter.async_log_success_event` hook
raises `ValueError` when `standard_logging_payload` is `None`.  This breaks
non-standard call types (e.g. vLLM `/classify`) that do not populate the
payload, and the resulting exception disrupts downstream success callbacks
like Langfuse.

Return early with a debug log instead, matching the existing pattern used
for missing `user_api_key_model_max_budget`.

Fixes #18986

---------

Co-authored-by: Sameer Kankute <sameer@berri.ai>
Co-authored-by: yuneng-jiang <yuneng.jiang@gmail.com>
Co-authored-by: Ishaan Jaff <ishaanjaffer0324@gmail.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Shivam Rawat <161387515+shivamrawat1@users.noreply.github.com>
Co-authored-by: michelligabriele <gabriele.michelli@icloud.com>
Sameerlite added a commit that referenced this pull request Feb 11, 2026
…logging_payload is missing (#20851)

* fix: Preserved nullable object fields by carrying schema properties

* Fix: _convert_schema_types

* Fix all mypy issues

* Add alert about email notifications

* fixing tests

* extending timeout for long running tests

* Text changes

* [Feat] MCP Oauth2 Fixes - Add support for MCP M2M Oauth2 support (#20788)

* add has_client_credentials

* MCPOAuth2TokenCache

* init MCP Oauth2 constants

* MCPOAuth2TokenCache

* resolve_mcp_auth

* test fixes

* docs fix

* address greptile review: min TTL, env-configurable constants, tests, docs

- Fix zero-TTL edge case: floor at MCP_OAUTH2_TOKEN_CACHE_MIN_TTL (10s)
- Make all MCP OAuth2 constants env-configurable via os.getenv()
- Move test file to follow 1:1 mapping convention (test_oauth2_token_cache.py)
- Add MCP OAuth doc page (mcp_oauth.md) with M2M and PKCE sections
- Update FAQ in mcp.md to reflect M2M support
- Add E2E test script and config

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix mypy lint

* fix oauth2

* remove old files

* docs fix

* address greptile comments

* fix: atomic lock creation + validate JSON response shape

- Use dict.setdefault() for atomic per-server lock creation
- Add isinstance(body, dict) check before accessing token response fields

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: replace asserts with proper guards, wrap HTTP errors with context

- Replace `assert` statements with `if/raise ValueError` (asserts can be
  disabled with python -O in production)
- Wrap `httpx.HTTPStatusError` to provide a clear error message with
  server_id and status code
- Add tests for HTTP error and non-dict JSON response error paths
- Remove unused imports

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>

* [UI] M2M OAuth2 UI Flow  (#20794)

* add has_client_credentials

* MCPOAuth2TokenCache

* init MCP Oauth2 constants

* MCPOAuth2TokenCache

* resolve_mcp_auth

* test fixes

* docs fix

* address greptile review: min TTL, env-configurable constants, tests, docs

- Fix zero-TTL edge case: floor at MCP_OAUTH2_TOKEN_CACHE_MIN_TTL (10s)
- Make all MCP OAuth2 constants env-configurable via os.getenv()
- Move test file to follow 1:1 mapping convention (test_oauth2_token_cache.py)
- Add MCP OAuth doc page (mcp_oauth.md) with M2M and PKCE sections
- Update FAQ in mcp.md to reflect M2M support
- Add E2E test script and config

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix mypy lint

* fix oauth2

* ui feat fixes

* test M2M

* test fix

* ui feats

* ui fixes

* ui fix client ID

* fix: backend endpoints

* docs fix

* fixes greptile

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>

* [Fix] prevent shared backend model key from being polluted by per-deployment custom pricing (#20679)

* bug: custom price override for models

* added associated test

* fix(mcp): resolve OAuth2 root endpoints returning "MCP server not found" (#20784)

When MCP SDK hits root-level /register, /authorize, /token without
server name prefix, auto-resolve to the single configured OAuth2
server. Also fix WWW-Authenticate header to use correct public URL
behind reverse proxy.

* Add support for langchain_aws via litellm passthrough

* fix(proxy): return early instead of raising ValueError when standard_logging_payload is missing

The `_PROXY_VirtualKeyModelMaxBudgetLimiter.async_log_success_event` hook
raises `ValueError` when `standard_logging_payload` is `None`.  This breaks
non-standard call types (e.g. vLLM `/classify`) that do not populate the
payload, and the resulting exception disrupts downstream success callbacks
like Langfuse.

Return early with a debug log instead, matching the existing pattern used
for missing `user_api_key_model_max_budget`.

Fixes #18986

---------

Co-authored-by: Sameer Kankute <sameer@berri.ai>
Co-authored-by: yuneng-jiang <yuneng.jiang@gmail.com>
Co-authored-by: Ishaan Jaff <ishaanjaffer0324@gmail.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Shivam Rawat <161387515+shivamrawat1@users.noreply.github.com>
Co-authored-by: michelligabriele <gabriele.michelli@icloud.com>
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.

1 participant