@closedloop-ai/mcp-client 1.3.1 → 1.5.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 (3) hide show
  1. package/index.js +8 -2
  2. package/package.json +1 -1
  3. package/privacy.js +139 -0
package/index.js CHANGED
@@ -5,6 +5,8 @@ const { StdioServerTransport } = require('@modelcontextprotocol/sdk/server/stdio
5
5
  const { CallToolRequestSchema, ListToolsRequestSchema } = require('@modelcontextprotocol/sdk/types.js');
6
6
  const axios = require('axios');
7
7
 
8
+ const { applyPrivacyMask, PRIVACY_MODE } = require('./privacy');
9
+
8
10
  // Get configuration from environment
9
11
  const CLOSEDLOOP_API_KEY = process.env.CLOSEDLOOP_API_KEY;
10
12
  const CLOSEDLOOP_SERVER_URL = process.env.CLOSEDLOOP_SERVER_URL || 'https://mcp.closedloop.sh';
@@ -183,12 +185,12 @@ const tools = [
183
185
  default: 5,
184
186
  description: 'Max patterns to return (default 5)'
185
187
  },
186
- limit_signals: {
188
+ limit_insights: {
187
189
  type: 'integer',
188
190
  minimum: 1,
189
191
  maximum: 20,
190
192
  default: 10,
191
- description: 'Max individual signals to return (default 10)'
193
+ description: 'Max individual insights to return (default 10)'
192
194
  }
193
195
  },
194
196
  required: ['query']
@@ -268,6 +270,10 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
268
270
  responseData = responseData.result;
269
271
  }
270
272
 
273
+ if (PRIVACY_MODE) {
274
+ responseData = applyPrivacyMask(responseData);
275
+ }
276
+
271
277
  return {
272
278
  content: [
273
279
  {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@closedloop-ai/mcp-client",
3
- "version": "1.3.1",
3
+ "version": "1.5.0",
4
4
  "description": "ClosedLoop AI MCP Client for AI assistant integration with advanced search capabilities",
5
5
  "main": "index.js",
6
6
  "bin": {
package/privacy.js ADDED
@@ -0,0 +1,139 @@
1
+ 'use strict';
2
+
3
+ // Ported from frontend/src/utils/gdprMasking.ts
4
+ // Deterministic masking: same input always produces same fake output
5
+
6
+ const FAKE_FIRST_NAMES = [
7
+ 'Alex', 'Jordan', 'Taylor', 'Morgan', 'Casey', 'Riley', 'Quinn', 'Avery', 'Cameron', 'Drew',
8
+ 'Jamie', 'Reese', 'Skyler', 'Parker', 'Hayden', 'Emerson', 'Rowan', 'Finley', 'Sage', 'Blake',
9
+ 'Emma', 'Liam', 'Olivia', 'Noah', 'Ava', 'Ethan', 'Sophia', 'Mason', 'Isabella', 'William',
10
+ 'Mia', 'James', 'Charlotte', 'Benjamin', 'Amelia', 'Lucas', 'Harper', 'Henry', 'Evelyn', 'Alexander',
11
+ 'Luna', 'Michael', 'Ella', 'Daniel', 'Elizabeth', 'Matthew', 'Sofia', 'David', 'Emily', 'Joseph',
12
+ 'Aria', 'Samuel', 'Scarlett', 'Sebastian', 'Grace', 'Jack', 'Chloe', 'Owen', 'Victoria', 'Gabriel',
13
+ 'Penelope', 'Carter', 'Layla', 'Jayden', 'Riley', 'John', 'Zoey', 'Luke', 'Nora', 'Dylan',
14
+ 'Lily', 'Grayson', 'Eleanor', 'Isaac', 'Hannah', 'Anthony', 'Lillian', 'Thomas', 'Addison', 'Charles',
15
+ 'Aubrey', 'Christopher', 'Ellie', 'Joshua', 'Stella', 'Andrew', 'Natalie', 'Lincoln', 'Zoe', 'Nathan',
16
+ 'Leah', 'Ryan', 'Hazel', 'Adrian', 'Violet', 'Eli', 'Aurora', 'Nolan', 'Savannah', 'Aaron'
17
+ ];
18
+
19
+ const FAKE_LAST_NAMES = [
20
+ 'Smith', 'Johnson', 'Williams', 'Brown', 'Jones', 'Garcia', 'Miller', 'Davis', 'Rodriguez', 'Martinez',
21
+ 'Anderson', 'Taylor', 'Thomas', 'Moore', 'Jackson', 'Martin', 'Lee', 'Thompson', 'White', 'Harris',
22
+ 'Sanchez', 'Clark', 'Lewis', 'Robinson', 'Walker', 'Young', 'Allen', 'King', 'Wright', 'Scott',
23
+ 'Torres', 'Nguyen', 'Hill', 'Flores', 'Green', 'Adams', 'Nelson', 'Baker', 'Hall', 'Rivera',
24
+ 'Campbell', 'Mitchell', 'Carter', 'Roberts', 'Turner', 'Phillips', 'Evans', 'Parker', 'Edwards', 'Collins',
25
+ 'Stewart', 'Morris', 'Murphy', 'Cook', 'Rogers', 'Morgan', 'Peterson', 'Cooper', 'Reed', 'Bailey',
26
+ 'Bell', 'Gomez', 'Kelly', 'Howard', 'Ward', 'Cox', 'Diaz', 'Richardson', 'Wood', 'Watson',
27
+ 'Brooks', 'Bennett', 'Gray', 'James', 'Reyes', 'Cruz', 'Hughes', 'Price', 'Myers', 'Long',
28
+ 'Foster', 'Sanders', 'Ross', 'Morales', 'Powell', 'Sullivan', 'Russell', 'Ortiz', 'Jenkins', 'Gutierrez',
29
+ 'Perry', 'Butler', 'Barnes', 'Fisher', 'Henderson', 'Coleman', 'Simmons', 'Patterson', 'Jordan', 'Reynolds'
30
+ ];
31
+
32
+ const FAKE_COMPANIES = [
33
+ 'Stackflow', 'Builderly', 'Teamspace', 'Docuwise', 'Chartly', 'Formstack', 'Taskbird', 'Codebase',
34
+ 'Metricly', 'Trackify', 'Insightly', 'Launchpad', 'Scalebase', 'Cloudly', 'Deployify', 'Hostbase',
35
+ 'Meetly', 'Recordly', 'Boardify', 'Designly', 'Workstream', 'Projectly', 'Taskflow', 'Notewise',
36
+ 'Chatly', 'Supportly', 'Helpwise', 'Ticketly', 'Dealflow', 'Pipebase', 'Leadify', 'Closely',
37
+ 'Paybase', 'Finwise', 'Expensely', 'Bankify', 'Fundly', 'Equitybase', 'Payrolly', 'Peoplewise',
38
+ 'Monitorly', 'Alertify', 'Logwise', 'Dashbase', 'Oncallify', 'Incidently', 'Debugly', 'Tracewise',
39
+ 'Messagely', 'Mailwise', 'Campaignly', 'Sendbase', 'Engagely', 'Pushify', 'Automately', 'Flowbase',
40
+ 'Authly', 'Securify', 'Loginwise', 'Accessly', 'Identifybase', 'Shieldly', 'Protectify', 'Scanwise',
41
+ 'Repobase', 'Codewise', 'Branchly', 'Buildify', 'Testbase', 'Pipelinewise', 'Shiply', 'Deploybase',
42
+ 'Datawise', 'Warehously', 'Syncify', 'Streambase', 'Transformly', 'Qualitywise', 'Catalogly', 'Governbase',
43
+ 'Contentwise', 'Headlessly', 'Schemabase', 'Mediafly', 'Componentwise', 'Blockify', 'Modelbase', 'Fieldly',
44
+ 'Edgewise', 'Cachely', 'Speedbase', 'Sitewise', 'Infrabase', 'Computewise', 'Containerfly', 'Serverbase',
45
+ 'Feedbackly', 'Analytify', 'Sessionbase', 'Replaywise', 'Heatmaply', 'Researchify', 'Prototypewise', 'Insightbase'
46
+ ];
47
+
48
+ function hashString(str) {
49
+ let hash = 5381;
50
+ const s = str.toLowerCase().trim();
51
+ for (let i = 0; i < s.length; i++) {
52
+ hash = ((hash << 5) + hash) + s.charCodeAt(i);
53
+ hash = hash & hash; // keep 32-bit integer
54
+ }
55
+ return Math.abs(hash);
56
+ }
57
+
58
+ function maskName(value) {
59
+ if (!value) return 'Anonymous User';
60
+ const hash = hashString(value);
61
+ return `${FAKE_FIRST_NAMES[hash % FAKE_FIRST_NAMES.length]} ${FAKE_LAST_NAMES[Math.floor(hash / FAKE_FIRST_NAMES.length) % FAKE_LAST_NAMES.length]}`;
62
+ }
63
+
64
+ function maskEmail(value) {
65
+ if (!value) return 'user@example.com';
66
+ const hash = hashString(value);
67
+ const first = FAKE_FIRST_NAMES[hash % FAKE_FIRST_NAMES.length].toLowerCase();
68
+ const last = FAKE_LAST_NAMES[Math.floor(hash / FAKE_FIRST_NAMES.length) % FAKE_LAST_NAMES.length].toLowerCase();
69
+ return `${first}.${last}@example.com`;
70
+ }
71
+
72
+ function maskCompany(value) {
73
+ if (!value) return 'Unknown Company';
74
+ return FAKE_COMPANIES[hashString(value) % FAKE_COMPANIES.length];
75
+ }
76
+
77
+ function isEmail(value) {
78
+ return typeof value === 'string' && value.includes('@');
79
+ }
80
+
81
+ // Mask a single customer string — handles both names and emails
82
+ function maskCustomer(value) {
83
+ if (!value) return value;
84
+ return isEmail(value) ? maskEmail(value) : maskName(value);
85
+ }
86
+
87
+ // Apply privacy masking to a tool response object
88
+ function applyPrivacyMask(data) {
89
+ if (!data || typeof data !== 'object') return data;
90
+
91
+ // Deep clone to avoid mutating the original
92
+ const d = JSON.parse(JSON.stringify(data));
93
+
94
+ // feedback / insight items (list_insights, search_insights, get_insight_detail)
95
+ const maskInsightItem = (item) => {
96
+ if (item.customer_name) item.customer_name = maskCustomer(item.customer_name);
97
+ if (item.entities_integrations) {} // keep — not PII
98
+ return item;
99
+ };
100
+
101
+ // get_planning_context response shape
102
+ if (d.data) {
103
+ const { patterns, insights } = d.data;
104
+
105
+ if (Array.isArray(patterns)) {
106
+ patterns.forEach(p => {
107
+ if (Array.isArray(p.customers)) {
108
+ p.customers = p.customers.map(maskCustomer);
109
+ }
110
+ if (Array.isArray(p.crm_deals)) {
111
+ p.crm_deals = p.crm_deals.map(maskCompany);
112
+ }
113
+ });
114
+ }
115
+
116
+ if (Array.isArray(insights)) {
117
+ insights.forEach(maskInsightItem);
118
+ }
119
+
120
+ // summary testing_candidates
121
+ if (d.data.summary && Array.isArray(d.data.summary.testing_candidates)) {
122
+ d.data.summary.testing_candidates = d.data.summary.testing_candidates.map(maskCustomer);
123
+ }
124
+ }
125
+
126
+ // list_insights / search_insights: top-level items array
127
+ if (Array.isArray(d.items)) {
128
+ d.items.forEach(maskInsightItem);
129
+ }
130
+
131
+ // get_insight_detail: single item at root
132
+ if (d.customer_name) maskInsightItem(d);
133
+
134
+ return d;
135
+ }
136
+
137
+ const PRIVACY_MODE = process.env.CLOSEDLOOP_PRIVACY_MODE === 'true';
138
+
139
+ module.exports = { applyPrivacyMask, PRIVACY_MODE };