redash-mcp 3.0.3 → 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 +225 -194
- package/dist/index.js +434 -326
- package/dist/query-cache.js +15 -0
- package/dist/redash-client.js +71 -11
- package/dist/tool-error.js +40 -0
- 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
|
@@ -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"),
|
|
@@ -58,48 +64,53 @@ export function registerBirdTools(server) {
|
|
|
58
64
|
.optional()
|
|
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
|
-
}, {
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
67
|
+
}, {}, async ({ data_source_id, action, example, example_id }) => {
|
|
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"),
|
|
@@ -107,21 +118,26 @@ export function registerBirdTools(server) {
|
|
|
107
118
|
generated_sql: z.string().describe("Generated SQL"),
|
|
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
|
-
}, {
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
121
|
+
}, {}, async ({ data_source_id, question, generated_sql, correct_sql, rating }) => {
|
|
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"),
|
|
@@ -143,146 +159,161 @@ export function registerBirdTools(server) {
|
|
|
143
159
|
}))
|
|
144
160
|
.optional()
|
|
145
161
|
.describe("List of SQL to evaluate (required when action=run)"),
|
|
146
|
-
}, {
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
162
|
+
}, {}, async ({ data_source_id, action, test_case, test_case_id, generated_sqls }) => {
|
|
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"),
|
|
207
228
|
action: z.enum(["list", "add", "remove", "reset"]).describe("Action to perform"),
|
|
208
229
|
mappings: z
|
|
209
|
-
.record(z.array(z.string()))
|
|
230
|
+
.record(z.string(), z.array(z.string()))
|
|
210
231
|
.optional()
|
|
211
232
|
.describe("Mappings to add (required when action=add). e.g., {\"revenue\": [\"payment\", \"billing\"]}"),
|
|
212
233
|
keywords: z
|
|
213
234
|
.array(z.string())
|
|
214
235
|
.optional()
|
|
215
236
|
.describe("Keywords to remove (required when action=remove). e.g., [\"revenue\", \"order\"]"),
|
|
216
|
-
}, {
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
237
|
+
}, {}, async ({ data_source_id, action, mappings, keywords }) => {
|
|
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
|
}
|