@shahid.la/cds-db-nlquery-mcp 0.5.2 → 0.5.3
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/package.json +1 -1
- package/src/llm-planner.js +10 -3
- package/src/mcp-server.js +2 -2
- package/src/query-executor.js +20 -1
- package/src/schema-reader.js +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shahid.la/cds-db-nlquery-mcp",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.3",
|
|
4
4
|
"description": "MCP server for natural language queries against CDS db-layer entities. Targets raw db/schema.cds entities via cds.run() with real SQL JOINs — bring your own LLM (Anthropic, OpenAI, or any OpenAI-compatible endpoint).",
|
|
5
5
|
"main": "src/mcp-server.js",
|
|
6
6
|
"bin": {
|
package/src/llm-planner.js
CHANGED
|
@@ -22,9 +22,15 @@ DESCRIPTOR FORMAT:
|
|
|
22
22
|
}
|
|
23
23
|
|
|
24
24
|
Comparing two columns instead of a column to a fixed value (e.g. "collateral worth
|
|
25
|
-
less than the loan
|
|
26
|
-
|
|
27
|
-
|
|
25
|
+
less than the loan it secures"): use "valCol" instead of "val". Both sides can be
|
|
26
|
+
plain columns or association paths, but they must both be reachable as paths FROM
|
|
27
|
+
the entity you choose as "entity" — pick "entity" by checking the schema's "assoc:"
|
|
28
|
+
list for whichever side gives you a path to the other. For example, if a
|
|
29
|
+
"Collateral" entity has an association to "Loan" (e.g. "loan"), but "Loan" has no
|
|
30
|
+
association back to "Collateral", you must start from "Collateral":
|
|
31
|
+
{ "entity": "Collateral", "select": [...], "where": [{ "col": "VALUE", "op": "<", "valCol": "loan.AMOUNT" }] }
|
|
32
|
+
— NOT from "Loan" (it has no path to collateral, so don't invent one or substitute
|
|
33
|
+
an unrelated column).
|
|
28
34
|
|
|
29
35
|
JOINS — use association path expressions, no "join" field needed:
|
|
30
36
|
- To access a related entity's column: "assocAlias.COLUMN" in select or where
|
|
@@ -51,6 +57,7 @@ RULES:
|
|
|
51
57
|
9. Do NOT add extra filters, conditions, joins, or business assumptions that the user did not ask for. Prefer the minimum query that answers the question.
|
|
52
58
|
10. Words like "active", "closed", or "expired" usually describe a status value only. Do NOT infer overdue payments, arrears, missed instalments, dunning, or delinquency unless the question explicitly asks for them.
|
|
53
59
|
11. If the question asks for "active loans with customer name and loan amount", that means filter loan status to active and select the customer name and loan amount. It does NOT imply any payment-status filter such as payments.DAYS_OVERDUE > 0.
|
|
60
|
+
12. Before picking "entity", check that every column/path you plan to use in select/where/orderBy is actually reachable from it via that entity's own "assoc:" list (directly, or hop by hop). If a needed column lives on an entity that only has an association TO your candidate entity (not FROM it), start from that other entity instead — never invent an association alias that isn't listed, and never substitute an unrelated column when the right path doesn't exist on your first choice of entity.
|
|
54
61
|
|
|
55
62
|
SCHEMA:
|
|
56
63
|
${schemaText}`;
|
package/src/mcp-server.js
CHANGED
|
@@ -111,7 +111,7 @@ async function bootstrap() {
|
|
|
111
111
|
// ── MCP server ───────────────────────────────────────────────────────────────
|
|
112
112
|
|
|
113
113
|
const server = new Server(
|
|
114
|
-
{ name: 'cds-db-nlquery-mcp', version: '0.5.
|
|
114
|
+
{ name: 'cds-db-nlquery-mcp', version: '0.5.3' },
|
|
115
115
|
{ capabilities: { tools: {} } }
|
|
116
116
|
);
|
|
117
117
|
|
|
@@ -222,7 +222,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
222
222
|
process.stderr.write(` entity : ${descriptor.entity}\n`);
|
|
223
223
|
if (descriptor.select) process.stderr.write(` select : ${descriptor.select.join(', ')}\n`);
|
|
224
224
|
(descriptor.where || []).forEach(w =>
|
|
225
|
-
process.stderr.write(` where : ${w.col} ${w.op} ${JSON.stringify(w.val)}\n`)
|
|
225
|
+
process.stderr.write(` where : ${w.col} ${w.op} ${w.valCol ? `[col]${w.valCol}` : JSON.stringify(w.val)}\n`)
|
|
226
226
|
);
|
|
227
227
|
if (descriptor.orderBy) process.stderr.write(` order : ${descriptor.orderBy} ${descriptor.orderDir || 'ASC'}\n`);
|
|
228
228
|
process.stderr.write(` limit : ${descriptor.limit || 50}\n`);
|
package/src/query-executor.js
CHANGED
|
@@ -178,7 +178,26 @@ async function executeDescriptor(descriptor, schema, callConfig = {}) {
|
|
|
178
178
|
let cols = null;
|
|
179
179
|
if (select?.length) {
|
|
180
180
|
const filtered = select.filter(c => !allBlocked.has(c.split('.').pop()));
|
|
181
|
-
|
|
181
|
+
|
|
182
|
+
// The LLM is told (rule 7) never to select the same leaf column name twice
|
|
183
|
+
// (e.g. "CURRENCY" via the top-level entity AND via "loan.CURRENCY"), but a
|
|
184
|
+
// cheap model occasionally does it anyway — HANA then rejects the query with
|
|
185
|
+
// "Duplicate column names". Rather than depend on prompt compliance, alias any
|
|
186
|
+
// joined-path column (length > 1) whose leaf name collides with another
|
|
187
|
+
// selected column, so the query is always valid regardless of what the LLM did.
|
|
188
|
+
const leafCounts = {};
|
|
189
|
+
for (const c of filtered) {
|
|
190
|
+
const leaf = c.split('.').pop();
|
|
191
|
+
leafCounts[leaf] = (leafCounts[leaf] || 0) + 1;
|
|
192
|
+
}
|
|
193
|
+
cols = filtered.map(c => {
|
|
194
|
+
const parts = c.split('.');
|
|
195
|
+
const leaf = parts[parts.length - 1];
|
|
196
|
+
if (leafCounts[leaf] > 1 && parts.length > 1) {
|
|
197
|
+
return { ref: parts, as: parts.join('_') };
|
|
198
|
+
}
|
|
199
|
+
return colRef(c);
|
|
200
|
+
});
|
|
182
201
|
} else if (allBlocked.size > 0) {
|
|
183
202
|
// No explicit select ("all columns") but some columns are blocked. Without this,
|
|
184
203
|
// the query would fall through to SELECT * — fetching blocked columns (e.g.
|
package/src/schema-reader.js
CHANGED
|
@@ -163,7 +163,7 @@ function buildSchemaPrompt(schema) {
|
|
|
163
163
|
s += `{values: ${pairs} — use the raw value in filters}`;
|
|
164
164
|
}
|
|
165
165
|
if (meta.textVia) {
|
|
166
|
-
s += `{readable text available via "${meta.textVia}" — include it in select to show the human-readable value}`;
|
|
166
|
+
s += `{readable text available via "${meta.textVia}" — include it in select to show the human-readable value, AND use this path (not the raw "${c}" column) when the question filters by a human term like "active"/"closed"/"overdue" rather than a raw code}`;
|
|
167
167
|
}
|
|
168
168
|
return s;
|
|
169
169
|
})
|