@crscreditapi/finstack-mcp-server 0.4.0-0c53dac → 0.6.0-06bd3a8

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/README.md CHANGED
@@ -12,6 +12,9 @@ MCP server exposing CRS B2B (Finstack) customer data over the [Model Context Pro
12
12
  | `finstack_get_customer_invoices` | Paginated list of finalized invoices for one customer with full line-item breakdown. |
13
13
  | `finstack_get_customer_pricing_config` | Full configured pricing state for one customer: products, pricing addendums, and environment-user productPricing. Use to cross-check invoiced rates against contracted pricing. |
14
14
  | `finstack_get_pricing_addendum_text` | Extracted text from a pricing addendum PDF (with or without disclaimer). Server-side parses the PDF and returns plain text. |
15
+ | `finstack_get_customer_cohort` | Bulk, paginated list of all customers with portal status, account type, and sales opportunities (type + stage). For cohort/tracker refreshes instead of per-customer summary calls. Optional `status` / `stage` filters. |
16
+ | `finstack_get_leadiq_analytics` | System-wide LeadsIQ analytics for a trailing window: totals vs previous period, the complete list of customers that pulled (revenue desc), entitled/active/invited split, and enrolled-but-never-pulled accounts. |
17
+ | `finstack_get_leadiq_customer` | One customer's LeadsIQ detail: enrollment state, window/YTD/lifetime stats, match rate and append hit rates, most commonly used pull filters, recent pulls. |
15
18
  | `finstack_get_vendors` | Look up who to contact at an external vendor/partner (credit bureaus and data vendors like Equifax, TransUnion, Experian, CIC). OR-matches `query` across company, name, role, notes, location, and specialty tags; optional `connection` degree filter. |
16
19
 
17
20
  ## Install
@@ -136,6 +139,14 @@ Identify the customer with either `customer_code` or `user_id`. `page` defaults
136
139
 
137
140
  Returns `{products, pricing_addendums, environment_users}`. `pricing_addendums[]` entries carry `id`, `name`, `status`, `quote_id`, timestamps, `has_pdf`, `has_pdf_without_disclaimer`, and `pdf_url` / `pdf_without_disclaimer_url`. The PDF URLs require the chatbase secret to hit directly — use `finstack_get_pricing_addendum_text` to read the contents instead.
138
141
 
142
+ ### `finstack_get_customer_cohort`
143
+
144
+ ```json
145
+ { "page": 1, "per_page": 500, "status": "active", "stage": "closed_won" }
146
+ ```
147
+
148
+ All parameters optional. `page` defaults to `1`; `per_page` defaults to `500`, capped at `1000`. `status` filters by portal account status (`active`, `inactive`, `blacklisted`, `locked`, `obsolete`); `stage` keeps only customers with at least one sales opportunity in that stage (`closed_won` / `closed_lost` / `churned`). Returns `{page, per_page, total_count, total_pages, customers: [...]}` where each customer carries `customer_code`, `name`, `status`, `account_type`, and `sales_opportunities[]`. Read `total_pages` and fetch the remaining pages to cover the whole cohort.
149
+
139
150
  ### `finstack_get_pricing_addendum_text`
140
151
 
141
152
  ```json
@@ -144,6 +155,22 @@ Returns `{products, pricing_addendums, environment_users}`. `pricing_addendums[]
144
155
 
145
156
  `variant` is optional and defaults to `"with_disclaimer"`. Returns `{addendum_id, variant, page_count, text}`. The server fetches the PDF over the chatbase API and extracts text with `pdf-parse`; complex table layouts may run columns together but remain readable for LLM reasoning.
146
157
 
158
+ ### `finstack_get_leadiq_analytics`
159
+
160
+ ```json
161
+ { "days": 30 }
162
+ ```
163
+
164
+ `days` is optional (default `7`, clamped server-side to 1–366). Returns `{period, totals, prev_period_totals, customers, customers_truncated, entitlement, never_pulled}`. `customers[]` is the complete list of accounts with ≥ 1 pull in the window, highest revenue first (`customers_truncated` is only `true` if the 50-account safety cap trimmed it). Definitions match the portal's admin LeadsIQ customer dashboard: a pull is one top-level LeadsIQ job, records count the export leg only, and revenue is the job's cost.
165
+
166
+ ### `finstack_get_leadiq_customer`
167
+
168
+ ```json
169
+ { "customer_code": "CID18068", "days": 30 }
170
+ ```
171
+
172
+ Identify the customer with either `customer_code` or `user_id`; `days` is optional (default `30`) and only scopes the `period` block. Returns `{customer, entitlement, contacts, period, year_to_date, lifetime, top_filters, recent_pulls}`. `top_filters[]` aggregates each pull's submitted form filters (dashboard formatting) with per-filter usage share and most common values.
173
+
147
174
  All tools accept an optional `environment: "prod" | "dev"` (default `"prod"`).
148
175
 
149
176
  ## Development
@@ -161,3 +161,29 @@ export declare function getPricingAddendumText(client: ApiClient, args: {
161
161
  id: number;
162
162
  variant: PricingAddendumVariant;
163
163
  }): Promise<PricingAddendumText>;
164
+ export interface CohortCustomer {
165
+ customer_code: string | null;
166
+ name: string;
167
+ status: string | null;
168
+ account_type: 'account' | 'channel_partner' | null;
169
+ sales_opportunities: Array<{
170
+ opportunity_code: string | null;
171
+ opportunity_type: 'full' | 'upsell' | 'expand' | 'land';
172
+ stage: 'closed_won' | 'closed_lost' | 'churned';
173
+ booking_value_mrr: number | null;
174
+ close_date: string | null;
175
+ }>;
176
+ }
177
+ export interface CustomerCohortPage {
178
+ page: number;
179
+ per_page: number;
180
+ total_count: number;
181
+ total_pages: number;
182
+ customers: CohortCustomer[];
183
+ }
184
+ export declare function getCustomerCohort(client: ApiClient, args: {
185
+ page?: number;
186
+ perPage?: number;
187
+ status?: string;
188
+ stage?: string;
189
+ }): Promise<CustomerCohortPage>;
@@ -106,3 +106,23 @@ export async function getPricingAddendumText(client, args) {
106
106
  await parser.destroy();
107
107
  }
108
108
  }
109
+ export async function getCustomerCohort(client, args) {
110
+ const secret = process.env.FINSTACK_CHATBASE_SECRET;
111
+ if (!secret) {
112
+ throw new Error('FINSTACK_CHATBASE_SECRET must be set');
113
+ }
114
+ const params = new URLSearchParams();
115
+ if (args.page !== undefined)
116
+ params.set('page', String(args.page));
117
+ if (args.perPage !== undefined)
118
+ params.set('per_page', String(args.perPage));
119
+ if (args.status)
120
+ params.set('status', args.status);
121
+ if (args.stage)
122
+ params.set('stage', args.stage);
123
+ const response = await client.get(`/api/v1/chatbase/customer_cohort?${params}`, { 'X-Chatbase-Secret': secret });
124
+ if (response.status !== 'success' || !response.data) {
125
+ throw new Error(response.message || 'Failed to fetch customer cohort');
126
+ }
127
+ return response.data;
128
+ }
@@ -0,0 +1,104 @@
1
+ /**
2
+ * LeadsIQ analytics calls against the mware-portal API.
3
+ *
4
+ * Both endpoints share the admin LeadsIQ customer dashboard's definitions:
5
+ * a pull is one top-level LeadiqJob, records count the export leg only,
6
+ * revenue is the pull's display cost, and entitlement means the customer has
7
+ * a LeadsIQ environment user in any environment.
8
+ */
9
+ import type { ApiClient } from './client.js';
10
+ export interface LeadiqPeriodTotals {
11
+ pulls: number;
12
+ records: number;
13
+ revenue: number;
14
+ active_customers: number;
15
+ }
16
+ export interface LeadiqActiveCustomer {
17
+ name: string | null;
18
+ customer_code: string | null;
19
+ pulls: number;
20
+ records: number;
21
+ revenue: number;
22
+ last_pull_at: string | null;
23
+ }
24
+ export interface LeadiqNeverPulledCustomer {
25
+ name: string;
26
+ customer_code: string | null;
27
+ sales: string | null;
28
+ }
29
+ export interface LeadiqAnalyticsDigest {
30
+ period: {
31
+ start_date: string;
32
+ end_date: string;
33
+ days: number;
34
+ };
35
+ totals: LeadiqPeriodTotals;
36
+ prev_period_totals: LeadiqPeriodTotals;
37
+ /** Every customer with >= 1 pull in the window, highest revenue first. */
38
+ customers: LeadiqActiveCustomer[];
39
+ /** True only if the 50-account safety cap trimmed `customers`. */
40
+ customers_truncated: boolean;
41
+ entitlement: {
42
+ entitled: number;
43
+ active: number;
44
+ invited: number;
45
+ };
46
+ never_pulled: LeadiqNeverPulledCustomer[];
47
+ }
48
+ export declare function getLeadiqAnalytics(client: ApiClient, days?: number): Promise<LeadiqAnalyticsDigest>;
49
+ export interface LeadiqStats {
50
+ pulls: number;
51
+ records: number;
52
+ revenue: number;
53
+ }
54
+ export interface LeadiqTopFilter {
55
+ filter: string;
56
+ used_in_pulls: number;
57
+ share_of_pulls_pct: number | null;
58
+ top_values: Array<{
59
+ value: string;
60
+ pulls: number;
61
+ }>;
62
+ }
63
+ export interface LeadiqRecentPull {
64
+ submitted_at: string | null;
65
+ job_type: string;
66
+ status: string;
67
+ records: number;
68
+ revenue: number;
69
+ append_products: string[] | null;
70
+ }
71
+ export interface LeadiqCustomerDetail {
72
+ customer: {
73
+ name: string;
74
+ customer_code: string | null;
75
+ };
76
+ entitlement: {
77
+ entitled: boolean;
78
+ production_access: boolean;
79
+ status: 'active' | 'invited';
80
+ };
81
+ contacts: {
82
+ admin: string | null;
83
+ sales: string | null;
84
+ };
85
+ period: {
86
+ start_date: string;
87
+ end_date: string;
88
+ days: number;
89
+ } & LeadiqStats;
90
+ year_to_date: LeadiqStats;
91
+ lifetime: LeadiqStats & {
92
+ first_pull_at: string | null;
93
+ last_pull_at: string | null;
94
+ match_rate_pct: number | null;
95
+ append_hit_rates_pct: Record<string, number | null>;
96
+ };
97
+ top_filters: LeadiqTopFilter[];
98
+ recent_pulls: LeadiqRecentPull[];
99
+ }
100
+ export declare function getLeadiqCustomer(client: ApiClient, args: {
101
+ customerCode?: string;
102
+ userId?: string;
103
+ days?: number;
104
+ }): Promise<LeadiqCustomerDetail>;
@@ -0,0 +1,36 @@
1
+ export async function getLeadiqAnalytics(client, days) {
2
+ const secret = process.env.FINSTACK_CHATBASE_SECRET;
3
+ if (!secret) {
4
+ throw new Error('FINSTACK_CHATBASE_SECRET must be set');
5
+ }
6
+ const params = new URLSearchParams();
7
+ if (days !== undefined) {
8
+ params.set('days', String(days));
9
+ }
10
+ const response = await client.get(`/api/v1/chatbase/leadiq_analytics?${params}`, { 'X-Chatbase-Secret': secret });
11
+ if (response.status !== 'success' || !response.data) {
12
+ throw new Error(response.message || 'Failed to fetch LeadsIQ analytics');
13
+ }
14
+ return response.data;
15
+ }
16
+ export async function getLeadiqCustomer(client, args) {
17
+ const secret = process.env.FINSTACK_CHATBASE_SECRET;
18
+ if (!secret) {
19
+ throw new Error('FINSTACK_CHATBASE_SECRET must be set');
20
+ }
21
+ if (!args.customerCode && !args.userId) {
22
+ throw new Error('Provide either customer_code or user_id');
23
+ }
24
+ const params = new URLSearchParams();
25
+ if (args.customerCode)
26
+ params.set('customer_code', args.customerCode);
27
+ if (args.userId)
28
+ params.set('user_id', args.userId);
29
+ if (args.days !== undefined)
30
+ params.set('days', String(args.days));
31
+ const response = await client.get(`/api/v1/chatbase/leadiq_customer?${params}`, { 'X-Chatbase-Secret': secret });
32
+ if (response.status !== 'success' || !response.data) {
33
+ throw new Error(response.message || 'Failed to fetch LeadsIQ customer detail');
34
+ }
35
+ return response.data;
36
+ }
@@ -31,6 +31,14 @@ interface GetCustomerPricingConfigArgs {
31
31
  environment?: string;
32
32
  }
33
33
  export declare function handleGetCustomerPricingConfig(args: GetCustomerPricingConfigArgs): Promise<unknown>;
34
+ interface GetCustomerCohortArgs {
35
+ page?: number;
36
+ per_page?: number;
37
+ status?: string;
38
+ stage?: string;
39
+ environment?: string;
40
+ }
41
+ export declare function handleGetCustomerCohort(args: GetCustomerCohortArgs): Promise<unknown>;
34
42
  interface GetPricingAddendumTextArgs {
35
43
  id: number;
36
44
  variant?: PricingAddendumVariant;
@@ -2,7 +2,7 @@
2
2
  * Customer tool handlers
3
3
  */
4
4
  import { getClient } from '../api/client.js';
5
- import { getCustomerInvoices, getCustomerPricingConfig, getCustomerRevenue, getCustomerSummary, getPricingAddendumText, searchCustomers, } from '../api/customer.api.js';
5
+ import { getCustomerCohort, getCustomerInvoices, getCustomerPricingConfig, getCustomerRevenue, getCustomerSummary, getPricingAddendumText, searchCustomers, } from '../api/customer.api.js';
6
6
  export async function handleGetCustomerSummary(args) {
7
7
  const client = getClient(args.environment ?? 'prod');
8
8
  return getCustomerSummary(client, args.customer_code);
@@ -33,6 +33,15 @@ export async function handleGetCustomerPricingConfig(args) {
33
33
  const client = getClient(args.environment ?? 'prod');
34
34
  return getCustomerPricingConfig(client, args.customer_code);
35
35
  }
36
+ export async function handleGetCustomerCohort(args) {
37
+ const client = getClient(args.environment ?? 'prod');
38
+ return getCustomerCohort(client, {
39
+ page: args.page,
40
+ perPage: args.per_page,
41
+ status: args.status,
42
+ stage: args.stage,
43
+ });
44
+ }
36
45
  export async function handleGetPricingAddendumText(args) {
37
46
  const client = getClient(args.environment ?? 'prod');
38
47
  return getPricingAddendumText(client, {
@@ -1,4 +1,5 @@
1
- import { handleGetCustomerInvoices, handleGetCustomerPricingConfig, handleGetCustomerRevenue, handleGetCustomerSummary, handleGetPricingAddendumText, handleSearchCustomer, } from './customer.handler.js';
1
+ import { handleGetCustomerCohort, handleGetCustomerInvoices, handleGetCustomerPricingConfig, handleGetCustomerRevenue, handleGetCustomerSummary, handleGetPricingAddendumText, handleSearchCustomer, } from './customer.handler.js';
2
+ import { handleGetLeadiqAnalytics, handleGetLeadiqCustomer, } from './leadiq.handler.js';
2
3
  import { handleGetVendors } from './vendor.handler.js';
3
4
  const wrap = (fn) => {
4
5
  return async (args) => fn(args);
@@ -10,5 +11,8 @@ export const handlers = {
10
11
  finstack_get_customer_invoices: wrap(handleGetCustomerInvoices),
11
12
  finstack_get_customer_pricing_config: wrap(handleGetCustomerPricingConfig),
12
13
  finstack_get_pricing_addendum_text: wrap(handleGetPricingAddendumText),
14
+ finstack_get_customer_cohort: wrap(handleGetCustomerCohort),
15
+ finstack_get_leadiq_analytics: wrap(handleGetLeadiqAnalytics),
16
+ finstack_get_leadiq_customer: wrap(handleGetLeadiqCustomer),
13
17
  finstack_get_vendors: wrap(handleGetVendors),
14
18
  };
@@ -0,0 +1,13 @@
1
+ interface GetLeadiqAnalyticsArgs {
2
+ days?: number;
3
+ environment?: string;
4
+ }
5
+ export declare function handleGetLeadiqAnalytics(args: GetLeadiqAnalyticsArgs): Promise<unknown>;
6
+ interface GetLeadiqCustomerArgs {
7
+ customer_code?: string;
8
+ user_id?: string;
9
+ days?: number;
10
+ environment?: string;
11
+ }
12
+ export declare function handleGetLeadiqCustomer(args: GetLeadiqCustomerArgs): Promise<unknown>;
13
+ export {};
@@ -0,0 +1,17 @@
1
+ /**
2
+ * LeadsIQ tool handlers
3
+ */
4
+ import { getClient } from '../api/client.js';
5
+ import { getLeadiqAnalytics, getLeadiqCustomer } from '../api/leadiq.api.js';
6
+ export async function handleGetLeadiqAnalytics(args) {
7
+ const client = getClient(args.environment ?? 'prod');
8
+ return getLeadiqAnalytics(client, args.days);
9
+ }
10
+ export async function handleGetLeadiqCustomer(args) {
11
+ const client = getClient(args.environment ?? 'prod');
12
+ return getLeadiqCustomer(client, {
13
+ customerCode: args.customer_code,
14
+ userId: args.user_id,
15
+ days: args.days,
16
+ });
17
+ }
@@ -152,4 +152,38 @@ export const customerTools = [
152
152
  required: ['customer_code', 'type', 'start_date', 'end_date'],
153
153
  },
154
154
  },
155
+ {
156
+ name: 'finstack_get_customer_cohort',
157
+ description: 'Bulk, paginated list of ALL B2B customers with their tracker fields: portal account status (active/inactive/blacklisted/locked/obsolete/new_account), account_type (account/channel_partner), and sales_opportunities[] (opportunity_code, opportunity_type full/upsell/expand/land, stage closed_won/closed_lost/churned, booking_value_mrr, close_date). Use this for cohort/tracker/dashboard refreshes or any question spanning many customers — NEVER call finstack_get_customer_summary once per customer for that. Default 500 customers per page (max 1000); read total_pages from the response and fetch the remaining pages. Optional server-side filters: status and stage.',
158
+ inputSchema: {
159
+ type: 'object',
160
+ properties: {
161
+ page: {
162
+ type: 'number',
163
+ description: 'Page number, 1-indexed. Default 1.',
164
+ minimum: 1,
165
+ },
166
+ per_page: {
167
+ type: 'number',
168
+ description: 'Customers per page. Default 500, capped at 1000.',
169
+ minimum: 1,
170
+ maximum: 1000,
171
+ },
172
+ status: {
173
+ type: 'string',
174
+ description: 'Only customers with this portal status, e.g. "active", "inactive", "blacklisted", "locked", "obsolete". Omit for all.',
175
+ },
176
+ stage: {
177
+ type: 'string',
178
+ enum: ['closed_won', 'closed_lost', 'churned'],
179
+ description: 'Only customers having at least one sales opportunity in this stage. Omit for all.',
180
+ },
181
+ environment: {
182
+ type: 'string',
183
+ description: 'Target environment: "prod" (default) or "dev"',
184
+ enum: ['dev', 'prod'],
185
+ },
186
+ },
187
+ },
188
+ },
155
189
  ];
@@ -1,3 +1,8 @@
1
1
  import { customerTools } from './customer.tools.js';
2
+ import { leadiqTools } from './leadiq.tools.js';
2
3
  import { vendorTools } from './vendor.tools.js';
3
- export const tools = [...customerTools, ...vendorTools];
4
+ export const tools = [
5
+ ...customerTools,
6
+ ...leadiqTools,
7
+ ...vendorTools,
8
+ ];
@@ -0,0 +1,5 @@
1
+ /**
2
+ * LeadsIQ tool definitions
3
+ */
4
+ import type { ToolDefinition } from '../types/tool.types.js';
5
+ export declare const leadiqTools: ToolDefinition[];
@@ -0,0 +1,50 @@
1
+ export const leadiqTools = [
2
+ {
3
+ name: 'finstack_get_leadiq_analytics',
4
+ description: 'System-wide LeadsIQ (lead generation / batch append) analytics for a trailing window. Returns totals and previous-period totals (pulls, records, revenue, active_customers), customers — the COMPLETE list of customers with at least one pull in the window (highest revenue first; customers_truncated is true only if the 50-account safety cap trimmed it), the entitled/active/invited account split, and never_pulled — entitled accounts that have never run a pull, with their sales owner. Use for questions like "which customers pulled last month" (set days=30 and read customers[]), "who is the most active LeadsIQ customer by revenue" (customers[0]), or "who is enrolled in LeadsIQ but never pulled". For one customer\'s detail use finstack_get_leadiq_customer.',
5
+ inputSchema: {
6
+ type: 'object',
7
+ properties: {
8
+ days: {
9
+ type: 'number',
10
+ description: 'Trailing window in days, 1-366 (default 7; values outside the range are clamped server-side). Use 30 for "last month", 365 for "this past year".',
11
+ minimum: 1,
12
+ maximum: 366,
13
+ },
14
+ environment: {
15
+ type: 'string',
16
+ description: 'Target environment: "prod" (default) or "dev"',
17
+ enum: ['dev', 'prod'],
18
+ },
19
+ },
20
+ },
21
+ },
22
+ {
23
+ name: 'finstack_get_leadiq_customer',
24
+ description: "One B2B customer's LeadsIQ usage detail: enrollment state (entitled, production_access, active/invited status), admin and sales contacts, pulls/records/revenue for a trailing window plus year_to_date and lifetime, lifetime match rate and per-field append hit rates (mobile/email/landline/demographic, as percentages), top_filters — the form filters the customer most commonly runs pulls with, each with usage share and most common values — and the 10 most recent pulls. Use for questions like \"what LeadsIQ revenue did customer X have this year\" (read year_to_date.revenue) or \"which filters does customer X commonly use\". Revenue here is LeadsIQ job cost only; for a customer's total billed revenue across all products use finstack_get_customer_revenue. Identify the customer with EITHER customer_code OR user_id.",
25
+ inputSchema: {
26
+ type: 'object',
27
+ properties: {
28
+ customer_code: {
29
+ type: 'string',
30
+ description: 'Customer code (CID) — e.g. "CID18068". Provide this OR user_id.',
31
+ },
32
+ user_id: {
33
+ type: 'string',
34
+ description: 'mware-portal user ID. Provide this OR customer_code.',
35
+ },
36
+ days: {
37
+ type: 'number',
38
+ description: 'Trailing window in days for the period block, 1-366 (default 30; values outside the range are clamped server-side).',
39
+ minimum: 1,
40
+ maximum: 366,
41
+ },
42
+ environment: {
43
+ type: 'string',
44
+ description: 'Target environment: "prod" (default) or "dev"',
45
+ enum: ['dev', 'prod'],
46
+ },
47
+ },
48
+ },
49
+ },
50
+ ];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@crscreditapi/finstack-mcp-server",
3
- "version": "0.4.0-0c53dac",
3
+ "version": "0.6.0-06bd3a8",
4
4
  "description": "MCP server for Finstack",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",