Announcing CIMD support for MCP Client registration
Learn more

Snowflake MCP vs API for AI Agents (2026)

TL;DR

  • The Snowflake-managed MCP server is Generally Available and is a schema-level object you create with CREATE MCP SERVER. It is not a hosted catalog someone else defines. You author the tool list yourself from five types: CORTEX_ANALYST_MESSAGE, CORTEX_SEARCH_SERVICE_QUERY, SYSTEM_EXECUTE_SQL, CORTEX_AGENT_RUN, and GENERIC for UDFs and stored procedures.
  • Auth is not the dividing line here. Both paths accept Snowflake OAuth, Programmatic Access Tokens, and the same bearer header scheme, because the MCP endpoint sits inside the Snowflake REST API surface. Snowflake recommends OAuth and does not support Dynamic Client Registration on the MCP server.
  • The real MCP ceiling is operational: a hard cap of 50 tools per server, 250 KB response truncation on SQL execution and generic tools, non-streaming responses only, no secondary roles in OAuth sessions, and MCP server objects are not replicated in failover groups.
  • Snowflake OAuth access tokens expire in 600 seconds. Ten minutes. A long-running analytical agent will cross that boundary mid-run, and single-use refresh tokens invalidate every prior token on rotation, which turns naive concurrent refresh into a race condition.
  • Scalekit's Snowflake connector handles the per-user OAuth flow, vaulted token storage, and proactive refresh against that 600-second window for both paths, so the MCP versus API decision does not change your credential infrastructure.

Your agent needs to query the warehouse. It has to find the right table, generate SQL a data engineer would not be embarrassed by, run it under the right role, and come back with numbers someone will put in a board deck. Snowflake ships a managed MCP server and a full REST API surface, and unlike most tools in this comparison, both live at the same /api/v2 endpoint prefix on your account URL. That symmetry makes the choice less obvious than it looks. Here is how to pick.

What Snowflake MCP and the Snowflake API Actually Are

Most MCP-versus-API comparisons pit a vendor-defined tool catalog against a raw REST surface. Snowflake does not fit that shape. Both objects below are Snowflake REST resources, addressed on the same host, authenticated the same way. What differs is who authors the contract and what the contract can carry.

The Snowflake-managed MCP server

The Snowflake-managed MCP server is Generally Available and supports MCP revision 2025-11-25. It is not available in government regions. Snowflake hosts and scales it; you define it as a first-class object inside a database and schema, and it is served at https://<account_url>/api/v2/databases/{database}/schemas/{schema}/mcp-servers/{name}.

You author the tool surface in a YAML specification:

CREATE MCP SERVER revenue_analytics_mcp FROM SPECIFICATION $$ tools: - name: "revenue-semantic-view" type: "CORTEX_ANALYST_MESSAGE" identifier: "analytics.finance.revenue_semantic_view" description: "Semantic view for all revenue tables" title: "Revenue Analyst" - title: "SQL Execution Tool" name: "sql_exec_tool" type: "SYSTEM_EXECUTE_SQL" description: "Execute SQL queries against the connected Snowflake database." config: read_only: true query_timeout: 600 warehouse: "ANALYTICS_WH" $$;

USAGE on the MCP server lets a client connect and discover tools. Invoking a tool requires a separate grant on the underlying object: USAGE on the Cortex Search Service, SELECT on the semantic view, USAGE on the Cortex Agent or the UDF. Server access does not imply tool access.

The Snowflake REST API surface

The Snowflake REST API covers control-plane resources including Databases, Schemas, Tables, Warehouses, Tasks, Dynamic Tables, Grants, Roles, Stages, and Users, plus the Cortex family. The SQL API at /api/v2/statements executes standard queries and most DDL and DML, synchronously or asynchronously, returning results in partitions Snowflake sizes for you.

Alongside it sit the purpose-built AI endpoints: Cortex Analyst for text-to-SQL, Cortex Search at cortex-search-services/{name}:query, Cortex Inference at /api/v2/cortex/v1/chat/completions, and the Cortex Agents Run API at agent:run, which streams by default and supports background runs up to six hours.

Authentication accepts KEYPAIR_JWT, OAUTH, and PROGRAMMATIC_ACCESS_TOKEN, selected with the optional X-Snowflake-Authorization-Token-Type header.

Comparing Them Where It Matters for Agents

Four dimensions decide this for a production agent: what the surface can do, how you authenticate, what you own operationally, and which shape of workload each one fits. Capability first, because for Snowflake the gap is narrower and stranger than you would expect.

What your agent can actually do

The MCP server covers the analytical read path well. Everything structural, streaming, or large lives on the REST side.

Capability
Snowflake-managed MCP
Snowflake REST API
Natural language to SQL
Yes, CORTEX_ANALYST_MESSAGE, semantic views only
Yes, semantic views and semantic model files
Semantic search over unstructured data
Yes, CORTEX_SEARCH_SERVICE_QUERY
Yes, cortex-search-services/{name}:query
Cortex Agent orchestration
Yes, CORTEX_AGENT_RUN
Yes, agent:run, streaming and background runs
Arbitrary SQL execution
Yes, SYSTEM_EXECUTE_SQL
Yes, /api/v2/statements, sync and async
UDFs and stored procedures as tools
Yes, GENERIC
Yes, via SQL API or procedure endpoints
Object management: warehouses, tasks, roles, grants, stages, users
No
Yes, dedicated /api/v2 resource endpoints
Cortex Inference chat and messages endpoints
No
Yes, /api/v2/cortex/v1/chat/completions
Streaming responses
No, non-streaming only
Yes, on Analyst, Agents, and Inference
Long-running execution
No
Yes, background agent runs up to 6 hours
Large result sets
No, truncated at 250 KB
Yes, partitioned result fetch
Tools available in one endpoint
50 maximum, all types combined
Not applicable
Cross-region failover replication
No, server objects are not replicated
Yes, standard object replication

Where the MCP ceiling actually sits

The limits are quantitative, not categorical. Fifty tools per server, counting Search, Analyst, Agents, SQL execution, and generic tools together. Responses from SYSTEM_EXECUTE_SQL and GENERIC tools truncate at 250 KB, which a single wide SELECT will blow through. Non-streaming only, so a two-minute Cortex Analyst response arrives as silence and then a wall of JSON.

The MCP server also drops most of the protocol: no resources, prompts, roots, notifications, version negotiation, or sampling. Tools only.

The tool surface is yours to author

This inverts the usual tradeoff and it is the most useful thing to internalize about Snowflake MCP. With Slack or Salesforce, the vendor decides what the MCP tools are and you accept the gap. With Snowflake, a poorly performing agent is frequently your specification, not Snowflake's ceiling.

Snowflake says it directly in the limitations: higher tool counts degrade tool-selection accuracy. The read_only flag on SYSTEM_EXECUTE_SQL is the single highest-leverage line in most specifications, and one Cortex Analyst tool per semantic domain beats one tool over everything. To understand more about how MCP and APIs differ structurally, the tradeoffs go deeper than just tool count.

The Auth Path Each One Puts You On

For most tools in this series, MCP forces an interactive OAuth flow and the API offers a headless escape hatch. Snowflake does not work that way, which changes what you should actually worry about.

Both paths accept the same three credential types

Snowflake recommends OAuth 2.0 for the MCP server and documents CREATE SECURITY INTEGRATION with OAUTH_CLIENT = CUSTOM as the setup path. Dynamic Client Registration is not supported, so the client ID and secret are provisioned once and shared across all users in the account. Each user still authenticates individually to obtain their own access token.

Programmatic Access Tokens work as a bearer credential against the MCP endpoint too. Snowflake's own getting-started guide posts tools/list with Authorization: Bearer <PAT>, and Snowflake's guidance is to scope any PAT to the least-privileged role that can reach the server, because the token is a secret carrying that role.

The ten-minute access token

This is the number that decides your architecture. Snowflake OAuth access tokens return expires_in: 600. Refresh token validity is configurable through OAUTH_REFRESH_TOKEN_VALIDITY and can extend to 90 days, but the access token is short by design.

An analytical agent that discovers schema, generates SQL, waits on a warehouse, and fetches three result partitions will cross a ten-minute boundary regularly. Waiting for a 401 is not a strategy at that cadence; it is a guaranteed mid-run failure on any query that takes real time. The operational challenge of token refresh for AI agents is well-documented and Snowflake's 600-second window makes it acute.

The role problem in multi-tenant agents

Secondary roles are not supported in Snowflake OAuth sessions. The session runs as the connecting user's DEFAULT_ROLE, and Snowflake's own recommendation for differentiated access is separate agents with dedicated roles rather than role switching.

Two operational consequences follow. Every user needs DEFAULT_ROLE and DEFAULT_WAREHOUSE set, because a null default warehouse fails session initialization outright. And ACCOUNTADMIN, ORGADMIN, GLOBALORGADMIN, and SECURITYADMIN are blocked from OAuth authentication by default, which is correct and worth leaving alone.

What You Own in Production

Snowflake manages more of the MCP path than most vendors do, because the server is an object in your account rather than a service in theirs. The ownership split still matters, and it is not where you would guess.

On the MCP path

Snowflake owns hosting, scaling, and protocol compliance. You own the specification, which means you own tool-selection accuracy. You own per-user token storage and proactive refresh against the 600-second window. You own the grant matrix, since server access and tool access are separate privileges.

You also own disaster recovery for the server definition. MCP server objects are not replicated in failover groups, so a secondary account needs them recreated. OAuth security integrations do replicate.

On the REST API path

You own the full surface: request construction, partition pagination on large result sets, 429 handling with jittered backoff, statement handles for async execution, and adapter code for each endpoint family you touch.

You gain explicit control in return. Async execution with async=true returns a statement handle you poll on your own schedule. Background Cortex Agent runs survive six hours. Partitioned fetch means a 40 GB scan is a pagination problem rather than a truncated response.

Network policies and the client-origin problem

This one surprises teams in the first week. When a remote MCP client such as Claude, ChatGPT, or Cursor connects to your MCP server, the request originates from the client provider's infrastructure, not from the end user's browser. If your account has network policies enabled, those outbound IP ranges must be allowlisted with an ingress network rule.

Underscores in hostnames also break MCP connections. Use hyphens in the account URL.

When to Use MCP, When to Use the API

Both lists below assume a production agent with real users, not a prototype. The split follows workload shape rather than capability envy.

Use the Snowflake-managed MCP server when

  • Your agent is interactive and analytical: an assistant answering revenue questions through Cortex Analyst over a curated semantic view, where the user is present and browser-based OAuth consent is natural
  • You want RBAC, row access policies, and column masking enforced by Snowflake on every tool call rather than reimplemented in your application layer
  • You are exposing a governed subset of the warehouse to Claude, Cursor, or ChatGPT and want a tool list your data team can review in a pull request
  • You are combining Cortex Search over documents with Cortex Analyst over tables and want one endpoint instead of two API clients

Use the Snowflake REST API when

  • Your agent runs headless on a schedule: overnight metric refreshes, anomaly sweeps, or pipeline health checks with no user in the loop
  • You need object management, because warehouses, tasks, roles, grants, and stages have no MCP tool type
  • Your results are large, since 250 KB truncation is a hard stop and partitioned fetch on /api/v2/statements is not
  • You need streaming responses or background Cortex Agent runs beyond the fifteen-minute default request timeout
  • You are running high query volume and need explicit 429 backoff, idempotent retries with request_id, and async statement handles

The Credential Problem That Exists on Both Paths

Snowflake enforces identity correctly. Every MCP tool call and every SQL API statement runs under the authorizing user's role, with row access policies and masking applied. What Snowflake does not do is manage the credential lifecycle for your application.

The N-credential math

Take an analytics agent serving 60 analysts across 5 Snowflake accounts. That is 60 OAuth grants to store encrypted, 60 access tokens to refresh before each 600-second expiry, and 60 grants to revoke at offboarding. A ten-minute token refreshed reactively across 60 users generates a steady stream of mid-query 401s. This is the core challenge that secure token management for AI agents at scale must solve, regardless of which Snowflake path you choose.

The token type differs between the two paths. The infrastructure required does not.

The single-use refresh race

Snowflake supports single-use refresh tokens, where a successful refresh invalidates all previous refresh tokens and access tokens for that grant. Security-wise this is the right default. Operationally it converts naive concurrent refresh into a correctness bug.

Two agent threads refresh the same user's token simultaneously. One wins. The other now holds a token Snowflake has already invalidated, and every subsequent call for that user fails until someone reauthorizes. Distributed locking around refresh is not optional at this token lifetime. The question of who holds the token across agent tool-calling patterns is precisely why credential infrastructure cannot be an afterthought.

Where Scalekit fits

Scalekit's Snowflake connector resolves the per-user credential server-side on every tool call, refreshes proactively against the 600-second window, and serializes refresh so concurrent agent threads do not invalidate each other. Credentials never touch the agent runtime or the LLM context.

A second connector, Snowflake Key Pair Auth, covers the key-pair JWT path for service-identity workloads. Both expose the same fourteen tools built on the SQL API, so the auth model changes without the agent code changing.

Building a Snowflake Agent with Scalekit

The connection is configured once per environment and reused for every user. What follows is the discovery, scope, and execution sequence with the Snowflake connector.

Configure the connection

In the Scalekit dashboard, go to AgentKit, Connections, Create Connection, pick Snowflake, and copy the redirect URI. Then create the matching security integration in Snowsight:

CREATE OR REPLACE SECURITY INTEGRATION scalekit_oauth TYPE = OAUTH OAUTH_CLIENT = CUSTOM OAUTH_CLIENT_TYPE = 'CONFIDENTIAL' OAUTH_REDIRECT_URI = '<redirect_uri_copied_from_scalekit>' ENABLED = TRUE; SELECT SYSTEM$SHOW_OAUTH_CLIENT_SECRETS('SCALEKIT_OAUTH');

Paste OAUTH_CLIENT_ID and one of the returned client secrets back into the Scalekit connection. The connection_name you set here must match every connection_name string in your code exactly. This mismatch is the single most common integration error.

Authorize the user

get_or_create_connected_account returns the account if it exists and creates it if it does not. Surface the authorization link only when the status is not ACTIVE.

import os import scalekit.client scalekit_client = scalekit.client.ScalekitClient( client_id=os.getenv("SCALEKIT_CLIENT_ID"), client_secret=os.getenv("SCALEKIT_CLIENT_SECRET"), env_url=os.getenv("SCALEKIT_ENV_URL"), ) actions = scalekit_client.actions # connection_name must match the connection configured in the Scalekit dashboard account = actions.get_or_create_connected_account( connection_name="snowflake", identifier="user_123", ) if account.connected_account.status != "ACTIVE": link = actions.get_authorization_link( connection_name="snowflake", identifier="user_123", ) print("Authorize Snowflake:", link.link) input("Press Enter after authorizing...")

Retrieve the authorized tool surface

Before any tool runs, the agent loads the tools this user's connected account is authorized to call. This is not a connector catalog and it is not exploration. It is a scoped, deterministic surface derived from what the user granted.

from google.protobuf.json_format import MessageToDict scoped_response, _ = actions.tools.list_scoped_tools( identifier="user_123", filter={"connection_names": ["snowflake"]}, page_size=100, # fetch beyond the default page so no connector tools are missed ) for t in scoped_response.tools: print(MessageToDict(t.tool).get("definition", {}).get("name"))

The Snowflake connector exposes fourteen tools, including snowflake_execute_query, snowflake_show_databases_schemas, snowflake_get_tables, snowflake_get_columns, snowflake_get_query_status, and snowflake_get_query_partition. A schema-exploration agent needs four of them.

Run the loop with LangChain

actions.langchain.get_tools() returns native StructuredTool objects. Filtering by tool_names is what keeps the decision space small enough for reliable selection. This pattern mirrors what the LangChain tool calling integration enables more broadly across connectors.

from langchain_openai import ChatOpenAI from langchain_core.messages import HumanMessage, ToolMessage tools = actions.langchain.get_tools( identifier="user_123", connection_names=["snowflake"], tool_names=[ "snowflake_show_databases_schemas", "snowflake_get_tables", "snowflake_get_columns", "snowflake_execute_query", ], page_size=100, ) tool_map = {t.name: t for t in tools} llm = ChatOpenAI(model="gpt-4o").bind_tools(tools) messages = [HumanMessage("Weekly revenue by product line for the last 12 weeks")] while True: response = llm.invoke(messages) messages.append(response) if not response.tool_calls: print(response.content) break for tc in response.tool_calls: result = tool_map[tc["name"]].invoke(tc["args"]) messages.append(ToolMessage(content=str(result), tool_call_id=tc["id"]))

Run the same agent in TypeScript with the Claude SDK

listScopedTools returns schemas in input_schema form, which is exactly what Anthropic's tool use API expects. No conversion layer.

import { ScalekitClient } from '@scalekit-sdk/node'; import Anthropic from '@anthropic-ai/sdk'; const scalekit = new ScalekitClient( process.env.SCALEKIT_ENV_URL!, process.env.SCALEKIT_CLIENT_ID!, process.env.SCALEKIT_CLIENT_SECRET!, ); const anthropic = new Anthropic(); const { tools } = await scalekit.tools.listScopedTools('user_123', { filter: { connectionNames: ['snowflake'], toolNames: [ 'snowflake_show_databases_schemas', 'snowflake_get_tables', 'snowflake_get_columns', 'snowflake_execute_query', 'snowflake_get_query_status', ], }, pageSize: 100, }); const llmTools = tools.map(t => ({ name: t.tool.definition.name, description: t.tool.definition.description, input_schema: t.tool.definition.input_schema, })); const messages: Anthropic.MessageParam[] = [ { role: 'user', content: 'Which queries used the most credits today?' }, ]; while (true) { const response = await anthropic.messages.create({ model: 'claude-sonnet-4-6', max_tokens: 1024, tools: llmTools, messages, }); if (response.stop_reason === 'end_turn') { const text = response.content.find(b => b.type === 'text'); if (text?.type === 'text') console.log(text.text); break; } const toolResults: Anthropic.ToolResultBlockParam[] = []; for (const block of response.content) { if (block.type === 'tool_use') { const result = await scalekit.actions.executeTool({ toolName: block.name, identifier: 'user_123', toolInput: block.input as Record<string, unknown>, }); toolResults.push({ type: 'tool_result', tool_use_id: block.id, content: JSON.stringify(result.data), }); } } messages.push({ role: 'assistant', content: response.content }); messages.push({ role: 'user', content: toolResults }); }

Reach endpoints the tools do not cover

Cortex Analyst, Cortex Search, and agent:run are not in the prebuilt tool list. actions.request proxies any Snowflake REST path with the user's credential injected server-side, and resolves the account domain from the connected account so you never hardcode it.

response = actions.request( connection_name="snowflake", identifier="user_123", path="/api/v2/statements", method="POST", body={ "statement": "SELECT product_line, SUM(amount) FROM revenue.transactions GROUP BY 1", "warehouse": "ANALYTICS_WH", "timeout": 60, }, ) print(response.json())

Virtual MCP Servers for Multi-Tool, Multi-Tenant Agents

A warehouse agent is rarely only a warehouse agent. It queries Snowflake, then posts to Slack, then files a ticket. Scalekit's Virtual MCP servers give that agent one endpoint across all of it, with per-user credential isolation and no MCP server to deploy or maintain.

Why the standard surface is the wrong shape here

A Snowflake connection surfaces fourteen tools. A revenue summarizer needs four. Handing it all fourteen means snowflake_execute_query with write-capable SQL sits inside the blast radius of a read-only reporting job, and every unused schema burns context on every run.

The tool-bloat math is unforgiving. Forty tools at roughly 200 tokens each is 8,000 tokens before the agent does any work. Scoping to five or ten cuts that by around 80 percent and materially improves selection accuracy. The fix is not better prompting. It is surface reduction. For perspective on why MCP is significantly more expensive than CLI in token terms, the math applies directly to tool count discipline.

Create the server once per agent role

Define the server once, not once per user. The response carries a static mcp_server_url you reuse for every user and every session. Add a McpConfigConnectionToolMapping entry per connector to span multiple tools in one endpoint.

import os from scalekit import ScalekitClient from scalekit.actions.models.mcp_config import McpConfigConnectionToolMapping scalekit_client = ScalekitClient( env_url=os.environ["SCALEKIT_ENV_URL"], client_id=os.environ["SCALEKIT_CLIENT_ID"], client_secret=os.environ["SCALEKIT_CLIENT_SECRET"], ) vmcp_response = scalekit_client.actions.mcp.create_config( name="warehouse-reporting-agent", connection_tool_mappings=[ McpConfigConnectionToolMapping( connection_name="snowflake", tools=[ "snowflake_show_databases_schemas", "snowflake_get_tables", "snowflake_get_columns", "snowflake_execute_query", ], ), ], ) config_id = vmcp_response.config.id mcp_server_url = vmcp_response.config.mcp_server_url

Mint a session token before each run

Runtime is a token mint. Confirm the user's connections are still active, then issue a short-lived token bound to that specific user's connected accounts. Never reuse a token across sessions.

from datetime import timedelta accounts_response = scalekit_client.actions.mcp.list_mcp_connected_accounts( config_id=config_id, identifier="user_123", include_auth_link=True, ) for account in accounts_response.connected_accounts: if account.connected_account_status != "ACTIVE": print(f"{account.connection_name} needs auth: {account.authentication_link}") token_response = scalekit_client.actions.mcp.create_session_token( mcp_config_id=config_id, identifier="user_123", expiry=timedelta(minutes=30), ) mcp_server = { "url": mcp_server_url, "headers": {"Authorization": f"Bearer {token_response.token}"}, }

Set expiry longer than the expected run. The endpoint is static; the identity is per-user.

Auth Logs and Downstream Tool Call Observability

Snowflake's own history views answer what ran. They cannot answer which agent run triggered it, under which authorization grant, or whether that grant was still valid at execution time. That gap is where audits stall.

What the log has to answer

The question a security reviewer asks is never "did a query run." It is: which human authorized this agent, when did they consent, what scope did they grant, and was the credential valid at the moment of the call. Standard application logs capture a user ID and a timestamp, which is not enough when four principals are involved in a single action. Understanding audit trails for agent auth in B2B SaaS clarifies exactly what events must be captured and why Snowflake's query history alone is insufficient.

Scalekit's auth logs record authorization events, token lifecycle events, and every downstream tool call tied back to the connected account that authorized it, with 90 days of queryable history.

Why Snowflake's query history is not enough on its own

QUERY_HISTORY shows the user and the role. With per-user connected accounts that attribution is at least correct, which is more than a shared service account gives you. What it still cannot show is the agent run that produced the query, the tool the model selected, or the consent event behind the grant.

Correlate on the connected account and both halves line up: Snowflake tells you what executed against the data, Scalekit tells you which authorization made it permissible. For teams building production observability, agent tool observability requires correlating the LLM decision, the tool call, and the credential event in one place.

Which One to Build Against

The decision is workload shape, not capability envy, and for Snowflake it is unusually clean.

If your agent is interactive and analytical

Build against the Snowflake-managed MCP server. Curate a semantic view, expose one Cortex Analyst tool per domain, set read_only: true on SQL execution, and let Snowflake's RBAC and masking policies do the constraining. Keep the specification small on purpose.

If your agent is headless, operational, or high volume

Build against the REST API directly. Object management, streaming, background runs, and partitioned result fetch are not coming to the MCP tool types, and truncation at 250 KB is a hard wall you will hit on the first real result set.

What does not change either way

Sixty analysts is sixty credentials, refreshed against a 600-second window, serialized so concurrent runs do not invalidate each other, and revoked the day someone leaves. That is the part that needs production-grade infrastructure, and it is identical on both paths.

Talk to Other Snowflake Agent Builders

If you are wiring an agent into a warehouse and want to compare notes on semantic view design, tool scoping, or refresh strategy, join the Scalekit Slack community.

For a working session on a specific architecture, talk to an engineer.

Browse the Scalekit Snowflake connector: scalekit.com/connectors/snowflake

No items found.
Agent
Auth Quickstart
On this page
Share this article
Agent
Auth Quickstart

Acquire enterprise customers with
zero upfront cost.

Every feature unlocked. No hidden fees.