From c716ad3dd71934bd65f083d4d11682f6578c3560 Mon Sep 17 00:00:00 2001 From: mattsenicksigma Date: Thu, 24 Sep 2026 10:53:51 -0700 Subject: [PATCH 1/3] feat: add cortex_mcp_api_integration materialization Makes create_mcp_api_integration a first-class, config-only dbt model materialization instead of a run-operation, per #33: - New cortex_mcp_api_integration materialization (macros/materializations/cortex_mcp_api_integration.sql) plus its relation shim (macros/relations/cortex_mcp_api_integration/), mirroring the existing cortex_mcp_server pattern for account-level objects with no database/schema (tracked as a `view` node purely for graph/lineage; dbt never issues view DDL for it). - Defaults to `if_not_exists=true` (CREATE API INTEGRATION IF NOT EXISTS) rather than CREATE OR REPLACE, so a routine `dbt build` sweep (broad selector, state:modified.body, --full-refresh) can't silently rotate a live OAuth-authenticated integration as a side effect. Pass if_not_exists=false to opt into replace-in-place. - New cortex_mcp_api_integration_name(ref(...)) helper so a cortex_mcp_server model's api_integration config can depend on the integration via ref() instead of a bare name string that has to match something created out-of-band. - Extracted the DDL-building/validation logic out of the create_mcp_api_integration run-operation into a shared _mcp_api_integration_ddl macro, called by both the operation (unchanged behavior/defaults, kept for one-off admin bootstrapping outside dbt build) and the new materialization, so they can't drift on DDL shape or validation rules. - Fixed a latent bug in create_mcp_api_integration's dry_run branch: it only logged the DDL and never returned it, so any caller capturing the macro's output (e.g. the existing assert_create_mcp_api_integration_ddl integration test) always got an empty string. Found while validating this change against duckdb; unrelated to the materialization feature itself but trivial to fix in the same file. - Updated atlassian_mcp_server integration test fixture to wire in a new jira_mcp_api_integration model via ref(), and extended CI to compile both and run the DDL-builder assertion. - README updates: new cortex_mcp_api_integration section, config reference table, "How it works" and "Limitations & notes" entries. Co-Authored-By: Claude Sonnet 5 --- .github/workflows/integration-tests.yml | 6 +- README.md | 127 ++++++++++++-- integration_tests/README.md | 38 ++--- .../models/atlassian_mcp_server.sql | 2 +- .../models/jira_mcp_api_integration.sql | 10 ++ .../cortex_mcp_api_integration.sql | 92 ++++++++++ .../operations/create_mcp_api_integration.sql | 146 +++++++++++----- .../cortex_mcp_api_integration/create.sql | 160 ++++++++++++++++++ .../cortex_mcp_api_integration/drop.sql | 10 ++ .../cortex_mcp_api_integration/rename.sql | 15 ++ macros/relations/cortex_mcp_server/create.sql | 11 +- 11 files changed, 533 insertions(+), 84 deletions(-) create mode 100644 integration_tests/models/jira_mcp_api_integration.sql create mode 100644 macros/materializations/cortex_mcp_api_integration.sql create mode 100644 macros/relations/cortex_mcp_api_integration/create.sql create mode 100644 macros/relations/cortex_mcp_api_integration/drop.sql create mode 100644 macros/relations/cortex_mcp_api_integration/rename.sql diff --git a/.github/workflows/integration-tests.yml b/.github/workflows/integration-tests.yml index 1dcf99c..13f15b6 100644 --- a/.github/workflows/integration-tests.yml +++ b/.github/workflows/integration-tests.yml @@ -31,5 +31,9 @@ jobs: - name: Compile agent models working-directory: integration_tests - run: dbt compile --select "agent_*" --target duckdb + run: dbt compile --select "agent_*" "jira_mcp_api_integration" "atlassian_mcp_server" --target duckdb + + - name: Validate create_mcp_api_integration DDL builder + working-directory: integration_tests + run: dbt run-operation assert_create_mcp_api_integration_ddl --target duckdb diff --git a/README.md b/README.md index ecd73a3..2bea158 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ Currently, as Cortex Agents are only available on the Snowflake adapter, this pa ## At a glance -- **Materializations:** `cortex_agent`, `cortex_skill` +- **Materializations:** `cortex_agent`, `cortex_skill`, `cortex_mcp_server`, `cortex_mcp_api_integration` - **Warehouse:** Snowflake (Cortex Agents) - **dbt compatibility:** dbt 1.5+ - **Underlying DDL:** [`CREATE AGENT`](https://docs.snowflake.com/en/sql-reference/sql/create-agent) / `PUT 'file://...' @stage` @@ -375,16 +375,64 @@ object (`CREATE EXTERNAL MCP SERVER`) from a config-only dbt model. Once created the MCP server can be wired into a `cortex_agent` model with `ref()` so that the DAG enforces correct build order. -### Bootstrap: create the API integration first +### Create the API integration with `cortex_mcp_api_integration` (recommended) An External MCP Server references a Snowflake **API INTEGRATION** object that authenticates Snowflake's outbound calls to the MCP endpoint. API integrations are account-level objects that require **ACCOUNTADMIN** (or **CREATE INTEGRATION**) -privilege to create — they cannot be created by a typical dbt service account -during `dbt build`. +privilege to create — the same as any other privilege a `dbt build` role +needs on its own target objects, just scoped to the account instead of a schema. -Run the `create_mcp_api_integration` operation **once per MCP endpoint** before -`dbt build`, using an admin-privileged role: +The `cortex_mcp_api_integration` materialization makes the integration a +first-class dbt model, the same way `cortex_mcp_server` and `cortex_skill` +already are: it's a real DAG node, so a `cortex_mcp_server` model can `ref()` +it and `dbt build` enforces creation order automatically, and it shows up in +`dbt ls`/lineage graphs instead of being a config string that has to match an +object created out-of-band. + +`models/jira_mcp_api_integration.sql` — model body is empty, all parameters via `config()`: + +```sql +{{ + config( + materialized = 'cortex_mcp_api_integration', + allowed_prefixes = ['https://mcp.atlassian.com'], + auth_type = 'OAUTH_DYNAMIC_CLIENT', + oauth_resource_url = 'https://mcp.atlassian.com/v1/mcp' + ) +}} +``` + +For OAuth2 client credentials (providers without Dynamic Client Registration): + +```sql +{{ + config( + materialized = 'cortex_mcp_api_integration', + allowed_prefixes = ['https://api.example.com/mcp'], + auth_type = 'OAUTH2', + oauth_client_id = 'abc123', + oauth_client_secret = 's3cr3t', + oauth_token_endpoint = 'https://api.example.com/oauth/token', + oauth_authorization_endpoint = 'https://api.example.com/oauth/authorize' + ) +}} +``` + +The integration's Snowflake object name is the model's alias (same convention +`cortex_mcp_server` uses). `dbt build` creates it with **`CREATE API INTEGRATION +IF NOT EXISTS`** by default (`if_not_exists=true`) rather than `CREATE OR REPLACE` +— a broad selector, `state:modified.body` sweep, or `--full-refresh` shouldn't be +able to silently rotate a live OAuth-authenticated integration as a side effect +of an unrelated rebuild. Pass `if_not_exists=false` explicitly if you do want +replace-in-place semantics (e.g. to deliberately rotate configuration). + +### Alternative: `create_mcp_api_integration` operation + +If you'd rather bootstrap the integration manually outside of `dbt build` — +e.g. from a session that only holds ACCOUNTADMIN for the duration of the +bootstrap — the original run-operation still works and behaves exactly as +before (including its `if_not_exists=false` / `CREATE OR REPLACE` default): ```bash # Dynamic Client Registration (recommended for DCR-capable providers, e.g. Atlassian): @@ -419,13 +467,18 @@ dbt run-operation create_mcp_api_integration --args '{ }' ``` -If the API integration does not exist when `dbt build` runs, the materialization -will fail immediately with a clear error message that names the missing integration -and shows the bootstrap command to run. +Whichever path creates it, if the API integration does not exist when +`dbt build` reaches a `cortex_mcp_server` model, the materialization fails +immediately with a clear error message that names the missing integration and +shows both bootstrap options. ### Defining an MCP server model -The model body is empty — all parameters are supplied via `config()`: +The model body is empty — all parameters are supplied via `config()`. Use +`cortex_mcp_api_integration_name(ref(...))` to wire in an integration created +by the materialization above (registers the DAG dependency); pass a plain +string instead if the integration was created out-of-band via the +run-operation. `models/atlassian_mcp_server.sql`: @@ -435,7 +488,7 @@ The model body is empty — all parameters are supplied via `config()`: materialized = 'cortex_mcp_server', display_name = 'Atlassian (Jira & Confluence)', url = 'https://mcp.atlassian.com/v1/mcp', - api_integration = 'jira_mcp_api_integration' + api_integration = dbt_cortex_agent.cortex_mcp_api_integration_name(ref('jira_mcp_api_integration')) ) }} ``` @@ -469,7 +522,29 @@ mcp_servers: |-------------------|----------|--------|-------------| | `display_name` | Yes | string | Human-readable label shown in Snowflake. | | `url` | Yes | string | MCP server endpoint URL. | -| `api_integration` | Yes | string | Name of the pre-existing Snowflake API integration object. | +| `api_integration` | Yes | string | Name of the Snowflake API integration object — pass `dbt_cortex_agent.cortex_mcp_api_integration_name(ref('...'))` to wire a DAG dependency, or a plain string for an out-of-band integration. | + +### `cortex_mcp_api_integration` configuration reference + +| Config | Required | Type | Description | +|--------------------------------|-----------------------------|--------------|-------------| +| `allowed_prefixes` | Yes | list[string] | Base URL(s) of the MCP server, matched as a prefix. | +| `auth_type` | No (default `OAUTH_DYNAMIC_CLIENT`) | string | `OAUTH_DYNAMIC_CLIENT` or `OAUTH2`. | +| `oauth_resource_url` | Yes (OAUTH_DYNAMIC_CLIENT) | string | MCP server URL used for DCR. | +| `oauth_client_id` | Yes (OAUTH2) | string | OAuth2 client ID. | +| `oauth_client_secret` | Yes (OAUTH2) | string | OAuth2 client secret. | +| `oauth_token_endpoint` | Yes (OAUTH2) | string | OAuth2 token endpoint URL. | +| `oauth_authorization_endpoint` | Yes (OAUTH2) | string | OAuth2 authorization endpoint URL. | +| `oauth_client_auth_method` | No (OAUTH2 only) | string | `CLIENT_SECRET_BASIC` or `CLIENT_SECRET_POST`. | +| `oauth_discovery_url` | No (OAUTH2 only) | string | OIDC discovery URL. | +| `oauth_refresh_token_validity` | No (OAUTH2 only) | int | Refresh token validity in seconds. | +| `enabled` | No (default `true`) | bool | Whether the integration is enabled. | +| `if_not_exists` | No (default **`true`**) | bool | Use `IF NOT EXISTS` instead of `OR REPLACE`. Defaults opposite to the `create_mcp_api_integration` operation — see the note above on why. | +| `comment` | No | string | Optional `COMMENT` clause. | + +The integration's Snowflake object name is always the model's alias — there is +no `integration_name` config (unlike the operation below), since the model +identity already provides it. ### `create_mcp_api_integration` operation reference @@ -527,6 +602,20 @@ in the model's target database/schema with the model's `alias` as its name. statement for both specification and raw modes. - **Drop / rename** (`macros/relations/cortex_agent/{drop,rename}.sql`) — provide `drop agent if exists` and `alter agent ... rename to` DDL. +- **`cortex_mcp_api_integration` materialization** + (`macros/materializations/cortex_mcp_api_integration.sql`) — sets the query + tag, runs pre-hooks, issues a single `CREATE API INTEGRATION IF NOT EXISTS` + (or `CREATE OR REPLACE` with `if_not_exists=false`) statement, runs + post-hooks, and returns the relation. +- **Shared DDL builder** (`macros/operations/create_mcp_api_integration.sql`, + `_mcp_api_integration_ddl`) — validates arguments and constructs the + `CREATE API INTEGRATION` statement. Both the `cortex_mcp_api_integration` + materialization and the `create_mcp_api_integration` run-operation call this + same macro, so they can never drift on DDL shape or validation rules — only + on their own `if_not_exists` default and whether they execute immediately or + log/return. +- **Drop / rename** (`macros/relations/cortex_mcp_api_integration/{drop,rename}.sql`) — + provide `drop api integration if exists` and `alter api integration ... rename to` DDL. Every run issues `CREATE OR REPLACE AGENT`, which is idempotent and atomic, so re-running a model simply replaces the agent in place. @@ -549,6 +638,20 @@ re-running a model simply replaces the agent in place. (e.g. `CREATE AGENT` on the schema) and to reference any semantic views or Cortex Search services named in `tool_resources`. See the [Cortex Agents docs](https://docs.snowflake.com/en/user-guide/snowflake-cortex/cortex-agents-manage). +- **`cortex_mcp_api_integration` is account-level, like `cortex_mcp_server`.** + Snowflake API INTEGRATION objects have no database/schema, so (same as + `cortex_mcp_server`) the node is tracked internally as a `view` for + graph/lineage purposes only — dbt never issues `CREATE VIEW` for it. The + executing role needs **ACCOUNTADMIN** or **CREATE INTEGRATION** account-level + privilege, which is broader than what most other models in a project need; + scope which role runs this model accordingly (e.g. a separate `dbt build + --select cortex_mcp_api_integration:*` step under an elevated role, if your + normal service account shouldn't hold that privilege day-to-day). +- **`if_not_exists` default differs by entry point.** The + `cortex_mcp_api_integration` materialization defaults to `if_not_exists=true`; + the `create_mcp_api_integration` operation defaults to `if_not_exists=false` + (`CREATE OR REPLACE`), preserved for backward compatibility with existing + callers. See the config reference tables above. --- diff --git a/integration_tests/README.md b/integration_tests/README.md index 3b447e0..9756a87 100644 --- a/integration_tests/README.md +++ b/integration_tests/README.md @@ -13,7 +13,8 @@ against a live Snowflake account. | `agent_minimal` | cortex_agent | Spec mode, instructions only, with `comment` + `profile`. | | `agent_with_semantic_view` | cortex_agent | Spec mode, Analyst tool wired to `orders_semantic_view` via `ref()`. | | `agent_raw_ddl` | cortex_agent | `raw_ddl=true` pass-through mode. | -| `atlassian_mcp_server` | cortex_mcp_server | External MCP server for the Atlassian Jira/Confluence endpoint. | +| `jira_mcp_api_integration` | cortex_mcp_api_integration | API integration backing the Atlassian MCP endpoint. | +| `atlassian_mcp_server` | cortex_mcp_server | External MCP server for the Atlassian Jira/Confluence endpoint, wired to `jira_mcp_api_integration` via `ref()`. | | `agent_with_mcp_server` | cortex_agent | Agent wired to `atlassian_mcp_server` via `cortex_mcp_server_name(ref(...))`. | ## Prerequisites @@ -21,30 +22,17 @@ against a live Snowflake account. - Python 3.9+ - A Snowflake account/role with privileges to create agents, semantic views, tables, and to use Cortex. - -### Bootstrap: API integration for MCP servers - -The `atlassian_mcp_server` model requires an API integration to exist before -`dbt build` runs. API integrations are account-level objects that require -**ACCOUNTADMIN** (or **CREATE INTEGRATION**) privilege to create — they cannot -be created by a typical dbt service account during `dbt build`. - -Run this once with an admin-privileged role before building: - -```bash -dbt run-operation create_mcp_api_integration --target snowflake --args '{ - integration_name: jira_mcp_api_integration, - allowed_prefixes: ["https://mcp.atlassian.com"], - auth_type: OAUTH_DYNAMIC_CLIENT, - oauth_resource_url: "https://mcp.atlassian.com/v1/mcp" -}' -``` - -If your role does not have the required privilege, switch to ACCOUNTADMIN first -in your Snowflake session, or ask your account admin to run the operation. - -The materialization will fail with a clear error message pointing to this -bootstrap step if the integration does not exist when `dbt build` runs. +- **ACCOUNTADMIN or CREATE INTEGRATION** privilege on the role running + `dbt build` — `jira_mcp_api_integration` creates a Snowflake API INTEGRATION, + an account-level object, and needs this privilege the same as any other + model in this project needs privileges on its own target schema. There is + no separate manual bootstrap step; `dbt build` creates it in DAG order + before `atlassian_mcp_server` runs. + +If your role does not have this privilege, either switch to ACCOUNTADMIN for +the run or fall back to the `create_mcp_api_integration` run-operation from an +admin-privileged session before building (see the main README's +`cortex_mcp_api_integration` section for both paths). ## Run diff --git a/integration_tests/models/atlassian_mcp_server.sql b/integration_tests/models/atlassian_mcp_server.sql index d1f1d70..bc0e39f 100644 --- a/integration_tests/models/atlassian_mcp_server.sql +++ b/integration_tests/models/atlassian_mcp_server.sql @@ -4,7 +4,7 @@ meta = { 'display_name': 'Atlassian (Jira & Confluence)', 'url': 'https://mcp.atlassian.com/v1/mcp', - 'api_integration': 'jira_mcp_api_integration' + 'api_integration': dbt_cortex_agent.cortex_mcp_api_integration_name(ref('jira_mcp_api_integration')) } ) }} diff --git a/integration_tests/models/jira_mcp_api_integration.sql b/integration_tests/models/jira_mcp_api_integration.sql new file mode 100644 index 0000000..945e96d --- /dev/null +++ b/integration_tests/models/jira_mcp_api_integration.sql @@ -0,0 +1,10 @@ +{{ + config( + materialized = 'cortex_mcp_api_integration', + meta = { + 'allowed_prefixes': ['https://mcp.atlassian.com'], + 'auth_type': 'OAUTH_DYNAMIC_CLIENT', + 'oauth_resource_url': 'https://mcp.atlassian.com/v1/mcp' + } + ) +}} diff --git a/macros/materializations/cortex_mcp_api_integration.sql b/macros/materializations/cortex_mcp_api_integration.sql new file mode 100644 index 0000000..9f1d8bc --- /dev/null +++ b/macros/materializations/cortex_mcp_api_integration.sql @@ -0,0 +1,92 @@ +{#- +-- cortex_mcp_api_integration materialization +-- +-- Creates (or, if missing, creates — see if_not_exists note below) a +-- Snowflake API INTEGRATION for an external MCP server from a dbt model. +-- The model body is empty (config-only); all parameters are supplied via +-- config(). This is the account-level object a `cortex_mcp_server` model's +-- `api_integration` config points at. +-- +-- Required config: +-- allowed_prefixes list[string] Base URL(s) of the MCP server, matched as a prefix. +-- +-- Optional config: auth_type, oauth_resource_url, oauth_client_id, +-- oauth_client_secret, oauth_token_endpoint, oauth_authorization_endpoint, +-- oauth_client_auth_method, oauth_discovery_url, +-- oauth_refresh_token_validity, enabled, if_not_exists, comment. +-- +-- Example: +-- +-- {{ config( +-- materialized = 'cortex_mcp_api_integration', +-- allowed_prefixes = ['https://mcp.atlassian.com'], +-- auth_type = 'OAUTH_DYNAMIC_CLIENT', +-- oauth_resource_url = 'https://mcp.atlassian.com/v1/mcp' +-- ) }} +-- +-- To wire this integration into a cortex_mcp_server model, use: +-- +-- {{ config( +-- materialized = 'cortex_mcp_server', +-- display_name = 'Atlassian (Jira & Confluence)', +-- url = 'https://mcp.atlassian.com/v1/mcp', +-- api_integration = dbt_cortex_agent.cortex_mcp_api_integration_name(ref('jira_mcp_api_integration')) +-- ) }} +-- +-- Requires ACCOUNTADMIN or CREATE INTEGRATION account-level privilege on the +-- executing role — the same requirement as the `create_mcp_api_integration` +-- run-operation this materialization supersedes for `dbt build`-tracked use. +-- +-- Note on `if_not_exists`: this materialization defaults to `if_not_exists=true` +-- (not `CREATE OR REPLACE`) so that a routine `dbt build` sweep (a broad +-- selector, `state:modified.body`, `--full-refresh`) never silently rotates a +-- live OAuth-authenticated integration as a side effect. Pass +-- `if_not_exists=false` explicitly to opt into replace-in-place semantics. +-- +-- See macros/relations/cortex_mcp_api_integration/create.sql for the DDL +-- construction and the README for usage and config options. +-#} + +{% materialization cortex_mcp_api_integration, adapter='snowflake' -%} + + {% set original_query_tag = set_query_tag() %} + + {% do dbt_cortex_agent.snowflake__create_or_replace_cortex_mcp_api_integration() %} + + {#- + -- Snowflake API INTEGRATION objects are account-level (no database/schema), + -- so they don't fit dbt's Relation model any more naturally than + -- cortex_mcp_server's own EXTERNAL MCP SERVER does. We incorporate the + -- node as a `view` purely so that dbt can track it in the graph and + -- downstream `ref()`s resolve. dbt never issues view DDL for this node. + -#} + {% set target_relation = this.incorporate(type='view') %} + + {% do unset_query_tag(original_query_tag) %} + + {% do return({'relations': [target_relation]}) %} + +{%- endmaterialization %} + + +{#- +-- Default (non-Snowflake) stub materialization. +-- +-- Renders the CREATE API INTEGRATION DDL without executing it, so +-- `dbt compile` works on any adapter (e.g. DuckDB) and the compiled SQL is +-- inspectable in target/compiled/. +-#} +{% materialization cortex_mcp_api_integration, default -%} + + {%- set identifier = model['alias'] -%} + {%- set target_relation = api.Relation.create( + identifier=identifier, schema=schema, database=database, + type='view') -%} + + {% call statement('main') -%} + {{ dbt_cortex_agent.snowflake__get_create_cortex_mcp_api_integration_sql(target_relation) }} + {%- endcall %} + + {% do return({'relations': [target_relation]}) %} + +{%- endmaterialization %} diff --git a/macros/operations/create_mcp_api_integration.sql b/macros/operations/create_mcp_api_integration.sql index 0c2b588..c584383 100644 --- a/macros/operations/create_mcp_api_integration.sql +++ b/macros/operations/create_mcp_api_integration.sql @@ -1,4 +1,4 @@ -{% macro create_mcp_api_integration( +{% macro _mcp_api_integration_ddl( integration_name, allowed_prefixes, auth_type='OAUTH_DYNAMIC_CLIENT', @@ -12,64 +12,31 @@ oauth_refresh_token_validity=none, enabled=true, if_not_exists=false, - dry_run=false, comment=none ) %} {#- --- Bootstrap operation: creates a Snowflake API INTEGRATION for an external MCP server. --- --- Requires ACCOUNTADMIN or CREATE INTEGRATION account-level privilege. Run once --- per MCP endpoint before `dbt build`: --- --- -- Dynamic Client Registration (recommended for DCR-capable providers): --- dbt run-operation create_mcp_api_integration --args '{ --- integration_name: jira_mcp_api_integration, --- allowed_prefixes: ["https://mcp.atlassian.com"], --- auth_type: OAUTH_DYNAMIC_CLIENT, --- oauth_resource_url: "https://mcp.atlassian.com/v1/mcp" --- }' +-- Validates arguments and builds the `CREATE API INTEGRATION` DDL text for a +-- Snowflake external-MCP API integration. Pure string builder — does not +-- execute anything. Shared by the `create_mcp_api_integration` run-operation +-- and the `cortex_mcp_api_integration` materialization so both produce +-- identical DDL from the same validation rules. -- --- -- OAuth2 client credentials (for providers without DCR): --- dbt run-operation create_mcp_api_integration --args '{ --- integration_name: my_mcp_api_integration, --- allowed_prefixes: ["https://api.example.com/mcp"], --- auth_type: OAUTH2, --- oauth_client_id: "abc123", --- oauth_client_secret: "s3cr3t", --- oauth_token_endpoint: "https://api.example.com/oauth/token", --- oauth_authorization_endpoint: "https://api.example.com/oauth/authorize" --- }' --- --- Parameters: --- integration_name string Snowflake object name for the integration. --- allowed_prefixes list Base URL(s) of the MCP server (matched as prefix). --- auth_type string 'OAUTH_DYNAMIC_CLIENT' (default) or 'OAUTH2'. --- oauth_resource_url string Required for OAUTH_DYNAMIC_CLIENT. MCP server URL. --- oauth_client_id string Required for OAUTH2. --- oauth_client_secret string Required for OAUTH2. --- oauth_token_endpoint string Required for OAUTH2. --- oauth_authorization_endpoint string Required for OAUTH2. --- oauth_client_auth_method string Optional for OAUTH2: CLIENT_SECRET_BASIC | CLIENT_SECRET_POST. --- oauth_discovery_url string Optional for OAUTH2: OIDC discovery URL. --- oauth_refresh_token_validity int Optional for OAUTH2: refresh token validity (seconds). --- enabled bool ENABLED clause (default true). --- if_not_exists bool Use IF NOT EXISTS instead of OR REPLACE (default false). --- dry_run bool Log DDL without executing (default false). --- comment string Optional COMMENT clause. +-- See `create_mcp_api_integration` (this file) and +-- `macros/relations/cortex_mcp_api_integration/create.sql` for callers. -#} {%- set auth_type_upper = auth_type | upper -%} {%- if auth_type_upper not in ['OAUTH2', 'OAUTH_DYNAMIC_CLIENT'] -%} {{ exceptions.raise_compiler_error( - "create_mcp_api_integration: unsupported auth_type '" ~ auth_type ~ "'. " + "_mcp_api_integration_ddl: unsupported auth_type '" ~ auth_type ~ "'. " ~ "Valid values: 'OAUTH_DYNAMIC_CLIENT', 'OAUTH2'." ) }} {%- endif -%} {%- if auth_type_upper == 'OAUTH_DYNAMIC_CLIENT' and oauth_resource_url is none -%} {{ exceptions.raise_compiler_error( - "create_mcp_api_integration: 'oauth_resource_url' is required when auth_type='OAUTH_DYNAMIC_CLIENT'." + "_mcp_api_integration_ddl: 'oauth_resource_url' is required when auth_type='OAUTH_DYNAMIC_CLIENT'." ) }} {%- endif -%} @@ -77,7 +44,7 @@ {%- if oauth_client_id is none or oauth_client_secret is none or oauth_token_endpoint is none or oauth_authorization_endpoint is none -%} {{ exceptions.raise_compiler_error( - "create_mcp_api_integration: auth_type='OAUTH2' requires " + "_mcp_api_integration_ddl: auth_type='OAUTH2' requires " ~ "'oauth_client_id', 'oauth_client_secret', 'oauth_token_endpoint', " ~ "and 'oauth_authorization_endpoint'." ) }} @@ -120,8 +87,99 @@ create {% if if_not_exists %}api integration if not exists{% else %}or replace a {%- endif %} {%- endset -%} + {{ return(ddl) }} + +{% endmacro %} + + +{% macro create_mcp_api_integration( + integration_name, + allowed_prefixes, + auth_type='OAUTH_DYNAMIC_CLIENT', + oauth_resource_url=none, + oauth_client_id=none, + oauth_client_secret=none, + oauth_token_endpoint=none, + oauth_authorization_endpoint=none, + oauth_client_auth_method=none, + oauth_discovery_url=none, + oauth_refresh_token_validity=none, + enabled=true, + if_not_exists=false, + dry_run=false, + comment=none +) %} +{#- +-- Bootstrap operation: creates a Snowflake API INTEGRATION for an external MCP server. +-- +-- Requires ACCOUNTADMIN or CREATE INTEGRATION account-level privilege. Run once +-- per MCP endpoint before `dbt build`: +-- +-- -- Dynamic Client Registration (recommended for DCR-capable providers): +-- dbt run-operation create_mcp_api_integration --args '{ +-- integration_name: jira_mcp_api_integration, +-- allowed_prefixes: ["https://mcp.atlassian.com"], +-- auth_type: OAUTH_DYNAMIC_CLIENT, +-- oauth_resource_url: "https://mcp.atlassian.com/v1/mcp" +-- }' +-- +-- -- OAuth2 client credentials (for providers without DCR): +-- dbt run-operation create_mcp_api_integration --args '{ +-- integration_name: my_mcp_api_integration, +-- allowed_prefixes: ["https://api.example.com/mcp"], +-- auth_type: OAUTH2, +-- oauth_client_id: "abc123", +-- oauth_client_secret: "s3cr3t", +-- oauth_token_endpoint: "https://api.example.com/oauth/token", +-- oauth_authorization_endpoint: "https://api.example.com/oauth/authorize" +-- }' +-- +-- Prefer the `cortex_mcp_api_integration` materialization instead of this +-- operation for anything that should show up in the DAG/manifest — see +-- macros/materializations/cortex_mcp_api_integration.sql. This operation +-- remains for one-off, admin-run bootstrapping outside of `dbt build` +-- (e.g. from a session that only has ACCOUNTADMIN for the duration of the +-- bootstrap) and defaults to `or replace` to preserve existing behavior for +-- callers already scripting against it. +-- +-- Parameters: +-- integration_name string Snowflake object name for the integration. +-- allowed_prefixes list Base URL(s) of the MCP server (matched as prefix). +-- auth_type string 'OAUTH_DYNAMIC_CLIENT' (default) or 'OAUTH2'. +-- oauth_resource_url string Required for OAUTH_DYNAMIC_CLIENT. MCP server URL. +-- oauth_client_id string Required for OAUTH2. +-- oauth_client_secret string Required for OAUTH2. +-- oauth_token_endpoint string Required for OAUTH2. +-- oauth_authorization_endpoint string Required for OAUTH2. +-- oauth_client_auth_method string Optional for OAUTH2: CLIENT_SECRET_BASIC | CLIENT_SECRET_POST. +-- oauth_discovery_url string Optional for OAUTH2: OIDC discovery URL. +-- oauth_refresh_token_validity int Optional for OAUTH2: refresh token validity (seconds). +-- enabled bool ENABLED clause (default true). +-- if_not_exists bool Use IF NOT EXISTS instead of OR REPLACE (default false). +-- dry_run bool Log DDL without executing (default false). +-- comment string Optional COMMENT clause. +-#} + + {%- set ddl = dbt_cortex_agent._mcp_api_integration_ddl( + integration_name=integration_name, + allowed_prefixes=allowed_prefixes, + auth_type=auth_type, + oauth_resource_url=oauth_resource_url, + oauth_client_id=oauth_client_id, + oauth_client_secret=oauth_client_secret, + oauth_token_endpoint=oauth_token_endpoint, + oauth_authorization_endpoint=oauth_authorization_endpoint, + oauth_client_auth_method=oauth_client_auth_method, + oauth_discovery_url=oauth_discovery_url, + oauth_refresh_token_validity=oauth_refresh_token_validity, + enabled=enabled, + if_not_exists=if_not_exists, + comment=comment + ) -%} + {%- if dry_run -%} {{ log("-- dry_run=true: DDL not executed\n" ~ ddl, info=true) }} + {{ return(ddl) }} {%- elif execute -%} {{ log("Creating API integration: " ~ integration_name, info=true) }} {%- do run_query(ddl) -%} diff --git a/macros/relations/cortex_mcp_api_integration/create.sql b/macros/relations/cortex_mcp_api_integration/create.sql new file mode 100644 index 0000000..393e608 --- /dev/null +++ b/macros/relations/cortex_mcp_api_integration/create.sql @@ -0,0 +1,160 @@ +{% macro snowflake__get_create_cortex_mcp_api_integration_sql(relation) -%} +{#- +-- Produce the DDL that creates a Snowflake API INTEGRATION for an external +-- MCP server, from a config-only cortex_mcp_api_integration model. +-- +-- The integration's Snowflake object name is the model's alias (relation +-- identifier) — the same convention cortex_mcp_server uses for its own name. +-- +-- Required config: +-- allowed_prefixes list[string] Base URL(s) of the MCP server, matched as a prefix. +-- +-- Optional config (see create_mcp_api_integration operation docs for full list): +-- auth_type, oauth_resource_url, oauth_client_id, oauth_client_secret, +-- oauth_token_endpoint, oauth_authorization_endpoint, +-- oauth_client_auth_method, oauth_discovery_url, +-- oauth_refresh_token_validity, enabled, if_not_exists, comment +-- +-- `if_not_exists` defaults to `true` here (unlike the `create_mcp_api_integration` +-- operation, which defaults to `false`) — a materialization can be swept into a +-- routine `dbt build` (a broad selector, `state:modified.body`, `--full-refresh`) +-- without the caller directly intending to rebuild this specific node, so we +-- avoid blindly `OR REPLACE`-ing a live OAuth-authenticated integration as a +-- side effect. Pass `if_not_exists=false` explicitly to opt back into +-- `CREATE OR REPLACE` semantics (e.g. to rotate configuration deliberately). +-- +-- Returns: a valid DDL statement that creates the API integration. +-#} + {%- set integration_name = relation.identifier -%} + + {%- set _m = config.get('meta', {}).get('allowed_prefixes') -%} + {%- set allowed_prefixes = _m if _m is not none else config.require('allowed_prefixes') -%} + + {%- set _m = config.get('meta', {}).get('auth_type') -%} + {%- set auth_type = _m if _m is not none else config.get('auth_type', default='OAUTH_DYNAMIC_CLIENT') -%} + + {%- set _m = config.get('meta', {}).get('oauth_resource_url') -%} + {%- set oauth_resource_url = _m if _m is not none else config.get('oauth_resource_url') -%} + + {%- set _m = config.get('meta', {}).get('oauth_client_id') -%} + {%- set oauth_client_id = _m if _m is not none else config.get('oauth_client_id') -%} + + {%- set _m = config.get('meta', {}).get('oauth_client_secret') -%} + {%- set oauth_client_secret = _m if _m is not none else config.get('oauth_client_secret') -%} + + {%- set _m = config.get('meta', {}).get('oauth_token_endpoint') -%} + {%- set oauth_token_endpoint = _m if _m is not none else config.get('oauth_token_endpoint') -%} + + {%- set _m = config.get('meta', {}).get('oauth_authorization_endpoint') -%} + {%- set oauth_authorization_endpoint = _m if _m is not none else config.get('oauth_authorization_endpoint') -%} + + {%- set _m = config.get('meta', {}).get('oauth_client_auth_method') -%} + {%- set oauth_client_auth_method = _m if _m is not none else config.get('oauth_client_auth_method') -%} + + {%- set _m = config.get('meta', {}).get('oauth_discovery_url') -%} + {%- set oauth_discovery_url = _m if _m is not none else config.get('oauth_discovery_url') -%} + + {%- set _m = config.get('meta', {}).get('oauth_refresh_token_validity') -%} + {%- set oauth_refresh_token_validity = _m if _m is not none else config.get('oauth_refresh_token_validity') -%} + + {%- set _m = config.get('meta', {}).get('enabled') -%} + {%- set enabled = _m if _m is not none else config.get('enabled', default=true) -%} + + {%- set _m = config.get('meta', {}).get('if_not_exists') -%} + {%- set if_not_exists = _m if _m is not none else config.get('if_not_exists', default=true) -%} + + {%- set _m = config.get('meta', {}).get('comment') -%} + {%- set comment = _m if _m is not none else config.get('comment') -%} + + {{ dbt_cortex_agent._mcp_api_integration_ddl( + integration_name=integration_name, + allowed_prefixes=allowed_prefixes, + auth_type=auth_type, + oauth_resource_url=oauth_resource_url, + oauth_client_id=oauth_client_id, + oauth_client_secret=oauth_client_secret, + oauth_token_endpoint=oauth_token_endpoint, + oauth_authorization_endpoint=oauth_authorization_endpoint, + oauth_client_auth_method=oauth_client_auth_method, + oauth_discovery_url=oauth_discovery_url, + oauth_refresh_token_validity=oauth_refresh_token_validity, + enabled=enabled, + if_not_exists=if_not_exists, + comment=comment + ) }} +{%- endmacro %} + + +{% macro snowflake__create_or_replace_cortex_mcp_api_integration() %} +{#- +-- Orchestrates CREATE API INTEGRATION DDL for a model using the +-- `cortex_mcp_api_integration` materialization. Runs pre/post hooks around +-- the main statement. +-- +-- Returns: {'relations': [target_relation]} +-#} + {%- set identifier = model['alias'] -%} + {%- set target_relation = api.Relation.create( + identifier=identifier, schema=schema, database=database, + type='view') -%} + + {{ run_hooks(pre_hooks) }} + + {% call statement('main') -%} + {{ dbt_cortex_agent.snowflake__get_create_cortex_mcp_api_integration_sql(target_relation) }} + {%- endcall %} + + {{ run_hooks(post_hooks) }} + + {{ return({'relations': [target_relation]}) }} + +{% endmacro %} + + +{% macro cortex_mcp_api_integration_name(integration_ref) -%} +{#- +-- Returns the Snowflake object name for a cortex_mcp_api_integration model +-- given its ref(), so it can be wired into a cortex_mcp_server model's +-- `api_integration` config. +-- +-- Calling ref() as the argument registers the DAG dependency (the MCP server +-- will not be created until the API integration exists). +-- +-- API integrations are account-level objects with no database/schema, so +-- (unlike `cortex_mcp_server_name`) this returns a bare identifier, not a +-- dotted `database.schema.name`. +-- +-- Usage in a cortex_mcp_server model's config: +-- +-- {{ config( +-- materialized = 'cortex_mcp_server', +-- display_name = 'Atlassian (Jira & Confluence)', +-- url = 'https://mcp.atlassian.com/v1/mcp', +-- api_integration = dbt_cortex_agent.cortex_mcp_api_integration_name(ref('jira_mcp_api_integration')) +-- ) }} +-- +-- Args: +-- - integration_ref: Relation returned by ref() for the cortex_mcp_api_integration model +-- +-- Returns: bare identifier string, e.g. 'jira_mcp_api_integration' +-#} + {%- set model_name = integration_ref.identifier -%} + {%- if execute -%} + {%- set ns = namespace(found=false, alias='') -%} + {%- for node in graph.nodes.values() -%} + {%- if node.resource_type == 'model' and node.name == model_name -%} + {%- set ns.found = true -%} + {%- set ns.alias = node.alias -%} + {%- endif -%} + {%- endfor -%} + {%- if not ns.found -%} + {{ exceptions.raise_compiler_error( + "cortex_mcp_api_integration_name: no model found for '" ~ model_name ~ "'. " + ~ "Ensure it uses the cortex_mcp_api_integration materialization." + ) }} + {%- endif -%} + {{- ns.alias -}} + {%- else -%} + {{- model_name -}} + {%- endif -%} +{%- endmacro %} diff --git a/macros/relations/cortex_mcp_api_integration/drop.sql b/macros/relations/cortex_mcp_api_integration/drop.sql new file mode 100644 index 0000000..46826bc --- /dev/null +++ b/macros/relations/cortex_mcp_api_integration/drop.sql @@ -0,0 +1,10 @@ +{% macro snowflake__get_drop_cortex_mcp_api_integration_sql(relation) %} +{#- +-- Produce DDL that drops a Snowflake API INTEGRATION object. +-- +-- Args: +-- - relation: SnowflakeRelation or str +-- Returns: DDL string +-#} + drop api integration if exists {{ relation }} +{% endmacro %} diff --git a/macros/relations/cortex_mcp_api_integration/rename.sql b/macros/relations/cortex_mcp_api_integration/rename.sql new file mode 100644 index 0000000..15217dd --- /dev/null +++ b/macros/relations/cortex_mcp_api_integration/rename.sql @@ -0,0 +1,15 @@ +{%- macro snowflake__get_cortex_mcp_api_integration_rename_sql(relation, new_name) -%} +{#- +-- Produce DDL that renames a Snowflake API INTEGRATION object. +-- +-- Unlike EXTERNAL MCP SERVER (see cortex_mcp_server/rename.sql), API +-- INTEGRATION is a standard, long-documented Snowflake object type and +-- `ALTER ... RENAME TO` is supported for it. +-- +-- Args: +-- - relation: SnowflakeRelation or str +-- - new_name: new identifier +-- Returns: DDL string +-#} + alter api integration {{ relation }} rename to {{ new_name }} +{%- endmacro -%} diff --git a/macros/relations/cortex_mcp_server/create.sql b/macros/relations/cortex_mcp_server/create.sql index 0f488eb..4720887 100644 --- a/macros/relations/cortex_mcp_server/create.sql +++ b/macros/relations/cortex_mcp_server/create.sql @@ -53,7 +53,16 @@ {{ exceptions.raise_compiler_error( "\n\ncortex_mcp_server: API integration '" ~ api_integration ~ "' does not exist " ~ "in Snowflake.\n\n" - ~ "Bootstrap it first (requires ACCOUNTADMIN or CREATE INTEGRATION privilege):\n\n" + ~ "Bootstrap it first (requires ACCOUNTADMIN or CREATE INTEGRATION privilege), " + ~ "either as a first-class dbt model (recommended — gives you a real DAG edge " + ~ "via cortex_mcp_api_integration_name(ref(...)) instead of a bare name string):\n\n" + ~ " {{ config(\n" + ~ " materialized = 'cortex_mcp_api_integration',\n" + ~ " allowed_prefixes = [\"\"],\n" + ~ " auth_type = 'OAUTH_DYNAMIC_CLIENT',\n" + ~ " oauth_resource_url = \"\"\n" + ~ " ) }}\n\n" + ~ "or as a one-off run-operation:\n\n" ~ " dbt run-operation create_mcp_api_integration --args '{\n" ~ " integration_name: " ~ api_integration ~ ",\n" ~ " allowed_prefixes: [\"\"],\n" From b6d20b340853da54c5db2a87d732d8634cf60e66 Mon Sep 17 00:00:00 2001 From: mattsenicksigma Date: Thu, 24 Sep 2026 11:25:34 -0700 Subject: [PATCH 2/3] test: make the cortex_mcp_api_integration fixture generic, not Jira-specific jira_mcp_api_integration -> example_mcp_api_integration, with generic mcp.example.com placeholder values. This model exists purely to exercise structural compilation (config resolution, DDL construction) in CI, not to represent a real MCP endpoint, so it shouldn't imply an Atlassian/Jira-specific integration. atlassian_mcp_server still refs it via cortex_mcp_api_integration_name(ref(...)) to demonstrate the wiring. Co-Authored-By: Claude Sonnet 5 --- .github/workflows/integration-tests.yml | 2 +- integration_tests/README.md | 14 +++++++------- integration_tests/models/atlassian_mcp_server.sql | 2 +- ...gration.sql => example_mcp_api_integration.sql} | 4 ++-- 4 files changed, 11 insertions(+), 11 deletions(-) rename integration_tests/models/{jira_mcp_api_integration.sql => example_mcp_api_integration.sql} (56%) diff --git a/.github/workflows/integration-tests.yml b/.github/workflows/integration-tests.yml index 13f15b6..6a573ee 100644 --- a/.github/workflows/integration-tests.yml +++ b/.github/workflows/integration-tests.yml @@ -31,7 +31,7 @@ jobs: - name: Compile agent models working-directory: integration_tests - run: dbt compile --select "agent_*" "jira_mcp_api_integration" "atlassian_mcp_server" --target duckdb + run: dbt compile --select "agent_*" "example_mcp_api_integration" "atlassian_mcp_server" --target duckdb - name: Validate create_mcp_api_integration DDL builder working-directory: integration_tests diff --git a/integration_tests/README.md b/integration_tests/README.md index 9756a87..697e3ab 100644 --- a/integration_tests/README.md +++ b/integration_tests/README.md @@ -13,8 +13,8 @@ against a live Snowflake account. | `agent_minimal` | cortex_agent | Spec mode, instructions only, with `comment` + `profile`. | | `agent_with_semantic_view` | cortex_agent | Spec mode, Analyst tool wired to `orders_semantic_view` via `ref()`. | | `agent_raw_ddl` | cortex_agent | `raw_ddl=true` pass-through mode. | -| `jira_mcp_api_integration` | cortex_mcp_api_integration | API integration backing the Atlassian MCP endpoint. | -| `atlassian_mcp_server` | cortex_mcp_server | External MCP server for the Atlassian Jira/Confluence endpoint, wired to `jira_mcp_api_integration` via `ref()`. | +| `example_mcp_api_integration` | cortex_mcp_api_integration | Generic API integration fixture — exercises structural compilation only, not tied to a real MCP endpoint. | +| `atlassian_mcp_server` | cortex_mcp_server | External MCP server for the Atlassian Jira/Confluence endpoint, wired to `example_mcp_api_integration` via `ref()`. | | `agent_with_mcp_server` | cortex_agent | Agent wired to `atlassian_mcp_server` via `cortex_mcp_server_name(ref(...))`. | ## Prerequisites @@ -23,11 +23,11 @@ against a live Snowflake account. - A Snowflake account/role with privileges to create agents, semantic views, tables, and to use Cortex. - **ACCOUNTADMIN or CREATE INTEGRATION** privilege on the role running - `dbt build` — `jira_mcp_api_integration` creates a Snowflake API INTEGRATION, - an account-level object, and needs this privilege the same as any other - model in this project needs privileges on its own target schema. There is - no separate manual bootstrap step; `dbt build` creates it in DAG order - before `atlassian_mcp_server` runs. + `dbt build` — `example_mcp_api_integration` creates a Snowflake API + INTEGRATION, an account-level object, and needs this privilege the same as + any other model in this project needs privileges on its own target schema. + There is no separate manual bootstrap step; `dbt build` creates it in DAG + order before `atlassian_mcp_server` runs. If your role does not have this privilege, either switch to ACCOUNTADMIN for the run or fall back to the `create_mcp_api_integration` run-operation from an diff --git a/integration_tests/models/atlassian_mcp_server.sql b/integration_tests/models/atlassian_mcp_server.sql index bc0e39f..814594f 100644 --- a/integration_tests/models/atlassian_mcp_server.sql +++ b/integration_tests/models/atlassian_mcp_server.sql @@ -4,7 +4,7 @@ meta = { 'display_name': 'Atlassian (Jira & Confluence)', 'url': 'https://mcp.atlassian.com/v1/mcp', - 'api_integration': dbt_cortex_agent.cortex_mcp_api_integration_name(ref('jira_mcp_api_integration')) + 'api_integration': dbt_cortex_agent.cortex_mcp_api_integration_name(ref('example_mcp_api_integration')) } ) }} diff --git a/integration_tests/models/jira_mcp_api_integration.sql b/integration_tests/models/example_mcp_api_integration.sql similarity index 56% rename from integration_tests/models/jira_mcp_api_integration.sql rename to integration_tests/models/example_mcp_api_integration.sql index 945e96d..e5297f2 100644 --- a/integration_tests/models/jira_mcp_api_integration.sql +++ b/integration_tests/models/example_mcp_api_integration.sql @@ -2,9 +2,9 @@ config( materialized = 'cortex_mcp_api_integration', meta = { - 'allowed_prefixes': ['https://mcp.atlassian.com'], + 'allowed_prefixes': ['https://mcp.example.com'], 'auth_type': 'OAUTH_DYNAMIC_CLIENT', - 'oauth_resource_url': 'https://mcp.atlassian.com/v1/mcp' + 'oauth_resource_url': 'https://mcp.example.com/v1/mcp' } ) }} From b66d0d6814d177f8b5b0e69fdf227ec2f66da151 Mon Sep 17 00:00:00 2001 From: mattsenicksigma Date: Thu, 24 Sep 2026 11:32:53 -0700 Subject: [PATCH 3/3] test: rename atlassian_mcp_server fixture to example_mcp_server MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Same rationale as the prior jira_mcp_api_integration -> example_mcp_api_ integration rename: this fixture exists to exercise structural compilation and DAG wiring for the cortex_mcp_server materialization, not to represent a real MCP endpoint, so it shouldn't hardcode Atlassian/Jira branding. Cascades the rename through agent_with_mcp_server.sql, integration_tests/models/schema.yml, integration_tests/README.md, and the CI compile selector. Left the Atlassian/Jira example untouched in the main README and in macros/schema.yml's macro-argument docs — those are illustrative real-world usage examples, not the test fixture itself. Co-Authored-By: Claude Sonnet 5 --- .github/workflows/integration-tests.yml | 2 +- integration_tests/README.md | 6 +++--- integration_tests/models/agent_with_mcp_server.sql | 4 ++-- .../{atlassian_mcp_server.sql => example_mcp_server.sql} | 4 ++-- integration_tests/models/schema.yml | 9 ++++++--- 5 files changed, 14 insertions(+), 11 deletions(-) rename integration_tests/models/{atlassian_mcp_server.sql => example_mcp_server.sql} (62%) diff --git a/.github/workflows/integration-tests.yml b/.github/workflows/integration-tests.yml index 6a573ee..81627f7 100644 --- a/.github/workflows/integration-tests.yml +++ b/.github/workflows/integration-tests.yml @@ -31,7 +31,7 @@ jobs: - name: Compile agent models working-directory: integration_tests - run: dbt compile --select "agent_*" "example_mcp_api_integration" "atlassian_mcp_server" --target duckdb + run: dbt compile --select "agent_*" "example_mcp_api_integration" "example_mcp_server" --target duckdb - name: Validate create_mcp_api_integration DDL builder working-directory: integration_tests diff --git a/integration_tests/README.md b/integration_tests/README.md index 697e3ab..e9b4f7f 100644 --- a/integration_tests/README.md +++ b/integration_tests/README.md @@ -14,8 +14,8 @@ against a live Snowflake account. | `agent_with_semantic_view` | cortex_agent | Spec mode, Analyst tool wired to `orders_semantic_view` via `ref()`. | | `agent_raw_ddl` | cortex_agent | `raw_ddl=true` pass-through mode. | | `example_mcp_api_integration` | cortex_mcp_api_integration | Generic API integration fixture — exercises structural compilation only, not tied to a real MCP endpoint. | -| `atlassian_mcp_server` | cortex_mcp_server | External MCP server for the Atlassian Jira/Confluence endpoint, wired to `example_mcp_api_integration` via `ref()`. | -| `agent_with_mcp_server` | cortex_agent | Agent wired to `atlassian_mcp_server` via `cortex_mcp_server_name(ref(...))`. | +| `example_mcp_server` | cortex_mcp_server | Generic External MCP server fixture, wired to `example_mcp_api_integration` via `ref()` — exercises structural compilation only, not tied to a real MCP endpoint. | +| `agent_with_mcp_server` | cortex_agent | Agent wired to `example_mcp_server` via `cortex_mcp_server_name(ref(...))`. | ## Prerequisites @@ -27,7 +27,7 @@ against a live Snowflake account. INTEGRATION, an account-level object, and needs this privilege the same as any other model in this project needs privileges on its own target schema. There is no separate manual bootstrap step; `dbt build` creates it in DAG - order before `atlassian_mcp_server` runs. + order before `example_mcp_server` runs. If your role does not have this privilege, either switch to ACCOUNTADMIN for the run or fall back to the `create_mcp_api_integration` run-operation from an diff --git a/integration_tests/models/agent_with_mcp_server.sql b/integration_tests/models/agent_with_mcp_server.sql index ebf49c3..60ffa87 100644 --- a/integration_tests/models/agent_with_mcp_server.sql +++ b/integration_tests/models/agent_with_mcp_server.sql @@ -12,7 +12,7 @@ orchestration: tokens: 16000 instructions: response: "Be concise." - orchestration: "Use the Atlassian MCP server to answer questions about Jira and Confluence." + orchestration: "Use the example MCP server to answer questions." mcp_servers: - server_spec: - name: "{{ dbt_cortex_agent.cortex_mcp_server_name(ref('atlassian_mcp_server')) }}" + name: "{{ dbt_cortex_agent.cortex_mcp_server_name(ref('example_mcp_server')) }}" diff --git a/integration_tests/models/atlassian_mcp_server.sql b/integration_tests/models/example_mcp_server.sql similarity index 62% rename from integration_tests/models/atlassian_mcp_server.sql rename to integration_tests/models/example_mcp_server.sql index 814594f..46e161b 100644 --- a/integration_tests/models/atlassian_mcp_server.sql +++ b/integration_tests/models/example_mcp_server.sql @@ -2,8 +2,8 @@ config( materialized = 'cortex_mcp_server', meta = { - 'display_name': 'Atlassian (Jira & Confluence)', - 'url': 'https://mcp.atlassian.com/v1/mcp', + 'display_name': 'Example MCP Server', + 'url': 'https://mcp.example.com/v1/mcp', 'api_integration': dbt_cortex_agent.cortex_mcp_api_integration_name(ref('example_mcp_api_integration')) } ) diff --git a/integration_tests/models/schema.yml b/integration_tests/models/schema.yml index d770089..7457d34 100644 --- a/integration_tests/models/schema.yml +++ b/integration_tests/models/schema.yml @@ -36,8 +36,11 @@ models: - name: agent_versioned description: "Cortex Agent with versioning=true. Compiles to CREATE AGENT IF NOT EXISTS ... ADD VERSION DDL. Compile-path test only." - - name: atlassian_mcp_server - description: "External MCP Server object for the Atlassian (Jira & Confluence) MCP endpoint. Created by the cortex_mcp_server materialization." + - name: example_mcp_api_integration + description: "Generic Snowflake API integration fixture. Created by the cortex_mcp_api_integration materialization. Exercises structural compilation only, not tied to a real MCP endpoint." + + - name: example_mcp_server + description: "External MCP Server object wired to example_mcp_api_integration via ref(). Created by the cortex_mcp_server materialization." - name: agent_with_mcp_server - description: "Cortex Agent that references atlassian_mcp_server via cortex_mcp_server_name(ref(...)). Verifies DAG wiring and mcp_servers: spec injection." + description: "Cortex Agent that references example_mcp_server via cortex_mcp_server_name(ref(...)). Verifies DAG wiring and mcp_servers: spec injection."