Enterprise AI needs to go beyond simple chat interfaces. To provide real value, AI agents must access your data warehouses to generate actionable insights, all while maintaining strict security and data governance.
This guide explores how to build a secure bridge between Google Cloud’s Gemini Enterprise and Snowflake. By using the Model Context Protocol (MCP) and the Google Agent Development Kit (ADK), you can empower your agent to act as an assistant that understands natural language, generates precise SQL via Snowflake Cortex and executes queries with user-level permissions.
Choosing the right integration pattern
Direct Connection vs. custom orchestration
The Google ADK architecture demonstrated in this post represents the advanced / custom orchestration pattern. It is designed for developers building complex, multi-agent systems on Google Cloud, such as workflows that combine Snowflake insights with external enterprise tools, custom reasoning loops, or cross-platform state management in Gemini Agent Engine.
For standard enterprise basic use cases where you want to connect Gemini Enterprise directly to Snowflake use the joint solutions mentioned in this link.
The architectural approach
This architecture allows a business user to ask questions like, "What were our top 5 selling items in Texas last quarter?"
Rather than exposing raw SQL execution to the client, the agent delegates both query generation and execution to a governed Snowflake Cortex Agent exposed through the Snowflake MCP server:
- Governed translation : The ADK agent passes the user's natural language question to the Snowflake MCP server, which invokes the Cortex Agent (CORTEX_AGENT_RUN) backed by Cortex Analyst semantic view.
- Direct execution within the governed boundary : The Cortex Agent translates the question into SQL and executes it directly inside Snowflake using the authenticated user's OAuth session and role privileges, returning the structured results to the ADK agent.
This approach provides deterministic text-to-SQL translation mapped to your corporate semantic definitions, keeps query execution inside Snowflake's governed boundary, and reduces the risk of schema hallucinations or unvalidated SQL execution.
System architecture flow
The diagram below illustrates the trust boundaries and the numbered flow of execution between the Gemini Enterprise app and Snowflake ecosystems.
Detailed execution sequence
To highlight how the agent delegates natural language query translation and governed execution to the Snowflake Cortex Agent over MCP, see the sequence diagram below:
Step 1: Setting up the Snowflake security integration
A major risk in enterprise AI is granting the model broad access via a generic service account, which could allow users to bypass security restrictions. We solve this by using OAuth 2.0 to perform user impersonation.
First, establish a confidential OAuth client in Snowflake to protect the client secret. The snippet below configures the security integration inside Snowflake:
- code_block
- <ListValue: [StructValue([('code', "USE ROLE ACCOUNTADMIN; \r\nCREATE OR REPLACE SECURITY INTEGRATION <SECURITY_INTEGRATION_NAME> \r\nTYPE = OAUTH \r\nENABLED = TRUE \r\nOAUTH_CLIENT = CUSTOM \r\nOAUTH_CLIENT_TYPE = 'CONFIDENTIAL' \r\n-- Google Cloud's official handshake receiver address: \r\nOAUTH_REDIRECT_URI = 'https://vertexaisearch.cloud.google.com/oauth-redirect' \r\nOAUTH_ISSUE_REFRESH_TOKENS = TRUE \r\n-- Let the connection stay alive for 90 days (max Snowflake allowed limit) \r\nOAUTH_REFRESH_TOKEN_VALIDITY = 7776000;"), ('language', ''), ('caption', <wagtail.rich_text.RichText object at 0x7fb00e55ce50>)])]>
To retrieve the generated client credentials needed for the Google Cloud configuration, run the following command in Snowflake:
- code_block
- <ListValue: [StructValue([('code', "-- Retrieve OAuth Client ID and Client Secret\r\nSELECT SYSTEM$SHOW_OAUTH_CLIENT_SECRETS('<SECURITY_INTEGRATION_NAME>');"), ('language', ''), ('caption', <wagtail.rich_text.RichText object at 0x7fb00d834a10>)])]>
Step 2: Data governance and access control
Apply the principle of least privilege by creating a dedicated role for your agent inside Snowflake.
- code_block
- <ListValue: [StructValue([('code', 'USE ROLE SECURITYADMIN;\r\n-- 1. Create a dedicated role\r\nCREATE ROLE IF NOT EXISTS <GENAI_ROLE_NAME>;\r\nGRANT ROLE <GENAI_ROLE_NAME> TO USER <BUSINESS_USER_NAME>; \r\nUSE ROLE SYSADMIN;\r\n-- 2. Grant structural access\r\nGRANT USAGE ON DATABASE <DATABASE_NAME> TO ROLE <GENAI_ROLE_NAME>;\r\nGRANT USAGE ON SCHEMA <DATABASE_NAME>.<SCHEMA_NAME> TO ROLE <GENAI_ROLE_NAME>;\r\n-- 3. Grant compute access\r\nGRANT USAGE ON WAREHOUSE <WAREHOUSE_NAME> TO ROLE <GENAI_ROLE_NAME>;\r\n-- 4 .Grant access to the semantic layer\r\nGRANT SELECT, REFERENCES ON SEMANTIC VIEW <DATABASE_NAME>.<SCHEMA_NAME>.<SEMANTIC_VIEW_NAME> TO ROLE <GENAI_ROLE_NAME>;\r\n-- 5. Grant select privileges on underlying raw data tables\r\nGRANT SELECT ON ALL TABLES IN SCHEMA <DATABASE_NAME>.<SCHEMA_NAME> TO ROLE <GENAI_ROLE_NAME>;'), ('language', ''), ('caption', <wagtail.rich_text.RichText object at 0x7faffefd9fd0>)])]>
IMPORTANT
-
The user context must have SELECT privileges on the physical tables backing the semantic view. Granting access only to the semantic view will cause runtime errors during query execution.
-
If granting direct access to base tables is not permitted by your security policy, place a Snowflake Secure View over the physical tables and build the Semantic View on top of it. Because Snowflake views execute withOwner's Rights , the user's GenAI role only needs SELECT privileges on the Secure View and Semantic View rather than the raw tables.
-
Snowflake OAuth tokens capture and bind to the user's DEFAULT_ROLE and DEFAULT_WAREHOUSE at the exact moment of authorization. If a user authorizes the connection before these defaults are configured, the session will be locked to the wrong role(role available at that time) and query execution will fail if the intended role will not have required privileges.
- Set defaults(as shown below) prior to authorizing in Gemini Enterprise.
- Authorize connection: Have the user perform the one-time OAuth authorization in Gemini Enterprise.
- (Optional) Revert defaults: Once authorized, the user can safely revert their default role and warehouse in Snowflake back to their standard operational preferences.
-
code_block
-
<ListValue: [StructValue([('code', "ALTER USER <BUSINESS_USER_NAME> SET DEFAULT_ROLE = '<GENAI_ROLE_NAME>';\r\nALTER USER <BUSINESS_USER_NAME> SET DEFAULT_WAREHOUSE = '<WAREHOUSE_NAME>';"), ('language', ''), ('caption', <wagtail.rich_text.RichText object at 0x7faffefd80d0>)])]>
Step 3: Provisioning the Cortex Agent and MCP server inside Snowflake
Next, create a governed Cortex Agent in Snowflake using your semantic view, then provision an MCP server that exposes the agent via the CORTEX_AGENT_RUN tool type. This approach delegates both SQL generation and query execution to Snowflake while enforcing the caller's OAuth identity and role permissions.
- code_block
- <ListValue: [StructValue([('code', 'USE DATABASE <DATABASE_NAME>;\r\nUSE SCHEMA <SCHEMA_NAME>;\r\nUSE ROLE SYSADMIN;\r\n\r\n-- 1. Create the Cortex Agent over your semantic view\r\nCREATE OR REPLACE AGENT <AGENT_NAME>\r\n COMMENT = 'Governed data assistant for business queries'\r\n PROFILE = '{"cortex_analyst": {"semantic_models": ["<DATABASE_NAME>.<SCHEMA_NAME>.<SEMANTIC_VIEW_NAME>"]}}';\r\n\r\n-- 2. Provision the MCP Server exposing the Cortex Agent\r\nCREATE OR REPLACE MCP SERVER <SERVER_NAME>\r\nFROM SPECIFICATION $$\r\ntools:\r\n - name: "run_cortex_agent"\r\n type: "CORTEX_AGENT_RUN"\r\n identifier: "<DATABASE_NAME>.<SCHEMA_NAME>.<AGENT_NAME>"\r\n title: "Cortex Agent Assistant"\r\n description: "Converts natural language questions into SQL and executes them directly within Snowflake using the caller's OAuth session permissions."\r\n$$;'), ('language', ''), ('caption', <wagtail.rich_text.RichText object at 0x7faffefdae90>)])]>
ISOLATION & GOVERNANCE NOTE: By using CORTEX_AGENT_RUN, query generation and execution remain fully contained within Snowflake's governed security boundary. Avoid exposing raw execution tools like SYSTEM_EXECUTE_SQL to external clients, as doing so introduces unnecessary security surface area and bypasses centralized Cortex Agent governance.
Step 4: Locating and constructing the MCP server URL
The ADK code requires the endpoint URL of your Snowflake-managed MCP server. Now that the server is provisioned, you must first grant access to the server, and then you can retrieve its exact endpoint URL using the following query in a Snowflake worksheet:
- code_block
- <ListValue: [StructValue([('code', '-- 1. Grant access to the Cortex Agent and the MCP server\r\nUSE ROLE SECURITYADMIN;\r\nGRANT USAGE ON AGENT <DATABASE_NAME>.<SCHEMA_NAME>.<AGENT_NAME> TO ROLE <GENAI_ROLE_NAME>;\r\nGRANT USAGE ON MCP SERVER <DATABASE_NAME>.<SCHEMA_NAME>.<SERVER_NAME> TO ROLE <GENAI_ROLE_NAME>;\r\n-- 2. View the MCP URL\r\nDESCRIBE MCP SERVER <DATABASE_NAME>.<SCHEMA_NAME>.<SERVER_NAME>;\r\n-- Or:\r\nDESC MCP SERVER <DATABASE_NAME>.<SCHEMA_NAME>.<SERVER_NAME>;'), ('language', ''), ('caption', <wagtail.rich_text.RichText object at 0x7faffefda490>)])]>
In the command output, inspect the metadata properties to locate the endpoint link. The URL is structured as follows:
- code_block
- <ListValue: [StructValue([('code', 'https://<org_name>-<account_name>.snowflakecomputing.com/api/v2/databases/<database_name>/schemas/<schema_name>/mcp-servers/<server_name>'), ('language', ''), ('caption', <wagtail.rich_text.RichText object at 0x7faffefd8bd0>)])]>
Step 5: Connecting via Google ADK
With the MCP Server created and its URL resolved, write your agent code using the Google Agent Development Kit (ADK). In your Python code, define the unique identifier AUTH_ID (e.g. snowflake_oauth_conn) that the agent will use to extract user credentials.
- code_block
- <ListValue: [StructValue([('code', 'import os\r\nimport re\r\nfrom typing import Optional\r\nfrom google.adk.agents.llm_agent import LlmAgent\r\nfrom google.adk.tools.mcp_tool.mcp_toolset import McpToolset\r\nfrom google.adk.tools.mcp_tool.mcp_session_manager import StreamableHTTPConnectionParams\r\nfrom google.adk.tools.tool_context import ToolContext\r\n\r\n# Define the Auth ID first in your code. \r\n# You will use this exact name when registering the agent in the Google Cloud Console.\r\n# Gemini Enterprise maps the authorization token to this identifier in the ToolContext.\r\nAUTH_ID = "snowflake_oauth_conn" \r\n\r\n# 1. OAuth Header Extraction:\r\n# Define a callback to dynamically extract the user's OAuth token injected by Gemini Enterprise \r\n# and generate the Authorization headers required to authenticate with the Snowflake MCP.\r\ndef get_auth_headers(tool_context: Optional[ToolContext] = None) -> dict: \r\n """ \r\n OAuth Handling Callback: Dynamically extracts the user's OAuth token injected by Gemini Enterprise and generates the headers required to authenticate with the Snowflake MCP. \r\n """ \r\n token = None \r\n if tool_context and hasattr(tool_context, "state"): \r\n try: \r\n # Parse the state dictionary injected by Gemini Enterprise containing authorization states \r\n state_dict = tool_context.state.to_dict() if hasattr(tool_context.state, 'to_dict') else tool_context.state \r\n pattern = re.compile(fr".{re.escape(AUTH_ID)}.", re.IGNORECASE) \r\n # Find the active token matching our Auth ID (snowflake_oauth_conn)\r\n matching_keys = [k for k in state_dict.keys() if pattern.match(k)] \r\n if matching_keys: \r\n token = state_dict.get(matching_keys[0]) \r\n except Exception as e: \r\n print(f"Warning: Failed to read ToolContext OAuth state: {e}") \r\n # Return authorization headers. If token is None, requests to MCP will fail.\r\n return { \r\n "Authorization": f"Bearer {token}", \r\n "Content-Type": "application/json", \r\n "Accept": "application/json, text/event-stream", \r\n } \r\n\r\n# 2. MCP Initialization:\r\n# Define the Snowflake MCP URL retrieved from 'DESCRIBE MCP SERVER' directly in the code:\r\nMCP_SERVER_URL = "<YOUR_MCP_SERVER_URL>" # Replace with the URL retrieved from 'DESCRIBE MCP SERVER' \r\n\r\n# Initialize the Snowflake MCP Toolset using this URL.\r\nsnowflake_mcp = McpToolset( \r\n connection_params=StreamableHTTPConnectionParams( \r\n url=MCP_SERVER_URL \r\n ), \r\n header_provider=get_auth_headers\r\n) \r\n\r\n# 3. Build the Agent:\r\n# Construct the LlmAgent using the Gemini 2.5 Flash model and attach the Snowflake MCP Toolset.\r\nroot_agent = LlmAgent( \r\n model="gemini-2.5-flash", \r\n name="enterprise_data_analyst", \r\n instruction=( \r\n "You are an enterprise data analyst. Use the
enterprise_data_agenttool " \r\n "on the Snowflake MCP server to answer business questions. Pass the user's " \r\n "natural language question to the tool, which translates it into SQL and " \r\n "executes it directly within Snowflake's governed boundary, then present " \r\n "the results clearly to the user." \r\n ), \r\n tools=[snowflake_mcp]\r\n)'), ('language', ''), ('caption', <wagtail.rich_text.RichText object at 0x7faffefd8890>)])]>
NOTE: The Python snippet above represents a plain/vanilla version of the agent setup. Depending on your organization's specific requirements, you should add robustness on top of this template (e.g., custom error handling, structured schemas, custom prompts, configurations, and additional tool wrappers).
Step 6: Project structure and deployment
To deploy your ADK agent to Agent Runtime in Agent Platform, organize your files in the following directory layout:
- code_block
- <ListValue: [StructValue([('code', 'snowflake_agent/\r\n├── agent.py # The agent definition and MCP connection code (from Step 5)\r\n├── init.py # Package initializer exposing the agent\r\n├── .env # Environment configurations (Google Cloud Project, etc.)\r\n└── requirements.txt # List of project dependencies (google-adk, etc.)'), ('language', ''), ('caption', <wagtail.rich_text.RichText object at 0x7faffefd8090>)])]>
- File details
-
init.py: Exposes the root_agent object to the deployment CLI:
-
code_block
- <ListValue: [StructValue([('code', '# Snowflake ADK Agent package\r\nfrom .agent import root_agent'), ('language', ''), ('caption', <wagtail.rich_text.RichText object at 0x7faffefd8110>)])]>
- **.env:** Declares environment configuration:
-
code_block
-
<ListValue: [StructValue([('code', 'GOOGLE_CLOUD_PROJECT="<YOUR_GCP_PROJECT_ID>"\r\nGOOGLE_CLOUD_LOCATION="<LOCATION>"\r\nGOOGLE_GENAI_USE_VERTEXAI="TRUE"'), ('language', ''), ('caption', <wagtail.rich_text.RichText object at 0x7faffefd9c90>)])]>
-
requirements.txt: Specifies dependencies:
-
code_block
-
<ListValue: [StructValue([('code', 'google-adk[mcp]==2.3.0\r\ngoogle-genai==2.10.0\r\npydantic-settings==2.14.2\r\ngoogle-cloud-aiplatform[agent_engines]>=1.50.0'), ('language', ''), ('caption', <wagtail.rich_text.RichText object at 0x7faffefd9350>)])]>
2. Deploying via the CLI
Rather than running complex custom Python scripts or manual container builds, you can deploy using the official Google Agents CLI (agents-cli). The CLI packages your code, sets up the server, and handles the deployment pipeline:
- code_block
- <ListValue: [StructValue([('code', '# Deploy the agent to the Google Cloud Agent Runtime\r\nagents-cli deploy --deployment-target agent_runtime'), ('language', ''), ('caption', <wagtail.rich_text.RichText object at 0x7faffefd85d0>)])]>
The CLI automatically handles the deployment lifecycle. Once complete, it outputs the unique project resource path (e.g., projects/PROJECT_ID/locations/LOCATION/reasoningEngines/RESOURCE_ID) which you will use to register the agent in the next step.
TIP: For more information and advanced configuration options for ADK agent deployment, refer to:
Step 7: Registering the custom agent in Gemini Enterprise
Once the agent code is ready and deployed, register the agent in the Google Cloud Console. Here, you will link the agent configuration and map the OAuth parameters retrieved from your Snowflake Security Integration.
- Extracting the OAuth Endpoints from Snowflake
Run this query in Snowflake to extract the precise Token and Authorization endpoints:
-
code_block
-
<ListValue: [StructValue([('code', 'DESCRIBE SECURITY INTEGRATION <SECURITY_INTEGRATION_NAME>;'), ('language', ''), ('caption', <wagtail.rich_text.RichText object at 0x7faffefdad50>)])]> In the results, locate the values for the following properties:
-
OAUTH_AUTHORIZATION_ENDPOINT : Your Snowflake Authorization URI.
-
OAUTH_TOKEN_ENDPOINT : Your Snowflake Token URI.
2. Registration steps in Google Cloud console
- Go to the Gemini Enterprise page in the console.
- Select the application you want to register the agent with.
- In the left navigation, click Agents , then click**+ Add agent** .
- Select Add forCustom agent via Agent Platform . The Authorizations setup displays.
- Set up the Snowflake OAuth connection:
-
Click Add authorization .
-
Authorization name : Enter the exact ID you defined in your ADK code (e.g., snowflake_oauth_conn). This ensures the credentials map correctly to the code's AUTH_ID.
-
Client ID &Client secret : Input the credentials you retrieved from Snowflake using the SYSTEM$SHOW_OAUTH_CLIENT_SECRETS query.
-
Token URI : Input the exact token endpoint value extracted from DESCRIBE SECURITY INTEGRATION.
-
Authorization URI : Take the authorization endpoint value extracted from DESCRIBE SECURITY INTEGRATION and append ?response_type=code at the end (along with your client ID and scope query parameters, for example):
-
code_block
-
<ListValue: [StructValue([('code', 'https://<org_name>-<account_name>.snowflakecomputing.com/oauth/authorize?client_id=YOUR_CLIENT_ID&redirect_uri=https%3A%2F%2Fvertexaisearch.cloud.google.com%2Foauth-redirect&scope=session%3Arole-any&response_type=code'), ('language', ''), ('caption', <wagtail.rich_text.RichText object at 0x7faffefdbe10>)])]>
IMPORTANT
Ensure the Authorization URI ends with &response_type=code (or ?response_type=code if there are no other parameters). This parameter is required by Google Cloud to trigger the OAuth 2.0 authorization code flow.
- Click Done , then clickNext .
-
Configure the agent properties:
-
Click Create to complete registration.
Understanding identifier alignment
- ADK Code AUTH_ID <==> Google Cloud Authorization Name : The AUTH_ID in your Python script must exactly match the Authorization Name/ID you input during console registration. Because we write code first, we reuse the code-defined identifier during registration.
- Snowflake Security Integration Name (e.g. <SECURITY_INTEGRATION_NAME>) : This is purely a Snowflake-side database object name. It does not need to match the Google Cloud Authorization Name or the AUTH_ID in your ADK Python code.
Get started
By uniting the Gemini model’s reasoning capabilities, ADK's orchestration, and Snowflake's governed data layer, you can safely bring AI to your data. This architecture provides an agentic assistant that respects your security perimeters while delivering business value.