-
Notifications
You must be signed in to change notification settings - Fork 2.9k
Description
Describe the Bug:
Authenticating MCP tool deployed in Cloud Run from agent in GCP (Cloud Run/GCF/Agent Engine/etc.).
Steps to Reproduce:
- Sample code with remote MCP server deployed in Cloud Run for example with "Require authentication" (i.e. NOT public):
auth_scheme=OAuth2(
flows=OAuthFlows(
clientCredentials=OAuthFlowClientCredentials(
tokenUrl="https://oauth2.googleapis.com/token",
scopes=SCOPES,
)
)
)
auth_credential = AuthCredential(
auth_type=AuthCredentialTypes.SERVICE_ACCOUNT, service_account=ServiceAccount(use_default_credential = True, scopes=SCOPES.keys())
)
mcp_toolset = McpToolset(
connection_params=StreamableHTTPConnectionParams(url=MCP_URL),
auth_scheme=auth_scheme,
auth_credential=auth_credential,
)
root_agent = LlmAgent(
name='My_Agent',
model='gemini-2.5-flash',
description=(
'Agent to discuss with.'
),
sub_agents=[],
instruction='Use available tools to greet your interlocutor.',
tools=[
mcp_toolset
]
)
- Run
adk web - Select agent and send message
- Guts spilling too graphic to convey here but in short:
...
2026-02-11 23:40:52,949 - INFO - _client.py:1740 - HTTP Request: POST https://XYZ.us-central1.run.app/mcp "HTTP/1.1 401 Unauthorized"
2026-02-11 23:40:52,950 - WARNING - session_context.py:190 - Error on session runner task: Attempted to exit cancel scope in a different task than it was entered in
2026-02-11 23:40:52,950 - INFO - mcp_session_manager.py:174 - Retrying get_tools due to error: Failed to create MCP session: Failed to create MCP session: Attempted to exit cancel scope in a different task than it was entered in
...
raise ConnectionError(f'Failed to create MCP session: {e}') from e
ConnectionError: Failed to create MCP session: Failed to create MCP session: Attempted to exit cancel scope in a different task than it was entered in
Expected Behavior:
Specify ID token as "token kind" + audience so that:
- No guts spilling
- and MCP tool called successfully, using
id_token
Observed Behavior:
Failed to call MCP tool because using Access Token instead of ID Token.
Environment Details:
My requirements.txt:
google-cloud-aiplatform[agent_engines,adk,langchain,ag2,llama_index]>=1.112.0
- google-adk==1.24.1
- Windows WSL2/Ubuntu 22.04.5 LTS
- Python 3.11.0rc1
Model Information:
I don't believe it matters here:
- gemini-2.5-flash
Work around
Hack to inject new definition of exchange_credential() in ServiceAccountCredentialExchanger and force the use of ID Token:
from typing import Optional
import google.auth
from google.auth.transport.requests import Request
import google.adk.tools.openapi_tool.auth.credential_exchangers.service_account_exchanger as mod
from google.adk.auth.auth_credential import AuthCredential
from google.adk.auth.auth_credential import AuthCredentialTypes
from google.adk.auth.auth_credential import HttpAuth
from google.adk.auth.auth_credential import HttpCredentials
from google.adk.auth.auth_schemes import AuthScheme
from google.adk.tools.openapi_tool.auth.credential_exchangers.base_credential_exchanger import AuthCredentialMissingError
def newExchange_credential(
self,
auth_scheme: AuthScheme,
auth_credential: Optional[AuthCredential] = None,
) -> AuthCredential:
"""Exchanges the service account auth credential for an access token.
If auth_credential contains a service account credential, it will be used
to fetch an access token. Otherwise, the default service credential will be
used for fetching an access token.
Args:
auth_scheme: The auth scheme.
auth_credential: The auth credential.
Returns:
An AuthCredential in HTTPBearer format, containing the access token.
"""
if (
auth_credential is None
or auth_credential.service_account is None
or (
auth_credential.service_account.service_account_credential is None
and not auth_credential.service_account.use_default_credential
)
):
raise AuthCredentialMissingError(
"Service account credentials are missing. Please provide them, or set"
" `use_default_credential = True` to use application default"
" credential in a hosted service like Cloud Run."
)
try:
if auth_credential.service_account.use_default_credential:
credentials, project_id = google.auth.default(
scopes=["https://www.googleapis.com/auth/cloud-platform"],
)
quota_project_id = (
getattr(credentials, "quota_project_id", None) or project_id
)
else:
config = auth_credential.service_account
credentials = service_account.Credentials.from_service_account_info(
config.service_account_credential.model_dump(), scopes=config.scopes
)
quota_project_id = None
credentials.refresh(Request())
updated_credential = AuthCredential(
auth_type=AuthCredentialTypes.HTTP, # Store as a bearer token
http=HttpAuth(
scheme="bearer",
credentials=HttpCredentials(token=credentials.id_token), # <--- all this for that!
additional_headers={
"x-goog-user-project": quota_project_id,
}
if quota_project_id
else None,
),
)
return updated_credential
except Exception as e:
raise AuthCredentialMissingError(
f"Failed to exchange service account token: {e}"
) from e
mod.ServiceAccountCredentialExchanger.exchange_credential = newExchange_credential
My 2 cents
The problem I believe is in the choice//approach taken for the use of token used in AuthCredential().
When opting for default credentials (ADC isn't it?), google.auth.default(...) is called and will return more than just token (the id_token in particular):
try:
if auth_credential.service_account.use_default_credential:
credentials, project_id = google.auth.default(
scopes=["https://www.googleapis.com/auth/cloud-platform"],
)
But then when creating (and returning) the credentials, the credentials.token is used:
updated_credential = AuthCredential(
auth_type=AuthCredentialTypes.HTTP, # Store as a bearer token
http=HttpAuth(
scheme="bearer",
credentials=HttpCredentials(token=credentials.token),
additional_headers={
"x-goog-user-project": quota_project_id,
}
if quota_project_id
else None,
),
)
return updated_credential
I could be wrong, but this setup doesn't work then when authenticating service-to-service.
See my workaround/hack above, as I could not find a better way to provide a different implementation for ServiceAccountCredentialExchanger since CredentialManager is instantiated by BaseLlmFlow and, even if it wasn't, ServiceAccountCredentialExchanger is also instantiated in CredentialManager.
NB: What he said --> # TODO: Move ServiceAccountCredentialExchanger to the auth module