@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.
- package/index.js +6 -0
- 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';
|
|
@@ -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 };
|