import { and, desc, eq, ilike, or } from "drizzle-orm" import { branches, contacts, costcentres, customers, inventoryitems, products, services, teams, units, vehicles, vendors, } from "../../../db/schema" import { McpTool } from "../types" const limitFromArgs = (args: Record, fallback = 25) => { const raw = Number(args.limit ?? fallback) if (!Number.isFinite(raw)) return fallback return Math.min(Math.max(Math.trunc(raw), 1), 100) } const stringArg = (args: Record, key: string) => { const value = args[key] return typeof value === "string" && value.trim() ? value.trim() : null } const numberArg = (args: Record, key: string) => { const value = Number(args[key]) return Number.isFinite(value) ? value : null } const uuidArg = (args: Record, key: string) => { const value = stringArg(args, key) return value && /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(value) ? value : null } export const masterdataTools: McpTool[] = [ { name: "masterdata.customers.get", title: "Kunde laden", description: "Lädt einen Kunden des aktiven Mandanten anhand seiner ID.", requiredPermissions: ["masterdata.customers.read"], inputSchema: { type: "object", required: ["id"], properties: { id: { type: "number" } }, }, async handler(context, args) { const id = numberArg(args, "id") if (!id) throw new Error("id ist erforderlich") const rows = await context.server.db .select() .from(customers) .where(and(eq(customers.id, id), eq(customers.tenant, context.tenantId))) .limit(1) if (!rows[0]) throw new Error("Kunde nicht gefunden") return { customer: rows[0] } }, }, { name: "masterdata.vendors.search", title: "Lieferanten suchen", description: "Sucht Lieferanten des aktiven Mandanten.", requiredPermissions: ["masterdata.vendors.read"], inputSchema: { 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 }, }, }, async handler(context, args) { const conditions = [eq(vendors.tenant, context.tenantId)] const query = stringArg(args, "query") if (query) { conditions.push(or( ilike(vendors.name, `%${query}%`), ilike(vendors.vendorNumber, `%${query}%`), ilike(vendors.notes, `%${query}%`) )) } if (args.includeArchived !== true) conditions.push(eq(vendors.archived, false)) const rows = await context.server.db .select() .from(vendors) .where(and(...conditions)) .orderBy(vendors.name) .limit(limitFromArgs(args)) return { rows } }, }, { name: "masterdata.vendors.get", title: "Lieferant laden", description: "Lädt einen Lieferanten des aktiven Mandanten anhand seiner ID.", requiredPermissions: ["masterdata.vendors.read"], inputSchema: { type: "object", required: ["id"], properties: { id: { type: "number" } }, }, async handler(context, args) { const id = numberArg(args, "id") if (!id) throw new Error("id ist erforderlich") const rows = await context.server.db .select() .from(vendors) .where(and(eq(vendors.id, id), eq(vendors.tenant, context.tenantId))) .limit(1) if (!rows[0]) throw new Error("Lieferant nicht gefunden") return { vendor: rows[0] } }, }, { name: "masterdata.contacts.search", title: "Kontakte suchen", description: "Sucht Kontakte des aktiven Mandanten, optional zu Kunde oder Lieferant.", requiredPermissions: ["masterdata.contacts.read"], inputSchema: { type: "object", properties: { 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 }, }, }, async handler(context, args) { const conditions = [eq(contacts.tenant, context.tenantId)] const query = stringArg(args, "query") const customer = numberArg(args, "customer") const vendor = numberArg(args, "vendor") if (query) { conditions.push(or( ilike(contacts.fullName, `%${query}%`), ilike(contacts.firstName, `%${query}%`), ilike(contacts.lastName, `%${query}%`), ilike(contacts.email, `%${query}%`), ilike(contacts.phoneMobile, `%${query}%`), ilike(contacts.phoneHome, `%${query}%`), ilike(contacts.role, `%${query}%`), ilike(contacts.notes, `%${query}%`) )) } 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)) const rows = await context.server.db .select() .from(contacts) .where(and(...conditions)) .orderBy(contacts.fullName) .limit(limitFromArgs(args)) return { rows } }, }, { name: "masterdata.products.search", title: "Artikel suchen", description: "Sucht Artikel und Materialstammdaten des aktiven Mandanten.", requiredPermissions: ["masterdata.products.read"], inputSchema: { 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 }, }, }, async handler(context, args) { const conditions = [eq(products.tenant, context.tenantId)] const query = stringArg(args, "query") if (query) { conditions.push(or( ilike(products.name, `%${query}%`), ilike(products.article_number, `%${query}%`), ilike(products.manufacturer, `%${query}%`), ilike(products.manufacturer_number, `%${query}%`), ilike(products.ean, `%${query}%`), ilike(products.barcode, `%${query}%`), ilike(products.description, `%${query}%`) )) } if (args.includeArchived !== true) conditions.push(eq(products.archived, false)) const rows = await context.server.db .select() .from(products) .where(and(...conditions)) .orderBy(products.name) .limit(limitFromArgs(args)) return { rows } }, }, { name: "masterdata.products.get", title: "Artikel laden", description: "Lädt einen Artikel des aktiven Mandanten anhand seiner ID.", requiredPermissions: ["masterdata.products.read"], inputSchema: { type: "object", required: ["id"], properties: { id: { type: "number" } }, }, async handler(context, args) { const id = numberArg(args, "id") if (!id) throw new Error("id ist erforderlich") const rows = await context.server.db .select() .from(products) .where(and(eq(products.id, id), eq(products.tenant, context.tenantId))) .limit(1) if (!rows[0]) throw new Error("Artikel nicht gefunden") return { product: rows[0] } }, }, { name: "masterdata.services.search", title: "Leistungen suchen", description: "Sucht Leistungsstammdaten des aktiven Mandanten.", requiredPermissions: ["masterdata.services.read"], inputSchema: { 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 }, }, }, async handler(context, args) { const conditions = [eq(services.tenant, context.tenantId)] const query = stringArg(args, "query") if (query) { conditions.push(or( ilike(services.name, `%${query}%`), ilike(services.description, `%${query}%`) )) } if (args.includeArchived !== true) conditions.push(eq(services.archived, false)) const rows = await context.server.db .select() .from(services) .where(and(...conditions)) .orderBy(services.name) .limit(limitFromArgs(args)) return { rows } }, }, { name: "masterdata.services.get", title: "Leistung laden", description: "Lädt eine Leistung des aktiven Mandanten anhand ihrer ID.", requiredPermissions: ["masterdata.services.read"], inputSchema: { type: "object", required: ["id"], properties: { id: { type: "number" } }, }, async handler(context, args) { const id = numberArg(args, "id") if (!id) throw new Error("id ist erforderlich") const rows = await context.server.db .select() .from(services) .where(and(eq(services.id, id), eq(services.tenant, context.tenantId))) .limit(1) if (!rows[0]) throw new Error("Leistung nicht gefunden") return { service: rows[0] } }, }, { name: "masterdata.cost_centres.list", title: "Kostenstellen auflisten", description: "Listet Kostenstellen des aktiven Mandanten.", requiredPermissions: ["masterdata.cost_centres.read"], inputSchema: { type: "object", properties: { 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 }, }, }, async handler(context, args) { const conditions = [eq(costcentres.tenant, context.tenantId)] const query = stringArg(args, "query") const branch = numberArg(args, "branch") const project = numberArg(args, "project") if (query) { conditions.push(or( ilike(costcentres.number, `%${query}%`), ilike(costcentres.name, `%${query}%`), ilike(costcentres.description, `%${query}%`) )) } 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)) const rows = await context.server.db .select() .from(costcentres) .where(and(...conditions)) .orderBy(costcentres.number) .limit(limitFromArgs(args)) return { rows } }, }, { name: "masterdata.cost_centres.get", title: "Kostenstelle laden", description: "Lädt eine Kostenstelle des aktiven Mandanten anhand ihrer UUID.", requiredPermissions: ["masterdata.cost_centres.read"], inputSchema: { type: "object", required: ["id"], properties: { id: { type: "string" } }, }, async handler(context, args) { const id = uuidArg(args, "id") if (!id) throw new Error("gültige id ist erforderlich") const rows = await context.server.db .select() .from(costcentres) .where(and(eq(costcentres.id, id), eq(costcentres.tenant, context.tenantId))) .limit(1) if (!rows[0]) throw new Error("Kostenstelle nicht gefunden") return { costCentre: rows[0] } }, }, { name: "masterdata.branches.list", title: "Niederlassungen auflisten", description: "Listet Niederlassungen des aktiven Mandanten.", requiredPermissions: ["masterdata.branches.read"], inputSchema: { 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 }, }, }, async handler(context, args) { const conditions = [eq(branches.tenant, context.tenantId)] const query = stringArg(args, "query") if (query) { conditions.push(or( ilike(branches.number, `%${query}%`), ilike(branches.name, `%${query}%`), ilike(branches.description, `%${query}%`) )) } if (args.includeArchived !== true) conditions.push(eq(branches.archived, false)) const rows = await context.server.db .select() .from(branches) .where(and(...conditions)) .orderBy(branches.name) .limit(limitFromArgs(args)) return { rows } }, }, { name: "masterdata.teams.list", title: "Teams auflisten", description: "Listet Teams des aktiven Mandanten.", requiredPermissions: ["masterdata.teams.read"], inputSchema: { type: "object", 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 }, }, }, async handler(context, args) { const conditions = [eq(teams.tenant, context.tenantId)] const query = stringArg(args, "query") const branch = numberArg(args, "branch") if (query) { conditions.push(or( ilike(teams.name, `%${query}%`), ilike(teams.description, `%${query}%`) )) } if (branch) conditions.push(eq(teams.branch, branch)) if (args.includeArchived !== true) conditions.push(eq(teams.archived, false)) const rows = await context.server.db .select() .from(teams) .where(and(...conditions)) .orderBy(teams.name) .limit(limitFromArgs(args)) return { rows } }, }, { name: "masterdata.vehicles.list", title: "Fahrzeuge auflisten", description: "Listet Fahrzeuge des aktiven Mandanten.", requiredPermissions: ["masterdata.vehicles.read"], inputSchema: { 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 }, }, }, async handler(context, args) { const conditions = [eq(vehicles.tenant, context.tenantId)] const query = stringArg(args, "query") if (query) { conditions.push(or( ilike(vehicles.name, `%${query}%`), ilike(vehicles.license_plate, `%${query}%`), ilike(vehicles.vin, `%${query}%`), ilike(vehicles.color, `%${query}%`) )) } if (args.includeArchived !== true) conditions.push(eq(vehicles.archived, false)) const rows = await context.server.db .select() .from(vehicles) .where(and(...conditions)) .orderBy(vehicles.name) .limit(limitFromArgs(args)) return { rows } }, }, { name: "masterdata.inventory_items.search", title: "Inventar suchen", description: "Sucht Inventar- und Geräte-Stammdaten des aktiven Mandanten.", requiredPermissions: ["masterdata.inventory_items.read"], inputSchema: { type: "object", 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 }, }, }, async handler(context, args) { const conditions = [eq(inventoryitems.tenant, context.tenantId)] const query = stringArg(args, "query") const vendor = numberArg(args, "vendor") if (query) { conditions.push(or( ilike(inventoryitems.name, `%${query}%`), ilike(inventoryitems.articleNumber, `%${query}%`), ilike(inventoryitems.serialNumber, `%${query}%`), ilike(inventoryitems.manufacturer, `%${query}%`), ilike(inventoryitems.manufacturerNumber, `%${query}%`), ilike(inventoryitems.description, `%${query}%`) )) } if (vendor) conditions.push(eq(inventoryitems.vendor, vendor)) if (args.includeArchived !== true) conditions.push(eq(inventoryitems.archived, false)) const rows = await context.server.db .select() .from(inventoryitems) .where(and(...conditions)) .orderBy(inventoryitems.name) .limit(limitFromArgs(args)) return { rows } }, }, { name: "masterdata.inventory_items.get", title: "Inventar laden", description: "Lädt einen Inventar- oder Geräte-Stammdatensatz anhand seiner ID.", requiredPermissions: ["masterdata.inventory_items.read"], inputSchema: { type: "object", required: ["id"], properties: { id: { type: "number" } }, }, async handler(context, args) { const id = numberArg(args, "id") if (!id) throw new Error("id ist erforderlich") const rows = await context.server.db .select() .from(inventoryitems) .where(and(eq(inventoryitems.id, id), eq(inventoryitems.tenant, context.tenantId))) .limit(1) if (!rows[0]) throw new Error("Inventar nicht gefunden") return { inventoryItem: rows[0] } }, }, { name: "masterdata.units.list", title: "Einheiten auflisten", description: "Listet globale Mengeneinheiten.", requiredPermissions: ["masterdata.units.read"], inputSchema: { type: "object", properties: { query: { type: "string", description: "Suchtext für Name, Singular, Plural oder Kürzel." }, limit: { type: "number", minimum: 1, maximum: 100 }, }, }, async handler(context, args) { const query = stringArg(args, "query") const rows = await context.server.db .select() .from(units) .where(query ? or( ilike(units.name, `%${query}%`), ilike(units.single, `%${query}%`), ilike(units.multiple, `%${query}%`), ilike(units.short, `%${query}%`) ) : undefined) .orderBy(units.name) .limit(limitFromArgs(args)) return { rows } }, }, ]