@hisaabo/mcp 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/index.ts ADDED
@@ -0,0 +1,64 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Hisaabo MCP Server
4
+ *
5
+ * Exposes Hisaabo invoicing data and operations as MCP tools and resources.
6
+ * Designed for use with Claude Desktop, OpenClaw, and any MCP-compatible host.
7
+ *
8
+ * Required environment variables:
9
+ * HISAABO_API_URL — Base URL of the Hisaabo API (default: http://localhost:3000)
10
+ * HISAABO_API_KEY — Session ID obtained from `hisaabo login` (Bearer token)
11
+ * HISAABO_TENANT_ID — Tenant (organization) UUID
12
+ * HISAABO_BUSINESS_ID — Active business UUID
13
+ *
14
+ * Usage in Claude Desktop claude_desktop_config.json:
15
+ * {
16
+ * "mcpServers": {
17
+ * "hisaabo": {
18
+ * "command": "npx",
19
+ * "args": ["@hisaabo/mcp"],
20
+ * "env": {
21
+ * "HISAABO_API_URL": "http://localhost:3000",
22
+ * "HISAABO_API_KEY": "<session-id-from-hisaabo-login>",
23
+ * "HISAABO_TENANT_ID": "<tenant-uuid>",
24
+ * "HISAABO_BUSINESS_ID": "<business-uuid>"
25
+ * }
26
+ * }
27
+ * }
28
+ * }
29
+ */
30
+
31
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
32
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
33
+ import { HisaaboClient } from "./client.js";
34
+ import { registerTools } from "./server.js";
35
+
36
+ function requireEnv(name: string): string {
37
+ const val = process.env[name];
38
+ if (!val) {
39
+ process.stderr.write(
40
+ `[hisaabo-mcp] Error: Required environment variable "${name}" is not set.\n` +
41
+ `[hisaabo-mcp] Run "hisaabo whoami --json" to get all required values.\n`
42
+ );
43
+ process.exit(1);
44
+ }
45
+ return val;
46
+ }
47
+
48
+ const config = {
49
+ apiUrl: process.env.HISAABO_API_URL ?? "http://localhost:3000",
50
+ token: requireEnv("HISAABO_API_KEY"),
51
+ tenantId: requireEnv("HISAABO_TENANT_ID"),
52
+ businessId: requireEnv("HISAABO_BUSINESS_ID"),
53
+ };
54
+
55
+ const client = new HisaaboClient(config);
56
+ const server = new McpServer({
57
+ name: "hisaabo",
58
+ version: "0.1.0",
59
+ });
60
+
61
+ registerTools(server, client);
62
+
63
+ const transport = new StdioServerTransport();
64
+ await server.connect(transport);
@@ -0,0 +1,49 @@
1
+ /**
2
+ * MCP tool error normalization.
3
+ *
4
+ * All tool handlers are wrapped with wrapTool() to ensure errors are returned
5
+ * as structured MCP content rather than thrown exceptions. The MCP SDK itself
6
+ * handles uncaught exceptions, but we want to give the AI agent a useful, plain
7
+ * English message rather than a raw JSON error envelope.
8
+ */
9
+
10
+ import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
11
+ import { HisaaboApiError, formatHisaaboError, type HisaaboError } from "../client.js";
12
+
13
+ type ToolHandler<T> = (input: T) => Promise<CallToolResult>;
14
+
15
+ /**
16
+ * Wrap a tool handler in error normalization.
17
+ *
18
+ * - Successful calls pass through unchanged.
19
+ * - HisaaboApiError is translated to a structured, agent-readable error message.
20
+ * - Any other thrown error is collapsed to a safe api_error (no stack traces exposed).
21
+ */
22
+ export function wrapTool<T>(handler: ToolHandler<T>): ToolHandler<T> {
23
+ return async (input: T): Promise<CallToolResult> => {
24
+ try {
25
+ return await handler(input);
26
+ } catch (err) {
27
+ const hisaaboErr = toHisaaboError(err);
28
+ return {
29
+ isError: true,
30
+ content: [
31
+ {
32
+ type: "text" as const,
33
+ text: formatHisaaboError(hisaaboErr),
34
+ },
35
+ ],
36
+ };
37
+ }
38
+ };
39
+ }
40
+
41
+ function toHisaaboError(err: unknown): HisaaboError {
42
+ if (err instanceof HisaaboApiError) {
43
+ return err.hisaaboError;
44
+ }
45
+ // Network or unexpected error — collapse to api_error, never surface stack traces
46
+ const message =
47
+ err instanceof Error ? err.message : "An unexpected error occurred. Check server logs.";
48
+ return { code: "api_error", message };
49
+ }
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Pagination helpers for MCP tool responses.
3
+ *
4
+ * AI agents process the full result set returned by a tool call, so list tools
5
+ * enforce a hard cap per page to keep context window usage bounded. See ADR-005.
6
+ *
7
+ * The default cap is 25 records. Operators may raise it (max 50) via the
8
+ * HISAABO_MCP_PAGE_SIZE environment variable.
9
+ */
10
+
11
+ const envCap = parseInt(process.env.HISAABO_MCP_PAGE_SIZE ?? "25", 10);
12
+
13
+ /** Maximum records per tool call response. */
14
+ export const MAX_PAGE_SIZE = Math.min(Math.max(isNaN(envCap) ? 25 : envCap, 1), 50);
15
+
16
+ /**
17
+ * Add pagination metadata to a list result so agents know whether to
18
+ * call the tool again with a higher page number.
19
+ */
20
+ export function withPaginationMeta<T>(
21
+ result: { data: T[]; total: number; page: number; limit: number },
22
+ ): { data: T[]; total: number; page: number; limit: number; hasMore: boolean } {
23
+ return {
24
+ ...result,
25
+ hasMore: result.total > result.page * result.limit,
26
+ };
27
+ }
28
+
29
+ /**
30
+ * Build the input for a list call with the enforced page size.
31
+ * Agents pass page (1-indexed); this returns the full input object.
32
+ */
33
+ export function pageInput(page: number): { page: number; limit: number } {
34
+ return { page, limit: MAX_PAGE_SIZE };
35
+ }
@@ -0,0 +1,211 @@
1
+ /**
2
+ * MCP Resource registrations.
3
+ *
4
+ * Resources are read-only data snapshots that MCP hosts (e.g. Claude Desktop)
5
+ * can load into the agent's context window without the agent explicitly calling
6
+ * a tool. They are fetched on demand, not pushed proactively.
7
+ *
8
+ * Resources registered:
9
+ * business://current — Active business profile (name, GSTIN, currency, etc.)
10
+ * parties://customers — Top 50 customers by name (for quick lookup)
11
+ * parties://suppliers — Top 50 suppliers by name
12
+ * items://inventory — All inventory items with current stock
13
+ * invoices://recent — Last 10 invoices (quick status overview)
14
+ * dashboard://summary — Current FY financial summary
15
+ * bank://accounts — Bank account list with current balances
16
+ * shipments://recent — Last 10 shipments
17
+ * targets://active — Active sales targets with progress
18
+ *
19
+ * Cache guidance:
20
+ * - business://current: long cache — changes rarely
21
+ * - parties/*: medium cache — new parties added occasionally
22
+ * - items://inventory: short cache — stock changes with every invoice
23
+ * - invoices://recent: short cache — changes with every new invoice
24
+ * - dashboard://summary: no-cache — changes with every transaction
25
+ * - bank://accounts: short cache — balance changes with every payment
26
+ * - shipments://recent: short cache — changes with every new shipment
27
+ * - targets://active: medium cache — progress changes with every sale
28
+ */
29
+
30
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
31
+ import type { HisaaboClient } from "../client.js";
32
+
33
+ export function registerResources(server: McpServer, client: HisaaboClient) {
34
+
35
+ // ── business://current ──────────────────────────────────────────────────
36
+ // Active business profile. Load this to understand the business context:
37
+ // name, GSTIN, GST registration type, currency, financial year start, etc.
38
+ server.resource(
39
+ "business_current",
40
+ "business://current",
41
+ async (_uri) => {
42
+ const biz = await client.business.get();
43
+ return {
44
+ contents: [{
45
+ uri: "business://current",
46
+ text: JSON.stringify(biz, null, 2),
47
+ mimeType: "application/json",
48
+ }],
49
+ };
50
+ }
51
+ );
52
+
53
+ // ── parties://customers ─────────────────────────────────────────────────
54
+ // Top 50 customers sorted by name. Includes id, name, phone, balance.
55
+ // Useful for seeding context before invoice creation or payment recording.
56
+ server.resource(
57
+ "parties_customers",
58
+ "parties://customers",
59
+ async (_uri) => {
60
+ const result = await client.party.list({
61
+ type: "customer",
62
+ sortBy: "name",
63
+ sortDir: "asc",
64
+ limit: 50,
65
+ page: 1,
66
+ });
67
+ return {
68
+ contents: [{
69
+ uri: "parties://customers",
70
+ text: JSON.stringify(result.data, null, 2),
71
+ mimeType: "application/json",
72
+ }],
73
+ };
74
+ }
75
+ );
76
+
77
+ // ── parties://suppliers ─────────────────────────────────────────────────
78
+ // Top 50 suppliers sorted by name. Includes id, name, phone, balance.
79
+ server.resource(
80
+ "parties_suppliers",
81
+ "parties://suppliers",
82
+ async (_uri) => {
83
+ const result = await client.party.list({
84
+ type: "supplier",
85
+ sortBy: "name",
86
+ sortDir: "asc",
87
+ limit: 50,
88
+ page: 1,
89
+ });
90
+ return {
91
+ contents: [{
92
+ uri: "parties://suppliers",
93
+ text: JSON.stringify(result.data, null, 2),
94
+ mimeType: "application/json",
95
+ }],
96
+ };
97
+ }
98
+ );
99
+
100
+ // ── items://inventory ───────────────────────────────────────────────────
101
+ // Inventory items with current stock. Load this to check what products are
102
+ // available, their prices, and which are low on stock.
103
+ server.resource(
104
+ "items_inventory",
105
+ "items://inventory",
106
+ async (_uri) => {
107
+ // Fetch up to 100 items — for context seeding, not exhaustive listing
108
+ const result = await client.item.list({ limit: 100, page: 1 });
109
+ return {
110
+ contents: [{
111
+ uri: "items://inventory",
112
+ text: JSON.stringify(result.data, null, 2),
113
+ mimeType: "application/json",
114
+ }],
115
+ };
116
+ }
117
+ );
118
+
119
+ // ── invoices://recent ───────────────────────────────────────────────────
120
+ // The 10 most recent sale invoices. Load this for a quick status overview.
121
+ server.resource(
122
+ "invoices_recent",
123
+ "invoices://recent",
124
+ async (_uri) => {
125
+ const result = await client.invoice.list({
126
+ type: "sale",
127
+ limit: 10,
128
+ page: 1,
129
+ });
130
+ return {
131
+ contents: [{
132
+ uri: "invoices://recent",
133
+ text: JSON.stringify(result.data, null, 2),
134
+ mimeType: "application/json",
135
+ }],
136
+ };
137
+ }
138
+ );
139
+
140
+ // ── dashboard://summary ─────────────────────────────────────────────────
141
+ // Current financial year summary: total sales, receivables, payables, cash.
142
+ // This is the same data as dashboard_summary tool with period='this-fy'.
143
+ server.resource(
144
+ "dashboard_summary",
145
+ "dashboard://summary",
146
+ async (_uri) => {
147
+ // No date range = API defaults to current financial year
148
+ const summary = await client.dashboard.summary();
149
+ return {
150
+ contents: [{
151
+ uri: "dashboard://summary",
152
+ text: JSON.stringify(summary, null, 2),
153
+ mimeType: "application/json",
154
+ }],
155
+ };
156
+ }
157
+ );
158
+
159
+ // ── bank://accounts ──────────────────────────────────────────────────────
160
+ // All bank and cash accounts with current balances. Load this to see the
161
+ // business's financial position across all accounts.
162
+ server.resource(
163
+ "bank_accounts",
164
+ "bank://accounts",
165
+ async (_uri) => {
166
+ const accounts = await client.bankAccount.list();
167
+ return {
168
+ contents: [{
169
+ uri: "bank://accounts",
170
+ text: JSON.stringify(accounts, null, 2),
171
+ mimeType: "application/json",
172
+ }],
173
+ };
174
+ }
175
+ );
176
+
177
+ // ── shipments://recent ───────────────────────────────────────────────────
178
+ // The 10 most recent shipments. Load this for a quick logistics overview.
179
+ server.resource(
180
+ "shipments_recent",
181
+ "shipments://recent",
182
+ async (_uri) => {
183
+ const result = await client.shipment.list({ page: 1, limit: 10 });
184
+ return {
185
+ contents: [{
186
+ uri: "shipments://recent",
187
+ text: JSON.stringify(result.data, null, 2),
188
+ mimeType: "application/json",
189
+ }],
190
+ };
191
+ }
192
+ );
193
+
194
+ // ── targets://active ────────────────────────────────────────────────────
195
+ // Active sales targets (whose period includes today) with real-time progress.
196
+ // Load this to understand team sales goals and performance at a glance.
197
+ server.resource(
198
+ "targets_active",
199
+ "targets://active",
200
+ async (_uri) => {
201
+ const targets = await client.target.list({ active: true, withProgress: true });
202
+ return {
203
+ contents: [{
204
+ uri: "targets://active",
205
+ text: JSON.stringify(targets, null, 2),
206
+ mimeType: "application/json",
207
+ }],
208
+ };
209
+ }
210
+ );
211
+ }
package/src/server.ts ADDED
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Tool and resource registration aggregator.
3
+ *
4
+ * All tool and resource registrations flow through here so that index.ts
5
+ * stays minimal and individual tool files stay focused on their domain.
6
+ */
7
+
8
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
9
+ import type { HisaaboClient } from "./client.js";
10
+ import { registerInvoiceTools } from "./tools/invoice.js";
11
+ import { registerPartyTools } from "./tools/party.js";
12
+ import { registerItemTools } from "./tools/item.js";
13
+ import { registerPaymentTools } from "./tools/payment.js";
14
+ import { registerExpenseTools } from "./tools/expense.js";
15
+ import { registerDashboardTools } from "./tools/dashboard.js";
16
+ import { registerGstTools } from "./tools/gst.js";
17
+ import { registerShipmentTools } from "./tools/shipment.js";
18
+ import { registerBankAccountTools } from "./tools/bankAccount.js";
19
+ import { registerReportTools } from "./tools/reports.js";
20
+ import { registerStoreTools } from "./tools/store.js";
21
+ import { registerTargetTools } from "./tools/target.js";
22
+ import { registerImportTools } from "./tools/import.js";
23
+ import { registerResources } from "./resources/index.js";
24
+
25
+ export function registerTools(server: McpServer, client: HisaaboClient): void {
26
+ // Core business operations
27
+ registerInvoiceTools(server, client);
28
+ registerPartyTools(server, client);
29
+ registerItemTools(server, client);
30
+ registerPaymentTools(server, client);
31
+ registerExpenseTools(server, client);
32
+
33
+ // Analytics and reporting
34
+ registerDashboardTools(server, client);
35
+ registerGstTools(server, client);
36
+ registerReportTools(server, client);
37
+
38
+ // Logistics
39
+ registerShipmentTools(server, client);
40
+
41
+ // Financial accounts
42
+ registerBankAccountTools(server, client);
43
+
44
+ // Online store
45
+ registerStoreTools(server, client);
46
+
47
+ // Sales targets
48
+ registerTargetTools(server, client);
49
+
50
+ // Data import
51
+ registerImportTools(server, client);
52
+
53
+ // Read-only context resources
54
+ registerResources(server, client);
55
+ }
@@ -0,0 +1,200 @@
1
+ /**
2
+ * Bank account tools — manage bank/cash accounts and transactions.
3
+ *
4
+ * Tools registered:
5
+ * bank_account_list — list all bank and cash accounts with balances
6
+ * bank_account_get — get account details with recent transactions
7
+ * bank_account_create — create a new bank or cash account
8
+ * bank_account_transfer — transfer funds between two accounts
9
+ * bank_account_transactions — list transactions for an account
10
+ * bank_account_summary — get total balance across all accounts
11
+ */
12
+
13
+ import { z } from "zod";
14
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
15
+ import type { HisaaboClient } from "../client.js";
16
+ import { wrapTool } from "../lib/errors.js";
17
+ import { MAX_PAGE_SIZE, withPaginationMeta } from "../lib/pagination.js";
18
+
19
+ const ACCOUNT_TYPES = ["savings", "current", "cash", "credit", "other"] as const;
20
+
21
+ export function registerBankAccountTools(server: McpServer, client: HisaaboClient) {
22
+
23
+ server.tool(
24
+ "bank_account_list",
25
+ [
26
+ "List all bank and cash accounts for the active business, including current balances.",
27
+ "Use this to find account UUIDs before recording payments or transfers.",
28
+ "The 'currentBalance' field reflects the running balance after all recorded transactions.",
29
+ "The default account (isDefault=true) is used automatically when no account is specified in payment_create.",
30
+ ].join(" "),
31
+ {},
32
+ wrapTool(async (_input) => {
33
+ const accounts = await client.bankAccount.list();
34
+ return {
35
+ content: [{
36
+ type: "text" as const,
37
+ text: JSON.stringify(accounts, null, 2),
38
+ }],
39
+ };
40
+ })
41
+ );
42
+
43
+ server.tool(
44
+ "bank_account_get",
45
+ [
46
+ "Get details of a single bank account including its 20 most recent transactions.",
47
+ "Use this to check an account's running balance and recent activity.",
48
+ ].join(" "),
49
+ {
50
+ account_id: z.string().uuid()
51
+ .describe("Bank account UUID from bank_account_list."),
52
+ },
53
+ wrapTool(async (input) => {
54
+ const account = await client.bankAccount.get(input.account_id);
55
+ return {
56
+ content: [{
57
+ type: "text" as const,
58
+ text: JSON.stringify(account, null, 2),
59
+ }],
60
+ };
61
+ })
62
+ );
63
+
64
+ server.tool(
65
+ "bank_account_create",
66
+ [
67
+ "Create a new bank account or cash account for the business.",
68
+ "Use account_type='cash' for a physical cash register/petty cash account.",
69
+ "Use account_type='savings' or 'current' for bank accounts.",
70
+ "opening_balance sets the starting balance (e.g. the balance when you started using Hisaabo).",
71
+ "Set is_default=true to make this the default account for payment recording.",
72
+ ].join(" "),
73
+ {
74
+ account_name: z.string().min(1).max(200)
75
+ .describe("Display name, e.g. 'HDFC Current Account' or 'Petty Cash'."),
76
+ account_type: z.enum(ACCOUNT_TYPES)
77
+ .describe("'savings', 'current', 'cash' (petty cash/physical cash), 'credit', or 'other'."),
78
+ account_number: z.string().max(34).optional()
79
+ .describe("Bank account number (not required for cash accounts)."),
80
+ ifsc: z.string().max(11).optional()
81
+ .describe("IFSC code, e.g. 'HDFC0001234'. Required for bank transfers."),
82
+ bank_name: z.string().max(200).optional()
83
+ .describe("Bank name, e.g. 'HDFC Bank', 'State Bank of India'."),
84
+ opening_balance: z.string().regex(/^-?\d+(\.\d{1,2})?$/).optional()
85
+ .describe("Opening balance as decimal string. Default '0'. Use the current account balance when onboarding."),
86
+ is_default: z.boolean().optional()
87
+ .describe("If true, this becomes the default account. Any previous default is cleared."),
88
+ },
89
+ wrapTool(async (input) => {
90
+ const account = await client.bankAccount.create({
91
+ accountName: input.account_name,
92
+ accountType: input.account_type,
93
+ accountNumber: input.account_number,
94
+ ifsc: input.ifsc,
95
+ bankName: input.bank_name,
96
+ openingBalance: input.opening_balance,
97
+ isDefault: input.is_default,
98
+ });
99
+ return {
100
+ content: [{
101
+ type: "text" as const,
102
+ text: JSON.stringify(account, null, 2),
103
+ }],
104
+ };
105
+ })
106
+ );
107
+
108
+ server.tool(
109
+ "bank_account_transfer",
110
+ [
111
+ "Transfer funds between two of the business's bank/cash accounts.",
112
+ "Creates a withdrawal transaction on the source account and a deposit on the destination.",
113
+ "Use this to record moving cash to the bank, or inter-account transfers.",
114
+ "Example: transfer from 'Petty Cash' to 'HDFC Current Account' to replenish cash.",
115
+ ].join(" "),
116
+ {
117
+ from_account_id: z.string().uuid()
118
+ .describe("UUID of the source account (funds leave this account)."),
119
+ to_account_id: z.string().uuid()
120
+ .describe("UUID of the destination account (funds arrive here). Must differ from from_account_id."),
121
+ amount: z.string().regex(/^\d+(\.\d{1,2})?$/)
122
+ .describe("Transfer amount as decimal string, e.g. '5000.00'."),
123
+ description: z.string().max(500).optional()
124
+ .describe("Description of the transfer, e.g. 'Monthly cash deposit to bank'."),
125
+ transaction_date: z.string().datetime().optional()
126
+ .describe("Date of the transfer (ISO 8601). Defaults to today."),
127
+ },
128
+ wrapTool(async (input) => {
129
+ const result = await client.bankAccount.transfer({
130
+ fromAccountId: input.from_account_id,
131
+ toAccountId: input.to_account_id,
132
+ amount: input.amount,
133
+ description: input.description,
134
+ transactionDate: input.transaction_date,
135
+ });
136
+ return {
137
+ content: [{
138
+ type: "text" as const,
139
+ text: JSON.stringify(result, null, 2),
140
+ }],
141
+ };
142
+ })
143
+ );
144
+
145
+ server.tool(
146
+ "bank_account_transactions",
147
+ [
148
+ "List transactions for a specific bank or cash account with date and type filters.",
149
+ "Each transaction includes a 'balanceAfter' field showing the running balance.",
150
+ "Use type='deposit' to see only incoming funds, 'withdrawal' for outgoing, 'transfer' for inter-account moves.",
151
+ ].join(" "),
152
+ {
153
+ account_id: z.string().uuid()
154
+ .describe("Bank account UUID from bank_account_list."),
155
+ from_date: z.string().datetime().optional()
156
+ .describe("Start date (ISO 8601)."),
157
+ to_date: z.string().datetime().optional()
158
+ .describe("End date (ISO 8601)."),
159
+ type: z.enum(["deposit", "withdrawal", "transfer"]).optional()
160
+ .describe("Filter by transaction type."),
161
+ page: z.number().int().min(1).default(1)
162
+ .describe("Page number for pagination."),
163
+ },
164
+ wrapTool(async (input) => {
165
+ const result = await client.bankAccount.listTransactions({
166
+ bankAccountId: input.account_id,
167
+ fromDate: input.from_date,
168
+ toDate: input.to_date,
169
+ type: input.type,
170
+ page: input.page,
171
+ limit: MAX_PAGE_SIZE,
172
+ });
173
+ return {
174
+ content: [{
175
+ type: "text" as const,
176
+ text: JSON.stringify(withPaginationMeta(result), null, 2),
177
+ }],
178
+ };
179
+ })
180
+ );
181
+
182
+ server.tool(
183
+ "bank_account_summary",
184
+ [
185
+ "Get a summary of total funds across all bank and cash accounts.",
186
+ "Returns totalBalance (all accounts), cashInHand (cash-type accounts only), bankBalance (non-cash accounts), and account count.",
187
+ "Use this to quickly answer 'How much money do we have in total?' or 'What is our cash in hand?'",
188
+ ].join(" "),
189
+ {},
190
+ wrapTool(async (_input) => {
191
+ const summary = await client.bankAccount.summary();
192
+ return {
193
+ content: [{
194
+ type: "text" as const,
195
+ text: JSON.stringify(summary, null, 2),
196
+ }],
197
+ };
198
+ })
199
+ );
200
+ }