redash-mcp 3.1.3 → 3.1.5
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/.dockerignore +17 -0
- package/Dockerfile +29 -0
- package/README.ja.md +21 -2
- package/README.ko.md +22 -3
- package/README.md +20 -1
- package/dist/index.js +43010 -512
- package/manifest.json +27 -2
- package/package.json +18 -5
- package/smithery.yaml +46 -0
- package/dist/bird/complexity.js +0 -87
- package/dist/bird/config.js +0 -73
- package/dist/bird/evaluation.js +0 -172
- package/dist/bird/feedback.js +0 -153
- package/dist/bird/few-shot.js +0 -95
- package/dist/bird/keyword-map.js +0 -56
- package/dist/bird/llm-table-selector.js +0 -59
- package/dist/bird/schema-pruning.js +0 -107
- package/dist/bird/smart-query.js +0 -122
- package/dist/bird/tools.js +0 -319
- package/dist/bird/types.js +0 -1
- package/dist/query-cache.js +0 -87
- package/dist/redash-client.js +0 -124
- package/dist/setup.js +0 -137
- package/dist/sql-guard.js +0 -151
- package/dist/tool-error.js +0 -40
package/dist/sql-guard.js
DELETED
|
@@ -1,151 +0,0 @@
|
|
|
1
|
-
function getConfig() {
|
|
2
|
-
const raw = process.env.REDASH_SAFETY_MODE ?? "warn";
|
|
3
|
-
const mode = ["off", "warn", "strict"].includes(raw) ? raw : "warn";
|
|
4
|
-
return {
|
|
5
|
-
mode,
|
|
6
|
-
disablePii: process.env.REDASH_SAFETY_DISABLE_PII === "true",
|
|
7
|
-
disableCost: process.env.REDASH_SAFETY_DISABLE_COST === "true",
|
|
8
|
-
autoLimit: parseInt(process.env.REDASH_AUTO_LIMIT ?? "0", 10) || 0,
|
|
9
|
-
};
|
|
10
|
-
}
|
|
11
|
-
function normalizeForAnalysis(sql) {
|
|
12
|
-
return sql
|
|
13
|
-
.replace(/--[^\n]*/g, " ")
|
|
14
|
-
.replace(/\/\*[\s\S]*?\*\//g, " ")
|
|
15
|
-
.replace(/\s+/g, " ")
|
|
16
|
-
.trim()
|
|
17
|
-
.toUpperCase();
|
|
18
|
-
}
|
|
19
|
-
function hasWhere(sql) {
|
|
20
|
-
return /\bWHERE\b/.test(sql);
|
|
21
|
-
}
|
|
22
|
-
function hasLimit(sql) {
|
|
23
|
-
return /\bLIMIT\b/.test(sql);
|
|
24
|
-
}
|
|
25
|
-
function isSelect(sql) {
|
|
26
|
-
return /^\s*(SELECT|WITH)\b/i.test(sql);
|
|
27
|
-
}
|
|
28
|
-
function injectLimit(sql, limit) {
|
|
29
|
-
if (/\bLIMIT\b/i.test(sql))
|
|
30
|
-
return sql;
|
|
31
|
-
if (!isSelect(sql))
|
|
32
|
-
return sql;
|
|
33
|
-
return `${sql.trimEnd()} LIMIT ${limit}`;
|
|
34
|
-
}
|
|
35
|
-
export function analyzeQuery(sql) {
|
|
36
|
-
const config = getConfig();
|
|
37
|
-
if (config.mode === "off") {
|
|
38
|
-
return { blocked: false, warnings: [], message: "" };
|
|
39
|
-
}
|
|
40
|
-
const upper = normalizeForAnalysis(sql);
|
|
41
|
-
const warnings = [];
|
|
42
|
-
let modifiedQuery;
|
|
43
|
-
if (/\bDROP\s+(TABLE|DATABASE|SCHEMA|VIEW|INDEX|FUNCTION)\b/.test(upper)) {
|
|
44
|
-
return {
|
|
45
|
-
blocked: true,
|
|
46
|
-
warnings: [],
|
|
47
|
-
message: "🚫 Query blocked.\n\nReason: DROP statements permanently delete data/schema.\nRule: DESTRUCTIVE / DROP\n\nSet REDASH_SAFETY_MODE=off to disable this check.",
|
|
48
|
-
};
|
|
49
|
-
}
|
|
50
|
-
if (/\bTRUNCATE\b/.test(upper)) {
|
|
51
|
-
return {
|
|
52
|
-
blocked: true,
|
|
53
|
-
warnings: [],
|
|
54
|
-
message: "🚫 Query blocked.\n\nReason: TRUNCATE deletes all data from the table.\nRule: DESTRUCTIVE / TRUNCATE",
|
|
55
|
-
};
|
|
56
|
-
}
|
|
57
|
-
if (/\bALTER\s+TABLE\b/.test(upper)) {
|
|
58
|
-
return {
|
|
59
|
-
blocked: true,
|
|
60
|
-
warnings: [],
|
|
61
|
-
message: "🚫 Query blocked.\n\nReason: ALTER TABLE modifies schema and requires prior coordination.\nRule: DESTRUCTIVE / ALTER_TABLE",
|
|
62
|
-
};
|
|
63
|
-
}
|
|
64
|
-
if (/\b(GRANT|REVOKE)\b/.test(upper)) {
|
|
65
|
-
return {
|
|
66
|
-
blocked: true,
|
|
67
|
-
warnings: [],
|
|
68
|
-
message: "🚫 Query blocked.\n\nReason: GRANT/REVOKE permission changes are not allowed.\nRule: DESTRUCTIVE / PRIVILEGE_CHANGE",
|
|
69
|
-
};
|
|
70
|
-
}
|
|
71
|
-
if (/\bDELETE\s+FROM\b/.test(upper) && !hasWhere(upper)) {
|
|
72
|
-
return {
|
|
73
|
-
blocked: true,
|
|
74
|
-
warnings: [],
|
|
75
|
-
message: "🚫 Query blocked.\n\nReason: DELETE without WHERE clause will delete all rows.\nRule: DESTRUCTIVE / DELETE_WITHOUT_WHERE\n\nSafe example:\n DELETE FROM orders WHERE created_at < '2024-01-01'",
|
|
76
|
-
};
|
|
77
|
-
}
|
|
78
|
-
if (/\bUPDATE\b/.test(upper) && /\bSET\b/.test(upper) && !hasWhere(upper)) {
|
|
79
|
-
return {
|
|
80
|
-
blocked: true,
|
|
81
|
-
warnings: [],
|
|
82
|
-
message: "🚫 Query blocked.\n\nReason: UPDATE without WHERE clause will modify all rows.\nRule: DESTRUCTIVE / UPDATE_WITHOUT_WHERE\n\nSafe example:\n UPDATE orders SET status = 'cancelled' WHERE created_at < '2024-01-01'",
|
|
83
|
-
};
|
|
84
|
-
}
|
|
85
|
-
if (/\bDELETE\s+FROM\b/.test(upper)) {
|
|
86
|
-
warnings.push("[DESTRUCTIVE] DELETE query detected. Please verify the WHERE clause.");
|
|
87
|
-
}
|
|
88
|
-
if (/\bUPDATE\b/.test(upper) && /\bSET\b/.test(upper)) {
|
|
89
|
-
warnings.push("[DESTRUCTIVE] UPDATE query detected. Please verify the WHERE clause.");
|
|
90
|
-
}
|
|
91
|
-
if (!config.disableCost && isSelect(sql)) {
|
|
92
|
-
const hasSelectStar = /SELECT\s+\*/.test(upper) || /SELECT\s+[\w.]+\.\*/.test(upper);
|
|
93
|
-
const noWhere = !hasWhere(upper);
|
|
94
|
-
const noLimit = !hasLimit(upper);
|
|
95
|
-
if (hasSelectStar) {
|
|
96
|
-
warnings.push("[COST] SELECT * detected. Specify only needed columns to reduce scan costs.");
|
|
97
|
-
}
|
|
98
|
-
if (noWhere) {
|
|
99
|
-
warnings.push("[COST] No WHERE clause. Consider adding date or condition filters.");
|
|
100
|
-
}
|
|
101
|
-
if (noLimit) {
|
|
102
|
-
if (config.autoLimit > 0) {
|
|
103
|
-
modifiedQuery = injectLimit(sql, config.autoLimit);
|
|
104
|
-
warnings.push(`[COST] No LIMIT clause — auto-appended LIMIT ${config.autoLimit}. Specify an explicit LIMIT if you need all rows.`);
|
|
105
|
-
}
|
|
106
|
-
else {
|
|
107
|
-
warnings.push("[COST] No LIMIT clause. Full table scans on large tables may incur significant costs.");
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
if (config.mode === "strict") {
|
|
111
|
-
const costWarnings = warnings.filter((w) => w.startsWith("[COST]"));
|
|
112
|
-
if (costWarnings.length > 0) {
|
|
113
|
-
return {
|
|
114
|
-
blocked: true,
|
|
115
|
-
warnings: [],
|
|
116
|
-
message: `🚫 Query blocked (strict mode).\n\n${costWarnings.join("\n")}\n\nSet REDASH_SAFETY_MODE=warn to allow with warnings.`,
|
|
117
|
-
};
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
if (!config.disablePii) {
|
|
122
|
-
const piiPatterns = [
|
|
123
|
-
"EMAIL",
|
|
124
|
-
"PHONE",
|
|
125
|
-
"PASSWORD",
|
|
126
|
-
"PASSWD",
|
|
127
|
-
"SSN",
|
|
128
|
-
"SOCIAL_SECURITY",
|
|
129
|
-
"CREDIT_CARD",
|
|
130
|
-
"CARD_NUMBER",
|
|
131
|
-
];
|
|
132
|
-
const matched = piiPatterns.filter((k) => upper.includes(k));
|
|
133
|
-
if (matched.length > 0) {
|
|
134
|
-
warnings.push(`[PII] Sensitive data columns detected: ${matched.join(", ")}. Please verify your data privacy compliance.`);
|
|
135
|
-
}
|
|
136
|
-
if (config.mode === "strict") {
|
|
137
|
-
const piiWarnings = warnings.filter((w) => w.startsWith("[PII]"));
|
|
138
|
-
if (piiWarnings.length > 0) {
|
|
139
|
-
return {
|
|
140
|
-
blocked: true,
|
|
141
|
-
warnings: [],
|
|
142
|
-
message: `🚫 Query blocked (strict mode).\n\n${piiWarnings.join("\n")}\n\nSet REDASH_SAFETY_MODE=warn to allow with warnings.`,
|
|
143
|
-
};
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
const message = warnings.length > 0
|
|
148
|
-
? `⚠️ Safety warnings (query will still execute)\n\n${warnings.join("\n")}\n\n---`
|
|
149
|
-
: "";
|
|
150
|
-
return { blocked: false, warnings, message, modifiedQuery };
|
|
151
|
-
}
|
package/dist/tool-error.js
DELETED
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Shared error handler for MCP tool callbacks.
|
|
3
|
-
* Logs the full error to stderr for debugging, then returns a
|
|
4
|
-
* user-friendly MCP error response without leaking internal details.
|
|
5
|
-
*/
|
|
6
|
-
export function handleToolError(toolName, error) {
|
|
7
|
-
const detail = error instanceof Error ? error.message : String(error);
|
|
8
|
-
// Log full detail to stderr so operators can debug.
|
|
9
|
-
console.error(`[${toolName}] ${detail}`);
|
|
10
|
-
// Derive a safe, user-facing message.
|
|
11
|
-
let userMessage;
|
|
12
|
-
if (detail.includes("Query timed out")) {
|
|
13
|
-
userMessage = "The query timed out. Try simplifying the query or increasing timeout_secs.";
|
|
14
|
-
}
|
|
15
|
-
else if (detail.includes("Query failed")) {
|
|
16
|
-
userMessage = "The query execution failed. Please check the SQL syntax and try again.";
|
|
17
|
-
}
|
|
18
|
-
else if (detail.includes("401") || detail.includes("Check your REDASH_API_KEY")) {
|
|
19
|
-
userMessage = "Authentication failed. Please verify your Redash API key.";
|
|
20
|
-
}
|
|
21
|
-
else if (detail.includes("403") || detail.includes("Access denied")) {
|
|
22
|
-
userMessage = "Access denied. You do not have permission for this resource.";
|
|
23
|
-
}
|
|
24
|
-
else if (/\b404\b/.test(detail) || detail.includes("Redash API error (404)")) {
|
|
25
|
-
userMessage = "The requested resource was not found. Please check the ID and try again.";
|
|
26
|
-
}
|
|
27
|
-
else if (detail.includes("Redash API error")) {
|
|
28
|
-
userMessage = "The Redash API returned an error. Please try again later.";
|
|
29
|
-
}
|
|
30
|
-
else if (detail.includes("fetch failed") || detail.includes("ECONNREFUSED") || detail.includes("ENOTFOUND")) {
|
|
31
|
-
userMessage = "Unable to connect to the Redash server. Please check REDASH_URL and network connectivity.";
|
|
32
|
-
}
|
|
33
|
-
else {
|
|
34
|
-
userMessage = "An unexpected error occurred. Please try again or check the server logs.";
|
|
35
|
-
}
|
|
36
|
-
return {
|
|
37
|
-
content: [{ type: "text", text: userMessage }],
|
|
38
|
-
isError: true,
|
|
39
|
-
};
|
|
40
|
-
}
|