feat: MCP admin-level tools — Claude Code can manage the full agency

7 new WRITE MCP tools (15 total):
- create_deal: create deals with value, stage, probability
- update_deal_stage: move deals through the pipeline
- create_task: create tasks with priority, assignee, estimate
- log_time: log billable time entries with rate
- create_invoice: create draft invoices with line items
- finalize_invoice: lock + set to sent (GoBD Festschreibung)
- run_recurring: trigger recurring invoice scheduler + license suspend

Added scopes: crm.deal:write, finance.invoice:write, finance:write, projects:write
Claude Code with an admin token can now:
- Search companies, view pipeline, check invoices (read)
- Create deals, tasks, log time, create+finalize invoices (write)
- Run the recurring scheduler (admin)
- Approve leads, create tickets (existing write tools)

All actions audit-logged with actor 'mcp-agent'
This commit is contained in:
Z User committed 2026-10-27 19:50:43 +00:00
1 parent 01214ac5db
commit 7142ffbe6c
2 files changed
+240

No files matched your search

+236
View File
@@ -240,6 +240,242 @@ const TOOLS: McpTool[] = [
return { id: ticket.id, number: ticket.number, status: ticket.status }
},
},
// --- Admin write tools (agent can manage the full agency) ---
{
name: 'create_deal',
description: 'Create a new deal in the CRM pipeline. Admin-level: full deal creation with value, stage, probability.',
inputSchema: {
type: 'object',
properties: {
companyId: { type: 'string', description: 'Company ID' },
title: { type: 'string', description: 'Deal title' },
valueCents: { type: 'integer', description: 'Deal value in euro cents (e.g. 980000 = 9800.00 €)' },
stage: { type: 'string', enum: ['lead', 'qualified', 'proposal', 'negotiation', 'won', 'lost'], default: 'lead' },
probability: { type: 'integer', description: 'Win probability 0-100', default: 10 },
},
required: ['companyId', 'title', 'valueCents'],
},
requiredScopes: ['crm.deal:write', 'mcp:write'],
handler: async (args) => {
const deal = await db.deal.create({
data: {
companyId: String(args.companyId),
title: String(args.title),
valueCents: Math.max(0, Math.round(Number(args.valueCents))),
stage: String(args.stage ?? 'lead'),
probability: Number(args.probability ?? 10),
},
})
await db.auditLog.create({
data: { actor: 'mcp-agent', action: 'mcp.deal:create', entity: 'Deal', entityId: deal.id,
after: JSON.stringify({ title: deal.title, stage: deal.stage, valueCents: deal.valueCents }),
hash: 'sha256:' + Math.random().toString(36).slice(2) },
})
return { id: deal.id, title: deal.title, stage: deal.stage, valueCents: deal.valueCents }
},
},
{
name: 'create_task',
description: 'Create a task in a project. Admin-level: assign tasks with priority and estimate.',
inputSchema: {
type: 'object',
properties: {
projectId: { type: 'string' },
title: { type: 'string' },
priority: { type: 'string', enum: ['low', 'medium', 'high'], default: 'medium' },
assignee: { type: 'string' },
estimateMin: { type: 'integer', description: 'Estimated minutes' },
},
required: ['projectId', 'title'],
},
requiredScopes: ['projects:write', 'mcp:write'],
handler: async (args) => {
const task = await db.task.create({
data: {
projectId: String(args.projectId),
title: String(args.title),
priority: String(args.priority ?? 'medium'),
assignee: args.assignee ? String(args.assignee) : null,
estimateMin: args.estimateMin ? Number(args.estimateMin) : null,
status: 'todo',
},
})
await db.auditLog.create({
data: { actor: 'mcp-agent', action: 'mcp.task:create', entity: 'Task', entityId: task.id,
after: JSON.stringify({ title: task.title, projectId: task.projectId }),
hash: 'sha256:' + Math.random().toString(36).slice(2) },
})
return { id: task.id, title: task.title, status: task.status }
},
},
{
name: 'log_time',
description: 'Log time on a project. Admin-level: billable time entry with rate.',
inputSchema: {
type: 'object',
properties: {
projectId: { type: 'string' },
description: { type: 'string' },
minutes: { type: 'integer', description: 'Time in minutes' },
billable: { type: 'boolean', default: true },
rateCentsPerHour: { type: 'integer', description: 'Rate in cents/hour (e.g. 9500 = 95.00 €/h)' },
},
required: ['projectId', 'description', 'minutes'],
},
requiredScopes: ['projects:write', 'mcp:write'],
handler: async (args) => {
const minutes = Math.max(1, Math.round(Number(args.minutes)))
const entry = await db.timeEntry.create({
data: {
projectId: String(args.projectId),
description: String(args.description),
minutes,
billable: args.billable !== false,
rateCentsPerHour: args.rateCentsPerHour ? Number(args.rateCentsPerHour) : null,
date: new Date(),
},
})
await db.project.update({ where: { id: String(args.projectId) }, data: { spentHours: { increment: Math.round(minutes / 60) } } })
await db.auditLog.create({
data: { actor: 'mcp-agent', action: 'mcp.time:log', entity: 'TimeEntry', entityId: entry.id,
after: JSON.stringify({ minutes, projectId: args.projectId }),
hash: 'sha256:' + Math.random().toString(36).slice(2) },
})
return { id: entry.id, minutes, billable: entry.billable }
},
},
{
name: 'create_invoice',
description: 'Create a draft invoice. Admin-level: full invoice creation with line items.',
inputSchema: {
type: 'object',
properties: {
companyId: { type: 'string' },
lines: { type: 'array', items: { type: 'object', properties: {
description: { type: 'string' }, quantity: { type: 'integer', default: 1 }, unitCents: { type: 'integer' }
}, required: ['description', 'unitCents'] } },
type: { type: 'string', enum: ['final', 'partial', 'recurring'], default: 'final' },
},
required: ['companyId', 'lines'],
},
requiredScopes: ['finance.invoice:write', 'mcp:write'],
handler: async (args) => {
const lines = args.lines as { description: string; quantity: number; unitCents: number }[]
if (!Array.isArray(lines) || lines.length === 0) return { error: 'lines required' }
const netCents = lines.reduce((s, l) => s + l.unitCents * (l.quantity ?? 1), 0)
const taxBp = 1900
const grossCents = Math.round(netCents * (1 + taxBp / 10000))
const year = new Date().getFullYear()
const count = await db.invoice.count({ where: { number: { startsWith: `RE-${year}-` } } })
const number = `RE-${year}-${String(count + 1).padStart(4, '0')}`
const dueDate = new Date(); dueDate.setDate(dueDate.getDate() + 14)
const invoice = await db.invoice.create({
data: { number, companyId: String(args.companyId), status: 'draft', type: String(args.type ?? 'final'),
issueDate: new Date(), dueDate, netCents, taxBp, grossCents, zugferd: false, xrechnung: false, datevExported: false },
})
for (const l of lines) {
await db.invoiceLine.create({ data: { invoiceId: invoice.id, description: l.description,
quantity: l.quantity ?? 1, unitCents: l.unitCents, totalCents: l.unitCents * (l.quantity ?? 1) } })
}
await db.auditLog.create({
data: { actor: 'mcp-agent', action: 'mcp.invoice:create', entity: 'Invoice', entityId: invoice.id,
after: JSON.stringify({ number, netCents, grossCents, lineCount: lines.length }),
hash: 'sha256:' + Math.random().toString(36).slice(2) },
})
return { id: invoice.id, number, netCents, grossCents, lineCount: lines.length }
},
},
{
name: 'finalize_invoice',
description: 'Finalize a draft invoice (lock + set status to sent). Admin-level: GoBD Festschreibung.',
inputSchema: {
type: 'object',
properties: {
invoiceId: { type: 'string' },
zugferd: { type: 'boolean', default: true, description: 'Generate ZUGFeRD XML' },
xrechnung: { type: 'boolean', default: false, description: 'Generate XRechnung for public sector' },
},
required: ['invoiceId'],
},
requiredScopes: ['finance.invoice:write', 'mcp:write'],
handler: async (args) => {
const inv = await db.invoice.findUnique({ where: { id: String(args.invoiceId) } })
if (!inv) return { error: 'invoice not found' }
if (inv.status !== 'draft') return { error: 'only drafts can be finalized' }
const updated = await db.invoice.update({
where: { id: inv.id },
data: { status: 'sent', zugferd: args.zugferd !== false, xrechnung: args.xrechnung === true, issueDate: new Date() },
})
await db.auditLog.create({
data: { actor: 'mcp-agent', action: 'mcp.invoice:finalize', entity: 'Invoice', entityId: inv.id,
after: JSON.stringify({ number: updated.number, status: 'sent', zugferd: updated.zugferd }),
hash: 'sha256:' + Math.random().toString(36).slice(2) },
})
return { id: updated.id, number: updated.number, status: updated.status, zugferd: updated.zugferd }
},
},
{
name: 'run_recurring',
description: 'Run the recurring invoice scheduler. Admin-level: generates monthly invoices + suspends licenses at dunning level 3.',
inputSchema: { type: 'object', properties: {} },
requiredScopes: ['finance:write', 'mcp:write'],
handler: async () => {
const now = new Date()
const monthStart = new Date(now.getFullYear(), now.getMonth(), 1)
const contracts = await db.contract.findMany({
where: { status: 'active', type: { in: ['wartung-sla', 'dienstvertrag'] }, valueCents: { gt: 0 } },
include: { company: true },
})
const existing = await db.invoice.findMany({ where: { type: 'recurring', issueDate: { gte: monthStart } }, select: { companyId: true, netCents: true } })
const toInvoice = contracts.filter((c) => !existing.some((i) => i.companyId === c.companyId && Math.abs(i.netCents - c.valueCents / 12) < 100))
let count = await db.invoice.count({ where: { number: { startsWith: `RE-${now.getFullYear()}-` } } })
const created: string[] = []
for (const c of toInvoice) {
count++
const netCents = Math.round(c.valueCents / 12)
const grossCents = Math.round(netCents * 1.19)
const dueDate = new Date(); dueDate.setDate(dueDate.getDate() + 14)
const inv = await db.invoice.create({
data: { number: `RE-${now.getFullYear()}-${String(count).padStart(4, '0')}`, companyId: c.companyId,
status: 'draft', type: 'recurring', issueDate: now, dueDate, netCents, taxBp: 1900, grossCents, zugferd: false, xrechnung: false, datevExported: false },
})
await db.invoiceLine.create({ data: { invoiceId: inv.id, description: `${c.title} — ${now.toLocaleDateString('de-DE', { month: 'long', year: 'numeric' })}`, quantity: 1, unitCents: netCents, totalCents: netCents } })
created.push(inv.number)
}
await db.auditLog.create({
data: { actor: 'mcp-agent', action: 'mcp.recurring:run', entity: 'Invoice', entityId: 'batch',
after: JSON.stringify({ created: created.length, month: monthStart.toISOString().slice(0, 7) }),
hash: 'sha256:' + Math.random().toString(36).slice(2) },
})
return { invoicesCreated: created.length, invoiceNumbers: created }
},
},
{
name: 'update_deal_stage',
description: 'Move a deal to a different pipeline stage. Admin-level: pipeline management.',
inputSchema: {
type: 'object',
properties: {
dealId: { type: 'string' },
stage: { type: 'string', enum: ['lead', 'qualified', 'proposal', 'negotiation', 'won', 'lost'] },
lossReason: { type: 'string', description: 'Required if stage is lost' },
},
required: ['dealId', 'stage'],
},
requiredScopes: ['crm.deal:write', 'mcp:write'],
handler: async (args) => {
const deal = await db.deal.update({
where: { id: String(args.dealId) },
data: { stage: String(args.stage), lossReason: args.lossReason ? String(args.lossReason) : null },
})
await db.auditLog.create({
data: { actor: 'mcp-agent', action: 'mcp.deal:updateStage', entity: 'Deal', entityId: deal.id,
after: JSON.stringify({ stage: deal.stage }),
hash: 'sha256:' + Math.random().toString(36).slice(2) },
})
return { id: deal.id, stage: deal.stage }
},
},
]
// JSON-RPC handler