diff --git a/.mcp.json b/.mcp.json new file mode 100644 index 0000000..b557296 --- /dev/null +++ b/.mcp.json @@ -0,0 +1,10 @@ +{ + "mcpServers": { + "aos": { + "url": "http://localhost:3000/api/mcp", + "headers": { + "Authorization": "Bearer aos_admin_seed_token_for_claude_code_2026" + } + } + } +} diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..aafa749 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,59 @@ +# AOS — Agency Operating System + +## Project Overview +Self-hosted platform for German software agencies. CRM, Leads Finder, Projects, Licenses, Contracts, Expenses, Finance, full Accounting (SKR04, GoBD, ELSTER, DATEV), Client Portal, MCP Server, PWA. + +## Tech Stack +- **Framework**: Next.js 16 (App Router, Turbopack) +- **Language**: TypeScript 5 (strict) +- **Database**: Prisma ORM (SQLite for dev) +- **UI**: shadcn/ui + Tailwind CSS 4 (monochrome design) +- **State**: Zustand + TanStack Query patterns +- **AI**: z-ai-web-dev-sdk (server-side only) + +## Commands +```bash +bun run dev # Start dev server (port 3000) +bun run lint # ESLint check +bun run db:push # Push Prisma schema to DB +bun run db:generate # Generate Prisma Client +bun run src/lib/seed.ts # Seed database +``` + +## Architecture +- **Single deployable** (modular monolith per PRD D1) +- **Money**: integer cents, tax in basis points. No floats. +- **GoBD**: immutable after Festschreibung, corrections via Storno (new row) +- **Audit**: hash-chain on all business operations +- **Auth**: Bearer token (SHA-256 hashed at rest) with scoped permissions + +## Key Directories +``` +src/app/api/aos/ # Internal API routes +src/app/api/v1/ # Public REST API (scoped tokens) +src/app/api/mcp/ # MCP Server (JSON-RPC for Claude Code) +src/components/aos/ # All UI components +src/lib/ # Shared logic (db, format, i18n, api-auth, webhook) +prisma/ # Schema + migrations +``` + +## MCP Server +- Endpoint: `POST /api/mcp` (JSON-RPC 2.0) +- 15 tools: 7 read + 8 write +- Auth: Bearer token with `mcp:read` or `mcp:write` scope +- All write actions audit-logged with actor `mcp-agent` + +## Compliance +- GoBD: Festschreibung (SHA-256 hash), immutable Buchungssätze, 8/6 year retention +- DSGVO: Art. 15/17/20/30, consent log, data export with checksum +- E-Rechnung: ZUGFeRD/XRechnung (EN 16931) +- SKR04 Kontenrahmen, EÜR, UStVA, DATEV EXTF export +- ELSTER: UStVA XML (ERiC requires cert in production) + +## Important Rules +- No mocks in production code — real implementations only +- No `any` types — all interfaces explicitly typed +- All API routes have try/catch error handling +- All icon-only buttons have `aria-label` (WCAG 2.1 AA) +- Client Portal: no agency-internal details visible to clients +- German UI by default (i18n: DE/EN/TR) diff --git a/db/custom.db b/db/custom.db index e192273..d097d14 100644 Binary files a/db/custom.db and b/db/custom.db differ diff --git a/src/app/api/health/route.ts b/src/app/api/health/route.ts new file mode 100644 index 0000000..a55b845 --- /dev/null +++ b/src/app/api/health/route.ts @@ -0,0 +1,25 @@ +import { NextResponse } from 'next/server' +import { db } from '@/lib/db' + +export const dynamic = 'force-dynamic' + +// GET /api/health — system health check +export async function GET() { + try { + const companyCount = await db.company.count() + + return NextResponse.json({ + status: 'ok', + timestamp: new Date().toISOString(), + version: '1.0.0', + database: { connected: true, companyCount }, + uptime: process.uptime ? `${Math.round(process.uptime())}s` : 'n/a', + }) + } catch { + return NextResponse.json({ + status: 'error', + timestamp: new Date().toISOString(), + database: { connected: false }, + }, { status: 503 }) + } +} diff --git a/src/app/api/mcp/route.ts b/src/app/api/mcp/route.ts index c4a4403..13aaeae 100755 --- a/src/app/api/mcp/route.ts +++ b/src/app/api/mcp/route.ts @@ -164,6 +164,127 @@ const TOOLS: McpTool[] = [ })) }, }, + // --- Accounting read tools --- + { + name: 'get_buchhaltung', + description: 'Get SKR04 accounting data: Konten, Journal (Buchungssätze), Saldenliste, EÜR summary. Full doppelte Buchführung overview.', + inputSchema: { type: 'object', properties: {} }, + requiredScopes: ['mcp:read'], + handler: async () => { + const [konten, buchungen, euerEntries] = await Promise.all([ + db.konto.findMany({ orderBy: { nummer: 'asc' } }), + db.buchungssatz.findMany({ take: 50, orderBy: { belegdatum: 'desc' }, include: { sollKonto: true, habenKonto: true } }), + db.euerEntry.findMany({ where: { jahr: 2026 }, orderBy: { zeile: 'asc' } }), + ]) + const einnahmen = euerEntries.filter((e) => e.art === 'einnahme').reduce((s, e) => s + e.betragCents, 0) + const ausgaben = euerEntries.filter((e) => e.art === 'ausgabe').reduce((s, e) => s + e.betragCents, 0) + return { + kontenCount: konten.length, + buchungenCount: buchungen.length, + festgeschrieben: buchungen.filter((b) => b.festgeschrieben).length, + euer: { einnahmen, ausgaben, gewinn: einnahmen - ausgaben }, + journal: buchungen.slice(0, 10).map((b) => ({ + number: b.number, belegdatum: b.belegdatum, betragCents: b.betragCents, + soll: b.sollKonto.nummer, haben: b.habenKonto.nummer, text: b.buchungstext, + festgeschrieben: b.festgeschrieben, + })), + } + }, + }, + { + name: 'get_anlagen', + description: 'Get asset register (Anlagenverzeichnis) with AfA (depreciation). Returns all assets with book values.', + inputSchema: { type: 'object', properties: {} }, + requiredScopes: ['mcp:read'], + handler: async () => { + const anlagen = await db.anlage.findMany({ orderBy: { purchaseDate: 'desc' } }) + return { + total: anlagen.length, + totalPurchaseCents: anlagen.reduce((s, a) => s + a.purchaseCostCents, 0), + totalBookValueCents: anlagen.reduce((s, a) => s + a.bookValueCents, 0), + totalAnnualAfaCents: anlagen.reduce((s, a) => s + a.annualAfaCents, 0), + gwgCount: anlagen.filter((a) => a.isGWG).length, + assets: anlagen.map((a) => ({ + number: a.number, name: a.name, category: a.category, + purchaseCostCents: a.purchaseCostCents, bookValueCents: a.bookValueCents, + annualAfaCents: a.annualAfaCents, isGWG: a.isGWG, status: a.status, + })), + } + }, + }, + { + name: 'get_kassenbuch', + description: 'Get cash book (Kassenbuch) entries. § 146 AO compliant — immutable.', + inputSchema: { + type: 'object', + properties: { limit: { type: 'integer', default: 20 } }, + }, + requiredScopes: ['mcp:read'], + handler: async (args) => { + const entries = await db.kassenbuchEntry.findMany({ + take: Math.min(Number(args.limit ?? 20), 100), + orderBy: { date: 'desc' }, + }) + const balance = entries[0]?.balanceCents ?? 0 + return { + entries: entries.map((e) => ({ + number: e.number, date: e.date, type: e.type, description: e.description, + amountCents: e.amountCents, balanceCents: e.balanceCents, + })), + currentBalanceCents: balance, + } + }, + }, + { + name: 'get_belege', + description: 'Get receipts/documents (Belege). Content-addressed (SHA-256 hash).', + inputSchema: { + type: 'object', + properties: { limit: { type: 'integer', default: 20 } }, + }, + requiredScopes: ['mcp:read'], + handler: async (args) => { + const belege = await db.beleg.findMany({ + take: Math.min(Number(args.limit ?? 20), 100), + orderBy: { createdAt: 'desc' }, + }) + return { + total: belege.length, + totalAmountCents: belege.reduce((s, b) => s + (b.amountCents ?? 0), 0), + belege: belege.map((b) => ({ + number: b.number, type: b.type, vendor: b.vendor, + invoiceNumber: b.invoiceNumber, amountCents: b.amountCents, + fileHash: b.fileHash?.slice(0, 16) + '…', bookingDate: b.bookingDate, + })), + } + }, + }, + { + name: 'get_finanzamt', + description: 'Get Finanzamt data: VAT returns, Jahreserklärungen, Jahresarchive. GoBD compliance overview.', + inputSchema: { type: 'object', properties: {} }, + requiredScopes: ['mcp:read'], + handler: async () => { + const [vatReturns, jahreserkl, archiv] = await Promise.all([ + db.vatReturn.findMany({ orderBy: [{ periodYear: 'desc' }, { periodMonth: 'desc' }] }), + db.jahreserklaerung.findMany({ orderBy: { jahr: 'desc' } }), + db.jahresarchiv.findMany({ orderBy: { jahr: 'desc' } }), + ]) + return { + vatReturns: vatReturns.map((v) => ({ + period: `${v.periodMonth}/${v.periodYear}`, status: v.status, + payableCents: v.payableCents, elsterRef: v.elsterRef, + })), + jahreserklarungen: jahreserkl.map((e) => ({ + jahr: e.jahr, art: e.art, status: e.status, elsterRef: e.elsterRef, + })), + jahresarchive: archiv.map((a) => ({ + jahr: a.jahr, archivHash: a.archivHash?.slice(0, 24) + '…', + buchungenCount: a.buchungenCount, belegeCount: a.belegeCount, + })), + } + }, + }, // --- Write tools (gated by write scopes) --- { name: 'approve_lead', diff --git a/src/lib/api-auth.ts b/src/lib/api-auth.ts index b153277..4214767 100755 --- a/src/lib/api-auth.ts +++ b/src/lib/api-auth.ts @@ -17,7 +17,7 @@ export async function verifyToken(authHeader: string | null): Promise