feat(mcp): native auth for GCP-managed MCP servers

This commit is contained in:
Devin AI 2026-07-24 20:54:00 +00:00
parent 35dc982692
commit 4dae85db50
13 changed files with 458 additions and 24 deletions

View file

@ -6,7 +6,9 @@ import asyncio
import base64
import os
from typing import (
TYPE_CHECKING,
Any,
AsyncGenerator,
Awaitable,
Callable,
Dict,
@ -52,6 +54,9 @@ from litellm.types.mcp import (
MCPTransportType,
)
if TYPE_CHECKING:
from litellm.llms.vertex_ai.vertex_llm_base import VertexBase
def to_basic_auth(auth_value: str) -> str:
"""Convert auth value to Basic Auth format."""
@ -192,6 +197,51 @@ class MCPSigV4Auth(httpx.Auth):
yield request
class MCPGoogleAuth(httpx.Auth):
"""
httpx Auth class that attaches a Google OAuth 2.0 access token to each request.
Used for GCP-managed MCP servers (e.g. https://bigquery.googleapis.com/mcp), which
authenticate with a Google access token. Credentials come from an explicit service
account JSON (inline or file path) or, when none is given, Application Default
Credentials, so a proxy running in GKE authenticates with its workload identity.
Tokens are cached and refreshed by the shared Vertex auth layer.
"""
def __init__(
self,
gcp_credentials: Union[str, Dict[str, str], None] = None,
gcp_project_id: str | None = None,
vertex_base: "VertexBase | None" = None,
):
from litellm.llms.vertex_ai.vertex_llm_base import VertexBase as _VertexBase
self.gcp_credentials = gcp_credentials
self.gcp_project_id = gcp_project_id
self._vertex_base = vertex_base if vertex_base is not None else _VertexBase()
def _apply_token(self, request: httpx.Request, token: str) -> None:
request.headers["Authorization"] = f"Bearer {token}"
if self.gcp_project_id:
request.headers["x-goog-user-project"] = self.gcp_project_id
def auth_flow(self, request: httpx.Request) -> Generator[httpx.Request, httpx.Response, None]:
token, _ = self._vertex_base.get_access_token(
credentials=self.gcp_credentials,
project_id=self.gcp_project_id,
)
self._apply_token(request, token)
yield request
async def async_auth_flow(self, request: httpx.Request) -> AsyncGenerator[httpx.Request, httpx.Response]:
token, _ = await self._vertex_base.get_access_token_async(
credentials=self.gcp_credentials,
project_id=self.gcp_project_id,
)
self._apply_token(request, token)
yield request
class MCPClient:
"""
MCP Client supporting:
@ -213,6 +263,7 @@ class MCPClient:
extra_headers: Optional[Dict[str, str]] = None,
ssl_verify: Optional[VerifyTypes] = None,
aws_auth: Optional[httpx.Auth] = None,
google_auth: httpx.Auth | None = None,
resolved_auth: Optional[httpx.Auth] = None,
sampling_callback: Optional[Callable] = None,
elicitation_callback: Optional[Callable] = None,
@ -227,6 +278,7 @@ class MCPClient:
self.extra_headers: Optional[Dict[str, str]] = extra_headers
self.ssl_verify: Optional[VerifyTypes] = ssl_verify
self._aws_auth: Optional[httpx.Auth] = aws_auth
self._google_auth: httpx.Auth | None = google_auth
# A pre-resolved httpx.Auth (e.g. from the v2 credential resolver) attached to the
# upstream client's auth= slot, taking precedence over the SigV4 aws_auth.
self._resolved_auth: Optional[httpx.Auth] = resolved_auth
@ -442,9 +494,10 @@ class MCPClient:
headers["Authorization"] = f"Bearer {self._mcp_auth_value}"
elif isinstance(self._mcp_auth_value, dict):
headers.update(self._mcp_auth_value)
# Note: aws_sigv4 auth is not handled here — SigV4 requires per-request
# signing (including the body hash), so it uses httpx.Auth flow instead
# of static headers. See MCPSigV4Auth and _create_httpx_client_factory().
# Note: aws_sigv4 and gcp_service_account auth are not handled here — SigV4
# requires per-request signing (including the body hash) and Google tokens
# need per-request refresh, so both use the httpx.Auth flow instead of static
# headers. See MCPSigV4Auth, MCPGoogleAuth and _create_httpx_client_factory().
# update the headers with the extra headers
if self.extra_headers:
headers.update(self.extra_headers)
@ -473,7 +526,7 @@ class MCPClient:
# The MCP SDK's sse_client and streamable_http_client call this factory without
# passing auth=, so the fallback is used: a v2-resolved auth if present, else the
# SigV4 aws_auth. Both are None for the common case — no behavior change.
fallback_auth = self._resolved_auth if self._resolved_auth is not None else self._aws_auth
fallback_auth = self._resolved_auth or self._aws_auth or self._google_auth
effective_auth = auth if auth is not None else fallback_auth
return httpx.AsyncClient(
headers=headers,

View file

@ -401,6 +401,12 @@ def encrypt_credentials(credentials: MCPCredentials, encryption_key: Optional[st
new_encryption_key=encryption_key,
)
# aws_region_name and aws_service_name are NOT secrets — stored as-is
gcp_credentials = credentials.get("gcp_credentials")
if gcp_credentials is not None:
credentials["gcp_credentials"] = encrypt_value_helper(
value=gcp_credentials,
new_encryption_key=encryption_key,
)
return credentials
@ -416,6 +422,7 @@ def decrypt_credentials(
"aws_access_key_id",
"aws_secret_access_key",
"aws_session_token",
"gcp_credentials",
]
for field in secret_fields:
value = credentials.get(field) # type: ignore[literal-required]

View file

@ -45,7 +45,7 @@ from litellm.constants import (
MCP_TOOL_LISTING_TIMEOUT,
)
from litellm.exceptions import BlockedPiiEntityError, GuardrailRaisedException
from litellm.experimental_mcp_client.client import MCPClient, MCPSigV4Auth
from litellm.experimental_mcp_client.client import MCPClient, MCPGoogleAuth, MCPSigV4Auth
from litellm.litellm_core_utils.url_utils import SSRFError, async_safe_get
from litellm.llms.custom_httpx.http_handler import get_async_httpx_client
from litellm.proxy._experimental.mcp_server.auth.user_api_key_auth_mcp import (
@ -751,8 +751,28 @@ def _consumes_caller_authorization(server: MCPServer) -> bool:
)
_SELF_CREDENTIALED_AUTH_TYPES = frozenset({MCPAuth.aws_sigv4, MCPAuth.gcp_service_account})
def _build_google_auth(server: MCPServer) -> MCPGoogleAuth | None:
"""The Google access-token auth for a GCP-managed MCP server, or None for any other auth type."""
if server.auth_type != MCPAuth.gcp_service_account:
return None
return MCPGoogleAuth(
gcp_credentials=server.gcp_credentials,
gcp_project_id=server.gcp_project_id,
)
_REGISTRY_DUMP_SECRET_FIELDS = frozenset(
{"authentication_token", "client_secret", "client_private_key", "aws_secret_access_key", "aws_session_token"}
{
"authentication_token",
"client_secret",
"client_private_key",
"aws_secret_access_key",
"aws_session_token",
"gcp_credentials",
}
)
@ -1226,7 +1246,7 @@ class MCPServerManager:
if (
server.auth_type
and server.auth_type != MCPAuth.none
and server.auth_type != MCPAuth.aws_sigv4
and server.auth_type not in _SELF_CREDENTIALED_AUTH_TYPES
and not server.authentication_token
):
return
@ -1510,6 +1530,8 @@ class MCPServerManager:
aws_service_name=server_config.get("aws_service_name", None),
aws_role_name=server_config.get("aws_role_name", None),
aws_session_name=server_config.get("aws_session_name", None),
gcp_credentials=server_config.get("gcp_credentials", None),
gcp_project_id=server_config.get("gcp_project_id", None),
instructions=server_config.get("instructions", None),
# Token Exchange (OBO) fields
token_exchange_endpoint=server_config.get("token_exchange_endpoint", None),
@ -1900,6 +1922,7 @@ class MCPServerManager:
# AWS SigV4 credential fields
aws_creds = self._extract_aws_credentials(credentials_dict, credentials_are_encrypted)
gcp_creds = self._extract_gcp_credentials(credentials_dict, credentials_are_encrypted)
scopes: Optional[list[str]] = None
if credentials_dict:
@ -2008,6 +2031,8 @@ class MCPServerManager:
aws_service_name=aws_creds.get("aws_service_name"),
aws_role_name=aws_creds.get("aws_role_name"),
aws_session_name=aws_creds.get("aws_session_name"),
gcp_credentials=gcp_creds.get("gcp_credentials"),
gcp_project_id=gcp_creds.get("gcp_project_id"),
instructions=mcp_server.instructions,
# Token exchange (OBO) fields: dedicated columns, with the credentials blob as a
# back-compat fallback for servers persisted before the columns existed.
@ -3173,6 +3198,8 @@ class MCPServerManager:
elicitation_callback=elicitation_cb,
)
google_auth = _build_google_auth(server)
# Create SigV4 auth if configured
aws_auth = None
if server.auth_type == MCPAuth.aws_sigv4:
@ -3194,6 +3221,7 @@ class MCPServerManager:
timeout=(server.timeout if server.timeout is not None else MCP_CLIENT_TIMEOUT),
extra_headers=extra_headers,
aws_auth=aws_auth,
google_auth=google_auth,
sampling_callback=sampling_cb,
elicitation_callback=elicitation_cb,
)
@ -4041,6 +4069,23 @@ class MCPServerManager:
"aws_session_name": credentials_dict.get("aws_session_name"),
}
def _extract_gcp_credentials(
self,
credentials_dict: dict[str, str] | None,
credentials_are_encrypted: bool,
) -> dict[str, str | None]:
"""Extract and decrypt Google Cloud credential fields from credentials dict."""
if not credentials_dict:
return {}
return {
"gcp_credentials": self._decrypt_credential_field(
credentials_dict.get("gcp_credentials"),
"gcp_credentials",
credentials_are_encrypted,
),
"gcp_project_id": credentials_dict.get("gcp_project_id"),
}
def _extract_scopes(self, scopes_value: Any) -> Optional[list[str]]:
if isinstance(scopes_value, str):
scopes = [s.strip() for s in scopes_value.split() if s.strip()]
@ -5676,11 +5721,11 @@ class MCPServerManager:
if server.requires_per_user_auth:
should_skip_health_check = True
# Skip if auth_type is not none and authentication_token is missing
# (except aws_sigv4 which uses its own credential fields)
# (except cloud-provider auth types which use their own credential fields)
elif (
server.auth_type
and server.auth_type != MCPAuth.none
and server.auth_type != MCPAuth.aws_sigv4
and server.auth_type not in _SELF_CREDENTIALED_AUTH_TYPES
and not server.authentication_token
):
should_skip_health_check = True

View file

@ -103,8 +103,8 @@ def to_server_spec(server: MCPServer) -> Optional[ServerSpec]:
return ServerSpec(server_id=server.server_id, resource=resource, config=PassthroughConfig())
case MCPAuth.oauth2_token_exchange:
return _token_exchange_spec(server, resource)
case MCPAuth.aws_sigv4:
return None # SigV4 is not migrated yet -> defer to v1
case MCPAuth.aws_sigv4 | MCPAuth.gcp_service_account:
return None # per-request cloud-provider signing is not migrated yet -> defer to v1
assert_never(auth_type)

View file

@ -21,7 +21,7 @@ import json
import os
from dataclasses import dataclass
from datetime import datetime, timedelta, timezone
from typing import Any, Dict, Iterable, List, Literal, Optional, Set
from typing import Any, Dict, Iterable, List, Literal, Optional, Set, cast
from fastapi import (
APIRouter,
@ -615,6 +615,20 @@ if MCP_AVAILABLE:
) -> List[LiteLLM_MCPServerTable]:
return [_sanitize_mcp_server_for_virtual_key(server) for server in mcp_servers]
def _cloud_provider_credentials(existing_server: MCPServer) -> MCPCredentials:
candidates: tuple[tuple[str, str | None], ...] = (
("aws_access_key_id", existing_server.aws_access_key_id),
("aws_secret_access_key", existing_server.aws_secret_access_key),
("aws_session_token", existing_server.aws_session_token),
("aws_region_name", existing_server.aws_region_name),
("aws_service_name", existing_server.aws_service_name),
("gcp_credentials", existing_server.gcp_credentials),
("gcp_project_id", existing_server.gcp_project_id),
)
return cast( # cast-ok: comprehension over literal MCPCredentials keys, unmodelable by TypedDict
MCPCredentials, {field: value for field, value in candidates if value}
)
def _inherit_credentials_from_existing_server(
payload: NewMCPServerRequest,
) -> NewMCPServerRequest:
@ -634,17 +648,7 @@ if MCP_AVAILABLE:
inherited_credentials["client_secret"] = existing_server.client_secret
if existing_server.scopes:
inherited_credentials["scopes"] = existing_server.scopes
# AWS SigV4 fields
if existing_server.aws_access_key_id:
inherited_credentials["aws_access_key_id"] = existing_server.aws_access_key_id
if existing_server.aws_secret_access_key:
inherited_credentials["aws_secret_access_key"] = existing_server.aws_secret_access_key
if existing_server.aws_session_token:
inherited_credentials["aws_session_token"] = existing_server.aws_session_token
if existing_server.aws_region_name:
inherited_credentials["aws_region_name"] = existing_server.aws_region_name
if existing_server.aws_service_name:
inherited_credentials["aws_service_name"] = existing_server.aws_service_name
inherited_credentials.update(_cloud_provider_credentials(existing_server))
if not inherited_credentials:
return payload

View file

@ -36,6 +36,7 @@ class MCPAuth(str, enum.Enum):
authorization = "authorization"
oauth2 = "oauth2"
aws_sigv4 = "aws_sigv4"
gcp_service_account = "gcp_service_account"
token = "token"
oauth2_token_exchange = "oauth2_token_exchange"
oauth2_id_jag = "oauth2_id_jag"
@ -61,6 +62,7 @@ MCPAuthType = Optional[
MCPAuth.authorization,
MCPAuth.oauth2,
MCPAuth.aws_sigv4,
MCPAuth.gcp_service_account,
MCPAuth.token,
MCPAuth.oauth2_token_exchange,
MCPAuth.oauth2_id_jag,
@ -132,6 +134,14 @@ class MCPCredentials(TypedDict, total=False):
aws_session_name: Optional[str]
"""Session name for STS AssumeRole (used in CloudTrail). Not a secret — stored unencrypted."""
gcp_credentials: Optional[str]
"""Google service account JSON (inline or file path) used to mint OAuth access tokens for
GCP-managed MCP servers. Optional falls back to Application Default Credentials (e.g. the
GKE workload identity of the pod running the proxy)."""
gcp_project_id: Optional[str]
"""Google Cloud project billed for the request, sent as ``x-goog-user-project``. Not a secret — stored unencrypted."""
audience: Optional[str]
"""
Target audience for OAuth 2.0 Token Exchange (RFC 8693).

View file

@ -83,6 +83,8 @@ class MCPServer(BaseModel):
aws_service_name: Optional[str] = None # defaults to "bedrock-agentcore"
aws_role_name: Optional[str] = None # IAM role ARN for STS AssumeRole
aws_session_name: Optional[str] = None # session name for CloudTrail auditing
gcp_credentials: Optional[str] = None
gcp_project_id: Optional[str] = None
# Token Exchange (OBO) fields
token_exchange_endpoint: Optional[str] = None
audience: Optional[str] = None

View file

@ -0,0 +1,220 @@
"""
Tests for Google Cloud authentication in MCP client.
Covers the MCPGoogleAuth httpx.Auth subclass used for GCP-managed MCP servers
(e.g. https://bigquery.googleapis.com/mcp), plus config loading, client wiring,
and credential encryption for the gcp_service_account auth type.
"""
import json
from unittest.mock import AsyncMock, MagicMock
import httpx
import pytest
from litellm.experimental_mcp_client.client import MCPClient, MCPGoogleAuth
from litellm.types.mcp import MCPAuth, MCPTransport
from litellm.types.mcp_server.mcp_server_manager import MCPServer
SERVICE_ACCOUNT_JSON = json.dumps({"type": "service_account", "project_id": "my-project"})
def _vertex_base_stub(token: str = "ya29.test-token", project_id: str = "my-project") -> MagicMock:
stub = MagicMock()
stub.get_access_token.return_value = (token, project_id)
stub.get_access_token_async = AsyncMock(return_value=(token, project_id))
return stub
def _request() -> httpx.Request:
return httpx.Request("POST", "https://bigquery.googleapis.com/mcp", json={"jsonrpc": "2.0"})
class TestMCPGoogleAuth:
def test_auth_flow_sets_bearer_token(self):
vertex_base = _vertex_base_stub()
auth = MCPGoogleAuth(gcp_credentials=SERVICE_ACCOUNT_JSON, vertex_base=vertex_base)
request = next(auth.auth_flow(_request()))
assert request.headers["Authorization"] == "Bearer ya29.test-token"
assert "x-goog-user-project" not in request.headers
vertex_base.get_access_token.assert_called_once_with(
credentials=SERVICE_ACCOUNT_JSON,
project_id=None,
)
def test_auth_flow_sets_quota_project_header_when_configured(self):
auth = MCPGoogleAuth(gcp_project_id="billing-project", vertex_base=_vertex_base_stub())
request = next(auth.auth_flow(_request()))
assert request.headers["x-goog-user-project"] == "billing-project"
def test_auth_flow_uses_application_default_credentials_when_unset(self):
vertex_base = _vertex_base_stub(token="ya29.workload-identity")
auth = MCPGoogleAuth(vertex_base=vertex_base)
request = next(auth.auth_flow(_request()))
assert request.headers["Authorization"] == "Bearer ya29.workload-identity"
vertex_base.get_access_token.assert_called_once_with(credentials=None, project_id=None)
def test_auth_flow_refreshes_token_per_request(self):
vertex_base = _vertex_base_stub()
vertex_base.get_access_token.side_effect = [("first", "p"), ("second", "p")]
auth = MCPGoogleAuth(vertex_base=vertex_base)
first = next(auth.auth_flow(_request()))
second = next(auth.auth_flow(_request()))
assert first.headers["Authorization"] == "Bearer first"
assert second.headers["Authorization"] == "Bearer second"
@pytest.mark.asyncio
async def test_async_auth_flow_uses_async_token_path(self):
vertex_base = _vertex_base_stub(token="ya29.async-token")
auth = MCPGoogleAuth(gcp_credentials=SERVICE_ACCOUNT_JSON, vertex_base=vertex_base)
request = await auth.async_auth_flow(_request()).__anext__()
assert request.headers["Authorization"] == "Bearer ya29.async-token"
vertex_base.get_access_token.assert_not_called()
vertex_base.get_access_token_async.assert_awaited_once_with(
credentials=SERVICE_ACCOUNT_JSON,
project_id=None,
)
class TestMCPClientGoogleAuth:
def test_factory_wires_google_auth_into_httpx_client(self):
google_auth = MCPGoogleAuth(vertex_base=_vertex_base_stub())
client = MCPClient(
server_url="https://bigquery.googleapis.com/mcp",
transport_type=MCPTransport.http,
auth_type=MCPAuth.gcp_service_account,
google_auth=google_auth,
)
httpx_client = client._create_httpx_client_factory()(
headers={"Content-Type": "application/json"},
timeout=httpx.Timeout(30.0),
)
assert httpx_client._auth is google_auth
def test_google_auth_does_not_add_static_auth_headers(self):
client = MCPClient(
server_url="https://bigquery.googleapis.com/mcp",
transport_type=MCPTransport.http,
auth_type=MCPAuth.gcp_service_account,
google_auth=MCPGoogleAuth(vertex_base=_vertex_base_stub()),
)
assert "Authorization" not in client._get_auth_headers()
class TestMCPServerManagerGoogleAuth:
@pytest.mark.asyncio
async def test_load_config_parses_gcp_fields(self):
from litellm.proxy._experimental.mcp_server.mcp_server_manager import MCPServerManager
manager = MCPServerManager()
await manager.load_servers_from_config(
{
"bigquery": {
"url": "https://bigquery.googleapis.com/mcp",
"transport": "http",
"auth_type": "gcp_service_account",
"gcp_credentials": SERVICE_ACCOUNT_JSON,
"gcp_project_id": "my-project",
}
}
)
server = next(iter(manager.config_mcp_servers.values()))
assert server.auth_type == MCPAuth.gcp_service_account
assert server.gcp_credentials == SERVICE_ACCOUNT_JSON
assert server.gcp_project_id == "my-project"
@pytest.mark.asyncio
async def test_create_mcp_client_builds_google_auth(self):
from litellm.proxy._experimental.mcp_server.mcp_server_manager import MCPServerManager
server = MCPServer(
server_id="test-gcp",
name="bigquery",
server_name="bigquery",
url="https://bigquery.googleapis.com/mcp",
transport=MCPTransport.http,
auth_type=MCPAuth.gcp_service_account,
gcp_credentials=SERVICE_ACCOUNT_JSON,
gcp_project_id="my-project",
)
client = await MCPServerManager()._create_mcp_client(server=server)
assert isinstance(client._google_auth, MCPGoogleAuth)
assert client._google_auth.gcp_credentials == SERVICE_ACCOUNT_JSON
assert client._google_auth.gcp_project_id == "my-project"
@pytest.mark.asyncio
async def test_create_mcp_client_without_gcp_auth(self):
from litellm.proxy._experimental.mcp_server.mcp_server_manager import MCPServerManager
server = MCPServer(
server_id="test-bearer",
name="bearer",
server_name="bearer",
url="https://example.com/mcp",
transport=MCPTransport.http,
auth_type=MCPAuth.bearer_token,
authentication_token="token",
)
client = await MCPServerManager()._create_mcp_client(server=server)
assert client._google_auth is None
def test_registry_dump_redacts_service_account_json(self):
from litellm.proxy._experimental.mcp_server.mcp_server_manager import _redacted_registry_dump
server = MCPServer(
server_id="test-gcp",
name="bigquery",
url="https://bigquery.googleapis.com/mcp",
transport=MCPTransport.http,
auth_type=MCPAuth.gcp_service_account,
gcp_credentials=SERVICE_ACCOUNT_JSON,
)
dump = _redacted_registry_dump({"test-gcp": server})["test-gcp"]
assert dump["gcp_credentials"] == "**REDACTED**"
def test_build_from_table_decrypts_gcp_credentials(self, monkeypatch):
from litellm.proxy._experimental.mcp_server.db import decrypt_credentials, encrypt_credentials
monkeypatch.setenv("LITELLM_SALT_KEY", "sk-test-salt-key")
credentials = encrypt_credentials(
{"gcp_credentials": SERVICE_ACCOUNT_JSON, "gcp_project_id": "my-project"},
encryption_key=None,
)
assert credentials["gcp_credentials"] != SERVICE_ACCOUNT_JSON
assert credentials["gcp_project_id"] == "my-project"
assert decrypt_credentials(credentials)["gcp_credentials"] == SERVICE_ACCOUNT_JSON
def test_extract_gcp_credentials_from_unencrypted_blob(self):
from litellm.proxy._experimental.mcp_server.mcp_server_manager import MCPServerManager
extracted = MCPServerManager()._extract_gcp_credentials(
{"gcp_credentials": SERVICE_ACCOUNT_JSON, "gcp_project_id": "my-project"},
credentials_are_encrypted=False,
)
assert extracted == {
"gcp_credentials": SERVICE_ACCOUNT_JSON,
"gcp_project_id": "my-project",
}

View file

@ -1428,6 +1428,8 @@ class TestTemporaryMCPSessionEndpoints:
existing_server.aws_session_token = None
existing_server.aws_region_name = None
existing_server.aws_service_name = None
existing_server.gcp_credentials = None
existing_server.gcp_project_id = None
mock_manager = MagicMock()
mock_manager.get_mcp_server_by_id.return_value = existing_server
@ -1686,6 +1688,8 @@ class TestTemporaryMCPSessionEndpoints:
aws_session_token=None,
aws_region_name=None,
aws_service_name=None,
gcp_credentials=None,
gcp_project_id=None,
)
built_server = generate_mock_mcp_server_config_record(server_id="temp-server")
mock_manager = MagicMock()

View file

@ -61,6 +61,7 @@ const AUTH_TYPES_REQUIRING_CREDENTIALS = [
AUTH_TYPE.OAUTH2,
AUTH_TYPE.OAUTH2_TOKEN_EXCHANGE,
AUTH_TYPE.AWS_SIGV4,
AUTH_TYPE.GCP_SERVICE_ACCOUNT,
AUTH_TYPE.TRUE_PASSTHROUGH,
AUTH_TYPE.OAUTH_DELEGATE,
];
@ -140,6 +141,7 @@ const CreateMCPServer: React.FC<CreateMCPServerProps> = ({
const isOAuthAuthType = authType === AUTH_TYPE.OAUTH2;
const isTokenExchangeAuthType = authType === AUTH_TYPE.OAUTH2_TOKEN_EXCHANGE;
const isAwsSigV4AuthType = authType === AUTH_TYPE.AWS_SIGV4;
const isGcpServiceAccountAuthType = authType === AUTH_TYPE.GCP_SERVICE_ACCOUNT;
const isM2MFlow = isOAuthAuthType && formValues.oauth_flow_type === OAUTH_FLOW.M2M;
const persistCreateUiState = () => {
@ -1079,6 +1081,7 @@ const CreateMCPServer: React.FC<CreateMCPServerProps> = ({
<Select.Option value="oauth2">OAuth</Select.Option>
<Select.Option value="oauth2_token_exchange">OAuth Token Exchange (OBO)</Select.Option>
<Select.Option value="aws_sigv4">AWS SigV4 (Bedrock AgentCore MCPs)</Select.Option>
<Select.Option value="gcp_service_account">Google Cloud (GCP-managed MCPs)</Select.Option>
<Select.Option value="true_passthrough">True Passthrough (no LiteLLM auth)</Select.Option>
<Select.Option value="oauth_delegate">
OAuth Delegate (client-supplied upstream token)
@ -1307,6 +1310,47 @@ const CreateMCPServer: React.FC<CreateMCPServerProps> = ({
</>
)}
{transportType !== "stdio" && transportType !== "" && isGcpServiceAccountAuthType && (
<>
<p className="text-sm text-gray-500 mb-2">
For GCP-managed MCP servers (e.g. https://bigquery.googleapis.com/mcp). LiteLLM mints a Google access
token for every request.
</p>
<Form.Item
label={
<span className="text-sm font-medium text-gray-700 flex items-center">
Service Account JSON
<Tooltip title="Optional. Service account key JSON or a path to it. If blank, LiteLLM uses Application Default Credentials (e.g. the GKE workload identity of the proxy pod).">
<InfoCircleOutlined className="ml-2 text-blue-400 hover:text-blue-600 cursor-help" />
</Tooltip>
</span>
}
name={["credentials", "gcp_credentials"]}
>
<Input.Password
placeholder='{"type": "service_account", ...} (optional — uses workload identity if blank)'
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">
GCP Project ID
<Tooltip title="Optional. Project billed for the request, sent as x-goog-user-project.">
<InfoCircleOutlined className="ml-2 text-blue-400 hover:text-blue-600 cursor-help" />
</Tooltip>
</span>
}
name={["credentials", "gcp_project_id"]}
>
<Input
placeholder="my-gcp-project (optional)"
className="rounded-lg border-gray-300 focus:border-blue-500 focus:ring-blue-500"
/>
</Form.Item>
</>
)}
{/* Stdio Configuration - only show for stdio transport */}
<StdioConfiguration isVisible={transportType === "stdio"} />
</div>

View file

@ -63,6 +63,7 @@ const AUTH_TYPES_REQUIRING_CREDENTIALS = [
AUTH_TYPE.OAUTH2,
AUTH_TYPE.OAUTH2_TOKEN_EXCHANGE,
AUTH_TYPE.AWS_SIGV4,
AUTH_TYPE.GCP_SERVICE_ACCOUNT,
AUTH_TYPE.TRUE_PASSTHROUGH,
AUTH_TYPE.OAUTH_DELEGATE,
];
@ -102,6 +103,7 @@ const MCPServerEdit: React.FC<MCPServerEditProps> = ({
const isOAuthAuthType = authType === AUTH_TYPE.OAUTH2;
const isTokenExchangeAuthType = authType === AUTH_TYPE.OAUTH2_TOKEN_EXCHANGE;
const isAwsSigV4AuthType = authType === AUTH_TYPE.AWS_SIGV4;
const isGcpServiceAccountAuthType = authType === AUTH_TYPE.GCP_SERVICE_ACCOUNT;
const oauthFlowTypeValue = Form.useWatch("oauth_flow_type", form) as string | undefined;
const isM2MFlow = isOAuthAuthType && oauthFlowTypeValue === OAUTH_FLOW.M2M;
// Watch reflects a live toggle when the delegate switch is mounted; fall back to
@ -1115,6 +1117,7 @@ const MCPServerEdit: React.FC<MCPServerEditProps> = ({
<Select.Option value="oauth2">OAuth</Select.Option>
<Select.Option value="oauth2_token_exchange">OAuth Token Exchange (OBO)</Select.Option>
<Select.Option value="aws_sigv4">AWS SigV4 (Bedrock AgentCore MCPs)</Select.Option>
<Select.Option value="gcp_service_account">Google Cloud (GCP-managed MCPs)</Select.Option>
<Select.Option value="true_passthrough">True Passthrough (no LiteLLM auth)</Select.Option>
<Select.Option value="oauth_delegate">
OAuth Delegate (client-supplied upstream token)
@ -1575,6 +1578,47 @@ const MCPServerEdit: React.FC<MCPServerEditProps> = ({
</>
)}
{!isStdioTransport && isGcpServiceAccountAuthType && (
<>
<p className="text-sm text-gray-500 mb-2">
For GCP-managed MCP servers (e.g. https://bigquery.googleapis.com/mcp). LiteLLM mints a Google access
token for every request.
</p>
<Form.Item
label={
<span className="text-sm font-medium text-gray-700 flex items-center">
Service Account JSON
<Tooltip title="Optional. Service account key JSON or a path to it. If blank, LiteLLM uses Application Default Credentials (e.g. the GKE workload identity of the proxy pod).">
<InfoCircleOutlined className="ml-2 text-blue-400 hover:text-blue-600 cursor-help" />
</Tooltip>
</span>
}
name={["credentials", "gcp_credentials"]}
>
<Input.Password
placeholder="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">
GCP Project ID
<Tooltip title="Optional. Project billed for the request, sent as x-goog-user-project.">
<InfoCircleOutlined className="ml-2 text-blue-400 hover:text-blue-600 cursor-help" />
</Tooltip>
</span>
}
name={["credentials", "gcp_project_id"]}
>
<Input
placeholder="my-gcp-project (optional)"
className="rounded-lg border-gray-300 focus:border-blue-500 focus:ring-blue-500"
/>
</Form.Item>
</>
)}
{/* Environment Variables Section */}
<div className="mt-6">
<EnvVarsSection />

View file

@ -41,6 +41,7 @@ export const AUTH_TYPE = {
OAUTH2: "oauth2",
OAUTH2_TOKEN_EXCHANGE: "oauth2_token_exchange",
AWS_SIGV4: "aws_sigv4",
GCP_SERVICE_ACCOUNT: "gcp_service_account",
TRUE_PASSTHROUGH: "true_passthrough",
OAUTH_DELEGATE: "oauth_delegate",
};

View file

@ -27423,7 +27423,7 @@ export interface components {
/** Alias */
alias?: string | null;
/** Auth Type */
auth_type?: ("none" | "api_key" | "bearer_token" | "basic" | "authorization" | "oauth2" | "aws_sigv4" | "token" | "oauth2_token_exchange" | "oauth2_id_jag" | "true_passthrough" | "oauth_delegate") | null;
auth_type?: ("none" | "api_key" | "bearer_token" | "basic" | "authorization" | "oauth2" | "aws_sigv4" | "gcp_service_account" | "token" | "oauth2_token_exchange" | "oauth2_id_jag" | "true_passthrough" | "oauth_delegate") | null;
/** Mcp Info */
mcp_info?: {
[key: string]: unknown;