Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 43 additions & 0 deletions packages/backend/src/mcp-server/error-hints.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
});
});
37 changes: 37 additions & 0 deletions packages/backend/src/mcp-server/error-hints.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down
Loading