redash-mcp 3.0.4 → 3.1.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.ko.md +4 -0
- package/README.md +4 -0
- package/dist/bird/evaluation.js +2 -14
- package/dist/bird/feedback.js +1 -2
- package/dist/bird/few-shot.js +1 -1
- package/dist/bird/smart-query.js +7 -2
- package/dist/bird/tools.js +5 -5
- package/dist/index.js +25 -8
- package/dist/query-cache.js +15 -0
- package/dist/redash-client.js +46 -10
- package/package.json +10 -7
package/README.ko.md
CHANGED
|
@@ -136,6 +136,10 @@ Redash → 우측 상단 프로필 → **Edit Profile** → **API Key** 복사
|
|
|
136
136
|
| `REDASH_DEFAULT_MAX_AGE` | `0` | Redash 캐시 TTL (초) |
|
|
137
137
|
| `REDASH_MCP_CACHE_TTL` | `300` | MCP 쿼리 캐시 TTL (초, 0 = 비활성화) |
|
|
138
138
|
| `REDASH_MCP_CACHE_MAX_MB` | `50` | MCP 쿼리 캐시 최대 메모리 (MB) |
|
|
139
|
+
| `REDASH_MCP_CONFIG_DIR` | `~/.redash-mcp` | BIRD few-shot/피드백/평가/키워드맵 저장 디렉터리 |
|
|
140
|
+
| `REDASH_BIRD_ENABLED` | `true` | `false`로 설정하면 BIRD smart query 도구 비활성화 |
|
|
141
|
+
| `REDASH_HTTP_TIMEOUT_SECS` | `30` | Redash API 요청 단위 HTTP 타임아웃 |
|
|
142
|
+
| `ANTHROPIC_API_KEY` | — | 설정 시 BIRD smart_query가 키워드 매칭 실패 시 Claude Haiku로 테이블 선택 폴백 |
|
|
139
143
|
|
|
140
144
|
---
|
|
141
145
|
|
package/README.md
CHANGED
|
@@ -136,6 +136,10 @@ Open `~/.claude/settings.json` and add:
|
|
|
136
136
|
| `REDASH_DEFAULT_MAX_AGE` | `0` | Redash cache TTL in seconds |
|
|
137
137
|
| `REDASH_MCP_CACHE_TTL` | `300` | MCP query cache TTL in seconds (0 = disabled) |
|
|
138
138
|
| `REDASH_MCP_CACHE_MAX_MB` | `50` | Max memory for MCP query cache in MB |
|
|
139
|
+
| `REDASH_MCP_CONFIG_DIR` | `~/.redash-mcp` | Directory for BIRD few-shot, feedback, eval, keyword-map data |
|
|
140
|
+
| `REDASH_BIRD_ENABLED` | `true` | Set to `false` to disable BIRD smart query tools |
|
|
141
|
+
| `REDASH_HTTP_TIMEOUT_SECS` | `30` | Per-request HTTP timeout against the Redash API |
|
|
142
|
+
| `ANTHROPIC_API_KEY` | — | If set, BIRD smart_query falls back to Claude Haiku for table selection when keyword scoring fails |
|
|
139
143
|
|
|
140
144
|
---
|
|
141
145
|
|
package/dist/bird/evaluation.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { readFile, writeFile } from "node:fs/promises";
|
|
2
2
|
import { ensureConfigDir, getDataSourcePath } from "./config.js";
|
|
3
|
-
import { redashFetch } from "../redash-client.js";
|
|
3
|
+
import { redashFetch, pollQueryResult } from "../redash-client.js";
|
|
4
4
|
export async function loadTestSuite(dataSourceId) {
|
|
5
5
|
try {
|
|
6
6
|
const raw = await readFile(getDataSourcePath("eval", dataSourceId), "utf-8");
|
|
@@ -135,19 +135,7 @@ async function executeQuery(dataSourceId, sql) {
|
|
|
135
135
|
});
|
|
136
136
|
let result;
|
|
137
137
|
if (res.job) {
|
|
138
|
-
|
|
139
|
-
const job = await redashFetch(`/jobs/${res.job.id}`);
|
|
140
|
-
if (job.job.status === 3) {
|
|
141
|
-
result = await redashFetch(`/query_results/${job.job.query_result_id}`);
|
|
142
|
-
break;
|
|
143
|
-
}
|
|
144
|
-
if (job.job.status === 4) {
|
|
145
|
-
throw new Error(`Query failed: ${job.job.error}`);
|
|
146
|
-
}
|
|
147
|
-
await new Promise((r) => setTimeout(r, 1000));
|
|
148
|
-
}
|
|
149
|
-
if (!result)
|
|
150
|
-
throw new Error("Query timed out");
|
|
138
|
+
result = await pollQueryResult(res.job.id, 30);
|
|
151
139
|
}
|
|
152
140
|
else {
|
|
153
141
|
result = res;
|
package/dist/bird/feedback.js
CHANGED
|
@@ -29,7 +29,6 @@ export async function recordFeedback(dataSourceId, entry) {
|
|
|
29
29
|
createdAt: new Date().toISOString(),
|
|
30
30
|
};
|
|
31
31
|
entries.push(newEntry);
|
|
32
|
-
await saveFeedback(dataSourceId, entries);
|
|
33
32
|
if (newEntry.rating === "down" && newEntry.correctSql && newEntry.errorType) {
|
|
34
33
|
const config = await loadConfig();
|
|
35
34
|
if (config.bird.feedback.enabled) {
|
|
@@ -37,10 +36,10 @@ export async function recordFeedback(dataSourceId, entry) {
|
|
|
37
36
|
if (sameErrorCount >= config.bird.feedback.autoPromoteThreshold) {
|
|
38
37
|
await promoteToFewShot(dataSourceId, newEntry);
|
|
39
38
|
newEntry.promotedToFewShot = true;
|
|
40
|
-
await saveFeedback(dataSourceId, entries);
|
|
41
39
|
}
|
|
42
40
|
}
|
|
43
41
|
}
|
|
42
|
+
await saveFeedback(dataSourceId, entries);
|
|
44
43
|
return newEntry;
|
|
45
44
|
}
|
|
46
45
|
export function classifyError(generatedSql, correctSql) {
|
package/dist/bird/few-shot.js
CHANGED
|
@@ -83,7 +83,7 @@ function tokenize(text) {
|
|
|
83
83
|
"it", "its", "this", "that", "these", "those", "what", "which",
|
|
84
84
|
"who", "whom", "how", "where", "when", "why",
|
|
85
85
|
"show", "give", "tell", "get", "find", "list", "display",
|
|
86
|
-
"의", "
|
|
86
|
+
"의", "가", "이", "은", "는", "을", "를", "에", "에서", "와", "과",
|
|
87
87
|
"도", "로", "으로", "만", "까지", "부터", "에게", "한테", "께",
|
|
88
88
|
"좀", "해줘", "알려줘", "보여줘", "해", "하는", "된", "인",
|
|
89
89
|
]);
|
package/dist/bird/smart-query.js
CHANGED
|
@@ -99,8 +99,13 @@ function detectVagueness(question, prunedTables) {
|
|
|
99
99
|
if (hasTimeKeyword && !hasTimeSpecifier) {
|
|
100
100
|
clarifications.push("Which specific time period? (e.g., last 7 days, last month, 2025-01-01 ~ 2025-03-31)");
|
|
101
101
|
}
|
|
102
|
-
const
|
|
103
|
-
|
|
102
|
+
const trimmed = question.trim();
|
|
103
|
+
const wordCount = trimmed.split(/\s+/).length;
|
|
104
|
+
const hasCJK = /[ㄱ-힝]/.test(trimmed);
|
|
105
|
+
const tooShort = hasCJK
|
|
106
|
+
? trimmed.replace(/\s+/g, "").length < 6
|
|
107
|
+
: wordCount < 4;
|
|
108
|
+
if (tooShort) {
|
|
104
109
|
clarifications.push("Could you provide more details about what data you need?");
|
|
105
110
|
}
|
|
106
111
|
const maxScore = Math.max(...prunedTables.map((t) => t.score), 0);
|
package/dist/bird/tools.js
CHANGED
|
@@ -64,7 +64,7 @@ export function registerBirdTools(server) {
|
|
|
64
64
|
.optional()
|
|
65
65
|
.describe("Example to add (required when action=add)"),
|
|
66
66
|
example_id: z.string().optional().describe("Example ID to remove (required when action=remove)"),
|
|
67
|
-
}, {
|
|
67
|
+
}, {}, async ({ data_source_id, action, example, example_id }) => {
|
|
68
68
|
try {
|
|
69
69
|
if (action === "list") {
|
|
70
70
|
const examples = await loadExamples(data_source_id);
|
|
@@ -118,7 +118,7 @@ export function registerBirdTools(server) {
|
|
|
118
118
|
generated_sql: z.string().describe("Generated SQL"),
|
|
119
119
|
correct_sql: z.string().optional().describe("Correct SQL (provide when rating=down for automatic learning)"),
|
|
120
120
|
rating: z.enum(["up", "down"]).describe("Rating: up (correct) or down (incorrect)"),
|
|
121
|
-
}, {
|
|
121
|
+
}, {}, async ({ data_source_id, question, generated_sql, correct_sql, rating }) => {
|
|
122
122
|
try {
|
|
123
123
|
const entry = await recordFeedback(data_source_id, {
|
|
124
124
|
question,
|
|
@@ -159,7 +159,7 @@ export function registerBirdTools(server) {
|
|
|
159
159
|
}))
|
|
160
160
|
.optional()
|
|
161
161
|
.describe("List of SQL to evaluate (required when action=run)"),
|
|
162
|
-
}, {
|
|
162
|
+
}, {}, async ({ data_source_id, action, test_case, test_case_id, generated_sqls }) => {
|
|
163
163
|
try {
|
|
164
164
|
if (action === "list_tests") {
|
|
165
165
|
const store = await loadTestSuite(data_source_id);
|
|
@@ -227,14 +227,14 @@ export function registerBirdTools(server) {
|
|
|
227
227
|
data_source_id: z.number().describe("Data source ID"),
|
|
228
228
|
action: z.enum(["list", "add", "remove", "reset"]).describe("Action to perform"),
|
|
229
229
|
mappings: z
|
|
230
|
-
.record(z.array(z.string()))
|
|
230
|
+
.record(z.string(), z.array(z.string()))
|
|
231
231
|
.optional()
|
|
232
232
|
.describe("Mappings to add (required when action=add). e.g., {\"revenue\": [\"payment\", \"billing\"]}"),
|
|
233
233
|
keywords: z
|
|
234
234
|
.array(z.string())
|
|
235
235
|
.optional()
|
|
236
236
|
.describe("Keywords to remove (required when action=remove). e.g., [\"revenue\", \"order\"]"),
|
|
237
|
-
}, {
|
|
237
|
+
}, {}, async ({ data_source_id, action, mappings, keywords }) => {
|
|
238
238
|
try {
|
|
239
239
|
if (action === "list") {
|
|
240
240
|
const effective = await getEffectiveMap(data_source_id);
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import path from "node:path";
|
|
2
5
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
6
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
7
|
import { z } from "zod";
|
|
@@ -16,9 +19,20 @@ if (!REDASH_URL || !REDASH_API_KEY) {
|
|
|
16
19
|
console.error("REDASH_URL and REDASH_API_KEY environment variables are required");
|
|
17
20
|
process.exit(1);
|
|
18
21
|
}
|
|
22
|
+
function readPackageVersion() {
|
|
23
|
+
try {
|
|
24
|
+
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
25
|
+
const pkgPath = path.join(here, "..", "package.json");
|
|
26
|
+
const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
|
|
27
|
+
return typeof pkg.version === "string" ? pkg.version : "0.0.0";
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
return "0.0.0";
|
|
31
|
+
}
|
|
32
|
+
}
|
|
19
33
|
const server = new McpServer({
|
|
20
34
|
name: "redash-mcp",
|
|
21
|
-
version:
|
|
35
|
+
version: readPackageVersion(),
|
|
22
36
|
});
|
|
23
37
|
server.tool("list_data_sources", "List connected data sources (id, name, type). Call this first to get data_source_id.", {}, { readOnlyHint: true }, async () => {
|
|
24
38
|
try {
|
|
@@ -174,9 +188,10 @@ server.tool("list_queries", "List saved queries in Redash.", {
|
|
|
174
188
|
page_size: z.number().optional().default(20).describe("Page size (max 100)"),
|
|
175
189
|
}, { readOnlyHint: true }, async ({ search, page, page_size }) => {
|
|
176
190
|
try {
|
|
177
|
-
const effectivePageSize = Math.min(page_size, 100);
|
|
191
|
+
const effectivePageSize = Math.max(1, Math.min(page_size, 100));
|
|
192
|
+
const effectivePage = Math.max(1, page);
|
|
178
193
|
const params = new URLSearchParams({
|
|
179
|
-
page: String(
|
|
194
|
+
page: String(effectivePage),
|
|
180
195
|
page_size: String(effectivePageSize),
|
|
181
196
|
...(search ? { q: search } : {}),
|
|
182
197
|
});
|
|
@@ -200,7 +215,8 @@ server.tool("get_query_result", "Execute a saved query by ID and return results.
|
|
|
200
215
|
query_id: z.number().describe("Saved query ID (from list_queries)"),
|
|
201
216
|
max_rows: z.number().optional().default(100).describe("Max rows to return (default 100)"),
|
|
202
217
|
format: z.enum(["table", "json"]).optional().default("table").describe("Output format: table (markdown) or json"),
|
|
203
|
-
|
|
218
|
+
timeout_secs: z.number().optional().default(30).describe("Query execution timeout in seconds"),
|
|
219
|
+
}, { readOnlyHint: true }, async ({ query_id, max_rows, format, timeout_secs }) => {
|
|
204
220
|
try {
|
|
205
221
|
const res = await redashFetch(`/queries/${query_id}/results`, {
|
|
206
222
|
method: "POST",
|
|
@@ -208,7 +224,7 @@ server.tool("get_query_result", "Execute a saved query by ID and return results.
|
|
|
208
224
|
});
|
|
209
225
|
let result;
|
|
210
226
|
if (res.job) {
|
|
211
|
-
result = await pollQueryResult(res.job.id);
|
|
227
|
+
result = await pollQueryResult(res.job.id, timeout_secs);
|
|
212
228
|
}
|
|
213
229
|
else {
|
|
214
230
|
result = res;
|
|
@@ -371,9 +387,10 @@ server.tool("list_dashboards", "List dashboards in Redash.", {
|
|
|
371
387
|
page_size: z.number().optional().default(20).describe("Page size (max 100)"),
|
|
372
388
|
}, { readOnlyHint: true }, async ({ search, page, page_size }) => {
|
|
373
389
|
try {
|
|
374
|
-
const effectivePageSize = Math.min(page_size, 100);
|
|
390
|
+
const effectivePageSize = Math.max(1, Math.min(page_size, 100));
|
|
391
|
+
const effectivePage = Math.max(1, page);
|
|
375
392
|
const params = new URLSearchParams({
|
|
376
|
-
page: String(
|
|
393
|
+
page: String(effectivePage),
|
|
377
394
|
page_size: String(effectivePageSize),
|
|
378
395
|
...(search ? { q: search } : {}),
|
|
379
396
|
});
|
|
@@ -447,7 +464,7 @@ server.tool("add_widget", "Add a visualization widget to a dashboard. Get visual
|
|
|
447
464
|
dashboard_id: z.number().describe("Dashboard ID"),
|
|
448
465
|
visualization_id: z.number().describe("Visualization ID (from get_query's visualizations)"),
|
|
449
466
|
text: z.string().optional().default("").describe("Widget text"),
|
|
450
|
-
width: z.
|
|
467
|
+
width: z.union([z.literal(1), z.literal(2)]).optional().default(1).describe("Widget width (1 = half, 2 = full)"),
|
|
451
468
|
}, { destructiveHint: true }, async ({ dashboard_id, visualization_id, text, width }) => {
|
|
452
469
|
try {
|
|
453
470
|
const data = await redashFetch("/widgets", {
|
package/dist/query-cache.js
CHANGED
|
@@ -25,6 +25,17 @@ function makeCacheKey(dataSourceId, sql) {
|
|
|
25
25
|
function roughSize(obj) {
|
|
26
26
|
return JSON.stringify(obj).length * 2;
|
|
27
27
|
}
|
|
28
|
+
function purgeExpired(ttl) {
|
|
29
|
+
if (ttl === 0)
|
|
30
|
+
return;
|
|
31
|
+
const now = Date.now();
|
|
32
|
+
for (const [key, entry] of cache) {
|
|
33
|
+
if (now - entry.ts > ttl) {
|
|
34
|
+
totalSizeBytes -= entry.size;
|
|
35
|
+
cache.delete(key);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
28
39
|
export function getCached(dataSourceId, sql) {
|
|
29
40
|
const ttl = getCacheTtlMs();
|
|
30
41
|
if (ttl === 0)
|
|
@@ -38,6 +49,9 @@ export function getCached(dataSourceId, sql) {
|
|
|
38
49
|
cache.delete(key);
|
|
39
50
|
return null;
|
|
40
51
|
}
|
|
52
|
+
// Touch entry to maintain LRU order via insertion-order Map semantics.
|
|
53
|
+
cache.delete(key);
|
|
54
|
+
cache.set(key, entry);
|
|
41
55
|
return entry.result;
|
|
42
56
|
}
|
|
43
57
|
export function setCached(dataSourceId, sql, result) {
|
|
@@ -48,6 +62,7 @@ export function setCached(dataSourceId, sql, result) {
|
|
|
48
62
|
const size = roughSize(result);
|
|
49
63
|
if (size > maxSize * 0.2)
|
|
50
64
|
return;
|
|
65
|
+
purgeExpired(ttl);
|
|
51
66
|
while (totalSizeBytes + size > maxSize && cache.size > 0) {
|
|
52
67
|
const oldestKey = cache.keys().next().value;
|
|
53
68
|
if (!oldestKey)
|
package/dist/redash-client.js
CHANGED
|
@@ -24,16 +24,35 @@ function validateRedashUrl(raw) {
|
|
|
24
24
|
}
|
|
25
25
|
const REDASH_URL = validateRedashUrl(process.env.REDASH_URL);
|
|
26
26
|
const REDASH_API_KEY = process.env.REDASH_API_KEY;
|
|
27
|
+
const HTTP_TIMEOUT_MS = (() => {
|
|
28
|
+
const raw = parseInt(process.env.REDASH_HTTP_TIMEOUT_SECS ?? "30", 10);
|
|
29
|
+
return (isNaN(raw) || raw <= 0 ? 30 : raw) * 1000;
|
|
30
|
+
})();
|
|
27
31
|
export { REDASH_URL, REDASH_API_KEY };
|
|
28
32
|
export async function redashFetch(path, options) {
|
|
29
|
-
const
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
...options
|
|
35
|
-
|
|
36
|
-
|
|
33
|
+
const controller = new AbortController();
|
|
34
|
+
const timer = setTimeout(() => controller.abort(), HTTP_TIMEOUT_MS);
|
|
35
|
+
let res;
|
|
36
|
+
try {
|
|
37
|
+
res = await fetch(`${REDASH_URL}/api${path}`, {
|
|
38
|
+
...options,
|
|
39
|
+
signal: options?.signal ?? controller.signal,
|
|
40
|
+
headers: {
|
|
41
|
+
"Authorization": `Key ${REDASH_API_KEY}`,
|
|
42
|
+
"Content-Type": "application/json",
|
|
43
|
+
...options?.headers,
|
|
44
|
+
},
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
catch (err) {
|
|
48
|
+
if (err?.name === "AbortError") {
|
|
49
|
+
throw new Error(`Redash request timed out after ${HTTP_TIMEOUT_MS / 1000}s`);
|
|
50
|
+
}
|
|
51
|
+
throw err;
|
|
52
|
+
}
|
|
53
|
+
finally {
|
|
54
|
+
clearTimeout(timer);
|
|
55
|
+
}
|
|
37
56
|
if (!res.ok) {
|
|
38
57
|
let hint = "";
|
|
39
58
|
if (res.status === 401)
|
|
@@ -50,7 +69,9 @@ export async function redashFetch(path, options) {
|
|
|
50
69
|
return res.json();
|
|
51
70
|
}
|
|
52
71
|
export async function pollQueryResult(jobId, timeoutSecs = 30) {
|
|
53
|
-
|
|
72
|
+
const deadline = Date.now() + timeoutSecs * 1000;
|
|
73
|
+
let delayMs = 250;
|
|
74
|
+
while (Date.now() < deadline) {
|
|
54
75
|
const job = await redashFetch(`/jobs/${jobId}`);
|
|
55
76
|
if (job.job.status === 3) {
|
|
56
77
|
return await redashFetch(`/query_results/${job.job.query_result_id}`);
|
|
@@ -58,7 +79,11 @@ export async function pollQueryResult(jobId, timeoutSecs = 30) {
|
|
|
58
79
|
if (job.job.status === 4) {
|
|
59
80
|
throw new Error(`Query failed: ${job.job.error}`);
|
|
60
81
|
}
|
|
61
|
-
|
|
82
|
+
const remaining = deadline - Date.now();
|
|
83
|
+
if (remaining <= 0)
|
|
84
|
+
break;
|
|
85
|
+
await new Promise((r) => setTimeout(r, Math.min(delayMs, remaining)));
|
|
86
|
+
delayMs = Math.min(delayMs * 2, 2000);
|
|
62
87
|
}
|
|
63
88
|
throw new Error(`Query timed out after ${timeoutSecs}s`);
|
|
64
89
|
}
|
|
@@ -73,16 +98,27 @@ export function formatAsMarkdownTable(columns, rows) {
|
|
|
73
98
|
}
|
|
74
99
|
const schemaCache = new Map();
|
|
75
100
|
const CACHE_TTL_MS = 10 * 60 * 1000;
|
|
101
|
+
const SCHEMA_CACHE_MAX_ENTRIES = 32;
|
|
76
102
|
export async function fetchSchema(dataSourceId) {
|
|
77
103
|
const cached = schemaCache.get(dataSourceId);
|
|
78
104
|
if (cached && Date.now() - cached.ts < CACHE_TTL_MS) {
|
|
105
|
+
schemaCache.delete(dataSourceId);
|
|
106
|
+
schemaCache.set(dataSourceId, cached);
|
|
79
107
|
return cached.schema;
|
|
80
108
|
}
|
|
109
|
+
if (cached)
|
|
110
|
+
schemaCache.delete(dataSourceId);
|
|
81
111
|
const result = await redashFetch(`/data_sources/${dataSourceId}/schema`);
|
|
82
112
|
const schema = (result.schema ?? []).map((table) => ({
|
|
83
113
|
...table,
|
|
84
114
|
columns: (table.columns ?? []).map((c) => typeof c === "string" ? { name: c, type: "unknown" } : c),
|
|
85
115
|
}));
|
|
116
|
+
while (schemaCache.size >= SCHEMA_CACHE_MAX_ENTRIES) {
|
|
117
|
+
const oldest = schemaCache.keys().next().value;
|
|
118
|
+
if (oldest === undefined)
|
|
119
|
+
break;
|
|
120
|
+
schemaCache.delete(oldest);
|
|
121
|
+
}
|
|
86
122
|
schemaCache.set(dataSourceId, { schema, ts: Date.now() });
|
|
87
123
|
return schema;
|
|
88
124
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "redash-mcp",
|
|
3
3
|
"mcpName": "io.github.seob717/redash-mcp",
|
|
4
|
-
"version": "3.0
|
|
4
|
+
"version": "3.1.0",
|
|
5
5
|
"description": "MCP server for Redash",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"author": "seob717",
|
|
@@ -32,17 +32,20 @@
|
|
|
32
32
|
"scripts": {
|
|
33
33
|
"build": "tsc",
|
|
34
34
|
"dev": "tsx src/index.ts",
|
|
35
|
-
"start": "node dist/index.js"
|
|
35
|
+
"start": "node dist/index.js",
|
|
36
|
+
"test": "vitest run",
|
|
37
|
+
"test:watch": "vitest"
|
|
36
38
|
},
|
|
37
39
|
"dependencies": {
|
|
38
|
-
"@anthropic-ai/sdk": "^0.
|
|
39
|
-
"@clack/prompts": "^1.
|
|
40
|
-
"@modelcontextprotocol/sdk": "^1.
|
|
41
|
-
"zod": "^
|
|
40
|
+
"@anthropic-ai/sdk": "^0.95.2",
|
|
41
|
+
"@clack/prompts": "^1.4.0",
|
|
42
|
+
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
43
|
+
"zod": "^4.4.3"
|
|
42
44
|
},
|
|
43
45
|
"devDependencies": {
|
|
44
46
|
"@types/node": "^22.0.0",
|
|
45
47
|
"tsx": "^4.0.0",
|
|
46
|
-
"typescript": "^5.0.0"
|
|
48
|
+
"typescript": "^5.0.0",
|
|
49
|
+
"vitest": "^4.1.6"
|
|
47
50
|
}
|
|
48
51
|
}
|