@closedloop-ai/mcp-client 1.4.0 → 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 +6 -0
  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';
@@ -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.4.0",
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 };