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 CHANGED
@@ -23,17 +23,27 @@ async function redashFetch(path, options) {
23
23
  },
24
24
  });
25
25
  if (!res.ok) {
26
- throw new Error(`Redash API error: ${res.status} ${res.statusText}`);
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 header = `| ${columns.join(" | ")} |`;
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.1",
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
- }, async ({ query_id }) => {
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}행\n컬럼: ${columns.join(", ")}\n\n${JSON.stringify(rows, null, 2)}`,
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
- const res = await fetch(`${REDASH_URL}/api/queries/${query_id}`, {
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,6 +1,6 @@
1
1
  {
2
2
  "name": "redash-mcp",
3
- "version": "2.0.4",
3
+ "version": "2.1.1",
4
4
  "description": "MCP server for Redash",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -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
- }