@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shahid.la/cds-db-nlquery-mcp",
3
- "version": "0.5.2",
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": {
@@ -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 amount"): use "valCol" instead of "val"
26
- { "col": "collateral.VALUE", "op": "<", "valCol": "AMOUNT" } both sides can be
27
- association paths.
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.0' },
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`);
@@ -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
- cols = filtered.map(colRef);
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.
@@ -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
  })