@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.
- package/index.js +8 -2
- package/package.json +1 -1
- 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
|
-
|
|
188
|
+
limit_insights: {
|
|
187
189
|
type: 'integer',
|
|
188
190
|
minimum: 1,
|
|
189
191
|
maximum: 20,
|
|
190
192
|
default: 10,
|
|
191
|
-
description: 'Max individual
|
|
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
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 };
|