From a185c6eb11cf601272f5d2c3e7f1283ddb2ade02 Mon Sep 17 00:00:00 2001 From: florianfederspiel Date: Mon, 11 May 2026 13:07:11 +0200 Subject: [PATCH] =?UTF-8?q?FEDEO=20MCP-Tools=20f=C3=BCr=20Organisation=20e?= =?UTF-8?q?rweitern?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ergänzt MCP-Tools für Kunden-, Projekt-, Anlagen- und Terminabfragen sowie das Laden einzelner Bankumsätze. Aufgaben können nun zusätzlich gezielt archiviert werden. --- backend/src/mcp/tools/accounting.ts | 27 ++- backend/src/mcp/tools/organisation.ts | 228 +++++++++++++++++++++++++- 2 files changed, 252 insertions(+), 3 deletions(-) diff --git a/backend/src/mcp/tools/accounting.ts b/backend/src/mcp/tools/accounting.ts index f2c9e17..184a733 100644 --- a/backend/src/mcp/tools/accounting.ts +++ b/backend/src/mcp/tools/accounting.ts @@ -157,6 +157,32 @@ export const accountingTools: McpTool[] = [ return { rows } }, }, + { + name: "accounting.bank_statements.get", + title: "Bankumsatz laden", + description: "Lädt einen Bankumsatz des aktiven Mandanten anhand seiner ID.", + requiredPermissions: ["accounting.bank.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(bankstatements) + .where(and(eq(bankstatements.id, id), eq(bankstatements.tenant, context.tenantId))) + .limit(1) + + if (!rows[0]) throw new Error("Bankumsatz nicht gefunden") + return { bankStatement: rows[0] } + }, + }, { name: "accounting.statement_allocations.list", title: "Buchungszuordnungen auflisten", @@ -191,4 +217,3 @@ export const accountingTools: McpTool[] = [ }, }, ] - diff --git a/backend/src/mcp/tools/organisation.ts b/backend/src/mcp/tools/organisation.ts index 18906f6..79d00bf 100644 --- a/backend/src/mcp/tools/organisation.ts +++ b/backend/src/mcp/tools/organisation.ts @@ -1,5 +1,5 @@ import { and, desc, eq, ilike, or } from "drizzle-orm" -import { tasks } from "../../../db/schema" +import { customers, events, plants, projects, tasks } from "../../../db/schema" import { McpTool } from "../types" const limitFromArgs = (args: Record, fallback = 25) => { @@ -19,6 +19,201 @@ const numberArg = (args: Record, key: string) => { } export const organisationTools: McpTool[] = [ + { + name: "organisation.customers.search", + title: "Kunden suchen", + description: "Sucht aktive Kunden des aktiven Mandanten.", + requiredPermissions: ["organisation.customers.read"], + inputSchema: { + 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 }, + }, + }, + async handler(context, args) { + const conditions = [eq(customers.tenant, context.tenantId)] + const query = stringArg(args, "query") + + if (query) { + conditions.push(or( + ilike(customers.name, `%${query}%`), + ilike(customers.customerNumber, `%${query}%`), + ilike(customers.firstname, `%${query}%`), + ilike(customers.lastname, `%${query}%`), + ilike(customers.notes, `%${query}%`) + )) + } + if (args.includeArchived !== true) conditions.push(eq(customers.archived, false)) + + const rows = await context.server.db + .select({ + id: customers.id, + customerNumber: customers.customerNumber, + name: customers.name, + firstname: customers.firstname, + lastname: customers.lastname, + type: customers.type, + isCompany: customers.isCompany, + active: customers.active, + archived: customers.archived, + }) + .from(customers) + .where(and(...conditions)) + .orderBy(customers.name) + .limit(limitFromArgs(args)) + + return { rows } + }, + }, + { + name: "organisation.projects.list", + title: "Projekte auflisten", + description: "Listet Projekte des aktiven Mandanten mit optionalen Filtern.", + requiredPermissions: ["organisation.projects.read"], + inputSchema: { + type: "object", + properties: { + 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 }, + }, + }, + async handler(context, args) { + const conditions = [eq(projects.tenant, context.tenantId)] + const query = stringArg(args, "query") + const customer = numberArg(args, "customer") + const activePhase = stringArg(args, "activePhase") + + if (query) { + conditions.push(or( + ilike(projects.name, `%${query}%`), + ilike(projects.projectNumber, `%${query}%`), + ilike(projects.customerRef, `%${query}%`), + ilike(projects.notes, `%${query}%`) + )) + } + 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)) + + const rows = await context.server.db + .select() + .from(projects) + .where(and(...conditions)) + .orderBy(desc(projects.createdAt)) + .limit(limitFromArgs(args)) + + return { rows } + }, + }, + { + name: "organisation.projects.get", + title: "Projekt laden", + description: "Lädt ein Projekt des aktiven Mandanten anhand seiner ID.", + requiredPermissions: ["organisation.projects.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(projects) + .where(and(eq(projects.id, id), eq(projects.tenant, context.tenantId))) + .limit(1) + + if (!rows[0]) throw new Error("Projekt nicht gefunden") + return { project: rows[0] } + }, + }, + { + name: "organisation.plants.list", + title: "Anlagen auflisten", + description: "Listet Anlagen des aktiven Mandanten mit optionalem Kundenfilter.", + requiredPermissions: ["organisation.plants.read"], + inputSchema: { + type: "object", + properties: { + query: { type: "string", description: "Suchtext für Name." }, + customer: { type: "number" }, + includeArchived: { type: "boolean", default: false }, + limit: { type: "number", minimum: 1, maximum: 100 }, + }, + }, + async handler(context, args) { + const conditions = [eq(plants.tenant, context.tenantId)] + const query = stringArg(args, "query") + const customer = numberArg(args, "customer") + + 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)) + + const rows = await context.server.db + .select() + .from(plants) + .where(and(...conditions)) + .orderBy(plants.name) + .limit(limitFromArgs(args)) + + return { rows } + }, + }, + { + name: "organisation.events.list", + title: "Termine auflisten", + description: "Listet Termine des aktiven Mandanten mit optionalen Projekt- oder Kundenfiltern.", + requiredPermissions: ["organisation.events.read"], + inputSchema: { + type: "object", + properties: { + query: { type: "string", description: "Suchtext für Name, Notizen oder Link." }, + project: { type: "number" }, + customer: { type: "number" }, + eventtype: { type: "string" }, + includeArchived: { type: "boolean", default: false }, + limit: { type: "number", minimum: 1, maximum: 100 }, + }, + }, + async handler(context, args) { + const conditions = [eq(events.tenant, context.tenantId)] + const query = stringArg(args, "query") + const project = numberArg(args, "project") + const customer = numberArg(args, "customer") + const eventtype = stringArg(args, "eventtype") + + if (query) { + conditions.push(or( + ilike(events.name, `%${query}%`), + ilike(events.notes, `%${query}%`), + ilike(events.link, `%${query}%`) + )) + } + 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)) + + const rows = await context.server.db + .select() + .from(events) + .where(and(...conditions)) + .orderBy(desc(events.startDate)) + .limit(limitFromArgs(args)) + + return { rows } + }, + }, { name: "organisation.tasks.list", title: "Aufgaben auflisten", @@ -179,5 +374,34 @@ export const organisationTools: McpTool[] = [ return { task: updated } }, }, -] + { + name: "organisation.tasks.archive", + title: "Aufgabe archivieren", + description: "Archiviert eine Aufgabe im aktiven Mandanten.", + requiredPermissions: ["organisation.tasks.write"], + 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 [updated] = await context.server.db + .update(tasks) + .set({ + archived: true, + updatedAt: new Date(), + updatedBy: context.userId, + }) + .where(and(eq(tasks.id, id), eq(tasks.tenant, context.tenantId))) + .returning() + + if (!updated) throw new Error("Aufgabe nicht gefunden") + return { task: updated } + }, + }, +]