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/bird/tools.js +220 -189
- package/dist/index.js +414 -323
- package/dist/redash-client.js +25 -1
- package/dist/tool-error.js +40 -0
- package/package.json +1 -1
package/dist/bird/tools.js
CHANGED
|
@@ -5,6 +5,7 @@ import { recordFeedback } from "./feedback.js";
|
|
|
5
5
|
import { loadTestSuite, addTestCase, removeTestCase, runEvaluation, formatEvalResults } from "./evaluation.js";
|
|
6
6
|
import { loadConfig, getConfigDir } from "./config.js";
|
|
7
7
|
import { getEffectiveMap, addMappings, removeMappings, resetMappings, loadKeywordMap, DEFAULT_KEYWORD_MAP } from "./keyword-map.js";
|
|
8
|
+
import { handleToolError } from "../tool-error.js";
|
|
8
9
|
export function registerBirdTools(server) {
|
|
9
10
|
if (process.env.REDASH_BIRD_ENABLED === "false")
|
|
10
11
|
return;
|
|
@@ -13,36 +14,41 @@ export function registerBirdTools(server) {
|
|
|
13
14
|
question: z.string().describe("Natural-language question (e.g., 'How many payments were completed last month?')"),
|
|
14
15
|
context: z.string().optional().describe("User's answer to a previous clarification question (for multi-turn)"),
|
|
15
16
|
}, { readOnlyHint: true }, async ({ data_source_id, question, context }) => {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
17
|
+
try {
|
|
18
|
+
const result = await handleSmartQuery({ question, data_source_id, context });
|
|
19
|
+
if (result.action === "clarify") {
|
|
20
|
+
const text = [
|
|
21
|
+
"## Clarification needed\n",
|
|
22
|
+
"Before generating SQL, I need more information:\n",
|
|
23
|
+
...(result.clarificationQuestions?.map((q, i) => `${i + 1}. ${q}`) ?? []),
|
|
24
|
+
"\nPlease provide answers, then call smart_query again with the `context` parameter containing your answers.",
|
|
25
|
+
].join("\n");
|
|
26
|
+
return { content: [{ type: "text", text }] };
|
|
27
|
+
}
|
|
28
|
+
if (result.action === "explain") {
|
|
29
|
+
return {
|
|
30
|
+
content: [{ type: "text", text: result.explanation ?? "Cannot generate SQL for this question." }],
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
const parts = [];
|
|
34
|
+
if (result.schema) {
|
|
35
|
+
parts.push(result.schema);
|
|
36
|
+
}
|
|
37
|
+
if (result.fewShotExamples) {
|
|
38
|
+
parts.push(result.fewShotExamples);
|
|
39
|
+
}
|
|
40
|
+
if (result.complexity) {
|
|
41
|
+
parts.push(`## Complexity: ${result.complexity.level}\n`);
|
|
42
|
+
}
|
|
43
|
+
if (result.guidance) {
|
|
44
|
+
parts.push(`## Guidance\n${result.guidance}`);
|
|
45
|
+
}
|
|
46
|
+
parts.push("\n---\nUse the schema and examples above to generate SQL, then execute with `run_query`.");
|
|
47
|
+
return { content: [{ type: "text", text: parts.join("\n") }] };
|
|
40
48
|
}
|
|
41
|
-
|
|
42
|
-
|
|
49
|
+
catch (error) {
|
|
50
|
+
return handleToolError("smart_query", error);
|
|
43
51
|
}
|
|
44
|
-
parts.push("\n---\nUse the schema and examples above to generate SQL, then execute with `run_query`.");
|
|
45
|
-
return { content: [{ type: "text", text: parts.join("\n") }] };
|
|
46
52
|
});
|
|
47
53
|
server.tool("manage_few_shot_examples", "Manage few-shot examples (list/add/remove). Register domain-specific examples to improve SQL accuracy.", {
|
|
48
54
|
data_source_id: z.number().describe("Data source ID"),
|
|
@@ -59,47 +65,52 @@ export function registerBirdTools(server) {
|
|
|
59
65
|
.describe("Example to add (required when action=add)"),
|
|
60
66
|
example_id: z.string().optional().describe("Example ID to remove (required when action=remove)"),
|
|
61
67
|
}, { destructiveHint: true }, async ({ data_source_id, action, example, example_id }) => {
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
68
|
+
try {
|
|
69
|
+
if (action === "list") {
|
|
70
|
+
const examples = await loadExamples(data_source_id);
|
|
71
|
+
if (examples.length === 0) {
|
|
72
|
+
return { content: [{ type: "text", text: "No few-shot examples registered." }] };
|
|
73
|
+
}
|
|
74
|
+
const text = examples
|
|
75
|
+
.map((e) => `**[${e.id}]** ${e.question}\n\`\`\`sql\n${e.sql}\n\`\`\`\nTables: ${e.tables.join(", ")} | Tags: ${e.tags.join(", ")} | Source: ${e.source}`)
|
|
76
|
+
.join("\n\n---\n\n");
|
|
77
|
+
return { content: [{ type: "text", text: `Total ${examples.length} example(s):\n\n${text}` }] };
|
|
66
78
|
}
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
79
|
+
if (action === "add") {
|
|
80
|
+
if (!example) {
|
|
81
|
+
return { content: [{ type: "text", text: "The example parameter is required." }] };
|
|
82
|
+
}
|
|
83
|
+
const added = await addExample(data_source_id, {
|
|
84
|
+
question: example.question,
|
|
85
|
+
sql: example.sql,
|
|
86
|
+
tables: example.tables,
|
|
87
|
+
tags: example.tags ?? [],
|
|
88
|
+
notes: example.notes ?? "",
|
|
89
|
+
source: "manual",
|
|
90
|
+
});
|
|
91
|
+
return {
|
|
92
|
+
content: [{ type: "text", text: `Few-shot example added. (ID: ${added.id})` }],
|
|
93
|
+
};
|
|
75
94
|
}
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
if (!example_id) {
|
|
90
|
-
return { content: [{ type: "text", text: "The example_id parameter is required." }] };
|
|
95
|
+
if (action === "remove") {
|
|
96
|
+
if (!example_id) {
|
|
97
|
+
return { content: [{ type: "text", text: "The example_id parameter is required." }] };
|
|
98
|
+
}
|
|
99
|
+
const removed = await removeExample(data_source_id, example_id);
|
|
100
|
+
return {
|
|
101
|
+
content: [
|
|
102
|
+
{
|
|
103
|
+
type: "text",
|
|
104
|
+
text: removed ? `Example ${example_id} has been removed.` : `Example ${example_id} not found.`,
|
|
105
|
+
},
|
|
106
|
+
],
|
|
107
|
+
};
|
|
91
108
|
}
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
type: "text",
|
|
97
|
-
text: removed ? `Example ${example_id} has been removed.` : `Example ${example_id} not found.`,
|
|
98
|
-
},
|
|
99
|
-
],
|
|
100
|
-
};
|
|
109
|
+
return { content: [{ type: "text", text: "Invalid action" }] };
|
|
110
|
+
}
|
|
111
|
+
catch (error) {
|
|
112
|
+
return handleToolError("manage_few_shot_examples", error);
|
|
101
113
|
}
|
|
102
|
-
return { content: [{ type: "text", text: "Invalid action" }] };
|
|
103
114
|
});
|
|
104
115
|
server.tool("submit_query_feedback", "Submit feedback on generated SQL. Incorrect SQL is automatically classified and may be promoted to a few-shot example.", {
|
|
105
116
|
data_source_id: z.number().describe("Data source ID"),
|
|
@@ -108,20 +119,25 @@ export function registerBirdTools(server) {
|
|
|
108
119
|
correct_sql: z.string().optional().describe("Correct SQL (provide when rating=down for automatic learning)"),
|
|
109
120
|
rating: z.enum(["up", "down"]).describe("Rating: up (correct) or down (incorrect)"),
|
|
110
121
|
}, { destructiveHint: true }, async ({ data_source_id, question, generated_sql, correct_sql, rating }) => {
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
122
|
+
try {
|
|
123
|
+
const entry = await recordFeedback(data_source_id, {
|
|
124
|
+
question,
|
|
125
|
+
generatedSql: generated_sql,
|
|
126
|
+
correctSql: correct_sql,
|
|
127
|
+
rating,
|
|
128
|
+
});
|
|
129
|
+
const parts = [`Feedback recorded. (ID: ${entry.id})`];
|
|
130
|
+
if (entry.errorType) {
|
|
131
|
+
parts.push(`Error type: ${entry.errorType}`);
|
|
132
|
+
}
|
|
133
|
+
if (entry.promotedToFewShot) {
|
|
134
|
+
parts.push("Repeated errors of the same type — auto-promoted to few-shot example.");
|
|
135
|
+
}
|
|
136
|
+
return { content: [{ type: "text", text: parts.join("\n") }] };
|
|
120
137
|
}
|
|
121
|
-
|
|
122
|
-
|
|
138
|
+
catch (error) {
|
|
139
|
+
return handleToolError("submit_query_feedback", error);
|
|
123
140
|
}
|
|
124
|
-
return { content: [{ type: "text", text: parts.join("\n") }] };
|
|
125
141
|
});
|
|
126
142
|
server.tool("evaluate_queries", "Manage SQL accuracy evaluation. Add/list test cases, run evaluations, and view results.", {
|
|
127
143
|
data_source_id: z.number().describe("Data source ID"),
|
|
@@ -144,63 +160,68 @@ export function registerBirdTools(server) {
|
|
|
144
160
|
.optional()
|
|
145
161
|
.describe("List of SQL to evaluate (required when action=run)"),
|
|
146
162
|
}, { destructiveHint: true }, async ({ data_source_id, action, test_case, test_case_id, generated_sqls }) => {
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
163
|
+
try {
|
|
164
|
+
if (action === "list_tests") {
|
|
165
|
+
const store = await loadTestSuite(data_source_id);
|
|
166
|
+
if (store.testCases.length === 0) {
|
|
167
|
+
return { content: [{ type: "text", text: "No test cases registered." }] };
|
|
168
|
+
}
|
|
169
|
+
const text = store.testCases
|
|
170
|
+
.map((tc) => `**[${tc.id}]** ${tc.question}\nDifficulty: ${tc.difficulty} | Tags: ${tc.tags.join(", ")}\n\`\`\`sql\n${tc.groundTruthSql}\n\`\`\``)
|
|
171
|
+
.join("\n\n---\n\n");
|
|
172
|
+
return { content: [{ type: "text", text: `Total ${store.testCases.length} test case(s):\n\n${text}` }] };
|
|
151
173
|
}
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
174
|
+
if (action === "add_test") {
|
|
175
|
+
if (!test_case) {
|
|
176
|
+
return { content: [{ type: "text", text: "The test_case parameter is required." }] };
|
|
177
|
+
}
|
|
178
|
+
const added = await addTestCase(data_source_id, {
|
|
179
|
+
question: test_case.question,
|
|
180
|
+
groundTruthSql: test_case.ground_truth_sql,
|
|
181
|
+
difficulty: test_case.difficulty,
|
|
182
|
+
tags: test_case.tags ?? [],
|
|
183
|
+
});
|
|
184
|
+
return {
|
|
185
|
+
content: [{ type: "text", text: `Test case added. (ID: ${added.id})` }],
|
|
186
|
+
};
|
|
160
187
|
}
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
188
|
+
if (action === "remove_test") {
|
|
189
|
+
if (!test_case_id) {
|
|
190
|
+
return { content: [{ type: "text", text: "The test_case_id parameter is required." }] };
|
|
191
|
+
}
|
|
192
|
+
const removed = await removeTestCase(data_source_id, test_case_id);
|
|
193
|
+
return {
|
|
194
|
+
content: [
|
|
195
|
+
{
|
|
196
|
+
type: "text",
|
|
197
|
+
text: removed ? `Test case ${test_case_id} has been removed.` : `Test case ${test_case_id} not found.`,
|
|
198
|
+
},
|
|
199
|
+
],
|
|
200
|
+
};
|
|
174
201
|
}
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
}
|
|
185
|
-
if (action === "run") {
|
|
186
|
-
if (!generated_sqls || generated_sqls.length === 0) {
|
|
187
|
-
return { content: [{ type: "text", text: "The generated_sqls parameter is required." }] };
|
|
202
|
+
if (action === "run") {
|
|
203
|
+
if (!generated_sqls || generated_sqls.length === 0) {
|
|
204
|
+
return { content: [{ type: "text", text: "The generated_sqls parameter is required." }] };
|
|
205
|
+
}
|
|
206
|
+
const run = await runEvaluation(data_source_id, generated_sqls.map((gs) => ({
|
|
207
|
+
testCaseId: gs.test_case_id,
|
|
208
|
+
generatedSql: gs.generated_sql,
|
|
209
|
+
})));
|
|
210
|
+
return { content: [{ type: "text", text: formatEvalResults(run) }] };
|
|
188
211
|
}
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
const store = await loadTestSuite(data_source_id);
|
|
197
|
-
if (store.runs.length === 0) {
|
|
198
|
-
return { content: [{ type: "text", text: "No evaluation runs found." }] };
|
|
212
|
+
if (action === "results") {
|
|
213
|
+
const store = await loadTestSuite(data_source_id);
|
|
214
|
+
if (store.runs.length === 0) {
|
|
215
|
+
return { content: [{ type: "text", text: "No evaluation runs found." }] };
|
|
216
|
+
}
|
|
217
|
+
const lastRun = store.runs[store.runs.length - 1];
|
|
218
|
+
return { content: [{ type: "text", text: formatEvalResults(lastRun) }] };
|
|
199
219
|
}
|
|
200
|
-
|
|
201
|
-
|
|
220
|
+
return { content: [{ type: "text", text: "Invalid action" }] };
|
|
221
|
+
}
|
|
222
|
+
catch (error) {
|
|
223
|
+
return handleToolError("evaluate_queries", error);
|
|
202
224
|
}
|
|
203
|
-
return { content: [{ type: "text", text: "Invalid action" }] };
|
|
204
225
|
});
|
|
205
226
|
server.tool("manage_keyword_map", "Manage keyword-to-table-name mappings. After inspecting the schema with list_tables, register domain-specific mappings to improve smart_query table-matching accuracy. e.g., {\"revenue\": [\"payment\"], \"creator\": [\"creator\"]}", {
|
|
206
227
|
data_source_id: z.number().describe("Data source ID"),
|
|
@@ -214,75 +235,85 @@ export function registerBirdTools(server) {
|
|
|
214
235
|
.optional()
|
|
215
236
|
.describe("Keywords to remove (required when action=remove). e.g., [\"revenue\", \"order\"]"),
|
|
216
237
|
}, { destructiveHint: true }, async ({ data_source_id, action, mappings, keywords }) => {
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
238
|
+
try {
|
|
239
|
+
if (action === "list") {
|
|
240
|
+
const effective = await getEffectiveMap(data_source_id);
|
|
241
|
+
const custom = await loadKeywordMap(data_source_id);
|
|
242
|
+
const defaultCount = Object.keys(DEFAULT_KEYWORD_MAP).length;
|
|
243
|
+
const customCount = Object.keys(custom).length;
|
|
244
|
+
const lines = [
|
|
245
|
+
`## Keyword Map (Data Source ${data_source_id})\n`,
|
|
246
|
+
`Default: ${defaultCount} | Custom: ${customCount} | Total: ${Object.keys(effective).length}\n`,
|
|
247
|
+
];
|
|
248
|
+
if (customCount > 0) {
|
|
249
|
+
lines.push("### Custom mappings:");
|
|
250
|
+
for (const [ko, en] of Object.entries(custom)) {
|
|
251
|
+
lines.push(`- **${ko}** → ${en.join(", ")}`);
|
|
252
|
+
}
|
|
253
|
+
lines.push("");
|
|
230
254
|
}
|
|
231
|
-
lines.push("");
|
|
255
|
+
lines.push("### All effective mappings:");
|
|
256
|
+
for (const [ko, en] of Object.entries(effective)) {
|
|
257
|
+
const isCustom = ko in custom;
|
|
258
|
+
lines.push(`- ${isCustom ? "**" : ""}${ko}${isCustom ? "** (custom)" : ""} → ${en.join(", ")}`);
|
|
259
|
+
}
|
|
260
|
+
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
232
261
|
}
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
262
|
+
if (action === "add") {
|
|
263
|
+
if (!mappings || Object.keys(mappings).length === 0) {
|
|
264
|
+
return { content: [{ type: "text", text: "The mappings parameter is required. e.g., {\"revenue\": [\"payment\"]}" }] };
|
|
265
|
+
}
|
|
266
|
+
const updated = await addMappings(data_source_id, mappings);
|
|
267
|
+
const added = Object.keys(mappings);
|
|
268
|
+
return {
|
|
269
|
+
content: [
|
|
270
|
+
{
|
|
271
|
+
type: "text",
|
|
272
|
+
text: `${added.length} keyword mapping(s) added/updated: ${added.join(", ")}`,
|
|
273
|
+
},
|
|
274
|
+
],
|
|
275
|
+
};
|
|
237
276
|
}
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
277
|
+
if (action === "remove") {
|
|
278
|
+
if (!keywords || keywords.length === 0) {
|
|
279
|
+
return { content: [{ type: "text", text: "The keywords parameter is required." }] };
|
|
280
|
+
}
|
|
281
|
+
await removeMappings(data_source_id, keywords);
|
|
282
|
+
return {
|
|
283
|
+
content: [{ type: "text", text: `${keywords.length} custom keyword(s) removed: ${keywords.join(", ")}` }],
|
|
284
|
+
};
|
|
243
285
|
}
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
type: "text",
|
|
250
|
-
text: `${added.length} keyword mapping(s) added/updated: ${added.join(", ")}`,
|
|
251
|
-
},
|
|
252
|
-
],
|
|
253
|
-
};
|
|
254
|
-
}
|
|
255
|
-
if (action === "remove") {
|
|
256
|
-
if (!keywords || keywords.length === 0) {
|
|
257
|
-
return { content: [{ type: "text", text: "The keywords parameter is required." }] };
|
|
286
|
+
if (action === "reset") {
|
|
287
|
+
await resetMappings(data_source_id);
|
|
288
|
+
return {
|
|
289
|
+
content: [{ type: "text", text: "Custom mappings have been reset. Only default mappings will be used." }],
|
|
290
|
+
};
|
|
258
291
|
}
|
|
259
|
-
|
|
260
|
-
return {
|
|
261
|
-
content: [{ type: "text", text: `${keywords.length} custom keyword(s) removed: ${keywords.join(", ")}` }],
|
|
262
|
-
};
|
|
292
|
+
return { content: [{ type: "text", text: "Invalid action" }] };
|
|
263
293
|
}
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
return {
|
|
267
|
-
content: [{ type: "text", text: "Custom mappings have been reset. Only default mappings will be used." }],
|
|
268
|
-
};
|
|
294
|
+
catch (error) {
|
|
295
|
+
return handleToolError("manage_keyword_map", error);
|
|
269
296
|
}
|
|
270
|
-
return { content: [{ type: "text", text: "Invalid action" }] };
|
|
271
297
|
});
|
|
272
298
|
server.tool("get_bird_config", "View BIRD SQL configuration and status.", {}, { readOnlyHint: true }, async () => {
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
299
|
+
try {
|
|
300
|
+
const config = await loadConfig();
|
|
301
|
+
const configDir = getConfigDir();
|
|
302
|
+
const lines = [
|
|
303
|
+
"## BIRD SQL Configuration\n",
|
|
304
|
+
`Config directory: \`${configDir}\`\n`,
|
|
305
|
+
"### Settings:",
|
|
306
|
+
`- Schema Pruning: ${config.bird.schemaPruning.enabled ? "ON" : "OFF"} (Top-K: ${config.bird.schemaPruning.topK})`,
|
|
307
|
+
`- Few-shot Examples: ${config.bird.fewShot.enabled ? "ON" : "OFF"} (Max per query: ${config.bird.fewShot.maxExamplesPerQuery})`,
|
|
308
|
+
`- Feedback Loop: ${config.bird.feedback.enabled ? "ON" : "OFF"} (Auto-promote threshold: ${config.bird.feedback.autoPromoteThreshold})`,
|
|
309
|
+
`- Complexity Assessment: ${config.bird.complexity.enabled ? "ON" : "OFF"}`,
|
|
310
|
+
"",
|
|
311
|
+
`Edit \`${configDir}/config.json\` to customize settings.`,
|
|
312
|
+
];
|
|
313
|
+
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
314
|
+
}
|
|
315
|
+
catch (error) {
|
|
316
|
+
return handleToolError("get_bird_config", error);
|
|
317
|
+
}
|
|
287
318
|
});
|
|
288
319
|
}
|