From 37cb23d245d728c0837cccfc46098d86856f3186 Mon Sep 17 00:00:00 2001 From: Z User Date: Mon, 26 Oct 2026 23:40:17 +0000 Subject: [PATCH] 115e52dd-042f-444c-9ed4-9bfde9257c45 --- db/custom.db | Bin 323584 -> 323584 bytes src/app/api/aos/api-tokens/route.ts | 7 + src/app/api/mcp/route.ts | 343 ++++++++++++++++++++++++++++ src/components/aos/views/api.tsx | 37 +++ src/lib/seed.ts | 2 +- 5 files changed, 388 insertions(+), 1 deletion(-) create mode 100644 src/app/api/mcp/route.ts diff --git a/db/custom.db b/db/custom.db index 84efc8a5d6e756b09c002764b02ca0f6afcac53f..4dcad710b7733d02a2f3a4237cd7f697a844ae9a 100644 GIT binary patch delta 26536 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 bo!EEh#Ot12c:read", +// write tools need ":write". + +interface McpTool { + name: string + description: string + inputSchema: Record + requiredScopes: string[] + handler: (args: Record, token: { id: string; name: string; scopes: string[] }) => Promise +} + +const TOOLS: McpTool[] = [ + // --- Read tools --- + { + name: 'search_companies', + description: 'Search companies in the CRM by name, city, or website. Returns matching companies with deal/project counts.', + inputSchema: { + type: 'object', + properties: { + query: { type: 'string', description: 'Search term (company name, city, or website)' }, + limit: { type: 'integer', description: 'Max results (default 10, max 50)', default: 10 }, + }, + required: ['query'], + }, + requiredScopes: ['crm.company:read', 'mcp:read'], + handler: async (args) => { + const q = String(args.query ?? '') + const limit = Math.min(Number(args.limit ?? 10), 50) + const companies = await db.company.findMany({ + where: { OR: [{ name: { contains: q } }, { city: { contains: q } }, { website: { contains: q } }] }, + take: limit, + include: { _count: { select: { deals: true, projects: true, invoices: true } } }, + }) + return companies.map((c) => ({ + id: c.id, name: c.name, legalForm: c.legalForm, vatId: c.vatId, + website: c.website, city: c.city, email: c.email, + dealCount: c._count.deals, projectCount: c._count.projects, invoiceCount: c._count.invoices, + })) + }, + }, + { + name: 'get_pipeline', + description: 'Get the current sales pipeline (deals grouped by stage). Returns deals with company, value, and probability.', + inputSchema: { + type: 'object', + properties: { + stage: { type: 'string', description: 'Filter by stage (lead, qualified, proposal, negotiation, won, lost)', enum: ['lead', 'qualified', 'proposal', 'negotiation', 'won', 'lost'] }, + }, + }, + requiredScopes: ['crm.deal:read', 'mcp:read'], + handler: async (args) => { + const where = args.stage ? { stage: String(args.stage) } : {} + const deals = await db.deal.findMany({ where, include: { company: true }, orderBy: { valueCents: 'desc' } }) + return deals.map((d) => ({ + id: d.id, title: d.title, stage: d.stage, company: d.company.name, + valueCents: d.valueCents, probability: d.probability, expectedClose: d.expectedClose, + })) + }, + }, + { + name: 'get_invoice', + description: 'Get invoice details by number (e.g. RE-2026-0042). Returns line items, E-Rechnung status, and payment status.', + inputSchema: { + type: 'object', + properties: { + number: { type: 'string', description: 'Invoice number (e.g. RE-2026-0042)' }, + }, + required: ['number'], + }, + requiredScopes: ['finance.invoice:read', 'mcp:read'], + handler: async (args) => { + const inv = await db.invoice.findUnique({ + where: { number: String(args.number) }, + include: { company: true, lines: true }, + }) + if (!inv) return { error: 'invoice not found' } + return { + number: inv.number, status: inv.status, type: inv.type, + company: inv.company.name, netCents: inv.netCents, grossCents: inv.grossCents, + issueDate: inv.issueDate, dueDate: inv.dueDate, paidDate: inv.paidDate, + eRechnung: { zugferd: inv.zugferd, xrechnung: inv.xrechnung }, + lines: inv.lines.map((l) => ({ description: l.description, quantity: l.quantity, totalCents: l.totalCents })), + } + }, + }, + { + name: 'list_leads', + description: 'List leads from the Leads Finder. Returns scored leads with audit evidence summary.', + inputSchema: { + type: 'object', + properties: { + status: { type: 'string', enum: ['pending', 'approved', 'rejected', 'converted'], description: 'Filter by status' }, + limit: { type: 'integer', default: 10 }, + }, + }, + requiredScopes: ['leads:read', 'mcp:read'], + handler: async (args) => { + const where = args.status ? { status: String(args.status) } : {} + const leads = await db.lead.findMany({ where, take: Math.min(Number(args.limit ?? 10), 50), orderBy: { score: 'desc' } }) + return leads.map((l) => ({ + id: l.id, companyName: l.companyName, website: l.website, city: l.city, + score: l.score, scoreLabel: l.scoreLabel, status: l.status, + explanation: l.explanation?.slice(0, 200), + })) + }, + }, + { + name: 'get_dashboard', + description: 'Get agency dashboard KPIs (pipeline value, open projects, MRR, overdue invoices, active licenses).', + inputSchema: { type: 'object', properties: {} }, + requiredScopes: ['mcp:read'], + handler: async () => { + const [deals, projects, invoices, licenses] = await Promise.all([ + db.deal.findMany(), + db.project.findMany(), + db.invoice.findMany(), + db.license.findMany({ where: { status: 'active' } }), + ]) + return { + pipelineValueCents: deals.filter((d) => d.stage !== 'won' && d.stage !== 'lost').reduce((s, d) => s + d.valueCents, 0), + wonValueCents: deals.filter((d) => d.stage === 'won').reduce((s, d) => s + d.valueCents, 0), + openProjects: projects.filter((p) => p.status !== 'done' && p.status !== 'cancelled').length, + outstandingCents: invoices.filter((i) => i.status === 'sent' || i.status === 'overdue').reduce((s, i) => s + i.grossCents, 0), + activeLicenses: licenses.length, + totalDeals: deals.length, + } + }, + }, + { + name: 'list_tickets', + description: 'List support tickets with status, priority, and SLA info.', + inputSchema: { + type: 'object', + properties: { + status: { type: 'string', enum: ['open', 'pending', 'resolved', 'closed'] }, + limit: { type: 'integer', default: 10 }, + }, + }, + requiredScopes: ['support:read', 'mcp:read'], + handler: async (args) => { + const where = args.status ? { status: String(args.status) } : {} + const tickets = await db.ticket.findMany({ + where, take: Math.min(Number(args.limit ?? 10), 50), + orderBy: { createdAt: 'desc' }, + include: { _count: { select: { messages: true } } }, + }) + return tickets.map((t) => ({ + number: t.number, subject: t.subject, status: t.status, priority: t.priority, + messageCount: t._count.messages, createdAt: t.createdAt, + })) + }, + }, + // --- Write tools (gated by write scopes) --- + { + name: 'approve_lead', + description: 'Approve a lead in the Leads Finder review queue. Changes status to "approved".', + inputSchema: { + type: 'object', + properties: { + leadId: { type: 'string', description: 'Lead ID' }, + reason: { type: 'string', description: 'Optional approval reason' }, + }, + required: ['leadId'], + }, + requiredScopes: ['leads:write', 'mcp:write'], + handler: async (args) => { + const lead = await db.lead.update({ + where: { id: String(args.leadId) }, + data: { status: 'approved', reviewReason: String(args.reason ?? 'MCP agent approval'), reviewedAt: new Date() }, + }) + await db.auditLog.create({ + data: { + actor: 'mcp-agent', + action: 'mcp.leads:approve', + entity: 'Lead', entityId: lead.id, + after: JSON.stringify({ status: 'approved' }), + hash: 'sha256:' + Math.random().toString(36).slice(2), + }, + }) + return { id: lead.id, status: lead.status } + }, + }, + { + name: 'create_ticket', + description: 'Create a new support ticket for a company.', + inputSchema: { + type: 'object', + properties: { + companyId: { type: 'string', description: 'Customer company ID' }, + subject: { type: 'string' }, + priority: { type: 'string', enum: ['low', 'normal', 'high', 'urgent'], default: 'normal' }, + message: { type: 'string', description: 'Initial message body' }, + }, + required: ['companyId', 'subject', 'message'], + }, + requiredScopes: ['support:write', 'mcp:write'], + handler: async (args) => { + const count = await db.ticket.count() + const ticket = await db.ticket.create({ + data: { + number: `TKT-2026-${String(count + 1).padStart(4, '0')}`, + subject: String(args.subject), + companyId: String(args.companyId), + priority: String(args.priority ?? 'normal'), + categoryId: 'anfrage', + status: 'open', + }, + }) + await db.ticketMessage.create({ + data: { + ticketId: ticket.id, + fromType: 'customer', + fromName: 'MCP Agent', + body: String(args.message), + }, + }) + await db.auditLog.create({ + data: { + actor: 'mcp-agent', + action: 'mcp.support:create', + entity: 'Ticket', entityId: ticket.id, + after: JSON.stringify({ number: ticket.number, subject: ticket.subject }), + hash: 'sha256:' + Math.random().toString(36).slice(2), + }, + }) + return { id: ticket.id, number: ticket.number, status: ticket.status } + }, + }, +] + +// JSON-RPC handler +export async function POST(req: Request) { + const authHeader = req.headers.get('authorization') + const auth = await verifyToken(authHeader) + + if (!auth.ok || !auth.token) { + return Response.json( + { jsonrpc: '2.0', error: { code: -32001, message: auth.error ?? 'unauthorized' }, id: null }, + { status: 401 } + ) + } + + let body: { jsonrpc?: string; method?: string; params?: unknown; id?: string | number | null } + try { + body = await req.json() + } catch { + return Response.json({ jsonrpc: '2.0', error: { code: -32700, message: 'parse error' }, id: null }, { status: 400 }) + } + + const { method, params, id } = body + const p = (params ?? {}) as Record + + try { + if (method === 'initialize') { + return Response.json({ + jsonrpc: '2.0', id, + result: { + protocolVersion: '2024-11-05', + capabilities: { tools: { listChanged: false } }, + serverInfo: { name: 'aos-mcp', version: '1.0.0' }, + }, + }) + } + + if (method === 'tools/list') { + return Response.json({ + jsonrpc: '2.0', id, + result: { + tools: TOOLS.map((t) => ({ + name: t.name, + description: t.description, + inputSchema: t.inputSchema, + annotations: { + readOnly: t.requiredScopes.some((s) => s.endsWith(':read')), + destructive: t.requiredScopes.some((s) => s.endsWith(':write')), + }, + })), + }, + }) + } + + if (method === 'tools/call') { + const toolName = String(p.name ?? '') + const toolArgs = (p.arguments ?? {}) as Record + const tool = TOOLS.find((t) => t.name === toolName) + if (!tool) { + return Response.json({ jsonrpc: '2.0', id, error: { code: -32601, message: `unknown tool: ${toolName}` } }, { status: 404 }) + } + // scope check + const hasAccess = tool.requiredScopes.some((s) => hasScope(auth.token, s)) + if (!hasAccess) { + return Response.json({ + jsonrpc: '2.0', id, + error: { code: -32603, message: `insufficient scope — requires one of: ${tool.requiredScopes.join(', ')}`, tokenScopes: auth.token.scopes }, + }, { status: 403 }) + } + const result = await tool.handler(toolArgs, auth.token) + return Response.json({ + jsonrpc: '2.0', id, + result: { + content: [{ type: 'text', text: JSON.stringify(result, null, 2) }], + structuredContent: result, + }, + }) + } + + return Response.json({ jsonrpc: '2.0', id, error: { code: -32601, message: `unknown method: ${method}` } }, { status: 404 }) + } catch (err) { + const msg = err instanceof Error ? err.message : 'internal error' + return Response.json({ jsonrpc: '2.0', id, error: { code: -32603, message: msg } }, { status: 500 }) + } +} + +// GET returns MCP server metadata (public, no token) +export async function GET() { + return Response.json({ + name: 'aos-mcp', + version: '1.0.0', + protocolVersion: '2024-11-05', + description: 'AOS Model Context Protocol server. Exposes agency data as tools for external agents (Claude, ZeroClaw).', + endpoint: '/api/mcp', + auth: 'Bearer aos_ (requires mcp:read or mcp:write scope)', + tools: TOOLS.map((t) => ({ name: t.name, description: t.description, scopes: t.requiredScopes, readOnly: t.requiredScopes.some((s) => s.endsWith(':read')) })), + example: { + method: 'tools/call', + params: { name: 'search_companies', arguments: { query: 'Hamburg' } }, + }, + }) +} diff --git a/src/components/aos/views/api.tsx b/src/components/aos/views/api.tsx index 9c2c0be..15a3e71 100644 --- a/src/components/aos/views/api.tsx +++ b/src/components/aos/views/api.tsx @@ -378,6 +378,43 @@ const sig = createHmac('sha256', WEBHOOK_SECRET)
  • • Alle API-Zugriffe werden im Audit-Log protokolliert
  • + + {/* MCP Server */} +
    +

    MCP Server (Model Context Protocol)

    +

    + AOS exposes its data as MCP tools (PRD §8) so external agents (Claude Desktop, ZeroClaw) can read agency data and perform scoped actions. +

    +
    {`# MCP endpoint (JSON-RPC 2.0)
    +POST /api/mcp
    +Authorization: Bearer aos_  # requires mcp:read or mcp:write scope
    +
    +# Available MCP tools:
    +search_companies   # crm.company:read    — search CRM
    +get_pipeline       # crm.deal:read        — sales pipeline
    +get_invoice        # finance.invoice:read — invoice by number
    +list_leads         # leads:read           — Leads Finder queue
    +get_dashboard      # mcp:read             — agency KPIs
    +list_tickets       # support:read         — support inbox
    +approve_lead       # leads:write          — approve a lead (write)
    +create_ticket      # support:write        — create ticket (write)`}
    +
    {`# Example: call search_companies via MCP
    +curl -X POST https://aos.nordlicht.digital/api/mcp \\
    +  -H "Authorization: Bearer aos_" \\
    +  -H "Content-Type: application/json" \\
    +  -d '{"jsonrpc":"2.0","method":"tools/call","params":{"name":"search_companies","arguments":{"query":"Hamburg"}},"id":1}'`}
    +

    + Claude Desktop config (claude_desktop_config.json): +

    +
    {`{
    +  "mcpServers": {
    +    "aos": {
    +      "url": "https://aos.nordlicht.digital/api/mcp",
    +      "headers": { "Authorization": "Bearer aos_" }
    +    }
    +  }
    +}`}
    +
    diff --git a/src/lib/seed.ts b/src/lib/seed.ts index dc25645..f07c675 100644 --- a/src/lib/seed.ts +++ b/src/lib/seed.ts @@ -992,7 +992,7 @@ Monatlich: Buchungsstapel an Steuerberater.`, // --- API Tokens + Webhooks (real crypto, hashed at rest) ----------------- const crypto = await import('crypto') const apiTokens = [ - { name: 'ZeroClaw Agent (read-only)', scopes: ['crm.company:read', 'finance.invoice:read', 'licenses.license:read'] }, + { name: 'ZeroClaw Agent (MCP)', scopes: ['mcp:read', 'mcp:write', 'crm.company:read', 'crm.deal:read', 'finance.invoice:read', 'leads:read', 'leads:write', 'support:read', 'support:write'] }, { name: 'DATEV Export-Skript', scopes: ['finance.invoice:read'] }, { name: 'WordPress Update-Server', scopes: ['licenses.license:read'] }, ]