snowflake-analytics-mcp-server 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +24 -17
- package/dist/index.js +3 -6
- package/dist/snowflake.d.ts +12 -0
- package/dist/snowflake.d.ts.map +1 -1
- package/dist/snowflake.js +50 -2
- package/dist/tools/cortex.d.ts +18 -0
- package/dist/tools/cortex.d.ts.map +1 -0
- package/dist/tools/cortex.js +216 -0
- package/dist/tools/index.d.ts +3 -3
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/index.js +10 -4
- package/dist/tools/listTables.d.ts +7 -8
- package/dist/tools/listTables.d.ts.map +1 -1
- package/dist/tools/listTables.js +3 -2
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
# snowflake-analytics-mcp-server
|
|
2
2
|
|
|
3
|
-
A [Model Context Protocol](https://modelcontextprotocol.io) server for **Snowflake
|
|
4
|
-
|
|
5
|
-
The `list_tables` tool returns the platform's exact table-discovery envelope, so it drops straight into the same `fetchMcpTables` flow as the Clio, Dropbox and Shopify servers.
|
|
3
|
+
A [Model Context Protocol](https://modelcontextprotocol.io) server for **Snowflake**. It connects to Snowflake via the official `snowflake-sdk` driver (pure Node — no client binary needed) and exposes table-discovery, SQL query, and Cortex AI tools over stdio. Runnable directly with `npx`.
|
|
6
4
|
|
|
7
5
|
## Install
|
|
8
6
|
|
|
@@ -22,7 +20,7 @@ All credentials come from environment variables.
|
|
|
22
20
|
| `SNOWFLAKE_USERNAME` | ✅ | — | Login name |
|
|
23
21
|
| `SNOWFLAKE_PASSWORD` | ✅ (password auth) | — | Password (required unless using key-pair/OAuth) |
|
|
24
22
|
| `SNOWFLAKE_WAREHOUSE` | — | — | Virtual warehouse to use |
|
|
25
|
-
| `SNOWFLAKE_DATABASE` | recommended | — | Default database (used by `list_tables
|
|
23
|
+
| `SNOWFLAKE_DATABASE` | recommended | — | Default database (used by `list_tables`, and as the fallback when a caller passes an inaccessible database) |
|
|
26
24
|
| `SNOWFLAKE_SCHEMA` | recommended | — | Default schema |
|
|
27
25
|
| `SNOWFLAKE_ROLE` | — | — | Role to assume |
|
|
28
26
|
| `SNOWFLAKE_AUTHENTICATOR` | — | `SNOWFLAKE` | `SNOWFLAKE` (password), `SNOWFLAKE_JWT` (key-pair), `OAUTH` |
|
|
@@ -33,6 +31,8 @@ All credentials come from environment variables.
|
|
|
33
31
|
| `SNOWFLAKE_READ_ONLY` | — | `true` | Blocks writes/DDL in `execute_query`. Set `false` to allow them |
|
|
34
32
|
| `SNOWFLAKE_ROW_LIMIT` | — | `1000` | Hard cap on returned rows |
|
|
35
33
|
| `QUERY_TIMEOUT_MS` | — | `60000` | Client-side query timeout |
|
|
34
|
+
| `SNOWFLAKE_CORTEX_ENABLED` | — | `true` | Register the Cortex AI tools. Set `false` to hide them |
|
|
35
|
+
| `SNOWFLAKE_CORTEX_MODEL` | — | `llama3.1-8b` | Default model for Cortex AI_COMPLETE / text-to-SQL |
|
|
36
36
|
|
|
37
37
|
### Account identifier tip
|
|
38
38
|
|
|
@@ -78,9 +78,11 @@ Add to `claude_desktop_config.json`:
|
|
|
78
78
|
|
|
79
79
|
## Tools
|
|
80
80
|
|
|
81
|
+
### SQL & discovery
|
|
82
|
+
|
|
81
83
|
| Tool | Args | Description |
|
|
82
84
|
|---|---|---|
|
|
83
|
-
| `list_tables` | `database?`, `schema?`, `include_views?` |
|
|
85
|
+
| `list_tables` | `database?`, `schema?`, `include_views?` | Returns `{ table_name }[]` for table discovery |
|
|
84
86
|
| `test_connection` | — | Returns version, account, user, role, warehouse, database, schema |
|
|
85
87
|
| `list_databases` | — | Databases visible to the current role |
|
|
86
88
|
| `list_schemas` | `database?` | Schemas in a database |
|
|
@@ -89,21 +91,26 @@ Add to `claude_desktop_config.json`:
|
|
|
89
91
|
| `get_table_sample` | `table`, `database?`, `schema?`, `limit?` | Preview rows from a table |
|
|
90
92
|
| `execute_query` | `query` | Run SQL. Read-only unless `SNOWFLAKE_READ_ONLY=false` |
|
|
91
93
|
|
|
92
|
-
|
|
94
|
+
### Cortex AI
|
|
93
95
|
|
|
94
|
-
|
|
96
|
+
These are additive generative-AI tools (token-billed, higher latency than the SQL tools). Each is invoked as a `SELECT`, so they work with `SNOWFLAKE_READ_ONLY=true`. Disable them with `SNOWFLAKE_CORTEX_ENABLED=false`.
|
|
95
97
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
98
|
+
| Tool | Args | Description |
|
|
99
|
+
|---|---|---|
|
|
100
|
+
| `cortex_ask` | `question`, `database?`, `schema?`, `model?` | Natural-language question → generated read-only SQL → answer. Best for analytics like "what is total revenue" |
|
|
101
|
+
| `cortex_complete` | `prompt`, `model?` | Free-form LLM completion via AI_COMPLETE |
|
|
102
|
+
| `cortex_sentiment` | `text` | Sentiment of a text via AI_SENTIMENT |
|
|
103
|
+
| `cortex_summarize` | `text` | Concise summary of a text |
|
|
104
|
+
| `cortex_classify` | `text`, `categories[]` | Classify text into your categories via AI_CLASSIFY |
|
|
105
|
+
| `cortex_translate` | `text`, `to_language`, `from_language?` | Translate text (source auto-detected when omitted) |
|
|
106
|
+
|
|
107
|
+
`cortex_ask` generates SQL and always restricts it to read-only statements, regardless of `SNOWFLAKE_READ_ONLY`.
|
|
108
|
+
|
|
109
|
+
**Cortex prerequisites:** the account must be in a region that supports Cortex, and the connecting role needs the `SNOWFLAKE.CORTEX_USER` database role plus the `USE AI FUNCTIONS` privilege. If a Cortex call fails on region or privileges, the tool returns a hint explaining what to grant.
|
|
110
|
+
|
|
111
|
+
## Database resolution
|
|
105
112
|
|
|
106
|
-
|
|
113
|
+
`list_tables`, `describe_table`, `get_table_sample`, `list_schemas`, and `cortex_ask` accept an optional `database`. If the value passed isn't an accessible database, the tool falls back to `SNOWFLAKE_DATABASE` instead of erroring — so a stray or wrong database name from an upstream caller doesn't cause a failed lookup.
|
|
107
114
|
|
|
108
115
|
## License
|
|
109
116
|
|
package/dist/index.js
CHANGED
|
@@ -3,11 +3,8 @@
|
|
|
3
3
|
* snowflake-analytics-mcp-server
|
|
4
4
|
*
|
|
5
5
|
* A Model Context Protocol server that exposes a Snowflake connection over
|
|
6
|
-
* stdio, runnable via `npx`.
|
|
7
|
-
*
|
|
8
|
-
* while the remaining tools are standard SQL helpers.
|
|
9
|
-
*
|
|
10
|
-
* Credentials come entirely from environment variables (see README).
|
|
6
|
+
* stdio, runnable via `npx`. Provides table-discovery, SQL query, and Cortex AI
|
|
7
|
+
* tools. Credentials come entirely from environment variables (see README).
|
|
11
8
|
*/
|
|
12
9
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
13
10
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
@@ -15,7 +12,7 @@ import { registerTools } from "./tools/index.js";
|
|
|
15
12
|
async function main() {
|
|
16
13
|
const server = new McpServer({
|
|
17
14
|
name: "snowflake-analytics-mcp-server",
|
|
18
|
-
version: "0.
|
|
15
|
+
version: "0.2.0",
|
|
19
16
|
title: "Snowflake Analytics MCP Server",
|
|
20
17
|
});
|
|
21
18
|
registerTools(server);
|
package/dist/snowflake.d.ts
CHANGED
|
@@ -13,6 +13,8 @@ export interface SnowflakeConfig {
|
|
|
13
13
|
queryTimeoutMs: number;
|
|
14
14
|
rowLimit: number;
|
|
15
15
|
readOnly: boolean;
|
|
16
|
+
cortexEnabled: boolean;
|
|
17
|
+
cortexModel: string;
|
|
16
18
|
}
|
|
17
19
|
export declare function loadConfig(): SnowflakeConfig;
|
|
18
20
|
export declare function getConfig(): SnowflakeConfig;
|
|
@@ -33,5 +35,15 @@ export declare function quoteIdent(name: string): string;
|
|
|
33
35
|
* Falls back to the connection's configured database/schema when omitted.
|
|
34
36
|
*/
|
|
35
37
|
export declare function qualify(table: string, database?: string, schema?: string): string;
|
|
38
|
+
export declare function accessibleDatabases(): Promise<Set<string>>;
|
|
39
|
+
/**
|
|
40
|
+
* Resolve the database a tool should use.
|
|
41
|
+
*
|
|
42
|
+
* If `requested` is provided but isn't an accessible database (e.g. an upstream
|
|
43
|
+
* caller sent a stray value that isn't a real database), fall back to the
|
|
44
|
+
* configured SNOWFLAKE_DATABASE instead of erroring — so tools self-correct on
|
|
45
|
+
* the first call rather than taking a failed detour.
|
|
46
|
+
*/
|
|
47
|
+
export declare function resolveDatabase(requested?: string): Promise<string | undefined>;
|
|
36
48
|
export declare function isReadOnlyStatement(sql: string): boolean;
|
|
37
49
|
//# sourceMappingURL=snowflake.d.ts.map
|
package/dist/snowflake.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"snowflake.d.ts","sourceRoot":"","sources":["../src/snowflake.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"snowflake.d.ts","sourceRoot":"","sources":["../src/snowflake.ts"],"names":[],"mappings":"AAyBA,MAAM,WAAW,eAAe;IAC9B,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,aAAa,EAAE,MAAM,CAAC;IACtB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,cAAc,EAAE,MAAM,CAAC;IACvB,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,OAAO,CAAC;IAClB,aAAa,EAAE,OAAO,CAAC;IACvB,WAAW,EAAE,MAAM,CAAC;CACrB;AAUD,wBAAgB,UAAU,IAAI,eAAe,CA0C5C;AAKD,wBAAgB,SAAS,IAAI,eAAe,CAG3C;AAiCD,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,CAAC;IAChC,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,wBAAsB,QAAQ,CAC5B,OAAO,EAAE,MAAM,EACf,KAAK,GAAE,OAAO,EAAO,GACpB,OAAO,CAAC,WAAW,CAAC,CAoBtB;AAED;;;;;GAKG;AACH,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAO/C;AAED;;;GAGG;AACH,wBAAgB,OAAO,CACrB,KAAK,EAAE,MAAM,EACb,QAAQ,CAAC,EAAE,MAAM,EACjB,MAAM,CAAC,EAAE,MAAM,GACd,MAAM,CASR;AAMD,wBAAsB,mBAAmB,IAAI,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,CAchE;AAED;;;;;;;GAOG;AACH,wBAAsB,eAAe,CAAC,SAAS,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAWrF;AAKD,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAExD"}
|
package/dist/snowflake.js
CHANGED
|
@@ -7,6 +7,8 @@
|
|
|
7
7
|
* tool calls (stdio is single-user, so a connection pool is overkill).
|
|
8
8
|
* - Reconnect transparently if the session has dropped.
|
|
9
9
|
* - Provide a promisified query runner and safe identifier quoting.
|
|
10
|
+
* - Resolve a usable database, falling back to SNOWFLAKE_DATABASE when a
|
|
11
|
+
* caller passes one that isn't accessible.
|
|
10
12
|
*
|
|
11
13
|
* Auth modes (pick via SNOWFLAKE_AUTHENTICATOR, default = password):
|
|
12
14
|
* - SNOWFLAKE : username + password
|
|
@@ -56,7 +58,11 @@ export function loadConfig() {
|
|
|
56
58
|
role: process.env.SNOWFLAKE_ROLE?.trim(),
|
|
57
59
|
queryTimeoutMs: Number(process.env.QUERY_TIMEOUT_MS || 60000),
|
|
58
60
|
rowLimit: Number(process.env.SNOWFLAKE_ROW_LIMIT || 1000),
|
|
61
|
+
// Read-only by default. Only an explicit "false" opens up writes/DDL.
|
|
59
62
|
readOnly: (process.env.SNOWFLAKE_READ_ONLY || "true").toLowerCase() !== "false",
|
|
63
|
+
// Cortex AI tools are on by default; set SNOWFLAKE_CORTEX_ENABLED=false to hide them.
|
|
64
|
+
cortexEnabled: (process.env.SNOWFLAKE_CORTEX_ENABLED || "true").toLowerCase() !== "false",
|
|
65
|
+
cortexModel: (process.env.SNOWFLAKE_CORTEX_MODEL || "llama3.1-8b").trim(),
|
|
60
66
|
};
|
|
61
67
|
}
|
|
62
68
|
let cfg = null;
|
|
@@ -110,8 +116,6 @@ export async function runQuery(sqlText, binds = []) {
|
|
|
110
116
|
connection.execute({
|
|
111
117
|
sqlText,
|
|
112
118
|
binds: binds,
|
|
113
|
-
// Cap runaway queries at the configured timeout.
|
|
114
|
-
// (SDK-level; server-side statement_timeout can also be set.)
|
|
115
119
|
complete: (err, _stmt, r) => {
|
|
116
120
|
if (err)
|
|
117
121
|
reject(err);
|
|
@@ -154,6 +158,50 @@ export function qualify(table, database, schema) {
|
|
|
154
158
|
parts.push(quoteIdent(table));
|
|
155
159
|
return parts.join(".");
|
|
156
160
|
}
|
|
161
|
+
// ── Database resolution ───────────────────────────────────────────────────────
|
|
162
|
+
// Cached uppercase set of databases the current role can actually see.
|
|
163
|
+
let dbCache = null;
|
|
164
|
+
export async function accessibleDatabases() {
|
|
165
|
+
if (dbCache && dbCache.size)
|
|
166
|
+
return dbCache;
|
|
167
|
+
const names = new Set();
|
|
168
|
+
try {
|
|
169
|
+
const { rows } = await runQuery("SHOW DATABASES");
|
|
170
|
+
for (const r of rows) {
|
|
171
|
+
const n = r.name;
|
|
172
|
+
if (typeof n === "string")
|
|
173
|
+
names.add(n.toUpperCase());
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
catch {
|
|
177
|
+
/* ignore — resolveDatabase will trust the caller's input below */
|
|
178
|
+
}
|
|
179
|
+
if (names.size)
|
|
180
|
+
dbCache = names;
|
|
181
|
+
return names;
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Resolve the database a tool should use.
|
|
185
|
+
*
|
|
186
|
+
* If `requested` is provided but isn't an accessible database (e.g. an upstream
|
|
187
|
+
* caller sent a stray value that isn't a real database), fall back to the
|
|
188
|
+
* configured SNOWFLAKE_DATABASE instead of erroring — so tools self-correct on
|
|
189
|
+
* the first call rather than taking a failed detour.
|
|
190
|
+
*/
|
|
191
|
+
export async function resolveDatabase(requested) {
|
|
192
|
+
const fallback = getConfig().database;
|
|
193
|
+
const wanted = requested?.trim();
|
|
194
|
+
if (!wanted)
|
|
195
|
+
return fallback;
|
|
196
|
+
const dbs = await accessibleDatabases();
|
|
197
|
+
// Couldn't enumerate databases — trust the caller, then the default.
|
|
198
|
+
if (dbs.size === 0)
|
|
199
|
+
return wanted || fallback;
|
|
200
|
+
if (dbs.has(wanted.toUpperCase()))
|
|
201
|
+
return wanted;
|
|
202
|
+
// Requested database isn't accessible — use the configured default.
|
|
203
|
+
return fallback || wanted;
|
|
204
|
+
}
|
|
157
205
|
const WRITE_RE = /^\s*(INSERT|UPDATE|DELETE|MERGE|CREATE|DROP|ALTER|TRUNCATE|GRANT|REVOKE|CALL|COPY|PUT|REMOVE|USE)\b/i;
|
|
158
206
|
export function isReadOnlyStatement(sql) {
|
|
159
207
|
return !WRITE_RE.test(sql);
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* cortex.ts — Snowflake Cortex AI tools.
|
|
3
|
+
*
|
|
4
|
+
* These are additive on top of the fast SQL/discovery tools: Cortex functions
|
|
5
|
+
* are generative AI calls (token-billed, higher latency), so they are exposed as
|
|
6
|
+
* their own tools rather than wrapped around metadata operations.
|
|
7
|
+
*
|
|
8
|
+
* Every Cortex function here is invoked as a SELECT, so the tools remain fully
|
|
9
|
+
* compatible with SNOWFLAKE_READ_ONLY=true. The text-to-SQL tool additionally
|
|
10
|
+
* enforces read-only on the *generated* SQL regardless of server settings.
|
|
11
|
+
*
|
|
12
|
+
* Model is configurable via SNOWFLAKE_CORTEX_MODEL (default: llama3.1-8b).
|
|
13
|
+
* Availability depends on the account's region and the role holding the
|
|
14
|
+
* SNOWFLAKE.CORTEX_USER database role — errors are surfaced with a hint.
|
|
15
|
+
*/
|
|
16
|
+
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
17
|
+
export declare function registerCortexTools(server: McpServer): void;
|
|
18
|
+
//# sourceMappingURL=cortex.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cortex.d.ts","sourceRoot":"","sources":["../../src/tools/cortex.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AACH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AA+EzE,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,SAAS,GAAG,IAAI,CA6L3D"}
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { getConfig, runQuery, quoteIdent, resolveDatabase, isReadOnlyStatement, } from "../snowflake.js";
|
|
3
|
+
function jsonText(value) {
|
|
4
|
+
return { content: [{ type: "text", text: JSON.stringify(value, null, 2) }] };
|
|
5
|
+
}
|
|
6
|
+
function cortexErr(err) {
|
|
7
|
+
const raw = err instanceof Error ? err.message : String(err);
|
|
8
|
+
// Surface a helpful hint for the two most common Cortex setup failures.
|
|
9
|
+
const hintable = /cortex|not enabled|unknown function|privilege|not authorized|CORTEX_USER|region/i.test(raw);
|
|
10
|
+
const message = hintable
|
|
11
|
+
? `${raw}\n\nHint: Cortex AI requires an account in a supported region and a role holding the SNOWFLAKE.CORTEX_USER database role (plus USE AI FUNCTIONS). Confirm both, or set SNOWFLAKE_CORTEX_ENABLED=false to hide these tools.`
|
|
12
|
+
: raw;
|
|
13
|
+
return {
|
|
14
|
+
content: [{ type: "text", text: JSON.stringify({ error: message }, null, 2) }],
|
|
15
|
+
isError: true,
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
// A Cortex model identifier is a simple slug — validate before inlining it.
|
|
19
|
+
function safeModel(model) {
|
|
20
|
+
const m = (model || getConfig().cortexModel).trim();
|
|
21
|
+
if (!/^[A-Za-z0-9._-]+$/.test(m)) {
|
|
22
|
+
throw new Error(`Invalid model name "${m}".`);
|
|
23
|
+
}
|
|
24
|
+
return m;
|
|
25
|
+
}
|
|
26
|
+
// Strip markdown fences and trailing semicolons; keep only the first statement.
|
|
27
|
+
function cleanSql(text) {
|
|
28
|
+
let s = text.trim();
|
|
29
|
+
s = s.replace(/^```(?:sql)?\s*/i, "").replace(/\s*```$/i, "").trim();
|
|
30
|
+
// Take up to the first semicolon so we execute a single statement.
|
|
31
|
+
const semi = s.indexOf(";");
|
|
32
|
+
if (semi !== -1)
|
|
33
|
+
s = s.slice(0, semi);
|
|
34
|
+
return s.trim();
|
|
35
|
+
}
|
|
36
|
+
// Build a compact schema description for the configured database/schema.
|
|
37
|
+
async function schemaContext(database, schema) {
|
|
38
|
+
const conditions = ["TABLE_SCHEMA <> 'INFORMATION_SCHEMA'"];
|
|
39
|
+
const binds = [];
|
|
40
|
+
if (schema) {
|
|
41
|
+
conditions[0] = "TABLE_SCHEMA = ?";
|
|
42
|
+
binds.push(schema);
|
|
43
|
+
}
|
|
44
|
+
const { rows } = await runQuery(`SELECT TABLE_SCHEMA, TABLE_NAME, COLUMN_NAME, DATA_TYPE ` +
|
|
45
|
+
`FROM ${quoteIdent(database)}.INFORMATION_SCHEMA.COLUMNS ` +
|
|
46
|
+
`WHERE ${conditions.join(" AND ")} ` +
|
|
47
|
+
`ORDER BY TABLE_SCHEMA, TABLE_NAME, ORDINAL_POSITION ` +
|
|
48
|
+
`LIMIT 800`, binds);
|
|
49
|
+
const tables = new Map();
|
|
50
|
+
for (const r of rows) {
|
|
51
|
+
const o = r;
|
|
52
|
+
const key = `${o.TABLE_SCHEMA}.${o.TABLE_NAME}`;
|
|
53
|
+
if (!tables.has(key))
|
|
54
|
+
tables.set(key, []);
|
|
55
|
+
tables.get(key).push(`${o.COLUMN_NAME} ${o.DATA_TYPE}`);
|
|
56
|
+
}
|
|
57
|
+
const lines = [];
|
|
58
|
+
for (const [tbl, cols] of tables) {
|
|
59
|
+
lines.push(`${tbl} (${cols.join(", ")})`);
|
|
60
|
+
}
|
|
61
|
+
return lines.join("\n");
|
|
62
|
+
}
|
|
63
|
+
export function registerCortexTools(server) {
|
|
64
|
+
// ── cortex_ask: natural language → SQL → answer ─────────────────────────────
|
|
65
|
+
server.registerTool("cortex_ask", {
|
|
66
|
+
title: "Ask (Cortex text-to-SQL)",
|
|
67
|
+
description: "Answer a natural-language question about the data. Reads the schema, uses " +
|
|
68
|
+
"Cortex AI_COMPLETE to generate a read-only SQL query, runs it, and returns " +
|
|
69
|
+
"the rows plus the generated SQL. Best for analytical questions like " +
|
|
70
|
+
"'what is total revenue' or 'top 5 customers by orders'.",
|
|
71
|
+
inputSchema: {
|
|
72
|
+
question: z.string().describe("The natural-language question to answer."),
|
|
73
|
+
database: z.string().optional().describe("Defaults to SNOWFLAKE_DATABASE."),
|
|
74
|
+
schema: z.string().optional().describe("Defaults to SNOWFLAKE_SCHEMA."),
|
|
75
|
+
model: z.string().optional().describe("Cortex model. Defaults to SNOWFLAKE_CORTEX_MODEL."),
|
|
76
|
+
},
|
|
77
|
+
}, async (args) => {
|
|
78
|
+
try {
|
|
79
|
+
const c = getConfig();
|
|
80
|
+
const database = (await resolveDatabase(args.database)) || c.database;
|
|
81
|
+
const schema = args.schema || c.schema;
|
|
82
|
+
if (!database)
|
|
83
|
+
throw new Error("No database. Set SNOWFLAKE_DATABASE or pass `database`.");
|
|
84
|
+
const model = safeModel(args.model);
|
|
85
|
+
const ctx = await schemaContext(database, schema);
|
|
86
|
+
if (!ctx)
|
|
87
|
+
throw new Error(`No tables found in ${database}${schema ? "." + schema : ""}.`);
|
|
88
|
+
const prompt = `You are a Snowflake SQL expert. Using ONLY the schema below, write a single ` +
|
|
89
|
+
`read-only Snowflake SQL query (SELECT or WITH only) that answers the question. ` +
|
|
90
|
+
`Fully qualify tables as ${database}.<schema>.<table>. Return ONLY the SQL with no ` +
|
|
91
|
+
`explanation and no markdown fences.\n\nSchema:\n${ctx}\n\nQuestion: ${args.question}\n\nSQL:`;
|
|
92
|
+
const gen = await runQuery(`SELECT AI_COMPLETE('${model}', ?) AS SQL_TEXT`, [prompt]);
|
|
93
|
+
const generatedSql = cleanSql(String((gen.rows[0]?.SQL_TEXT ?? "")));
|
|
94
|
+
if (!generatedSql)
|
|
95
|
+
throw new Error("Model did not return any SQL.");
|
|
96
|
+
// Generated SQL is ALWAYS restricted to read-only, regardless of server config.
|
|
97
|
+
if (!isReadOnlyStatement(generatedSql)) {
|
|
98
|
+
return jsonText({
|
|
99
|
+
question: args.question,
|
|
100
|
+
generated_sql: generatedSql,
|
|
101
|
+
error: "Generated SQL was not read-only and was blocked. Rephrase the question.",
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
const result = await runQuery(generatedSql);
|
|
105
|
+
const capped = result.rows.slice(0, c.rowLimit);
|
|
106
|
+
return jsonText({
|
|
107
|
+
question: args.question,
|
|
108
|
+
generated_sql: generatedSql,
|
|
109
|
+
row_count: result.rowCount,
|
|
110
|
+
returned: capped.length,
|
|
111
|
+
truncated: result.rowCount > capped.length,
|
|
112
|
+
rows: capped,
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
catch (err) {
|
|
116
|
+
return cortexErr(err);
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
// ── cortex_complete: general LLM completion ─────────────────────────────────
|
|
120
|
+
server.registerTool("cortex_complete", {
|
|
121
|
+
title: "Complete (Cortex AI_COMPLETE)",
|
|
122
|
+
description: "Generate a free-form LLM completion for a prompt using Cortex AI_COMPLETE. " +
|
|
123
|
+
"Use for summaries, explanations, or generation that isn't a SQL query.",
|
|
124
|
+
inputSchema: {
|
|
125
|
+
prompt: z.string().describe("The prompt to send to the model."),
|
|
126
|
+
model: z.string().optional().describe("Cortex model. Defaults to SNOWFLAKE_CORTEX_MODEL."),
|
|
127
|
+
},
|
|
128
|
+
}, async (args) => {
|
|
129
|
+
try {
|
|
130
|
+
const model = safeModel(args.model);
|
|
131
|
+
const { rows } = await runQuery(`SELECT AI_COMPLETE('${model}', ?) AS RESPONSE`, [
|
|
132
|
+
args.prompt,
|
|
133
|
+
]);
|
|
134
|
+
return jsonText({ model, response: rows[0]?.RESPONSE ?? null });
|
|
135
|
+
}
|
|
136
|
+
catch (err) {
|
|
137
|
+
return cortexErr(err);
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
// ── cortex_sentiment: sentiment of a text ───────────────────────────────────
|
|
141
|
+
server.registerTool("cortex_sentiment", {
|
|
142
|
+
title: "Sentiment (Cortex AI_SENTIMENT)",
|
|
143
|
+
description: "Return the sentiment of a text string using Cortex AI_SENTIMENT.",
|
|
144
|
+
inputSchema: { text: z.string().describe("The text to analyze.") },
|
|
145
|
+
}, async (args) => {
|
|
146
|
+
try {
|
|
147
|
+
const { rows } = await runQuery(`SELECT AI_SENTIMENT(?) AS SENTIMENT`, [args.text]);
|
|
148
|
+
return jsonText({ sentiment: rows[0]?.SENTIMENT ?? null });
|
|
149
|
+
}
|
|
150
|
+
catch (err) {
|
|
151
|
+
return cortexErr(err);
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
// ── cortex_summarize: summarize a text ──────────────────────────────────────
|
|
155
|
+
server.registerTool("cortex_summarize", {
|
|
156
|
+
title: "Summarize (Cortex SUMMARIZE)",
|
|
157
|
+
description: "Return a concise summary of a text string using Cortex.",
|
|
158
|
+
inputSchema: { text: z.string().describe("The text to summarize.") },
|
|
159
|
+
}, async (args) => {
|
|
160
|
+
try {
|
|
161
|
+
const { rows } = await runQuery(`SELECT SNOWFLAKE.CORTEX.SUMMARIZE(?) AS SUMMARY`, [
|
|
162
|
+
args.text,
|
|
163
|
+
]);
|
|
164
|
+
return jsonText({ summary: rows[0]?.SUMMARY ?? null });
|
|
165
|
+
}
|
|
166
|
+
catch (err) {
|
|
167
|
+
return cortexErr(err);
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
// ── cortex_classify: classify into user categories ──────────────────────────
|
|
171
|
+
server.registerTool("cortex_classify", {
|
|
172
|
+
title: "Classify (Cortex AI_CLASSIFY)",
|
|
173
|
+
description: "Classify a text string into one of the provided categories using Cortex AI_CLASSIFY.",
|
|
174
|
+
inputSchema: {
|
|
175
|
+
text: z.string().describe("The text to classify."),
|
|
176
|
+
categories: z
|
|
177
|
+
.array(z.string())
|
|
178
|
+
.min(2)
|
|
179
|
+
.describe("Two or more category labels to choose from."),
|
|
180
|
+
},
|
|
181
|
+
}, async (args) => {
|
|
182
|
+
try {
|
|
183
|
+
// Build a SQL array literal of escaped string categories; bind the input.
|
|
184
|
+
const cats = args.categories
|
|
185
|
+
.map((x) => `'${String(x).replace(/'/g, "''")}'`)
|
|
186
|
+
.join(", ");
|
|
187
|
+
const { rows } = await runQuery(`SELECT AI_CLASSIFY(?, [${cats}]) AS CLASSIFICATION`, [args.text]);
|
|
188
|
+
return jsonText({ classification: rows[0]?.CLASSIFICATION ?? null });
|
|
189
|
+
}
|
|
190
|
+
catch (err) {
|
|
191
|
+
return cortexErr(err);
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
// ── cortex_translate: translate text ────────────────────────────────────────
|
|
195
|
+
server.registerTool("cortex_translate", {
|
|
196
|
+
title: "Translate (Cortex TRANSLATE)",
|
|
197
|
+
description: "Translate text to a target language using Cortex. Source language is " +
|
|
198
|
+
"auto-detected when omitted.",
|
|
199
|
+
inputSchema: {
|
|
200
|
+
text: z.string().describe("The text to translate."),
|
|
201
|
+
to_language: z.string().describe("Target language code, e.g. 'en', 'hi', 'es'."),
|
|
202
|
+
from_language: z
|
|
203
|
+
.string()
|
|
204
|
+
.optional()
|
|
205
|
+
.describe("Source language code. Empty = auto-detect."),
|
|
206
|
+
},
|
|
207
|
+
}, async (args) => {
|
|
208
|
+
try {
|
|
209
|
+
const { rows } = await runQuery(`SELECT SNOWFLAKE.CORTEX.TRANSLATE(?, ?, ?) AS TRANSLATION`, [args.text, args.from_language || "", args.to_language]);
|
|
210
|
+
return jsonText({ translation: rows[0]?.TRANSLATION ?? null });
|
|
211
|
+
}
|
|
212
|
+
catch (err) {
|
|
213
|
+
return cortexErr(err);
|
|
214
|
+
}
|
|
215
|
+
});
|
|
216
|
+
}
|
package/dist/tools/index.d.ts
CHANGED
|
@@ -3,9 +3,9 @@
|
|
|
3
3
|
*
|
|
4
4
|
* `list_tables` is registered first and follows the platform's double-wrapped
|
|
5
5
|
* envelope contract (see listTables.ts). The remaining tools are standard
|
|
6
|
-
* SQL-database helpers
|
|
7
|
-
*
|
|
8
|
-
*
|
|
6
|
+
* SQL-database helpers (test_connection, describe_table, get_table_sample,
|
|
7
|
+
* execute_query, list_schemas, ...). Cortex AI tools are registered last and
|
|
8
|
+
* only when enabled.
|
|
9
9
|
*/
|
|
10
10
|
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
11
11
|
export declare function registerTools(server: McpServer): void;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/tools/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/tools/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAiCzE,wBAAgB,aAAa,CAAC,MAAM,EAAE,SAAS,GAAG,IAAI,CA2NrD"}
|
package/dist/tools/index.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import { getConfig, runQuery, quoteIdent, qualify, isReadOnlyStatement, } from "../snowflake.js";
|
|
2
|
+
import { getConfig, runQuery, quoteIdent, qualify, resolveDatabase, isReadOnlyStatement, } from "../snowflake.js";
|
|
3
3
|
import { registerListTables } from "./listTables.js";
|
|
4
|
+
import { registerCortexTools } from "./cortex.js";
|
|
4
5
|
function jsonText(value) {
|
|
5
6
|
return { content: [{ type: "text", text: JSON.stringify(value, null, 2) }] };
|
|
6
7
|
}
|
|
@@ -65,7 +66,7 @@ export function registerTools(server) {
|
|
|
65
66
|
},
|
|
66
67
|
}, async (args) => {
|
|
67
68
|
try {
|
|
68
|
-
const db = args.database || getConfig().database;
|
|
69
|
+
const db = (await resolveDatabase(args.database)) || getConfig().database;
|
|
69
70
|
if (!db)
|
|
70
71
|
throw new Error("No database. Set SNOWFLAKE_DATABASE or pass `database`.");
|
|
71
72
|
const { rows } = await runQuery(`SELECT SCHEMA_NAME FROM ${quoteIdent(db)}.INFORMATION_SCHEMA.SCHEMATA ORDER BY SCHEMA_NAME`);
|
|
@@ -104,7 +105,7 @@ export function registerTools(server) {
|
|
|
104
105
|
}, async (args) => {
|
|
105
106
|
try {
|
|
106
107
|
const c = getConfig();
|
|
107
|
-
const db = args.database || c.database;
|
|
108
|
+
const db = (await resolveDatabase(args.database)) || c.database;
|
|
108
109
|
const sc = args.schema || c.schema;
|
|
109
110
|
if (!db)
|
|
110
111
|
throw new Error("No database. Set SNOWFLAKE_DATABASE or pass `database`.");
|
|
@@ -140,7 +141,8 @@ export function registerTools(server) {
|
|
|
140
141
|
try {
|
|
141
142
|
const c = getConfig();
|
|
142
143
|
const limit = Math.min(args.limit ?? 10, c.rowLimit);
|
|
143
|
-
const
|
|
144
|
+
const db = (await resolveDatabase(args.database)) || c.database;
|
|
145
|
+
const fqn = qualify(args.table, db, args.schema);
|
|
144
146
|
const { rows } = await runQuery(`SELECT * FROM ${fqn} LIMIT ${limit}`);
|
|
145
147
|
return jsonText(rows);
|
|
146
148
|
}
|
|
@@ -177,4 +179,8 @@ export function registerTools(server) {
|
|
|
177
179
|
return errText(err);
|
|
178
180
|
}
|
|
179
181
|
});
|
|
182
|
+
// 9) Cortex AI tools (opt-out via SNOWFLAKE_CORTEX_ENABLED=false).
|
|
183
|
+
if (getConfig().cortexEnabled) {
|
|
184
|
+
registerCortexTools(server);
|
|
185
|
+
}
|
|
180
186
|
}
|
|
@@ -1,20 +1,19 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* listTables.ts — table-discovery tool for the Analytics Model platform.
|
|
3
3
|
*
|
|
4
|
-
* The platform's
|
|
5
|
-
*
|
|
6
|
-
* flat array of { table_name } objects.
|
|
4
|
+
* The platform's connector flow expects a `list_tables` tool whose result text,
|
|
5
|
+
* once unwrapped, yields a flat array of { table_name } objects.
|
|
7
6
|
*
|
|
8
|
-
* OUTPUT SHAPE —
|
|
9
|
-
* The platform parser expects:
|
|
7
|
+
* OUTPUT SHAPE — the platform parser expects:
|
|
10
8
|
* 1. The tool result text is JSON.stringify of an object with a `data` field.
|
|
11
9
|
* 2. `data` is ITSELF a JSON string of the [{ table_name }] array.
|
|
12
10
|
* i.e. the array is DOUBLE-stringified. Do not "simplify" this to a raw array
|
|
13
11
|
* or the backend parse fails and the table dropdown stays empty.
|
|
14
12
|
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
13
|
+
* Snowflake is a real database, so the live table list is read from
|
|
14
|
+
* INFORMATION_SCHEMA, scoped to the connection's database/schema (overridable
|
|
15
|
+
* via arguments). If a caller passes a database that isn't accessible, the tool
|
|
16
|
+
* falls back to the configured SNOWFLAKE_DATABASE.
|
|
18
17
|
*/
|
|
19
18
|
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
20
19
|
export declare function registerListTables(server: McpServer): void;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"listTables.d.ts","sourceRoot":"","sources":["../../src/tools/listTables.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"listTables.d.ts","sourceRoot":"","sources":["../../src/tools/listTables.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AACH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AA0BzE,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,SAAS,GAAG,IAAI,CA0F1D"}
|
package/dist/tools/listTables.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import { getConfig, runQuery, quoteIdent } from "../snowflake.js";
|
|
2
|
+
import { getConfig, runQuery, quoteIdent, resolveDatabase } from "../snowflake.js";
|
|
3
3
|
function envelope(tableNames, requestedPayload, message) {
|
|
4
4
|
return {
|
|
5
5
|
is_success: true,
|
|
@@ -31,7 +31,8 @@ export function registerListTables(server) {
|
|
|
31
31
|
},
|
|
32
32
|
}, async (args) => {
|
|
33
33
|
const c = getConfig();
|
|
34
|
-
|
|
34
|
+
// Resolve to an accessible database, falling back to SNOWFLAKE_DATABASE.
|
|
35
|
+
const database = (await resolveDatabase(args.database)) || c.database;
|
|
35
36
|
const schema = args.schema || c.schema;
|
|
36
37
|
const includeViews = args.include_views !== false;
|
|
37
38
|
const requestedPayload = { database, schema, include_views: includeViews };
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "snowflake-analytics-mcp-server",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Snowflake MCP server for the Analytics Model platform.
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Snowflake MCP server for the Analytics Model platform. Table discovery, SQL query, and Cortex AI tools over stdio, runnable via npx.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
7
7
|
"bin": {
|
|
@@ -35,4 +35,4 @@
|
|
|
35
35
|
"@types/snowflake-sdk": "^1.6.24",
|
|
36
36
|
"typescript": "^7.0.2"
|
|
37
37
|
}
|
|
38
|
-
}
|
|
38
|
+
}
|