@privacyscrubber/mcp-server 1.7.2 → 2.0.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/.well-known/mcp/server-card.json +78 -0
- package/LICENSE +21 -0
- package/README.md +5 -5
- package/config-schema.json +10 -0
- package/index.js +387 -47
- package/package.json +5 -1
- package/ps-license-manager.js +257 -0
- package/ps-pii-engine.cjs +1179 -0
- package/ps-pii-engine.js +1179 -0
- package/scrubber-core.cjs +1707 -11
package/index.js
CHANGED
|
@@ -37,7 +37,10 @@ const MCP_VERSION = require('./package.json').version;
|
|
|
37
37
|
// Import the production core engine with 100% parity
|
|
38
38
|
const scrubberCorePath = path.resolve(__dirname, './scrubber-core.cjs');
|
|
39
39
|
const PrivacyScrubberCore = require(scrubberCorePath);
|
|
40
|
-
|
|
40
|
+
let LicenseManager = require('./ps-license-manager.js');
|
|
41
|
+
if (!LicenseManager || typeof LicenseManager.validate !== 'function') {
|
|
42
|
+
LicenseManager = global.LicenseManager;
|
|
43
|
+
}
|
|
41
44
|
|
|
42
45
|
// Initialize core engine
|
|
43
46
|
PrivacyScrubberCore.init();
|
|
@@ -45,6 +48,9 @@ PrivacyScrubberCore.init();
|
|
|
45
48
|
// Volatile in-memory token map
|
|
46
49
|
const sessionMap = {};
|
|
47
50
|
|
|
51
|
+
// Volatile in-memory false positive ignore list (values excluded from future scrubs)
|
|
52
|
+
const sessionIgnoreList = new Set();
|
|
53
|
+
|
|
48
54
|
// ANSI terminal color helpers
|
|
49
55
|
const colors = {
|
|
50
56
|
yellowBold: '\x1b[1;33m',
|
|
@@ -73,6 +79,8 @@ const DEVOPS_SECRETS_DETECTOR = [
|
|
|
73
79
|
{ name: 'JSON Web Token (JWT)', regex: /\beyJ[a-zA-Z0-9_-]+\.[a-zA-Z0-9_-]+\.[a-zA-Z0-9_-]+\b/g },
|
|
74
80
|
{ name: 'API Token/Key (GitHub/Slack/NPM)', regex: /\b(?:ghp|gho|ghu|ghs|ghr|glpat|npm|xox[baprs])[-_][A-Za-z0-9_]{10,}\b/g },
|
|
75
81
|
{ name: 'Stripe API Key', regex: /\b(?:[rs]k)_(?:test|live)_[a-zA-Z0-9]{24,}\b/g },
|
|
82
|
+
{ name: 'OpenAI Project API Key', regex: /\b(?:sk|pk)-(?:proj-)?[a-zA-Z0-9_-]{16,}\b/gi },
|
|
83
|
+
{ name: 'Database Connection URI', regex: /\b(?:postgres(?:ql)?|mysql|mongodb(?:\+srv)?|redis|amqp|mssql):\/\/[^\s"']+/gi },
|
|
76
84
|
{ name: 'Database/API Secret', regex: /\b(DB|POSTGRES|REDIS|MYSQL|AWS|SECRET|PASSWORD|TOKEN|API|KEY)[A-Z0-9_]*\s*[:=]\s*[^ \t\r\n"']{8,}\b/gi }
|
|
77
85
|
];
|
|
78
86
|
|
|
@@ -83,15 +91,144 @@ function checkLicenseStatus() {
|
|
|
83
91
|
const result = LicenseManager.validate(key);
|
|
84
92
|
|
|
85
93
|
if (!result.valid) {
|
|
94
|
+
mcpLog(`DEBUG: License invalid because: ${result.reason || "Unknown"}`);
|
|
86
95
|
return { isPro: false, type: null, error: result.reason || "Invalid license format or signature." };
|
|
87
96
|
}
|
|
88
97
|
|
|
89
98
|
return { isPro: true, type: result.tier, error: null };
|
|
90
99
|
}
|
|
91
100
|
|
|
92
|
-
// ──
|
|
93
|
-
|
|
94
|
-
const
|
|
101
|
+
// ── Free Tier Usage Tracking (Persistent) ─────────────────────────────
|
|
102
|
+
const FREE_TIER_DAILY_LIMIT = 10;
|
|
103
|
+
const STAR_PROMPT_THRESHOLD = 5;
|
|
104
|
+
|
|
105
|
+
function getUsageFilePath() {
|
|
106
|
+
const homeDir = process.env.HOME || process.env.USERPROFILE || '';
|
|
107
|
+
return path.resolve(homeDir, '.privacyscrubber-usage.json');
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function getDailyUsage() {
|
|
111
|
+
const file = getUsageFilePath();
|
|
112
|
+
const today = new Date().toISOString().split('T')[0];
|
|
113
|
+
if (!fs.existsSync(file)) return 0;
|
|
114
|
+
try {
|
|
115
|
+
const data = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
116
|
+
if (data.date === today) {
|
|
117
|
+
return data.count || 0;
|
|
118
|
+
}
|
|
119
|
+
} catch (e) {}
|
|
120
|
+
return 0;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function incrementDailyUsage() {
|
|
124
|
+
const file = getUsageFilePath();
|
|
125
|
+
const today = new Date().toISOString().split('T')[0];
|
|
126
|
+
let count = 0;
|
|
127
|
+
if (fs.existsSync(file)) {
|
|
128
|
+
try {
|
|
129
|
+
const data = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
130
|
+
if (data.date === today) count = data.count || 0;
|
|
131
|
+
} catch (e) {}
|
|
132
|
+
}
|
|
133
|
+
count++;
|
|
134
|
+
try {
|
|
135
|
+
fs.writeFileSync(file, JSON.stringify({ date: today, count }), 'utf8');
|
|
136
|
+
} catch (e) {}
|
|
137
|
+
return count;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function checkFreeTierLimit(isPro) {
|
|
141
|
+
if (isPro || process.env.NODE_ENV === 'test' || process.env.PRIVACYSCRUBBER_TEST_MODE === '1') return { blocked: false, count: 0 };
|
|
142
|
+
|
|
143
|
+
const count = incrementDailyUsage();
|
|
144
|
+
// Feedback / Growth loop triggers
|
|
145
|
+
if (count === 3) {
|
|
146
|
+
mcpLog(`${colors.cyan}💬 We are building the ultimate privacy tool for developers. What feature should we add next? Let us know: https://privacyscrubber.com/feedback${colors.reset}\n`);
|
|
147
|
+
}
|
|
148
|
+
if (count === STAR_PROMPT_THRESHOLD) {
|
|
149
|
+
mcpLog(`${colors.yellowBold}🔒 PrivacyScrubber is running securely. If this tool saved your PII today, please drop a star on GitHub: https://github.com/moxno/privacyscrubber-mcp${colors.reset}\n`);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (count >= FREE_TIER_DAILY_LIMIT) {
|
|
153
|
+
mcpLog(`${colors.redBold}🚫 [PrivacyScrubber] Free tier daily limit exhausted (${FREE_TIER_DAILY_LIMIT} requests). Request BLOCKED.${colors.reset}\n${colors.cyan}👉 Get a PRO key for unlimited use: https://privacyscrubber.com/pricing${colors.reset}\n`);
|
|
154
|
+
return { blocked: true, count };
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return { blocked: false, count };
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function buildCisoAuditTelemetry(currentTokenMap = {}) {
|
|
161
|
+
const entities = {};
|
|
162
|
+
let totalCount = 0;
|
|
163
|
+
|
|
164
|
+
if (currentTokenMap && typeof currentTokenMap === 'object') {
|
|
165
|
+
const keys = Array.isArray(currentTokenMap) ? currentTokenMap : Object.keys(currentTokenMap);
|
|
166
|
+
for (const t of keys) {
|
|
167
|
+
const tokenStr = typeof t === 'string' ? t : (t.token || t.mask || '');
|
|
168
|
+
const match = tokenStr.match(/\[([A-Z_]+)_\d+\]/);
|
|
169
|
+
const baseType = match ? match[1] : (tokenStr.replace(/\[|\]/g, '').replace(/_[0-9]+$/, '') || 'CUSTOM');
|
|
170
|
+
entities[baseType] = (entities[baseType] || 0) + 1;
|
|
171
|
+
totalCount++;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const types = Object.keys(entities);
|
|
176
|
+
let riskLevel = 'LOW EXPOSURE';
|
|
177
|
+
|
|
178
|
+
if (totalCount > 0) {
|
|
179
|
+
const hasHighRiskEntities = types.some(t => ['ID', 'SSN', 'CREDIT_CARD', 'PASSPORT', 'BANK', 'API_KEY', 'SECRET', 'PASSWORD', 'MRN', 'KEY'].includes(t));
|
|
180
|
+
if (hasHighRiskEntities || types.length >= 3 || totalCount >= 10) {
|
|
181
|
+
riskLevel = 'CRITICAL (HIGH EXPOSURE)';
|
|
182
|
+
} else if (types.length >= 2 || totalCount >= 3) {
|
|
183
|
+
riskLevel = 'MODERATE EXPOSURE';
|
|
184
|
+
} else {
|
|
185
|
+
riskLevel = 'LOW EXPOSURE';
|
|
186
|
+
}
|
|
187
|
+
} else {
|
|
188
|
+
riskLevel = 'CLEAN (ZERO PII)';
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const frameworksSet = new Set(['ZTDS Standard']);
|
|
192
|
+
if (types.includes('NAME') || types.includes('EMAIL') || types.includes('PHONE')) {
|
|
193
|
+
frameworksSet.add('GDPR (Art. 4)');
|
|
194
|
+
frameworksSet.add('CCPA/CPRA');
|
|
195
|
+
}
|
|
196
|
+
if (types.includes('ID') || types.includes('SSN') || types.includes('PASSPORT')) {
|
|
197
|
+
frameworksSet.add('SOC 2 Type II');
|
|
198
|
+
frameworksSet.add('ISO 27001 (A.8.11)');
|
|
199
|
+
}
|
|
200
|
+
if (types.some(t => ['CREDIT_CARD', 'BANK', 'IBAN', 'FINANCIAL', 'CARD'].includes(t))) {
|
|
201
|
+
frameworksSet.add('PCI DSS v4.0');
|
|
202
|
+
}
|
|
203
|
+
if (types.some(t => ['MRN', 'HEALTH', 'MEDICAL', 'PATIENT'].includes(t))) {
|
|
204
|
+
frameworksSet.add('HIPAA §164.514');
|
|
205
|
+
}
|
|
206
|
+
if (types.some(t => ['API_KEY', 'SECRET', 'PASSWORD', 'TOKEN', 'KEY'].includes(t))) {
|
|
207
|
+
frameworksSet.add('NIST SP 800-53');
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
return {
|
|
211
|
+
totalCount,
|
|
212
|
+
entities,
|
|
213
|
+
types,
|
|
214
|
+
riskLevel,
|
|
215
|
+
frameworksList: Array.from(frameworksSet)
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function formatAuditReceipt(telemetry) {
|
|
220
|
+
if (telemetry.totalCount === 0) {
|
|
221
|
+
return "\\n\\n> 🛡️ **PrivacyScrubber Audit Receipt**: CLEAN (ZERO PII DETECTED)\\n";
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const entitiesList = Object.entries(telemetry.entities)
|
|
225
|
+
.map(([type, count]) => `${count} ${type}`)
|
|
226
|
+
.join(', ');
|
|
227
|
+
|
|
228
|
+
const icon = telemetry.riskLevel.includes('CRITICAL') ? '🔴' : (telemetry.riskLevel.includes('MODERATE') ? '🟠' : '🟢');
|
|
229
|
+
|
|
230
|
+
return `\\n\\n> 🛡️ **PrivacyScrubber Audit Receipt**\\n> * **Risk Level:** ${icon} ${telemetry.riskLevel}\\n> * **Compliance Enforced:** ${telemetry.frameworksList.join(', ')}\\n> * **Tokens Masked:** ${telemetry.totalCount} (${entitiesList})\\n`;
|
|
231
|
+
}
|
|
95
232
|
|
|
96
233
|
// Build a clean upsell content block (Disabled to prevent agent-visible marketing content)
|
|
97
234
|
function buildUpsellBlock(reason) {
|
|
@@ -136,6 +273,11 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
136
273
|
profile: {
|
|
137
274
|
type: "string",
|
|
138
275
|
description: "The detection profile to use. Available: 'General' (Free), or PRO profiles: 'Dev' (Engineering/Code), 'Medical', 'Pharma', 'Legal', 'Compliance', 'CCPA', 'Finance', 'Bizops', 'Sales', 'WealthMgmt', 'Insurance', 'Accounting', 'HR', 'Security', 'Marketing', 'Support', 'RealEstate', 'Agents', 'Academic', 'Creative', 'Tech', 'Personal'. Defaults to 'General'."
|
|
276
|
+
},
|
|
277
|
+
ignore_list: {
|
|
278
|
+
type: "array",
|
|
279
|
+
items: { type: "string" },
|
|
280
|
+
description: "Optional list of plaintext values to skip during detection (false positives from previous scrubs). These values will also be persisted in the session ignore list for all future calls."
|
|
139
281
|
}
|
|
140
282
|
},
|
|
141
283
|
required: ["text"]
|
|
@@ -235,6 +377,43 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
235
377
|
required: []
|
|
236
378
|
}
|
|
237
379
|
},
|
|
380
|
+
{
|
|
381
|
+
name: "generate_compliance_report",
|
|
382
|
+
description: "Generates an official Zero-Trust Compliance Audit Certificate (GDPR, HIPAA, EU AI Act, SOC 2) for the current MCP session. Returns cryptographic session hash, masked entities breakdown, and compliance certification.",
|
|
383
|
+
inputSchema: {
|
|
384
|
+
type: "object",
|
|
385
|
+
properties: {
|
|
386
|
+
format: {
|
|
387
|
+
type: "string",
|
|
388
|
+
description: "The output format: 'markdown' (default), 'json', or 'summary'.",
|
|
389
|
+
enum: ["markdown", "json", "summary"]
|
|
390
|
+
},
|
|
391
|
+
company_name: {
|
|
392
|
+
type: "string",
|
|
393
|
+
description: "Optional organization or team name to brand the compliance report."
|
|
394
|
+
},
|
|
395
|
+
department: {
|
|
396
|
+
type: "string",
|
|
397
|
+
description: "Optional department or auditor ID (e.g. 'SecOps / Engineering')."
|
|
398
|
+
}
|
|
399
|
+
},
|
|
400
|
+
required: []
|
|
401
|
+
}
|
|
402
|
+
},
|
|
403
|
+
{
|
|
404
|
+
name: "mark_false_positive",
|
|
405
|
+
description: "Marks a previously detected token as a false positive. The original plaintext value will be excluded from all future sanitize_text calls in this session. Returns the restored original value and updated ignore list size.",
|
|
406
|
+
inputSchema: {
|
|
407
|
+
type: "object",
|
|
408
|
+
properties: {
|
|
409
|
+
token: {
|
|
410
|
+
type: "string",
|
|
411
|
+
description: "The token to mark as false positive (e.g., '[NAME_1]', '[EMAIL_2]')."
|
|
412
|
+
}
|
|
413
|
+
},
|
|
414
|
+
required: ["token"]
|
|
415
|
+
}
|
|
416
|
+
},
|
|
238
417
|
{
|
|
239
418
|
name: "check_status",
|
|
240
419
|
description: "Returns the current PrivacyScrubber MCP tier, session usage, available profiles, and PRO upgrade instructions. Call this to see your license status or get setup help.",
|
|
@@ -364,7 +543,12 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
364
543
|
|
|
365
544
|
try {
|
|
366
545
|
if (name === "sanitize_text") {
|
|
367
|
-
const { text, profile = "General" } = args || {};
|
|
546
|
+
const { text, profile = "General", ignore_list } = args || {};
|
|
547
|
+
|
|
548
|
+
// Merge per-call ignore_list into persistent sessionIgnoreList
|
|
549
|
+
if (Array.isArray(ignore_list)) {
|
|
550
|
+
ignore_list.forEach(v => { if (typeof v === 'string' && v.trim()) sessionIgnoreList.add(v.trim()); });
|
|
551
|
+
}
|
|
368
552
|
if (text === undefined || text === null) {
|
|
369
553
|
return {
|
|
370
554
|
isError: true,
|
|
@@ -385,7 +569,13 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
385
569
|
let finalProfile = targetProfile;
|
|
386
570
|
const extraBlocks = [];
|
|
387
571
|
|
|
388
|
-
|
|
572
|
+
const limitStatus = checkFreeTierLimit(license.isPro);
|
|
573
|
+
if (limitStatus.blocked) {
|
|
574
|
+
return {
|
|
575
|
+
isError: true,
|
|
576
|
+
content: [{ type: "text", text: `Error: Free tier daily limit exhausted (${FREE_TIER_DAILY_LIMIT} requests). Please set your PRO license key to continue. Get a key at: https://privacyscrubber.com/pricing` }]
|
|
577
|
+
};
|
|
578
|
+
}
|
|
389
579
|
|
|
390
580
|
const charLimit = (isAdvanced && !license.isPro) ? 5000 : 15000;
|
|
391
581
|
if (isAdvanced && !license.isPro) {
|
|
@@ -409,16 +599,14 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
409
599
|
}
|
|
410
600
|
}
|
|
411
601
|
|
|
412
|
-
const
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
extraBlocks.push(buildUpsellBlock(null));
|
|
417
|
-
}
|
|
602
|
+
const { scrubbedText, newTokens } = performSanitization(processedText, finalProfile, sessionIgnoreList);
|
|
603
|
+
|
|
604
|
+
const telemetry = buildCisoAuditTelemetry(newTokens);
|
|
605
|
+
const receiptMd = formatAuditReceipt(telemetry);
|
|
418
606
|
|
|
419
607
|
return {
|
|
420
608
|
content: [
|
|
421
|
-
{ type: "text", text:
|
|
609
|
+
{ type: "text", text: scrubbedText + receiptMd },
|
|
422
610
|
...extraBlocks.filter(Boolean)
|
|
423
611
|
]
|
|
424
612
|
};
|
|
@@ -464,6 +652,38 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
464
652
|
};
|
|
465
653
|
}
|
|
466
654
|
|
|
655
|
+
if (name === "mark_false_positive") {
|
|
656
|
+
const { token } = args || {};
|
|
657
|
+
if (!token || typeof token !== 'string') {
|
|
658
|
+
return {
|
|
659
|
+
isError: true,
|
|
660
|
+
content: [{ type: "text", text: "Error: Missing required parameter 'token'. Provide the token to mark as false positive (e.g., '[NAME_1]')." }]
|
|
661
|
+
};
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
const original = sessionMap[token];
|
|
665
|
+
if (!original) {
|
|
666
|
+
return {
|
|
667
|
+
isError: true,
|
|
668
|
+
content: [{ type: "text", text: `Error: Token '${token}' not found in current session map. Available tokens: ${Object.keys(sessionMap).join(', ') || '(empty session)'}` }]
|
|
669
|
+
};
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
// Add the original plaintext to the ignore list
|
|
673
|
+
sessionIgnoreList.add(original);
|
|
674
|
+
// Remove the token from the session map
|
|
675
|
+
delete sessionMap[token];
|
|
676
|
+
|
|
677
|
+
mcpLog(`${colors.yellow}🔖 [PrivacyScrubber] False Positive: '${token}' → '${original}' will be excluded from future scrubs.${colors.reset}\n`);
|
|
678
|
+
|
|
679
|
+
return {
|
|
680
|
+
content: [{
|
|
681
|
+
type: "text",
|
|
682
|
+
text: `✅ Marked '${token}' as false positive.\n\n**Restored value:** ${original}\n**Session ignore list size:** ${sessionIgnoreList.size}\n\nThis value will be excluded from all future \`sanitize_text\` calls in this session.`
|
|
683
|
+
}]
|
|
684
|
+
};
|
|
685
|
+
}
|
|
686
|
+
|
|
467
687
|
if (name === "sanitize_file") {
|
|
468
688
|
const rawPath = args.file_path || args.filePath;
|
|
469
689
|
if (!rawPath) {
|
|
@@ -532,7 +752,13 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
532
752
|
let finalProfile = targetProfile;
|
|
533
753
|
const extraBlocks = [];
|
|
534
754
|
|
|
535
|
-
|
|
755
|
+
const limitStatus = checkFreeTierLimit(license.isPro);
|
|
756
|
+
if (limitStatus.blocked) {
|
|
757
|
+
return {
|
|
758
|
+
isError: true,
|
|
759
|
+
content: [{ type: "text", text: `Error: Free tier daily limit exhausted (${FREE_TIER_DAILY_LIMIT} requests). Please set your PRO license key to continue. Get a key at: https://privacyscrubber.com/pricing` }]
|
|
760
|
+
};
|
|
761
|
+
}
|
|
536
762
|
|
|
537
763
|
const charLimit = (isAdvanced && !license.isPro) ? 5000 : 15000;
|
|
538
764
|
if (isAdvanced && !license.isPro) {
|
|
@@ -554,15 +780,15 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
554
780
|
}
|
|
555
781
|
}
|
|
556
782
|
|
|
557
|
-
const sanitized = performSanitization(processedContent, finalProfile);
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
}
|
|
783
|
+
const sanitized = performSanitization(processedContent, finalProfile, sessionIgnoreList);
|
|
784
|
+
const { scrubbedText, newTokens } = sanitized;
|
|
785
|
+
const telemetry = buildCisoAuditTelemetry(newTokens);
|
|
786
|
+
const receiptMarkdown = formatAuditReceipt(telemetry);
|
|
787
|
+
const combinedOutput = `${scrubbedText}\n\n${receiptMarkdown}`;
|
|
562
788
|
|
|
563
789
|
return {
|
|
564
790
|
content: [
|
|
565
|
-
{ type: "text", text:
|
|
791
|
+
{ type: "text", text: combinedOutput },
|
|
566
792
|
...extraBlocks.filter(Boolean)
|
|
567
793
|
]
|
|
568
794
|
};
|
|
@@ -604,7 +830,13 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
604
830
|
let finalProfile = targetProfile;
|
|
605
831
|
const extraBlocks = [];
|
|
606
832
|
|
|
607
|
-
|
|
833
|
+
const limitStatus = checkFreeTierLimit(license.isPro);
|
|
834
|
+
if (limitStatus.blocked) {
|
|
835
|
+
return {
|
|
836
|
+
isError: true,
|
|
837
|
+
content: [{ type: "text", text: `Error: Free tier daily limit exhausted (${FREE_TIER_DAILY_LIMIT} requests). Please set your PRO license key to continue. Get a key at: https://privacyscrubber.com/pricing` }]
|
|
838
|
+
};
|
|
839
|
+
}
|
|
608
840
|
|
|
609
841
|
const charLimit = (isAdvanced && !license.isPro) ? 5000 : 15000;
|
|
610
842
|
if (isAdvanced && !license.isPro) {
|
|
@@ -626,15 +858,15 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
626
858
|
}
|
|
627
859
|
}
|
|
628
860
|
|
|
629
|
-
const sanitized = performSanitization(processedContent, finalProfile);
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
}
|
|
861
|
+
const sanitized = performSanitization(processedContent, finalProfile, sessionIgnoreList);
|
|
862
|
+
const { scrubbedText, newTokens } = sanitized;
|
|
863
|
+
const telemetry = buildCisoAuditTelemetry(newTokens);
|
|
864
|
+
const receiptMarkdown = formatAuditReceipt(telemetry);
|
|
865
|
+
const combinedOutput = `${scrubbedText}\n\n${receiptMarkdown}`;
|
|
634
866
|
|
|
635
867
|
return {
|
|
636
868
|
content: [
|
|
637
|
-
{ type: "text", text:
|
|
869
|
+
{ type: "text", text: combinedOutput },
|
|
638
870
|
...extraBlocks.filter(Boolean)
|
|
639
871
|
]
|
|
640
872
|
};
|
|
@@ -663,7 +895,13 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
663
895
|
let finalProfile = targetProfile;
|
|
664
896
|
const extraBlocks = [];
|
|
665
897
|
|
|
666
|
-
|
|
898
|
+
const limitStatus = checkFreeTierLimit(license.isPro);
|
|
899
|
+
if (limitStatus.blocked) {
|
|
900
|
+
return {
|
|
901
|
+
isError: true,
|
|
902
|
+
content: [{ type: "text", text: `Error: Free tier daily limit exhausted (${FREE_TIER_DAILY_LIMIT} requests). Please set your PRO license key to continue. Get a key at: https://privacyscrubber.com/pricing` }]
|
|
903
|
+
};
|
|
904
|
+
}
|
|
667
905
|
|
|
668
906
|
const charLimit = (isAdvanced && !license.isPro) ? 5000 : 15000;
|
|
669
907
|
if (isAdvanced && !license.isPro) {
|
|
@@ -685,15 +923,15 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
685
923
|
}
|
|
686
924
|
}
|
|
687
925
|
|
|
688
|
-
const sanitized = performSanitization(processedContent, finalProfile);
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
}
|
|
926
|
+
const sanitized = performSanitization(processedContent, finalProfile, sessionIgnoreList);
|
|
927
|
+
const { scrubbedText, newTokens } = sanitized;
|
|
928
|
+
const telemetry = buildCisoAuditTelemetry(newTokens);
|
|
929
|
+
const receiptMarkdown = formatAuditReceipt(telemetry);
|
|
930
|
+
const combinedOutput = `${scrubbedText}\n\n${receiptMarkdown}`;
|
|
693
931
|
|
|
694
932
|
return {
|
|
695
933
|
content: [
|
|
696
|
-
{ type: "text", text:
|
|
934
|
+
{ type: "text", text: combinedOutput },
|
|
697
935
|
...extraBlocks.filter(Boolean)
|
|
698
936
|
]
|
|
699
937
|
};
|
|
@@ -736,7 +974,13 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
736
974
|
let finalProfile = targetProfile;
|
|
737
975
|
const extraBlocks = [];
|
|
738
976
|
|
|
739
|
-
|
|
977
|
+
const limitStatus = checkFreeTierLimit(license.isPro);
|
|
978
|
+
if (limitStatus.blocked) {
|
|
979
|
+
return {
|
|
980
|
+
isError: true,
|
|
981
|
+
content: [{ type: "text", text: `Error: Free tier daily limit exhausted (${FREE_TIER_DAILY_LIMIT} requests). Please set your PRO license key to continue. Get a key at: https://privacyscrubber.com/pricing` }]
|
|
982
|
+
};
|
|
983
|
+
}
|
|
740
984
|
|
|
741
985
|
const charLimit = (isAdvanced && !license.isPro) ? 5000 : 15000;
|
|
742
986
|
if (isAdvanced && !license.isPro) {
|
|
@@ -758,15 +1002,13 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
758
1002
|
}
|
|
759
1003
|
}
|
|
760
1004
|
|
|
761
|
-
const
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
extraBlocks.push(buildUpsellBlock(null));
|
|
765
|
-
}
|
|
1005
|
+
const { scrubbedText, newTokens } = performSanitization(processedContent2, finalProfile, sessionIgnoreList);
|
|
1006
|
+
const telemetry = buildCisoAuditTelemetry(newTokens);
|
|
1007
|
+
const receiptMd = formatAuditReceipt(telemetry);
|
|
766
1008
|
|
|
767
1009
|
return {
|
|
768
1010
|
content: [
|
|
769
|
-
{ type: "text", text:
|
|
1011
|
+
{ type: "text", text: scrubbedText + receiptMd },
|
|
770
1012
|
...extraBlocks.filter(Boolean)
|
|
771
1013
|
]
|
|
772
1014
|
};
|
|
@@ -793,6 +1035,14 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
793
1035
|
const customRules = loadCustomRules();
|
|
794
1036
|
const normalizedProfile = (profile || "general").trim().toLowerCase();
|
|
795
1037
|
const license = checkLicenseStatus();
|
|
1038
|
+
|
|
1039
|
+
const limitStatus = checkFreeTierLimit(license.isPro);
|
|
1040
|
+
if (limitStatus.blocked) {
|
|
1041
|
+
return {
|
|
1042
|
+
isError: true,
|
|
1043
|
+
content: [{ type: "text", text: `Error: Free tier daily limit exhausted (${FREE_TIER_DAILY_LIMIT} requests). Please set your PRO license key to continue. Get a key at: https://privacyscrubber.com/pricing` }]
|
|
1044
|
+
};
|
|
1045
|
+
}
|
|
796
1046
|
const localSessionMap = {};
|
|
797
1047
|
|
|
798
1048
|
const result = PrivacyScrubberCore.scrubText(content, customRules, {}, normalizedProfile, localSessionMap, license.isPro);
|
|
@@ -943,7 +1193,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
943
1193
|
`║ PrivacyScrubber MCP Server v${MCP_VERSION.padEnd(10)} ║`,
|
|
944
1194
|
'╠══════════════════════════════════════════════════╣',
|
|
945
1195
|
`║ ${tierIcon} Tier: ${tier.padEnd(43)}║`,
|
|
946
|
-
`║ 📊 Session requests: ${String(
|
|
1196
|
+
`║ 📊 Session requests: ${String(getDailyUsage()).padEnd(27)}║`,
|
|
947
1197
|
`║ 📁 Input size limit: ${sizeLimit.padEnd(27)}║`,
|
|
948
1198
|
'╠══════════════════════════════════════════════════╣',
|
|
949
1199
|
`║ 🏷️ Profiles: ${profileList.substring(0,35).padEnd(35)}║`,
|
|
@@ -1013,20 +1263,30 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1013
1263
|
const customRules = loadCustomRules();
|
|
1014
1264
|
const normalizedProfile = (profile || "general").trim().toLowerCase();
|
|
1015
1265
|
const license = checkLicenseStatus();
|
|
1266
|
+
|
|
1267
|
+
const limitStatus = checkFreeTierLimit(license.isPro);
|
|
1268
|
+
if (limitStatus.blocked) {
|
|
1269
|
+
return {
|
|
1270
|
+
isError: true,
|
|
1271
|
+
content: [{ type: "text", text: `Error: Free tier daily limit exhausted (${FREE_TIER_DAILY_LIMIT} requests). Please set your PRO license key to continue. Get a key at: https://privacyscrubber.com/pricing` }]
|
|
1272
|
+
};
|
|
1273
|
+
}
|
|
1016
1274
|
|
|
1275
|
+
let totalTokensForDirectory = {};
|
|
1017
1276
|
for (const file of filesToScan) {
|
|
1018
1277
|
try {
|
|
1019
1278
|
const stat = await fs.promises.stat(file);
|
|
1020
1279
|
if (stat.size > 10 * 1024 * 1024) continue; // skip >10MB
|
|
1021
1280
|
|
|
1022
1281
|
const buffer = fs.readFileSync(file);
|
|
1023
|
-
// basic binary check
|
|
1024
1282
|
if (buffer.indexOf(0) !== -1) continue;
|
|
1025
1283
|
const content = buffer.toString('utf8');
|
|
1026
1284
|
|
|
1027
1285
|
const localSessionMap = {};
|
|
1028
1286
|
const result = PrivacyScrubberCore.scrubText(content, customRules, {}, normalizedProfile, localSessionMap, license.isPro);
|
|
1029
1287
|
|
|
1288
|
+
Object.assign(totalTokensForDirectory, localSessionMap);
|
|
1289
|
+
|
|
1030
1290
|
let isSecretLeak = false;
|
|
1031
1291
|
if (!license.isPro && result.tokenMap) {
|
|
1032
1292
|
Object.entries(result.tokenMap).forEach(([token, original]) => {
|
|
@@ -1062,8 +1322,86 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1062
1322
|
reportLines.push("✅ Clean. No PII or secrets detected in the scanned files.");
|
|
1063
1323
|
}
|
|
1064
1324
|
|
|
1325
|
+
const telemetry = buildCisoAuditTelemetry(totalTokensForDirectory);
|
|
1326
|
+
const receiptMd = formatAuditReceipt(telemetry);
|
|
1327
|
+
|
|
1328
|
+
return {
|
|
1329
|
+
content: [{ type: "text", text: reportLines.join('\n') + receiptMd }]
|
|
1330
|
+
};
|
|
1331
|
+
}
|
|
1332
|
+
|
|
1333
|
+
if (name === "generate_compliance_report") {
|
|
1334
|
+
const { format = "markdown", company_name = "PrivacyScrubber Client", department = "SecOps / Compliance" } = args || {};
|
|
1335
|
+
const telemetry = buildCisoAuditTelemetry(sessionMap);
|
|
1336
|
+
|
|
1337
|
+
const sessionHash = crypto.createHash('sha256')
|
|
1338
|
+
.update(JSON.stringify(sessionMap) + Date.now().toString())
|
|
1339
|
+
.digest('hex');
|
|
1340
|
+
|
|
1341
|
+
const timestamp = new Date().toISOString();
|
|
1342
|
+
const entitySummary = Object.entries(telemetry.entities)
|
|
1343
|
+
.map(([t, count]) => `[${t}]: ${count}`)
|
|
1344
|
+
.join(', ') || 'None (Clean)';
|
|
1345
|
+
|
|
1346
|
+
if (format === "json") {
|
|
1347
|
+
const jsonReport = {
|
|
1348
|
+
protocol: "Zero-Trust Data Sanitization (ZTDS)",
|
|
1349
|
+
certificate: `ZTDS-CERT-${sessionHash.substring(0, 16).toUpperCase()}`,
|
|
1350
|
+
company: company_name,
|
|
1351
|
+
department: department,
|
|
1352
|
+
timestamp: timestamp,
|
|
1353
|
+
session_hash: sessionHash,
|
|
1354
|
+
verification_mode: "100% Offline (Local In-Memory RAM)",
|
|
1355
|
+
compliance_status: "VERIFIED PASS",
|
|
1356
|
+
risk_level: telemetry.riskLevel,
|
|
1357
|
+
frameworks_enforced: telemetry.frameworksList,
|
|
1358
|
+
total_masked_tokens: telemetry.totalCount,
|
|
1359
|
+
entities_breakdown: telemetry.entities,
|
|
1360
|
+
zero_egress_verified: true,
|
|
1361
|
+
verification_url: `https://privacyscrubber.com/features/audit-receipt/#verify?hash=${sessionHash.substring(0, 16)}`
|
|
1362
|
+
};
|
|
1363
|
+
|
|
1364
|
+
return {
|
|
1365
|
+
content: [{ type: "text", text: JSON.stringify(jsonReport, null, 2) }]
|
|
1366
|
+
};
|
|
1367
|
+
}
|
|
1368
|
+
|
|
1369
|
+
if (format === "summary") {
|
|
1370
|
+
const summaryText = `[PrivacyScrubber Compliance Certificate] ID: ZTDS-${sessionHash.substring(0, 8).toUpperCase()} | Organization: ${company_name} | Masked Tokens: ${telemetry.totalCount} | Risk: ${telemetry.riskLevel} | Frameworks: ${telemetry.frameworksList.join(', ')} | Status: PASS (100% Air-Gapped)`;
|
|
1371
|
+
return {
|
|
1372
|
+
content: [{ type: "text", text: summaryText }]
|
|
1373
|
+
};
|
|
1374
|
+
}
|
|
1375
|
+
|
|
1376
|
+
// Default: Comprehensive Markdown Certificate
|
|
1377
|
+
const mdReport = `# 🛡️ Zero-Trust Data Sanitization Compliance Certificate
|
|
1378
|
+
**Certificate ID:** \`ZTDS-CERT-${sessionHash.substring(0, 16).toUpperCase()}\`
|
|
1379
|
+
**Organization:** ${company_name} (${department})
|
|
1380
|
+
**Timestamp:** ${timestamp}
|
|
1381
|
+
**Verification Mode:** 100% Local In-Memory Processing (Air-Gapped)
|
|
1382
|
+
**Status:** **VERIFIED PASS** (Zero Network Egress)
|
|
1383
|
+
|
|
1384
|
+
---
|
|
1385
|
+
|
|
1386
|
+
### 📊 Sanitization Metrics & Risk Assessment
|
|
1387
|
+
* **Overall Risk Rating:** **${telemetry.riskLevel}**
|
|
1388
|
+
* **Total Sensitive Entities Masked:** \`${telemetry.totalCount}\`
|
|
1389
|
+
* **Entity Breakdown:** ${entitySummary}
|
|
1390
|
+
* **Network Data Transmitted:** \`0.00 KB (Zero-Trust Local RAM)\`
|
|
1391
|
+
|
|
1392
|
+
### 📜 Regulatory Frameworks Enforced
|
|
1393
|
+
${telemetry.frameworksList.map(f => `- **${f}**`).join('\n')}
|
|
1394
|
+
|
|
1395
|
+
### 🔒 CISO Compliance Declaration
|
|
1396
|
+
1. **EU AI Act (Art. 50) & GDPR (Art. 25 & 32):** Data minimization and local pseudonymization enforced prior to model interaction.
|
|
1397
|
+
2. **HIPAA Safe Harbor (§164.514) / SOC 2 Type II:** All direct and indirect identifiers sanitized locally without cloud processor liability.
|
|
1398
|
+
3. **Cryptographic Verification:** Tamper-evident session verification hash: \`${sessionHash}\`
|
|
1399
|
+
|
|
1400
|
+
*Certified Offline by PrivacyScrubber Engine v${MCP_VERSION}*
|
|
1401
|
+
*Verify at: https://privacyscrubber.com/features/audit-receipt/*`;
|
|
1402
|
+
|
|
1065
1403
|
return {
|
|
1066
|
-
content: [{ type: "text", text:
|
|
1404
|
+
content: [{ type: "text", text: mdReport }]
|
|
1067
1405
|
};
|
|
1068
1406
|
}
|
|
1069
1407
|
|
|
@@ -1160,20 +1498,22 @@ function detectSecrets(text) {
|
|
|
1160
1498
|
return detected;
|
|
1161
1499
|
}
|
|
1162
1500
|
|
|
1163
|
-
function performSanitization(text, profile) {
|
|
1501
|
+
function performSanitization(text, profile, ignoreList = null) {
|
|
1164
1502
|
const customRules = loadCustomRules();
|
|
1165
1503
|
const normalizedProfile = (profile || "general").trim().toLowerCase();
|
|
1166
1504
|
const license = checkLicenseStatus();
|
|
1167
|
-
const result = PrivacyScrubberCore.scrubText(text, customRules, {}, normalizedProfile, sessionMap, license.isPro);
|
|
1505
|
+
const result = PrivacyScrubberCore.scrubText(text, customRules, {}, normalizedProfile, sessionMap, license.isPro, ignoreList);
|
|
1168
1506
|
|
|
1507
|
+
const newTokens = {};
|
|
1169
1508
|
// Update our volatile map with new matches
|
|
1170
1509
|
if (result.tokenMap) {
|
|
1171
1510
|
Object.entries(result.tokenMap).forEach(([token, original]) => {
|
|
1172
1511
|
sessionMap[token] = original;
|
|
1512
|
+
newTokens[token] = original;
|
|
1173
1513
|
});
|
|
1174
1514
|
}
|
|
1175
1515
|
|
|
1176
|
-
return result.scrubbedText;
|
|
1516
|
+
return { scrubbedText: result.scrubbedText, newTokens };
|
|
1177
1517
|
}
|
|
1178
1518
|
|
|
1179
1519
|
function truncateIfFree(text, isPro, charLimit = 15000) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@privacyscrubber/mcp-server",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.0",
|
|
4
4
|
"mcpName": "io.github.moxno/privacyscrubber-mcp",
|
|
5
5
|
"description": "CISO-Approved Zero-Trust PII & Secrets Redaction MCP Server for Cursor, Windsurf, and Claude Desktop. Prevent API leaks and comply with HIPAA/SOC 2 locally.",
|
|
6
6
|
"main": "index.js",
|
|
@@ -13,6 +13,10 @@
|
|
|
13
13
|
"install.js",
|
|
14
14
|
"pii-masking-run.js",
|
|
15
15
|
"scrubber-core.cjs",
|
|
16
|
+
"ps-license-manager.js",
|
|
17
|
+
"ps-pii-engine.cjs",
|
|
18
|
+
"ps-pii-engine.js",
|
|
19
|
+
"config-schema.json",
|
|
16
20
|
"smithery.yaml",
|
|
17
21
|
"server.json",
|
|
18
22
|
"manifest.json",
|