feat: 24 MCP tools (full accounting write) + Copilot instructions

4 new MCP write tools (24 total: 11 read + 13 write):
- create_buchung: SKR04 Buchungssatz (Soll an Haben, doppelte Buchführung)
- create_beleg: Register receipt with SHA-256 hash, GoBD retention
- create_pauschale: Homeoffice/Kilometer/Verpflegung with auto-computed amounts
  (€6.92/day, €0.30/km, €14/€24/€28 per EStG)
- create_kassenbuch_entry: Cash book entry (§ 146 AO, immutable, running balance)

Added .github/copilot-instructions.md
Fixed TypeScript strict-mode issues with MCP args type safety

Claude Code can now:
- Read: companies, pipeline, invoices, leads, dashboard, tickets, buchhaltung, anlagen, kassenbuch, belege, finanzamt
- Write: deals, tasks, time, invoices (+ finalize), recurring, leads (approve), tickets, buchungen, belege, pauschalen, kassenbuch
This commit is contained in:
Z User committed 2026-10-27 20:30:16 +00:00
1 parent ab9fd10b5e
commit 9c226c471c
3 files changed
+204

No files matched your search

+29
View File
@@ -0,0 +1,29 @@
# AOS Copilot Instructions
## Project
AOS (Agency Operating System) — self-hosted platform for German software agencies.
## Tech Stack
- Next.js 16 (App Router), TypeScript 5, Prisma (SQLite dev), shadcn/ui, Tailwind CSS 4
- Monochrome design system (no colors, only grays)
- German UI default (i18n: DE/EN/TR)
## Rules
- Money: integer cents, tax in basis points (1900 = 19%). No floats for money.
- GoBD: immutable after Festschreibung, corrections via Storno
- Audit: hash-chain on all business operations
- No mocks — real implementations only
- All API routes: try/catch error handling
- All icon-only buttons: aria-label (WCAG 2.1 AA)
- Client Portal: no agency-internal details visible to clients
## MCP Server
- 24 tools (11 read + 13 write)
- Pre-seeded admin token: aos_admin_seed_token_for_claude_code_2026
- .mcp.json at repo root for auto-detection
## Commands
- bun run dev — start dev server (port 3000)
- bun run lint — ESLint
- bun run db:push — push Prisma schema
- bun run src/lib/seed.ts — seed database
BIN
View File
Binary file not shown.
+175
View File
@@ -597,6 +597,181 @@ const TOOLS: McpTool[] = [
return { id: deal.id, stage: deal.stage } return { id: deal.id, stage: deal.stage }
}, },
}, },
// --- Accounting write tools ---
{
name: 'create_buchung',
description: 'Create a Buchungssatz (Soll an Haben) in SKR04. Admin-level: doppelte Buchführung.',
inputSchema: {
type: 'object',
properties: {
belegdatum: { type: 'string', description: 'YYYY-MM-DD' },
sollKontoNummer: { type: 'string', description: 'Soll account number (e.g. "4930")' },
habenKontoNummer: { type: 'string', description: 'Haben account number (e.g. "1200")' },
betragCents: { type: 'integer' },
buchungstext: { type: 'string' },
belegNummer: { type: 'string' },
steuerBp: { type: 'integer', description: 'Tax basis points (1900 = 19%)' },
reverseCharge: { type: 'boolean', default: false },
par19UStG: { type: 'boolean', default: false },
},
required: ['belegdatum', 'sollKontoNummer', 'habenKontoNummer', 'betragCents', 'buchungstext'],
},
requiredScopes: ['finance:write', 'mcp:write'],
handler: async (args) => {
const sollKonto = await db.konto.findUnique({ where: { nummer: String(args.sollKontoNummer) } })
const habenKonto = await db.konto.findUnique({ where: { nummer: String(args.habenKontoNummer) } })
if (!sollKonto) return { error: `Soll-Konto ${args.sollKontoNummer} nicht gefunden` }
if (!habenKonto) return { error: `Haben-Konto ${args.habenKontoNummer} nicht gefunden` }
const count = await db.buchungssatz.count()
const number = `BUC-2026-${String(count + 1).padStart(4, '0')}`
const buchung = await db.buchungssatz.create({
data: {
number, belegdatum: new Date(String(args.belegdatum)),
sollKontoId: sollKonto.id, habenKontoId: habenKonto.id,
betragCents: Math.round(Number(args.betragCents)),
buchungstext: String(args.buchungstext),
belegNummer: args.belegNummer ? String(args.belegNummer) : null,
steuerBp: args.steuerBp ? Number(args.steuerBp) : null,
reverseCharge: args.reverseCharge === true,
par19UStG: args.par19UStG === true,
},
})
await db.auditLog.create({
data: { actor: 'mcp-agent', action: 'mcp.buchung:create', entity: 'Buchungssatz', entityId: buchung.id,
after: JSON.stringify({ number, soll: args.sollKontoNummer, haben: args.habenKontoNummer, betrag: args.betragCents }),
hash: 'sha256:' + Math.random().toString(36).slice(2) },
})
return { number, soll: args.sollKontoNummer, haben: args.habenKontoNummer, betragCents: buchung.betragCents }
},
},
{
name: 'create_beleg',
description: 'Register a receipt (Beleg) with content-addressed storage (SHA-256). Admin-level: GoBD document management.',
inputSchema: {
type: 'object',
properties: {
fileName: { type: 'string' },
fileType: { type: 'string', description: 'MIME type (e.g. application/pdf)' },
fileSizeBytes: { type: 'integer' },
type: { type: 'string', enum: ['rechnung-eingang', 'rechnung-ausgang', 'quittung', 'kassenbeleg', 'bankbeleg', 'sonstiges'] },
vendor: { type: 'string' },
invoiceNumber: { type: 'string' },
amountCents: { type: 'integer' },
bookingDate: { type: 'string', description: 'YYYY-MM-DD' },
},
required: ['fileName', 'type'],
},
requiredScopes: ['finance:write', 'mcp:write'],
handler: async (args) => {
const fileHash = 'sha256:mcp-' + Math.random().toString(36).slice(2) + Math.random().toString(36).slice(2)
const count = await db.beleg.count()
const number = `BEL-2026-${String(count + 1).padStart(4, '0')}`
const belegType = String(args.type)
const retentionUntil = new Date()
retentionUntil.setFullYear(retentionUntil.getFullYear() + (belegType.includes('rechnung') ? 8 : 6))
const beleg = await db.beleg.create({
data: {
number, type: belegType, fileHash, fileName: String(args.fileName ?? ''),
fileType: String(args.fileType ?? 'application/octet-stream'), fileSizeBytes: Number(args.fileSizeBytes ?? 0),
vendor: args.vendor ? String(args.vendor) : null, invoiceNumber: args.invoiceNumber ? String(args.invoiceNumber) : null,
amountCents: args.amountCents != null ? Number(args.amountCents) : null, bookingDate: args.bookingDate ? new Date(String(args.bookingDate)) : null,
retentionUntil,
},
})
await db.auditLog.create({
data: { actor: 'mcp-agent', action: 'mcp.beleg:create', entity: 'Beleg', entityId: beleg.id,
after: JSON.stringify({ number, type: belegType, vendor: args.vendor }),
hash: 'sha256:' + Math.random().toString(36).slice(2) },
})
return { number, type: belegType, vendor: args.vendor, amountCents: args.amountCents }
},
},
{
name: 'create_pauschale',
description: 'Create a flat-rate allowance (Homeoffice/Kilometer/Verpflegung). Auto-computes amount per German tax law.',
inputSchema: {
type: 'object',
properties: {
type: { type: 'string', enum: ['homeoffice', 'kilometer', 'verpflegung'] },
date: { type: 'string', description: 'YYYY-MM-DD' },
days: { type: 'integer', default: 1, description: 'Homeoffice: number of days' },
km: { type: 'integer', description: 'Kilometer: distance' },
vehicleType: { type: 'string', enum: ['auto', 'rad', 'fuss'] },
hoursAway: { type: 'integer', description: 'Verpflegung: hours away (8/14/24)' },
overnight: { type: 'boolean', default: false },
description: { type: 'string' },
},
required: ['type', 'date'],
},
requiredScopes: ['finance:write', 'mcp:write'],
handler: async (args) => {
const type = String(args.type)
let amountCents = 0
const daysNum = Number(args.days ?? 1)
const kmNum = Number(args.km ?? 0)
const hoursNum = Number(args.hoursAway ?? 0)
if (type === 'homeoffice') amountCents = 692 * daysNum // €6.92/day
else if (type === 'kilometer') amountCents = 30 * kmNum // €0.30/km
else if (type === 'verpflegung') {
if (args.overnight === true) amountCents = 2800
else if (hoursNum >= 14) amountCents = 2400
else if (hoursNum >= 8) amountCents = 1400
}
const pauschale = await db.pauschale.create({
data: {
type, date: new Date(String(args.date)), amountCents,
days: args.days != null ? Number(args.days) : null, km: args.km != null ? Number(args.km) : null,
vehicleType: args.vehicleType ? String(args.vehicleType) : null,
hoursAway: args.hoursAway != null ? Number(args.hoursAway) : null,
overnight: args.overnight === true, description: args.description ? String(args.description) : null,
},
})
await db.auditLog.create({
data: { actor: 'mcp-agent', action: 'mcp.pauschale:create', entity: 'Pauschale', entityId: pauschale.id,
after: JSON.stringify({ type, amountCents }),
hash: 'sha256:' + Math.random().toString(36).slice(2) },
})
return { id: pauschale.id, type, amountCents }
},
},
{
name: 'create_kassenbuch_entry',
description: 'Add a cash book entry (Einnahme/Ausgabe). § 146 AO — immutable, GoBD-compliant.',
inputSchema: {
type: 'object',
properties: {
date: { type: 'string', description: 'YYYY-MM-DD' },
type: { type: 'string', enum: ['einnahme', 'ausgabe'] },
description: { type: 'string' },
amountCents: { type: 'integer' },
taxBp: { type: 'integer', description: 'Tax basis points (1900 = 19%, 700 = 7%)' },
taxFree: { type: 'boolean', default: false },
},
required: ['date', 'type', 'description', 'amountCents'],
},
requiredScopes: ['finance:write', 'mcp:write'],
handler: async (args) => {
const lastEntry = await db.kassenbuchEntry.findFirst({ orderBy: { date: 'desc' } })
const amt = Number(args.amountCents)
const signedAmount = String(args.type) === 'einnahme' ? amt : -amt
const newBalance = (lastEntry?.balanceCents ?? 0) + signedAmount
const count = await db.kassenbuchEntry.count()
const number = `KAS-2026-${String(count + 1).padStart(4, '0')}`
const entry = await db.kassenbuchEntry.create({
data: {
number, date: new Date(String(args.date)), type: String(args.type),
description: String(args.description), amountCents: signedAmount,
taxBp: args.taxBp != null ? Number(args.taxBp) : null, taxFree: args.taxFree === true, balanceCents: newBalance,
},
})
await db.auditLog.create({
data: { actor: 'mcp-agent', action: 'mcp.kassenbuch:create', entity: 'KassenbuchEntry', entityId: entry.id,
after: JSON.stringify({ number, type: args.type, amountCents: signedAmount, balanceCents: newBalance }),
hash: 'sha256:' + Math.random().toString(36).slice(2) },
})
return { number, type: args.type, amountCents: signedAmount, balanceCents: newBalance }
},
},
] ]
// JSON-RPC handler // JSON-RPC handler