billing
Query billing and financial data in OfficeRnD to retrieve payments, fees, plans, and coin balances with filtering and pagination.
Instructions
Query billing/financial data in OfficeRnD.
action=list: List entities with optional filters and pagination (max 50 per page). action=get: Get a single entity by ID (payments, plans). action=coin_stats: Get coin/credit balance for a member or company in a given month.
Entity-specific filters when listing:
payments: status, member, company, documentType (creditNote|invoice|overpayment|paymentCharge), dateFrom, dateTo (ISO dates), sort (e.g. 'createdAt,desc')
fees: (pagination only)
plans: sort
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| action | Yes | Action to perform | |
| entity | No | Entity type (required for list/get, not used for coin_stats) | |
| id | No | Entity ID (required for action=get) | |
| status | No | Filter by payment status | |
| member | No | Filter by member ID (payments, coin_stats) | |
| company | No | Filter by company ID (payments, coin_stats) | |
| documentType | No | Filter by document type (payments only) | |
| dateFrom | No | Payments issued on/after this ISO date | |
| dateTo | No | Payments issued before this ISO date | |
| sort | No | Sort expression, e.g. 'createdAt,desc' (payments, plans) | |
| month | No | Month for coin_stats (e.g. '2026-03') | |
| cursorNext | No | Cursor token for next page of results | |
| limit | No | Results per page (max 50, default 50) |
Implementation Reference
- src/tools/billing.ts:175-281 (handler)The main handler function for the 'billing' MCP tool, which implements list, get, and coin_stats actions.
async ({ action, entity, id, status, member, company, documentType, dateFrom, dateTo, sort, month, cursorNext, limit, }) => { try { // coin_stats if (action === "coin_stats") { const params: Record<string, string> = {}; if (member) params["memberId"] = member; if (company) params["companyId"] = company; if (month) params["month"] = month; const stats = await apiGet<CoinStats>("/coins/stats", params); const lines: string[] = []; if (stats.balance !== undefined) lines.push(`Balance: ${stats.balance}`); if (stats.earned !== undefined) lines.push(`Earned: ${stats.earned}`); if (stats.spent !== undefined) lines.push(`Spent: ${stats.spent}`); if (lines.length === 0) lines.push(JSON.stringify(stats, null, 2)); return { content: [{ type: "text" as const, text: lines.join("\n") }] }; } if (!entity) { return { content: [{ type: "text" as const, text: "entity is required for list/get actions." }], isError: true, }; } const cfg = ENTITIES[entity]; // get if (action === "get") { if (!cfg.getPath) { return { content: [{ type: "text" as const, text: `Entity "${entity}" does not support get by ID.` }], isError: true, }; } if (!id) { return { content: [{ type: "text" as const, text: "id is required for action=get." }], isError: true, }; } const item = await apiGet<Record<string, unknown>>(`${cfg.getPath}/${id}`); return { content: [{ type: "text" as const, text: cfg.formatter(item) }] }; } // list const params: Record<string, string> = {}; if (cursorNext) params["$cursorNext"] = cursorNext; if (limit) params["$limit"] = limit; switch (entity) { case "payments": if (status) params["status"] = status; if (member) params["member"] = member; if (company) params["company"] = company; if (documentType) params["documentType"] = documentType; if (dateFrom) params["date[$gte]"] = dateFrom; if (dateTo) params["date[$lt]"] = dateTo; if (sort) params["$sort"] = sort; break; case "plans": if (sort) params["$sort"] = sort; break; } const data = await apiGet<PaginatedResponse<Record<string, unknown>>>(cfg.listPath, params); if (data.results.length === 0) { return { content: [{ type: "text" as const, text: `No ${cfg.label} found.` }] }; } const text = data.results.map(cfg.formatter).join("\n---\n"); let result = `Found ${data.results.length} ${cfg.label} (range ${data.rangeStart}-${data.rangeEnd}):\n\n${text}`; if (data.cursorNext) { result += `\n\n[More results available — use cursorNext: "${data.cursorNext}"]`; } return { content: [{ type: "text" as const, text: result }] }; } catch (error) { return { content: [ { type: "text" as const, text: `Error querying billing: ${error instanceof Error ? error.message : String(error)}`, }, ], isError: true, }; } } ); - src/tools/billing.ts:121-173 (schema)Zod schema definition for the input arguments of the 'billing' tool.
inputSchema: { action: z .enum(["list", "get", "coin_stats"]) .describe("Action to perform"), entity: z .enum(["payments", "fees", "plans"]) .optional() .describe("Entity type (required for list/get, not used for coin_stats)"), id: z .string() .optional() .describe("Entity ID (required for action=get)"), status: z .string() .optional() .describe("Filter by payment status"), member: z .string() .optional() .describe("Filter by member ID (payments, coin_stats)"), company: z .string() .optional() .describe("Filter by company ID (payments, coin_stats)"), documentType: z .enum(["creditNote", "invoice", "overpayment", "paymentCharge"]) .optional() .describe("Filter by document type (payments only)"), dateFrom: z .string() .optional() .describe("Payments issued on/after this ISO date"), dateTo: z .string() .optional() .describe("Payments issued before this ISO date"), sort: z .string() .optional() .describe("Sort expression, e.g. 'createdAt,desc' (payments, plans)"), month: z .string() .optional() .describe("Month for coin_stats (e.g. '2026-03')"), cursorNext: z .string() .optional() .describe("Cursor token for next page of results"), limit: z .string() .optional() .describe("Results per page (max 50, default 50)"), }, - src/tools/billing.ts:106-282 (registration)Registration function for the 'billing' tool using server.registerTool.
export function registerBillingTool(server: McpServer): void { server.registerTool( "billing", { title: "Billing", description: `Query billing/financial data in OfficeRnD. action=list: List entities with optional filters and pagination (max 50 per page). action=get: Get a single entity by ID (payments, plans). action=coin_stats: Get coin/credit balance for a member or company in a given month. Entity-specific filters when listing: - payments: status, member, company, documentType (creditNote|invoice|overpayment|paymentCharge), dateFrom, dateTo (ISO dates), sort (e.g. 'createdAt,desc') - fees: (pagination only) - plans: sort`, inputSchema: { action: z .enum(["list", "get", "coin_stats"]) .describe("Action to perform"), entity: z .enum(["payments", "fees", "plans"]) .optional() .describe("Entity type (required for list/get, not used for coin_stats)"), id: z .string() .optional() .describe("Entity ID (required for action=get)"), status: z .string() .optional() .describe("Filter by payment status"), member: z .string() .optional() .describe("Filter by member ID (payments, coin_stats)"), company: z .string() .optional() .describe("Filter by company ID (payments, coin_stats)"), documentType: z .enum(["creditNote", "invoice", "overpayment", "paymentCharge"]) .optional() .describe("Filter by document type (payments only)"), dateFrom: z .string() .optional() .describe("Payments issued on/after this ISO date"), dateTo: z .string() .optional() .describe("Payments issued before this ISO date"), sort: z .string() .optional() .describe("Sort expression, e.g. 'createdAt,desc' (payments, plans)"), month: z .string() .optional() .describe("Month for coin_stats (e.g. '2026-03')"), cursorNext: z .string() .optional() .describe("Cursor token for next page of results"), limit: z .string() .optional() .describe("Results per page (max 50, default 50)"), }, }, async ({ action, entity, id, status, member, company, documentType, dateFrom, dateTo, sort, month, cursorNext, limit, }) => { try { // coin_stats if (action === "coin_stats") { const params: Record<string, string> = {}; if (member) params["memberId"] = member; if (company) params["companyId"] = company; if (month) params["month"] = month; const stats = await apiGet<CoinStats>("/coins/stats", params); const lines: string[] = []; if (stats.balance !== undefined) lines.push(`Balance: ${stats.balance}`); if (stats.earned !== undefined) lines.push(`Earned: ${stats.earned}`); if (stats.spent !== undefined) lines.push(`Spent: ${stats.spent}`); if (lines.length === 0) lines.push(JSON.stringify(stats, null, 2)); return { content: [{ type: "text" as const, text: lines.join("\n") }] }; } if (!entity) { return { content: [{ type: "text" as const, text: "entity is required for list/get actions." }], isError: true, }; } const cfg = ENTITIES[entity]; // get if (action === "get") { if (!cfg.getPath) { return { content: [{ type: "text" as const, text: `Entity "${entity}" does not support get by ID.` }], isError: true, }; } if (!id) { return { content: [{ type: "text" as const, text: "id is required for action=get." }], isError: true, }; } const item = await apiGet<Record<string, unknown>>(`${cfg.getPath}/${id}`); return { content: [{ type: "text" as const, text: cfg.formatter(item) }] }; } // list const params: Record<string, string> = {}; if (cursorNext) params["$cursorNext"] = cursorNext; if (limit) params["$limit"] = limit; switch (entity) { case "payments": if (status) params["status"] = status; if (member) params["member"] = member; if (company) params["company"] = company; if (documentType) params["documentType"] = documentType; if (dateFrom) params["date[$gte]"] = dateFrom; if (dateTo) params["date[$lt]"] = dateTo; if (sort) params["$sort"] = sort; break; case "plans": if (sort) params["$sort"] = sort; break; } const data = await apiGet<PaginatedResponse<Record<string, unknown>>>(cfg.listPath, params); if (data.results.length === 0) { return { content: [{ type: "text" as const, text: `No ${cfg.label} found.` }] }; } const text = data.results.map(cfg.formatter).join("\n---\n"); let result = `Found ${data.results.length} ${cfg.label} (range ${data.rangeStart}-${data.rangeEnd}):\n\n${text}`; if (data.cursorNext) { result += `\n\n[More results available — use cursorNext: "${data.cursorNext}"]`; } return { content: [{ type: "text" as const, text: result }] }; } catch (error) { return { content: [ { type: "text" as const, text: `Error querying billing: ${error instanceof Error ? error.message : String(error)}`, }, ], isError: true, }; } } ); }