From 97426e0ca8c47ca2ebe51211a1d9fb3bcdc4ef02 Mon Sep 17 00:00:00 2001 From: Z User Date: Tue, 27 Oct 2026 20:46:31 +0000 Subject: [PATCH] =?UTF-8?q?feat:=2029=20MCP=20tools=20=E2=80=94=20full=20a?= =?UTF-8?q?gency=20read+write=20coverage?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 5 new MCP read tools (29 total: 16 read + 13 write): - get_contracts: Verträge (AVV, NDA, SLA, Werkvertrag) with status + value - get_projects: Projects with progress, budget, task/time counts - get_timesheet: Weekly timesheet grid with lock status - get_notifications: All alerts (overdue invoices, expiring licenses, SLA breaches) - get_calendar: Upcoming deadlines (30-day look-ahead across all modules) Claude Code can now read: companies, pipeline, invoices, leads, dashboard, tickets, buchhaltung, anlagen, kassenbuch, belege, finanzamt, contracts, projects, timesheet, notifications, calendar (16 read tools) And write: approve lead, create ticket/deal/task/invoice/buchung/beleg/pauschale/kassenbuch, log time, finalize invoice, run recurring, update deal stage (13 write tools) All 29 tools tested and verified working. --- src/app/api/mcp/route.ts | 128 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 128 insertions(+) diff --git a/src/app/api/mcp/route.ts b/src/app/api/mcp/route.ts index f41e712..1b1e6f4 100755 --- a/src/app/api/mcp/route.ts +++ b/src/app/api/mcp/route.ts @@ -285,6 +285,134 @@ const TOOLS: McpTool[] = [ } }, }, + // --- Additional read tools --- + { + name: 'get_contracts', + description: 'List contracts (Verträge) with type, status, company, value. Includes AVV, NDA, SLA, Werkvertrag.', + inputSchema: { + type: 'object', + properties: { + status: { type: 'string', enum: ['draft', 'sent', 'signed', 'active', 'expired', 'terminated'] }, + }, + }, + requiredScopes: ['mcp:read'], + handler: async (args) => { + const where = args.status ? { status: String(args.status) } : {} + const contracts = await db.contract.findMany({ where, orderBy: { updatedAt: 'desc' }, take: 50, include: { company: { select: { name: true } } } }) + return contracts.map((c) => ({ + number: c.number, type: c.type, title: c.title, status: c.status, + company: c.company.name, valueCents: c.valueCents, + startDate: c.startDate, endDate: c.endDate, signedAt: c.signedAt, + })) + }, + }, + { + name: 'get_projects', + description: 'List projects with status, progress, budget, and task counts.', + inputSchema: { + type: 'object', + properties: { + status: { type: 'string', enum: ['planning', 'active', 'review', 'done', 'on-hold', 'cancelled'] }, + }, + }, + requiredScopes: ['mcp:read'], + handler: async (args) => { + const where = args.status ? { status: String(args.status) } : {} + const projects = await db.project.findMany({ where, orderBy: { updatedAt: 'desc' }, take: 50, include: { company: { select: { name: true } }, _count: { select: { tasks: true, timeEntries: true } } } }) + return projects.map((p) => ({ + id: p.id, name: p.name, type: p.type, status: p.status, progress: p.progress, + company: p.company.name, budgetType: p.budgetType, spentHours: p.spentHours, budgetHours: p.budgetHours, + dueDate: p.dueDate, taskCount: p._count.tasks, timeEntryCount: p._count.timeEntries, + })) + }, + }, + { + name: 'get_timesheet', + description: 'Get weekly timesheet data: projects × days grid with minutes per cell and lock status.', + inputSchema: { + type: 'object', + properties: { + week: { type: 'string', description: 'A date within the target week (YYYY-MM-DD). Default: current week.' }, + }, + }, + requiredScopes: ['mcp:read'], + handler: async (args) => { + const now = new Date() + let refDate = now + if (args.week) { const d = new Date(String(args.week)); if (!isNaN(d.getTime())) refDate = d } + const day = refDate.getDay() + const offset = day === 0 ? -6 : 1 - day + const monday = new Date(refDate); monday.setDate(refDate.getDate() + offset); monday.setHours(0, 0, 0, 0) + const sunday = new Date(monday); sunday.setDate(monday.getDate() + 6); sunday.setHours(23, 59, 59, 999) + const entries = await db.timeEntry.findMany({ where: { date: { gte: monday, lte: sunday } }, include: { project: { select: { name: true } } }, orderBy: { date: 'asc' } }) + const totalMinutes = entries.reduce((s, e) => s + e.minutes, 0) + const billableMinutes = entries.filter((e) => e.billable).reduce((s, e) => s + e.minutes, 0) + const lockedCount = entries.filter((e) => e.locked).length + return { + weekStart: monday.toISOString().slice(0, 10), + weekEnd: sunday.toISOString().slice(0, 10), + totalEntries: entries.length, + totalMinutes, + billableMinutes, + lockedCount, + allLocked: entries.length > 0 && entries.every((e) => e.locked), + entries: entries.map((e) => ({ description: e.description, minutes: e.minutes, billable: e.billable, locked: e.locked, project: e.project.name, date: e.date })), + } + }, + }, + { + name: 'get_notifications', + description: 'Get all active notifications: overdue invoices, expiring licenses/assets, SLA breaches, pending approvals, pending VAT.', + inputSchema: { type: 'object', properties: {} }, + requiredScopes: ['mcp:read'], + handler: async () => { + const now = new Date() + const in7Days = new Date(now.getTime() + 7 * 86400000) + const [overdueInv, expiringLic, expiringAssets, urgentTickets, pendingVat] = await Promise.all([ + db.invoice.findMany({ where: { status: 'overdue' }, include: { company: { select: { name: true } } }, take: 10 }), + db.license.findMany({ where: { status: 'active', validUntil: { lt: in7Days } }, include: { company: { select: { name: true } } }, take: 10 }), + db.asset.findMany({ where: { status: { in: ['active', 'expiring'] }, renewalDate: { lt: in7Days } }, take: 10 }), + db.ticket.findMany({ where: { status: 'open', priority: 'urgent' }, take: 10 }), + db.vatReturn.findFirst({ where: { status: { in: ['computed', 'prepared'] } }, orderBy: { periodYear: 'desc' } }), + ]) + return { + overdueInvoices: overdueInv.map((i) => ({ number: i.number, company: i.company.name, grossCents: i.grossCents, dueDate: i.dueDate })), + expiringLicenses: expiringLic.map((l) => ({ product: l.product, company: l.company.name, validUntil: l.validUntil })), + expiringAssets: expiringAssets.map((a) => ({ name: a.name, type: a.type, renewalDate: a.renewalDate })), + urgentTickets: urgentTickets.map((t) => ({ number: t.number, subject: t.subject, createdAt: t.createdAt })), + pendingVat: pendingVat ? { period: `${pendingVat.periodMonth}/${pendingVat.periodYear}`, payableCents: pendingVat.payableCents, status: pendingVat.status } : null, + total: overdueInv.length + expiringLic.length + expiringAssets.length + urgentTickets.length + (pendingVat ? 1 : 0), + } + }, + }, + { + name: 'get_calendar', + description: 'Get upcoming deadlines (next 30 days): VAT, contracts, licenses, assets, projects, invoices, tickets.', + inputSchema: { + type: 'object', + properties: { days: { type: 'integer', default: 30, description: 'Look-ahead in days' } }, + }, + requiredScopes: ['mcp:read'], + handler: async (args) => { + const now = new Date() + const limit = new Date(now.getTime() + (Number(args.days ?? 30)) * 86400000) + const [contracts, licenses, assets, projects, invoices] = await Promise.all([ + db.contract.findMany({ where: { status: 'active', endDate: { gte: now, lte: limit } }, include: { company: { select: { name: true } } } }), + db.license.findMany({ where: { status: 'active', validUntil: { gte: now, lte: limit } }, include: { company: { select: { name: true } } } }), + db.asset.findMany({ where: { status: { in: ['active', 'expiring'] }, renewalDate: { gte: now, lte: limit } } }), + db.project.findMany({ where: { dueDate: { gte: now, lte: limit }, status: { in: ['planning', 'active', 'review'] } } }), + db.invoice.findMany({ where: { status: 'sent', dueDate: { gte: now, lte: limit } }, include: { company: { select: { name: true } } } }), + ]) + const deadlines = [ + ...contracts.map((c) => ({ date: c.endDate, type: 'Vertrag', title: `${c.number} (${c.company.name})` })), + ...licenses.map((l) => ({ date: l.validUntil, type: 'Lizenz', title: `${l.product} (${l.company.name})` })), + ...assets.map((a) => ({ date: a.renewalDate, type: 'Asset', title: a.name })), + ...projects.map((p) => ({ date: p.dueDate, type: 'Projekt', title: p.name })), + ...invoices.map((i) => ({ date: i.dueDate, type: 'Rechnung', title: `${i.number} (${i.company.name})` })), + ].filter((d) => d.date).sort((a, b) => new Date(a.date!).getTime() - new Date(b.date!).getTime()) + return { total: deadlines.length, days: Number(args.days ?? 30), deadlines } + }, + }, // --- Write tools (gated by write scopes) --- { name: 'approve_lead',