From d0bc8a5e0fed3c1bc9ec5584cd3fcdd2eaef9a0b Mon Sep 17 00:00:00 2001 From: flfeders Date: Wed, 9 Sep 2026 14:19:34 +0200 Subject: [PATCH] fix(mcp): exclude archived records --- backend/src/mcp/result.ts | 35 ++++++++++++++++++--- backend/src/mcp/tools/accounting.ts | 18 +++++------ backend/src/mcp/tools/masterdata.ts | 40 +++++++++--------------- backend/src/mcp/tools/organisation.ts | 19 +++++------ backend/tests/mcpArchivedResults.test.ts | 37 ++++++++++++++++++++++ 5 files changed, 96 insertions(+), 53 deletions(-) create mode 100644 backend/tests/mcpArchivedResults.test.ts diff --git a/backend/src/mcp/result.ts b/backend/src/mcp/result.ts index f3bfab7..f894b25 100644 --- a/backend/src/mcp/result.ts +++ b/backend/src/mcp/result.ts @@ -1,16 +1,42 @@ import { McpToolResult } from "./types" +const OMIT_ARCHIVED = Symbol("omit-archived") + +function omitArchivedRecords(value: unknown): unknown | typeof OMIT_ARCHIVED { + if (Array.isArray(value)) { + return value + .map(omitArchivedRecords) + .filter((item) => item !== OMIT_ARCHIVED) + } + + if (!value || typeof value !== "object") return value + + const prototype = Object.getPrototypeOf(value) + if (prototype !== Object.prototype && prototype !== null) return value + + const record = value as Record + if (record.archived === true) return OMIT_ARCHIVED + + return Object.fromEntries( + Object.entries(record) + .map(([key, item]) => [key, omitArchivedRecords(item)] as const) + .filter(([, item]) => item !== OMIT_ARCHIVED), + ) +} + export function asToolResult(payload: unknown): McpToolResult { + const sanitizedPayload = omitArchivedRecords(payload) + const resultPayload = sanitizedPayload === OMIT_ARCHIVED ? {} : sanitizedPayload const structuredContent = - payload && typeof payload === "object" && !Array.isArray(payload) - ? payload as Record - : { result: payload } + resultPayload && typeof resultPayload === "object" && !Array.isArray(resultPayload) + ? resultPayload as Record + : { result: resultPayload } return { content: [ { type: "text", - text: JSON.stringify(payload, null, 2), + text: JSON.stringify(resultPayload, null, 2), }, ], structuredContent, @@ -33,4 +59,3 @@ export function asToolError(error: unknown): McpToolResult { }, } } - diff --git a/backend/src/mcp/tools/accounting.ts b/backend/src/mcp/tools/accounting.ts index ae840c3..89e8c2e 100644 --- a/backend/src/mcp/tools/accounting.ts +++ b/backend/src/mcp/tools/accounting.ts @@ -437,7 +437,6 @@ export const accountingTools: McpTool[] = [ state: { type: "string", description: "Optionaler Statusfilter, z. B. Entwurf oder Gebucht." }, customer: { type: "number" }, project: { type: "number" }, - includeArchived: { type: "boolean", default: false }, limit: { type: "number", minimum: 1, maximum: 100 }, }, }, @@ -455,7 +454,7 @@ export const accountingTools: McpTool[] = [ if (state) conditions.push(eq(createddocuments.state, state)) if (customer) conditions.push(eq(createddocuments.customer, customer)) if (project) conditions.push(eq(createddocuments.project, project)) - if (args.includeArchived !== true) conditions.push(eq(createddocuments.archived, false)) + conditions.push(eq(createddocuments.archived, false)) const rows = await context.server.db .select() @@ -486,7 +485,7 @@ export const accountingTools: McpTool[] = [ const rows = await context.server.db .select() .from(createddocuments) - .where(and(eq(createddocuments.id, id), eq(createddocuments.tenant, context.tenantId))) + .where(and(eq(createddocuments.id, id), eq(createddocuments.tenant, context.tenantId), eq(createddocuments.archived, false))) .limit(1) if (!rows[0]) throw new Error("Ausgangsbeleg nicht gefunden") @@ -754,7 +753,6 @@ export const accountingTools: McpTool[] = [ properties: { state: { type: "string", description: "Optionaler Statusfilter." }, paid: { type: "boolean", description: "Optionaler Zahlungsstatus." }, - includeArchived: { type: "boolean", default: false }, limit: { type: "number", minimum: 1, maximum: 100 }, }, }, @@ -764,7 +762,7 @@ export const accountingTools: McpTool[] = [ if (state) conditions.push(eq(incominginvoices.state, state)) if (typeof args.paid === "boolean") conditions.push(eq(incominginvoices.paid, args.paid)) - if (args.includeArchived !== true) conditions.push(eq(incominginvoices.archived, false)) + conditions.push(eq(incominginvoices.archived, false)) const rows = await context.server.db .select() @@ -795,7 +793,7 @@ export const accountingTools: McpTool[] = [ const rows = await context.server.db .select() .from(incominginvoices) - .where(and(eq(incominginvoices.id, id), eq(incominginvoices.tenant, context.tenantId))) + .where(and(eq(incominginvoices.id, id), eq(incominginvoices.tenant, context.tenantId), eq(incominginvoices.archived, false))) .limit(1) if (!rows[0]) throw new Error("Eingangsrechnung nicht gefunden") @@ -1142,7 +1140,6 @@ export const accountingTools: McpTool[] = [ type: "object", properties: { account: { type: "number", description: "Optionale Bankkonto-ID." }, - includeArchived: { type: "boolean", default: false }, limit: { type: "number", minimum: 1, maximum: 100 }, }, }, @@ -1151,7 +1148,7 @@ export const accountingTools: McpTool[] = [ const account = numberArg(args, "account") if (account) conditions.push(eq(bankstatements.account, account)) - if (args.includeArchived !== true) conditions.push(eq(bankstatements.archived, false)) + conditions.push(eq(bankstatements.archived, false)) const rows = await context.server.db .select() @@ -1182,7 +1179,7 @@ export const accountingTools: McpTool[] = [ const rows = await context.server.db .select() .from(bankstatements) - .where(and(eq(bankstatements.id, id), eq(bankstatements.tenant, context.tenantId))) + .where(and(eq(bankstatements.id, id), eq(bankstatements.tenant, context.tenantId), eq(bankstatements.archived, false))) .limit(1) if (!rows[0]) throw new Error("Bankumsatz nicht gefunden") @@ -1199,7 +1196,6 @@ export const accountingTools: McpTool[] = [ properties: { bankstatement: { type: "number" }, incominginvoice: { type: "number" }, - includeArchived: { type: "boolean", default: false }, limit: { type: "number", minimum: 1, maximum: 100 }, }, }, @@ -1210,7 +1206,7 @@ export const accountingTools: McpTool[] = [ if (bankstatement) conditions.push(eq(statementallocations.bankstatement, bankstatement)) if (incominginvoice) conditions.push(eq(statementallocations.incominginvoice, incominginvoice)) - if (args.includeArchived !== true) conditions.push(eq(statementallocations.archived, false)) + conditions.push(eq(statementallocations.archived, false)) const rows = await context.server.db .select() diff --git a/backend/src/mcp/tools/masterdata.ts b/backend/src/mcp/tools/masterdata.ts index ccfc792..fef5c7e 100644 --- a/backend/src/mcp/tools/masterdata.ts +++ b/backend/src/mcp/tools/masterdata.ts @@ -55,7 +55,7 @@ export const masterdataTools: McpTool[] = [ const rows = await context.server.db .select() .from(customers) - .where(and(eq(customers.id, id), eq(customers.tenant, context.tenantId))) + .where(and(eq(customers.id, id), eq(customers.tenant, context.tenantId), eq(customers.archived, false))) .limit(1) if (!rows[0]) throw new Error("Kunde nicht gefunden") @@ -71,7 +71,6 @@ export const masterdataTools: McpTool[] = [ type: "object", properties: { query: { type: "string", description: "Suchtext für Name, Lieferantennummer oder Notizen." }, - includeArchived: { type: "boolean", default: false }, limit: { type: "number", minimum: 1, maximum: 100 }, }, }, @@ -86,7 +85,7 @@ export const masterdataTools: McpTool[] = [ ilike(vendors.notes, `%${query}%`) )) } - if (args.includeArchived !== true) conditions.push(eq(vendors.archived, false)) + conditions.push(eq(vendors.archived, false)) const rows = await context.server.db .select() @@ -115,7 +114,7 @@ export const masterdataTools: McpTool[] = [ const rows = await context.server.db .select() .from(vendors) - .where(and(eq(vendors.id, id), eq(vendors.tenant, context.tenantId))) + .where(and(eq(vendors.id, id), eq(vendors.tenant, context.tenantId), eq(vendors.archived, false))) .limit(1) if (!rows[0]) throw new Error("Lieferant nicht gefunden") @@ -133,7 +132,6 @@ export const masterdataTools: McpTool[] = [ query: { type: "string", description: "Suchtext für Name, E-Mail, Telefon, Rolle oder Notizen." }, customer: { type: "number" }, vendor: { type: "number" }, - includeArchived: { type: "boolean", default: false }, limit: { type: "number", minimum: 1, maximum: 100 }, }, }, @@ -157,7 +155,7 @@ export const masterdataTools: McpTool[] = [ } if (customer) conditions.push(eq(contacts.customer, customer)) if (vendor) conditions.push(eq(contacts.vendor, vendor)) - if (args.includeArchived !== true) conditions.push(eq(contacts.archived, false)) + conditions.push(eq(contacts.archived, false)) const rows = await context.server.db .select() @@ -178,7 +176,6 @@ export const masterdataTools: McpTool[] = [ type: "object", properties: { query: { type: "string", description: "Suchtext für Name, Artikelnummer, Hersteller, EAN, Barcode oder Beschreibung." }, - includeArchived: { type: "boolean", default: false }, limit: { type: "number", minimum: 1, maximum: 100 }, }, }, @@ -197,7 +194,7 @@ export const masterdataTools: McpTool[] = [ ilike(products.description, `%${query}%`) )) } - if (args.includeArchived !== true) conditions.push(eq(products.archived, false)) + conditions.push(eq(products.archived, false)) const rows = await context.server.db .select() @@ -226,7 +223,7 @@ export const masterdataTools: McpTool[] = [ const rows = await context.server.db .select() .from(products) - .where(and(eq(products.id, id), eq(products.tenant, context.tenantId))) + .where(and(eq(products.id, id), eq(products.tenant, context.tenantId), eq(products.archived, false))) .limit(1) if (!rows[0]) throw new Error("Artikel nicht gefunden") @@ -242,7 +239,6 @@ export const masterdataTools: McpTool[] = [ type: "object", properties: { query: { type: "string", description: "Suchtext für Name, Leistungsnummer oder Beschreibung." }, - includeArchived: { type: "boolean", default: false }, limit: { type: "number", minimum: 1, maximum: 100 }, }, }, @@ -256,7 +252,7 @@ export const masterdataTools: McpTool[] = [ ilike(services.description, `%${query}%`) )) } - if (args.includeArchived !== true) conditions.push(eq(services.archived, false)) + conditions.push(eq(services.archived, false)) const rows = await context.server.db .select() @@ -285,7 +281,7 @@ export const masterdataTools: McpTool[] = [ const rows = await context.server.db .select() .from(services) - .where(and(eq(services.id, id), eq(services.tenant, context.tenantId))) + .where(and(eq(services.id, id), eq(services.tenant, context.tenantId), eq(services.archived, false))) .limit(1) if (!rows[0]) throw new Error("Leistung nicht gefunden") @@ -303,7 +299,6 @@ export const masterdataTools: McpTool[] = [ query: { type: "string", description: "Suchtext für Nummer, Name oder Beschreibung." }, branch: { type: "number" }, project: { type: "number" }, - includeArchived: { type: "boolean", default: false }, limit: { type: "number", minimum: 1, maximum: 100 }, }, }, @@ -322,7 +317,7 @@ export const masterdataTools: McpTool[] = [ } if (branch) conditions.push(eq(costcentres.branch, branch)) if (project) conditions.push(eq(costcentres.project, project)) - if (args.includeArchived !== true) conditions.push(eq(costcentres.archived, false)) + conditions.push(eq(costcentres.archived, false)) const rows = await context.server.db .select() @@ -351,7 +346,7 @@ export const masterdataTools: McpTool[] = [ const rows = await context.server.db .select() .from(costcentres) - .where(and(eq(costcentres.id, id), eq(costcentres.tenant, context.tenantId))) + .where(and(eq(costcentres.id, id), eq(costcentres.tenant, context.tenantId), eq(costcentres.archived, false))) .limit(1) if (!rows[0]) throw new Error("Kostenstelle nicht gefunden") @@ -367,7 +362,6 @@ export const masterdataTools: McpTool[] = [ type: "object", properties: { query: { type: "string", description: "Suchtext für Nummer, Name oder Beschreibung." }, - includeArchived: { type: "boolean", default: false }, limit: { type: "number", minimum: 1, maximum: 100 }, }, }, @@ -382,7 +376,7 @@ export const masterdataTools: McpTool[] = [ ilike(branches.description, `%${query}%`) )) } - if (args.includeArchived !== true) conditions.push(eq(branches.archived, false)) + conditions.push(eq(branches.archived, false)) const rows = await context.server.db .select() @@ -404,7 +398,6 @@ export const masterdataTools: McpTool[] = [ properties: { query: { type: "string", description: "Suchtext für Name oder Beschreibung." }, branch: { type: "number" }, - includeArchived: { type: "boolean", default: false }, limit: { type: "number", minimum: 1, maximum: 100 }, }, }, @@ -420,7 +413,7 @@ export const masterdataTools: McpTool[] = [ )) } if (branch) conditions.push(eq(teams.branch, branch)) - if (args.includeArchived !== true) conditions.push(eq(teams.archived, false)) + conditions.push(eq(teams.archived, false)) const rows = await context.server.db .select() @@ -441,7 +434,6 @@ export const masterdataTools: McpTool[] = [ type: "object", properties: { query: { type: "string", description: "Suchtext für Name, Kennzeichen, FIN oder Farbe." }, - includeArchived: { type: "boolean", default: false }, limit: { type: "number", minimum: 1, maximum: 100 }, }, }, @@ -457,7 +449,7 @@ export const masterdataTools: McpTool[] = [ ilike(vehicles.color, `%${query}%`) )) } - if (args.includeArchived !== true) conditions.push(eq(vehicles.archived, false)) + conditions.push(eq(vehicles.archived, false)) const rows = await context.server.db .select() @@ -479,7 +471,6 @@ export const masterdataTools: McpTool[] = [ properties: { query: { type: "string", description: "Suchtext für Name, Artikelnummer, Seriennummer, Hersteller oder Beschreibung." }, vendor: { type: "number" }, - includeArchived: { type: "boolean", default: false }, limit: { type: "number", minimum: 1, maximum: 100 }, }, }, @@ -499,7 +490,7 @@ export const masterdataTools: McpTool[] = [ )) } if (vendor) conditions.push(eq(inventoryitems.vendor, vendor)) - if (args.includeArchived !== true) conditions.push(eq(inventoryitems.archived, false)) + conditions.push(eq(inventoryitems.archived, false)) const rows = await context.server.db .select() @@ -528,7 +519,7 @@ export const masterdataTools: McpTool[] = [ const rows = await context.server.db .select() .from(inventoryitems) - .where(and(eq(inventoryitems.id, id), eq(inventoryitems.tenant, context.tenantId))) + .where(and(eq(inventoryitems.id, id), eq(inventoryitems.tenant, context.tenantId), eq(inventoryitems.archived, false))) .limit(1) if (!rows[0]) throw new Error("Inventar nicht gefunden") @@ -568,4 +559,3 @@ export const masterdataTools: McpTool[] = [ }, }, ] - diff --git a/backend/src/mcp/tools/organisation.ts b/backend/src/mcp/tools/organisation.ts index 42a8ead..61d498d 100644 --- a/backend/src/mcp/tools/organisation.ts +++ b/backend/src/mcp/tools/organisation.ts @@ -30,7 +30,6 @@ export const organisationTools: McpTool[] = [ type: "object", properties: { query: { type: "string", description: "Suchtext für Name, Kundennummer, Vorname, Nachname oder Notizen." }, - includeArchived: { type: "boolean", default: false }, limit: { type: "number", minimum: 1, maximum: 100 }, }, }, @@ -47,7 +46,7 @@ export const organisationTools: McpTool[] = [ ilike(customers.notes, `%${query}%`) )) } - if (args.includeArchived !== true) conditions.push(eq(customers.archived, false)) + conditions.push(eq(customers.archived, false)) const rows = await context.server.db .select({ @@ -80,7 +79,6 @@ export const organisationTools: McpTool[] = [ query: { type: "string", description: "Suchtext für Name, Projektnummer, Kundenreferenz oder Notizen." }, customer: { type: "number" }, activePhase: { type: "string" }, - includeArchived: { type: "boolean", default: false }, limit: { type: "number", minimum: 1, maximum: 100 }, }, }, @@ -100,7 +98,7 @@ export const organisationTools: McpTool[] = [ } if (customer) conditions.push(eq(projects.customer, customer)) if (activePhase) conditions.push(eq(projects.active_phase, activePhase)) - if (args.includeArchived !== true) conditions.push(eq(projects.archived, false)) + conditions.push(eq(projects.archived, false)) const rows = await context.server.db .select() @@ -131,7 +129,7 @@ export const organisationTools: McpTool[] = [ const rows = await context.server.db .select() .from(projects) - .where(and(eq(projects.id, id), eq(projects.tenant, context.tenantId))) + .where(and(eq(projects.id, id), eq(projects.tenant, context.tenantId), eq(projects.archived, false))) .limit(1) if (!rows[0]) throw new Error("Projekt nicht gefunden") @@ -219,7 +217,6 @@ export const organisationTools: McpTool[] = [ properties: { query: { type: "string", description: "Suchtext für Name." }, customer: { type: "number" }, - includeArchived: { type: "boolean", default: false }, limit: { type: "number", minimum: 1, maximum: 100 }, }, }, @@ -230,7 +227,7 @@ export const organisationTools: McpTool[] = [ if (query) conditions.push(ilike(plants.name, `%${query}%`)) if (customer) conditions.push(eq(plants.customer, customer)) - if (args.includeArchived !== true) conditions.push(eq(plants.archived, false)) + conditions.push(eq(plants.archived, false)) const rows = await context.server.db .select() @@ -254,7 +251,6 @@ export const organisationTools: McpTool[] = [ project: { type: "number" }, customer: { type: "number" }, eventtype: { type: "string" }, - includeArchived: { type: "boolean", default: false }, limit: { type: "number", minimum: 1, maximum: 100 }, }, }, @@ -275,7 +271,7 @@ export const organisationTools: McpTool[] = [ if (project) conditions.push(eq(events.project, project)) if (customer) conditions.push(eq(events.customer, customer)) if (eventtype) conditions.push(eq(events.eventtype, eventtype)) - if (args.includeArchived !== true) conditions.push(eq(events.archived, false)) + conditions.push(eq(events.archived, false)) const rows = await context.server.db .select() @@ -298,7 +294,6 @@ export const organisationTools: McpTool[] = [ query: { type: "string", description: "Suchtext für Name, Beschreibung oder Kategorie." }, project: { type: "number" }, customer: { type: "number" }, - includeArchived: { type: "boolean", default: false }, limit: { type: "number", minimum: 1, maximum: 100 }, }, }, @@ -317,7 +312,7 @@ export const organisationTools: McpTool[] = [ } if (project) conditions.push(eq(tasks.project, project)) if (customer) conditions.push(eq(tasks.customer, customer)) - if (args.includeArchived !== true) conditions.push(eq(tasks.archived, false)) + conditions.push(eq(tasks.archived, false)) const rows = await context.server.db .select() @@ -348,7 +343,7 @@ export const organisationTools: McpTool[] = [ const rows = await context.server.db .select() .from(tasks) - .where(and(eq(tasks.id, id), eq(tasks.tenant, context.tenantId))) + .where(and(eq(tasks.id, id), eq(tasks.tenant, context.tenantId), eq(tasks.archived, false))) .limit(1) if (!rows[0]) throw new Error("Aufgabe nicht gefunden") diff --git a/backend/tests/mcpArchivedResults.test.ts b/backend/tests/mcpArchivedResults.test.ts new file mode 100644 index 0000000..93cf86a --- /dev/null +++ b/backend/tests/mcpArchivedResults.test.ts @@ -0,0 +1,37 @@ +import assert from "node:assert/strict" +import test from "node:test" +import { mcpTools } from "../src/mcp/registry" +import { asToolResult } from "../src/mcp/result" + +test("removes archived records from MCP text and structured output", () => { + const result = asToolResult({ + rows: [ + { id: 1, archived: false, name: "Aktiv" }, + { id: 2, archived: true, name: "Archiviert" }, + ], + nested: { + current: { id: 3, archived: false }, + previous: { id: 4, archived: true }, + }, + }) + + assert.deepEqual(result.structuredContent, { + rows: [{ id: 1, archived: false, name: "Aktiv" }], + nested: { current: { id: 3, archived: false } }, + }) + assert.deepEqual(JSON.parse(result.content[0].text), result.structuredContent) +}) + +test("does not expose a singular archived record", () => { + const result = asToolResult({ task: { id: 1, archived: true } }) + + assert.deepEqual(result.structuredContent, {}) + assert.deepEqual(JSON.parse(result.content[0].text), {}) +}) + +test("does not advertise includeArchived on MCP tools", () => { + for (const tool of mcpTools) { + const properties = (tool.inputSchema.properties || {}) as Record + assert.equal(properties.includeArchived, undefined, tool.name) + } +})