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,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)"
|
||||
Reference in New Issue
Block a user