Complete MCP coverage: 58 tools, invoice filters, templates, email config, profile

MCP expanded from 49 to 58 tools:
- templates_list, templates_list_by_type, templates_create, templates_update,
  templates_delete — full invoice template CRUD
- businesses_get_email_config, businesses_update_email_config — configure
  per-business Resend API key and sending domain
- profile_get, profile_update — user profile read/write

invoices_list now accepts optional status and clientId filters (e.g. list
all draft invoices, or all invoices for a specific client). Backed by a
new optional input on invoices.getAll in the tRPC router.

https://claude.ai/code/session_014126WHVRT8mftmqkU6dajG
This commit is contained in:
Claude
2026-06-11 05:33:13 +00:00
parent c6b6641dfa
commit c0a333710f
2 changed files with 162 additions and 24 deletions
+131 -4
View File
@@ -367,10 +367,20 @@ function textResult(data: unknown): ToolResult {
const tools = { const tools = {
invoices_list: defineTool({ invoices_list: defineTool({
description: "List invoices for the authenticated beenvoice user.", description: "List invoices for the authenticated user. Optionally filter by status ('draft', 'sent', or 'paid') and/or clientId.",
inputSchema: jsonSchemas.empty, inputSchema: {
schema: z.object({}).optional().default({}), type: "object",
handler: async (_input, caller) => caller.invoices.getAll(), properties: {
status: { type: "string", enum: ["draft", "sent", "paid"], description: "Filter by invoice status" },
clientId: { type: "string", description: "Filter by client ID" },
},
additionalProperties: false,
},
schema: z.object({
status: z.enum(["draft", "sent", "paid"]).optional(),
clientId: z.string().optional(),
}).optional().default({}),
handler: async (input, caller) => caller.invoices.getAll(input ?? {}),
}), }),
invoices_get: defineTool({ invoices_get: defineTool({
description: "Get one invoice by ID.", description: "Get one invoice by ID.",
@@ -859,6 +869,123 @@ const tools = {
schema: z.object({ ids: z.array(z.string()).min(1) }), schema: z.object({ ids: z.array(z.string()).min(1) }),
handler: async (input, caller) => caller.invoices.bulkDelete(input), handler: async (input, caller) => caller.invoices.bulkDelete(input),
}), }),
// ── Invoice Templates ────────────────────────────────────────────────────────
templates_list: defineTool({
description: "List all saved invoice templates (notes and terms). Use these to populate invoice notes/terms fields.",
inputSchema: jsonSchemas.empty,
schema: z.object({}).optional().default({}),
handler: async (_input, caller) => caller.invoiceTemplates.getAll(),
}),
templates_list_by_type: defineTool({
description: "List invoice templates filtered by type: 'notes' for invoice notes, 'terms' for payment terms.",
inputSchema: {
type: "object",
properties: { type: { type: "string", enum: ["notes", "terms"] } },
required: ["type"],
additionalProperties: false,
},
schema: z.object({ type: z.enum(["notes", "terms"]) }),
handler: async (input, caller) => caller.invoiceTemplates.getByType(input),
}),
templates_create: defineTool({
description: "Create an invoice template. Set isDefault=true to automatically apply this template to new invoices of this type.",
inputSchema: {
type: "object",
properties: {
name: { type: "string", minLength: 1, maxLength: 255 },
type: { type: "string", enum: ["notes", "terms"] },
content: { type: "string", minLength: 1 },
isDefault: { type: "boolean" },
},
required: ["name", "content"],
additionalProperties: false,
},
schema: z.object({
name: z.string().min(1).max(255),
type: z.enum(["notes", "terms"]).default("notes"),
content: z.string().min(1),
isDefault: z.boolean().default(false),
}),
handler: async (input, caller) => caller.invoiceTemplates.create(input),
}),
templates_update: defineTool({
description: "Update an existing invoice template by ID.",
inputSchema: {
type: "object",
properties: {
id: { type: "string" },
name: { type: "string", minLength: 1, maxLength: 255 },
type: { type: "string", enum: ["notes", "terms"] },
content: { type: "string", minLength: 1 },
isDefault: { type: "boolean" },
},
required: ["id"],
additionalProperties: false,
},
schema: z.object({
id: z.string(),
name: z.string().min(1).max(255).optional(),
type: z.enum(["notes", "terms"]).optional(),
content: z.string().min(1).optional(),
isDefault: z.boolean().optional(),
}),
handler: async (input, caller) => caller.invoiceTemplates.update(input),
}),
templates_delete: defineTool({
description: "Delete an invoice template by ID.",
inputSchema: jsonSchemas.id,
schema: z.object({ id: z.string() }),
handler: async (input, caller) => caller.invoiceTemplates.delete(input),
}),
// ── Business email config ─────────────────────────────────────────────────────
businesses_get_email_config: defineTool({
description: "Get the email configuration for a business (Resend domain, from-name, and whether an API key is set). The API key itself is never returned.",
inputSchema: jsonSchemas.id,
schema: z.object({ id: z.string() }),
handler: async (input, caller) => caller.businesses.getEmailConfig(input),
}),
businesses_update_email_config: defineTool({
description: "Configure custom email sending for a business via Resend. Set resendApiKey and resendDomain to send invoices from your own domain. Set emailFromName for the sender display name.",
inputSchema: {
type: "object",
properties: {
id: { type: "string" },
resendApiKey: { type: "string", description: "Resend API key (re_...)" },
resendDomain: { type: "string", description: "Verified Resend sending domain (e.g. mail.example.com)" },
emailFromName: { type: "string", description: "Display name for the From field" },
},
required: ["id"],
additionalProperties: false,
},
schema: z.object({
id: z.string(),
resendApiKey: z.string().optional().or(z.literal("")),
resendDomain: z.string().optional().or(z.literal("")),
emailFromName: z.string().optional().or(z.literal("")),
}),
handler: async (input, caller) => caller.businesses.updateEmailConfig(input),
}),
// ── User profile ──────────────────────────────────────────────────────────────
profile_get: defineTool({
description: "Get the authenticated user's profile: id, name, email, and role.",
inputSchema: jsonSchemas.empty,
schema: z.object({}).optional().default({}),
handler: async (_input, caller) => caller.settings.getProfile(),
}),
profile_update: defineTool({
description: "Update the authenticated user's display name.",
inputSchema: {
type: "object",
properties: { name: { type: "string", minLength: 1 } },
required: ["name"],
additionalProperties: false,
},
schema: z.object({ name: z.string().min(1) }),
handler: async (input, caller) => caller.settings.updateProfile(input),
}),
} satisfies Record<string, ToolDefinition>; } satisfies Record<string, ToolDefinition>;
function rpcResult(id: JsonRpcId, result: unknown, init?: ResponseInit) { function rpcResult(id: JsonRpcId, result: unknown, init?: ResponseInit) {
+31 -20
View File
@@ -1,5 +1,5 @@
import { z } from "zod"; import { z } from "zod";
import { desc, eq, inArray } from "drizzle-orm"; import { and, desc, eq, inArray } from "drizzle-orm";
import { createTRPCRouter, protectedProcedure, publicProcedure } from "../trpc"; import { createTRPCRouter, protectedProcedure, publicProcedure } from "../trpc";
import { import {
invoices, invoices,
@@ -114,25 +114,36 @@ const calculateInvoiceTotal = (
}; };
export const invoicesRouter = createTRPCRouter({ export const invoicesRouter = createTRPCRouter({
getAll: protectedProcedure.query(async ({ ctx }) => { getAll: protectedProcedure
try { .input(
return await ctx.db.query.invoices.findMany({ z.object({
where: eq(invoices.createdById, ctx.session.user.id), status: z.enum(["draft", "sent", "paid"]).optional(),
with: { clientId: z.string().optional(),
business: true, }).optional(),
client: true, )
items: true, .query(async ({ ctx, input }) => {
}, try {
orderBy: (invoices, { desc }) => [desc(invoices.issueDate)], const conditions = [eq(invoices.createdById, ctx.session.user.id)];
}); if (input?.status) conditions.push(eq(invoices.status, input.status));
} catch (error) { if (input?.clientId) conditions.push(eq(invoices.clientId, input.clientId));
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR", return await ctx.db.query.invoices.findMany({
message: "Failed to fetch invoices", where: and(...conditions),
cause: error, with: {
}); business: true,
} client: true,
}), items: true,
},
orderBy: (invoices, { desc }) => [desc(invoices.issueDate)],
});
} catch (error) {
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: "Failed to fetch invoices",
cause: error,
});
}
}),
getLineItemHistory: protectedProcedure.query(async ({ ctx }) => { getLineItemHistory: protectedProcedure.query(async ({ ctx }) => {
const userInvoices = await ctx.db const userInvoices = await ctx.db