redash-mcp 2.0.4 → 2.1.1
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/dist/index.js +33 -18
- package/package.json +1 -1
- package/.claude/settings.local.json +0 -20
package/dist/index.js
CHANGED
|
@@ -23,17 +23,27 @@ async function redashFetch(path, options) {
|
|
|
23
23
|
},
|
|
24
24
|
});
|
|
25
25
|
if (!res.ok) {
|
|
26
|
-
|
|
26
|
+
let hint = "";
|
|
27
|
+
if (res.status === 401)
|
|
28
|
+
hint = " (REDASH_API_KEY를 확인하세요)";
|
|
29
|
+
else if (res.status === 403)
|
|
30
|
+
hint = " (해당 리소스에 대한 접근 권한이 없습니다)";
|
|
31
|
+
else if (res.status === 404)
|
|
32
|
+
hint = " (리소스를 찾을 수 없습니다. ID를 확인하세요)";
|
|
33
|
+
throw new Error(`Redash API error: ${res.status} ${res.statusText}${hint}`);
|
|
34
|
+
}
|
|
35
|
+
if (res.status === 204 || res.headers.get("content-length") === "0") {
|
|
36
|
+
return null;
|
|
27
37
|
}
|
|
28
38
|
return res.json();
|
|
29
39
|
}
|
|
30
40
|
async function pollQueryResult(jobId, timeoutSecs = 30) {
|
|
31
41
|
for (let i = 0; i < timeoutSecs; i++) {
|
|
32
42
|
const job = await redashFetch(`/jobs/${jobId}`);
|
|
33
|
-
if (job.job.status === 3) {
|
|
43
|
+
if (job.job.status === 3) { // 3 = success
|
|
34
44
|
return await redashFetch(`/query_results/${job.job.query_result_id}`);
|
|
35
45
|
}
|
|
36
|
-
if (job.job.status === 4) {
|
|
46
|
+
if (job.job.status === 4) { // 4 = failure
|
|
37
47
|
throw new Error(`Query failed: ${job.job.error}`);
|
|
38
48
|
}
|
|
39
49
|
await new Promise((r) => setTimeout(r, 1000));
|
|
@@ -41,10 +51,11 @@ async function pollQueryResult(jobId, timeoutSecs = 30) {
|
|
|
41
51
|
throw new Error(`Query timed out after ${timeoutSecs}s`);
|
|
42
52
|
}
|
|
43
53
|
function formatAsMarkdownTable(columns, rows) {
|
|
44
|
-
const
|
|
54
|
+
const escape = (s) => s.replace(/\|/g, "\\|");
|
|
55
|
+
const header = `| ${columns.map(escape).join(" | ")} |`;
|
|
45
56
|
const separator = `| ${columns.map(() => "---").join(" | ")} |`;
|
|
46
57
|
const body = rows
|
|
47
|
-
.map((row) => `| ${columns.map((c) => String(row[c] ?? "")).join(" | ")} |`)
|
|
58
|
+
.map((row) => `| ${columns.map((c) => escape(String(row[c] ?? ""))).join(" | ")} |`)
|
|
48
59
|
.join("\n");
|
|
49
60
|
return `${header}\n${separator}\n${body}`;
|
|
50
61
|
}
|
|
@@ -63,7 +74,7 @@ async function fetchSchema(dataSourceId) {
|
|
|
63
74
|
}
|
|
64
75
|
const server = new McpServer({
|
|
65
76
|
name: "redash-mcp",
|
|
66
|
-
version: "2.0.
|
|
77
|
+
version: "2.0.2",
|
|
67
78
|
});
|
|
68
79
|
// ─── Data Sources ────────────────────────────────────────────────────────────
|
|
69
80
|
server.tool("list_data_sources", "Redash에 연결된 데이터소스 목록(id, name, type)을 반환합니다. 항상 이 툴을 먼저 호출해 data_source_id를 확인하세요.", {}, async () => {
|
|
@@ -183,7 +194,9 @@ server.tool("list_queries", "Redash에 저장된 쿼리 목록을 조회합니
|
|
|
183
194
|
});
|
|
184
195
|
server.tool("get_query_result", "저장된 Redash 쿼리를 ID로 실행하고 결과를 반환합니다.", {
|
|
185
196
|
query_id: z.number().describe("저장된 쿼리 ID (list_queries로 확인)"),
|
|
186
|
-
|
|
197
|
+
max_rows: z.number().optional().default(100).describe("반환할 최대 행 수 (기본 100)"),
|
|
198
|
+
format: z.enum(["table", "json"]).optional().default("table").describe("결과 포맷: table(마크다운) 또는 json"),
|
|
199
|
+
}, async ({ query_id, max_rows, format }) => {
|
|
187
200
|
const res = await redashFetch(`/queries/${query_id}/results`, {
|
|
188
201
|
method: "POST",
|
|
189
202
|
body: JSON.stringify({}),
|
|
@@ -198,11 +211,22 @@ server.tool("get_query_result", "저장된 Redash 쿼리를 ID로 실행하고
|
|
|
198
211
|
const qr = result.query_result;
|
|
199
212
|
const rows = qr.data.rows;
|
|
200
213
|
const columns = qr.data.columns.map((c) => c.name);
|
|
214
|
+
const displayRows = rows.slice(0, max_rows);
|
|
215
|
+
const truncated = rows.length > max_rows
|
|
216
|
+
? `\n⚠️ 전체 ${rows.length}행 중 ${max_rows}행만 표시합니다.`
|
|
217
|
+
: "";
|
|
218
|
+
let body;
|
|
219
|
+
if (format === "json") {
|
|
220
|
+
body = JSON.stringify(displayRows, null, 2);
|
|
221
|
+
}
|
|
222
|
+
else {
|
|
223
|
+
body = formatAsMarkdownTable(columns, displayRows);
|
|
224
|
+
}
|
|
201
225
|
return {
|
|
202
226
|
content: [
|
|
203
227
|
{
|
|
204
228
|
type: "text",
|
|
205
|
-
text: `쿼리 ID: ${query_id}\n총 ${rows.length}
|
|
229
|
+
text: `쿼리 ID: ${query_id}\n총 ${rows.length}행 | 컬럼: ${columns.join(", ")}${truncated}\n\n${body}`,
|
|
206
230
|
},
|
|
207
231
|
],
|
|
208
232
|
};
|
|
@@ -302,16 +326,7 @@ server.tool("fork_query", "기존 쿼리를 복제(fork)합니다.", {
|
|
|
302
326
|
server.tool("archive_query", "쿼리를 아카이브(삭제)합니다. 복구 불가합니다.", {
|
|
303
327
|
query_id: z.number().describe("아카이브할 쿼리 ID"),
|
|
304
328
|
}, async ({ query_id }) => {
|
|
305
|
-
|
|
306
|
-
method: "DELETE",
|
|
307
|
-
headers: {
|
|
308
|
-
"Authorization": `Key ${REDASH_API_KEY}`,
|
|
309
|
-
"Content-Type": "application/json",
|
|
310
|
-
},
|
|
311
|
-
});
|
|
312
|
-
if (!res.ok) {
|
|
313
|
-
throw new Error(`Redash API error: ${res.status} ${res.statusText}`);
|
|
314
|
-
}
|
|
329
|
+
await redashFetch(`/queries/${query_id}`, { method: "DELETE" });
|
|
315
330
|
return {
|
|
316
331
|
content: [{ type: "text", text: `쿼리 ${query_id}가 아카이브되었습니다.` }],
|
|
317
332
|
};
|
package/package.json
CHANGED
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"permissions": {
|
|
3
|
-
"allow": [
|
|
4
|
-
"Bash(git clone:*)",
|
|
5
|
-
"Bash(npm:*)",
|
|
6
|
-
"Bash(git push:*)",
|
|
7
|
-
"WebFetch(domain:registry.npmjs.org)",
|
|
8
|
-
"Bash(NPM_TOKEN=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 npm publish:*)"
|
|
9
|
-
]
|
|
10
|
-
},
|
|
11
|
-
"enabledMcpjsonServers": [
|
|
12
|
-
"context7",
|
|
13
|
-
"filesystem",
|
|
14
|
-
"github"
|
|
15
|
-
],
|
|
16
|
-
"sandbox": {
|
|
17
|
-
"enabled": true,
|
|
18
|
-
"autoAllowBashIfSandboxed": true
|
|
19
|
-
}
|
|
20
|
-
}
|