feat(mcp): paginate record lists and describe document inputs

This commit is contained in:
2026-09-14 13:17:37 +02:00
parent 58fc252110
commit 342ad9764e
8 changed files with 302 additions and 123 deletions

View File

@@ -1,3 +1,5 @@
import { outgoingAddressSchema, outgoingRowsSchema, outgoingReportSchema } from "../documentSchemas"
import { limitFromArgs, offsetFromArgs, paginatedResult, paginationProperties } from "../pagination"
import { and, desc, eq, ilike, or } from "drizzle-orm"
import { randomUUID } from "node:crypto"
import {
@@ -27,11 +29,6 @@ import { McpTool } from "../types"
export { prepareStatementAllocationInput } from "../statementAllocations"
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]
@@ -456,7 +453,7 @@ export const accountingTools: McpTool[] = [
state: { type: "string", description: "Optionaler Statusfilter, z. B. Entwurf oder Gebucht." },
customer: { type: "number" },
project: { type: "number" },
limit: { type: "number", minimum: 1, maximum: 100 },
...paginationProperties,
},
},
async handler(context, args) {
@@ -479,10 +476,11 @@ export const accountingTools: McpTool[] = [
.select()
.from(createddocuments)
.where(and(...conditions))
.orderBy(desc(createddocuments.createdAt))
.limit(limitFromArgs(args))
.orderBy(desc(createddocuments.createdAt), createddocuments.id)
.limit(limitFromArgs(args) + 1)
.offset(offsetFromArgs(args))
return { rows }
return paginatedResult(rows, args)
},
},
{
@@ -542,12 +540,12 @@ export const accountingTools: McpTool[] = [
description: { type: "string" },
startText: { type: "string" },
endText: { type: "string" },
address: { type: "object" },
rows: { type: "array" },
address: outgoingAddressSchema,
rows: outgoingRowsSchema,
letterhead: { type: "number" },
availableInPortal: { type: "boolean" },
customSurchargePercentage: { type: "number" },
report: { type: "object" },
report: outgoingReportSchema,
serialConfig: serialConfigSchema,
},
},
@@ -598,12 +596,12 @@ export const accountingTools: McpTool[] = [
description: { type: "string" },
startText: { type: "string" },
endText: { type: "string" },
address: { type: "object" },
rows: { type: "array" },
address: outgoingAddressSchema,
rows: outgoingRowsSchema,
letterhead: { type: "number" },
availableInPortal: { type: "boolean" },
customSurchargePercentage: { type: "number" },
report: { type: "object" },
report: outgoingReportSchema,
serialConfig: serialConfigSchema,
},
},
@@ -720,8 +718,8 @@ export const accountingTools: McpTool[] = [
enum: ["Standard", "13b UStG", "19 UStG", "12.3 UStG"],
description: "Steuertyp des gesamten Ausgangsdokuments, nicht der einzelne USt-Satz einer Position.",
},
rows: { type: "array" },
report: { type: "object" },
rows: outgoingRowsSchema,
report: outgoingReportSchema,
availableInPortal: { type: "boolean" },
},
},
@@ -803,7 +801,7 @@ export const accountingTools: McpTool[] = [
type: "object",
properties: {
status: { type: "string", enum: ["draft", "completed", "error"] },
limit: { type: "number", minimum: 1, maximum: 100 },
...paginationProperties,
},
},
async handler(context, args) {
@@ -815,10 +813,11 @@ export const accountingTools: McpTool[] = [
.select()
.from(serialExecutions)
.where(and(...conditions))
.orderBy(desc(serialExecutions.createdAt))
.limit(limitFromArgs(args))
.orderBy(desc(serialExecutions.createdAt), serialExecutions.id)
.limit(limitFromArgs(args) + 1)
.offset(offsetFromArgs(args))
return { rows }
return paginatedResult(rows, args)
},
},
{
@@ -878,7 +877,7 @@ export const accountingTools: McpTool[] = [
type: "object",
properties: {
query: { type: "string", description: "Suchtext für Kontonummer, Bezeichnung oder Beschreibung." },
limit: { type: "number", minimum: 1, maximum: 100 },
...paginationProperties,
},
},
async handler(context, args) {
@@ -908,10 +907,11 @@ export const accountingTools: McpTool[] = [
.select()
.from(accounts)
.where(whereCond)
.orderBy(accounts.number)
.limit(limit)
.orderBy(accounts.number, accounts.id)
.limit(limit + 1)
.offset(offsetFromArgs(args))
return { accountChart, rows }
return { accountChart, ...paginatedResult(rows, args) }
},
},
{
@@ -924,7 +924,7 @@ export const accountingTools: McpTool[] = [
properties: {
state: { type: "string", description: "Optionaler Statusfilter." },
paid: { type: "boolean", description: "Optionaler Zahlungsstatus." },
limit: { type: "number", minimum: 1, maximum: 100 },
...paginationProperties,
},
},
async handler(context, args) {
@@ -939,10 +939,11 @@ export const accountingTools: McpTool[] = [
.select()
.from(incominginvoices)
.where(and(...conditions))
.orderBy(desc(incominginvoices.createdAt))
.limit(limitFromArgs(args))
.orderBy(desc(incominginvoices.createdAt), incominginvoices.id)
.limit(limitFromArgs(args) + 1)
.offset(offsetFromArgs(args))
return { rows }
return paginatedResult(rows, args)
},
},
{
@@ -981,6 +982,7 @@ export const accountingTools: McpTool[] = [
required: ["id"],
properties: {
id: { type: "number" },
...paginationProperties,
},
},
async handler(context, args) {
@@ -995,9 +997,11 @@ export const accountingTools: McpTool[] = [
eq(files.tenant, context.tenantId),
eq(files.archived, false)
))
.orderBy(desc(files.createdAt))
.orderBy(desc(files.createdAt), files.id)
.limit(limitFromArgs(args) + 1)
.offset(offsetFromArgs(args))
return { rows }
return paginatedResult(rows, args)
},
},
{
@@ -1311,7 +1315,7 @@ export const accountingTools: McpTool[] = [
type: "object",
properties: {
account: { type: "number", description: "Optionale Bankkonto-ID." },
limit: { type: "number", minimum: 1, maximum: 100 },
...paginationProperties,
},
},
async handler(context, args) {
@@ -1326,9 +1330,10 @@ export const accountingTools: McpTool[] = [
.from(bankstatements)
.where(and(...conditions))
.orderBy(desc(bankstatements.date), desc(bankstatements.id))
.limit(limitFromArgs(args))
.limit(limitFromArgs(args) + 1)
.offset(offsetFromArgs(args))
return { rows }
return paginatedResult(rows, args)
},
},
{
@@ -1367,7 +1372,7 @@ export const accountingTools: McpTool[] = [
properties: {
bankstatement: { type: "number" },
incominginvoice: { type: "number" },
limit: { type: "number", minimum: 1, maximum: 100 },
...paginationProperties,
},
},
async handler(context, args) {
@@ -1383,10 +1388,11 @@ export const accountingTools: McpTool[] = [
.select()
.from(statementallocations)
.where(and(...conditions))
.orderBy(desc(statementallocations.created_at))
.limit(limitFromArgs(args))
.orderBy(desc(statementallocations.created_at), statementallocations.id)
.limit(limitFromArgs(args) + 1)
.offset(offsetFromArgs(args))
return { rows }
return paginatedResult(rows, args)
},
},
{

View File

@@ -1,3 +1,4 @@
import { limitFromArgs, offsetFromArgs, paginatedResult, paginationProperties } from "../pagination"
import { and, desc, eq, ilike, or } from "drizzle-orm"
import {
branches,
@@ -14,11 +15,6 @@ import {
} 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]
@@ -71,7 +67,7 @@ export const masterdataTools: McpTool[] = [
type: "object",
properties: {
query: { type: "string", description: "Suchtext für Name, Lieferantennummer oder Notizen." },
limit: { type: "number", minimum: 1, maximum: 100 },
...paginationProperties,
},
},
async handler(context, args) {
@@ -91,10 +87,11 @@ export const masterdataTools: McpTool[] = [
.select()
.from(vendors)
.where(and(...conditions))
.orderBy(vendors.name)
.limit(limitFromArgs(args))
.orderBy(vendors.name, vendors.id)
.limit(limitFromArgs(args) + 1)
.offset(offsetFromArgs(args))
return { rows }
return paginatedResult(rows, args)
},
},
{
@@ -132,7 +129,7 @@ export const masterdataTools: McpTool[] = [
query: { type: "string", description: "Suchtext für Name, E-Mail, Telefon, Rolle oder Notizen." },
customer: { type: "number" },
vendor: { type: "number" },
limit: { type: "number", minimum: 1, maximum: 100 },
...paginationProperties,
},
},
async handler(context, args) {
@@ -161,10 +158,11 @@ export const masterdataTools: McpTool[] = [
.select()
.from(contacts)
.where(and(...conditions))
.orderBy(contacts.fullName)
.limit(limitFromArgs(args))
.orderBy(contacts.fullName, contacts.id)
.limit(limitFromArgs(args) + 1)
.offset(offsetFromArgs(args))
return { rows }
return paginatedResult(rows, args)
},
},
{
@@ -176,7 +174,7 @@ export const masterdataTools: McpTool[] = [
type: "object",
properties: {
query: { type: "string", description: "Suchtext für Name, Artikelnummer, Hersteller, EAN, Barcode oder Beschreibung." },
limit: { type: "number", minimum: 1, maximum: 100 },
...paginationProperties,
},
},
async handler(context, args) {
@@ -200,10 +198,11 @@ export const masterdataTools: McpTool[] = [
.select()
.from(products)
.where(and(...conditions))
.orderBy(products.name)
.limit(limitFromArgs(args))
.orderBy(products.name, products.id)
.limit(limitFromArgs(args) + 1)
.offset(offsetFromArgs(args))
return { rows }
return paginatedResult(rows, args)
},
},
{
@@ -239,7 +238,7 @@ export const masterdataTools: McpTool[] = [
type: "object",
properties: {
query: { type: "string", description: "Suchtext für Name, Leistungsnummer oder Beschreibung." },
limit: { type: "number", minimum: 1, maximum: 100 },
...paginationProperties,
},
},
async handler(context, args) {
@@ -258,10 +257,11 @@ export const masterdataTools: McpTool[] = [
.select()
.from(services)
.where(and(...conditions))
.orderBy(services.name)
.limit(limitFromArgs(args))
.orderBy(services.name, services.id)
.limit(limitFromArgs(args) + 1)
.offset(offsetFromArgs(args))
return { rows }
return paginatedResult(rows, args)
},
},
{
@@ -299,7 +299,7 @@ export const masterdataTools: McpTool[] = [
query: { type: "string", description: "Suchtext für Nummer, Name oder Beschreibung." },
branch: { type: "number" },
project: { type: "number" },
limit: { type: "number", minimum: 1, maximum: 100 },
...paginationProperties,
},
},
async handler(context, args) {
@@ -323,10 +323,11 @@ export const masterdataTools: McpTool[] = [
.select()
.from(costcentres)
.where(and(...conditions))
.orderBy(costcentres.number)
.limit(limitFromArgs(args))
.orderBy(costcentres.number, costcentres.id)
.limit(limitFromArgs(args) + 1)
.offset(offsetFromArgs(args))
return { rows }
return paginatedResult(rows, args)
},
},
{
@@ -362,7 +363,7 @@ export const masterdataTools: McpTool[] = [
type: "object",
properties: {
query: { type: "string", description: "Suchtext für Nummer, Name oder Beschreibung." },
limit: { type: "number", minimum: 1, maximum: 100 },
...paginationProperties,
},
},
async handler(context, args) {
@@ -382,10 +383,11 @@ export const masterdataTools: McpTool[] = [
.select()
.from(branches)
.where(and(...conditions))
.orderBy(branches.name)
.limit(limitFromArgs(args))
.orderBy(branches.name, branches.id)
.limit(limitFromArgs(args) + 1)
.offset(offsetFromArgs(args))
return { rows }
return paginatedResult(rows, args)
},
},
{
@@ -398,7 +400,7 @@ export const masterdataTools: McpTool[] = [
properties: {
query: { type: "string", description: "Suchtext für Name oder Beschreibung." },
branch: { type: "number" },
limit: { type: "number", minimum: 1, maximum: 100 },
...paginationProperties,
},
},
async handler(context, args) {
@@ -419,10 +421,11 @@ export const masterdataTools: McpTool[] = [
.select()
.from(teams)
.where(and(...conditions))
.orderBy(teams.name)
.limit(limitFromArgs(args))
.orderBy(teams.name, teams.id)
.limit(limitFromArgs(args) + 1)
.offset(offsetFromArgs(args))
return { rows }
return paginatedResult(rows, args)
},
},
{
@@ -434,7 +437,7 @@ export const masterdataTools: McpTool[] = [
type: "object",
properties: {
query: { type: "string", description: "Suchtext für Name, Kennzeichen, FIN oder Farbe." },
limit: { type: "number", minimum: 1, maximum: 100 },
...paginationProperties,
},
},
async handler(context, args) {
@@ -455,10 +458,11 @@ export const masterdataTools: McpTool[] = [
.select()
.from(vehicles)
.where(and(...conditions))
.orderBy(vehicles.name)
.limit(limitFromArgs(args))
.orderBy(vehicles.name, vehicles.id)
.limit(limitFromArgs(args) + 1)
.offset(offsetFromArgs(args))
return { rows }
return paginatedResult(rows, args)
},
},
{
@@ -471,7 +475,7 @@ export const masterdataTools: McpTool[] = [
properties: {
query: { type: "string", description: "Suchtext für Name, Artikelnummer, Seriennummer, Hersteller oder Beschreibung." },
vendor: { type: "number" },
limit: { type: "number", minimum: 1, maximum: 100 },
...paginationProperties,
},
},
async handler(context, args) {
@@ -496,10 +500,11 @@ export const masterdataTools: McpTool[] = [
.select()
.from(inventoryitems)
.where(and(...conditions))
.orderBy(inventoryitems.name)
.limit(limitFromArgs(args))
.orderBy(inventoryitems.name, inventoryitems.id)
.limit(limitFromArgs(args) + 1)
.offset(offsetFromArgs(args))
return { rows }
return paginatedResult(rows, args)
},
},
{
@@ -535,7 +540,7 @@ export const masterdataTools: McpTool[] = [
type: "object",
properties: {
query: { type: "string", description: "Suchtext für Name, Singular, Plural oder Kürzel." },
limit: { type: "number", minimum: 1, maximum: 100 },
...paginationProperties,
},
},
async handler(context, args) {
@@ -552,10 +557,11 @@ export const masterdataTools: McpTool[] = [
ilike(units.short, `%${query}%`)
)
: undefined)
.orderBy(units.name)
.limit(limitFromArgs(args))
.orderBy(units.name, units.id)
.limit(limitFromArgs(args) + 1)
.offset(offsetFromArgs(args))
return { rows }
return paginatedResult(rows, args)
},
},
]

View File

@@ -1,14 +1,10 @@
import { limitFromArgs, offsetFromArgs, paginatedResult, paginationProperties } from "../pagination"
import { and, desc, eq, ilike, or } from "drizzle-orm"
import { customers, events, plants, projects, tasks } from "../../../db/schema"
import { insertHistoryItem } from "../../utils/history"
import { activateProjectPhase } from "../projectPhases"
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]
@@ -30,7 +26,7 @@ export const organisationTools: McpTool[] = [
type: "object",
properties: {
query: { type: "string", description: "Suchtext für Name, Kundennummer, Vorname, Nachname oder Notizen." },
limit: { type: "number", minimum: 1, maximum: 100 },
...paginationProperties,
},
},
async handler(context, args) {
@@ -62,10 +58,11 @@ export const organisationTools: McpTool[] = [
})
.from(customers)
.where(and(...conditions))
.orderBy(customers.name)
.limit(limitFromArgs(args))
.orderBy(customers.name, customers.id)
.limit(limitFromArgs(args) + 1)
.offset(offsetFromArgs(args))
return { rows }
return paginatedResult(rows, args)
},
},
{
@@ -79,7 +76,7 @@ export const organisationTools: McpTool[] = [
query: { type: "string", description: "Suchtext für Name, Projektnummer, Kundenreferenz oder Notizen." },
customer: { type: "number" },
activePhase: { type: "string" },
limit: { type: "number", minimum: 1, maximum: 100 },
...paginationProperties,
},
},
async handler(context, args) {
@@ -104,10 +101,11 @@ export const organisationTools: McpTool[] = [
.select()
.from(projects)
.where(and(...conditions))
.orderBy(desc(projects.createdAt))
.limit(limitFromArgs(args))
.orderBy(desc(projects.createdAt), projects.id)
.limit(limitFromArgs(args) + 1)
.offset(offsetFromArgs(args))
return { rows }
return paginatedResult(rows, args)
},
},
{
@@ -217,7 +215,7 @@ export const organisationTools: McpTool[] = [
properties: {
query: { type: "string", description: "Suchtext für Name." },
customer: { type: "number" },
limit: { type: "number", minimum: 1, maximum: 100 },
...paginationProperties,
},
},
async handler(context, args) {
@@ -233,10 +231,11 @@ export const organisationTools: McpTool[] = [
.select()
.from(plants)
.where(and(...conditions))
.orderBy(plants.name)
.limit(limitFromArgs(args))
.orderBy(plants.name, plants.id)
.limit(limitFromArgs(args) + 1)
.offset(offsetFromArgs(args))
return { rows }
return paginatedResult(rows, args)
},
},
{
@@ -251,7 +250,7 @@ export const organisationTools: McpTool[] = [
project: { type: "number" },
customer: { type: "number" },
eventtype: { type: "string" },
limit: { type: "number", minimum: 1, maximum: 100 },
...paginationProperties,
},
},
async handler(context, args) {
@@ -277,10 +276,11 @@ export const organisationTools: McpTool[] = [
.select()
.from(events)
.where(and(...conditions))
.orderBy(desc(events.startDate))
.limit(limitFromArgs(args))
.orderBy(desc(events.startDate), events.id)
.limit(limitFromArgs(args) + 1)
.offset(offsetFromArgs(args))
return { rows }
return paginatedResult(rows, args)
},
},
{
@@ -294,7 +294,7 @@ export const organisationTools: McpTool[] = [
query: { type: "string", description: "Suchtext für Name, Beschreibung oder Kategorie." },
project: { type: "number" },
customer: { type: "number" },
limit: { type: "number", minimum: 1, maximum: 100 },
...paginationProperties,
},
},
async handler(context, args) {
@@ -318,10 +318,11 @@ export const organisationTools: McpTool[] = [
.select()
.from(tasks)
.where(and(...conditions))
.orderBy(desc(tasks.createdAt))
.limit(limitFromArgs(args))
.orderBy(desc(tasks.createdAt), tasks.id)
.limit(limitFromArgs(args) + 1)
.offset(offsetFromArgs(args))
return { rows }
return paginatedResult(rows, args)
},
},
{

View File

@@ -1,3 +1,4 @@
import { limitFromArgs, offsetFromArgs, paginatedResult, paginationProperties } from "../pagination"
import { and, asc, eq, ilike, isNull } from "drizzle-orm"
import { wikiPages } from "../../../db/schema"
import { McpContext, McpTool } from "../types"
@@ -27,17 +28,7 @@ const pageIdArg = (args: Record<string, unknown>, key = "id") => {
return value
}
const limitFromArgs = (args: Record<string, unknown>) => {
const value = Number(args.limit ?? 50)
if (!Number.isFinite(value)) return 50
return Math.min(Math.max(Math.trunc(value), 1), 100)
}
const offsetFromArgs = (args: Record<string, unknown>) => {
const value = Number(args.offset ?? 0)
if (!Number.isFinite(value)) return 0
return Math.max(Math.trunc(value), 0)
}
export function prepareWikiScope(args: Record<string, unknown>): WikiScope {
const entityType = stringArg(args, "entityType")
@@ -144,8 +135,8 @@ export const wikiTools: McpTool[] = [
...entityScopeProperties,
query: { type: "string", description: "Optionaler Suchtext im Titel." },
parentId: { type: ["string", "null"], format: "uuid", description: "Optional auf direkte Kinder dieses Ordners einschränken; null steht für die oberste Ebene." },
limit: { type: "integer", minimum: 1, maximum: 100, default: 50 },
offset: { type: "integer", minimum: 0, default: 0 },
...paginationProperties,
limit: { ...paginationProperties.limit, default: 50 },
},
},
async handler(context, args) {
@@ -173,11 +164,11 @@ export const wikiTools: McpTool[] = [
})
.from(wikiPages)
.where(and(...conditions))
.orderBy(asc(wikiPages.sortOrder), asc(wikiPages.title))
.limit(limitFromArgs(args))
.orderBy(asc(wikiPages.sortOrder), asc(wikiPages.title), wikiPages.id)
.limit(limitFromArgs(args, 50) + 1)
.offset(offsetFromArgs(args))
return { rows }
return paginatedResult(rows, args, 50)
},
},
{