chore: import upstream snapshot with attribution
Continuous Integration / Pre-commit Linter (push) Has been cancelled
Continuous Integration / Mypy Check (Python 3.10) (push) Has been cancelled
Continuous Integration / Mypy Check (Python 3.11) (push) Has been cancelled
Continuous Integration / Mypy Check (Python 3.12) (push) Has been cancelled
Continuous Integration / Mypy Check (Python 3.13) (push) Has been cancelled
Continuous Integration / Unit Tests (Python 3.10) (push) Has been cancelled
Continuous Integration / Unit Tests (Python 3.11) (push) Has been cancelled
Continuous Integration / Unit Tests (Python 3.12) (push) Has been cancelled
Continuous Integration / Unit Tests (Python 3.13) (push) Has been cancelled
Continuous Integration / Unit Tests (Python 3.14) (push) Has been cancelled
Continuous Integration / A2A v0.3 Tests (Python 3.10) (push) Has been cancelled
Continuous Integration / A2A v0.3 Tests (Python 3.11) (push) Has been cancelled
Continuous Integration / A2A v0.3 Tests (Python 3.12) (push) Has been cancelled
Copybara PR Handler / close-imported-pr (push) Has been cancelled
Continuous Integration / A2A v0.3 Tests (Python 3.13) (push) Has been cancelled
Continuous Integration / A2A v0.3 Tests (Python 3.14) (push) Has been cancelled
Continuous Integration / Pre-commit Linter (push) Has been cancelled
Continuous Integration / Mypy Check (Python 3.10) (push) Has been cancelled
Continuous Integration / Mypy Check (Python 3.11) (push) Has been cancelled
Continuous Integration / Mypy Check (Python 3.12) (push) Has been cancelled
Continuous Integration / Mypy Check (Python 3.13) (push) Has been cancelled
Continuous Integration / Unit Tests (Python 3.10) (push) Has been cancelled
Continuous Integration / Unit Tests (Python 3.11) (push) Has been cancelled
Continuous Integration / Unit Tests (Python 3.12) (push) Has been cancelled
Continuous Integration / Unit Tests (Python 3.13) (push) Has been cancelled
Continuous Integration / Unit Tests (Python 3.14) (push) Has been cancelled
Continuous Integration / A2A v0.3 Tests (Python 3.10) (push) Has been cancelled
Continuous Integration / A2A v0.3 Tests (Python 3.11) (push) Has been cancelled
Continuous Integration / A2A v0.3 Tests (Python 3.12) (push) Has been cancelled
Copybara PR Handler / close-imported-pr (push) Has been cancelled
Continuous Integration / A2A v0.3 Tests (Python 3.13) (push) Has been cancelled
Continuous Integration / A2A v0.3 Tests (Python 3.14) (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,8 @@
|
||||
This agent connects to a local MCP server via Streamable HTTP and provides
|
||||
custom per-request headers to the MCP server.
|
||||
|
||||
To run this agent, start the local MCP server first by running:
|
||||
|
||||
```bash
|
||||
uv run header_server.py
|
||||
```
|
||||
@@ -0,0 +1,15 @@
|
||||
# Copyright 2026 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from . import agent
|
||||
@@ -0,0 +1,33 @@
|
||||
# Copyright 2026 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
|
||||
from google.adk.agents.llm_agent import LlmAgent
|
||||
from google.adk.tools.mcp_tool.mcp_session_manager import StreamableHTTPConnectionParams
|
||||
from google.adk.tools.mcp_tool.mcp_toolset import McpToolset
|
||||
|
||||
root_agent = LlmAgent(
|
||||
name='tenant_agent',
|
||||
instruction="""You are a helpful assistant that helps users get tenant
|
||||
information. Call the get_tenant_data tool when the user asks for tenant data.""",
|
||||
tools=[
|
||||
McpToolset(
|
||||
connection_params=StreamableHTTPConnectionParams(
|
||||
url='http://localhost:3000/mcp',
|
||||
),
|
||||
tool_filter=['get_tenant_data'],
|
||||
header_provider=lambda ctx: {'X-Tenant-ID': 'tenant1'},
|
||||
)
|
||||
],
|
||||
)
|
||||
@@ -0,0 +1,50 @@
|
||||
# Copyright 2026 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from fastapi import Request
|
||||
from mcp.server.fastmcp import Context
|
||||
from mcp.server.fastmcp import FastMCP
|
||||
|
||||
mcp = FastMCP('Header Check Server', host='localhost', port=3000)
|
||||
|
||||
TENANT_DATA = {
|
||||
'tenant1': {'name': 'Tenant 1', 'data': 'Data for tenant 1'},
|
||||
'tenant2': {'name': 'Tenant 2', 'data': 'Data for tenant 2'},
|
||||
}
|
||||
|
||||
|
||||
@mcp.tool(
|
||||
description='Returns tenant specific data based on X-Tenant-ID header.'
|
||||
)
|
||||
def get_tenant_data(context: Context) -> dict:
|
||||
"""Return tenant specific data."""
|
||||
if context.request_context and context.request_context.request:
|
||||
headers = context.request_context.request.headers
|
||||
tenant_id = headers.get('x-tenant-id')
|
||||
if tenant_id in TENANT_DATA:
|
||||
return TENANT_DATA[tenant_id]
|
||||
else:
|
||||
return {'error': f'Tenant {tenant_id} not found'}
|
||||
else:
|
||||
return {'error': 'Could not get request context'}
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
try:
|
||||
print('Starting Header Check MCP server on http://localhost:3000')
|
||||
mcp.run(transport='streamable-http')
|
||||
except KeyboardInterrupt:
|
||||
print('\nServer stopped.')
|
||||
@@ -0,0 +1,74 @@
|
||||
# AgentTool with MCP Demo (SSE Mode)
|
||||
|
||||
This demo shows how `AgentTool` works with MCP (Model Context Protocol) toolsets using **SSE mode**.
|
||||
|
||||
## SSE vs Stdio Mode
|
||||
|
||||
This demo uses **SSE (Server-Sent Events) mode** where the MCP server runs as a separate HTTP server:
|
||||
|
||||
- **Remote connection** - Connects to server via HTTP
|
||||
- **Separate process** - Server must be started manually
|
||||
- **Network communication** - Uses HTTP/SSE for messaging
|
||||
|
||||
For the **stdio (subprocess) version**, see [mcp_in_agent_tool_stdio](../mcp_in_agent_tool_stdio/).
|
||||
|
||||
## Setup
|
||||
|
||||
**Start the MCP simple-tool server in SSE mode** (in a separate terminal):
|
||||
|
||||
```bash
|
||||
# Run the server using uvx (no installation needed)
|
||||
# Port 3000 avoids conflict with adk web (which uses 8000)
|
||||
uvx --from 'git+https://github.com/modelcontextprotocol/python-sdk.git#subdirectory=examples/servers/simple-tool' \
|
||||
mcp-simple-tool --transport sse --port 3000
|
||||
```
|
||||
|
||||
The server should be accessible at `http://localhost:3000/sse`.
|
||||
|
||||
## Running the Demo
|
||||
|
||||
```bash
|
||||
adk web contributing/samples
|
||||
```
|
||||
|
||||
Then select **mcp_in_agent_tool_remote** from the list and interact with the agent.
|
||||
|
||||
## Try These Prompts
|
||||
|
||||
This demo uses **Gemini 2.5 Flash** as the model. Try these prompts:
|
||||
|
||||
1. **Check available tools:**
|
||||
|
||||
```
|
||||
What tools do you have access to?
|
||||
```
|
||||
|
||||
1. **Fetch and summarize JSON Schema specification:**
|
||||
|
||||
```
|
||||
Use the mcp_helper to fetch https://json-schema.org/specification and summarize the key features of JSON Schema
|
||||
```
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
main_agent (root_agent)
|
||||
│
|
||||
└── AgentTool wrapping:
|
||||
│
|
||||
└── mcp_helper (sub_agent)
|
||||
│
|
||||
└── McpToolset (SSE connection)
|
||||
│
|
||||
└── http://localhost:3000/sse
|
||||
│
|
||||
└── MCP simple-tool server
|
||||
│
|
||||
└── Website Fetcher Tool
|
||||
```
|
||||
|
||||
## Related
|
||||
|
||||
- **Issue:** [#1112 - Using agent as tool outside of adk web doesn't exit cleanly](https://github.com/google/adk-python/issues/1112)
|
||||
- **Related Issue:** [#929 - LiteLLM giving error with OpenAI models and Grafana's MCP server](https://github.com/google/adk-python/issues/929)
|
||||
- **Stdio Version:** [mcp_in_agent_tool_stdio](../mcp_in_agent_tool_stdio/) - Uses local subprocess connection
|
||||
@@ -0,0 +1,15 @@
|
||||
# Copyright 2026 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from . import agent
|
||||
@@ -0,0 +1,68 @@
|
||||
# Copyright 2026 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from google.adk.agents import Agent
|
||||
from google.adk.tools import AgentTool
|
||||
from google.adk.tools.mcp_tool import McpToolset
|
||||
from google.adk.tools.mcp_tool.mcp_session_manager import SseConnectionParams
|
||||
|
||||
# Create MCP toolset
|
||||
# This uses the simple-tool MCP server via SSE
|
||||
# You need to start the MCP server separately (see README.md)
|
||||
mcp_toolset = McpToolset(
|
||||
connection_params=SseConnectionParams(
|
||||
url="http://localhost:3000/sse",
|
||||
timeout=10.0,
|
||||
sse_read_timeout=300.0,
|
||||
)
|
||||
)
|
||||
|
||||
# Create sub-agent with MCP tools
|
||||
# This agent has direct access to MCP tools
|
||||
sub_agent = Agent(
|
||||
name="mcp_helper",
|
||||
description=(
|
||||
"A helpful assistant with access to MCP tools for fetching websites."
|
||||
),
|
||||
instruction="""You are a helpful assistant with access to MCP tools.
|
||||
|
||||
When the user asks for help:
|
||||
1. Explain what tools you have available (website fetching)
|
||||
2. Use the appropriate tool if needed
|
||||
3. Provide clear and helpful responses
|
||||
|
||||
You have access to a website fetcher tool via MCP. Use it to fetch and return website content.""",
|
||||
tools=[mcp_toolset],
|
||||
)
|
||||
|
||||
# Wrap sub-agent as an AgentTool
|
||||
# This allows the main agent to delegate tasks to the sub-agent
|
||||
# The sub-agent has access to MCP tools for fetching websites
|
||||
mcp_agent_tool = AgentTool(agent=sub_agent)
|
||||
|
||||
# Create main agent
|
||||
# This agent can delegate to the sub-agent via AgentTool
|
||||
root_agent = Agent(
|
||||
name="main_agent",
|
||||
description="Main agent that can delegate to a sub-agent with MCP tools.",
|
||||
instruction="""You are a helpful assistant. You have access to a sub-agent (mcp_helper)
|
||||
that has MCP tools for fetching websites.
|
||||
|
||||
When the user asks for help:
|
||||
- If they need to fetch a website, call the mcp_helper tool
|
||||
- Otherwise, respond directly
|
||||
|
||||
Always be helpful and explain what you're doing.""",
|
||||
tools=[mcp_agent_tool],
|
||||
)
|
||||
@@ -0,0 +1,74 @@
|
||||
# AgentTool with MCP Demo (Stdio Mode)
|
||||
|
||||
This demo shows how `AgentTool` works with MCP (Model Context Protocol) toolsets using **stdio mode**.
|
||||
|
||||
## Stdio vs SSE Mode
|
||||
|
||||
This demo uses **stdio mode** where the MCP server runs as a subprocess:
|
||||
|
||||
- **Simpler setup** - No need to start a separate server
|
||||
- **Auto-launched** - Server starts automatically when agent runs
|
||||
- **Local process** - Uses stdin/stdout for communication
|
||||
|
||||
For the **SSE (remote server) version**, see [mcp_in_agent_tool_remote](../mcp_in_agent_tool_remote/).
|
||||
|
||||
## Setup
|
||||
|
||||
**No installation required!** The MCP server will be launched automatically using `uvx` when you run the agent.
|
||||
|
||||
The demo uses `uvx` to fetch and run the MCP simple-tool server directly from the GitHub repository's subdirectory:
|
||||
|
||||
```bash
|
||||
uvx --from 'git+https://github.com/modelcontextprotocol/python-sdk.git#subdirectory=examples/servers/simple-tool' \
|
||||
mcp-simple-tool
|
||||
```
|
||||
|
||||
This happens automatically via the stdio connection when the agent starts.
|
||||
|
||||
## Running the Demo
|
||||
|
||||
```bash
|
||||
adk web contributing/samples
|
||||
```
|
||||
|
||||
Then select **mcp_in_agent_tool_stdio** from the list and interact with the agent.
|
||||
|
||||
## Try These Prompts
|
||||
|
||||
This demo uses **Gemini 2.5 Flash** as the model. Try these prompts:
|
||||
|
||||
1. **Check available tools:**
|
||||
|
||||
```
|
||||
What tools do you have access to?
|
||||
```
|
||||
|
||||
1. **Fetch and summarize JSON Schema specification:**
|
||||
|
||||
```
|
||||
Use the mcp_helper to fetch https://json-schema.org/specification and summarize the key features of JSON Schema
|
||||
```
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
main_agent (root_agent)
|
||||
│
|
||||
└── AgentTool wrapping:
|
||||
│
|
||||
└── mcp_helper (sub_agent)
|
||||
│
|
||||
└── McpToolset (stdio connection)
|
||||
│
|
||||
└── MCP Server (subprocess via uvx)
|
||||
│
|
||||
└── uvx --from git+...#subdirectory=... mcp-simple-tool
|
||||
│
|
||||
└── Website Fetcher Tool
|
||||
```
|
||||
|
||||
## Related
|
||||
|
||||
- **Issue:** [#1112 - Using agent as tool outside of adk web doesn't exit cleanly](https://github.com/google/adk-python/issues/1112)
|
||||
- **Related Issue:** [#929 - LiteLLM giving error with OpenAI models and Grafana's MCP server](https://github.com/google/adk-python/issues/929)
|
||||
- **SSE Version:** [mcp_in_agent_tool_remote](../mcp_in_agent_tool_remote/) - Uses remote server connection
|
||||
@@ -0,0 +1,15 @@
|
||||
# Copyright 2026 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from . import agent
|
||||
@@ -0,0 +1,75 @@
|
||||
# Copyright 2026 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from google.adk.agents import Agent
|
||||
from google.adk.tools import AgentTool
|
||||
from google.adk.tools.mcp_tool import McpToolset
|
||||
from google.adk.tools.mcp_tool.mcp_session_manager import StdioConnectionParams
|
||||
from mcp import StdioServerParameters
|
||||
|
||||
# Create MCP toolset
|
||||
# This uses the simple-tool MCP server via stdio
|
||||
# The server will be launched automatically using uvx from the subdirectory
|
||||
mcp_toolset = McpToolset(
|
||||
connection_params=StdioConnectionParams(
|
||||
server_params=StdioServerParameters(
|
||||
command="uvx",
|
||||
args=[
|
||||
"--from",
|
||||
"git+https://github.com/modelcontextprotocol/python-sdk.git#subdirectory=examples/servers/simple-tool",
|
||||
"mcp-simple-tool",
|
||||
],
|
||||
),
|
||||
timeout=10.0,
|
||||
)
|
||||
)
|
||||
|
||||
# Create sub-agent with MCP tools
|
||||
# This agent has direct access to MCP tools
|
||||
sub_agent = Agent(
|
||||
name="mcp_helper",
|
||||
description=(
|
||||
"A helpful assistant with access to MCP tools for fetching websites."
|
||||
),
|
||||
instruction="""You are a helpful assistant with access to MCP tools.
|
||||
|
||||
When the user asks for help:
|
||||
1. Explain what tools you have available (website fetching)
|
||||
2. Use the appropriate tool if needed
|
||||
3. Provide clear and helpful responses
|
||||
|
||||
You have access to a website fetcher tool via MCP. Use it to fetch and return website content.""",
|
||||
tools=[mcp_toolset],
|
||||
)
|
||||
|
||||
# Wrap sub-agent as an AgentTool
|
||||
# This allows the main agent to delegate tasks to the sub-agent
|
||||
# The sub-agent has access to MCP tools for fetching websites
|
||||
mcp_agent_tool = AgentTool(agent=sub_agent)
|
||||
|
||||
# Create main agent
|
||||
# This agent can delegate to the sub-agent via AgentTool
|
||||
root_agent = Agent(
|
||||
name="main_agent",
|
||||
description="Main agent that can delegate to a sub-agent with MCP tools.",
|
||||
instruction="""You are a helpful assistant. You have access to a sub-agent (mcp_helper)
|
||||
that has MCP tools for fetching websites.
|
||||
|
||||
When the user asks for help:
|
||||
- If they need to fetch a website, call the mcp_helper tool
|
||||
- Otherwise, respond directly
|
||||
|
||||
Always be helpful and explain what you're doing.""",
|
||||
tools=[mcp_agent_tool],
|
||||
)
|
||||
@@ -0,0 +1,69 @@
|
||||
# PostgreSQL MCP Agent
|
||||
|
||||
This agent uses the PostgreSQL MCP server to interact with PostgreSQL databases. It demonstrates how to:
|
||||
|
||||
- Connect to a PostgreSQL database using MCP (Model Context Protocol)
|
||||
- Use `uvx` to run the MCP server without manual installation
|
||||
- Pass database credentials securely via environment variables
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- **PostgreSQL Database**: You need access to a PostgreSQL database with a connection string
|
||||
- **uvx**: The agent uses `uvx` (part of the `uv` package manager) to run the MCP server
|
||||
|
||||
## Setup Instructions
|
||||
|
||||
### 1. Configure Database Connection
|
||||
|
||||
Create a `.env` file in the `mcp_postgres_agent` directory:
|
||||
|
||||
```bash
|
||||
POSTGRES_CONNECTION_STRING=postgresql://user:password@host:port/database
|
||||
```
|
||||
|
||||
Example connection string format:
|
||||
|
||||
```
|
||||
postgresql://username:password@localhost:5432/mydb
|
||||
postgresql://postgres.xyz:password@aws-region.pooler.supabase.com:5432/postgres
|
||||
```
|
||||
|
||||
### 2. Run the Agent
|
||||
|
||||
Start the ADK Web UI from the samples directory:
|
||||
|
||||
```bash
|
||||
adk web
|
||||
```
|
||||
|
||||
The agent will automatically:
|
||||
|
||||
- Load the connection string from the `.env` file
|
||||
- Use `uvx` to run the `postgres-mcp` server with unrestricted access mode
|
||||
- Connect to your PostgreSQL database
|
||||
|
||||
### 3. Example Queries
|
||||
|
||||
Once the agent is running, try these queries:
|
||||
|
||||
- "What tables are in the database?"
|
||||
- "Show me the schema for the users table"
|
||||
- "Query the first 10 rows from the products table"
|
||||
- "What indexes exist on the orders table?"
|
||||
- "Create a new table called test_table with columns id and name"
|
||||
|
||||
## Configuration Details
|
||||
|
||||
The agent uses:
|
||||
|
||||
- **Model**: Gemini 2.0 Flash
|
||||
- **MCP Server**: `postgres-mcp` (via `uvx`)
|
||||
- **Access Mode**: Unrestricted (allows read/write operations). **Warning**: Using unrestricted mode in a production environment can pose significant security risks. It is recommended to use a more restrictive access mode or configure database user permissions appropriately for production use.
|
||||
- **Connection**: StdioConnectionParams with 60-second timeout
|
||||
- **Environment Variable**: `DATABASE_URI` (mapped from `POSTGRES_CONNECTION_STRING`)
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
- Ensure your `POSTGRES_CONNECTION_STRING` is correctly formatted
|
||||
- Verify database credentials and network access
|
||||
- Check that `uv` is installed (`pip install uv` or `brew install uv`)
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
# Copyright 2026 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from . import agent
|
||||
@@ -0,0 +1,56 @@
|
||||
# Copyright 2026 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import os
|
||||
|
||||
from dotenv import load_dotenv
|
||||
from google.adk.agents.llm_agent import LlmAgent
|
||||
from google.adk.tools.mcp_tool import StdioConnectionParams
|
||||
from google.adk.tools.mcp_tool.mcp_toolset import MCPToolset
|
||||
from google.genai.types import GenerateContentConfig
|
||||
from mcp import StdioServerParameters
|
||||
|
||||
load_dotenv()
|
||||
|
||||
POSTGRES_CONNECTION_STRING = os.getenv("POSTGRES_CONNECTION_STRING")
|
||||
if not POSTGRES_CONNECTION_STRING:
|
||||
raise ValueError(
|
||||
"POSTGRES_CONNECTION_STRING environment variable not set. "
|
||||
"Please create a .env file with this variable."
|
||||
)
|
||||
|
||||
root_agent = LlmAgent(
|
||||
name="postgres_agent",
|
||||
instruction=(
|
||||
"You are a PostgreSQL database assistant. "
|
||||
"Use the provided tools to query, manage, and interact with "
|
||||
"the PostgreSQL database. Ask clarifying questions when unsure."
|
||||
),
|
||||
tools=[
|
||||
MCPToolset(
|
||||
connection_params=StdioConnectionParams(
|
||||
server_params=StdioServerParameters(
|
||||
command="uvx",
|
||||
args=["postgres-mcp", "--access-mode=unrestricted"],
|
||||
env={"DATABASE_URI": POSTGRES_CONNECTION_STRING},
|
||||
),
|
||||
timeout=60,
|
||||
),
|
||||
)
|
||||
],
|
||||
generate_content_config=GenerateContentConfig(
|
||||
temperature=0.2,
|
||||
top_p=0.95,
|
||||
),
|
||||
)
|
||||
@@ -0,0 +1,15 @@
|
||||
# Copyright 2026 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from . import agent
|
||||
@@ -0,0 +1,165 @@
|
||||
# Copyright 2026 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""Sample agent demonstrating MCP progress callback feature.
|
||||
|
||||
This sample shows how to use the progress_callback parameter in McpToolset
|
||||
to receive progress notifications from MCP servers during long-running tool
|
||||
executions.
|
||||
|
||||
There are two ways to use progress callbacks:
|
||||
|
||||
1. Simple callback (shared by all tools):
|
||||
Pass a ProgressFnT callback that receives (progress, total, message).
|
||||
|
||||
2. Factory function (per-tool callbacks with runtime context):
|
||||
Pass a ProgressCallbackFactory that takes (tool_name, callback_context, **kwargs)
|
||||
and returns a ProgressFnT or None. This allows different tools to have different
|
||||
progress handling logic, and the factory can access and modify session state
|
||||
via the CallbackContext. The **kwargs ensures forward compatibility for future
|
||||
parameters.
|
||||
|
||||
IMPORTANT: Progress callbacks only work when the MCP server actually sends
|
||||
progress notifications. Most simple MCP servers (like the filesystem server)
|
||||
do not send progress updates. This sample uses a mock server that demonstrates
|
||||
progress reporting.
|
||||
|
||||
Usage:
|
||||
adk run contributing/samples/mcp_progress_callback_agent
|
||||
|
||||
Then try:
|
||||
"Run the long running task with 5 steps"
|
||||
"Process these items: apple, banana, cherry"
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
from typing import Any
|
||||
|
||||
from google.adk.agents.callback_context import CallbackContext
|
||||
from google.adk.agents.llm_agent import LlmAgent
|
||||
from google.adk.tools.mcp_tool import McpToolset
|
||||
from google.adk.tools.mcp_tool import StdioConnectionParams
|
||||
from mcp import StdioServerParameters
|
||||
from mcp.shared.session import ProgressFnT
|
||||
|
||||
_current_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
_mock_server_path = os.path.join(_current_dir, "mock_progress_server.py")
|
||||
|
||||
|
||||
# Option 1: Simple shared callback
|
||||
async def simple_progress_callback(
|
||||
progress: float,
|
||||
total: float | None,
|
||||
message: str | None,
|
||||
) -> None:
|
||||
"""Handle progress notifications from MCP server.
|
||||
|
||||
This callback is shared by all tools in the toolset.
|
||||
"""
|
||||
if total is not None:
|
||||
percentage = (progress / total) * 100
|
||||
bar_length = 20
|
||||
filled = int(bar_length * progress / total)
|
||||
bar = "=" * filled + "-" * (bar_length - filled)
|
||||
print(f"[{bar}] {percentage:.0f}% ({progress}/{total}) {message or ''}")
|
||||
else:
|
||||
print(f"Progress: {progress} {f'- {message}' if message else ''}")
|
||||
|
||||
|
||||
# Option 2: Factory function for per-tool callbacks with runtime context
|
||||
def progress_callback_factory(
|
||||
tool_name: str,
|
||||
*,
|
||||
callback_context: CallbackContext | None = None,
|
||||
**kwargs: Any,
|
||||
) -> ProgressFnT | None:
|
||||
"""Create a progress callback for a specific tool.
|
||||
|
||||
This factory allows different tools to have different progress handling.
|
||||
It receives a CallbackContext for accessing and modifying runtime information
|
||||
like session state. The **kwargs parameter ensures forward compatibility.
|
||||
|
||||
Args:
|
||||
tool_name: The name of the MCP tool.
|
||||
callback_context: The callback context providing access to session,
|
||||
state, artifacts, and other runtime information. Allows modifying
|
||||
state via ctx.state['key'] = value. May be None if not available.
|
||||
**kwargs: Additional keyword arguments for future extensibility.
|
||||
|
||||
Returns:
|
||||
A progress callback function, or None if no callback is needed.
|
||||
"""
|
||||
# Example: Access session info from context (if available)
|
||||
session_id = "unknown"
|
||||
if callback_context and callback_context.session:
|
||||
session_id = callback_context.session.id
|
||||
|
||||
async def callback(
|
||||
progress: float,
|
||||
total: float | None,
|
||||
message: str | None,
|
||||
) -> None:
|
||||
# Include tool name and session info in the progress output
|
||||
prefix = f"[{tool_name}][session:{session_id}]"
|
||||
if total is not None:
|
||||
percentage = (progress / total) * 100
|
||||
bar_length = 20
|
||||
filled = int(bar_length * progress / total)
|
||||
bar = "=" * filled + "-" * (bar_length - filled)
|
||||
print(f"{prefix} [{bar}] {percentage:.0f}% {message or ''}")
|
||||
# Example: Store progress in state (callback_context allows modification)
|
||||
if callback_context:
|
||||
callback_context.state["last_progress"] = progress
|
||||
callback_context.state["last_total"] = total
|
||||
else:
|
||||
print(
|
||||
f"{prefix} Progress: {progress} {f'- {message}' if message else ''}"
|
||||
)
|
||||
|
||||
return callback
|
||||
|
||||
|
||||
root_agent = LlmAgent(
|
||||
name="progress_demo_agent",
|
||||
instruction="""\
|
||||
You are a helpful assistant that can run long-running tasks.
|
||||
|
||||
Available tools:
|
||||
- long_running_task: Simulates a task with multiple steps. You can specify
|
||||
the number of steps and delay between them.
|
||||
- process_items: Processes a list of items one by one with progress updates.
|
||||
|
||||
When the user asks you to run a task, use these tools and the progress
|
||||
will be logged automatically.
|
||||
|
||||
Example requests:
|
||||
- "Run a long task with 5 steps"
|
||||
- "Process these items: apple, banana, cherry, date"
|
||||
""",
|
||||
tools=[
|
||||
McpToolset(
|
||||
connection_params=StdioConnectionParams(
|
||||
server_params=StdioServerParameters(
|
||||
command=sys.executable, # Use current Python interpreter
|
||||
args=[_mock_server_path],
|
||||
),
|
||||
timeout=60,
|
||||
),
|
||||
# Use factory function for per-tool callbacks (Option 2)
|
||||
# Or use simple_progress_callback for shared callback (Option 1)
|
||||
progress_callback=progress_callback_factory,
|
||||
)
|
||||
],
|
||||
)
|
||||
@@ -0,0 +1,161 @@
|
||||
# Copyright 2026 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""Mock MCP server that sends progress notifications.
|
||||
|
||||
This server demonstrates how MCP servers can send progress updates
|
||||
during long-running tool execution.
|
||||
|
||||
Run this server directly:
|
||||
python mock_progress_server.py
|
||||
|
||||
Or use it with the sample agent:
|
||||
See agent_with_mock_server.py
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
|
||||
from mcp.server import Server
|
||||
from mcp.server.stdio import stdio_server
|
||||
from mcp.types import TextContent
|
||||
from mcp.types import Tool
|
||||
|
||||
server = Server("mock-progress-server")
|
||||
|
||||
|
||||
@server.list_tools()
|
||||
async def list_tools() -> list[Tool]:
|
||||
"""List available tools."""
|
||||
return [
|
||||
Tool(
|
||||
name="long_running_task",
|
||||
description=(
|
||||
"A simulated long-running task that reports progress. "
|
||||
"Use this to test progress callback functionality."
|
||||
),
|
||||
inputSchema={
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"steps": {
|
||||
"type": "integer",
|
||||
"description": "Number of steps to simulate (default: 5)",
|
||||
"default": 5,
|
||||
},
|
||||
"delay": {
|
||||
"type": "number",
|
||||
"description": (
|
||||
"Delay in seconds between steps (default: 0.5)"
|
||||
),
|
||||
"default": 0.5,
|
||||
},
|
||||
},
|
||||
},
|
||||
),
|
||||
Tool(
|
||||
name="process_items",
|
||||
description="Process a list of items with progress reporting.",
|
||||
inputSchema={
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"items": {
|
||||
"type": "array",
|
||||
"items": {"type": "string"},
|
||||
"description": "List of items to process",
|
||||
},
|
||||
},
|
||||
"required": ["items"],
|
||||
},
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
@server.call_tool()
|
||||
async def call_tool(name: str, arguments: dict) -> list[TextContent]:
|
||||
"""Handle tool calls with progress reporting."""
|
||||
ctx = server.request_context
|
||||
|
||||
if name == "long_running_task":
|
||||
steps = arguments.get("steps", 5)
|
||||
delay = arguments.get("delay", 0.5)
|
||||
|
||||
# Get progress token from request metadata
|
||||
progress_token = None
|
||||
if ctx.meta and hasattr(ctx.meta, "progressToken"):
|
||||
progress_token = ctx.meta.progressToken
|
||||
|
||||
for i in range(steps):
|
||||
# Simulate work
|
||||
await asyncio.sleep(delay)
|
||||
|
||||
# Send progress notification if client supports it
|
||||
if progress_token is not None:
|
||||
await ctx.session.send_progress_notification(
|
||||
progress_token=progress_token,
|
||||
progress=i + 1,
|
||||
total=steps,
|
||||
message=f"Completed step {i + 1} of {steps}",
|
||||
)
|
||||
|
||||
return [
|
||||
TextContent(
|
||||
type="text",
|
||||
text=f"Successfully completed {steps} steps!",
|
||||
)
|
||||
]
|
||||
|
||||
elif name == "process_items":
|
||||
items = arguments.get("items", [])
|
||||
total = len(items)
|
||||
|
||||
progress_token = None
|
||||
if ctx.meta and hasattr(ctx.meta, "progressToken"):
|
||||
progress_token = ctx.meta.progressToken
|
||||
|
||||
results = []
|
||||
for i, item in enumerate(items):
|
||||
# Simulate processing
|
||||
await asyncio.sleep(0.3)
|
||||
results.append(f"Processed: {item}")
|
||||
|
||||
# Send progress
|
||||
if progress_token is not None:
|
||||
await ctx.session.send_progress_notification(
|
||||
progress_token=progress_token,
|
||||
progress=i + 1,
|
||||
total=total,
|
||||
message=f"Processing item: {item}",
|
||||
)
|
||||
|
||||
return [
|
||||
TextContent(
|
||||
type="text",
|
||||
text="\n".join(results),
|
||||
)
|
||||
]
|
||||
|
||||
return [TextContent(type="text", text=f"Unknown tool: {name}")]
|
||||
|
||||
|
||||
async def main():
|
||||
"""Run the MCP server."""
|
||||
async with stdio_server() as (read_stream, write_stream):
|
||||
await server.run(
|
||||
read_stream,
|
||||
write_stream,
|
||||
server.create_initialization_options(),
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,52 @@
|
||||
# FastMCP Server-Side Sampling with ADK
|
||||
|
||||
This project demonstrates how to use server-side sampling with a `fastmcp` server connected to an ADK `MCPToolset`.
|
||||
|
||||
## Description
|
||||
|
||||
The setup consists of two main components:
|
||||
|
||||
1. **ADK Agent (`agent.py`):** An `LlmAgent` is configured with an `MCPToolset`. This toolset connects to a local `fastmcp` server.
|
||||
1. **FastMCP Server (`mcp_server.py`):** A `fastmcp` server that exposes a single tool, `analyze_sentiment`. This server is configured to use its own LLM for sampling, independent of the ADK agent's LLM.
|
||||
|
||||
The flow is as follows:
|
||||
|
||||
1. The user provides a text prompt to the ADK agent.
|
||||
1. The agent decides to use the `analyze_sentiment` tool from the `MCPToolset`.
|
||||
1. The tool call is sent to the `mcp_server.py`.
|
||||
1. Inside the `analyze_sentiment` tool, `ctx.sample()` is called. This delegates an LLM call to the `fastmcp` server's own sampling handler.
|
||||
1. The `mcp_server`'s LLM processes the prompt from `ctx.sample()` and returns the result to the server.
|
||||
1. The server processes the LLM response and returns the final sentiment to the agent.
|
||||
1. The agent displays the result to the user.
|
||||
|
||||
## Steps to Run
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- Python 3.10+
|
||||
- `google-adk` library installed.
|
||||
- A configured OpenAI API key.
|
||||
|
||||
### 1. Set up the Environment
|
||||
|
||||
Clone the project and navigate to the directory. Make sure your `OPENAI_API_KEY` is available as an environment variable.
|
||||
|
||||
### 2. Install Dependencies
|
||||
|
||||
Install the required Python libraries:
|
||||
|
||||
```bash
|
||||
pip install fastmcp openai litellm
|
||||
```
|
||||
|
||||
### 3. Run the Example
|
||||
|
||||
Navigate to the `samples` directory and choose this ADK agent:
|
||||
|
||||
```bash
|
||||
adk web .
|
||||
```
|
||||
|
||||
The agent will automatically start the FastMCP server in the background.
|
||||
|
||||
- **Sample user prompt:** "What is the sentiment of 'I love building things with Python'?"
|
||||
@@ -0,0 +1,15 @@
|
||||
# Copyright 2026 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from . import agent
|
||||
@@ -0,0 +1,56 @@
|
||||
# Copyright 2026 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import os
|
||||
|
||||
from google.adk.agents import LlmAgent
|
||||
from google.adk.models.lite_llm import LiteLlm
|
||||
from google.adk.tools.mcp_tool import MCPToolset
|
||||
from google.adk.tools.mcp_tool.mcp_session_manager import StdioConnectionParams
|
||||
from mcp import StdioServerParameters
|
||||
|
||||
# This example uses the OpenAI API for both the agent and the server.
|
||||
# Ensure your OPENAI_API_KEY is available as an environment variable.
|
||||
api_key = os.getenv('OPENAI_API_KEY')
|
||||
if not api_key:
|
||||
raise ValueError('The OPENAI_API_KEY environment variable must be set.')
|
||||
|
||||
# Configure the StdioServerParameters to start the mcp_server.py script
|
||||
# as a subprocess. The OPENAI_API_KEY is passed to the server's environment.
|
||||
server_params = StdioServerParameters(
|
||||
command='python',
|
||||
args=['mcp_server.py'],
|
||||
env={'OPENAI_API_KEY': api_key},
|
||||
)
|
||||
|
||||
# Create the ADK MCPToolset, which connects to the FastMCP server.
|
||||
# The `tool_filter` ensures that only the 'analyze_sentiment' tool is exposed
|
||||
# to the agent.
|
||||
mcp_toolset = MCPToolset(
|
||||
connection_params=StdioConnectionParams(
|
||||
server_params=server_params,
|
||||
),
|
||||
tool_filter=['analyze_sentiment'],
|
||||
)
|
||||
|
||||
# Define the ADK agent that uses the MCP toolset.
|
||||
root_agent = LlmAgent(
|
||||
model=LiteLlm(model='openai/gpt-4o'),
|
||||
name='SentimentAgent',
|
||||
instruction=(
|
||||
'You are an expert at analyzing text sentiment. Use the'
|
||||
' analyze_sentiment tool to classify user input.'
|
||||
),
|
||||
tools=[mcp_toolset],
|
||||
)
|
||||
@@ -0,0 +1,81 @@
|
||||
# Copyright 2026 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import logging
|
||||
import os
|
||||
|
||||
from fastmcp import Context
|
||||
from fastmcp import FastMCP
|
||||
from fastmcp.experimental.sampling.handlers.openai import OpenAISamplingHandler
|
||||
from openai import OpenAI
|
||||
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
API_KEY = os.getenv("OPENAI_API_KEY")
|
||||
|
||||
# Set up the server's LLM handler using the OpenAI API.
|
||||
# This handler will be used for all sampling requests from tools on this server.
|
||||
llm_handler = OpenAISamplingHandler(
|
||||
default_model="gpt-4o",
|
||||
client=OpenAI(
|
||||
api_key=API_KEY,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
# Create the FastMCP Server instance.
|
||||
# The `sampling_handler` is configured to use the server's own LLM.
|
||||
# `sampling_handler_behavior="always"` ensures the server never delegates
|
||||
# sampling back to the ADK agent.
|
||||
mcp = FastMCP(
|
||||
name="SentimentAnalysis",
|
||||
sampling_handler=llm_handler,
|
||||
sampling_handler_behavior="always",
|
||||
)
|
||||
|
||||
|
||||
@mcp.tool
|
||||
async def analyze_sentiment(text: str, ctx: Context) -> dict:
|
||||
"""Analyzes sentiment by delegating to the server's own LLM."""
|
||||
logging.info("analyze_sentiment tool called with text: %s", text)
|
||||
prompt = f"""Analyze the sentiment of the following text as positive,
|
||||
negative, or neutral. Just output a single word.
|
||||
Text to analyze: {text}"""
|
||||
|
||||
# This delegates the LLM call to the server's own sampling handler,
|
||||
# as configured in the FastMCP instance.
|
||||
logging.info("Attempting to call ctx.sample()")
|
||||
try:
|
||||
response = await ctx.sample(prompt)
|
||||
logging.info("ctx.sample() successful. Response: %s", response)
|
||||
except Exception as e:
|
||||
logging.error("ctx.sample() failed: %s", e, exc_info=True)
|
||||
raise
|
||||
|
||||
sentiment = response.text.strip().lower()
|
||||
|
||||
if "positive" in sentiment:
|
||||
result = "positive"
|
||||
elif "negative" in sentiment:
|
||||
result = "negative"
|
||||
else:
|
||||
result = "neutral"
|
||||
|
||||
logging.info("Sentiment analysis result: %s", result)
|
||||
return {"text": text, "sentiment": result}
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("Starting FastMCP server with tool 'analyze_sentiment'...")
|
||||
# This runs the server process, which the ADK agent will connect to.
|
||||
mcp.run()
|
||||
@@ -0,0 +1,57 @@
|
||||
# MCP Service Account Agent Sample
|
||||
|
||||
This agent demonstrates how to connect to a remote MCP server using a gcloud service account for authentication. It uses Streamable HTTP for communication.
|
||||
|
||||
## Setup
|
||||
|
||||
Before running the agent, you need to configure the MCP server URL and your service account credentials in `agent.py`.
|
||||
|
||||
1. **Configure MCP Server URL:**
|
||||
Update the `MCP_SERVER_URL` variable with the URL of your MCP server instance.
|
||||
|
||||
```python
|
||||
# agent.py
|
||||
# TODO: Update this to the production MCP server url and scopes.
|
||||
MCP_SERVER_URL = "https://test.sandbox.googleapis.com/mcp"
|
||||
```
|
||||
|
||||
1. **Set up Service Account Credentials:**
|
||||
|
||||
- Obtain the JSON key file for your gcloud service account.
|
||||
- In `agent.py`, find the `ServiceAccountCredential` object and populate its parameters (e.g., `project_id`, `private_key`, `client_email`, etc.) with the corresponding values from your JSON key file.
|
||||
|
||||
```python
|
||||
# agent.py
|
||||
# TODO: Update this to the user's service account credentials.
|
||||
auth_credential=AuthCredential(
|
||||
auth_type=AuthCredentialTypes.SERVICE_ACCOUNT,
|
||||
service_account=ServiceAccount(
|
||||
service_account_credential=ServiceAccountCredential(
|
||||
type_="service_account",
|
||||
project_id="example",
|
||||
private_key_id="123",
|
||||
private_key="123",
|
||||
client_email="test@example.iam.gserviceaccount.com",
|
||||
client_id="123",
|
||||
auth_uri="https://accounts.google.com/o/oauth2/auth",
|
||||
token_uri="https://oauth2.googleapis.com/token",
|
||||
auth_provider_x509_cert_url=(
|
||||
"https://www.googleapis.com/oauth2/v1/certs"
|
||||
),
|
||||
client_x509_cert_url="https://www.googleapis.com/robot/v1/metadata/x509/example.iam.gserviceaccount.com",
|
||||
universe_domain="googleapis.com",
|
||||
),
|
||||
scopes=SCOPES.keys(),
|
||||
),
|
||||
),
|
||||
```
|
||||
|
||||
## Running the Agent
|
||||
|
||||
Once configured, you can run the agent.
|
||||
|
||||
For example:
|
||||
|
||||
```bash
|
||||
adk web
|
||||
```
|
||||
@@ -0,0 +1,15 @@
|
||||
# Copyright 2026 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from . import agent
|
||||
@@ -0,0 +1,73 @@
|
||||
# Copyright 2026 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
|
||||
from fastapi.openapi.models import OAuth2
|
||||
from fastapi.openapi.models import OAuthFlowClientCredentials
|
||||
from fastapi.openapi.models import OAuthFlows
|
||||
from google.adk.agents.llm_agent import LlmAgent
|
||||
from google.adk.auth.auth_credential import AuthCredential
|
||||
from google.adk.auth.auth_credential import AuthCredentialTypes
|
||||
from google.adk.auth.auth_credential import ServiceAccount
|
||||
from google.adk.auth.auth_credential import ServiceAccountCredential
|
||||
from google.adk.tools.mcp_tool.mcp_session_manager import StreamableHTTPServerParams
|
||||
from google.adk.tools.mcp_tool.mcp_toolset import MCPToolset
|
||||
|
||||
# TODO: Update this to the production MCP server url and scopes.
|
||||
MCP_SERVER_URL = "https://test.sandbox.googleapis.com/mcp"
|
||||
SCOPES = {"https://www.googleapis.com/auth/cloud-platform": ""}
|
||||
|
||||
root_agent = LlmAgent(
|
||||
name="enterprise_assistant",
|
||||
instruction="""
|
||||
Help the user with the tools available to you.
|
||||
""",
|
||||
tools=[
|
||||
MCPToolset(
|
||||
connection_params=StreamableHTTPServerParams(
|
||||
url=MCP_SERVER_URL,
|
||||
),
|
||||
auth_scheme=OAuth2(
|
||||
flows=OAuthFlows(
|
||||
clientCredentials=OAuthFlowClientCredentials(
|
||||
tokenUrl="https://oauth2.googleapis.com/token",
|
||||
scopes=SCOPES,
|
||||
)
|
||||
)
|
||||
),
|
||||
# TODO: Update this to the user's service account credentials.
|
||||
auth_credential=AuthCredential(
|
||||
auth_type=AuthCredentialTypes.SERVICE_ACCOUNT,
|
||||
service_account=ServiceAccount(
|
||||
service_account_credential=ServiceAccountCredential(
|
||||
type_="service_account",
|
||||
project_id="example",
|
||||
private_key_id="123",
|
||||
private_key="123",
|
||||
client_email="test@example.iam.gserviceaccount.com",
|
||||
client_id="123",
|
||||
auth_uri="https://accounts.google.com/o/oauth2/auth",
|
||||
token_uri="https://oauth2.googleapis.com/token",
|
||||
auth_provider_x509_cert_url=(
|
||||
"https://www.googleapis.com/oauth2/v1/certs"
|
||||
),
|
||||
client_x509_cert_url="https://www.googleapis.com/robot/v1/metadata/x509/example.iam.gserviceaccount.com",
|
||||
universe_domain="googleapis.com",
|
||||
),
|
||||
scopes=SCOPES.keys(),
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
)
|
||||
@@ -0,0 +1,7 @@
|
||||
This agent connects to a local MCP server via sse.
|
||||
|
||||
To run this agent, start the local MCP server first by :
|
||||
|
||||
```bash
|
||||
uv run filesystem_server.py
|
||||
```
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
# Copyright 2026 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from . import agent
|
||||
+86
@@ -0,0 +1,86 @@
|
||||
# Copyright 2026 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import logging
|
||||
import os
|
||||
import pprint
|
||||
from typing import Any
|
||||
|
||||
from google.adk.agents.llm_agent import LlmAgent
|
||||
from google.adk.agents.mcp_instruction_provider import McpInstructionProvider
|
||||
from google.adk.tools.base_tool import BaseTool
|
||||
from google.adk.tools.mcp_tool.mcp_session_manager import SseConnectionParams
|
||||
from google.adk.tools.mcp_tool.mcp_toolset import MCPToolset
|
||||
from google.adk.tools.tool_context import ToolContext
|
||||
|
||||
# Configure logging; the mcp_tool logger must be set to
|
||||
# DEBUG to capture http_debug_info
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
logging.getLogger('google_adk.google.adk.tools.mcp_tool.mcp_tool').setLevel(
|
||||
logging.DEBUG
|
||||
)
|
||||
|
||||
_allowed_path = os.path.dirname(os.path.abspath(__file__))
|
||||
|
||||
connection_params = SseConnectionParams(
|
||||
url='http://localhost:3000/sse',
|
||||
headers={'Accept': 'text/event-stream'},
|
||||
)
|
||||
|
||||
|
||||
def after_tool_debug_callback(
|
||||
tool: BaseTool,
|
||||
args: dict[str, Any],
|
||||
tool_context: ToolContext,
|
||||
tool_response: dict[str, Any],
|
||||
) -> dict[str, Any] | None:
|
||||
# pylint: disable=unused-argument
|
||||
print(f'\n=== HTTP Debug Info (from Callback for {tool.name}) ===')
|
||||
pprint.pprint(tool_context.custom_metadata.get('http_debug_info'))
|
||||
print('====================================================\n')
|
||||
return None
|
||||
|
||||
|
||||
root_agent = LlmAgent(
|
||||
name='enterprise_assistant',
|
||||
instruction=McpInstructionProvider(
|
||||
connection_params=connection_params,
|
||||
prompt_name='file_system_prompt',
|
||||
),
|
||||
tools=[
|
||||
MCPToolset(
|
||||
connection_params=connection_params,
|
||||
# don't want agent to do write operation
|
||||
# you can also do below
|
||||
# tool_filter=lambda tool, ctx=None: tool.name
|
||||
# not in [
|
||||
# 'write_file',
|
||||
# 'edit_file',
|
||||
# 'create_directory',
|
||||
# 'move_file',
|
||||
# ],
|
||||
tool_filter=[
|
||||
'read_file',
|
||||
'read_multiple_files',
|
||||
'list_directory',
|
||||
'directory_tree',
|
||||
'search_files',
|
||||
'get_file_info',
|
||||
'list_allowed_directories',
|
||||
],
|
||||
require_confirmation=True,
|
||||
)
|
||||
],
|
||||
after_tool_callback=after_tool_debug_callback,
|
||||
)
|
||||
@@ -0,0 +1,88 @@
|
||||
# Copyright 2026 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import asyncio
|
||||
import os
|
||||
from pathlib import Path
|
||||
import sys
|
||||
|
||||
from mcp.server.fastmcp import FastMCP
|
||||
|
||||
# Create an MCP server with a name
|
||||
mcp = FastMCP("Filesystem Server", host="localhost", port=3000)
|
||||
|
||||
|
||||
# Add a tool to read file contents
|
||||
@mcp.tool(description="Read contents of a file")
|
||||
def read_file(filepath: str) -> str:
|
||||
"""Read and return the contents of a file."""
|
||||
with open(filepath, "r") as f:
|
||||
return f.read()
|
||||
|
||||
|
||||
# Add a tool to list directory contents
|
||||
@mcp.tool(description="List contents of a directory")
|
||||
def list_directory(dirpath: str) -> list:
|
||||
"""List all files and directories in the given directory."""
|
||||
return os.listdir(dirpath)
|
||||
|
||||
|
||||
# Add a tool to get current working directory
|
||||
@mcp.tool(description="Get current working directory")
|
||||
def get_cwd() -> str:
|
||||
"""Return the current working directory."""
|
||||
return str(Path.cwd())
|
||||
|
||||
|
||||
# Add a prompt for accessing file systems
|
||||
@mcp.prompt(name="file_system_prompt")
|
||||
def file_system_prompt() -> str:
|
||||
return f"""\
|
||||
Help the user access their file systems."""
|
||||
|
||||
|
||||
# Graceful shutdown handler
|
||||
async def shutdown(signal, loop):
|
||||
"""Cleanup tasks tied to the service's shutdown."""
|
||||
print(f"\nReceived exit signal {signal.name}...")
|
||||
|
||||
# Get all running tasks
|
||||
tasks = [t for t in asyncio.all_tasks() if t is not asyncio.current_task()]
|
||||
|
||||
# Cancel all tasks
|
||||
for task in tasks:
|
||||
task.cancel()
|
||||
|
||||
print(f"Cancelling {len(tasks)} outstanding tasks")
|
||||
await asyncio.gather(*tasks, return_exceptions=True)
|
||||
|
||||
# Stop the loop
|
||||
loop.stop()
|
||||
print("Shutdown complete!")
|
||||
|
||||
|
||||
# Main entry point with graceful shutdown handling
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
# The MCP run function ultimately uses asyncio.run() internally
|
||||
mcp.run(transport="sse")
|
||||
except KeyboardInterrupt:
|
||||
print("\nServer shutting down gracefully...")
|
||||
# The asyncio event loop has already been stopped by the KeyboardInterrupt
|
||||
print("Server has been shut down.")
|
||||
except Exception as e:
|
||||
print(f"Unexpected error: {e}")
|
||||
sys.exit(1)
|
||||
finally:
|
||||
print("Thank you for using the Filesystem MCP Server!")
|
||||
@@ -0,0 +1,76 @@
|
||||
# MCP SSE Agent with mTLS
|
||||
|
||||
This sample demonstrates how to configure an ADK agent to connect to an MCP server using **mutual TLS (mTLS)** over SSE (HTTPS).
|
||||
|
||||
## Prerequisites
|
||||
|
||||
To test mTLS locally, you need to generate local certificates (CA, Server, and Client) and configure your environment to trust them.
|
||||
|
||||
### 1. Generate Certificates
|
||||
|
||||
Run the helper script in this directory to generate a local CA and sign the server and client certificates:
|
||||
|
||||
```bash
|
||||
./generate_mtls_certs.sh
|
||||
```
|
||||
|
||||
This will generate:
|
||||
|
||||
- `ca.crt`, `ca.key` (Local CA)
|
||||
- `server.crt`, `server.key` (Server certificate/key)
|
||||
- `client.crt`, `client.key` (Client certificate/key)
|
||||
- `certificate_config.json` (Workload certificate configuration for `google-auth`)
|
||||
|
||||
______________________________________________________________________
|
||||
|
||||
## Running the Sample
|
||||
|
||||
### Step 1: Start the MCP Server
|
||||
|
||||
Start the server in this directory. We configure it to trust our local CA so it can verify the client certificate:
|
||||
|
||||
```bash
|
||||
# Point to the certificate config
|
||||
export GOOGLE_API_CERTIFICATE_CONFIG=$(pwd)/certificate_config.json
|
||||
|
||||
# Tell the server to trust our test CA for client verification
|
||||
export SSL_CA_CERTS=$(pwd)/ca.crt
|
||||
|
||||
# Run the server
|
||||
python filesystem_server.py
|
||||
```
|
||||
|
||||
*(The server will run on `https://localhost:3000`)*
|
||||
|
||||
### Step 2: Run the ADK Agent (Client)
|
||||
|
||||
In a second terminal, navigate to the open-source workspace root and run the client.
|
||||
|
||||
```bash
|
||||
cd third_party/py/google/adk/open_source_workspace
|
||||
source .venv/bin/activate
|
||||
|
||||
# 1. Combine system CAs with our test CA so the client trusts the server cert
|
||||
cat /usr/lib/ssl/cert.pem contributing/samples/mcp/mcp_sse_mtls_agent/ca.crt > combined_ca.pem
|
||||
export SSL_CERT_FILE=$(pwd)/combined_ca.pem
|
||||
|
||||
# 2. Point google-auth to our simulated workload config
|
||||
export GOOGLE_API_CERTIFICATE_CONFIG=$(pwd)/contributing/samples/mcp/mcp_sse_mtls_agent/certificate_config.json
|
||||
|
||||
# 3. Enable client certificate usage
|
||||
export GOOGLE_API_USE_CLIENT_CERTIFICATE=true
|
||||
|
||||
# 4. Set your LLM credentials (e.g. source your env file)
|
||||
source test/.env
|
||||
|
||||
# 5. Run the agent
|
||||
adk run contributing/samples/mcp/mcp_sse_mtls_agent
|
||||
```
|
||||
|
||||
______________________________________________________________________
|
||||
|
||||
## How it works
|
||||
|
||||
1. **Client Certificate (mTLS):** The `google-auth` library (used by ADK) reads `GOOGLE_API_CERTIFICATE_CONFIG` to load the client certificate (`client.crt`) and key (`client.key`) as a simulated Workload Certificate.
|
||||
1. **Server Verification:** The server loads the CA (`ca.crt`) via `SSL_CA_CERTS` and requires the client to present a certificate signed by this CA (`ssl_cert_reqs=ssl.CERT_REQUIRED`).
|
||||
1. **Client Verification:** The client trusts the server certificate (`server.crt`) because it is signed by the same CA, which we added to `SSL_CERT_FILE`.
|
||||
@@ -0,0 +1,45 @@
|
||||
# Copyright 2026 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
|
||||
import os
|
||||
|
||||
from google.adk.agents.llm_agent import LlmAgent
|
||||
from google.adk.agents.mcp_instruction_provider import McpInstructionProvider
|
||||
from google.adk.tools.mcp_tool.mcp_session_manager import SseConnectionParams
|
||||
from google.adk.tools.mcp_tool.mcp_toolset import MCPToolset
|
||||
|
||||
connection_params = SseConnectionParams(
|
||||
url=os.environ.get('MCP_SERVER_URL', 'https://localhost:3000/sse'),
|
||||
headers={'Accept': 'text/event-stream'},
|
||||
)
|
||||
|
||||
root_agent = LlmAgent(
|
||||
name='enterprise_assistant',
|
||||
model='gemini-2.5-flash',
|
||||
instruction=McpInstructionProvider(
|
||||
connection_params=connection_params,
|
||||
prompt_name='file_system_prompt',
|
||||
),
|
||||
tools=[
|
||||
MCPToolset(
|
||||
connection_params=connection_params,
|
||||
tool_filter=[
|
||||
'read_file',
|
||||
'list_directory',
|
||||
'get_cwd',
|
||||
],
|
||||
)
|
||||
],
|
||||
)
|
||||
@@ -0,0 +1,151 @@
|
||||
# Copyright 2026 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
|
||||
import asyncio
|
||||
import os
|
||||
import pathlib
|
||||
import ssl
|
||||
import sys
|
||||
import tempfile
|
||||
|
||||
import google.auth.transport.mtls as google_mtls
|
||||
from mcp.server.fastmcp import FastMCP
|
||||
import uvicorn
|
||||
|
||||
# Create an MCP server with a name
|
||||
mcp = FastMCP("Filesystem Server (mTLS)", host="localhost", port=3000)
|
||||
|
||||
|
||||
# Add a tool to read file contents
|
||||
@mcp.tool(description="Read contents of a file")
|
||||
def read_file(filepath: str) -> str:
|
||||
"""Read and return the contents of a file."""
|
||||
with open(filepath, "r") as f:
|
||||
return f.read()
|
||||
|
||||
|
||||
# Add a tool to list directory contents
|
||||
@mcp.tool(description="List contents of a directory")
|
||||
def list_directory(dirpath: str) -> list:
|
||||
"""List all files and directories in the given directory."""
|
||||
return os.listdir(dirpath)
|
||||
|
||||
|
||||
# Add a tool to get current working directory
|
||||
@mcp.tool(description="Get current working directory")
|
||||
def get_cwd() -> str:
|
||||
"""Return the current working directory."""
|
||||
return str(pathlib.Path.cwd())
|
||||
|
||||
|
||||
# Add a prompt for accessing file systems
|
||||
@mcp.prompt()
|
||||
def file_system_prompt() -> str:
|
||||
"""Prompt helper for accessing file systems."""
|
||||
return (
|
||||
"You are a helpful assistant with access to the local filesystem. You can"
|
||||
" read files and list directories to help the user with their request."
|
||||
)
|
||||
|
||||
|
||||
# Graceful shutdown handler
|
||||
async def shutdown(signal, loop):
|
||||
"""Cleanup tasks on shutdown."""
|
||||
print(f"\nReceived exit signal {signal.name}...")
|
||||
tasks = [t for t in asyncio.all_tasks() if t is not asyncio.current_task()]
|
||||
for task in tasks:
|
||||
task.cancel()
|
||||
print(f"Cancelling {len(tasks)} outstanding tasks")
|
||||
await asyncio.gather(*tasks, return_exceptions=True)
|
||||
loop.stop()
|
||||
|
||||
|
||||
# Main entry point with mTLS enabled
|
||||
if __name__ == "__main__":
|
||||
cert_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
keyfile = os.path.join(cert_dir, "server.key")
|
||||
certfile = os.path.join(cert_dir, "server.crt")
|
||||
|
||||
if not (os.path.exists(keyfile) and os.path.exists(certfile)):
|
||||
print(f"Error: mTLS cert files not found in {cert_dir}")
|
||||
print("Please generate them using the helper script:")
|
||||
print(f" ./generate_mtls_certs.sh")
|
||||
sys.exit(1)
|
||||
|
||||
# Configure SSL context for mTLS
|
||||
print("Configuring SSL context for mTLS...")
|
||||
|
||||
# Allow explicit CA certs override (useful for testing with custom CA signed certs)
|
||||
ca_certs = os.environ.get("SSL_CA_CERTS")
|
||||
temp_ca_file = None
|
||||
|
||||
if ca_certs:
|
||||
print(f" Using explicit SSL_CA_CERTS: {ca_certs}")
|
||||
else:
|
||||
has_cert_source = google_mtls.has_default_client_cert_source()
|
||||
print(f" has_default_client_cert_source: {has_cert_source}")
|
||||
print(f" default cafile: {ssl.get_default_verify_paths().cafile}")
|
||||
|
||||
if has_cert_source:
|
||||
try:
|
||||
callback = google_mtls.default_client_cert_source()
|
||||
client_cert_bytes, _ = callback()
|
||||
temp_ca_file = tempfile.NamedTemporaryFile(delete=False, suffix=".crt")
|
||||
temp_ca_file.write(client_cert_bytes)
|
||||
temp_ca_file.close()
|
||||
ca_certs = temp_ca_file.name
|
||||
print(f" Loaded client cert to trust: {ca_certs}")
|
||||
except Exception as e:
|
||||
print(f" Warning: Failed to load default client cert: {e}")
|
||||
ca_certs = ssl.get_default_verify_paths().cafile
|
||||
else:
|
||||
print(" No default client cert source found. Using system CAs.")
|
||||
ca_certs = ssl.get_default_verify_paths().cafile
|
||||
|
||||
print(f" Using ca_certs for client verification: {ca_certs}")
|
||||
|
||||
app = mcp.sse_app()
|
||||
|
||||
config = uvicorn.Config(
|
||||
app,
|
||||
host=mcp.settings.host,
|
||||
port=mcp.settings.port,
|
||||
log_level=mcp.settings.log_level.lower(),
|
||||
ssl_keyfile=keyfile,
|
||||
ssl_certfile=certfile,
|
||||
ssl_cert_reqs=int(ssl.CERT_REQUIRED),
|
||||
ssl_ca_certs=ca_certs,
|
||||
)
|
||||
server = uvicorn.Server(config)
|
||||
|
||||
print(
|
||||
"Starting MCP server with mTLS on"
|
||||
f" https://{mcp.settings.host}:{mcp.settings.port}"
|
||||
)
|
||||
try:
|
||||
asyncio.run(server.serve())
|
||||
except KeyboardInterrupt:
|
||||
print("\nServer shutting down gracefully...")
|
||||
except Exception as e:
|
||||
print(f"Unexpected error: {e}")
|
||||
sys.exit(1)
|
||||
finally:
|
||||
if temp_ca_file:
|
||||
try:
|
||||
os.unlink(temp_ca_file.name)
|
||||
print(f"Cleaned up temp CA file: {temp_ca_file.name}")
|
||||
except OSError:
|
||||
pass
|
||||
print("Thank you for using the Filesystem MCP Server!")
|
||||
@@ -0,0 +1,56 @@
|
||||
#!/bin/bash
|
||||
# Copyright 2026 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
set -e
|
||||
|
||||
# Directory where this script is located
|
||||
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
|
||||
cd "$DIR"
|
||||
|
||||
echo "Generating certificates in $DIR..."
|
||||
|
||||
# 1. Create CA
|
||||
openssl req -x509 -new -nodes -newkey rsa:2048 -keyout ca.key -sha256 -days 365 -out ca.crt -subj '/CN=TestCA'
|
||||
|
||||
# 2. Create Server Cert
|
||||
openssl req -new -nodes -newkey rsa:2048 -keyout server.key -out server.csr -subj '/CN=localhost'
|
||||
# Sign with CA
|
||||
openssl x509 -req -in server.csr -CA ca.crt -CAkey ca.key -CAcreateserial -out server.crt -days 365 -sha256
|
||||
|
||||
# 3. Create Client Cert
|
||||
openssl req -new -nodes -newkey rsa:2048 -keyout client.key -out client.csr -subj '/CN=TestClient'
|
||||
# Sign with CA
|
||||
openssl x509 -req -in client.csr -CA ca.crt -CAkey ca.key -CAcreateserial -out client.crt -days 365 -sha256
|
||||
|
||||
# Clean up CSRs and serial file
|
||||
rm -f server.csr client.csr ca.srl
|
||||
|
||||
# 4. Create certificate_config.json
|
||||
cat <<EOF > certificate_config.json
|
||||
{
|
||||
"cert_configs": {
|
||||
"workload": {
|
||||
"cert_path": "$DIR/client.crt",
|
||||
"key_path": "$DIR/client.key"
|
||||
}
|
||||
}
|
||||
}
|
||||
EOF
|
||||
|
||||
echo "Done! Generated:"
|
||||
echo " - ca.crt, ca.key (CA)"
|
||||
echo " - server.crt, server.key (Server cert)"
|
||||
echo " - client.crt, client.key (Client cert)"
|
||||
echo " - certificate_config.json (Workload config for google-auth)"
|
||||
@@ -0,0 +1,21 @@
|
||||
# Notion MCP Agent
|
||||
|
||||
This is an agent that is using Notion MCP tool to call Notion API. And it demonstrates how to pass in the Notion API key.
|
||||
|
||||
Follow below instruction to use it:
|
||||
|
||||
- Follow the installation instruction in below page to get an API key for Notion API:
|
||||
https://www.npmjs.com/package/@notionhq/notion-mcp-server
|
||||
|
||||
- Set the environment variable `NOTION_API_KEY` to the API key you obtained in the previous step.
|
||||
|
||||
```bash
|
||||
export NOTION_API_KEY=<your_notion_api_key>
|
||||
```
|
||||
|
||||
- Run the agent in ADK Web UI
|
||||
|
||||
- Send below queries:
|
||||
|
||||
- What can you do for me ?
|
||||
- Search `XXXX` in my pages.
|
||||
@@ -0,0 +1,15 @@
|
||||
# Copyright 2026 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from . import agent
|
||||
@@ -0,0 +1,47 @@
|
||||
# Copyright 2026 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import json
|
||||
import os
|
||||
|
||||
from dotenv import load_dotenv
|
||||
from google.adk.agents.llm_agent import LlmAgent
|
||||
from google.adk.tools.mcp_tool.mcp_toolset import MCPToolset
|
||||
from google.adk.tools.mcp_tool.mcp_toolset import StdioServerParameters
|
||||
|
||||
load_dotenv()
|
||||
|
||||
NOTION_API_KEY = os.getenv("NOTION_API_KEY")
|
||||
NOTION_HEADERS = json.dumps({
|
||||
"Authorization": f"Bearer {NOTION_API_KEY}",
|
||||
"Notion-Version": "2022-06-28",
|
||||
})
|
||||
|
||||
root_agent = LlmAgent(
|
||||
name="notion_agent",
|
||||
instruction=(
|
||||
"You are my workspace assistant. "
|
||||
"Use the provided tools to read, search, comment on, "
|
||||
"or create Notion pages. Ask clarifying questions when unsure."
|
||||
),
|
||||
tools=[
|
||||
MCPToolset(
|
||||
connection_params=StdioServerParameters(
|
||||
command="npx",
|
||||
args=["-y", "@notionhq/notion-mcp-server"],
|
||||
env={"OPENAPI_MCP_HEADERS": NOTION_HEADERS},
|
||||
)
|
||||
)
|
||||
],
|
||||
)
|
||||
@@ -0,0 +1,15 @@
|
||||
# Copyright 2026 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from . import agent
|
||||
@@ -0,0 +1,65 @@
|
||||
# Copyright 2026 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
|
||||
import os
|
||||
|
||||
from google.adk.agents.llm_agent import LlmAgent
|
||||
from google.adk.tools.mcp_tool 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__))
|
||||
|
||||
root_agent = LlmAgent(
|
||||
name='enterprise_assistant',
|
||||
instruction=f"""\
|
||||
Help user accessing their file systems.
|
||||
|
||||
Allowed directory: {_allowed_path}
|
||||
""",
|
||||
tools=[
|
||||
MCPToolset(
|
||||
connection_params=StdioConnectionParams(
|
||||
server_params=StdioServerParameters(
|
||||
command='npx',
|
||||
args=[
|
||||
'-y', # Arguments for the command
|
||||
'@modelcontextprotocol/server-filesystem',
|
||||
_allowed_path,
|
||||
],
|
||||
),
|
||||
timeout=5,
|
||||
),
|
||||
# don't want agent to do write operation
|
||||
# you can also do below
|
||||
# tool_filter=lambda tool, ctx=None: tool.name
|
||||
# not in [
|
||||
# 'write_file',
|
||||
# 'edit_file',
|
||||
# 'create_directory',
|
||||
# 'move_file',
|
||||
# ],
|
||||
tool_filter=[
|
||||
'read_file',
|
||||
'read_multiple_files',
|
||||
'list_directory',
|
||||
'directory_tree',
|
||||
'search_files',
|
||||
'get_file_info',
|
||||
'list_allowed_directories',
|
||||
],
|
||||
)
|
||||
],
|
||||
)
|
||||
@@ -0,0 +1,7 @@
|
||||
This agent connects to a local MCP server via Streamable HTTP.
|
||||
|
||||
To run this agent, start the local MCP server first by:
|
||||
|
||||
```bash
|
||||
uv run filesystem_server.py
|
||||
```
|
||||
@@ -0,0 +1,15 @@
|
||||
# Copyright 2026 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from . import agent
|
||||
@@ -0,0 +1,57 @@
|
||||
# Copyright 2026 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
|
||||
import os
|
||||
|
||||
from google.adk.agents.llm_agent import LlmAgent
|
||||
from google.adk.tools.mcp_tool.mcp_session_manager import StreamableHTTPServerParams
|
||||
from google.adk.tools.mcp_tool.mcp_toolset import MCPToolset
|
||||
|
||||
_allowed_path = os.path.dirname(os.path.abspath(__file__))
|
||||
|
||||
root_agent = LlmAgent(
|
||||
name='enterprise_assistant',
|
||||
instruction=f"""\
|
||||
Help user accessing their file systems.
|
||||
|
||||
Allowed directory: {_allowed_path}
|
||||
""",
|
||||
tools=[
|
||||
MCPToolset(
|
||||
connection_params=StreamableHTTPServerParams(
|
||||
url='http://localhost:3000/mcp',
|
||||
),
|
||||
# don't want agent to do write operation
|
||||
# you can also do below
|
||||
# tool_filter=lambda tool, ctx=None: tool.name
|
||||
# not in [
|
||||
# 'write_file',
|
||||
# 'edit_file',
|
||||
# 'create_directory',
|
||||
# 'move_file',
|
||||
# ],
|
||||
tool_filter=[
|
||||
'read_file',
|
||||
'read_multiple_files',
|
||||
'list_directory',
|
||||
'directory_tree',
|
||||
'search_files',
|
||||
'get_file_info',
|
||||
'list_allowed_directories',
|
||||
],
|
||||
use_mcp_resources=True,
|
||||
)
|
||||
],
|
||||
)
|
||||
@@ -0,0 +1,100 @@
|
||||
# Copyright 2026 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import sys
|
||||
|
||||
from mcp.server.fastmcp import FastMCP
|
||||
|
||||
# Create an MCP server with a name
|
||||
mcp = FastMCP("Filesystem Server", host="localhost", port=3000)
|
||||
|
||||
|
||||
# Add a tool to read file contents
|
||||
@mcp.tool(description="Read contents of a file")
|
||||
def read_file(filepath: str) -> str:
|
||||
"""Read and return the contents of a file."""
|
||||
with open(filepath, "r") as f:
|
||||
return f.read()
|
||||
|
||||
|
||||
# Add a tool to list directory contents
|
||||
@mcp.tool(description="List contents of a directory")
|
||||
def list_directory(dirpath: str) -> list:
|
||||
"""List all files and directories in the given directory."""
|
||||
return os.listdir(dirpath)
|
||||
|
||||
|
||||
# Add a tool to get current working directory
|
||||
@mcp.tool(description="Get current working directory")
|
||||
def get_cwd() -> str:
|
||||
"""Return the current working directory."""
|
||||
return str(Path.cwd())
|
||||
|
||||
|
||||
# Add a resource for testing with JSON data
|
||||
@mcp.resource(
|
||||
name="sample_data",
|
||||
uri="file:///sample_data.json",
|
||||
mime_type="application/json",
|
||||
)
|
||||
def sample_data() -> str:
|
||||
data = {
|
||||
"users": [
|
||||
{"id": 1, "name": "Alice", "role": "admin"},
|
||||
{"id": 2, "name": "Bob", "role": "user"},
|
||||
{"id": 3, "name": "Charlie", "role": "user"},
|
||||
],
|
||||
"settings": {"theme": "dark", "notifications": True},
|
||||
}
|
||||
return json.dumps(data, indent=2)
|
||||
|
||||
|
||||
# Graceful shutdown handler
|
||||
async def shutdown(signal, loop):
|
||||
"""Cleanup tasks tied to the service's shutdown."""
|
||||
print(f"\nReceived exit signal {signal.name}...")
|
||||
|
||||
# Get all running tasks
|
||||
tasks = [t for t in asyncio.all_tasks() if t is not asyncio.current_task()]
|
||||
|
||||
# Cancel all tasks
|
||||
for task in tasks:
|
||||
task.cancel()
|
||||
|
||||
print(f"Cancelling {len(tasks)} outstanding tasks")
|
||||
await asyncio.gather(*tasks, return_exceptions=True)
|
||||
|
||||
# Stop the loop
|
||||
loop.stop()
|
||||
print("Shutdown complete!")
|
||||
|
||||
|
||||
# Main entry point with graceful shutdown handling
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
# The MCP run function ultimately uses asyncio.run() internally
|
||||
mcp.run(transport="streamable-http")
|
||||
except KeyboardInterrupt:
|
||||
print("\nServer shutting down gracefully...")
|
||||
# The asyncio event loop has already been stopped by the KeyboardInterrupt
|
||||
print("Server has been shut down.")
|
||||
except Exception as e:
|
||||
print(f"Unexpected error: {e}")
|
||||
sys.exit(1)
|
||||
finally:
|
||||
print("Thank you for using the Filesystem MCP Server!")
|
||||
@@ -0,0 +1,47 @@
|
||||
# MCP Toolset OAuth Authentication Sample
|
||||
|
||||
This sample demonstrates the toolset authentication feature where OAuth credentials are required for both tool listing and tool calling.
|
||||
|
||||
## Overview
|
||||
|
||||
The toolset authentication flow works in two phases:
|
||||
|
||||
1. **Phase 1**: When the agent tries to get tools from the MCP server without credentials, the toolset signals "authentication required" and returns an auth request event.
|
||||
|
||||
1. **Phase 2**: After the user provides OAuth credentials, the agent can successfully list and call tools.
|
||||
|
||||
## Files
|
||||
|
||||
- `oauth_mcp_server.py` - MCP server that requires Bearer token authentication
|
||||
- `agent.py` - Agent configuration with OAuth-protected MCP toolset
|
||||
- `main.py` - Test script demonstrating the two-phase auth flow
|
||||
|
||||
## Running the Sample
|
||||
|
||||
1. Start the MCP server in one terminal:
|
||||
|
||||
```bash
|
||||
PYTHONPATH=src python contributing/samples/mcp_toolset_auth/oauth_mcp_server.py
|
||||
```
|
||||
|
||||
2. Run the test script in another terminal:
|
||||
|
||||
```bash
|
||||
PYTHONPATH=src python contributing/samples/mcp_toolset_auth/main.py
|
||||
```
|
||||
|
||||
## Expected Behavior
|
||||
|
||||
1. First invocation yields an `adk_request_credential` function call
|
||||
1. The credential ID is `_adk_toolset_auth_McpToolset` to indicate toolset auth
|
||||
1. After providing the access token, the agent can list and call tools
|
||||
|
||||
## Testing with ADK Web UI
|
||||
|
||||
You can also test with the ADK web UI:
|
||||
|
||||
```bash
|
||||
adk web contributing/samples/mcp_toolset_auth
|
||||
```
|
||||
|
||||
Note: The web UI will display the auth request and you'll need to manually provide credentials.
|
||||
@@ -0,0 +1,15 @@
|
||||
# Copyright 2026 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from . import agent
|
||||
@@ -0,0 +1,75 @@
|
||||
# Copyright 2026 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""Agent that uses MCP toolset requiring OAuth authentication.
|
||||
|
||||
This agent demonstrates the toolset authentication feature where OAuth
|
||||
credentials are required for both tool listing and tool calling.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from fastapi.openapi.models import OAuth2
|
||||
from fastapi.openapi.models import OAuthFlowAuthorizationCode
|
||||
from fastapi.openapi.models import OAuthFlows
|
||||
from google.adk.agents import LlmAgent
|
||||
from google.adk.auth.auth_credential import AuthCredential
|
||||
from google.adk.auth.auth_credential import AuthCredentialTypes
|
||||
from google.adk.auth.auth_credential import OAuth2Auth
|
||||
from google.adk.tools.mcp_tool.mcp_session_manager import StreamableHTTPConnectionParams
|
||||
from google.adk.tools.mcp_tool.mcp_toolset import McpToolset
|
||||
|
||||
# OAuth2 auth scheme with authorization code flow
|
||||
# This specifies the OAuth metadata needed for the full OAuth flow
|
||||
auth_scheme = OAuth2(
|
||||
flows=OAuthFlows(
|
||||
authorizationCode=OAuthFlowAuthorizationCode(
|
||||
authorizationUrl='https://example.com/oauth/authorize',
|
||||
tokenUrl='https://example.com/oauth/token',
|
||||
scopes={'read': 'Read access', 'write': 'Write access'},
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
# OAuth credential with client credentials (used for token exchange)
|
||||
# In a real scenario, this would be used to obtain the access token
|
||||
auth_credential = AuthCredential(
|
||||
auth_type=AuthCredentialTypes.OAUTH2,
|
||||
oauth2=OAuth2Auth(
|
||||
client_id='test_client_id',
|
||||
client_secret='test_client_secret',
|
||||
),
|
||||
)
|
||||
|
||||
# Create the MCP toolset with OAuth authentication
|
||||
mcp_toolset = McpToolset(
|
||||
connection_params=StreamableHTTPConnectionParams(
|
||||
url='http://localhost:3001/mcp',
|
||||
),
|
||||
auth_scheme=auth_scheme,
|
||||
auth_credential=auth_credential,
|
||||
)
|
||||
|
||||
# Define the agent that uses the OAuth-protected MCP toolset
|
||||
root_agent = LlmAgent(
|
||||
name='oauth_mcp_agent',
|
||||
instruction="""You are a helpful assistant that can access user information.
|
||||
|
||||
You have access to tools that require authentication:
|
||||
- get_user_profile: Get profile information for a specific user
|
||||
- list_users: List all available users
|
||||
|
||||
When the user asks about users, use these tools to help them.""",
|
||||
tools=[mcp_toolset],
|
||||
)
|
||||
@@ -0,0 +1,168 @@
|
||||
# Copyright 2026 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""Test script for MCP Toolset OAuth Authentication Flow.
|
||||
|
||||
This script demonstrates the two-phase tool discovery flow:
|
||||
1. First invocation: Agent tries to get tools, auth is required, returns auth
|
||||
request event (adk_request_credential)
|
||||
2. User provides OAuth credentials (simulated)
|
||||
3. Second invocation: Agent has credentials, can list and call tools
|
||||
|
||||
Usage:
|
||||
# Start the MCP server first (in another terminal):
|
||||
PYTHONPATH=src python contributing/samples/mcp_toolset_auth/oauth_mcp_server.py
|
||||
|
||||
# Run the demo:
|
||||
PYTHONPATH=src python contributing/samples/mcp_toolset_auth/main.py
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
|
||||
from agent import auth_credential
|
||||
from agent import auth_scheme
|
||||
from agent import mcp_toolset
|
||||
from agent import root_agent
|
||||
from google.adk.auth.auth_credential import AuthCredential
|
||||
from google.adk.auth.auth_credential import AuthCredentialTypes
|
||||
from google.adk.auth.auth_credential import OAuth2Auth
|
||||
from google.adk.auth.auth_tool import AuthConfig
|
||||
from google.adk.runners import Runner
|
||||
from google.adk.sessions.in_memory_session_service import InMemorySessionService
|
||||
from google.genai import types
|
||||
|
||||
|
||||
async def run_demo():
|
||||
"""Run demo with real MCP server."""
|
||||
print('=' * 60)
|
||||
print('MCP Toolset OAuth Authentication Demo')
|
||||
print('=' * 60)
|
||||
print('\nNote: Make sure the MCP server is running:')
|
||||
print(' python oauth_mcp_server.py\n')
|
||||
|
||||
# Create session service and runner
|
||||
session_service = InMemorySessionService()
|
||||
runner = Runner(
|
||||
agent=root_agent,
|
||||
app_name='toolset_auth_demo',
|
||||
session_service=session_service,
|
||||
)
|
||||
|
||||
# Create a session
|
||||
session = await session_service.create_session(
|
||||
app_name='toolset_auth_demo',
|
||||
user_id='test_user',
|
||||
)
|
||||
|
||||
print(f'Session created: {session.id}')
|
||||
print('\n--- Phase 1: Initial request (no credentials) ---\n')
|
||||
|
||||
# First invocation - should trigger auth request
|
||||
user_message = 'List all users'
|
||||
print(f'User: {user_message}')
|
||||
|
||||
events = []
|
||||
auth_function_call_id = None
|
||||
max_events = 10
|
||||
|
||||
try:
|
||||
async for event in runner.run_async(
|
||||
session_id=session.id,
|
||||
user_id='test_user',
|
||||
new_message=types.Content(
|
||||
role='user',
|
||||
parts=[types.Part(text=user_message)],
|
||||
),
|
||||
):
|
||||
events.append(event)
|
||||
print(f'\nEvent from {event.author}:')
|
||||
if event.content and event.content.parts:
|
||||
for part in event.content.parts:
|
||||
if part.text:
|
||||
print(f' Text: {part.text}')
|
||||
if part.function_call:
|
||||
print(f' Function call: {part.function_call.name}')
|
||||
if part.function_call.name == 'adk_request_credential':
|
||||
auth_function_call_id = part.function_call.id
|
||||
|
||||
if len(events) >= max_events:
|
||||
print(f'\n** SAFETY LIMIT ({max_events} events) **')
|
||||
break
|
||||
|
||||
except Exception as e:
|
||||
print(f'\nError: {e}')
|
||||
print('Make sure the MCP server is running!')
|
||||
await mcp_toolset.close()
|
||||
return
|
||||
|
||||
if auth_function_call_id:
|
||||
print('\n** Auth request detected! **')
|
||||
print('\n--- Phase 2: Provide OAuth credentials ---\n')
|
||||
|
||||
# Simulate user providing OAuth credentials after completing OAuth flow
|
||||
auth_response = AuthConfig(
|
||||
auth_scheme=auth_scheme,
|
||||
raw_auth_credential=auth_credential,
|
||||
exchanged_auth_credential=AuthCredential(
|
||||
auth_type=AuthCredentialTypes.OAUTH2,
|
||||
oauth2=OAuth2Auth(
|
||||
access_token='test_access_token_12345',
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
print('Providing access token: test_access_token_12345')
|
||||
|
||||
auth_response_message = types.Content(
|
||||
role='user',
|
||||
parts=[
|
||||
types.Part(
|
||||
function_response=types.FunctionResponse(
|
||||
name='adk_request_credential',
|
||||
id=auth_function_call_id,
|
||||
response=auth_response.model_dump(exclude_none=True),
|
||||
)
|
||||
)
|
||||
],
|
||||
)
|
||||
|
||||
async for event in runner.run_async(
|
||||
session_id=session.id,
|
||||
user_id='test_user',
|
||||
new_message=auth_response_message,
|
||||
):
|
||||
print(f'\nEvent from {event.author}:')
|
||||
if event.content and event.content.parts:
|
||||
for part in event.content.parts:
|
||||
if part.text:
|
||||
text = (
|
||||
part.text[:200] + '...' if len(part.text) > 200 else part.text
|
||||
)
|
||||
print(f' Text: {text}')
|
||||
if part.function_call:
|
||||
print(f' Function call: {part.function_call.name}')
|
||||
else:
|
||||
print('\n** No auth request - credentials may already be available **')
|
||||
|
||||
print('\n' + '=' * 60)
|
||||
print('Demo completed')
|
||||
print('=' * 60)
|
||||
|
||||
await mcp_toolset.close()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
asyncio.run(run_demo())
|
||||
@@ -0,0 +1,120 @@
|
||||
# Copyright 2026 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""MCP Server that requires OAuth Bearer token for both tool listing and calling.
|
||||
|
||||
This server validates the Authorization header on every request including:
|
||||
- Tool listing (list_tools endpoint)
|
||||
- Tool calling (call_tool endpoint)
|
||||
|
||||
This is used to test the toolset authentication feature in ADK.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
|
||||
from fastapi import FastAPI
|
||||
from fastapi import HTTPException
|
||||
from fastapi import Request
|
||||
from mcp.server.fastmcp import Context
|
||||
from mcp.server.fastmcp import FastMCP
|
||||
import uvicorn
|
||||
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
logger = logging.getLogger('google_adk.' + __name__)
|
||||
|
||||
# Expected OAuth token for testing
|
||||
VALID_TOKEN = 'test_access_token_12345'
|
||||
|
||||
# Create FastMCP server
|
||||
mcp = FastMCP('OAuth Protected MCP Server', host='localhost', port=3001)
|
||||
|
||||
|
||||
def validate_auth_header(request: Request) -> bool:
|
||||
"""Validate the Authorization header contains a valid Bearer token."""
|
||||
auth_header = request.headers.get('authorization', '')
|
||||
if not auth_header.startswith('Bearer '):
|
||||
logger.warning('Missing or invalid Authorization header: %s', auth_header)
|
||||
return False
|
||||
|
||||
token = auth_header[7:] # Remove 'Bearer ' prefix
|
||||
if token != VALID_TOKEN:
|
||||
logger.warning('Invalid token: %s', token)
|
||||
return False
|
||||
|
||||
logger.info('Valid token received')
|
||||
return True
|
||||
|
||||
|
||||
@mcp.tool(description='Get user profile information. Requires authentication.')
|
||||
def get_user_profile(user_id: str, context: Context) -> dict:
|
||||
"""Return user profile data for the given user ID."""
|
||||
logger.info('get_user_profile called for user: %s', user_id)
|
||||
|
||||
if context.request_context and context.request_context.request:
|
||||
if not validate_auth_header(context.request_context.request):
|
||||
return {'error': 'Unauthorized - invalid or missing token'}
|
||||
|
||||
# Mock user data
|
||||
users = {
|
||||
'user1': {'id': 'user1', 'name': 'Alice', 'email': 'alice@example.com'},
|
||||
'user2': {'id': 'user2', 'name': 'Bob', 'email': 'bob@example.com'},
|
||||
}
|
||||
|
||||
if user_id in users:
|
||||
return users[user_id]
|
||||
return {'error': f'User {user_id} not found'}
|
||||
|
||||
|
||||
@mcp.tool(description='List all available users. Requires authentication.')
|
||||
def list_users(context: Context) -> dict:
|
||||
"""Return a list of all users."""
|
||||
logger.info('list_users called')
|
||||
|
||||
if context.request_context and context.request_context.request:
|
||||
if not validate_auth_header(context.request_context.request):
|
||||
return {'error': 'Unauthorized - invalid or missing token'}
|
||||
|
||||
return {
|
||||
'users': [
|
||||
{'id': 'user1', 'name': 'Alice'},
|
||||
{'id': 'user2', 'name': 'Bob'},
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
# Create custom FastAPI app to add auth middleware for list_tools
|
||||
app = FastAPI()
|
||||
|
||||
|
||||
@app.middleware('http')
|
||||
async def auth_middleware(request: Request, call_next):
|
||||
"""Middleware to validate auth on all MCP endpoints."""
|
||||
# Check if this is an MCP request
|
||||
if request.url.path.startswith('/mcp'):
|
||||
if not validate_auth_header(request):
|
||||
raise HTTPException(status_code=401, detail='Unauthorized')
|
||||
return await call_next(request)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
print(f'Starting OAuth Protected MCP server on http://localhost:3001')
|
||||
print(f'Expected token: Bearer {VALID_TOKEN}')
|
||||
print(
|
||||
'This server requires authentication for both tool listing and calling.'
|
||||
)
|
||||
|
||||
# Run with streamable-http transport
|
||||
mcp.run(transport='streamable-http')
|
||||
@@ -0,0 +1,48 @@
|
||||
# Config-based Agent Sample - MCP Toolset with Notion MCP Server
|
||||
|
||||
This sample demonstrates how to configure an ADK agent to use the Notion MCP server for interacting with Notion pages and databases.
|
||||
|
||||
## Setup Instructions
|
||||
|
||||
### 1. Create a Notion Integration
|
||||
|
||||
1. Go to [Notion Integrations](https://www.notion.so/my-integrations)
|
||||
1. Click "New integration"
|
||||
1. Give it a name and select your workspace
|
||||
1. Copy the "Internal Integration Secret" (starts with `ntn_`)
|
||||
|
||||
For detailed setup instructions, see the [Notion MCP Server documentation](https://www.npmjs.com/package/@notionhq/notion-mcp-server).
|
||||
|
||||
### 2. Configure the Agent
|
||||
|
||||
Replace `<your_notion_token>` in `root_agent.yaml` with your actual Notion integration token:
|
||||
|
||||
```yaml
|
||||
env:
|
||||
OPENAPI_MCP_HEADERS: '{"Authorization": "Bearer secret_your_actual_token_here", "Notion-Version": "2022-06-28"}'
|
||||
```
|
||||
|
||||
### 3. Grant Integration Access
|
||||
|
||||
**Important**: After creating the integration, you must grant it access to specific pages and databases:
|
||||
|
||||
1. Go to `Access` tab in [Notion Integrations](https://www.notion.so/my-integrations) page
|
||||
1. Click "Edit access"
|
||||
1. Add pages or databases as needed
|
||||
|
||||
### 4. Run the Agent
|
||||
|
||||
Use the `adk web` to run the agent and interact with your Notion workspace.
|
||||
|
||||
## Example Queries
|
||||
|
||||
- "What can you do for me?"
|
||||
- "Search for 'project' in my pages"
|
||||
- "Create a new page called 'Meeting Notes'"
|
||||
- "List all my databases"
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
- If you get "Unauthorized" errors, check that your token is correct
|
||||
- If you get "Object not found" errors, ensure you've granted the integration access to the specific pages/databases
|
||||
- Make sure the Notion API version in the headers matches what the MCP server expects
|
||||
@@ -0,0 +1,30 @@
|
||||
# Copyright 2026 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
# yaml-language-server: $schema=https://raw.githubusercontent.com/google/adk-python/refs/heads/main/src/google/adk/agents/config_schemas/AgentConfig.json
|
||||
name: notion_agent
|
||||
model: gemini-2.5-flash
|
||||
instruction: |
|
||||
You are my workspace assistant. Use the provided tools to read, search, comment on, or create
|
||||
Notion pages. Ask clarifying questions when unsure.
|
||||
tools:
|
||||
- name: MCPToolset
|
||||
args:
|
||||
stdio_server_params:
|
||||
command: "npx"
|
||||
args:
|
||||
- "-y"
|
||||
- "@notionhq/notion-mcp-server"
|
||||
env:
|
||||
OPENAPI_MCP_HEADERS: '{"Authorization": "Bearer <your_notion_token>", "Notion-Version": "2022-06-28"}'
|
||||
Reference in New Issue
Block a user