> ## Documentation Index
> Fetch the complete documentation index at: https://www.truefoundry.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Govern Snowflake Cortex agents calling MCP servers

> Route Snowflake Cortex agents through the TrueFoundry MCP Gateway so each tool call carries the signed-in user's identity, with passthrough or on-behalf-of.

Use this guide when you have **Snowflake Cortex agents** that need tools hosted outside Snowflake. Instead of pointing the agent at each MCP server directly, you point it at the TrueFoundry MCP Gateway. The gateway authenticates the caller, resolves it to a registered agent, applies your access policy, and either forwards the caller's token or exchanges it for one your MCP server accepts.

Your MCP server never has to trust a raw connector credential, and every call is attributable to a user and an agent in the audit trail.

> [!NOTE]
>
> Snowflake documents MCP connectors for Cortex Agents in [its own guide](https://docs.snowflake.com/en/user-guide/snowflake-cortex/cortex-agents-mcp-connectors). Check the feature's release status there before depending on it, because syntax and limits can change.

## What you are setting up

| Component | Who runs it | Its job in this setup |
| - | - | - |
| **Cortex agent** | Snowflake | Plans and calls tools through an MCP connector. |
| **MCP connector** | Snowflake | An API integration plus an external MCP server object that points at the gateway. |
| **Identity provider (IdP)** | You | Issues the token the connector presents. Okta and Microsoft Entra both work for sign-in and passthrough. The exchange in Part 2 is documented for Okta. |
| **MCP Gateway** | TrueFoundry | Validates the token, resolves the user and the agent, enforces policy, and forwards or exchanges the token. |
| **MCP server** | You | Validates the token it receives and serves tools. |

## Prerequisites

* A Snowflake account where you can create API integrations and external MCP servers, and run Cortex agents.
* An identity provider where you can create an OIDC application, registered in TrueFoundry. See [Identity Providers](/docs/platform/identity-providers).
* An MCP server that validates JWTs from the same IdP, or one you can place behind the exchange in Part 2. To build one, see [MCP server getting started](/docs/ai-gateway/mcp/mcp-server-getting-started).
* A gateway URL that Snowflake can reach over HTTPS with a valid public certificate and a hostname **without underscores**.

## Why the connector needs an identity provider

Snowflake's MCP connector authenticates with OAuth only: authorization code with a client secret, a public client (no secret), or dynamic client registration. It cannot send a static API key or header. The gateway validates tokens but does not issue them for this flow, so your IdP does the sign-in and issues the token, and the gateway accepts that token from the IdP you registered.

That one design explains the rest of the page: the user signs in once at the IdP, Snowflake keeps the refresh token, and every tool call after that carries a token the gateway can verify.

## Choosing between passthrough and on-behalf-of

| | **Passthrough** | **On-behalf-of** |
| - | - | - |
| What the MCP server receives | The user's own token, unchanged | A new token minted for your server that still names the user |
| MCP server trusts | Your IdP | Your IdP's service authorization server |
| Audience of the token | The gateway | Your MCP server |
| The user's broad token reaches the server | Yes | No, it stays inside the gateway |
| Setup effort | Low | Higher: a second authorization server and a service app |
| Use when | Your MCP server already validates your IdP's tokens | The server must see "an agent acting for a user" and nothing wider |

For the gateway-side details of each mode, see [MCP Gateway authentication and security](/docs/ai-gateway/mcp/mcp-gateway-auth-security).

## Part 1: Passthrough

The signed-in user's token travels from the connector to your MCP server. The gateway checks it and decides whether the call may proceed, but it does not change the token.

![Passthrough flow: the user signs in once at the identity provider through the Snowflake MCP connector. The identity provider issues an access token that names the user in sub and the connector application in cid. The agent sends that token to the TrueFoundry MCP Gateway, which validates it against the identity provider, resolves the user and the agent, checks the grants, and forwards the same token unchanged to your MCP server.](https://mintcdn.com/truefoundry/Hhbl5tQ6rAvMlaGq/images/cortex-passthrough-flow-light.svg?fit=max\&auto=format\&n=Hhbl5tQ6rAvMlaGq\&q=85\&s=306036269625b4cdc57e76c305d582df)

**1. Create the connector application in your IdP**

Create an OIDC application of type **Web** with the authorization code grant and refresh tokens enabled. Set the redirect URI to the callback Snowflake documents for MCP connectors:

```text theme={"dark"}
https://identity.snowflake.com/oauth2/callback
```

Note the application's **client ID** and **client secret**, and the issuer's authorization and token endpoints. Allow the scopes your MCP server needs, plus `offline_access` so Snowflake can refresh the token.

> [!NOTE]
>
> If your account uses PrivateLink, the callback URL is different. Snowflake derives it from `SYSTEM$ALLOWLIST_PRIVATELINK` and appends `/oauth/complete-secret`.

**2. Register the IdP in TrueFoundry**

Add the IdP so the gateway can validate its tokens. Set `audiences` to the audience your IdP puts in tokens for the gateway, and map the token to an agent and a user:

```yaml theme={"dark"}
type: identity-provider
name: my-idp
provider: custom
config:
  type: jwt
  issuer: https://<your-idp>/oauth2/<authorization-server-id>
  jwks_uri: https://<your-idp>/oauth2/<authorization-server-id>/v1/keys
  audiences:
    - <gateway-audience>
resolve_token_to:
  agent:
    name_claim: cid
    acting_on_behalf_of_slug_claim: sub
```

`name_claim: cid` identifies the connector application, and `acting_on_behalf_of_slug_claim: sub` identifies the Snowflake user it acts for. See [Identity Providers](/docs/platform/identity-providers) for the issuer, key and audience fields.

**3. Register the Cortex agent in the Agent Registry**

Create an entry keyed to the connector application's client ID. The gateway reads `cid` out of the token and matches it against this entry:

```yaml theme={"dark"}
type: agent/remote
name: snowflake-connector
identity:
  type: identity-provider-backed
  identity_provider_mapping:
    - identity_provider: my-idp
      value: <connector-app-client-id>
proxy_via_gateway:
  enabled: false
```

`proxy_via_gateway.enabled: false` registers the agent for governance only: the gateway does not proxy calls to it. The registry entry creates no new credential. It is a governance record for the connector application your IdP already issues tokens to. See [Agent Registry](/docs/agent-platform/agent-governance/agent-registry#registering-an-agent).

**4. Register the MCP server with token passthrough**

```yaml theme={"dark"}
type: mcp-server/remote
name: my-mcp-server
url: https://<your-mcp-server>/mcp
auth_data:
  type: passthrough
```

Then grant the `snowflake-connector` agent the **MCP Server User** role on `my-mcp-server`. An agent with no grant is refused, not passed through.

> [!WARNING]
>
> Auth Overrides do not support token passthrough, so do not enable overrides on a server that uses it.

**5. Create the API integration in Snowflake**

```sql theme={"dark"}
CREATE API INTEGRATION my_tfy_gateway_int
  API_PROVIDER = external_mcp
  API_ALLOWED_PREFIXES = ('https://<gateway>/<tenant>/mcp')
  API_USER_AUTHENTICATION = (
    TYPE = OAUTH2
    OAUTH_CLIENT_ID = '<connector-app-client-id>'
    OAUTH_CLIENT_SECRET = '<connector-app-client-secret>'
    OAUTH_TOKEN_ENDPOINT = 'https://<your-idp>/oauth2/<authorization-server-id>/v1/token'
    OAUTH_AUTHORIZATION_ENDPOINT = 'https://<your-idp>/oauth2/<authorization-server-id>/v1/authorize'
    OAUTH_ALLOWED_SCOPES = ('<scope-your-mcp-server-needs>', 'offline_access')
    OAUTH_REFRESH_TOKEN_VALIDITY = 86400
  )
  ENABLED = TRUE;
```

> [!WARNING]
>
> `OAUTH_REFRESH_TOKEN_VALIDITY` defaults to `0`, which means the refresh token never expires. Always set it explicitly. Snowflake's minimum is 3600 seconds. Re-enabling an integration forces every user to sign in again.

`API_ALLOWED_PREFIXES` must be a prefix of the server URL you use in the next step. `CREATE OR REPLACE` is not supported for external MCP integrations, so change an existing one with `ALTER API INTEGRATION`, or drop and create it again.

**6. Point an external MCP server at the gateway**

```sql theme={"dark"}
CREATE OR REPLACE EXTERNAL MCP SERVER my_db.my_schema.my_tfy_mcp
  WITH DISPLAY_NAME = 'TrueFoundry MCP Gateway'
  URL = 'https://<gateway>/<tenant>/mcp/my-mcp-server/server'
  API_INTEGRATION = my_tfy_gateway_int;
```

Copy the exact URL from the server's **How To Use** tab in TrueFoundry. It is the source of truth, and it has the shape `https://<gateway>/<tenant>/mcp/<server-name>/server`. Register one external MCP server per gateway MCP server you want the agent to use.

**7. Give the Cortex agent the server**

Updating the specification replaces it, so include everything the agent already uses:

```sql theme={"dark"}
ALTER AGENT my_db.my_schema.my_agent MODIFY LIVE VERSION SET SPECIFICATION = $$
models:
  orchestration: auto
mcp_servers:
  - server_spec:
      name: "my_db.my_schema.my_tfy_mcp"
$$;
```

The role that runs the agent needs `USAGE` on the external MCP server and on the API integration.

**8. Connect and run**

Each user signs in once. In Snowsight, open the connector and choose **Connect**. To do the same from SQL, which you need before calling the agent over its REST API, run this in a Snowsight worksheet:

```sql theme={"dark"}
SELECT SYSTEM$START_USER_OAUTH_FLOW('my_tfy_gateway_int');
```

Open the link in the result and sign in at your IdP. Snowflake completes the exchange itself, stores the tokens, and shows **OAuth Flow Completed**. If the page instead shows a code and state without completing, run `SYSTEM$FINISH_OAUTH_FLOW('<query-string-from-the-redirect>')` in the same session.

> [!WARNING]
>
> Open the link exactly as Snowsight returns it. The same call made through another client can return a link on a different host name for your account, and that link answers `400 Bad Request`.

Run the agent and ask for something that needs the tool. In the gateway's **Request Traces** you should see the call attributed to `snowflake-connector`, acting for the Snowflake user.

### What your MCP server receives

Your server gets the user's access token exactly as the IdP issued it. A decoded token from a test run looks like this (your IdP's claim names and extras differ):

```json theme={"dark"}
{
  "iss": "https://<your-idp>/oauth2/<authorization-server-id>",
  "aud": "<gateway-audience>",
  "sub": "user@example.com",
  "cid": "<connector-app-client-id>",
  "scp": ["<scope-your-mcp-server-needs>", "offline_access"]
}
```

Validate the issuer and signature against your IdP's keys, check the audience, and require the scope your tools need.

> [!WARNING]
>
> The audience is the **gateway**, so your server must accept it. If your server only accepts tokens issued for its own audience, use on-behalf-of in Part 2 instead of loosening the check.

### What you can enforce

| Where | Control |
| - | - |
| **Your IdP** | Who can sign in to the connector application, token lifetime, MFA and Conditional Access. |
| **Snowflake** | Who can use the agent, the external MCP server and the integration. |
| **MCP Gateway** | Which agent may call which server and tool, guardrails, rate limits, and the audit trail. |
| **Your MCP server** | Its own authorization decisions from the user's claims. |

## Part 2: On-behalf-of

The gateway receives the same user token as in Part 1 but does not forward it. It exchanges it at your IdP, using [RFC 8693](https://www.rfc-editor.org/rfc/rfc8693.html) token exchange, for a fresh token whose audience is your MCP server. The user's broad token never leaves the gateway.

![On-behalf-of flow: the user signs in once at the identity provider through the Snowflake MCP connector, producing a token that names the user in sub and the connector application in cid. The agent sends it to the TrueFoundry MCP Gateway. The gateway validates it, then exchanges it at the identity provider using RFC 8693 token exchange as a service application. The identity provider returns a delegated token that still names the user but carries the service application and the audience of your MCP server, and the gateway forwards that token.](https://mintcdn.com/truefoundry/Hhbl5tQ6rAvMlaGq/images/cortex-obo-flow-light.svg?fit=max\&auto=format\&n=Hhbl5tQ6rAvMlaGq\&q=85\&s=fa7919a8b7e26bb9844c59915face070)

Part 2 builds on Part 1: complete steps 1 to 3 and 5 to 8, then replace step 4 with the steps below.

**1. Set up token exchange in your IdP**

You need a second authorization server for your MCP server's audience, a trusted-server link from the user authorization server to it, and a service application that is allowed the **Token Exchange** grant. For Okta, follow [Okta setup for On-Behalf-Of](/docs/ai-gateway/mcp/okta-app-setup). Note the service application's client ID and secret.

> [!NOTE]
>
> The gateway uses the inbound token as the `subject_token`, so the user token and the exchange target must come from the same IdP organization.

**2. Store the service application's secret**

Save the client secret as a TrueFoundry secret and reference it by its fully qualified name, so it never appears in a manifest. See [Manage secrets](/docs/manage-secrets).

**3. Register the MCP server with token exchange**

```yaml theme={"dark"}
type: mcp-server/remote
name: my-mcp-server
url: https://<your-mcp-server>/mcp
auth_data:
  type: oauth2
  grant_type: token_exchange
  provider: okta
  token_url: https://<your-idp>/oauth2/<service-authorization-server-id>/v1/token
  audience: service
  client_id: <service-app-client-id>
  client_secret: tfy-secret://<tenant>:<secret-group>:<secret-key>
  scopes:
    - <scope-your-mcp-server-needs>
```

Grant the `snowflake-connector` agent the **MCP Server User** role on `my-mcp-server`, exactly as in Part 1.

> [!WARNING]
>
> The gateway accepts `grant_type: token_exchange` only with `provider: okta`, and it rejects other providers with an error. If your IdP is not Okta, check your gateway version's documentation before relying on the exchange.

### What your MCP server receives

A token minted for your server, naming the user and the service application:

```json theme={"dark"}
{
  "iss": "https://<your-idp>/oauth2/<service-authorization-server-id>",
  "aud": "<your-mcp-server-audience>",
  "sub": "user@example.com",
  "cid": "<service-app-client-id>",
  "scp": ["<scope-your-mcp-server-needs>"]
}
```

The gateway runs the exchange for each caller and caches the exchanged token. The call is refused with `401` if there is no caller token to exchange.

> [!WARNING]
>
> In testing, a request whose token exchange failed was still forwarded to the MCP server **without an `Authorization` header**. Make your server reject unauthenticated requests, and watch Request Traces for calls that arrive with no token.

### What you can enforce

Everything in Part 1, plus:

| Where | Control |
| - | - |
| **Your IdP** | Which users the service application may exchange tokens for, scopes it may request, and the exchange policy. |
| **Your MCP server** | A token audience limited to the server, so a leaked token cannot be replayed elsewhere. |

## Comparison, limits, and scale

| | **Passthrough** | **On-behalf-of** |
| - | - | - |
| Token reaching your server | The user's token | An exchanged, narrower token |
| Extra IdP setup | None beyond the connector app | Second authorization server, service app, trust link |
| Per-call IdP traffic | None | One exchange, cached |
| Check on your side | Your server validates the gateway audience and rejects missing tokens | Your server rejects a missing token, because a failed exchange can forward none |

* The connector supports the MCP **tools** capability only. Resources, prompts and sampling are not available to Cortex agents.
* Each Snowflake user completes their own sign-in for each integration.
* Snowflake's MCP connector page does not list egress IP ranges. If your gateway sits behind an IP allowlist, confirm the current ranges with Snowflake before relying on it.
* Register one agent entry per connector application, not per Snowflake user. Users are identified by `sub`.

## Troubleshooting

| Symptom | Likely cause | Fix |
| - | - | - |
| The connector shows "Version negotiation failed" | Snowflake could not complete the MCP handshake with the URL | Send an unauthenticated MCP `initialize` request to the URL from outside your network. A working gateway answers `401`, not a redirect or an HTML page. |
| `400 Bad Request` on the sign-in link | The link was generated outside Snowsight and uses a different host name | Run `SYSTEM$START_USER_OAUTH_FLOW` in a Snowsight worksheet and open the link as returned. |
| Sign-in loops back to the connector | Redirect URI mismatch | Make the IdP application's redirect URI match the one Snowflake reports for your account. |
| `401` from the gateway after sign-in | The IdP is not registered, or `audiences` does not match the token's `aud` | Decode the token and compare `iss` and `aud` with the identity provider entry. |
| `403` or "not authorized to access MCP server" | The agent has no **MCP Server User** grant | Grant the `snowflake-connector` agent access to the server. |
| Connection problems with a hostname that contains underscores | Snowflake reports connection issues for hostnames with underscores | Use a hostname with hyphens only. |
| `API_ALLOWED_PREFIXES` error | The prefix is not a prefix of the server URL | Use the start of the URL you copied, up to and including `/mcp`, as the prefix. |
| Your MCP server returns `invalid_token` in passthrough | Your server expects its own audience | Accept the gateway audience, or switch to Part 2. |
| Calls reach your server with no token in Part 2 | The token exchange failed | Check the service application's Token Exchange grant, the trust link, and the secret reference. |


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.