redash-mcp 3.0.2 → 3.0.4

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
@@ -6,6 +6,7 @@ import { analyzeQuery } from "./sql-guard.js";
6
6
  import { getCached, setCached } from "./query-cache.js";
7
7
  import { REDASH_URL, REDASH_API_KEY, redashFetch, pollQueryResult, formatAsMarkdownTable, fetchSchema } from "./redash-client.js";
8
8
  import { registerBirdTools } from "./bird/tools.js";
9
+ import { handleToolError } from "./tool-error.js";
9
10
  if (process.argv[2] === "setup") {
10
11
  const { main } = await import("./setup.js");
11
12
  await main();
@@ -20,57 +21,72 @@ const server = new McpServer({
20
21
  version: "3.0.0",
21
22
  });
22
23
  server.tool("list_data_sources", "List connected data sources (id, name, type). Call this first to get data_source_id.", {}, { readOnlyHint: true }, async () => {
23
- const data = await redashFetch("/data_sources");
24
- const sources = data.map((ds) => ({
25
- id: ds.id,
26
- name: ds.name,
27
- type: ds.type,
28
- }));
29
- return {
30
- content: [{ type: "text", text: JSON.stringify(sources, null, 2) }],
31
- };
24
+ try {
25
+ const data = await redashFetch("/data_sources");
26
+ const sources = data.map((ds) => ({
27
+ id: ds.id,
28
+ name: ds.name,
29
+ type: ds.type,
30
+ }));
31
+ return {
32
+ content: [{ type: "text", text: JSON.stringify(sources, null, 2) }],
33
+ };
34
+ }
35
+ catch (error) {
36
+ return handleToolError("list_data_sources", error);
37
+ }
32
38
  });
33
39
  server.tool("list_tables", "List tables in a data source. Use keyword to filter by name. Verify table names here before writing SQL.", {
34
40
  data_source_id: z.number().describe("Data source ID from list_data_sources"),
35
41
  keyword: z.string().optional().describe("Filter keyword for table names (e.g., 'user', 'order')"),
36
42
  }, { readOnlyHint: true }, async ({ data_source_id, keyword }) => {
37
- const schema = await fetchSchema(data_source_id);
38
- let tables = schema.map((t) => t.name);
39
- if (keyword) {
40
- tables = tables.filter((name) => name.toLowerCase().includes(keyword.toLowerCase()));
41
- }
42
- const total = tables.length;
43
- const MAX_TABLES = 200;
44
- const truncated = tables.length > MAX_TABLES;
45
- if (truncated)
46
- tables = tables.slice(0, MAX_TABLES);
47
- const summary = `${total} tables${keyword ? ` (matching '${keyword}')` : ""}${truncated ? ` (showing first ${MAX_TABLES}, use keyword to filter)` : ""}\n\n${tables.join("\n")}`;
48
- return {
49
- content: [{ type: "text", text: summary }],
50
- };
43
+ try {
44
+ const schema = await fetchSchema(data_source_id);
45
+ let tables = schema.map((t) => t.name);
46
+ if (keyword) {
47
+ tables = tables.filter((name) => name.toLowerCase().includes(keyword.toLowerCase()));
48
+ }
49
+ const total = tables.length;
50
+ const MAX_TABLES = 200;
51
+ const truncated = tables.length > MAX_TABLES;
52
+ if (truncated)
53
+ tables = tables.slice(0, MAX_TABLES);
54
+ const summary = `${total} tables${keyword ? ` (matching '${keyword}')` : ""}${truncated ? ` (showing first ${MAX_TABLES}, use keyword to filter)` : ""}\n\n${tables.join("\n")}`;
55
+ return {
56
+ content: [{ type: "text", text: summary }],
57
+ };
58
+ }
59
+ catch (error) {
60
+ return handleToolError("list_tables", error);
61
+ }
51
62
  });
52
63
  server.tool("get_table_columns", "Get column names and types for one or more tables (comma-separated). Verify columns before writing SQL.", {
53
64
  data_source_id: z.number().describe("Data source ID from list_data_sources"),
54
65
  table_name: z.string().describe("Table name(s), comma-separated (e.g., 'users' or 'users,orders')"),
55
66
  }, { readOnlyHint: true }, async ({ data_source_id, table_name }) => {
56
- const schema = await fetchSchema(data_source_id);
57
- const tableNames = table_name.split(",").map((n) => n.trim()).filter(Boolean);
58
- const results = [];
59
- for (const name of tableNames) {
60
- let table = schema.find((t) => t.name.toLowerCase() === name.toLowerCase());
61
- if (!table) {
62
- table = schema.find((t) => t.name.toLowerCase().includes(name.toLowerCase()));
63
- }
64
- if (!table) {
65
- results.push(`Table '${name}' not found. Use list_tables to verify the table name.`);
66
- continue;
67
+ try {
68
+ const schema = await fetchSchema(data_source_id);
69
+ const tableNames = table_name.split(",").map((n) => n.trim()).filter(Boolean);
70
+ const results = [];
71
+ for (const name of tableNames) {
72
+ let table = schema.find((t) => t.name.toLowerCase() === name.toLowerCase());
73
+ if (!table) {
74
+ table = schema.find((t) => t.name.toLowerCase().includes(name.toLowerCase()));
75
+ }
76
+ if (!table) {
77
+ results.push(`Table '${name}' not found. Use list_tables to verify the table name.`);
78
+ continue;
79
+ }
80
+ const cols = (table.columns ?? []).map((c) => typeof c === "string" ? c : `${c.name} (${c.type ?? "unknown"})`).join("\n");
81
+ results.push(`[${table.name}]\n${cols}`);
67
82
  }
68
- const cols = (table.columns ?? []).map((c) => typeof c === "string" ? c : `${c.name} (${c.type ?? "unknown"})`).join("\n");
69
- results.push(`[${table.name}]\n${cols}`);
83
+ return {
84
+ content: [{ type: "text", text: results.join("\n\n") }],
85
+ };
86
+ }
87
+ catch (error) {
88
+ return handleToolError("get_table_columns", error);
70
89
  }
71
- return {
72
- content: [{ type: "text", text: results.join("\n\n") }],
73
- };
74
90
  });
75
91
  const DEFAULT_MAX_AGE = parseInt(process.env.REDASH_DEFAULT_MAX_AGE ?? "0", 10) || 0;
76
92
  server.tool("run_query", "Execute SQL against a data source and return results. Check schema with list_tables and get_table_columns first.", {
@@ -81,15 +97,53 @@ server.tool("run_query", "Execute SQL against a data source and return results.
81
97
  format: z.enum(["table", "json"]).optional().default("table").describe("Output format: table (markdown) or json"),
82
98
  timeout_secs: z.number().optional().default(30).describe("Query execution timeout in seconds"),
83
99
  }, { readOnlyHint: true }, async ({ data_source_id, query, max_age, max_rows, format, timeout_secs }) => {
84
- const guard = analyzeQuery(query);
85
- if (guard.blocked) {
86
- return { content: [{ type: "text", text: guard.message }] };
87
- }
88
- const effectiveQuery = guard.modifiedQuery ?? query;
89
- const effectiveMaxAge = max_age ?? DEFAULT_MAX_AGE;
90
- const cached = getCached(data_source_id, effectiveQuery);
91
- if (cached) {
92
- const { rows, columns, warningPrefix } = cached;
100
+ try {
101
+ const guard = analyzeQuery(query);
102
+ if (guard.blocked) {
103
+ return { content: [{ type: "text", text: guard.message }] };
104
+ }
105
+ const effectiveQuery = guard.modifiedQuery ?? query;
106
+ const effectiveMaxAge = max_age ?? DEFAULT_MAX_AGE;
107
+ const cached = getCached(data_source_id, effectiveQuery);
108
+ if (cached) {
109
+ const { rows, columns, warningPrefix } = cached;
110
+ const displayRows = rows.slice(0, max_rows);
111
+ const truncated = rows.length > max_rows
112
+ ? `\n⚠️ Showing ${max_rows} of ${rows.length} rows.`
113
+ : "";
114
+ let body;
115
+ if (format === "json") {
116
+ body = JSON.stringify(displayRows, null, 2);
117
+ }
118
+ else {
119
+ body = formatAsMarkdownTable(columns, displayRows);
120
+ }
121
+ const cacheNote = "Returned from MCP cache.\n\n";
122
+ return {
123
+ content: [
124
+ {
125
+ type: "text",
126
+ text: `${warningPrefix}${cacheNote}${rows.length} rows | Columns: ${columns.join(", ")}${truncated}\n\n${body}`,
127
+ },
128
+ ],
129
+ };
130
+ }
131
+ const res = await redashFetch("/query_results", {
132
+ method: "POST",
133
+ body: JSON.stringify({ data_source_id, query: effectiveQuery, max_age: effectiveMaxAge }),
134
+ });
135
+ let result;
136
+ if (res.job) {
137
+ result = await pollQueryResult(res.job.id, timeout_secs);
138
+ }
139
+ else {
140
+ result = res;
141
+ }
142
+ const qr = result.query_result;
143
+ const rows = qr.data.rows;
144
+ const columns = qr.data.columns.map((c) => c.name);
145
+ const warningPrefix = guard.message ? `${guard.message}\n\n` : "";
146
+ setCached(data_source_id, effectiveQuery, { rows, columns, warningPrefix });
93
147
  const displayRows = rows.slice(0, max_rows);
94
148
  const truncated = rows.length > max_rows
95
149
  ? `\n⚠️ Showing ${max_rows} of ${rows.length} rows.`
@@ -101,135 +155,117 @@ server.tool("run_query", "Execute SQL against a data source and return results.
101
155
  else {
102
156
  body = formatAsMarkdownTable(columns, displayRows);
103
157
  }
104
- const cacheNote = "Returned from MCP cache.\n\n";
105
158
  return {
106
159
  content: [
107
160
  {
108
161
  type: "text",
109
- text: `${warningPrefix}${cacheNote}${rows.length} rows | Columns: ${columns.join(", ")}${truncated}\n\n${body}`,
162
+ text: `${warningPrefix}${rows.length} rows | Columns: ${columns.join(", ")}${truncated}\n\n${body}`,
110
163
  },
111
164
  ],
112
165
  };
113
166
  }
114
- const res = await redashFetch("/query_results", {
115
- method: "POST",
116
- body: JSON.stringify({ data_source_id, query: effectiveQuery, max_age: effectiveMaxAge }),
117
- });
118
- let result;
119
- if (res.job) {
120
- result = await pollQueryResult(res.job.id, timeout_secs);
121
- }
122
- else {
123
- result = res;
124
- }
125
- const qr = result.query_result;
126
- const rows = qr.data.rows;
127
- const columns = qr.data.columns.map((c) => c.name);
128
- const warningPrefix = guard.message ? `${guard.message}\n\n` : "";
129
- setCached(data_source_id, effectiveQuery, { rows, columns, warningPrefix });
130
- const displayRows = rows.slice(0, max_rows);
131
- const truncated = rows.length > max_rows
132
- ? `\n⚠️ Showing ${max_rows} of ${rows.length} rows.`
133
- : "";
134
- let body;
135
- if (format === "json") {
136
- body = JSON.stringify(displayRows, null, 2);
137
- }
138
- else {
139
- body = formatAsMarkdownTable(columns, displayRows);
140
- }
141
- return {
142
- content: [
143
- {
144
- type: "text",
145
- text: `${warningPrefix}${rows.length} rows | Columns: ${columns.join(", ")}${truncated}\n\n${body}`,
146
- },
147
- ],
148
- };
167
+ catch (error) {
168
+ return handleToolError("run_query", error);
169
+ }
149
170
  });
150
171
  server.tool("list_queries", "List saved queries in Redash.", {
151
172
  search: z.string().optional().describe("Search keyword"),
152
173
  page: z.number().optional().default(1),
153
174
  page_size: z.number().optional().default(20).describe("Page size (max 100)"),
154
175
  }, { readOnlyHint: true }, async ({ search, page, page_size }) => {
155
- const effectivePageSize = Math.min(page_size, 100);
156
- const params = new URLSearchParams({
157
- page: String(page),
158
- page_size: String(effectivePageSize),
159
- ...(search ? { q: search } : {}),
160
- });
161
- const data = await redashFetch(`/queries?${params}`);
162
- const queries = data.results.map((q) => ({
163
- id: q.id,
164
- name: q.name,
165
- description: q.description,
166
- data_source_id: q.data_source_id,
167
- updated_at: q.updated_at,
168
- }));
169
- return {
170
- content: [{ type: "text", text: JSON.stringify(queries, null, 2) }],
171
- };
176
+ try {
177
+ const effectivePageSize = Math.min(page_size, 100);
178
+ const params = new URLSearchParams({
179
+ page: String(page),
180
+ page_size: String(effectivePageSize),
181
+ ...(search ? { q: search } : {}),
182
+ });
183
+ const data = await redashFetch(`/queries?${params}`);
184
+ const queries = data.results.map((q) => ({
185
+ id: q.id,
186
+ name: q.name,
187
+ description: q.description,
188
+ data_source_id: q.data_source_id,
189
+ updated_at: q.updated_at,
190
+ }));
191
+ return {
192
+ content: [{ type: "text", text: JSON.stringify(queries, null, 2) }],
193
+ };
194
+ }
195
+ catch (error) {
196
+ return handleToolError("list_queries", error);
197
+ }
172
198
  });
173
199
  server.tool("get_query_result", "Execute a saved query by ID and return results.", {
174
200
  query_id: z.number().describe("Saved query ID (from list_queries)"),
175
201
  max_rows: z.number().optional().default(100).describe("Max rows to return (default 100)"),
176
202
  format: z.enum(["table", "json"]).optional().default("table").describe("Output format: table (markdown) or json"),
177
203
  }, { readOnlyHint: true }, async ({ query_id, max_rows, format }) => {
178
- const res = await redashFetch(`/queries/${query_id}/results`, {
179
- method: "POST",
180
- body: JSON.stringify({}),
181
- });
182
- let result;
183
- if (res.job) {
184
- result = await pollQueryResult(res.job.id);
185
- }
186
- else {
187
- result = res;
188
- }
189
- const qr = result.query_result;
190
- const rows = qr.data.rows;
191
- const columns = qr.data.columns.map((c) => c.name);
192
- const displayRows = rows.slice(0, max_rows);
193
- const truncated = rows.length > max_rows
194
- ? `\n⚠️ Showing ${max_rows} of ${rows.length} rows.`
195
- : "";
196
- let body;
197
- if (format === "json") {
198
- body = JSON.stringify(displayRows, null, 2);
199
- }
200
- else {
201
- body = formatAsMarkdownTable(columns, displayRows);
202
- }
203
- return {
204
- content: [
205
- {
206
- type: "text",
207
- text: `Query ID: ${query_id}\n${rows.length} rows | Columns: ${columns.join(", ")}${truncated}\n\n${body}`,
208
- },
209
- ],
210
- };
204
+ try {
205
+ const res = await redashFetch(`/queries/${query_id}/results`, {
206
+ method: "POST",
207
+ body: JSON.stringify({}),
208
+ });
209
+ let result;
210
+ if (res.job) {
211
+ result = await pollQueryResult(res.job.id);
212
+ }
213
+ else {
214
+ result = res;
215
+ }
216
+ const qr = result.query_result;
217
+ const rows = qr.data.rows;
218
+ const columns = qr.data.columns.map((c) => c.name);
219
+ const displayRows = rows.slice(0, max_rows);
220
+ const truncated = rows.length > max_rows
221
+ ? `\n⚠️ Showing ${max_rows} of ${rows.length} rows.`
222
+ : "";
223
+ let body;
224
+ if (format === "json") {
225
+ body = JSON.stringify(displayRows, null, 2);
226
+ }
227
+ else {
228
+ body = formatAsMarkdownTable(columns, displayRows);
229
+ }
230
+ return {
231
+ content: [
232
+ {
233
+ type: "text",
234
+ text: `Query ID: ${query_id}\n${rows.length} rows | Columns: ${columns.join(", ")}${truncated}\n\n${body}`,
235
+ },
236
+ ],
237
+ };
238
+ }
239
+ catch (error) {
240
+ return handleToolError("get_query_result", error);
241
+ }
211
242
  });
212
243
  server.tool("get_query", "Get saved query details (SQL, visualizations, tags).", {
213
244
  query_id: z.number().describe("Query ID (from list_queries)"),
214
245
  }, { readOnlyHint: true }, async ({ query_id }) => {
215
- const data = await redashFetch(`/queries/${query_id}`);
216
- const result = {
217
- id: data.id,
218
- name: data.name,
219
- description: data.description,
220
- query: data.query,
221
- data_source_id: data.data_source_id,
222
- tags: data.tags,
223
- visualizations_count: Array.isArray(data.visualizations) ? data.visualizations.length : 0,
224
- visualizations: Array.isArray(data.visualizations)
225
- ? data.visualizations.map((v) => ({ id: v.id, name: v.name, type: v.type }))
226
- : [],
227
- updated_at: data.updated_at,
228
- user: data.user?.name,
229
- };
230
- return {
231
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
232
- };
246
+ try {
247
+ const data = await redashFetch(`/queries/${query_id}`);
248
+ const result = {
249
+ id: data.id,
250
+ name: data.name,
251
+ description: data.description,
252
+ query: data.query,
253
+ data_source_id: data.data_source_id,
254
+ tags: data.tags,
255
+ visualizations_count: Array.isArray(data.visualizations) ? data.visualizations.length : 0,
256
+ visualizations: Array.isArray(data.visualizations)
257
+ ? data.visualizations.map((v) => ({ id: v.id, name: v.name, type: v.type }))
258
+ : [],
259
+ updated_at: data.updated_at,
260
+ user: data.user?.name,
261
+ };
262
+ return {
263
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
264
+ };
265
+ }
266
+ catch (error) {
267
+ return handleToolError("get_query", error);
268
+ }
233
269
  });
234
270
  server.tool("create_query", "Save a new query to Redash.", {
235
271
  name: z.string().describe("Query name"),
@@ -238,23 +274,28 @@ server.tool("create_query", "Save a new query to Redash.", {
238
274
  description: z.string().optional().describe("Query description"),
239
275
  tags: z.array(z.string()).optional().describe("Tags"),
240
276
  }, { destructiveHint: true }, async ({ name, query, data_source_id, description, tags }) => {
241
- const body = { name, query, data_source_id };
242
- if (description !== undefined)
243
- body.description = description;
244
- if (tags !== undefined)
245
- body.tags = tags;
246
- const data = await redashFetch("/queries", {
247
- method: "POST",
248
- body: JSON.stringify(body),
249
- });
250
- return {
251
- content: [
252
- {
253
- type: "text",
254
- text: JSON.stringify({ id: data.id, name: data.name, created_at: data.created_at }, null, 2),
255
- },
256
- ],
257
- };
277
+ try {
278
+ const body = { name, query, data_source_id };
279
+ if (description !== undefined)
280
+ body.description = description;
281
+ if (tags !== undefined)
282
+ body.tags = tags;
283
+ const data = await redashFetch("/queries", {
284
+ method: "POST",
285
+ body: JSON.stringify(body),
286
+ });
287
+ return {
288
+ content: [
289
+ {
290
+ type: "text",
291
+ text: JSON.stringify({ id: data.id, name: data.name, created_at: data.created_at }, null, 2),
292
+ },
293
+ ],
294
+ };
295
+ }
296
+ catch (error) {
297
+ return handleToolError("create_query", error);
298
+ }
258
299
  });
259
300
  server.tool("update_query", "Update a saved query's name, SQL, description, or tags.", {
260
301
  query_id: z.number().describe("Query ID to update"),
@@ -263,114 +304,144 @@ server.tool("update_query", "Update a saved query's name, SQL, description, or t
263
304
  description: z.string().optional().describe("New description"),
264
305
  tags: z.array(z.string()).optional().describe("New tags"),
265
306
  }, { destructiveHint: true }, async ({ query_id, name, query, description, tags }) => {
266
- const body = {};
267
- if (name !== undefined)
268
- body.name = name;
269
- if (query !== undefined)
270
- body.query = query;
271
- if (description !== undefined)
272
- body.description = description;
273
- if (tags !== undefined)
274
- body.tags = tags;
275
- const data = await redashFetch(`/queries/${query_id}`, {
276
- method: "POST",
277
- body: JSON.stringify(body),
278
- });
279
- return {
280
- content: [
281
- {
282
- type: "text",
283
- text: JSON.stringify({ id: data.id, name: data.name, updated_at: data.updated_at }, null, 2),
284
- },
285
- ],
286
- };
307
+ try {
308
+ const body = {};
309
+ if (name !== undefined)
310
+ body.name = name;
311
+ if (query !== undefined)
312
+ body.query = query;
313
+ if (description !== undefined)
314
+ body.description = description;
315
+ if (tags !== undefined)
316
+ body.tags = tags;
317
+ const data = await redashFetch(`/queries/${query_id}`, {
318
+ method: "POST",
319
+ body: JSON.stringify(body),
320
+ });
321
+ return {
322
+ content: [
323
+ {
324
+ type: "text",
325
+ text: JSON.stringify({ id: data.id, name: data.name, updated_at: data.updated_at }, null, 2),
326
+ },
327
+ ],
328
+ };
329
+ }
330
+ catch (error) {
331
+ return handleToolError("update_query", error);
332
+ }
287
333
  });
288
334
  server.tool("fork_query", "Fork (duplicate) an existing query.", {
289
335
  query_id: z.number().describe("Query ID to fork"),
290
336
  }, { destructiveHint: true }, async ({ query_id }) => {
291
- const data = await redashFetch(`/queries/${query_id}/fork`, {
292
- method: "POST",
293
- body: JSON.stringify({}),
294
- });
295
- return {
296
- content: [
297
- {
298
- type: "text",
299
- text: JSON.stringify({ id: data.id, name: data.name }, null, 2),
300
- },
301
- ],
302
- };
337
+ try {
338
+ const data = await redashFetch(`/queries/${query_id}/fork`, {
339
+ method: "POST",
340
+ body: JSON.stringify({}),
341
+ });
342
+ return {
343
+ content: [
344
+ {
345
+ type: "text",
346
+ text: JSON.stringify({ id: data.id, name: data.name }, null, 2),
347
+ },
348
+ ],
349
+ };
350
+ }
351
+ catch (error) {
352
+ return handleToolError("fork_query", error);
353
+ }
303
354
  });
304
355
  server.tool("archive_query", "Archive (delete) a query. This action is irreversible.", {
305
356
  query_id: z.number().describe("Query ID to archive"),
306
357
  }, { destructiveHint: true }, async ({ query_id }) => {
307
- await redashFetch(`/queries/${query_id}`, { method: "DELETE" });
308
- return {
309
- content: [{ type: "text", text: `Query ${query_id} has been archived.` }],
310
- };
358
+ try {
359
+ await redashFetch(`/queries/${query_id}`, { method: "DELETE" });
360
+ return {
361
+ content: [{ type: "text", text: `Query ${query_id} has been archived.` }],
362
+ };
363
+ }
364
+ catch (error) {
365
+ return handleToolError("archive_query", error);
366
+ }
311
367
  });
312
368
  server.tool("list_dashboards", "List dashboards in Redash.", {
313
369
  search: z.string().optional().describe("Search keyword"),
314
370
  page: z.number().optional().default(1),
315
371
  page_size: z.number().optional().default(20).describe("Page size (max 100)"),
316
372
  }, { readOnlyHint: true }, async ({ search, page, page_size }) => {
317
- const effectivePageSize = Math.min(page_size, 100);
318
- const params = new URLSearchParams({
319
- page: String(page),
320
- page_size: String(effectivePageSize),
321
- ...(search ? { q: search } : {}),
322
- });
323
- const data = await redashFetch(`/dashboards?${params}`);
324
- const results = (data.results ?? data).map((d) => ({
325
- id: d.id,
326
- name: d.name,
327
- slug: d.slug,
328
- created_at: d.created_at,
329
- updated_at: d.updated_at,
330
- }));
331
- return {
332
- content: [{ type: "text", text: JSON.stringify(results, null, 2) }],
333
- };
373
+ try {
374
+ const effectivePageSize = Math.min(page_size, 100);
375
+ const params = new URLSearchParams({
376
+ page: String(page),
377
+ page_size: String(effectivePageSize),
378
+ ...(search ? { q: search } : {}),
379
+ });
380
+ const data = await redashFetch(`/dashboards?${params}`);
381
+ const results = (data.results ?? data).map((d) => ({
382
+ id: d.id,
383
+ name: d.name,
384
+ slug: d.slug,
385
+ created_at: d.created_at,
386
+ updated_at: d.updated_at,
387
+ }));
388
+ return {
389
+ content: [{ type: "text", text: JSON.stringify(results, null, 2) }],
390
+ };
391
+ }
392
+ catch (error) {
393
+ return handleToolError("list_dashboards", error);
394
+ }
334
395
  });
335
396
  server.tool("get_dashboard", "Get dashboard details including widgets and visualizations.", {
336
397
  dashboard_id_or_slug: z.string().describe("Dashboard ID or slug (from list_dashboards)"),
337
398
  }, { readOnlyHint: true }, async ({ dashboard_id_or_slug }) => {
338
- const data = await redashFetch(`/dashboards/${dashboard_id_or_slug}`);
339
- const result = {
340
- id: data.id,
341
- name: data.name,
342
- slug: data.slug,
343
- widgets: Array.isArray(data.widgets)
344
- ? data.widgets.map((w) => ({
345
- id: w.id,
346
- visualization: w.visualization
347
- ? { id: w.visualization.id, name: w.visualization.name, type: w.visualization.type }
348
- : null,
349
- query: w.visualization?.query
350
- ? { id: w.visualization.query.id, name: w.visualization.query.name }
351
- : null,
352
- }))
353
- : [],
354
- };
355
- return {
356
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
357
- };
399
+ try {
400
+ const data = await redashFetch(`/dashboards/${dashboard_id_or_slug}`);
401
+ const result = {
402
+ id: data.id,
403
+ name: data.name,
404
+ slug: data.slug,
405
+ widgets: Array.isArray(data.widgets)
406
+ ? data.widgets.map((w) => ({
407
+ id: w.id,
408
+ visualization: w.visualization
409
+ ? { id: w.visualization.id, name: w.visualization.name, type: w.visualization.type }
410
+ : null,
411
+ query: w.visualization?.query
412
+ ? { id: w.visualization.query.id, name: w.visualization.query.name }
413
+ : null,
414
+ }))
415
+ : [],
416
+ };
417
+ return {
418
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
419
+ };
420
+ }
421
+ catch (error) {
422
+ return handleToolError("get_dashboard", error);
423
+ }
358
424
  });
359
425
  server.tool("create_dashboard", "Create a new dashboard.", {
360
426
  name: z.string().describe("Dashboard name"),
361
427
  }, { destructiveHint: true }, async ({ name }) => {
362
- const data = await redashFetch("/dashboards", {
363
- method: "POST",
364
- body: JSON.stringify({ name }),
365
- });
366
- return {
367
- content: [
368
- {
369
- type: "text",
370
- text: JSON.stringify({ id: data.id, name: data.name, slug: data.slug }, null, 2),
371
- },
372
- ],
373
- };
428
+ try {
429
+ const data = await redashFetch("/dashboards", {
430
+ method: "POST",
431
+ body: JSON.stringify({ name }),
432
+ });
433
+ return {
434
+ content: [
435
+ {
436
+ type: "text",
437
+ text: JSON.stringify({ id: data.id, name: data.name, slug: data.slug }, null, 2),
438
+ },
439
+ ],
440
+ };
441
+ }
442
+ catch (error) {
443
+ return handleToolError("create_dashboard", error);
444
+ }
374
445
  });
375
446
  server.tool("add_widget", "Add a visualization widget to a dashboard. Get visualization_id from get_query.", {
376
447
  dashboard_id: z.number().describe("Dashboard ID"),
@@ -378,62 +449,77 @@ server.tool("add_widget", "Add a visualization widget to a dashboard. Get visual
378
449
  text: z.string().optional().default("").describe("Widget text"),
379
450
  width: z.number().optional().default(1).describe("Widget width (1 or 2)"),
380
451
  }, { destructiveHint: true }, async ({ dashboard_id, visualization_id, text, width }) => {
381
- const data = await redashFetch("/widgets", {
382
- method: "POST",
383
- body: JSON.stringify({
384
- dashboard_id,
385
- visualization_id,
386
- text,
387
- width,
388
- options: {},
389
- }),
390
- });
391
- return {
392
- content: [
393
- {
394
- type: "text",
395
- text: JSON.stringify({ id: data.id, dashboard_id: data.dashboard_id }, null, 2),
396
- },
397
- ],
398
- };
452
+ try {
453
+ const data = await redashFetch("/widgets", {
454
+ method: "POST",
455
+ body: JSON.stringify({
456
+ dashboard_id,
457
+ visualization_id,
458
+ text,
459
+ width,
460
+ options: {},
461
+ }),
462
+ });
463
+ return {
464
+ content: [
465
+ {
466
+ type: "text",
467
+ text: JSON.stringify({ id: data.id, dashboard_id: data.dashboard_id }, null, 2),
468
+ },
469
+ ],
470
+ };
471
+ }
472
+ catch (error) {
473
+ return handleToolError("add_widget", error);
474
+ }
399
475
  });
400
476
  server.tool("list_alerts", "List alerts in Redash.", {}, { readOnlyHint: true }, async () => {
401
- const data = await redashFetch("/alerts");
402
- const alerts = Array.isArray(data) ? data : [];
403
- const result = alerts.map((alert) => ({
404
- id: alert.id,
405
- name: alert.name,
406
- state: alert.state,
407
- last_triggered_at: alert.last_triggered_at,
408
- query: alert.query ? { id: alert.query.id, name: alert.query.name } : null,
409
- options: alert.options,
410
- }));
411
- return {
412
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
413
- };
477
+ try {
478
+ const data = await redashFetch("/alerts");
479
+ const alerts = Array.isArray(data) ? data : [];
480
+ const result = alerts.map((alert) => ({
481
+ id: alert.id,
482
+ name: alert.name,
483
+ state: alert.state,
484
+ last_triggered_at: alert.last_triggered_at,
485
+ query: alert.query ? { id: alert.query.id, name: alert.query.name } : null,
486
+ options: alert.options,
487
+ }));
488
+ return {
489
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
490
+ };
491
+ }
492
+ catch (error) {
493
+ return handleToolError("list_alerts", error);
494
+ }
414
495
  });
415
496
  server.tool("get_alert", "Get alert details (threshold, linked query, state).", {
416
497
  alert_id: z.number().describe("Alert ID (from list_alerts)"),
417
498
  }, { readOnlyHint: true }, async ({ alert_id }) => {
418
- const alert = await redashFetch(`/alerts/${alert_id}`);
419
- const result = {
420
- id: alert.id,
421
- name: alert.name,
422
- state: alert.state,
423
- last_triggered_at: alert.last_triggered_at,
424
- query: alert.query
425
- ? {
426
- id: alert.query.id,
427
- name: alert.query.name,
428
- description: alert.query.description,
429
- data_source_id: alert.query.data_source_id,
430
- }
431
- : null,
432
- options: alert.options,
433
- };
434
- return {
435
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
436
- };
499
+ try {
500
+ const alert = await redashFetch(`/alerts/${alert_id}`);
501
+ const result = {
502
+ id: alert.id,
503
+ name: alert.name,
504
+ state: alert.state,
505
+ last_triggered_at: alert.last_triggered_at,
506
+ query: alert.query
507
+ ? {
508
+ id: alert.query.id,
509
+ name: alert.query.name,
510
+ description: alert.query.description,
511
+ data_source_id: alert.query.data_source_id,
512
+ }
513
+ : null,
514
+ options: alert.options,
515
+ };
516
+ return {
517
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
518
+ };
519
+ }
520
+ catch (error) {
521
+ return handleToolError("get_alert", error);
522
+ }
437
523
  });
438
524
  server.tool("create_alert", "Create a new alert. Triggers when a query result column crosses a threshold.", {
439
525
  name: z.string().describe("Alert name"),
@@ -443,23 +529,28 @@ server.tool("create_alert", "Create a new alert. Triggers when a query result co
443
529
  value: z.number().describe("Threshold value"),
444
530
  rearm: z.number().optional().default(0).describe("Rearm interval in seconds (0 = fire once)"),
445
531
  }, { destructiveHint: true }, async ({ name, query_id, column, op, value, rearm }) => {
446
- const data = await redashFetch("/alerts", {
447
- method: "POST",
448
- body: JSON.stringify({
449
- name,
450
- query_id,
451
- rearm: rearm ?? 0,
452
- options: { column, op, value },
453
- }),
454
- });
455
- return {
456
- content: [
457
- {
458
- type: "text",
459
- text: JSON.stringify({ id: data.id, name: data.name }, null, 2),
460
- },
461
- ],
462
- };
532
+ try {
533
+ const data = await redashFetch("/alerts", {
534
+ method: "POST",
535
+ body: JSON.stringify({
536
+ name,
537
+ query_id,
538
+ rearm: rearm ?? 0,
539
+ options: { column, op, value },
540
+ }),
541
+ });
542
+ return {
543
+ content: [
544
+ {
545
+ type: "text",
546
+ text: JSON.stringify({ id: data.id, name: data.name }, null, 2),
547
+ },
548
+ ],
549
+ };
550
+ }
551
+ catch (error) {
552
+ return handleToolError("create_alert", error);
553
+ }
463
554
  });
464
555
  registerBirdTools(server);
465
556
  const transport = new StdioServerTransport();