# Set Up Snowflake as an OpenAPI MCP Server

> For the complete documentation index, see [llms.txt](https://docs.redpanda.com/llms.txt). Component-specific: [agentic-data-plane-full.txt](https://docs.redpanda.com/agentic-data-plane-full.txt)

---
title: Set Up Snowflake as an OpenAPI MCP Server
latest-operator-version: v26.2.4
latest-console-tag: v3.12.0
latest-connect-version: 4.113.1
latest-redpanda-tag: v26.2.4
docname: managed/openapi-snowflake
page-component-name: agentic-data-plane
page-version: master
page-component-version: master
page-component-title: Agentic Data Plane
page-relative-src-path: managed/openapi-snowflake.adoc
page-edit-url: https://github.com/redpanda-data/adp-docs/edit/main/modules/connect/pages/managed/openapi-snowflake.adoc
description: Connect the Snowflake SQL API to your agents with the OpenAPI managed MCP server and a Snowflake programmatic access token.
page-topic-type: tutorial
personas: agent_builder, platform_engineer
learning-objective-1: Create a Snowflake programmatic access token and the authentication policy it requires
learning-objective-2: Configure an OpenAPI managed MCP server that runs SQL through the Snowflake SQL API
learning-objective-3: Verify Snowflake queries from the Inspector before you attach the server to an agent
page-git-created-date: "2026-09-17"
page-git-modified-date: "2026-10-05"
---

<!-- Source: https://docs.redpanda.com/agentic-data-plane/connect/managed/openapi-snowflake.md -->

This tutorial connects Snowflake to Redpanda Agentic Data Plane so an agent can run SQL against your warehouse. You authenticate with a Snowflake programmatic access token and expose the Snowflake SQL API as a [tool](https://docs.redpanda.com/agentic-data-plane/reference/glossary/#tool) with the OpenAPI managed MCP server.

After reading this page, you will be able to:

-   Create a Snowflake programmatic access token and the authentication policy it requires

-   Configure an OpenAPI managed MCP server that runs SQL through the Snowflake SQL API

-   Verify Snowflake queries from the Inspector before you attach the server to an agent


## [](#prerequisites)Prerequisites

-   A Snowflake account and a Snowflake user whose role can read the data you want agents to query. See [Account identifiers](https://docs.snowflake.com/en/user-guide/admin-account-identifier) in the Snowflake documentation.

-   A Snowflake administrator. Two statements in this tutorial need the `ACCOUNTADMIN` role, or a role that can create authentication policies and owns the user. See [CREATE AUTHENTICATION POLICY](https://docs.snowflake.com/en/sql-reference/sql/create-authentication-policy) in the Snowflake documentation for the privileges involved.

-   Permission to create MCP servers and secrets in Agentic Data Plane. See [Roles and Permissions Reference](https://docs.redpanda.com/agentic-data-plane/control/permissions-reference/).

-   Familiarity with the OpenAPI managed MCP server. See [OpenAPI Managed MCP Server](https://docs.redpanda.com/agentic-data-plane/connect/managed/openapi/).


## [](#what-youll-build)What you’ll build

An agent calls one tool, `executestatement`, which the server generates from the spec’s `executeStatement` operation. The OpenAPI managed MCP server turns that call into a `POST /api/v2/statements` request to the Snowflake SQL API. It authenticates with a programmatic access token from the Agentic Data Plane secret store and returns the result rows to the agent.

The token belongs to one Snowflake user and one role, so every query runs with that role’s privileges. If you need each end user to query Snowflake as themselves, see [Troubleshooting](#troubleshooting).

## [](#gather-your-snowflake-account-details)Gather your Snowflake account details

Run this statement in a Snowflake worksheet to collect the values you need throughout the tutorial:

```sql
SELECT
    CURRENT_ORGANIZATION_NAME() AS org_name,
    CURRENT_ACCOUNT_NAME()      AS account_name,
    CURRENT_USER()              AS username,
    CURRENT_ROLE()              AS role,
    CURRENT_WAREHOUSE()         AS warehouse;
```

The rest of this tutorial refers to these values as placeholders:

| Placeholder | Example | Description |
| --- | --- | --- |
| <org-name> | myorg | Your Snowflake organization name. |
| <account-name> | analytics | Your Snowflake account name. |
| <username> | agent_reader | The Snowflake user that owns the token. |
| <role> | analyst | A role granted to that user, with access to the data to expose. |
| <warehouse> | analytics_wh | The warehouse that runs the queries. |
| <database> | sales_db | The database to query. |
| <schema> | public | The schema to query. |
| <table> | orders | A table in that schema, used in the verification queries. |

Your Snowflake base URL combines the organization and account names:

```text
https://<org-name>-<account-name>.snowflakecomputing.com
```

For example, `[https://myorg-analytics.snowflakecomputing.com](https://myorg-analytics.snowflakecomputing.com)`.

> 📝 **NOTE**
>
> If your account name contains underscores, Snowflake also accepts the URL with hyphens in their place.

## [](#allow-token-authentication)Allow token authentication for the user

By default, Snowflake lets a user create and use programmatic access tokens only when that user is subject to a network policy. An authentication policy with `NETWORK_POLICY_EVALUATION = ENFORCED_NOT_REQUIRED` removes that requirement while still enforcing any network policy that does apply to the user.

These statements need the `ACCOUNTADMIN` role, or a role that has the `CREATE AUTHENTICATION POLICY` privilege on the schema and owns the user.

1.  Create the authentication policy in a schema of your choice:

    ```sql
    USE ROLE ACCOUNTADMIN;

    CREATE OR REPLACE AUTHENTICATION POLICY <database>.<schema>.openapi_pat_auth_policy
      PAT_POLICY = (
        NETWORK_POLICY_EVALUATION = ENFORCED_NOT_REQUIRED
      );
    ```

2.  Assign the policy to the user:

    ```sql
    ALTER USER <username> SET AUTHENTICATION POLICY <database>.<schema>.openapi_pat_auth_policy;
    ```

3.  (Optional) Grant the role access to the data, if it doesn’t have it already:

    ```sql
    GRANT USAGE ON DATABASE <database> TO ROLE <role>;
    GRANT USAGE ON SCHEMA <database>.<schema> TO ROLE <role>;
    GRANT SELECT ON ALL TABLES IN SCHEMA <database>.<schema> TO ROLE <role>;
    GRANT SELECT ON ALL VIEWS IN SCHEMA <database>.<schema> TO ROLE <role>;
    GRANT USAGE ON WAREHOUSE <warehouse> TO ROLE <role>;
    ```


> 💡 **TIP**
>
> If someone else administers Snowflake, send them the first two statements with your values filled in. They are the only required steps in this tutorial that need administrator privileges.

## [](#create-a-programmatic-access-token)Create a programmatic access token

Snowflake users can create tokens for themselves without extra privileges. Run this statement as `<username>`:

```sql
ALTER USER <username> ADD PROGRAMMATIC ACCESS TOKEN openapi_tool_token
  ROLE_RESTRICTION = '<role>'
  DAYS_TO_EXPIRY = 30
  COMMENT = 'Agentic Data Plane OpenAPI MCP server';
```

-   `ROLE_RESTRICTION` limits the token to one role. Snowflake requires it for service users and recommends it for everyone else.

-   `DAYS_TO_EXPIRY` defaults to 15 days. The maximum is 365 days unless an authentication policy sets a lower limit.


The output has two columns. `token_name` is the name you gave the token. `token_secret` is the token itself.

> ❗ **IMPORTANT**
>
> The token secret appears only in the output of this statement. No other SQL command returns it. Copy it now. You store it in the Agentic Data Plane secret store in the next section.

If you lose the secret, rotate the token to get a new one:

```sql
ALTER USER <username> ROTATE PROGRAMMATIC ACCESS TOKEN openapi_tool_token;
```

To confirm the token exists and see when it expires, list the current user’s tokens:

```sql
SHOW USER PROGRAMMATIC ACCESS TOKENS;
```

The `expires_at` and `status` columns tell you whether the token is still usable.

## [](#describe-the-sql-api-in-an-openapi-spec)Describe the SQL API in an OpenAPI spec

Save the following spec as `snowflake-sql-api.yaml`. Replace the placeholder in the `servers` block and the `default` values under `StatementRequest` with your account details.

The spec declares three operations. You filter the MCP server down to `executeStatement` in the next section, so the agent never calls the status or cancel endpoints with an empty statement handle. Keep the other two operations in the spec if you plan to support long-running queries later.

```yaml
openapi: "3.0.3"
info:
  title: Snowflake SQL API
  description: >
    Execute SQL against Snowflake with the SQL API.
  version: "1.0.0"

servers:
  - url: https://<org-name>-<account-name>.snowflakecomputing.com
    description: Snowflake account

paths:
  /api/v2/statements:
    post:
      operationId: executeStatement
      tags:
        - sql
      summary: Submit a SQL statement for execution
      description: >
        Executes a SQL statement against Snowflake and returns the result set.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/StatementRequest"
      parameters:
        - name: async
          in: query
          required: false
          schema:
            type: boolean
            default: false
          description: Set to true to execute asynchronously.
        - name: nullable
          in: query
          required: false
          schema:
            type: boolean
            default: true
          description: Set to false to return SQL NULL as the string "null".
        - name: requestId
          in: query
          required: false
          schema:
            type: string
            format: uuid
          description: Optional idempotency key (UUID).
      responses:
        "200":
          description: Statement executed successfully. Result set returned.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ResultSet"
        "202":
          description: Execution in progress. Use the statementHandle to poll.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/QueryStatus"
        "408":
          description: Statement execution timed out.
        "422":
          description: SQL compilation or execution error.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/QueryFailure"

  /api/v2/statements/{statementHandle}:
    get:
      operationId: getStatementStatus
      tags:
        - sql
      summary: Check status or fetch results of a previously submitted statement
      parameters:
        - name: statementHandle
          in: path
          required: true
          schema:
            type: string
          description: Handle returned by the POST call.
        - name: partition
          in: query
          required: false
          schema:
            type: integer
            default: 0
          description: Partition number for large result sets.
        - name: requestId
          in: query
          required: false
          schema:
            type: string
            format: uuid
      responses:
        "200":
          description: Statement completed. Result set returned.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ResultSet"
        "202":
          description: Execution still in progress.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/QueryStatus"

  /api/v2/statements/{statementHandle}/cancel:
    post:
      operationId: cancelStatement
      tags:
        - sql
      summary: Cancel a running statement
      parameters:
        - name: statementHandle
          in: path
          required: true
          schema:
            type: string
          description: Handle of the statement to cancel.
        - name: requestId
          in: query
          required: false
          schema:
            type: string
            format: uuid
      responses:
        "200":
          description: Statement cancelled successfully.
          content:
            application/json:
              schema:
                type: object
                properties:
                  code:
                    type: string
                  message:
                    type: string

components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      description: >
        A Snowflake programmatic access token, sent as a bearer token.

  schemas:
    StatementRequest:
      type: object
      required:
        - statement
      properties:
        statement:
          type: string
          description: The SQL statement to execute.
        warehouse:
          type: string
          description: Warehouse to use.
          default: "<warehouse>"
        database:
          type: string
          description: Database context.
          default: "<database>"
        schema:
          type: string
          description: Schema context.
          default: "<schema>"
        role:
          type: string
          description: Role for the session.
          default: "<role>"
        timeout:
          type: integer
          description: Statement timeout in seconds.
          default: 60
        parameters:
          type: object
          description: Optional session parameters.
          properties:
            TIMEZONE:
              type: string
              default: "UTC"
        bindings:
          type: object
          description: Optional bind variables for parameterized queries.
          additionalProperties:
            type: object
            properties:
              type:
                type: string
              value:
                type: string

    ResultSet:
      type: object
      properties:
        code:
          type: string
        message:
          type: string
        statementHandle:
          type: string
        statementStatusUrl:
          type: string
        sqlState:
          type: string
        createdOn:
          type: integer
          format: int64
        statementHandles:
          type: array
          items:
            type: string
          description: Present when multiple statements were submitted.
        resultSetMetaData:
          type: object
          properties:
            numRows:
              type: integer
            format:
              type: string
            rowType:
              type: array
              items:
                type: object
                properties:
                  name:
                    type: string
                  type:
                    type: string
                  nullable:
                    type: boolean
                  precision:
                    type: integer
                  scale:
                    type: integer
                  length:
                    type: integer
            partitionInfo:
              type: array
              items:
                type: object
                properties:
                  rowCount:
                    type: integer
                  uncompressedSize:
                    type: integer
                  compressedSize:
                    type: integer
        data:
          type: array
          items:
            type: array
            items:
              type: string
              nullable: true
          description: Row data as arrays of string values.

    QueryStatus:
      type: object
      properties:
        code:
          type: string
        message:
          type: string
        statementHandle:
          type: string
        statementStatusUrl:
          type: string

    QueryFailure:
      type: object
      properties:
        code:
          type: string
        message:
          type: string
        sqlState:
          type: string
        statementHandle:
          type: string

security:
  - bearerAuth: []
```

> 💡 **TIP**
>
> The `default` values in `StatementRequest` appear in the generated tool’s input schema so that agents can pick them up, but the MCP server sends only the fields the caller supplies. When a request omits `warehouse`, `database`, `schema`, or `role`, Snowflake falls back to the user’s `DEFAULT_WAREHOUSE`, `DEFAULT_NAMESPACE`, and `DEFAULT_ROLE`. Set those defaults on `<username>` in Snowflake as well, so a bare statement still runs.

## [](#create-the-mcp-server)Create the MCP server

1.  Open **MCP servers** in the sidebar.

2.  Click **Add MCP server**.

3.  Click the **OpenAPI** card in the marketplace picker.

4.  Replace the suggested name with a name such as `snowflake-sql`, and add an optional description.

5.  Turn off `Enable code mode`. It’s on by default for OpenAPI servers, and this tutorial calls the one tool directly.

6.  In the OpenAPI configuration form, enter the following values:

    | Field | Value | Notes |
    | --- | --- | --- |
    | Spec | Leave empty. | You paste the spec inline instead of hosting it. |
    | Spec Content | Paste the contents of snowflake-sql-api.yaml, or load the file. | The inline limit is 3 MiB. This spec is far smaller. |
    | Base URL | Leave empty. | The servers entry in the spec already names your account. |
    | Filter > Include Operations | executeStatement | Exposes only the execute endpoint. The filter matches the spec’s operationId, so use the original casing. Without this filter, the agent also sees the getstatementstatus and cancelstatement tools and can call them with an empty statement handle. |
    | Max Schema Depth | Leave the default of 5. |  |
    | Extract Headers | Leave empty. | Snowflake returns paging information in the response body, under partitionInfo, not in response headers. |
    | Enabled under Queryable | Turn it off. | SQL tables come only from GET operations, and the operation filter leaves one POST, so SQL exposure adds no tools here. See Expose API data as SQL tables. |

7.  For `Auth`, select `Bearer`. For `Key Secret Ref`, select or create a secret that holds the `token_secret` value from Snowflake, for example `SNOWFLAKE_PAT`. Secret names use `UPPER_SNAKE_CASE`.

    The server sends the secret as `Authorization: Bearer <token>` on every request. Snowflake also accepts an optional `X-Snowflake-Authorization-Token-Type: PROGRAMMATIC_ACCESS_TOKEN` header, which Snowflake documents as optional for programmatic access tokens, so this tutorial does not send it.

8.  Click **Create server**.


## [](#verify-your-work)Verify your work

Test the tool in the Inspector before you attach the server to an agent.

1.  Open the server’s **Inspector** tab. The Tools panel lists one tool, `executestatement`. If it also lists `getstatementstatus` and `cancelstatement`, the operation filter is not applied.

2.  Click `executestatement`. In the `body` group, set `statement` to a query that needs no table access:

    ```sql
    SELECT CURRENT_USER(), CURRENT_ROLE(), CURRENT_WAREHOUSE();
    ```

3.  Run the tool. A successful call returns a result with `status_code` 200 and a `body` whose `data` array holds one row with your user, role, and warehouse. The `resultSetMetaData.rowType` array names the columns.

4.  Run a second query against your data, such as `SELECT COUNT(*) FROM <database>.<schema>.<table>;`, to confirm the role’s grants.


After the Inspector calls succeed, attach the server to an agent (see [Create an Agent](https://docs.redpanda.com/agentic-data-plane/connect/create-agent/)) and try prompts that increase in complexity:

-   "What tables are available in the `<schema>` schema?"

-   "How many rows does `<table>` have?"

-   "Show me the ten most recent rows in `<table>`, ordered by date."


See [Test an MCP Server’s Tools with the Inspector](https://docs.redpanda.com/agentic-data-plane/connect/test-tools/) for general Inspector usage.

## [](#clean-up)Clean up

When you finish the tutorial, or need to revoke access, remove the token and the authentication policy.

1.  As `<username>`, remove the token:

    ```sql
    ALTER USER <username> REMOVE PROGRAMMATIC ACCESS TOKEN openapi_tool_token;
    ```

2.  As `ACCOUNTADMIN`, detach the authentication policy from the user, then drop it. Snowflake refuses to drop a policy that is still assigned to a user or account.

    ```sql
    ALTER USER <username> UNSET AUTHENTICATION POLICY;
    DROP AUTHENTICATION POLICY IF EXISTS <database>.<schema>.openapi_pat_auth_policy;
    ```

3.  In Agentic Data Plane, delete the MCP server and the `SNOWFLAKE_PAT` secret.


## [](#troubleshooting)Troubleshooting

| Symptom | What to check |
| --- | --- |
| Creating the token, or the first tool call, fails with an error about a network policy being required | The authentication policy is not assigned to the user. Repeat Allow token authentication for the user, and confirm the ALTER USER …​ SET AUTHENTICATION POLICY statement ran against <username>. |
| The Inspector lists three tools, or a call fails because the statement handle is empty | The agent called getstatementstatus instead of executestatement. Set Include Operations to executeStatement so the server exposes only the execute endpoint. |
| Snowflake returns an authentication error for a token that used to work | The token expired, or someone rotated or removed it. Run SHOW USER PROGRAMMATIC ACCESS TOKENS; and check expires_at and status. Create a new token and update the SNOWFLAKE_PAT secret. If Snowflake rejects a token that is valid and unexpired, add X-Snowflake-Authorization-Token-Type as a required header parameter on the spec’s executeStatement operation, with PROGRAMMATIC_ACCESS_TOKEN as its only allowed value, so the agent sends it on every call. |
| The tool returns an error with status_code 422 | Snowflake could not compile or run the statement, or the role cannot access the referenced objects. The error carries Snowflake’s message and sqlState. Run the statement in a Snowflake worksheet as <role> first, and confirm the grants from Allow token authentication for the user. |
| The result is a QueryStatus object with a statementHandle instead of rows | The statement ran longer than 45 seconds, so Snowflake returned 202 and kept running it. To support long-running queries, add getStatementStatus to Include Operations and instruct the agent to poll the getstatementstatus tool with the returned handle. |
| You want each end user to query Snowflake as themselves | Snowflake’s OAuth for custom clients documents the authorization code and refresh token grants, not the client credentials grant, so Service Account OAuth does not work with Snowflake’s built-in OAuth. Configure a custom OAuth provider with Snowflake’s authorization endpoint (<snowflake-base-url>/oauth/authorize) and token endpoint (<snowflake-base-url>/oauth/token-request), and select User OAuth on the MCP server instead. See Configure User-Delegated OAuth. |

## [](#next-steps)Next steps

-   [OpenAPI Managed MCP Server](https://docs.redpanda.com/agentic-data-plane/connect/managed/openapi/)

-   [Create an Agent](https://docs.redpanda.com/agentic-data-plane/connect/create-agent/)

-   [Shape Tool Data with Data Policies](https://docs.redpanda.com/agentic-data-plane/connect/data-policies/)

-   [Configure User-Delegated OAuth](https://docs.redpanda.com/agentic-data-plane/connect/user-delegated-oauth/)