Skip to main content
Glama

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

TableJSON Schema
NameRequiredDescriptionDefault
actionYesAction to perform
entityNoEntity type (required for list/get, not used for coin_stats)
idNoEntity ID (required for action=get)
statusNoFilter by payment status
memberNoFilter by member ID (payments, coin_stats)
companyNoFilter by company ID (payments, coin_stats)
documentTypeNoFilter by document type (payments only)
dateFromNoPayments issued on/after this ISO date
dateToNoPayments issued before this ISO date
sortNoSort expression, e.g. 'createdAt,desc' (payments, plans)
monthNoMonth for coin_stats (e.g. '2026-03')
cursorNextNoCursor token for next page of results
limitNoResults per page (max 50, default 50)

Implementation Reference

  • 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,
          };
        }
      }
    );
  • 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)"),
    },
  • 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,
            };
          }
        }
      );
    }
Install Server

Other Tools

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/MrBoor/officernd-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server