Skip to content

Commit 7e5f05e

Browse files
authored
Merge branch 'main' into security/ssrf-embedded-ipv4-block
2 parents cd4b5ba + 1ac6875 commit 7e5f05e

88 files changed

Lines changed: 1977 additions & 349 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

contributing/samples/adk_team/adk_documentation/adk_release_analyzer/agent.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -559,6 +559,7 @@ def summary_instruction(readonly_context: ReadonlyContext) -> str:
559559
name="summary_agent",
560560
description="Compiles recommendations and creates the GitHub issue.",
561561
instruction=summary_instruction,
562+
include_contents="none",
562563
tools=[
563564
get_all_recommendations,
564565
create_issue,

contributing/samples/managed_agent/basic/README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
# Managed Agent
22

3+
> For setup, authentication, backends, and background on `ManagedAgent`, see the
4+
> [ManagedAgent guide](../../../../docs/guides/agents/managed_agent/index.md).
5+
36
## Overview
47

58
This sample runs a `ManagedAgent` configured with the built-in `google_search`

contributing/samples/managed_agent/code_execution/README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
# Managed Agent - Code Execution
22

3+
> For setup, authentication, backends, and background on `ManagedAgent`, see the
4+
> [ManagedAgent guide](../../../../docs/guides/agents/managed_agent/index.md).
5+
36
## Overview
47

58
This sample runs a `ManagedAgent` configured with the built-in **code execution**

docs/guides/README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@ This directory contains specific developer guides for the ADK Python implementat
1313
* [Event and NodeInfo](events/event/index.md) - Understanding Event and NodeInfo in workflows.
1414
* [RequestInput](events/request_input/index.md) - How to use RequestInput for human-in-the-loop interactions.
1515

16+
### Tools
17+
* [to_mcp_server](tools/mcp_tool/agent_to_mcp/index.md) - Expose an ADK agent as an MCP server so any MCP host can drive it as a single tool (the MCP counterpart of to_a2a).
18+
1619
### Workflows
1720
* [Workflow](workflow/workflow/index.md) - Graph-based orchestration of complex, multi-step agent interactions.
1821
* [Workflow Graphs](workflow/graph/index.md) - Understanding nodes, edges, and graph structures in workflows.
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
# to_mcp_server
2+
3+
Exposes an ADK agent as an MCP server so any MCP host (Claude Code, OpenAI
4+
Codex, an IDE, or any MCP client) can drive it as a single tool. It is the MCP
5+
counterpart of `to_a2a`.
6+
7+
## Introduction
8+
9+
`to_mcp_server` turns a whole ADK agent into a standard
10+
[Model Context Protocol](https://modelcontextprotocol.io/) server. The agent —
11+
its model loop and all of its tools — is registered as a *single* MCP tool named
12+
after the agent. A host that speaks MCP sends a request string and receives the
13+
agent's final response; it never imports ADK and does not see the agent's
14+
individual tools.
15+
16+
This solves the problem of making an ADK agent consumable by harnesses that are
17+
not ADK. Where `to_a2a` publishes an agent over A2A, `to_mcp_server` publishes it
18+
over MCP, so coding agents and IDEs that already speak MCP can delegate a task to
19+
an ADK agent as if it were any other tool. It builds on `Runner` to execute the
20+
agent and returns a `FastMCP` server, leaving the choice of transport (stdio for
21+
local hosts, streamable-http for networked ones) to the caller.
22+
23+
## Get started
24+
25+
Define an agent and expose it. Running the file starts the MCP server on stdio;
26+
an MCP host can also launch it as a subprocess.
27+
28+
```python
29+
import random
30+
31+
from google.adk.agents import LlmAgent
32+
from google.adk.tools.mcp_tool import to_mcp_server
33+
34+
35+
def roll_die(sides: int) -> int:
36+
"""Roll a die with the given number of sides and return the result."""
37+
return random.randint(1, sides)
38+
39+
40+
dice_agent = LlmAgent(
41+
name="dice_agent",
42+
description="Rolls dice with any number of sides and reports the outcome.",
43+
instruction="Use the roll_die tool to roll the dice the user asks for.",
44+
tools=[roll_die],
45+
)
46+
47+
# The whole agent becomes one MCP tool named "dice_agent".
48+
server = to_mcp_server(dice_agent)
49+
50+
if __name__ == "__main__":
51+
server.run(transport="stdio")
52+
```
53+
54+
A host configured to launch this file sees one tool, `dice_agent`, and calls it
55+
with a `request` string; the ADK agent runs its own model and `roll_die` loop and
56+
returns the answer.
57+
58+
## How it works
59+
60+
`to_mcp_server` creates a `FastMCP` server and registers one tool whose handler
61+
runs the agent through a `Runner`. If no `runner` is supplied, one is built with
62+
in-memory session, artifact, memory, and credential services.
63+
64+
On each tool call the handler:
65+
66+
1. Resolves an ADK session (see below), then wraps the incoming `request` string
67+
as a user `Content`.
68+
2. Drives `Runner.run_async` and iterates the event stream.
69+
3. Forwards intermediate (non-final) text events to the host as MCP **progress
70+
notifications**, so the host can show the agent working in real time.
71+
4. Maps the parts of the final response to MCP content blocks and returns them:
72+
text becomes `TextContent`, inline image data becomes `ImageContent`, audio
73+
becomes `AudioContent`, and any other inline data becomes an
74+
`EmbeddedResource`. This is why a multimodal agent's output is preserved
75+
rather than flattened to text.
76+
77+
### Session continuity
78+
79+
`to_mcp_server` keeps one ADK session per MCP connection, so successive tool
80+
calls on the same connection form a single multi-turn conversation. The mapping
81+
from connection to session is held in a `weakref.WeakKeyDictionary`, so a
82+
session's entry is dropped when its connection is garbage-collected. Over stdio
83+
there is one connection per process, so all calls share one conversation; over
84+
streamable-http each client connection gets its own session.
85+
86+
`to_mcp_server` depends on `Runner`, the agent (`BaseAgent`/`LlmAgent`),
87+
`google.genai.types`, and `mcp.server.fastmcp.FastMCP`; it returns a `FastMCP`
88+
that the caller runs on a transport of their choice.
89+
90+
## Configuration options
91+
92+
| Option | Type | Default | Description |
93+
| --- | --- | --- | --- |
94+
| `agent` | `BaseAgent` | *required* | The agent to serve. Its model loop and all of its tools are exposed together as one MCP tool. |
95+
| `name` | `str \| None` | `None` | The MCP server and tool name. Defaults to the agent's name (or `"adk_agent"`). Set it when you want the tool to appear under a name other than the agent's. |
96+
| `instructions` | `str \| None` | `None` | Optional server instructions an MCP host may surface to its model to describe how to use the tool. |
97+
| `runner` | `Runner \| None` | `None` | A pre-built `Runner`. If omitted, one is created with in-memory services. Supply your own to use persistent or custom session, artifact, memory, or credential services — this is the recommended path for a long-lived networked server. |
98+
99+
## Advanced applications
100+
101+
### Serving over the network
102+
103+
* **Problem solved**: a host on another machine needs to reach the agent.
104+
* **Implementation**: run the same server with the networked transport:
105+
`server.run(transport="streamable-http")`. Nothing about the agent changes;
106+
only the transport differs.
107+
108+
### Bringing your own services
109+
110+
* **Problem solved**: the default in-memory services do not persist across
111+
process restarts and are not suited to multi-client production serving.
112+
* **Implementation**: build a `Runner` with your chosen services and pass it in:
113+
`to_mcp_server(agent, runner=my_runner)`. The tool then uses those services
114+
for every call.
115+
116+
### Multimodal responses
117+
118+
* **Problem solved**: the agent produces images or audio, not just text.
119+
* **Implementation**: no extra work — non-text parts of the final response are
120+
returned as `ImageContent`, `AudioContent`, or `EmbeddedResource`, so the
121+
host receives them alongside any text.
122+
123+
## Limitations
124+
125+
* **Text input only**: the tool accepts a single `request` string. Passing
126+
media *into* the agent is not supported through the tool call, because MCP
127+
tool arguments are JSON that the host's model fills in and hosts do not place
128+
media in tool arguments. For media input, use MCP resources or elicitation
129+
instead.
130+
* **Default services are in-memory**: for a long-lived streamable-http server,
131+
sessions accumulate with no eviction; inject a `runner` with a persistent or
132+
cleaning session service. Tool calls on a single connection are expected to
133+
be sequential, since they share one session.
134+
* **Experimental**: `to_mcp_server` is `@experimental` and lives behind the
135+
`mcp` extra; its behavior may change in future releases.
136+
137+
## Related samples
138+
139+
* [MCP: serve an ADK agent](../../../../../contributing/samples/mcp/mcp_serve_agent)

src/google/adk/agents/llm_agent.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -147,7 +147,7 @@ async def _convert_tool_union_to_tools(
147147

148148
# Wrap google_search tool with AgentTool if there are multiple tools because
149149
# the built-in tools cannot be used together with other tools.
150-
# TODO(b/448114567): Remove once the workaround is no longer needed.
150+
# TODO: Remove once the workaround is no longer needed.
151151
if multiple_tools and isinstance(tool_union, GoogleSearchTool):
152152
from ..tools.google_search_agent_tool import create_google_search_agent
153153
from ..tools.google_search_agent_tool import GoogleSearchAgentTool
@@ -159,7 +159,7 @@ async def _convert_tool_union_to_tools(
159159
# Replace VertexAiSearchTool with DiscoveryEngineSearchTool if there are
160160
# multiple tools because the built-in tools cannot be used together with
161161
# other tools.
162-
# TODO(b/448114567): Remove once the workaround is no longer needed.
162+
# TODO: Remove once the workaround is no longer needed.
163163
if multiple_tools and isinstance(tool_union, VertexAiSearchTool):
164164
from ..tools.discovery_engine_search_tool import DiscoveryEngineSearchTool
165165

@@ -742,7 +742,7 @@ async def canonical_tools(
742742
"""
743743
# We may need to wrap some built-in tools if there are other tools
744744
# because the built-in tools cannot be used together with other tools.
745-
# TODO(b/448114567): Remove once the workaround is no longer needed.
745+
# TODO: Remove once the workaround is no longer needed.
746746
multiple_tools = len(self.tools) > 1
747747
model = self.canonical_model
748748

src/google/adk/agents/run_config.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -242,6 +242,9 @@ class RunConfig(BaseModel):
242242
realtime_input_config: Optional[types.RealtimeInputConfig] = None
243243
"""Realtime input config for live agents with audio input from user."""
244244

245+
explicit_vad_signal: Optional[bool] = None
246+
"""Whether to enable explicit voice activity detection (VAD) signals from the model."""
247+
245248
translation_config: Optional[types.TranslationConfig] = None
246249
"""Configures real-time speech-to-speech translation.
247250
@@ -379,6 +382,14 @@ class RunConfig(BaseModel):
379382
callers provide per-turn context without changing the conversation history.
380383
"""
381384

385+
include_thoughts_from_other_agents: bool = False
386+
"""Whether to include other agents' thought parts in LLM context.
387+
388+
By default, thoughts from other agents are excluded when their messages are
389+
reformatted as user context for the current agent. Enable this only when
390+
agents are expected to share internal reasoning with one another.
391+
"""
392+
382393
@model_validator(mode='before')
383394
@classmethod
384395
def check_for_deprecated_save_live_audio(cls, data: Any) -> Any:

src/google/adk/apps/llm_event_summarizer.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,13 @@ class LlmEventSummarizer(BaseEventsSummarizer):
5050
' It may or may not start from a compacted history. Please identify and'
5151
' reiterate the user request, summarize the context so far, focusing on'
5252
' key decisions made and information obtained, as well as any unresolved'
53-
' questions or tasks. The summary should be concise and capture the'
53+
' questions or tasks. '
54+
'CRITICAL INSTRUCTIONS: '
55+
'1. Explicitly identify and state the primary language used by the user '
56+
'at the top of your summary (e.g., "Conversation Language: English"). '
57+
'2. If the agent called any tools, accurately list the exact tool names '
58+
'used to maintain tool grounding. '
59+
'The rest of the summary should be concise and capture the'
5460
' essence of the interaction.\n\n{conversation_history}'
5561
)
5662

src/google/adk/cli/agent_graph.py

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -41,9 +41,9 @@
4141
async def build_graph(
4242
graph: graphviz.Digraph,
4343
agent: BaseAgent,
44-
highlight_pairs,
45-
parent_agent=None,
46-
):
44+
highlight_pairs: list[tuple[str, str]] | None,
45+
parent_agent: BaseAgent | None = None,
46+
) -> None:
4747
"""
4848
Build a graph of the agent and its sub-agents.
4949
Args:
@@ -63,7 +63,7 @@ async def build_graph(
6363
light_gray = '#cccccc'
6464
white = '#ffffff'
6565

66-
def get_node_name(tool_or_agent: Union[BaseAgent, BaseTool]):
66+
def get_node_name(tool_or_agent: Union[BaseAgent, BaseTool]) -> str:
6767
if isinstance(tool_or_agent, BaseAgent):
6868
# Added Workflow Agent checks for different agent types
6969
if isinstance(tool_or_agent, SequentialAgent):
@@ -81,7 +81,7 @@ def get_node_name(tool_or_agent: Union[BaseAgent, BaseTool]):
8181
else:
8282
raise ValueError(f'Unsupported tool type: {tool_or_agent}')
8383

84-
def get_node_caption(tool_or_agent: Union[BaseAgent, BaseTool]):
84+
def get_node_caption(tool_or_agent: Union[BaseAgent, BaseTool]) -> str:
8585

8686
if isinstance(tool_or_agent, BaseAgent):
8787
return '🤖 ' + tool_or_agent.name
@@ -105,7 +105,7 @@ def get_node_caption(tool_or_agent: Union[BaseAgent, BaseTool]):
105105
)
106106
return f'❓ Unsupported tool type: {type(tool_or_agent)}'
107107

108-
def get_node_shape(tool_or_agent: Union[BaseAgent, BaseTool]):
108+
def get_node_shape(tool_or_agent: Union[BaseAgent, BaseTool]) -> str:
109109
if isinstance(tool_or_agent, BaseAgent):
110110
return 'ellipse'
111111
elif retrieval_tool_module_loaded and isinstance(
@@ -126,7 +126,9 @@ def get_node_shape(tool_or_agent: Union[BaseAgent, BaseTool]):
126126
)
127127
return 'cylinder'
128128

129-
def should_build_agent_cluster(tool_or_agent):
129+
def should_build_agent_cluster(
130+
tool_or_agent: Union[BaseAgent, BaseTool],
131+
) -> bool:
130132
if isinstance(tool_or_agent, Workflow):
131133
return True
132134
elif isinstance(tool_or_agent, BaseAgent):
@@ -149,7 +151,9 @@ def should_build_agent_cluster(tool_or_agent):
149151
else:
150152
return False
151153

152-
async def build_cluster(child: graphviz.Digraph, agent: BaseAgent, name: str):
154+
async def build_cluster(
155+
child: graphviz.Digraph, agent: BaseAgent, name: str
156+
) -> None:
153157
if isinstance(agent, LoopAgent):
154158
# Draw the edge from the parent agent to the first sub-agent
155159
if parent_agent:
@@ -215,7 +219,7 @@ async def build_cluster(child: graphviz.Digraph, agent: BaseAgent, name: str):
215219
fontcolor=light_gray,
216220
)
217221

218-
async def draw_node(tool_or_agent: Union[BaseAgent, BaseTool]):
222+
async def draw_node(tool_or_agent: Union[BaseAgent, BaseTool]) -> None:
219223
name = get_node_name(tool_or_agent)
220224
shape = get_node_shape(tool_or_agent)
221225
caption = get_node_caption(tool_or_agent)
@@ -261,7 +265,7 @@ async def draw_node(tool_or_agent: Union[BaseAgent, BaseTool]):
261265

262266
return
263267

264-
def draw_edge(from_name, to_name):
268+
def draw_edge(from_name: str, to_name: str) -> None:
265269
if highlight_pairs:
266270
for highlight_from, highlight_to in highlight_pairs:
267271
if from_name == highlight_from and to_name == highlight_to:

src/google/adk/cli/agent_test_runner.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -370,7 +370,7 @@ def _normalize_ids(events: list[AdkEvent]) -> list[AdkEvent]:
370370
fc.args[k] = new_id
371371

372372
# Pass 2: Update actions and user responses in all events
373-
call_name_to_ids = {}
373+
call_name_to_ids: dict[str | None, list[str | None]] = {}
374374
for e in events:
375375
for fc in e.get_function_calls():
376376
call_name_to_ids.setdefault(fc.name, []).append(fc.id)

0 commit comments

Comments
 (0)