@crscreditapi/finstack-mcp-server 0.2.0-cbc96e9 → 0.2.0-fac1a77

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/.env.example CHANGED
@@ -11,3 +11,8 @@ FINSTACK_PROD_BASE_URL=https://portal.example.com
11
11
 
12
12
  # Shared secret for the mware-portal chatbase endpoints (X-Chatbase-Secret header)
13
13
  FINSTACK_CHATBASE_SECRET=
14
+
15
+ # === Vendor contact directory (crs-vendor-contacts) ===
16
+ # Base URL of the already-deployed vendor contacts app. Read-only, no auth.
17
+ # Defaults to the production Vercel URL if unset.
18
+ # VENDOR_CONTACTS_BASE_URL=https://crs-vendor-contacts.vercel.app
@@ -0,0 +1,34 @@
1
+ export interface VendorContact {
2
+ id: string;
3
+ vendor: string;
4
+ name: string;
5
+ email: string;
6
+ phone: string;
7
+ role: string;
8
+ notes: string;
9
+ connection: string;
10
+ location: string;
11
+ bestFor: string;
12
+ specialties: string[];
13
+ }
14
+ export type VendorConnection = '1st Degree' | '2nd Degree' | 'all';
15
+ export interface VendorSearchResult {
16
+ query: string | null;
17
+ connection: VendorConnection;
18
+ total_matched: number;
19
+ returned: number;
20
+ contacts: VendorContact[];
21
+ }
22
+ /**
23
+ * Search the vendor contact directory.
24
+ *
25
+ * The connection filter is applied server-side; the keyword query is applied
26
+ * here, OR-matched across every contact field plus specialty tags so that a
27
+ * question like "who handles ECM pricing" matches on role, notes, AND the
28
+ * `pricing` specialty.
29
+ */
30
+ export declare function searchVendorContacts(args: {
31
+ query?: string;
32
+ connection?: VendorConnection;
33
+ limit?: number;
34
+ }): Promise<VendorSearchResult>;
@@ -0,0 +1,76 @@
1
+ /**
2
+ * Vendor contact directory calls.
3
+ *
4
+ * Backed by the standalone crs-vendor-contacts web service (the CPO's
5
+ * Slack-powered vendor Rolodex). There is no database — the service stores
6
+ * contacts in a JSON blob and exposes them over a read-only REST endpoint
7
+ * (GET /api/vendors). We fetch the list and filter in-process so callers can
8
+ * match on specialty tags too (the upstream ?q= filter only covers a subset
9
+ * of fields).
10
+ *
11
+ * This calls the vendor app's already-deployed public API, so it needs no
12
+ * redeploy of that project — only the env var VENDOR_CONTACTS_BASE_URL
13
+ * (defaults to the production Vercel URL).
14
+ */
15
+ import { ApiClient } from './client.js';
16
+ const DEFAULT_BASE_URL = 'https://crs-vendor-contacts.vercel.app';
17
+ const DEFAULT_TIMEOUT_MS = 30_000;
18
+ let _vendorClient = null;
19
+ function getVendorClient() {
20
+ if (_vendorClient)
21
+ return _vendorClient;
22
+ const baseUrl = process.env.VENDOR_CONTACTS_BASE_URL ?? DEFAULT_BASE_URL;
23
+ const timeoutMs = process.env.FINSTACK_TIMEOUT_MS
24
+ ? parseInt(process.env.FINSTACK_TIMEOUT_MS, 10)
25
+ : DEFAULT_TIMEOUT_MS;
26
+ _vendorClient = new ApiClient({ baseUrl, timeoutMs });
27
+ return _vendorClient;
28
+ }
29
+ /**
30
+ * Search the vendor contact directory.
31
+ *
32
+ * The connection filter is applied server-side; the keyword query is applied
33
+ * here, OR-matched across every contact field plus specialty tags so that a
34
+ * question like "who handles ECM pricing" matches on role, notes, AND the
35
+ * `pricing` specialty.
36
+ */
37
+ export async function searchVendorContacts(args) {
38
+ const client = getVendorClient();
39
+ const params = new URLSearchParams();
40
+ if (args.connection && args.connection !== 'all') {
41
+ params.set('connection', args.connection);
42
+ }
43
+ const qs = params.toString();
44
+ const all = await client.get(qs ? `/api/vendors?${qs}` : '/api/vendors');
45
+ let matched = all;
46
+ const q = args.query?.trim().toLowerCase();
47
+ if (q) {
48
+ const terms = q.split(/\s+/).filter((t) => t.length > 1);
49
+ if (terms.length > 0) {
50
+ matched = all.filter((v) => {
51
+ const haystack = [
52
+ v.vendor,
53
+ v.name,
54
+ v.email,
55
+ v.role,
56
+ v.notes,
57
+ v.location,
58
+ v.bestFor,
59
+ ...(v.specialties ?? []),
60
+ ]
61
+ .filter(Boolean)
62
+ .join(' ')
63
+ .toLowerCase();
64
+ return terms.some((t) => haystack.includes(t));
65
+ });
66
+ }
67
+ }
68
+ const limit = args.limit && args.limit > 0 ? Math.min(args.limit, 100) : 25;
69
+ return {
70
+ query: args.query ?? null,
71
+ connection: args.connection ?? 'all',
72
+ total_matched: matched.length,
73
+ returned: Math.min(matched.length, limit),
74
+ contacts: matched.slice(0, limit),
75
+ };
76
+ }
@@ -1,4 +1,5 @@
1
1
  import { handleGetCustomerInvoices, handleGetCustomerPricingConfig, handleGetCustomerRevenue, handleGetCustomerSummary, handleGetPricingAddendumText, handleSearchCustomer, } from './customer.handler.js';
2
+ import { handleGetVendor } from './vendor.handler.js';
2
3
  const wrap = (fn) => {
3
4
  return async (args) => fn(args);
4
5
  };
@@ -9,4 +10,5 @@ export const handlers = {
9
10
  finstack_get_customer_invoices: wrap(handleGetCustomerInvoices),
10
11
  finstack_get_customer_pricing_config: wrap(handleGetCustomerPricingConfig),
11
12
  finstack_get_pricing_addendum_text: wrap(handleGetPricingAddendumText),
13
+ vendorproject_find_vendor_contact: wrap(handleGetVendor),
12
14
  };
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Vendor contact directory handlers
3
+ */
4
+ import { type VendorConnection } from '../api/vendor.api.js';
5
+ interface GetVendorArgs {
6
+ query?: string;
7
+ connection?: VendorConnection;
8
+ limit?: number;
9
+ }
10
+ export declare function handleGetVendor(args: GetVendorArgs): Promise<unknown>;
11
+ export {};
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Vendor contact directory handlers
3
+ */
4
+ import { searchVendorContacts, } from '../api/vendor.api.js';
5
+ export async function handleGetVendor(args) {
6
+ return searchVendorContacts({
7
+ query: args.query,
8
+ connection: args.connection,
9
+ limit: args.limit,
10
+ });
11
+ }
@@ -1,2 +1,3 @@
1
1
  import { customerTools } from './customer.tools.js';
2
- export const tools = [...customerTools];
2
+ import { vendorTools } from './vendor.tools.js';
3
+ export const tools = [...customerTools, ...vendorTools];
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Vendor contact directory tool definitions
3
+ */
4
+ import type { ToolDefinition } from '../types/tool.types.js';
5
+ export declare const vendorTools: ToolDefinition[];
@@ -0,0 +1,26 @@
1
+ export const vendorTools = [
2
+ {
3
+ name: 'vendorproject_find_vendor_contact',
4
+ description: "Look up WHO to contact at an external vendor or partner — the person to email, call, or get introduced to. This is about CRS's OWN vendors/partners (credit bureaus and data vendors like Equifax, TransUnion, Experian, CIC/Asurint, SentiLink), NOT CRS customers (use the finstack_* tools for customer accounts, billing, and pricing). Use whenever the user asks who to speak to / talk to / contact / reach out to / get an intro to, or who the point of contact or relationship owner is — for a vendor, a product or topic area (e.g. ECM), a role, or a specialty. Examples: 'who should I speak to for new ECM products', 'who is our pricing contact at Equifax', 'who handles compliance at Experian', 'who owns the TransUnion relationship'. Pass the key terms (vendor name, product/topic like 'ECM', role, or specialty) as `query` — it OR-matches across company, contact name, role, notes, location, and specialty tags (pricing, technical, legal, paperwork, champion, compliance, onboarding, sales, support, executive). Each result includes name, vendor, role, email, phone, location, connection degree, and specialties. Omit `query` to list the whole directory. Read-only — it cannot add, edit, or delete contacts.",
5
+ inputSchema: {
6
+ type: 'object',
7
+ properties: {
8
+ query: {
9
+ type: 'string',
10
+ description: "Keywords to match — company, person, role, product area, or specialty. e.g. 'ECM', 'Equifax pricing', 'compliance'. Tokenized and OR-matched across all contact fields and specialty tags. Omit to return the full directory.",
11
+ },
12
+ connection: {
13
+ type: 'string',
14
+ enum: ['1st Degree', '2nd Degree', 'all'],
15
+ description: "Filter by connection strength. '1st Degree' = direct contacts, '2nd Degree' = warm intros. Default 'all'.",
16
+ },
17
+ limit: {
18
+ type: 'number',
19
+ description: 'Max contacts to return. Default 25, capped at 100.',
20
+ minimum: 1,
21
+ maximum: 100,
22
+ },
23
+ },
24
+ },
25
+ },
26
+ ];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@crscreditapi/finstack-mcp-server",
3
- "version": "0.2.0-cbc96e9",
3
+ "version": "0.2.0-fac1a77",
4
4
  "description": "MCP server for Finstack",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",