[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>
This commit is contained in:
Ishaan Jaff 2026-02-09 19:28:02 -08:00 committed by GitHub
parent b70f97e653
commit 36e0361187
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 494 additions and 280 deletions

View file

@ -1,6 +1,3 @@
import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';
# MCP OAuth
LiteLLM supports two OAuth 2.0 flows for MCP servers:
@ -98,8 +95,71 @@ LiteLLM automatically fetches, caches, and refreshes OAuth2 tokens using the `cl
### Setup
<Tabs>
<TabItem value="config" label="config.yaml">
You can configure M2M OAuth via the LiteLLM UI or `config.yaml`.
### UI Setup
Navigate to the **MCP Servers** page and click **+ Add New MCP Server**.
![](https://colony-recorder.s3.amazonaws.com/files/2026-02-10/d1f1e89c-a789-4975-8846-b15d9821984a/ascreenshot_630800e00a2e4b598baabfc25efbabd3_text_export.jpeg)
Enter a name for your server and select **HTTP** as the transport type.
![](https://colony-recorder.s3.amazonaws.com/files/2026-02-10/2008c9d6-6093-4121-beab-1e52c71376aa/ascreenshot_516ffd6c7b524465a253a56048c3d228_text_export.jpeg)
Paste the MCP server URL.
![](https://colony-recorder.s3.amazonaws.com/files/2026-02-10/b0ee8b7d-6de8-492b-8962-287987feec29/ascreenshot_b3efca82078a4c6bb1453c58161909f9_text_export.jpeg)
Under **Authentication**, select **OAuth**.
![](https://colony-recorder.s3.amazonaws.com/files/2026-02-10/e1597814-ff8e-40b9-9d7b-864dcdbe0910/ascreenshot_2097612712264d8f9e553f7ca9175fb0_text_export.jpeg)
![](https://colony-recorder.s3.amazonaws.com/files/2026-02-10/f6ea5694-f28a-4bc3-9c9a-bb79f199bd65/ascreenshot_9be839f55b1b4f96bfe24030ba2c7f8d_text_export.jpeg)
Choose **Machine-to-Machine (M2M)** as the OAuth flow type. This is for server-to-server authentication using the `client_credentials` grant — no browser interaction required.
![](https://colony-recorder.s3.amazonaws.com/files/2026-02-10/9853310c-1d86-4628-bad1-7a391eca0e4d/ascreenshot_f302a286fa264fdd8d56db53b8f9395c_text_export.jpeg)
![](https://colony-recorder.s3.amazonaws.com/files/2026-02-10/df64dc65-ef86-475d-adaf-12e227d5e873/ascreenshot_9e2f41d43a76435f918a00b52ffcc639_text_export.jpeg)
Fill in the **Client ID** and **Client Secret** provided by your OAuth provider.
![](https://colony-recorder.s3.amazonaws.com/files/2026-02-10/0de5a7bd-9898-4fc7-8843-b23dd5aac47f/ascreenshot_b9087aaa81a14b5b9c199929efc4a563_text_export.jpeg)
Enter the **Token URL** — this is the endpoint LiteLLM will call to fetch access tokens using `client_credentials`.
![](https://colony-recorder.s3.amazonaws.com/files/2026-02-10/0aea70f1-558c-4dca-91bc-1175fe1ddc89/ascreenshot_b3fcf8a1287e4e2d9a3d67c4a29f7bff_text_export.jpeg)
![](https://colony-recorder.s3.amazonaws.com/files/2026-02-10/e842ef09-1fd7-47a6-909b-252d389f0abc/ascreenshot_2a87dad3624847e7ac370591d1d1aedd_text_export.jpeg)
Scroll down and review the server URL and all fields, then click **Create MCP Server**.
![](https://colony-recorder.s3.amazonaws.com/files/2026-02-10/0857712b-4b53-40f8-8c1f-a4c72edaa644/ascreenshot_47be3fcd5de64ed391f70c1fb74a8bfc_text_export.jpeg)
![](https://colony-recorder.s3.amazonaws.com/files/2026-02-10/9d961765-955f-4905-a3dc-1a446aa3b2cc/ascreenshot_43fd39d014224564bc6b35aced1fb6d3_text_export.jpeg)
![](https://colony-recorder.s3.amazonaws.com/files/2026-02-10/3825d5fa-8fd1-4e71-b090-77ff0259c3f6/ascreenshot_2509a7ebd9bf421eb0e82f2553566745_text_export.jpeg)
Once created, open the server and navigate to the **MCP Tools** tab to verify that LiteLLM can connect and list available tools.
![](https://colony-recorder.s3.amazonaws.com/files/2026-02-10/8107e27b-5072-4675-8fd6-89b47692b1bd/ascreenshot_f774bc76138f430d808fb4482ebfcdca_text_export.jpeg)
![](https://colony-recorder.s3.amazonaws.com/files/2026-02-10/ce94bb7b-c81b-4396-9939-178efb2cdfce/ascreenshot_28b838ab6ae34c76858454555c4c1d79_text_export.jpeg)
Select a tool (e.g. **echo**) to test it. Fill in the required parameters and click **Call Tool**.
![](https://colony-recorder.s3.amazonaws.com/files/2026-02-10/c459c1d3-ec29-4211-9c28-37fbe7783bbc/ascreenshot_e9b138b3c2cc4440bb1a6f42ac7ae861_text_export.jpeg)
![](https://colony-recorder.s3.amazonaws.com/files/2026-02-10/5438ac60-e0ac-4a79-bf6f-5594f160d3b5/ascreenshot_9133a17d26204c46bce497e74685c483_text_export.jpeg)
![](https://colony-recorder.s3.amazonaws.com/files/2026-02-10/a8f6821b-3982-4b4d-9b25-70c8aff5ac31/ascreenshot_28d474d0e62545a482cff6128527883a_text_export.jpeg)
LiteLLM automatically fetches an OAuth token behind the scenes and calls the tool. The result confirms the M2M OAuth flow is working end-to-end.
![](https://colony-recorder.s3.amazonaws.com/files/2026-02-10/c3924549-a949-48d1-ac67-ab4c30475859/ascreenshot_8f6eca9d717f45478d50a881bd244bb3_text_export.jpeg)
### Config.yaml Setup
```yaml title="config.yaml" showLineNumbers
mcp_servers:
@ -112,14 +172,6 @@ mcp_servers:
scopes: ["mcp:read", "mcp:write"] # optional
```
</TabItem>
<TabItem value="ui" label="LiteLLM UI">
Navigate to **MCP Servers → Add Server → Authentication → OAuth**, then fill in `client_id`, `client_secret`, and `token_url`.
</TabItem>
</Tabs>
### How It Works
1. On first MCP request, LiteLLM POSTs to `token_url` with `grant_type=client_credentials`

View file

@ -1,6 +1,6 @@
import importlib
from datetime import datetime
from typing import Dict, List, Optional, Union
from typing import Any, Awaitable, Callable, Dict, List, Optional, Union
from fastapi import APIRouter, Depends, HTTPException, Query, Request
@ -501,24 +501,50 @@ if MCP_AVAILABLE:
NewMCPServerRequest,
)
def _extract_credentials(
request: NewMCPServerRequest,
) -> tuple:
"""
Extract OAuth credentials from the nested ``request.credentials`` dict.
Returns:
(client_id, client_secret, scopes) any value may be ``None``.
"""
creds = request.credentials if isinstance(request.credentials, dict) else {}
client_id: Optional[str] = creds.get("client_id")
client_secret: Optional[str] = creds.get("client_secret")
scopes_raw = creds.get("scopes")
scopes: Optional[List[str]] = scopes_raw if isinstance(scopes_raw, list) else None
return client_id, client_secret, scopes
async def _execute_with_mcp_client(
request: NewMCPServerRequest,
operation,
operation: Callable[..., Awaitable[Any]],
mcp_auth_header: Optional[Union[str, Dict[str, str]]] = None,
oauth2_headers: Optional[Dict[str, str]] = None,
raw_headers: Optional[Dict[str, str]] = None,
):
) -> dict:
"""
Common helper to create MCP client, execute operation, and ensure proper cleanup.
Create a temporary MCP client from *request*, run *operation*, and return the result.
For M2M OAuth servers (those with ``client_id``, ``client_secret``, and
``token_url``), the incoming ``oauth2_headers`` are dropped so that
``resolve_mcp_auth`` can auto-fetch a token via ``client_credentials``.
Args:
request: MCP server configuration
operation: Async function that takes a client and returns the operation result
request: MCP server configuration submitted by the UI.
operation: Async callable that receives the created client and returns a result dict.
mcp_auth_header: Pre-resolved credential header (API-key / bearer token).
oauth2_headers: Headers extracted from the incoming request (may contain the
litellm API key must NOT be forwarded for M2M servers).
raw_headers: Raw request headers forwarded for stdio env construction.
Returns:
Operation result or error response
The dict returned by *operation*, or an error dict on failure.
"""
try:
client_id, client_secret, scopes = _extract_credentials(request)
server_model = MCPServer(
server_id=request.server_id or "",
name=request.alias or request.server_name or "",
@ -530,14 +556,26 @@ if MCP_AVAILABLE:
args=request.args,
env=request.env,
static_headers=request.static_headers,
client_id=client_id,
client_secret=client_secret,
token_url=request.token_url,
scopes=scopes,
authorization_url=request.authorization_url,
registration_url=request.registration_url,
)
stdio_env = global_mcp_server_manager._build_stdio_env(
server_model, raw_headers
)
# For M2M OAuth servers, drop the incoming Authorization header so that
# resolve_mcp_auth can auto-fetch a token via client_credentials.
effective_oauth2_headers = (
None if server_model.has_client_credentials else oauth2_headers
)
merged_headers = merge_mcp_headers(
extra_headers=oauth2_headers,
extra_headers=effective_oauth2_headers,
static_headers=request.static_headers,
)
@ -550,11 +588,14 @@ if MCP_AVAILABLE:
return await operation(client)
except Exception as e:
verbose_logger.error(f"Error in MCP operation: {e}", exc_info=True)
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": "Failed to connect to MCP server. Check proxy logs for details.",
}
@router.post("/test/connection", dependencies=[Depends(user_api_key_auth)])

View file

@ -46,6 +46,7 @@ mcp_servers:
transport: "http"
url: "https://mcp.deepwiki.com/mcp"
# General Settings
general_settings:
master_key: sk-1234

View file

@ -0,0 +1,14 @@
model_list:
- model_name: fake-model
litellm_params:
model: openai/fake
api_key: fake-key
mcp_servers:
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"

View file

@ -155,6 +155,160 @@ class TestExecuteWithMcpClient:
}
@pytest.mark.asyncio
async def test_m2m_credentials_forwarded_to_server_model(self, monkeypatch):
"""M2M OAuth credentials (client_id, client_secret) from the nested
``credentials`` dict must be forwarded to the MCPServer model so that
``has_client_credentials`` returns True and the proxy auto-fetches tokens."""
captured: dict = {}
def fake_build_stdio_env(server, raw_headers):
return None
async def fake_create_client(*args, **kwargs):
captured["server"] = kwargs.get("server")
return object()
monkeypatch.setattr(
rest_endpoints.global_mcp_server_manager,
"_build_stdio_env",
fake_build_stdio_env,
raising=False,
)
monkeypatch.setattr(
rest_endpoints.global_mcp_server_manager,
"_create_mcp_client",
fake_create_client,
raising=False,
)
async def ok_operation(client):
return {"status": "ok"}
payload = NewMCPServerRequest(
server_name="m2m-server",
url="https://example.com",
auth_type=MCPAuth.oauth2,
token_url="https://auth.example.com/token",
credentials={
"client_id": "my-id",
"client_secret": "my-secret",
"scopes": ["read", "write"],
},
)
result = await rest_endpoints._execute_with_mcp_client(
payload, ok_operation
)
assert result["status"] == "ok"
server = captured["server"]
assert server.client_id == "my-id"
assert server.client_secret == "my-secret"
assert server.token_url == "https://auth.example.com/token"
assert server.scopes == ["read", "write"]
assert server.has_client_credentials is True
@pytest.mark.asyncio
async def test_m2m_drops_incoming_oauth2_headers(self, monkeypatch):
"""For M2M OAuth servers the incoming Authorization header (which carries
the litellm API key) must NOT be forwarded as extra_headers otherwise
it overwrites the auto-fetched M2M token."""
captured: dict = {}
def fake_build_stdio_env(server, raw_headers):
return None
async def fake_create_client(*args, **kwargs):
captured["extra_headers"] = kwargs.get("extra_headers")
return object()
monkeypatch.setattr(
rest_endpoints.global_mcp_server_manager,
"_build_stdio_env",
fake_build_stdio_env,
raising=False,
)
monkeypatch.setattr(
rest_endpoints.global_mcp_server_manager,
"_create_mcp_client",
fake_create_client,
raising=False,
)
async def ok_operation(client):
return {"status": "ok"}
payload = NewMCPServerRequest(
server_name="m2m-server",
url="https://example.com",
auth_type=MCPAuth.oauth2,
token_url="https://auth.example.com/token",
credentials={
"client_id": "my-id",
"client_secret": "my-secret",
},
)
incoming_oauth2 = {"Authorization": "Bearer sk-litellm-api-key"}
result = await rest_endpoints._execute_with_mcp_client(
payload,
ok_operation,
oauth2_headers=incoming_oauth2,
)
assert result["status"] == "ok"
# The incoming Authorization must be dropped — extra_headers should
# contain no oauth2 headers (only static_headers, which are None here).
assert captured["extra_headers"] is None or "Authorization" not in captured["extra_headers"]
@pytest.mark.asyncio
async def test_catches_exception_group(self, monkeypatch):
"""MCP SDK's anyio TaskGroup raises BaseExceptionGroup which does not
inherit from Exception. The handler must catch it and return an error
dict instead of letting a raw 500 propagate."""
def fake_build_stdio_env(server, raw_headers):
return None
async def fake_create_client(*args, **kwargs):
raise BaseExceptionGroup(
"test group", [RuntimeError("Cancelled via cancel scope")]
)
monkeypatch.setattr(
rest_endpoints.global_mcp_server_manager,
"_build_stdio_env",
fake_build_stdio_env,
raising=False,
)
monkeypatch.setattr(
rest_endpoints.global_mcp_server_manager,
"_create_mcp_client",
fake_create_client,
raising=False,
)
async def ok_operation(client):
return {"status": "ok"}
payload = NewMCPServerRequest(
server_name="bad-server",
url="https://example.com",
auth_type=MCPAuth.none,
)
result = await rest_endpoints._execute_with_mcp_client(
payload, ok_operation
)
assert result["status"] == "error"
assert result["error"] is True
assert "Failed to connect to MCP server" in result["message"]
# Error message must not leak raw exception details
assert "cancel scope" not in result["message"]
class TestTestConnection:
def test_requires_auth_dependency(self):
route = _get_route("/mcp-rest/test/connection", "POST")

View file

@ -0,0 +1,166 @@
import React from "react";
import { Form, Select, Tooltip } from "antd";
import { InfoCircleOutlined } from "@ant-design/icons";
import { Button, TextInput } from "@tremor/react";
import { OAUTH_FLOW } from "./types";
interface OAuthFlowStatus {
startOAuthFlow: () => void;
status: string;
error: string | null;
tokenResponse: { access_token?: string; expires_in?: number } | null;
}
interface OAuthFormFieldsProps {
isM2M: boolean;
isEditing?: boolean;
oauthFlow?: OAuthFlowStatus;
initialFlowType?: string;
}
const fieldClassName = "rounded-lg border-gray-300 focus:border-blue-500 focus:ring-blue-500";
const FieldLabel: React.FC<{ label: string; tooltip: string }> = ({ label, tooltip }) => (
<span className="text-sm font-medium text-gray-700 flex items-center">
{label}
<Tooltip title={tooltip}>
<InfoCircleOutlined className="ml-2 text-blue-400 hover:text-blue-600 cursor-help" />
</Tooltip>
</span>
);
const OAuthFormFields: React.FC<OAuthFormFieldsProps> = ({
isM2M,
isEditing = false,
oauthFlow,
initialFlowType,
}) => {
const placeholderSuffix = isEditing ? " (leave blank to keep existing)" : "";
return (
<>
<Form.Item
label={
<FieldLabel
label="OAuth Flow Type"
tooltip="Choose how the proxy authenticates with this MCP server. M2M is for server-to-server communication using client credentials. Interactive (PKCE) is for user-facing flows that require browser-based authorization."
/>
}
name="oauth_flow_type"
{...(initialFlowType ? { initialValue: initialFlowType } : {})}
>
<Select className="rounded-lg" size="large">
<Select.Option value={OAUTH_FLOW.M2M}>
<div>
<span className="font-medium">Machine-to-Machine (M2M)</span>
<span className="text-gray-400 text-xs ml-2">server-to-server, no user interaction</span>
</div>
</Select.Option>
<Select.Option value={OAUTH_FLOW.INTERACTIVE}>
<div>
<span className="font-medium">Interactive (PKCE)</span>
<span className="text-gray-400 text-xs ml-2">browser-based user authorization</span>
</div>
</Select.Option>
</Select>
</Form.Item>
{isM2M ? (
<>
<Form.Item
label={<FieldLabel label="Client ID" tooltip="OAuth2 client ID for the client_credentials grant." />}
name={["credentials", "client_id"]}
rules={[{ required: true, message: "Client ID is required for M2M OAuth" }]}
>
<TextInput type="password" placeholder={`Enter OAuth client ID${placeholderSuffix}`} className={fieldClassName} />
</Form.Item>
<Form.Item
label={<FieldLabel label="Client Secret" tooltip="OAuth2 client secret for the client_credentials grant." />}
name={["credentials", "client_secret"]}
rules={[{ required: true, message: "Client Secret is required for M2M OAuth" }]}
>
<TextInput type="password" placeholder={`Enter OAuth client secret${placeholderSuffix}`} className={fieldClassName} />
</Form.Item>
<Form.Item
label={<FieldLabel label="Token URL" tooltip="Token endpoint URL for the client_credentials grant." />}
name="token_url"
rules={[{ required: true, message: "Token URL is required for M2M OAuth" }]}
>
<TextInput placeholder="https://auth.example.com/oauth/token" className={fieldClassName} />
</Form.Item>
<Form.Item
label={<FieldLabel label="Scopes (optional)" tooltip="Optional scopes to request with the client_credentials grant." />}
name={["credentials", "scopes"]}
>
<Select mode="tags" tokenSeparators={[","]} placeholder="Add scopes" className="rounded-lg" size="large" />
</Form.Item>
</>
) : (
<>
<Form.Item
label={<FieldLabel label="Client ID (optional)" tooltip="Provide only if your MCP server cannot handle dynamic client registration." />}
name={["credentials", "client_id"]}
>
<TextInput type="password" placeholder={`Enter client ID${placeholderSuffix}`} className={fieldClassName} />
</Form.Item>
<Form.Item
label={<FieldLabel label="Client Secret (optional)" tooltip="Provide only if your MCP server cannot handle dynamic client registration." />}
name={["credentials", "client_secret"]}
>
<TextInput type="password" placeholder={`Enter client secret${placeholderSuffix}`} className={fieldClassName} />
</Form.Item>
<Form.Item
label={<FieldLabel label="Scopes (optional)" tooltip="Optional scopes requested during token exchange. Separate multiple scopes with enter or commas." />}
name={["credentials", "scopes"]}
>
<Select mode="tags" tokenSeparators={[","]} placeholder="Add scopes" className="rounded-lg" size="large" />
</Form.Item>
<Form.Item
label={<FieldLabel label="Authorization URL (optional)" tooltip="Optional override for the authorization endpoint." />}
name="authorization_url"
>
<TextInput placeholder="https://example.com/oauth/authorize" className={fieldClassName} />
</Form.Item>
<Form.Item
label={<FieldLabel label="Token URL (optional)" tooltip="Optional override for the token endpoint." />}
name="token_url"
>
<TextInput placeholder="https://example.com/oauth/token" className={fieldClassName} />
</Form.Item>
<Form.Item
label={<FieldLabel label="Registration URL (optional)" tooltip="Optional override for the dynamic client registration endpoint." />}
name="registration_url"
>
<TextInput placeholder="https://example.com/oauth/register" className={fieldClassName} />
</Form.Item>
{oauthFlow && (
<div className="rounded-lg border border-dashed border-gray-300 p-4 space-y-2">
<p className="text-sm text-gray-600">
Use OAuth to fetch a fresh access token and temporarily save it in the session as the authentication value.
</p>
<Button
variant="secondary"
onClick={oauthFlow.startOAuthFlow}
disabled={oauthFlow.status === "authorizing" || oauthFlow.status === "exchanging"}
>
{oauthFlow.status === "authorizing"
? "Waiting for authorization..."
: oauthFlow.status === "exchanging"
? "Exchanging authorization code..."
: "Authorize & Fetch Token"}
</Button>
{oauthFlow.error && <p className="text-sm text-red-500">{oauthFlow.error}</p>}
{oauthFlow.status === "success" && oauthFlow.tokenResponse?.access_token && (
<p className="text-sm text-green-600">
Token fetched. Expires in {oauthFlow.tokenResponse.expires_in ?? "?"} seconds.
</p>
)}
</div>
)}
</>
)}
</>
);
};
export default OAuthFormFields;

View file

@ -3,7 +3,8 @@ import { Modal, Tooltip, Form, Select, Input } from "antd";
import { InfoCircleOutlined } from "@ant-design/icons";
import { Button, TextInput } from "@tremor/react";
import { createMCPServer } from "../networking";
import { AUTH_TYPE, MCPServer, MCPServerCostInfo } from "./types";
import { AUTH_TYPE, OAUTH_FLOW, MCPServer, MCPServerCostInfo } from "./types";
import OAuthFormFields from "./OAuthFormFields";
import MCPServerCostConfig from "./mcp_server_cost_config";
import MCPConnectionStatus from "./mcp_connection_status";
import MCPToolConfiguration from "./mcp_tool_configuration";
@ -52,6 +53,7 @@ const CreateMCPServer: React.FC<CreateMCPServerProps> = ({
const authType = formValues.auth_type as string | undefined;
const shouldShowAuthValueField = authType ? AUTH_TYPES_REQUIRING_AUTH_VALUE.includes(authType) : false;
const isOAuthAuthType = authType === AUTH_TYPE.OAUTH2;
const isM2MFlow = isOAuthAuthType && formValues.oauth_flow_type === OAUTH_FLOW.M2M;
const persistCreateUiState = () => {
if (typeof window === "undefined") {
@ -477,7 +479,7 @@ const CreateMCPServer: React.FC<CreateMCPServerProps> = ({
rules={[
{
required: false,
message: "Please enter a server description!!!!!!!!!",
message: "Please enter a server description",
},
]}
>
@ -561,131 +563,16 @@ const CreateMCPServer: React.FC<CreateMCPServerProps> = ({
)}
{transportType !== "stdio" && isOAuthAuthType && (
<>
<Form.Item
label={
<span className="text-sm font-medium text-gray-700 flex items-center">
OAuth Client ID (optional)
<Tooltip title="Provide only if your MCP server cannot handle dynamic client registration.">
<InfoCircleOutlined className="ml-2 text-blue-400 hover:text-blue-600 cursor-help" />
</Tooltip>
</span>
}
name={["credentials", "client_id"]}
>
<TextInput
type="password"
placeholder="Enter OAuth client ID"
className="rounded-lg border-gray-300 focus:border-blue-500 focus:ring-blue-500"
/>
</Form.Item>
<Form.Item
label={
<span className="text-sm font-medium text-gray-700 flex items-center">
OAuth Client Secret (optional)
<Tooltip title="Provide only if your MCP server cannot handle dynamic client registration.">
<InfoCircleOutlined className="ml-2 text-blue-400 hover:text-blue-600 cursor-help" />
</Tooltip>
</span>
}
name={["credentials", "client_secret"]}
>
<TextInput
type="password"
placeholder="Enter OAuth client secret"
className="rounded-lg border-gray-300 focus:border-blue-500 focus:ring-blue-500"
/>
</Form.Item>
<Form.Item
label={
<span className="text-sm font-medium text-gray-700 flex items-center">
OAuth Scopes (optional)
<Tooltip title="Optional scopes requested during token exchange. Separate multiple scopes with enter or commas.">
<InfoCircleOutlined className="ml-2 text-blue-400 hover:text-blue-600 cursor-help" />
</Tooltip>
</span>
}
name={["credentials", "scopes"]}
>
<Select
mode="tags"
tokenSeparators={[","]}
placeholder="Add scopes"
className="rounded-lg"
size="large"
/>
</Form.Item>
<Form.Item
label={
<span className="text-sm font-medium text-gray-700 flex items-center">
Authorization URL Override (optional)
<Tooltip title="Optional override for the authorization endpoint.">
<InfoCircleOutlined className="ml-2 text-blue-400 hover:text-blue-600 cursor-help" />
</Tooltip>
</span>
}
name="authorization_url"
>
<TextInput
placeholder="https://example.com/oauth/authorize"
className="rounded-lg border-gray-300 focus:border-blue-500 focus:ring-blue-500"
/>
</Form.Item>
<Form.Item
label={
<span className="text-sm font-medium text-gray-700 flex items-center">
Token URL Override (optional)
<Tooltip title="Optional override for the token endpoint.">
<InfoCircleOutlined className="ml-2 text-blue-400 hover:text-blue-600 cursor-help" />
</Tooltip>
</span>
}
name="token_url"
>
<TextInput
placeholder="https://example.com/oauth/token"
className="rounded-lg border-gray-300 focus:border-blue-500 focus:ring-blue-500"
/>
</Form.Item>
<Form.Item
label={
<span className="text-sm font-medium text-gray-700 flex items-center">
Registration URL Override (optional)
<Tooltip title="Optional orverride for the dynamic client registration endpoint.">
<InfoCircleOutlined className="ml-2 text-blue-400 hover:text-blue-600 cursor-help" />
</Tooltip>
</span>
}
name="registration_url"
>
<TextInput
placeholder="https://example.com/oauth/register"
className="rounded-lg border-gray-300 focus:border-blue-500 focus:ring-blue-500"
/>
</Form.Item>
<div className="rounded-lg border border-dashed border-gray-300 p-4 space-y-2">
<p className="text-sm text-gray-600">
Use OAuth to fetch a fresh access token and temporarily save it in the session as the authentication value.
</p>
<Button
variant="secondary"
onClick={startOAuthFlow}
disabled={oauthStatus === "authorizing" || oauthStatus === "exchanging"}
>
{oauthStatus === "authorizing"
? "Waiting for authorization..."
: oauthStatus === "exchanging"
? "Exchanging authorization code..."
: "Authorize & Fetch Token"}
</Button>
{oauthError && <p className="text-sm text-red-500">{oauthError}</p>}
{oauthStatus === "success" && oauthTokenResponse?.access_token && (
<p className="text-sm text-green-600">
Token fetched. Expires in {oauthTokenResponse.expires_in ?? "?"} seconds.
</p>
)}
</div>
</>
<OAuthFormFields
isM2M={isM2MFlow}
initialFlowType={OAUTH_FLOW.INTERACTIVE}
oauthFlow={{
startOAuthFlow,
status: oauthStatus,
error: oauthError,
tokenResponse: oauthTokenResponse,
}}
/>
)}
{/* Stdio Configuration - only show for stdio transport */}

View file

@ -2,7 +2,8 @@ import React, { useState, useEffect } from "react";
import { Form, Select, Button as AntdButton, Tooltip } from "antd";
import { InfoCircleOutlined } from "@ant-design/icons";
import { Button, TextInput, TabGroup, TabList, Tab, TabPanels, TabPanel } from "@tremor/react";
import { AUTH_TYPE, MCPServer, MCPServerCostInfo } from "./types";
import { AUTH_TYPE, OAUTH_FLOW, MCPServer, MCPServerCostInfo } from "./types";
import OAuthFormFields from "./OAuthFormFields";
import { updateMCPServer, testMCPToolsListRequest } from "../networking";
import MCPServerCostConfig from "./mcp_server_cost_config";
import MCPPermissionManagement from "./MCPPermissionManagement";
@ -41,7 +42,9 @@ const MCPServerEdit: React.FC<MCPServerEditProps> = ({
const authType = Form.useWatch("auth_type", form) as string | undefined;
const shouldShowAuthValueField = authType ? AUTH_TYPES_REQUIRING_AUTH_VALUE.includes(authType) : false;
const isOAuthAuthType = authType === AUTH_TYPE.OAUTH2;
const oauthFlowTypeValue = Form.useWatch("oauth_flow_type", form) as string | undefined;
const isM2MFlow = isOAuthAuthType && oauthFlowTypeValue === OAUTH_FLOW.M2M;
const [oauthAccessToken, setOauthAccessToken] = useState<string | null>(null);
const persistEditUiState = () => {
@ -128,6 +131,7 @@ const MCPServerEdit: React.FC<MCPServerEditProps> = ({
() => ({
...mcpServer,
static_headers: initialStaticHeaders,
oauth_flow_type: mcpServer.token_url ? OAUTH_FLOW.M2M : OAUTH_FLOW.INTERACTIVE,
}),
[mcpServer, initialStaticHeaders],
);
@ -214,7 +218,8 @@ const MCPServerEdit: React.FC<MCPServerEditProps> = ({
return;
}
if (mcpServer.auth_type === AUTH_TYPE.OAUTH2 && !oauthAccessToken) {
const isM2M = mcpServer.auth_type === AUTH_TYPE.OAUTH2 && !!mcpServer.token_url;
if (mcpServer.auth_type === AUTH_TYPE.OAUTH2 && !isM2M && !oauthAccessToken) {
return;
}
@ -452,129 +457,16 @@ const MCPServerEdit: React.FC<MCPServerEditProps> = ({
)}
{isOAuthAuthType && (
<>
<Form.Item
label={
<span className="text-sm font-medium text-gray-700 flex items-center">
OAuth Client ID (optional)
<Tooltip title="Provide only if your MCP server cannot handle dynamic client registration.">
<InfoCircleOutlined className="ml-2 text-blue-400 hover:text-blue-600 cursor-help" />
</Tooltip>
</span>
}
name={["credentials", "client_id"]}
>
<TextInput
type="password"
placeholder="Enter OAuth client ID (leave blank to keep existing)"
className="rounded-lg border-gray-300 focus:border-blue-500 focus:ring-blue-500"
/>
</Form.Item>
<Form.Item
label={
<span className="text-sm font-medium text-gray-700 flex items-center">
OAuth Client Secret (optional)
<Tooltip title="Provide only if your MCP server cannot handle dynamic client registration.">
<InfoCircleOutlined className="ml-2 text-blue-400 hover:text-blue-600 cursor-help" />
</Tooltip>
</span>
}
name={["credentials", "client_secret"]}
>
<TextInput
type="password"
placeholder="Enter OAuth client secret (leave blank to keep existing)"
className="rounded-lg border-gray-300 focus:border-blue-500 focus:ring-blue-500"
/>
</Form.Item>
<Form.Item
label={
<span className="text-sm font-medium text-gray-700 flex items-center">
OAuth Scopes (optional)
<Tooltip title="Add scopes to override the default scope list used for this MCP server.">
<InfoCircleOutlined className="ml-2 text-blue-400 hover:text-blue-600 cursor-help" />
</Tooltip>
</span>
}
name={["credentials", "scopes"]}
>
<Select
mode="tags"
tokenSeparators={[","]}
placeholder="Add scopes"
className="rounded-lg"
size="large"
/>
</Form.Item>
<Form.Item
label={
<span className="text-sm font-medium text-gray-700 flex items-center">
Authorization URL Override (optional)
<Tooltip title="Optional override for the authorization endpoint.">
<InfoCircleOutlined className="ml-2 text-blue-400 hover:text-blue-600 cursor-help" />
</Tooltip>
</span>
}
name="authorization_url"
>
<TextInput
placeholder="https://example.com/oauth/authorize"
className="rounded-lg border-gray-300 focus:border-blue-500 focus:ring-blue-500"
/>
</Form.Item>
<Form.Item
label={
<span className="text-sm font-medium text-gray-700 flex items-center">
Token URL Override (optional)
<Tooltip title="Optional override for the token endpoint.">
<InfoCircleOutlined className="ml-2 text-blue-400 hover:text-blue-600 cursor-help" />
</Tooltip>
</span>
}
name="token_url"
>
<TextInput
placeholder="https://example.com/oauth/token"
className="rounded-lg border-gray-300 focus:border-blue-500 focus:ring-blue-500"
/>
</Form.Item>
<Form.Item
label={
<span className="text-sm font-medium text-gray-700 flex items-center">
Registration URL Override (optional)
<Tooltip title="Optional override for the dynamic client registration endpoint.">
<InfoCircleOutlined className="ml-2 text-blue-400 hover:text-blue-600 cursor-help" />
</Tooltip>
</span>
}
name="registration_url"
>
<TextInput
placeholder="https://example.com/oauth/register"
className="rounded-lg border-gray-300 focus:border-blue-500 focus:ring-blue-500"
/>
</Form.Item>
<div className="rounded-lg border border-dashed border-gray-300 p-4 space-y-2">
<p className="text-sm text-gray-600">Use OAuth to fetch a fresh access token and temporarily save it in the session as the authentication value.</p>
<Button
variant="secondary"
onClick={startOAuthFlow}
disabled={oauthStatus === "authorizing" || oauthStatus === "exchanging"}
>
{oauthStatus === "authorizing"
? "Waiting for authorization..."
: oauthStatus === "exchanging"
? "Exchanging authorization code..."
: "Authorize & Fetch Token"}
</Button>
{oauthError && <p className="text-sm text-red-500">{oauthError}</p>}
{oauthStatus === "success" && oauthTokenResponse?.access_token && (
<p className="text-sm text-green-600">
Token fetched. Expires in {oauthTokenResponse.expires_in ?? "?"} seconds.
</p>
)}
</div>
</>
<OAuthFormFields
isM2M={isM2MFlow}
isEditing
oauthFlow={{
startOAuthFlow,
status: oauthStatus,
error: oauthError,
tokenResponse: oauthTokenResponse,
}}
/>
)}
{/* Permission Management / Access Control Section */}
@ -600,6 +492,7 @@ const MCPServerEdit: React.FC<MCPServerEditProps> = ({
transport: mcpServer.transport,
auth_type: mcpServer.auth_type,
mcp_info: mcpServer.mcp_info,
oauth_flow_type: mcpServer.token_url ? OAUTH_FLOW.M2M : OAUTH_FLOW.INTERACTIVE,
}}
allowedTools={allowedTools}
existingAllowedTools={mcpServer.allowed_tools || null}

View file

@ -13,13 +13,17 @@ export const AUTH_TYPE = {
OAUTH2: "oauth2",
};
export const OAUTH_FLOW = {
INTERACTIVE: "interactive",
M2M: "m2m",
};
export const TRANSPORT = {
SSE: "sse",
HTTP: "http",
};
export const handleTransport = (transport?: string | null): string => {
console.log(transport);
if (transport === null || transport === undefined) {
return TRANSPORT.SSE;
}

View file

@ -1,6 +1,6 @@
import { useState, useEffect } from "react";
import { testMCPToolsListRequest } from "../components/networking";
import { AUTH_TYPE } from "@/components/mcp_tools/types";
import { AUTH_TYPE, OAUTH_FLOW } from "@/components/mcp_tools/types";
interface MCPServerConfig {
server_id?: string;
@ -52,7 +52,9 @@ export const useTestMCPConnection = ({
const [hasShownSuccessMessage, setHasShownSuccessMessage] = useState(false);
// Check if we have the minimum required fields to fetch tools
const requiresOAuthToken = formValues.auth_type === AUTH_TYPE.OAUTH2;
const isM2MOAuth = formValues.auth_type === AUTH_TYPE.OAUTH2
&& formValues.oauth_flow_type === OAUTH_FLOW.M2M;
const requiresOAuthToken = formValues.auth_type === AUTH_TYPE.OAUTH2 && !isM2MOAuth;
const canFetchTools = !!(
formValues.url &&
formValues.transport &&