MCP-Server für Buchhaltung und Organisation ergänzen
Fügt einen geschützten MCP-JSON-RPC-Endpunkt mit Buchhaltungs-Tools und Aufgaben-Tools hinzu. Berechtigungen werden rollenbasiert pro Mandant geprüft und die Auth-Logik berücksichtigt nun alle Rollen eines Nutzers.
This commit is contained in:
194
backend/src/mcp/tools/accounting.ts
Normal file
194
backend/src/mcp/tools/accounting.ts
Normal file
@@ -0,0 +1,194 @@
|
||||
import { and, desc, eq, ilike, or } from "drizzle-orm"
|
||||
import {
|
||||
accounts,
|
||||
bankstatements,
|
||||
incominginvoices,
|
||||
statementallocations,
|
||||
} from "../../../db/schema"
|
||||
import { McpTool } from "../types"
|
||||
|
||||
const limitFromArgs = (args: Record<string, unknown>, 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<string, unknown>, key: string) => {
|
||||
const value = args[key]
|
||||
return typeof value === "string" && value.trim() ? value.trim() : null
|
||||
}
|
||||
|
||||
const numberArg = (args: Record<string, unknown>, key: string) => {
|
||||
const value = Number(args[key])
|
||||
return Number.isFinite(value) ? value : null
|
||||
}
|
||||
|
||||
export const accountingTools: McpTool[] = [
|
||||
{
|
||||
name: "accounting.accounts.search",
|
||||
title: "Konten suchen",
|
||||
description: "Sucht Sachkonten im aktiven Kontenrahmen des Mandanten.",
|
||||
requiredPermissions: ["accounting.accounts.read"],
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
query: { type: "string", description: "Suchtext für Kontonummer, Bezeichnung oder Beschreibung." },
|
||||
limit: { type: "number", minimum: 1, maximum: 100 },
|
||||
},
|
||||
},
|
||||
async handler(context, args) {
|
||||
const tenantRows = await context.server.db.query.tenants.findMany({
|
||||
where: (tenant, { eq }) => eq(tenant.id, context.tenantId),
|
||||
columns: {
|
||||
accountChart: true,
|
||||
},
|
||||
limit: 1,
|
||||
})
|
||||
const accountChart = tenantRows[0]?.accountChart || "skr03"
|
||||
const query = stringArg(args, "query")
|
||||
const limit = limitFromArgs(args)
|
||||
|
||||
const whereCond = query
|
||||
? and(
|
||||
eq(accounts.accountChart, accountChart),
|
||||
or(
|
||||
ilike(accounts.number, `%${query}%`),
|
||||
ilike(accounts.label, `%${query}%`),
|
||||
ilike(accounts.description, `%${query}%`)
|
||||
)
|
||||
)
|
||||
: eq(accounts.accountChart, accountChart)
|
||||
|
||||
const rows = await context.server.db
|
||||
.select()
|
||||
.from(accounts)
|
||||
.where(whereCond)
|
||||
.orderBy(accounts.number)
|
||||
.limit(limit)
|
||||
|
||||
return { accountChart, rows }
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "accounting.incoming_invoices.list",
|
||||
title: "Eingangsrechnungen auflisten",
|
||||
description: "Listet Eingangsrechnungen des aktiven Mandanten.",
|
||||
requiredPermissions: ["accounting.incoming_invoices.read"],
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
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 },
|
||||
},
|
||||
},
|
||||
async handler(context, args) {
|
||||
const conditions = [eq(incominginvoices.tenant, context.tenantId)]
|
||||
const state = stringArg(args, "state")
|
||||
|
||||
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))
|
||||
|
||||
const rows = await context.server.db
|
||||
.select()
|
||||
.from(incominginvoices)
|
||||
.where(and(...conditions))
|
||||
.orderBy(desc(incominginvoices.createdAt))
|
||||
.limit(limitFromArgs(args))
|
||||
|
||||
return { rows }
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "accounting.incoming_invoices.get",
|
||||
title: "Eingangsrechnung laden",
|
||||
description: "Lädt eine Eingangsrechnung des aktiven Mandanten anhand ihrer ID.",
|
||||
requiredPermissions: ["accounting.incoming_invoices.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(incominginvoices)
|
||||
.where(and(eq(incominginvoices.id, id), eq(incominginvoices.tenant, context.tenantId)))
|
||||
.limit(1)
|
||||
|
||||
if (!rows[0]) throw new Error("Eingangsrechnung nicht gefunden")
|
||||
return { invoice: rows[0] }
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "accounting.bank_statements.list",
|
||||
title: "Bankumsätze auflisten",
|
||||
description: "Listet Bankumsätze des aktiven Mandanten.",
|
||||
requiredPermissions: ["accounting.bank.read"],
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
account: { type: "number", description: "Optionale Bankkonto-ID." },
|
||||
includeArchived: { type: "boolean", default: false },
|
||||
limit: { type: "number", minimum: 1, maximum: 100 },
|
||||
},
|
||||
},
|
||||
async handler(context, args) {
|
||||
const conditions = [eq(bankstatements.tenant, context.tenantId)]
|
||||
const account = numberArg(args, "account")
|
||||
|
||||
if (account) conditions.push(eq(bankstatements.account, account))
|
||||
if (args.includeArchived !== true) conditions.push(eq(bankstatements.archived, false))
|
||||
|
||||
const rows = await context.server.db
|
||||
.select()
|
||||
.from(bankstatements)
|
||||
.where(and(...conditions))
|
||||
.orderBy(desc(bankstatements.date), desc(bankstatements.id))
|
||||
.limit(limitFromArgs(args))
|
||||
|
||||
return { rows }
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "accounting.statement_allocations.list",
|
||||
title: "Buchungszuordnungen auflisten",
|
||||
description: "Listet Buchungszuordnungen des aktiven Mandanten.",
|
||||
requiredPermissions: ["accounting.statement_allocations.read"],
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
bankstatement: { type: "number" },
|
||||
incominginvoice: { type: "number" },
|
||||
includeArchived: { type: "boolean", default: false },
|
||||
limit: { type: "number", minimum: 1, maximum: 100 },
|
||||
},
|
||||
},
|
||||
async handler(context, args) {
|
||||
const conditions = [eq(statementallocations.tenant, context.tenantId)]
|
||||
const bankstatement = numberArg(args, "bankstatement")
|
||||
const incominginvoice = numberArg(args, "incominginvoice")
|
||||
|
||||
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))
|
||||
|
||||
const rows = await context.server.db
|
||||
.select()
|
||||
.from(statementallocations)
|
||||
.where(and(...conditions))
|
||||
.orderBy(desc(statementallocations.created_at))
|
||||
.limit(limitFromArgs(args))
|
||||
|
||||
return { rows }
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user