@bbookmind/mcp 1.0.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.
Files changed (118) hide show
  1. package/.claude-plugin/marketplace.json +27 -0
  2. package/.claude-plugin/plugin.json +18 -0
  3. package/.env.example +15 -0
  4. package/.github/workflows/validate.yml +55 -0
  5. package/CEO_QUICKSTART.md +161 -0
  6. package/CLAUDE.md +30 -0
  7. package/CNAME +1 -0
  8. package/LAUNCH_CONTENT.md +210 -0
  9. package/LICENSE +21 -0
  10. package/README.md +196 -0
  11. package/dist/index.d.ts +3 -0
  12. package/dist/index.d.ts.map +1 -0
  13. package/dist/index.js +283 -0
  14. package/dist/index.js.map +1 -0
  15. package/dist/lib/db.d.ts +6 -0
  16. package/dist/lib/db.d.ts.map +1 -0
  17. package/dist/lib/db.js +209 -0
  18. package/dist/lib/db.js.map +1 -0
  19. package/dist/lib/llm.d.ts +14 -0
  20. package/dist/lib/llm.d.ts.map +1 -0
  21. package/dist/lib/llm.js +110 -0
  22. package/dist/lib/llm.js.map +1 -0
  23. package/dist/lib/qbo.d.ts +35 -0
  24. package/dist/lib/qbo.d.ts.map +1 -0
  25. package/dist/lib/qbo.js +157 -0
  26. package/dist/lib/qbo.js.map +1 -0
  27. package/dist/tools/anomalies.d.ts +3 -0
  28. package/dist/tools/anomalies.d.ts.map +1 -0
  29. package/dist/tools/anomalies.js +142 -0
  30. package/dist/tools/anomalies.js.map +1 -0
  31. package/dist/tools/approveCategories.d.ts +3 -0
  32. package/dist/tools/approveCategories.d.ts.map +1 -0
  33. package/dist/tools/approveCategories.js +41 -0
  34. package/dist/tools/approveCategories.js.map +1 -0
  35. package/dist/tools/categorize.d.ts +3 -0
  36. package/dist/tools/categorize.d.ts.map +1 -0
  37. package/dist/tools/categorize.js +160 -0
  38. package/dist/tools/categorize.js.map +1 -0
  39. package/dist/tools/chaseEmail.d.ts +3 -0
  40. package/dist/tools/chaseEmail.d.ts.map +1 -0
  41. package/dist/tools/chaseEmail.js +29 -0
  42. package/dist/tools/chaseEmail.js.map +1 -0
  43. package/dist/tools/fetchTransactions.d.ts +3 -0
  44. package/dist/tools/fetchTransactions.d.ts.map +1 -0
  45. package/dist/tools/fetchTransactions.js +72 -0
  46. package/dist/tools/fetchTransactions.js.map +1 -0
  47. package/dist/tools/learnRules.d.ts +3 -0
  48. package/dist/tools/learnRules.d.ts.map +1 -0
  49. package/dist/tools/learnRules.js +72 -0
  50. package/dist/tools/learnRules.js.map +1 -0
  51. package/dist/tools/listCompanies.d.ts +3 -0
  52. package/dist/tools/listCompanies.d.ts.map +1 -0
  53. package/dist/tools/listCompanies.js +26 -0
  54. package/dist/tools/listCompanies.js.map +1 -0
  55. package/dist/tools/monthlyClose.d.ts +3 -0
  56. package/dist/tools/monthlyClose.d.ts.map +1 -0
  57. package/dist/tools/monthlyClose.js +92 -0
  58. package/dist/tools/monthlyClose.js.map +1 -0
  59. package/dist/tools/reconcile.d.ts +3 -0
  60. package/dist/tools/reconcile.d.ts.map +1 -0
  61. package/dist/tools/reconcile.js +86 -0
  62. package/dist/tools/reconcile.js.map +1 -0
  63. package/dist/tools/rejectCategories.d.ts +3 -0
  64. package/dist/tools/rejectCategories.d.ts.map +1 -0
  65. package/dist/tools/rejectCategories.js +36 -0
  66. package/dist/tools/rejectCategories.js.map +1 -0
  67. package/dist/tools/selectCompany.d.ts +3 -0
  68. package/dist/tools/selectCompany.d.ts.map +1 -0
  69. package/dist/tools/selectCompany.js +28 -0
  70. package/dist/tools/selectCompany.js.map +1 -0
  71. package/dist/tools/syncToQbo.d.ts +3 -0
  72. package/dist/tools/syncToQbo.d.ts.map +1 -0
  73. package/dist/tools/syncToQbo.js +53 -0
  74. package/dist/tools/syncToQbo.js.map +1 -0
  75. package/dist/tools/undoCategories.d.ts +3 -0
  76. package/dist/tools/undoCategories.d.ts.map +1 -0
  77. package/dist/tools/undoCategories.js +50 -0
  78. package/dist/tools/undoCategories.js.map +1 -0
  79. package/dist/tools/vendorSearch.d.ts +3 -0
  80. package/dist/tools/vendorSearch.d.ts.map +1 -0
  81. package/dist/tools/vendorSearch.js +39 -0
  82. package/dist/tools/vendorSearch.js.map +1 -0
  83. package/dist/types.d.ts +16 -0
  84. package/dist/types.d.ts.map +1 -0
  85. package/dist/types.js +3 -0
  86. package/dist/types.js.map +1 -0
  87. package/index.html +403 -0
  88. package/install/claude-desktop.json +14 -0
  89. package/install/cline.json +16 -0
  90. package/install/continue.json +14 -0
  91. package/install/cursor.json +14 -0
  92. package/install/goose.yaml +10 -0
  93. package/install/lm-studio.json +14 -0
  94. package/install/vscode-copilot.json +14 -0
  95. package/install/windsurf.json +14 -0
  96. package/mcp-server.toml +30 -0
  97. package/package.json +49 -0
  98. package/server.json +136 -0
  99. package/src/index.ts +313 -0
  100. package/src/lib/db.ts +212 -0
  101. package/src/lib/llm.ts +140 -0
  102. package/src/lib/qbo.ts +187 -0
  103. package/src/tools/anomalies.ts +144 -0
  104. package/src/tools/approveCategories.ts +46 -0
  105. package/src/tools/categorize.ts +166 -0
  106. package/src/tools/chaseEmail.ts +30 -0
  107. package/src/tools/fetchTransactions.ts +79 -0
  108. package/src/tools/learnRules.ts +71 -0
  109. package/src/tools/listCompanies.ts +30 -0
  110. package/src/tools/monthlyClose.ts +95 -0
  111. package/src/tools/reconcile.ts +86 -0
  112. package/src/tools/rejectCategories.ts +43 -0
  113. package/src/tools/selectCompany.ts +33 -0
  114. package/src/tools/syncToQbo.ts +56 -0
  115. package/src/tools/undoCategories.ts +50 -0
  116. package/src/tools/vendorSearch.ts +32 -0
  117. package/src/types.ts +11 -0
  118. package/tsconfig.json +23 -0
@@ -0,0 +1,43 @@
1
+ import { ToolDefinition } from "../types.js";
2
+ import { Database } from "sql.js";
3
+ import { getCurrentCompanyId, auditChange } from "../lib/db.js";
4
+
5
+ export const rejectCategoriesTool: ToolDefinition = {
6
+ name: "bookmind.reject_categorizations",
7
+ description:
8
+ "Reject pending categorization suggestions with feedback. Rejected transactions return to 'uncategorized'. Optional rule learning: set learn_rules=true and provide corrected_category, vendor_pattern, or description_pattern to auto-create a correction rule.",
9
+ schema: {
10
+ type: "object",
11
+ properties: {
12
+ transaction_ids: { type: "array", items: { type: "string" } },
13
+ reason: { type: "string" },
14
+ learn_rules: { type: "boolean", default: false },
15
+ corrected_category: { type: "string" },
16
+ vendor_pattern: { type: "string" },
17
+ description_pattern: { type: "string" },
18
+ },
19
+ required: ["transaction_ids"],
20
+ },
21
+ handler: async (args, db: Database) => {
22
+ const coId = getCurrentCompanyId();
23
+ const { transaction_ids, reason, learn_rules = false, corrected_category, vendor_pattern, description_pattern } = args as any;
24
+
25
+ let updated = 0;
26
+ for (const id of transaction_ids) {
27
+ const old = db.exec(`SELECT status FROM transactions WHERE id=? AND company_id=?`);
28
+ auditChange(db, "transaction", id, "status", "pending", "rejected", `rejection: ${reason || "manual"}`);
29
+ db.exec(`UPDATE transactions SET status='rejected', approved=0, updated_at=datetime('now') WHERE id='${String(id).replace(/'/g, "''")}' AND company_id='${coId}'`);
30
+ updated++;
31
+ }
32
+
33
+ let ruleMsg = "";
34
+ if (learn_rules && corrected_category && (vendor_pattern || description_pattern)) {
35
+ const ruleId = crypto.randomUUID();
36
+ db.run(`INSERT INTO categorization_rules (id, company_id, vendor_pattern, description_pattern, category, confidence, priority, created_at, uses) VALUES (?,?,?,?,?,0.95,10,datetime('now'),0)`,
37
+ [ruleId, coId, vendor_pattern || null, description_pattern || null, corrected_category] as any);
38
+ ruleMsg = `\n✅ Auto-created rule: "${vendor_pattern || description_pattern}" → ${corrected_category} (priority 10)`;
39
+ }
40
+
41
+ return `Rejected ${updated} categorization(s)${reason ? ` — reason: ${reason}` : ""}${ruleMsg}`;
42
+ },
43
+ };
@@ -0,0 +1,33 @@
1
+ import { ToolDefinition } from "../types.js";
2
+ import { Database } from "sql.js";
3
+ import { activeCompanyId } from "../types.js";
4
+
5
+ export const selectCompanyTool: ToolDefinition = {
6
+ name: "bookmind.select_company",
7
+ description:
8
+ "Switch between connected QuickBooks companies. Sets the active company for all subsequent operations. Returns the current company info.",
9
+ schema: {
10
+ type: "object",
11
+ properties: {
12
+ company_id: {
13
+ type: "string",
14
+ description: "Company ID to switch to. Use bookmind.list_companies to see available ones. Set to 'default' for single-company mode.",
15
+ },
16
+ },
17
+ required: ["company_id"],
18
+ },
19
+ handler: async (args, db: Database) => {
20
+ const { company_id } = args as { company_id: string };
21
+
22
+ const rows = db.exec(`SELECT id, name, realm_id FROM companies WHERE id=? OR name=?`);
23
+ const match = rows[0]?.values?.[0];
24
+ if (!match) {
25
+ db.exec(`INSERT INTO companies (id, name) VALUES ('${company_id.replace(/'/g, "''")}', '${company_id.replace(/'/g, "''")}') ON CONFLICT(id) DO UPDATE SET name='${company_id.replace(/'/g, "''")}'`);
26
+ }
27
+
28
+ activeCompanyId.current = company_id;
29
+ const name = match?.[1] || company_id;
30
+ const realmId = match?.[2];
31
+ return `Switched to company "${name}" (ID: ${company_id})${realmId ? `, QBO Realm: ${realmId}` : ""}.\nData is now isolated to company-specific SQLite file.`;
32
+ },
33
+ };
@@ -0,0 +1,56 @@
1
+ import { ToolDefinition } from "../types.js";
2
+ import { Database } from "sql.js";
3
+ import { getCurrentCompanyId } from "../lib/db.js";
4
+ import { qboClient } from "../lib/qbo.js";
5
+
6
+ export const syncToQboTool: ToolDefinition = {
7
+ name: "bookmind.sync_to_qbo",
8
+ description:
9
+ "Push approved categorizations from local SQLite back to QuickBooks Online. Creates journal entries for category reclassifications. Only syncs approved (approved=1) transactions.",
10
+ schema: {
11
+ type: "object",
12
+ properties: {
13
+ transaction_ids: {
14
+ type: "array",
15
+ items: { type: "string" },
16
+ description: "Specific transaction IDs to sync, or ['all'] for all approved uncategorized transactions",
17
+ },
18
+ },
19
+ required: ["transaction_ids"],
20
+ },
21
+ handler: async (args, db: Database) => {
22
+ const coId = getCurrentCompanyId();
23
+ const { transaction_ids } = args as { transaction_ids: string[] };
24
+ const useAll = transaction_ids.length === 1 && transaction_ids[0] === "all";
25
+
26
+ if (!qboClient.isAuthenticated()) return "QBO not connected. Run OAuth flow first.";
27
+
28
+ let rows: any[];
29
+ if (useAll) {
30
+ const r = db.exec(`SELECT id, qbo_id, date, amount, description, vendor, category, account_id FROM transactions WHERE company_id='${coId}' AND approved=1 AND status='categorized' LIMIT 200`);
31
+ rows = r[0]?.values || [];
32
+ } else {
33
+ const quoted = transaction_ids.map((id: string) => `'${id.replace(/'/g, "''")}'`).join(",");
34
+ const r = db.exec(`SELECT id, qbo_id, date, amount, description, vendor, category, account_id FROM transactions WHERE company_id='${coId}' AND id IN (${quoted}) AND approved=1`);
35
+ rows = r[0]?.values || [];
36
+ }
37
+
38
+ let synced = 0;
39
+ for (const [id, qboId, date, amount, description, vendor, category, accountId] of rows) {
40
+ try {
41
+ if (qboId) {
42
+ await qboClient.updateTransaction(String(qboId), {
43
+ Id: String(qboId),
44
+ TotalAmt: parseFloat(String(amount)) || 0,
45
+ Line: [{ DetailType: "DescriptionOnly", Amount: Math.abs(parseFloat(String(amount))) || 0, Description: `[BookMind] ${String(category)} — ${String(description || "")}` }],
46
+ });
47
+ }
48
+ db.exec(`UPDATE transactions SET status='synced', updated_at=datetime('now') WHERE id='${String(id).replace(/'/g, "''")}'`);
49
+ synced++;
50
+ } catch (e: any) {
51
+ return `Sync error at transaction ${id}: ${e.message || "Unknown error"}. Synced ${synced} before failure.`;
52
+ }
53
+ }
54
+ return `Synced ${synced} transaction(s) to QuickBooks Online.`;
55
+ },
56
+ };
@@ -0,0 +1,50 @@
1
+ import { ToolDefinition } from "../types.js";
2
+ import { Database } from "sql.js";
3
+ import { getCurrentCompanyId, auditChange } from "../lib/db.js";
4
+
5
+ export const undoCategoriesTool: ToolDefinition = {
6
+ name: "bookmind.undo_categorize",
7
+ description:
8
+ "Undo categorization changes on transactions. Restore previous category from audit log. Accepts transaction IDs or 'all' to undo all categorizations from today's session.",
9
+ schema: {
10
+ type: "object",
11
+ properties: {
12
+ transaction_ids: {
13
+ type: "array",
14
+ items: { type: "string" },
15
+ description: "Transaction IDs to undo, or ['all'] to undo all today",
16
+ },
17
+ },
18
+ required: ["transaction_ids"],
19
+ },
20
+ handler: async (args, db: Database) => {
21
+ const coId = getCurrentCompanyId();
22
+ const { transaction_ids } = args as { transaction_ids: string[] };
23
+ const useAll = transaction_ids.length === 1 && transaction_ids[0] === "all";
24
+ const today = new Date().toISOString().split("T")[0];
25
+
26
+ let restored = 0;
27
+ if (useAll) {
28
+ const rows: any = db.exec(`SELECT entity_id, old_value FROM audit_log WHERE company_id='${coId}' AND entity_type='transaction' AND field='category' AND created_at >= '${today}' AND old_value IS NOT NULL ORDER BY created_at DESC`);
29
+ const ids: any[] = rows[0]?.values || [];
30
+ const latest: Record<string, string> = {};
31
+ for (const [entityId, oldVal] of ids) { if (!latest[entityId]) latest[entityId] = String(oldVal); }
32
+ for (const [id, oldCategory] of Object.entries(latest)) {
33
+ db.exec(`UPDATE transactions SET category='${String(oldCategory).replace(/'/g, "''")}', status='uncategorized', approved=0, updated_at=datetime('now') WHERE id='${String(id).replace(/'/g, "''")}' AND company_id='${coId}'`);
34
+ auditChange(db, "transaction", id, "category", null, String(oldCategory), "undo");
35
+ restored++;
36
+ }
37
+ } else {
38
+ for (const id of transaction_ids) {
39
+ const rows: any = db.exec(`SELECT old_value FROM audit_log WHERE company_id='${coId}' AND entity_type='transaction' AND entity_id='${String(id).replace(/'/g, "''")}' AND field='category' AND old_value IS NOT NULL ORDER BY created_at DESC LIMIT 1`);
40
+ const oldCat = rows[0]?.values?.[0]?.[0];
41
+ if (oldCat) {
42
+ db.exec(`UPDATE transactions SET category='${String(oldCat).replace(/'/g, "''")}', status='uncategorized', approved=0, updated_at=datetime('now') WHERE id='${id.replace(/'/g, "''")}' AND company_id='${coId}'`);
43
+ auditChange(db, "transaction", id, "category", null, String(oldCat), "undo");
44
+ restored++;
45
+ }
46
+ }
47
+ }
48
+ return `Restored ${restored} transaction(s) to previous categories.`;
49
+ },
50
+ };
@@ -0,0 +1,32 @@
1
+ import { ToolDefinition } from "../types.js";
2
+ import { getCurrentCompanyId } from "../lib/db.js";
3
+
4
+ export const vendorSearchTool: ToolDefinition = {
5
+ name: "bookmind.vendor_search",
6
+ description: "Search transactions by vendor, description, category, or amount. Supports lookback and limit.",
7
+ schema: {
8
+ type: "object",
9
+ properties: {
10
+ transactions: { type: "array", items: { type: "object", properties: { id: { type: "string" }, date: { type: "string" }, amount: { type: "number" }, description: { type: "string" }, vendor: { type: "string" }, category: { type: "string" } }, required: ["id", "date", "amount"] } },
11
+ query: { type: "string" },
12
+ lookback_days: { type: "number", default: 365 },
13
+ limit: { type: "number", default: 50 },
14
+ },
15
+ required: ["transactions", "query"],
16
+ },
17
+ handler: async (args) => {
18
+ const { transactions, query, lookback_days = 365, limit = 50 } = args as any;
19
+ const since = new Date(Date.now() - lookback_days * 24 * 60 * 60 * 1000).toISOString().split("T")[0];
20
+ const q = query.toLowerCase().trim();
21
+ const results = (transactions as any[])
22
+ .filter((t: any) => new Date(t.date) >= new Date(since))
23
+ .filter((t: any) => `${t.vendor || ""} ${t.description || ""} ${t.category || ""} ${t.amount}`.toLowerCase().includes(q))
24
+ .slice(0, limit);
25
+ const total = results.reduce((s: number, t: any) => s + Math.abs(t.amount), 0);
26
+ const byVendor: Record<string, { amount: number; count: number }> = {};
27
+ for (const t of results) { const v = t.vendor || "Unknown"; if (!byVendor[v]) byVendor[v] = { amount: 0, count: 0 }; byVendor[v].amount += Math.abs(t.amount); byVendor[v].count++; }
28
+ const byCategory: Record<string, number> = {};
29
+ for (const t of results) { const c = t.category || "Uncategorized"; byCategory[c] = (byCategory[c] || 0) + Math.abs(t.amount); }
30
+ return `Found ${results.length} matching "${query}" (${lookback_days}d). Total: $${total.toLocaleString(undefined, { minimumFractionDigits: 2 })}\n\nBy Vendor:\n${JSON.stringify(byVendor, null, 2)}\n\nBy Category:\n${JSON.stringify(byCategory, null, 2)}\n\n${JSON.stringify(results.slice(0, 10), null, 2)}${results.length > 10 ? `\n... and ${results.length - 10} more` : ""}`;
31
+ },
32
+ };
package/src/types.ts ADDED
@@ -0,0 +1,11 @@
1
+ import { Database } from "sql.js";
2
+
3
+ export interface ToolDefinition {
4
+ name: string;
5
+ description: string;
6
+ schema: Record<string, unknown>;
7
+ handler: (args: Record<string, unknown>, db: Database) => Promise<string>;
8
+ }
9
+
10
+ export const activeCompanyId: { current: string | null } = { current: null };
11
+ export let companyState: Record<string, { name: string; realmId: string; tokenFile: string }> = {};
package/tsconfig.json ADDED
@@ -0,0 +1,23 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "NodeNext",
5
+ "moduleResolution": "NodeNext",
6
+ "lib": ["ES2022"],
7
+ "outDir": "./dist",
8
+ "rootDir": "./src",
9
+ "strict": true,
10
+ "esModuleInterop": true,
11
+ "skipLibCheck": true,
12
+ "forceConsistentCasingInFileNames": true,
13
+ "declaration": true,
14
+ "declarationMap": true,
15
+ "sourceMap": true,
16
+ "resolveJsonModule": true,
17
+ "isolatedModules": true,
18
+ "noEmit": false,
19
+ "verbatimModuleSyntax": false
20
+ },
21
+ "include": ["src/**/*"],
22
+ "exclude": ["node_modules", "dist"]
23
+ }