返回

Who is actually using your Snowflake MCP server?

RSS Snowflake Engineering (Medium) 原文 · 未翻译 发布 2026-11-09 23:01 收录于 11-10

有效阅读仅保存在当前浏览器,用于「为你」推荐;暂停或清除记录。

来源摘要

How to see who sends queries through MCP, how many and how often, using one join in account usage Snowflake has no column that tells you which MCP server a query came through. It does store the name of the OAuth security integration that every MCP client signs in with. Give each MCP server its own i

本文目录 15 节
  1. How to see who sends queries through MCP, how many and how often, using one join in account usage
  2. Two identical rows in query history
  3. The sign-in tells them apart
  4. What I tried before the integration
  5. One integration, or one per MCP server?
  6. Setting up the integration
  7. The model
  8. PAT and key-pair sign-ins
  9. Which columns matter, and what do they mean in practice?
  10. Which queries should I leave out?
  11. The Cortex Analyst tool
  12. Cortex Search
  13. Cost
  14. What can break the numbers later
  15. Where this leaves me

原文

How to see who sends queries through MCP, how many and how often, using one join in account usage

Snowflake has no column that tells you which MCP server a query came through. It does store the name of the OAuth security integration that every MCP client signs in with. Give each MCP server its own integration, join one column from query history to login history, and you have usage per server, user and day. This article shows how, and where the approach stops working.

Setting up a Snowflake-managed MCP server takes a few minutes. A few weeks later, the same client usually comes back with a different question: is anyone using it? Which users, how often, and is it worth it?

I get this question from clients regularly. The last one put it as a requirement: a model that contains only queries run by their reporting team through one specific MCP server, without the same people’s Snowsight work, and with user, role, timestamp and volume.

Two identical rows in query history

My first thought was that a filter on query history would do it. It won’t, because query history has no column that says “this came from MCP server X”, and I did not find one anywhere else in account usage either.

That client’s reporting team works in Snowsight every day and has recently started asking Claude as well. When an analyst writes select region, sum(revenue) from sales in a worksheet and later asks Claude the same question, Claude sends the same SQL through the MCP server. QUERY_HISTORY then holds two rows with the same text, the same user, the same role and, if the warehouse is shared, the same warehouse.

The sign-in tells them apart

An MCP client such as Claude Desktop, Claude Code or Cursor signs in through an OAuth security integration, while Snowsight uses SSO or a password. Snowflake stores the name of the integration with every login, and every query points to the login that opened its session, so all it takes is one join:

select q.query_id, q.start_time, q.user_name, q.role_name,
       l.authorizing_integration_name
from snowflake.account_usage.query_history q
join snowflake.account_usage.login_history l
  on l.event_id = q.authn_event_id;

QUERY_HISTORY.AUTHN_EVENT_ID is the id of the sign-in that authenticated the session. It matches LOGIN_HISTORY.EVENT_ID, and there you find AUTHORIZING_INTEGRATION_NAME. For the Snowsight query it is empty; for the MCP query it holds the name of the integration.

You may have seen older examples that go through SESSIONS (SESSIONS.LOGIN_EVENT_ID). That works too, and on my test account the two keys matched in 100% of rows over 30 days. Going straight through AUTHN_EVENT_ID saves a join and the extra latency of the SESSIONS view.

Everything else in this article builds on this join. The other columns help you clean up and split what the integration filter returns, but none of them can replace it.

What I tried before the integration

I went through the obvious candidates first, and each one broke for a different reason:

The query tag took me the longest. I tested setting my own tag: inside a single call, a script block can run ALTER SESSION SET QUERY_TAG = 'x', and the statements after it do carry x. The top-level statement keeps Snowflake's tag, though, the setting is gone by the next call (every call is a new session), and the client would have to write it every single time. I gave up on it and went back to the sign-in.

One integration, or one per MCP server?

The integration records how a client signed in. It knows nothing about which server the client then called.

An OAuth token from one integration works against any MCP server the role can use, so the mapping between integration and server is a convention you set up yourself, and it decides what your report can show.

  • One integration per MCP server: usage per server. More client IDs to hand out.
  • One shared integration: usage for the group as a whole. Inside it you can still separate the SQL tool from the agent tool (with AGENT_TYPE, see the columns section), but two servers with the same tools look the same.

Whichever you choose, give the client ID and secret only to MCP clients. A Python script that reuses them will be counted under the same name.

Setting up the integration

You need one OAuth security integration for your MCP clients. The same integration can serve Claude Desktop and Claude Code at once, with one redirect URI for each.

create security integration MCP_OAUTH_SALES
  type = oauth
  oauth_client = custom
  oauth_client_type = 'CONFIDENTIAL'                                 -- client ID + secret
  oauth_redirect_uri = 'https://claude.ai/api/mcp/auth_callback'      -- Claude Desktop and claude.ai
  oauth_alternate_redirect_uris = ('http://localhost:8766/callback')  -- Claude Code
  oauth_allow_non_tls_redirect_uri = true                            -- needed for http://localhost
  oauth_issue_refresh_tokens = true
  oauth_refresh_token_validity = 7776000                             -- 90 days
  oauth_use_secondary_roles = none                                   -- the session runs under one role
  allowed_roles_list = ('SALES_REPORTING')
  enabled = true;

select system$show_oauth_client_secrets('MCP_OAUTH_SALES');          -- client ID and secret

The parameters that matter:

  • The name. Every report will group by it, so pick it with the report in mind. MCP_OAUTH_SALES says more in a dashboard than OAUTH_3.
  • OAUTH_REDIRECT_URI: https://claude.ai/api/mcp/auth_callback for Claude Desktop and claude.ai. Claude Code listens on localhost, so it needs http://localhost:<port>/callback and OAUTH_ALLOW_NON_TLS_REDIRECT_URI = TRUE. I learned the hard way that an integration with only the localhost URI does not work from Claude Desktop.
  • OAUTH_CLIENT_TYPE = 'CONFIDENTIAL': client ID plus secret. Both Claude clients support it.
  • OAUTH_USE_SECONDARY_ROLES = NONE: the session gets exactly one role, so ROLE_NAME in query history tells you what the user could see. With IMPLICIT the session also gets all secondary roles and ROLE_NAME stops telling the full story; QUERY_HISTORY.SECONDARY_ROLE_STATS then helps, a small JSON with the number and ids of the secondary roles. In my data it is null for MCP sessions on an integration with NONE, and {"roleCount":7,…} for Snowsight with USE SECONDARY ROLES ALL. ALLOWED_ROLES_LIST only works with NONE.
  • ALLOWED_ROLES_LIST: which roles may sign in through this integration. ACCOUNTADMIN, ORGADMIN and SECURITYADMIN are blocked by default.
  • PRE_AUTHORIZED_ROLES_LIST (optional): skips the consent screen for these roles.

Then connect the clients. The MCP server URL is https://<account>.snowflakecomputing.com/api/v2/databases/<db>/schemas/<schema>/mcp-servers/<server>.

  • Claude Desktop: Settings → Connectors → Add custom connector. Paste the URL, then open Advanced settings and fill in the client ID and secret.
  • Claude Code:
claude mcp add --transport http --scope user \
  --client-id '<client_id>' --client-secret --callback-port 8766 \
  snowflake-sales 'https://<account>.snowflakecomputing.com/api/v2/databases/<db>/schemas/<schema>/mcp-servers/<server>'

--client-secret without a value asks for the secret in a hidden prompt, so it does not end up in your shell history. The callback port must match the redirect URI in the integration.

The model

This is the query detail: one row per query sent by MCP clients that signed in through the integration. The integration filter does the real work. The mcp_tool column and the LIST exclusion are explained further down.

select q.query_id,
       q.start_time,
       q.user_name,
       q.role_name,
       q.warehouse_name,
       q.query_type,
       q.execution_status,
       case when q.agent_type = 'CORTEX_LITE_AGENT'       then 'sql tool'
            when q.agent_type = 'CORTEX_AGENT'            then 'agent tool'
            when q.query_tag like 'cortex-analyst-%'      then 'analyst tool'
            else 'not an MCP tool' end         as mcp_tool,
       q.session_id                            as mcp_call_id
from snowflake.account_usage.query_history q
join snowflake.account_usage.login_history l
  on l.event_id = q.authn_event_id
where l.authorizing_integration_name = 'MCP_OAUTH_SALES'
  and not (q.query_type = 'LIST_FILES'
           and q.query_text like 'LIST snow://workspace/%/.snowflake/si/skills%');

Most people actually want the daily overview across all MCP integrations on the account:

with mcp as (
  select q.start_time, q.user_name, q.session_id, q.execution_status,
         l.authorizing_integration_name as integration,
         case when q.agent_type = 'CORTEX_LITE_AGENT'  then 'sql tool'
              when q.agent_type = 'CORTEX_AGENT'       then 'agent tool'
              when q.query_tag like 'cortex-analyst-%' then 'analyst tool'
              else 'not an MCP tool' end as mcp_tool
  from snowflake.account_usage.query_history q
  join snowflake.account_usage.login_history l on l.event_id = q.authn_event_id
  where l.authorizing_integration_name <> ''
    and l.reported_client_type = 'SQL_API'
    and q.start_time >= dateadd(day, -30, current_date())
    and not (q.query_type = 'LIST_FILES'
             and q.query_text like 'LIST snow://workspace/%/.snowflake/si/skills%')
)
select integration,
       start_time::date                        as usage_date,
       user_name,
       count(distinct iff(mcp_tool <> 'not an MCP tool', session_id, null)) as mcp_calls,
       count_if(mcp_tool = 'sql tool')         as sql_tool_queries,
       count_if(mcp_tool = 'agent tool')       as agent_tool_queries,
       count_if(mcp_tool = 'analyst tool')     as analyst_tool_queries,
       count_if(mcp_tool = 'not an MCP tool')  as non_tool_queries,
       count_if(execution_status <> 'SUCCESS') as failed
from mcp
group by all
order by usage_date desc, integration, mcp_calls desc;

One number surprised me. For the people on my account who use MCP, it carries only 6 to 14% of their queries in a typical week, and the rest still goes through Snowsight. “Is anyone using it” is easy to answer; the share of the work tells you more about whether MCP has changed how people work. You get it from the same join: the share of a user’s queries whose sign-in has an MCP integration name.

PAT and key-pair sign-ins

A login with a programmatic access token (PAT) or a key pair has no integration name, so this approach does not see it as MCP traffic.

The Snowflake documentation recommends OAuth for MCP and mentions PATs as an alternative. Community guides show Claude Code connecting with a PAT in an Authorization: Bearer header. In both cases AUTHORIZING_INTEGRATION_NAME stays empty.

You can still find such traffic. AGENT_TYPE marks queries run by MCP tools (and by Cortex Code and Snowflake Intelligence, so look at the client and the user as well). I expect that to hold whatever the sign-in method, but I have only seen it with OAuth. For a PAT, LOGIN_HISTORY.FIRST_AUTHENTICATION_FACTOR_ID joins to ACCOUNT_USAGE.CREDENTIALS.CREDENTIAL_ID, which gives you the token's name. A PAT works from any tool, though, so its name only means something if people stick to a naming convention. A key pair only tells you the user.

This query shows which sign-in methods carry MCP traffic on your account today:

select l.first_authentication_factor                as auth_method,
       nullif(l.authorizing_integration_name, '')   as oauth_integration,
       c.name                                       as credential_name,
       q.agent_type,
       count(*)                                     as queries,
       count(distinct q.user_name)                  as users
from snowflake.account_usage.query_history q
join snowflake.account_usage.login_history l on l.event_id = q.authn_event_id
left join snowflake.account_usage.credentials c
  on c.credential_id::string = l.first_authentication_factor_id
where q.agent_type in ('CORTEX_LITE_AGENT', 'CORTEX_AGENT')
  and l.reported_client_type = 'SQL_API'
  and q.start_time >= dateadd(day, -30, current_timestamp())
group by all
order by queries desc;

If everything comes back as OAUTH_ACCESS_TOKEN with an integration name, you are fine. If not, you know who to talk to.

If you only need the setup, you can stop here. The rest is for whoever builds the model: the columns that help, the queries to leave out, and how the other MCP tools show up.

Which columns matter, and what do they mean in practice?

A few notes the table does not have room for:

  • AGENT_TYPE is the most useful column after the integration name, and easy to overlook. The documentation defines it as "the type of agent that directly invoked the query" and lists three values: CORTEX_AGENT (a persistent, named Cortex Agent), CORTEX_LITE_AGENT (a stateless, on-demand agent called via the REST API or a Cortex Code client) and EXTERNAL_AGENT (an external agent, for example a custom OAuth integration with IS_AGENTIC = TRUE). In my data CORTEX_LITE_AGENT marks queries from the MCP SQL tool (SYSTEM_EXECUTE_SQL) and CORTEX_AGENT marks SQL generated by an agent tool (CORTEX_AGENT_RUN). Cortex Code and Snowflake Intelligence use the same two values, so the column tells you which tool ran a query and only the integration tells you it came through MCP. I have not seen EXTERNAL_AGENT on my account.
  • Queries from an agent’s custom tool (a procedure) also get CORTEX_AGENT, but no query tag. An empty AGENT_TYPE under the MCP integration usually means the query did not come from an MCP tool: a dbt project run started through MCP, or another client signed in with the integration's client ID. The exception is the Cortex Analyst tool, which leaves it empty too (see the Analyst section).
  • REPORTED_CLIENT_TYPE shouldn't be read as the client's name. It only tells you which API was used. MCP shows up as SQL_API, and so does anything else that uses Snowflake's REST endpoints: Snowsight itself (through Snowflake's own SNOWFLAKE_UI integration), a script that calls the SQL API with a PAT, or an OAuth token without a named integration. I use it only next to the integration filter.
  • LOGIN_HISTORY rows are not calls. I was surprised by this one: the integration behind my claude.ai connector has 6,300 sign-ins in 90 days with no query at all, against 230 sign-ins that ran queries. Some are probably tool listings and reconnects, but I have not traced them. Use LOGIN_HISTORY for the integration name, and count calls from QUERY_HISTORY sessions.
  • QUERY_TAG looks like the obvious marker and only half works. The MCP runtime sets it on the top-level statement of every call: cortex-agent-<uuid> for the SQL tool and the agent tool, cortex-analyst-<request_id> for the Analyst tool. That identifies one call, but it says nothing about the MCP server, and the same cortex-agent- prefix also appears on agents in Snowsight, in Cortex Code and on the REST API. In my last 90 days, 258 of 3,044 tagged queries were outside the MCP integrations. What the tag does give you is a call id: the uuid of an agent-tool call equals REQUEST_ID in CORTEX_AGENT_USAGE_HISTORY, and the id in the Analyst tag is the request id in CORTEX_ANALYST_REQUESTS.
  • SESSION_ID: every MCP tool call opens a new session. If you want "how often", count distinct sessions.

Which queries should I leave out?

The integration filter does most of the cleaning. There is one query to exclude explicitly, and a few cases that look suspicious but should stay.

  • LIST snow://workspace/…/.snowflake/si/skills. Snowflake runs this before MCP calls, under the user's account, through the integration. Nobody asked for it, and if you leave it in, it roughly doubles the query count. Exclude only this path. A LIST @my_stage that the user ran on purpose is real work.
  • Snowsight file operations (PUT 'file://viewedFiles.json', GET 'snow://workspace/…', LIST ?, CALL SYSTEM$FBE_CAPTURE_FILE_REVISION_HISTORY). You will see hundreds of these in query history for the same user. They come from Snowsight without the integration, so the integration filter already drops them.
  • PUT and GET cannot succeed through the MCP SQL tool, which runs over the SQL API, and the SQL API rejects both. The attempt is still logged: a PUT_FILES row with status FAIL, the error Command not supported by SQL API: PUT and AGENT_TYPE = CORTEX_LITE_AGENT. Stages themselves are fine: LIST, REMOVE, reading files with SELECT … FROM @stage and unloading data with COPY INTO @stage all go through. What MCP cannot do is move a file between your laptop and a stage. So a successful PUT under the MCP integration name means someone signed in with its client ID in a driver-based client, such as a Python script with the Snowflake connector.
  • Queries inside a procedure that the user called through MCP. They run in the same session and fall under the integration, but they have no query tag. Keep them, they cost money. Just count usage in calls (mcp_call_id), because one CALL can produce dozens of rows. If you need to tie them to the call that started them, ACCESS_HISTORY has ROOT_QUERY_ID and PARENT_QUERY_ID, which QUERY_HISTORY does not.
  • dbt project runs started through MCP (EXECUTE DBT PROJECT). dbt sends its queries in its own sessions, four per run in my case, with an empty AGENT_TYPE and no tag. They use the same sign-in as the MCP call, so they still fall under the integration. They are real work, but leave their sessions out when you count tool calls.

The Cortex Analyst tool

An MCP server can also expose Cortex Analyst directly (CORTEX_ANALYST_MESSAGE, pointed at a semantic view). I added it as a temporary tool to my server, measured it, and removed it again. It behaves differently from the other two tools:

  1. It does not run the SQL. The tool returns the interpretation of the question and a SQL statement over SEMANTIC_VIEW(...), ending with a comment -- Generated by Cortex Analyst (request_id: …). No data. The client has to run the statement itself, normally through your SQL tool, where it shows up as a normal sql tool query. The comment with the request id survives in QUERY_TEXT, and it is the only link between the two.
  2. It leaves two rows in QUERY_HISTORY: two SELECT SYSTEM$VALIDATE_QUERY(?, TRUE) per call, each about 100 ms, with an empty AGENT_TYPE and the query tag cortex-analyst-<request_id> (the same request id as in the comment). Each call opens its own session under the MCP sign-in, so the integration filter finds it like any other call. If the client never runs the SQL, these two rows are all you get. Filtering on AGENT_TYPE alone would miss the call entirely, which is why the model above recognises the tool by its tag.
  3. Requests can be split by channel through the tag. CORTEX_ANALYST_USAGE_HISTORY only has user, hour, credits and a request count, so an MCP request and one from the Snowsight playground look the same there. The table function SNOWFLAKE.LOCAL.CORTEX_ANALYST_REQUESTS returns one row per request (user, role, question, generated SQL, model, latency) with a request_id. Its SOURCE column is empty for both channels, so once again the sign-in has to tell them apart:
select coalesce(nullif(l.authorizing_integration_name, ''), '(no integration)') as integration,
       count(*) as analyst_requests
from table(snowflake.local.cortex_analyst_requests('SEMANTIC_VIEW', '<db.schema.semantic_view>')) r
join (select distinct substr(query_tag, 16) as request_id, authn_event_id
      from snowflake.account_usage.query_history
      where query_tag like 'cortex-analyst-%') t
  on t.request_id = r.request_id
join snowflake.account_usage.login_history l
  on l.event_id = t.authn_event_id
group by 1;

In an hour with four requests, two in the playground and two through MCP, this query returned 2 and 2, which matched the 4 requests in the usage view.

Cortex Search

Search is the hardest to see, because there is no query to map to a sign-in. I ran a few searches against a Cortex Search service from Snowsight and then looked at what was left in account usage.

The search itself never reached QUERY_HISTORY. All I found were metadata queries (SHOW CORTEX SEARCH SERVICES, DESCRIBE CORTEX SEARCH SERVICE and a SYSTEM$UI_CORTEX_SEARCH_SERVICE_METADATA call), because the search runs inside the service and never becomes a SQL statement. The one exception is the SQL function: a call to SNOWFLAKE.CORTEX.SEARCH_PREVIEW(...) is an ordinary query and does appear, for example from a Cortex Code script. And the usage views, CORTEX_SEARCH_SERVING_USAGE_HISTORY and CORTEX_SEARCH_DAILY_USAGE_HISTORY, are per service, with no user, query id or channel.

So you can say what a search service costs per day, but not who used it or through which channel. I have not yet tested the MCP search tool (CORTEX_SEARCH_SERVICE_QUERY). I expect it to leave nothing in query history either, but I haven't measured it.

Cost

Cost is a topic of its own, with a lot of nuance: different tools, warehouse runtime, AI credits. I will not go into it here beyond one warning and a map of where the pieces are.

The warning: neither counting queries nor summing per-query credits gives you the cost. QUERY_ATTRIBUTION_HISTORY has compute credits per query, but it leaves out the time the warehouse keeps running until auto-suspend and the 60-second minimum Snowflake bills after every resume. MCP traffic is exactly the pattern where this hurts: a question, a pause while the user reads the answer, another question.

On our internal account, with a dedicated XS warehouse for the MCP SQL tool and auto-suspend at 60 seconds, the queries accounted for 3.6% of what the warehouse billed.

Where the cost pieces are, and how they connect back to the integration:

In CORTEX_AGENT_USAGE_HISTORY, agent calls made through MCP have metadata:interaction_interface = 'mcp', so at least the agent tool can be split by channel without the tag.

What can break the numbers later

  • CREATE OR REPLACE on the integration creates a new object. Plan for a new client ID and secret, so users have to reconnect (the documentation does not say this explicitly). History stores the name, so as long as the name stays the same, the report keeps working.
  • Renaming the integration or moving to a new one: old logins keep the old name, so you will be filtering on both.
  • CREATE OR REPLACE MCP SERVER is the only way to add or change a tool, because there is no ALTER. It drops the grants on the server, so run GRANT USAGE ON MCP SERVER again, and clients have to reconnect to see the new tool. Tracking is not affected, because it does not depend on the server.
  • Completeness. The documentation notes that short queries that run only against hybrid tables are not recorded, and that some other queries may be sampled. If your MCP tools mostly hit hybrid tables, expect the counts to run low. I have not measured how much.
  • Latency. QUERY_HISTORY lags by about 45 minutes, LOGIN_HISTORY by up to 2 hours. Build the model incrementally with a few hours of lookback.

Where this leaves me

Every MCP call goes to a URL with the server’s name in it, yet that name never reaches a view I can join to.

The whole approach is a workaround: I track the OAuth integration instead and keep the integration-to-server mapping in my head and in naming conventions. I hope the Snowflake team closes this gap and adds the MCP server to query history, or at least fills in the MCP_SERVER_EVENTS view or similar. Until then, if I set this up for a client tomorrow, I'd create one integration per MCP server from day one.

I’m Ondřej Blaha, Tech Lead at Revolt BI, a data consultancy that is now part of Joyful Craftsmen. Revolt BI builds data platforms on Snowflake and helps clients put AI agents and MCP into production. Everything above I tested on a Snowflake account we use internally, in October 2026.

Who is actually using your Snowflake MCP server? was originally published in Snowflake Builders Blog: Data Engineers, App Developers, AI, & Data Science on Medium, where people are continuing the conversation by highlighting and responding to this story.

这篇内容对你有用吗?

反馈只用于改善内容筛选,不等同于收藏

分享这条资讯
分享海报
保存图片
iOS 也可以长按图片保存