Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion contributing/samples/gepa/experiment.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,6 @@
from tau_bench.types import EnvRunResult
from tau_bench.types import RunConfig
import tau_bench_agent as tau_bench_agent_lib

import utils


Expand Down
1 change: 0 additions & 1 deletion contributing/samples/gepa/run_experiment.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@
from absl import flags
import experiment
from google.genai import types

import utils

_OUTPUT_DIR = flags.DEFINE_string(
Expand Down
80 changes: 80 additions & 0 deletions contributing/samples/mcp_stdio_user_auth_passing_sample/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
# Sample: Passing User Token from Agent State to MCP via ContextToEnvMapperCallback

This sample demonstrates how to use the `context_to_env_mapper_callback` feature in ADK to pass a user token from the agent's session state to an MCP process (using stdio transport). This is useful when your MCP server (built by your organization) requires the same user token for internal API calls.

## How it works
- The agent is initialized with a `MCPToolset` using `StdioServerParameters`.
- The `context_to_env_mapper_callback` is set to a function that extracts the `user_token` from the agent's state and maps it to the `USER_TOKEN` environment variable.
- When the agent calls the MCP, the token is injected into the MCP process environment, allowing the MCP to use it for internal authentication.

## Directory Structure
```
contributing/samples/stdio_mcp_user_auth_passing_sample/
├── agent.py # Basic agent setup
├── main.py # Complete runnable example
└── README.md
```

## How to Run

### Option 1: Run the complete example
```bash
cd /home/sanjay-dev/Workspace/adk-python
python -m contributing.samples.stdio_mcp_user_auth_passing_sample.main
Comment on lines +22 to +23
Copy link
Contributor

Choose a reason for hiding this comment

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

medium

The command to run the sample includes a hardcoded, user-specific path (/home/sanjay-dev/Workspace/adk-python). This will not work for other users and will cause confusion. The instructions should be generic.

Suggested change
cd /home/sanjay-dev/Workspace/adk-python
python -m contributing.samples.stdio_mcp_user_auth_passing_sample.main
cd <path_to_adk-python_repo_root>
python -m contributing.samples.stdio_mcp_user_auth_passing_sample.main

```

### Option 2: Use the agent in your own code
```python
from contributing.samples.stdio_mcp_user_auth_passing_sample.agent import create_agent
from google.adk.sessions import Session

agent = create_agent()
session = Session(
id="your_session_id",
app_name="your_app_name",
user_id="your_user_id"
)

# Set user token in session state
session.state['user_token'] = 'YOUR_ACTUAL_TOKEN_HERE'
session.state['api_endpoint'] = 'https://your-internal-api.com'

# Then use the agent in your workflow...
```

## Flow Diagram

```mermaid
graph TD
subgraph "User Application"
U[User]
end
subgraph "Agent Process"
A[Agent Instance<br/>per user-app-agentid]
S[Session State<br/>user_token, api_endpoint]
C[ContextToEnvMapperCallback]
end
subgraph "MCP Process"
M[MCP Server<br/>stdio transport]
E[Environment Variables<br/>USER_TOKEN, API_ENDPOINT]
API[Internal API Calls]
end
U -->|Sends request| A
A -->|Reads state| S
S -->|Extracts tokens| C
C -->|Maps to env vars| E
A -->|Spawns with env| M
M -->|Uses env vars| API
API -->|Response| M
M -->|Tool result| A
A -->|Response| U
```

## Context
- Each agent instance is initiated per user-app-agentid.
- The agent receives a user context (with token) and calls the MCP using stdio transport.
- The MCP, built by the same organization, uses the token for internal API calls.
- The ADK's context-to-env mapping feature makes this seamless.
Empty file.
60 changes: 60 additions & 0 deletions contributing/samples/mcp_stdio_user_auth_passing_sample/agent.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
"""
Sample: Using ContextToEnvMapperCallback to pass user token from agent state to MCP via stdio transport.
"""

import os
import tempfile
from typing import Any
from typing import Dict

from google.adk.agents.llm_agent import LlmAgent
from google.adk.tools.mcp_tool.mcp_session_manager import StdioConnectionParams
from google.adk.tools.mcp_tool.mcp_toolset import MCPToolset
from mcp import StdioServerParameters

_allowed_path = os.path.dirname(os.path.abspath(__file__))


def user_token_env_mapper(state: Dict[str, Any]) -> Dict[str, str]:
"""Extracts USER_TOKEN from agent state and maps to MCP env."""
env = {}
if "user_token" in state:
env["USER_TOKEN"] = state["user_token"]
if "api_endpoint" in state:
env["API_ENDPOINT"] = state["api_endpoint"]

print(f"Environment variables being passed to MCP: {env}")
return env


def create_agent() -> LlmAgent:
"""Create the agent with context to env mapper callback."""
# Create a temporary directory for the filesystem server
temp_dir = tempfile.mkdtemp()

return LlmAgent(
model="gemini-2.0-flash",
name="user_token_agent",
instruction=f"""
You are an agent that calls an internal MCP server which requires a user token for internal API calls.
The user token is available in your session state and must be passed to the MCP process as an environment variable.
Test directory: {temp_dir}
""",
Comment on lines +38 to +42
Copy link
Contributor

Choose a reason for hiding this comment

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

medium

The agent's instruction string includes Test directory: {temp_dir}, but the StdioServerParameters for the filesystem server is configured with _allowed_path (on line 51). The temp_dir variable is created but not actually used by the MCP tool. This is misleading for a sample and could confuse users about how to configure the tool's working directory.

To make the sample clearer, I recommend using temp_dir as the path for StdioServerParameters to align the instruction with the implementation.

tools=[
MCPToolset(
connection_params=StdioConnectionParams(
server_params=StdioServerParameters(
command="npx",
args=[
"-y", # Arguments for the command
"@modelcontextprotocol/server-filesystem",
_allowed_path,
],
),
timeout=5,
),
get_env_from_context_fn=user_token_env_mapper,
tool_filter=["read_file", "list_directory"],
)
],
)
95 changes: 95 additions & 0 deletions contributing/samples/mcp_stdio_user_auth_passing_sample/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
"""
Sample: Using ContextToEnvMapperCallback to pass user token from agent state to MCP via stdio transport.
"""

import asyncio

from google.adk.agents.invocation_context import InvocationContext
from google.adk.agents.readonly_context import ReadonlyContext
from google.adk.sessions import InMemorySessionService
from google.adk.sessions import Session

from .agent import create_agent


async def main():
"""Example of how to set up and run the agent with user token."""
print("=== STDIO MCP User Auth Passing Sample ===")
print()

# Create the agent
agent = create_agent()
print(f"✓ Created agent: {agent.name}")

# Create session service and session
session_service = InMemorySessionService()
session = Session(
id="sample_session",
app_name="stdio_mcp_user_auth_passing_sample",
user_id="sample_user",
)
print(f"✓ Created session: {session.id}")

# Set user token in session state
session.state["user_token"] = "sample_user_token_123"
session.state["api_endpoint"] = "https://internal-api.company.com"
print(f"✓ Set session state with user_token: {session.state['user_token']}")

# Create invocation context
invocation_context = InvocationContext(
invocation_id="sample_invocation",
agent=agent,
session=session,
session_service=session_service,
)

# Create readonly context
readonly_context = ReadonlyContext(invocation_context)
print(f"✓ Created readonly context")

print()
print("=== Demonstrating User Auth Token Passing to MCP ===")
print(
"Note: This sample shows how the callback extracts environment variables."
)
print("In a real scenario, these would be passed to an actual MCP server.")
print()

# Access the MCP toolset to demonstrate the callback
mcp_toolset = agent.tools[0]
mcp_session_manager = mcp_toolset._mcp_session_manager

# Extract environment variables using the callback (without connecting to MCP)
if mcp_session_manager._context_to_env_mapper_callback:
print("✓ Context-to-env mapper callback is configured")

# Simulate what happens during MCP session creation
env_vars = mcp_session_manager._extract_env_from_context(readonly_context)
Comment on lines +60 to +67
Copy link
Contributor

Choose a reason for hiding this comment

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

critical

The sample code attempts to access _mcp_session_manager._context_to_env_mapper_callback and call _mcp_session_manager._extract_env_from_context. These attributes and methods have been removed from MCPSessionManager and the logic moved to MCPToolset in this PR. As a result, the sample is broken and will not demonstrate the intended feature.

The sample should be updated to use the public APIs and reflect the new implementation. A better sample would perform an actual tool call to show the end-to-end flow, rather than relying on private members which can change.


print(f"✓ Extracted environment variables:")
for key, value in env_vars.items():
print(f" {key}={value}")
print()

print(
"✓ These environment variables would be injected into the MCP process"
)
print("✓ The MCP server can then use them for internal API calls")
else:
print("✗ No context-to-env mapper callback configured")

print()
print("=== Sample completed successfully! ===")
print()
print("Key points demonstrated:")
print("1. Session state holds user tokens and configuration")
print(
"2. Context-to-env mapper callback extracts these as environment"
" variables"
)
print("3. Environment variables would be passed to MCP server processes")
print("4. MCP servers can use these for authenticated API calls")


if __name__ == "__main__":
asyncio.run(main())
34 changes: 32 additions & 2 deletions src/google/adk/tools/mcp_tool/mcp_session_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
import logging
import sys
from typing import Any
from typing import Callable
from typing import Dict
from typing import Optional
from typing import TextIO
Expand Down Expand Up @@ -167,13 +168,42 @@ def __init__(
else:
self._connection_params = connection_params
self._errlog = errlog

# Session pool: maps session keys to (session, exit_stack) tuples
self._sessions: Dict[str, tuple[ClientSession, AsyncExitStack]] = {}

# Lock to prevent race conditions in session creation
self._session_lock = asyncio.Lock()

def update_connection_params(
self,
new_connection_params: Union[
StdioServerParameters,
StdioConnectionParams,
SseConnectionParams,
StreamableHTTPConnectionParams,
],
) -> None:
"""Updates the connection parameters and invalidates existing sessions.

Args:
new_connection_params: New connection parameters to use.
"""
if isinstance(new_connection_params, StdioServerParameters):
logger.warning(
'StdioServerParameters is not recommended. Please use'
' StdioConnectionParams.'
)
self._connection_params = StdioConnectionParams(
server_params=new_connection_params,
timeout=5,
)
else:
self._connection_params = new_connection_params

# Clear existing sessions since connection params changed
# Sessions will be recreated on next request
# Note: We don't close sessions here to avoid blocking,
# they will be cleaned up when detected as disconnected
Comment on lines +202 to +205
Copy link
Contributor

Choose a reason for hiding this comment

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

high

The docstring for update_connection_params states that it "invalidates existing sessions". However, the implementation only updates self._connection_params. The session cache self._sessions is not actually cleared, which means existing sessions with outdated connection parameters might be reused. This is a bug.

To fix this, you should clear the session cache.

Suggested change
# Clear existing sessions since connection params changed
# Sessions will be recreated on next request
# Note: We don't close sessions here to avoid blocking,
# they will be cleaned up when detected as disconnected
# Clear existing sessions since connection params changed
self._sessions.clear()
# Sessions will be recreated on next request
# Note: We don't close sessions here to avoid blocking,
# they will be cleaned up when detected as disconnected


def _generate_session_key(
self, merged_headers: Optional[Dict[str, str]] = None
) -> str:
Expand Down
Loading
Loading