From 333d4dbf927655765a31ee52b9dc76e1f8070bad Mon Sep 17 00:00:00 2001 From: Matteo Date: Thu, 17 Sep 2026 10:43:08 +0200 Subject: [PATCH] feat(mcp): actionable hints when a SQL-backed API rejects a filter MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Several customer connectors sit on SQL Server views and leak the database's own complaint. On 2026-09-16 one of them failed 16 of 155 calls, every time the model inventing filter grammar and then permuting spellings: filter=client_number in (13078,13201) -> Falsche Syntax in der Nähe von ")" filter=deleted eq false -> Ungültiger Spaltenname "false" fields=...,exit_date,... -> Invalid field in selected fields The body already said what was wrong; nothing said what to do instead. Three hints, keyed on the error text rather than the host because the phrasing comes from the database, not the vendor: quote the boolean, split the IN list into one call per value, and drop the field list to see which fields exist instead of guessing another spelling. --- .../src/mcp-server/error-hints.spec.ts | 43 +++++++++++++++++++ .../backend/src/mcp-server/error-hints.ts | 37 ++++++++++++++++ 2 files changed, 80 insertions(+) diff --git a/packages/backend/src/mcp-server/error-hints.spec.ts b/packages/backend/src/mcp-server/error-hints.spec.ts index b6b5309c..94a36d6e 100644 --- a/packages/backend/src/mcp-server/error-hints.spec.ts +++ b/packages/backend/src/mcp-server/error-hints.spec.ts @@ -71,3 +71,46 @@ describe('hostFromAxiosConfig', () => { expect(hostFromAxiosConfig(undefined)).toBeUndefined(); }); }); + +describe('deriveErrorHint — SQL-backed customer APIs', () => { + // Real failures from a customer connector on 2026-09-16: 16 errors in 155 + // calls, every one of them the model inventing filter grammar. + it('names the boolean when SQL read a filter value as a column', () => { + const hint = deriveErrorHint({ + status: 400, + body: { message: 'Ungültiger Spaltenname "false".', error: 'Bad Request' }, + }); + expect(hint).toMatch(/Booleans are the usual culprit/); + }); + + it('answers the English phrasing of the same error', () => { + const hint = deriveErrorHint({ + status: 400, + body: { message: "Invalid column name 'false'." }, + }); + expect(hint).toMatch(/was not quoted/); + }); + + it('tells the model that a parenthesised IN list is the problem, not the value', () => { + const hint = deriveErrorHint({ + status: 400, + body: { + message: + 'Falsche Syntax in der Nähe von ")".\r\nUngültige Verwendung der Option NEXT in der FETCH-Anweisung.', + }, + }); + expect(hint).toMatch(/one call per value/); + }); + + it('tells the model to drop the field list rather than permute spellings', () => { + const hint = deriveErrorHint({ + status: 400, + body: { message: 'Invalid field in selected fields: exit_date' }, + }); + expect(hint).toMatch(/`fields` omitted/); + }); + + it('still says nothing about an error it does not recognise', () => { + expect(deriveErrorHint({ status: 500, body: { message: 'boom' } })).toBeUndefined(); + }); +}); diff --git a/packages/backend/src/mcp-server/error-hints.ts b/packages/backend/src/mcp-server/error-hints.ts index 6f6dc4c4..973c92af 100644 --- a/packages/backend/src/mcp-server/error-hints.ts +++ b/packages/backend/src/mcp-server/error-hints.ts @@ -56,6 +56,28 @@ const NEW_COUNTRY_HINT = 'must open the email the service just sent ("new country / new device") and ' + 'confirm the login, then the tool works. Tell the user exactly that.'; +const SQL_COLUMN_HINT = + 'The upstream folded your `filter` into SQL and SQL read part of it as a column ' + + 'name, which means the value was not quoted the way this API expects. Booleans ' + + 'are the usual culprit: `deleted eq false` becomes the column `false`. Try the ' + + 'call again with that condition removed to confirm the rest of the filter is ' + + 'fine, then reintroduce it using the spelling the endpoint documents (often `0` ' + + 'and `1`, or a quoted `"false"`). Do not guess more than one variant per call.'; + +const SQL_SYNTAX_HINT = + 'The upstream folded your `filter` into SQL and SQL rejected the syntax, so this ' + + 'is a filter GRAMMAR problem, not a wrong value. A parenthesised list such as ' + + '`client_number in (1,2)` is the common cause: most of these endpoints accept ' + + 'only simple `field op value` conditions joined with AND. Split it into one call ' + + 'per value, or drop the filter, read what a plain page returns, and filter on a ' + + 'field you have seen. Retrying the same expression will fail the same way.'; + +const SELECTED_FIELDS_HINT = + 'One of the names in `fields` does not exist on this view, and the API rejects ' + + 'the whole list because of it. Do not guess another spelling: repeat the call ' + + 'with `fields` omitted, read the field names that actually come back, then ask ' + + 'again with only those.'; + function bodyText(body: unknown): string { if (body === undefined || body === null) return ''; if (typeof body === 'string') return body; @@ -84,6 +106,21 @@ export function deriveErrorHint(input: ErrorHintInput): string | undefined { return NEW_COUNTRY_HINT; } + // SQL-backed APIs (several customer connectors sit on SQL Server views) leak + // the database's own complaint. The model reads "Ungültiger Spaltenname" and + // starts permuting field names; these three say which part of the request to + // change. Keyed on the text, not the host, because the phrasing belongs to the + // database rather than to any one vendor. + if (/Invalid field in selected fields/i.test(text)) { + return SELECTED_FIELDS_HINT; + } + if (/Ungültiger Spaltenname|Invalid column name/i.test(text)) { + return SQL_COLUMN_HINT; + } + if (/Falsche Syntax in der Nähe von|Incorrect syntax near/i.test(text)) { + return SQL_SYNTAX_HINT; + } + if (hostMatches(input.host, 'weclapp.com')) { if (/unknown property|unexpected filter property/i.test(text)) { return WECLAPP_FIELD_HINT;