@privacyscrubber/mcp-server 1.6.7 β 1.6.8
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 +1 -1
- package/README.md +17 -5
- package/bundle.mcpb +0 -0
- package/index.js +340 -15
- package/manifest.json +2 -2
- package/package.json +21 -8
- package/scrubber-core.cjs +1 -1
- package/server.json +2 -2
package/README.md
CHANGED
|
@@ -6,8 +6,8 @@
|
|
|
6
6
|
[](https://privacyscrubber.com)
|
|
7
7
|
[](https://privacyscrubber.com)
|
|
8
8
|
|
|
9
|
-
**Zero-Trust
|
|
10
|
-
Locally scrubs PII, secrets, credentials, and custom regex rules from files and text contexts before they reach remote LLM providers.
|
|
9
|
+
**CISO-Approved Zero-Trust PII & Secrets Redaction MCP Server for Cursor, Windsurf, and Claude Desktop.**
|
|
10
|
+
Locally scrubs PII, secrets, credentials, and custom regex rules from files and text contexts before they reach remote LLM providers to prevent API leaks and ensure HIPAA/SOC 2 compliance at the developer endpoint.
|
|
11
11
|
|
|
12
12
|
---
|
|
13
13
|
|
|
@@ -149,11 +149,23 @@ Looking for real-time protection directly inside your web browser?
|
|
|
149
149
|
* **Chrome Extension:** Get the [PrivacyScrubber Chrome Extension](https://chromewebstore.google.com/detail/privacyscrubber-%E2%80%94-pii-red/pimoejgefeilajmmbpghifdmhdlkgjol) to sanitize prompts directly inside ChatGPT, Claude, and Gemini in real-time.
|
|
150
150
|
* **Web Sandbox:** Use the zero-server browser sanitization tools at [PrivacyScrubber Homepage](https://privacyscrubber.com/).
|
|
151
151
|
|
|
152
|
-
---
|
|
153
|
-
|
|
154
152
|
## π License & Commercial Upgrade
|
|
155
153
|
|
|
156
|
-
|
|
154
|
+
By default, the server runs under the **Free Tier** (restricted to 50,000 characters per request and the basic `General` PII profile). To unlock advanced engineering, medical, legal, and financial PII profiles, as well as team-wide custom rules, you can purchase a commercial license.
|
|
155
|
+
|
|
156
|
+
### Feature Comparison
|
|
157
|
+
|
|
158
|
+
| Feature | Free Tier | PRO Tier | TEAMS Tier |
|
|
159
|
+
| :--- | :--- | :--- | :--- |
|
|
160
|
+
| **Volatile Tokenization** | β
Yes | β
Yes | β
Yes |
|
|
161
|
+
| **Standard PII Masking** | β
Yes | β
Yes | β
Yes |
|
|
162
|
+
| **Max Character Length** | 50,000 chars | βΎοΈ Unlimited | βΎοΈ Unlimited |
|
|
163
|
+
| **Industry Profiles** | General Only | 22+ Profiles | 22+ Profiles |
|
|
164
|
+
| **Custom Regex Rules** | β Locked | βΎοΈ Unlimited | βΎοΈ Unlimited |
|
|
165
|
+
| **Team Rules Sync (GPO)** | β No | β No | β
Yes (Shared Link) |
|
|
166
|
+
| **Licensing Cost** | $0 | **$110 Lifetime** | **$99/mo Flat Rate** |
|
|
167
|
+
|
|
168
|
+
π **[Acquire a PRO / TEAMS License Key at privacyscrubber.com/pricing](https://privacyscrubber.com/pricing)**
|
|
157
169
|
|
|
158
170
|
---
|
|
159
171
|
|
package/bundle.mcpb
CHANGED
|
Binary file
|
package/index.js
CHANGED
|
@@ -24,6 +24,11 @@ const require = createRequire(import.meta.url);
|
|
|
24
24
|
const __filename = fileURLToPath(import.meta.url);
|
|
25
25
|
const __dirname = path.dirname(__filename);
|
|
26
26
|
|
|
27
|
+
// Version is sourced from mcp-server/package.json.
|
|
28
|
+
// build.js syncs mcp-server/package.json from root package.json on every build.
|
|
29
|
+
// Never hardcode the version β bump package.json at root instead.
|
|
30
|
+
const MCP_VERSION = require('./package.json').version;
|
|
31
|
+
|
|
27
32
|
// Import the production core engine with 100% parity
|
|
28
33
|
const scrubberCorePath = path.resolve(__dirname, './scrubber-core.cjs');
|
|
29
34
|
const PrivacyScrubberCore = require(scrubberCorePath);
|
|
@@ -34,6 +39,24 @@ PrivacyScrubberCore.init();
|
|
|
34
39
|
// Volatile in-memory token map
|
|
35
40
|
const sessionMap = {};
|
|
36
41
|
|
|
42
|
+
// ANSI terminal color helpers
|
|
43
|
+
const colors = {
|
|
44
|
+
yellowBold: '\x1b[1;33m',
|
|
45
|
+
yellow: '\x1b[33m',
|
|
46
|
+
cyan: '\x1b[36m',
|
|
47
|
+
redBold: '\x1b[1;31m',
|
|
48
|
+
reset: '\x1b[0m'
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
// Secrets detection patterns β defined once here, shared by detectSecrets() and performSanitization()
|
|
52
|
+
const DEVOPS_SECRETS_DETECTOR = [
|
|
53
|
+
{ name: 'AWS Credentials', regex: /\b(?:AKIA|ASIA|AGPA|AIDA|AROA|AIPA)[A-Z0-9]{16}\b/g },
|
|
54
|
+
{ name: 'JSON Web Token (JWT)', regex: /\beyJ[a-zA-Z0-9_-]+\.[a-zA-Z0-9_-]+\.[a-zA-Z0-9_-]+\b/g },
|
|
55
|
+
{ 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 },
|
|
56
|
+
{ name: 'Stripe API Key', regex: /\b(?:[rs]k)_(?:test|live)_[a-zA-Z0-9]{24,}\b/g },
|
|
57
|
+
{ 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 }
|
|
58
|
+
];
|
|
59
|
+
|
|
37
60
|
// Hardcoded public key for offline cryptographic verification
|
|
38
61
|
const PUBLIC_KEY = `-----BEGIN PUBLIC KEY-----
|
|
39
62
|
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAw3f37srO402PU4++Baf8
|
|
@@ -117,7 +140,7 @@ function shouldNudge(isPro) {
|
|
|
117
140
|
const server = new Server(
|
|
118
141
|
{
|
|
119
142
|
name: "privacyscrubber/pii-masking-mcp",
|
|
120
|
-
version:
|
|
143
|
+
version: MCP_VERSION,
|
|
121
144
|
},
|
|
122
145
|
{
|
|
123
146
|
capabilities: {
|
|
@@ -180,6 +203,15 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
180
203
|
required: ["file_path"]
|
|
181
204
|
}
|
|
182
205
|
},
|
|
206
|
+
{
|
|
207
|
+
name: "create_default_config",
|
|
208
|
+
description: "Creates a default 'privacyscrubber.json' configuration file in the active workspace root directory if one does not exist. Includes template structures for custom regex rules and exclusion bypass patterns.",
|
|
209
|
+
inputSchema: {
|
|
210
|
+
type: "object",
|
|
211
|
+
properties: {},
|
|
212
|
+
required: []
|
|
213
|
+
}
|
|
214
|
+
},
|
|
183
215
|
{
|
|
184
216
|
name: "check_status",
|
|
185
217
|
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.",
|
|
@@ -224,7 +256,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
224
256
|
|
|
225
257
|
if (isAdvanced && !license.isPro) {
|
|
226
258
|
const reason = license.error ? ` (${license.error})` : "";
|
|
227
|
-
process.stderr.write(
|
|
259
|
+
process.stderr.write(`${colors.yellowBold}β οΈ [PrivacyScrubber] Advanced profile '${targetProfile}' is locked in the FREE tier. Falling back to 'General' profile.${reason}${colors.reset}\n${colors.cyan}π Get a PRO key at: https://privacyscrubber.com/pricing${colors.reset}\n`);
|
|
228
260
|
finalProfile = "General";
|
|
229
261
|
extraBlocks.push(buildUpsellBlock(`Profile '${targetProfile}' requires PRO. Using 'General' as fallback.`));
|
|
230
262
|
}
|
|
@@ -234,6 +266,14 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
234
266
|
extraBlocks.push(buildUpsellBlock(`Input was truncated to 50,000 characters (Free Tier limit).`));
|
|
235
267
|
}
|
|
236
268
|
|
|
269
|
+
if (!license.isPro) {
|
|
270
|
+
const detected = detectSecrets(processedText);
|
|
271
|
+
if (detected.length > 0) {
|
|
272
|
+
process.stderr.write(`${colors.yellowBold}β οΈ [PrivacyScrubber] API Key / Secret (${detected.join(', ')}) detected! Sanitization skipped (Requires PRO Profile).${colors.reset}\n${colors.cyan}π Upgrade at: https://privacyscrubber.com/pricing${colors.reset}\n`);
|
|
273
|
+
extraBlocks.push(buildUpsellBlock(`API Key / Secret (${detected.join(', ')}) detected! Sanitization skipped (Requires PRO Profile).`));
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
237
277
|
const sanitized = performSanitization(processedText, finalProfile);
|
|
238
278
|
|
|
239
279
|
// Periodic soft nudge every N free-tier requests (no trigger event needed)
|
|
@@ -263,6 +303,17 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
263
303
|
content: [{ type: "text", text: `Error: Parameter 'text' must be a string, got '${typeof text}'.` }]
|
|
264
304
|
};
|
|
265
305
|
}
|
|
306
|
+
|
|
307
|
+
// Warn early if session has no tokens β reveal would be a no-op
|
|
308
|
+
if (Object.keys(sessionMap).length === 0) {
|
|
309
|
+
return {
|
|
310
|
+
content: [{
|
|
311
|
+
type: "text",
|
|
312
|
+
text: `β οΈ Session map is empty β no tokens to restore. Call 'sanitize_text' or 'sanitize_file' first to build the token map, then pass the AI's response here.`
|
|
313
|
+
}]
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
|
|
266
317
|
const restored = PrivacyScrubberCore.unscrubText(text, sessionMap);
|
|
267
318
|
return {
|
|
268
319
|
content: [
|
|
@@ -346,7 +397,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
346
397
|
|
|
347
398
|
if (isAdvanced && !license.isPro) {
|
|
348
399
|
const reason = license.error ? ` (${license.error})` : "";
|
|
349
|
-
process.stderr.write(
|
|
400
|
+
process.stderr.write(`${colors.yellowBold}β οΈ [PrivacyScrubber] Advanced profile '${targetProfile}' is locked in the FREE tier. Falling back to 'General' profile.${reason}${colors.reset}\n${colors.cyan}π Get a PRO key at: https://privacyscrubber.com/pricing${colors.reset}\n`);
|
|
350
401
|
finalProfile = "General";
|
|
351
402
|
extraBlocks.push(buildUpsellBlock(`Profile '${targetProfile}' requires PRO. Using 'General' as fallback.`));
|
|
352
403
|
}
|
|
@@ -354,6 +405,14 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
354
405
|
const { processedText: processedContent, wasTruncated } = truncateIfFree(content, license.isPro);
|
|
355
406
|
if (wasTruncated) extraBlocks.push(buildUpsellBlock(`File content was truncated to 50,000 characters (Free Tier limit).`));
|
|
356
407
|
|
|
408
|
+
if (!license.isPro) {
|
|
409
|
+
const detected = detectSecrets(processedContent);
|
|
410
|
+
if (detected.length > 0) {
|
|
411
|
+
process.stderr.write(`${colors.yellowBold}β οΈ [PrivacyScrubber] API Key / Secret (${detected.join(', ')}) detected! Sanitization skipped (Requires PRO Profile).${colors.reset}\n${colors.cyan}π Upgrade at: https://privacyscrubber.com/pricing${colors.reset}\n`);
|
|
412
|
+
extraBlocks.push(buildUpsellBlock(`API Key / Secret (${detected.join(', ')}) detected! Sanitization skipped (Requires PRO Profile).`));
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
|
|
357
416
|
const sanitized = performSanitization(processedContent, finalProfile);
|
|
358
417
|
|
|
359
418
|
if (!license.isPro && extraBlocks.length === 0 && sessionRequestCount % UPSELL_EVERY_N === 0) {
|
|
@@ -375,7 +434,133 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
375
434
|
}
|
|
376
435
|
|
|
377
436
|
const buffer = fs.readFileSync(resolvedPath);
|
|
378
|
-
|
|
437
|
+
|
|
438
|
+
const isPdf = resolvedPath.toLowerCase().endsWith(".pdf");
|
|
439
|
+
const isExcel = resolvedPath.toLowerCase().endsWith(".xlsx") || resolvedPath.toLowerCase().endsWith(".xls");
|
|
440
|
+
|
|
441
|
+
if (isPdf || isExcel) {
|
|
442
|
+
const license = checkLicenseStatus();
|
|
443
|
+
if (!license.isPro) {
|
|
444
|
+
return {
|
|
445
|
+
isError: true,
|
|
446
|
+
content: [
|
|
447
|
+
{
|
|
448
|
+
type: "text",
|
|
449
|
+
text: `Error: PDF and Excel file sanitization is a PRO feature. Set PRIVACYSCRUBBER_KEY to your PRO license key to unlock local document parsing. Get a key at: https://privacyscrubber.com/pricing`
|
|
450
|
+
}
|
|
451
|
+
]
|
|
452
|
+
};
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
if (isPdf) {
|
|
456
|
+
try {
|
|
457
|
+
const pdf = require('pdf-parse');
|
|
458
|
+
const data = await pdf(buffer);
|
|
459
|
+
const content = data.text;
|
|
460
|
+
const targetProfile = profile.trim();
|
|
461
|
+
const isAdvanced = targetProfile.toLowerCase() !== "general";
|
|
462
|
+
|
|
463
|
+
let finalProfile = targetProfile;
|
|
464
|
+
const extraBlocks = [];
|
|
465
|
+
|
|
466
|
+
sessionRequestCount++;
|
|
467
|
+
|
|
468
|
+
if (isAdvanced && !license.isPro) {
|
|
469
|
+
const reason = license.error ? ` (${license.error})` : "";
|
|
470
|
+
process.stderr.write(`${colors.yellowBold}β οΈ [PrivacyScrubber] Advanced profile '${targetProfile}' is locked in the FREE tier. Falling back to 'General' profile.${reason}${colors.reset}\n${colors.cyan}π Get a PRO key at: https://privacyscrubber.com/pricing${colors.reset}\n`);
|
|
471
|
+
finalProfile = "General";
|
|
472
|
+
extraBlocks.push(buildUpsellBlock(`Profile '${targetProfile}' requires PRO. Using 'General' as fallback.`));
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
const { processedText: processedContent, wasTruncated } = truncateIfFree(content, license.isPro);
|
|
476
|
+
if (wasTruncated) extraBlocks.push(buildUpsellBlock(`File content was truncated to 50,000 characters (Free Tier limit).`));
|
|
477
|
+
|
|
478
|
+
if (!license.isPro) {
|
|
479
|
+
const detected = detectSecrets(processedContent);
|
|
480
|
+
if (detected.length > 0) {
|
|
481
|
+
process.stderr.write(`${colors.yellowBold}β οΈ [PrivacyScrubber] API Key / Secret (${detected.join(', ')}) detected! Sanitization skipped (Requires PRO Profile).${colors.reset}\n${colors.cyan}π Upgrade at: https://privacyscrubber.com/pricing${colors.reset}\n`);
|
|
482
|
+
extraBlocks.push(buildUpsellBlock(`API Key / Secret (${detected.join(', ')}) detected! Sanitization skipped (Requires PRO Profile).`));
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
const sanitized = performSanitization(processedContent, finalProfile);
|
|
487
|
+
|
|
488
|
+
if (!license.isPro && extraBlocks.length === 0 && sessionRequestCount % UPSELL_EVERY_N === 0) {
|
|
489
|
+
extraBlocks.push(buildUpsellBlock(null));
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
return {
|
|
493
|
+
content: [
|
|
494
|
+
{ type: "text", text: sanitized },
|
|
495
|
+
...extraBlocks
|
|
496
|
+
]
|
|
497
|
+
};
|
|
498
|
+
} catch (pdfError) {
|
|
499
|
+
return {
|
|
500
|
+
isError: true,
|
|
501
|
+
content: [{ type: "text", text: `Error: Failed to parse PDF file: ${pdfError.message}` }]
|
|
502
|
+
};
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
if (isExcel) {
|
|
507
|
+
try {
|
|
508
|
+
const XLSX = require('xlsx');
|
|
509
|
+
const workbook = XLSX.read(buffer, { type: 'buffer' });
|
|
510
|
+
let content = '';
|
|
511
|
+
workbook.SheetNames.forEach(sheetName => {
|
|
512
|
+
const worksheet = workbook.Sheets[sheetName];
|
|
513
|
+
content += `--- Sheet: ${sheetName} ---\n`;
|
|
514
|
+
content += XLSX.utils.sheet_to_csv(worksheet) + '\n';
|
|
515
|
+
});
|
|
516
|
+
|
|
517
|
+
const targetProfile = profile.trim();
|
|
518
|
+
const isAdvanced = targetProfile.toLowerCase() !== "general";
|
|
519
|
+
|
|
520
|
+
let finalProfile = targetProfile;
|
|
521
|
+
const extraBlocks = [];
|
|
522
|
+
|
|
523
|
+
sessionRequestCount++;
|
|
524
|
+
|
|
525
|
+
if (isAdvanced && !license.isPro) {
|
|
526
|
+
const reason = license.error ? ` (${license.error})` : "";
|
|
527
|
+
process.stderr.write(`${colors.yellowBold}β οΈ [PrivacyScrubber] Advanced profile '${targetProfile}' is locked in the FREE tier. Falling back to 'General' profile.${reason}${colors.reset}\n${colors.cyan}π Get a PRO key at: https://privacyscrubber.com/pricing${colors.reset}\n`);
|
|
528
|
+
finalProfile = "General";
|
|
529
|
+
extraBlocks.push(buildUpsellBlock(`Profile '${targetProfile}' requires PRO. Using 'General' as fallback.`));
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
const { processedText: processedContent, wasTruncated } = truncateIfFree(content, license.isPro);
|
|
533
|
+
if (wasTruncated) extraBlocks.push(buildUpsellBlock(`File content was truncated to 50,000 characters (Free Tier limit).`));
|
|
534
|
+
|
|
535
|
+
if (!license.isPro) {
|
|
536
|
+
const detected = detectSecrets(processedContent);
|
|
537
|
+
if (detected.length > 0) {
|
|
538
|
+
process.stderr.write(`${colors.yellowBold}β οΈ [PrivacyScrubber] API Key / Secret (${detected.join(', ')}) detected! Sanitization skipped (Requires PRO Profile).${colors.reset}\n${colors.cyan}π Upgrade at: https://privacyscrubber.com/pricing${colors.reset}\n`);
|
|
539
|
+
extraBlocks.push(buildUpsellBlock(`API Key / Secret (${detected.join(', ')}) detected! Sanitization skipped (Requires PRO Profile).`));
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
const sanitized = performSanitization(processedContent, finalProfile);
|
|
544
|
+
|
|
545
|
+
if (!license.isPro && extraBlocks.length === 0 && sessionRequestCount % UPSELL_EVERY_N === 0) {
|
|
546
|
+
extraBlocks.push(buildUpsellBlock(null));
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
return {
|
|
550
|
+
content: [
|
|
551
|
+
{ type: "text", text: sanitized },
|
|
552
|
+
...extraBlocks
|
|
553
|
+
]
|
|
554
|
+
};
|
|
555
|
+
} catch (xlsxError) {
|
|
556
|
+
return {
|
|
557
|
+
isError: true,
|
|
558
|
+
content: [{ type: "text", text: `Error: Failed to parse Excel file: ${xlsxError.message}` }]
|
|
559
|
+
};
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
|
|
379
564
|
// Check for null bytes in the first 8KB to detect binary files
|
|
380
565
|
let isBinary = false;
|
|
381
566
|
const checkLen = Math.min(buffer.length, 8000);
|
|
@@ -392,7 +577,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
392
577
|
content: [
|
|
393
578
|
{
|
|
394
579
|
type: "text",
|
|
395
|
-
text: `Error: Binary file format detected. The local MCP server only supports plain text files (e.g., source code, logs, CSV, markdown, JSON)
|
|
580
|
+
text: `Error: Binary file format detected. The local MCP server only supports plain text files (e.g., source code, logs, CSV, markdown, JSON) and premium document formats (PDF, DOCX, XLSX). To sanitize PDF or Excel files, please upgrade to PRO or use the web interface.`
|
|
396
581
|
}
|
|
397
582
|
]
|
|
398
583
|
};
|
|
@@ -410,7 +595,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
410
595
|
|
|
411
596
|
if (isAdvanced && !license.isPro) {
|
|
412
597
|
const reason = license.error ? ` (${license.error})` : "";
|
|
413
|
-
process.stderr.write(
|
|
598
|
+
process.stderr.write(`${colors.yellowBold}β οΈ [PrivacyScrubber] Advanced profile '${targetProfile}' is locked in the FREE tier. Falling back to 'General' profile.${reason}${colors.reset}\n${colors.cyan}π Get a PRO key at: https://privacyscrubber.com/pricing${colors.reset}\n`);
|
|
414
599
|
finalProfile = "General";
|
|
415
600
|
extraBlocks.push(buildUpsellBlock(`Profile '${targetProfile}' requires PRO. Using 'General' as fallback.`));
|
|
416
601
|
}
|
|
@@ -418,6 +603,14 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
418
603
|
const { processedText: processedContent2, wasTruncated: wasTruncated2 } = truncateIfFree(content, license.isPro);
|
|
419
604
|
if (wasTruncated2) extraBlocks.push(buildUpsellBlock(`File content was truncated to 50,000 characters (Free Tier limit).`));
|
|
420
605
|
|
|
606
|
+
if (!license.isPro) {
|
|
607
|
+
const detected = detectSecrets(processedContent2);
|
|
608
|
+
if (detected.length > 0) {
|
|
609
|
+
process.stderr.write(`${colors.yellowBold}β οΈ [PrivacyScrubber] API Key / Secret (${detected.join(', ')}) detected! Sanitization skipped (Requires PRO Profile).${colors.reset}\n${colors.cyan}π Upgrade at: https://privacyscrubber.com/pricing${colors.reset}\n`);
|
|
610
|
+
extraBlocks.push(buildUpsellBlock(`API Key / Secret (${detected.join(', ')}) detected! Sanitization skipped (Requires PRO Profile).`));
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
|
|
421
614
|
const sanitized = performSanitization(processedContent2, finalProfile);
|
|
422
615
|
|
|
423
616
|
if (!license.isPro && extraBlocks.length === 0 && sessionRequestCount % UPSELL_EVERY_N === 0) {
|
|
@@ -432,6 +625,47 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
432
625
|
};
|
|
433
626
|
}
|
|
434
627
|
|
|
628
|
+
if (name === "create_default_config") {
|
|
629
|
+
const configPath = path.resolve(process.cwd(), 'privacyscrubber.json');
|
|
630
|
+
if (fs.existsSync(configPath)) {
|
|
631
|
+
return {
|
|
632
|
+
content: [{
|
|
633
|
+
type: "text",
|
|
634
|
+
text: `βΉοΈ Configuration file 'privacyscrubber.json' already exists in the active workspace root directory:\n ${configPath}\nNo changes were made.`
|
|
635
|
+
}]
|
|
636
|
+
};
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
const defaultTemplate = {
|
|
640
|
+
"_comment": "PrivacyScrubber local configuration. Add custom rules and restart your MCP client (Cursor/Windsurf). Details: https://privacyscrubber.com/docs/config",
|
|
641
|
+
"customRules": [
|
|
642
|
+
{
|
|
643
|
+
"pattern": "my-secret-pattern-\\d+",
|
|
644
|
+
"label": "CUSTOM_TAG",
|
|
645
|
+
"_comment": "pattern must be a valid Javascript regex string. label is the tag replacement (e.g. CUSTOM_TAG)"
|
|
646
|
+
}
|
|
647
|
+
]
|
|
648
|
+
};
|
|
649
|
+
|
|
650
|
+
try {
|
|
651
|
+
fs.writeFileSync(configPath, JSON.stringify(defaultTemplate, null, 2) + '\n', 'utf8');
|
|
652
|
+
return {
|
|
653
|
+
content: [{
|
|
654
|
+
type: "text",
|
|
655
|
+
text: `β
Configuration file 'privacyscrubber.json' successfully created in your workspace root:\n ${configPath}\n\nRestart your MCP client (e.g. Cursor or Claude Desktop) to load the config rules.`
|
|
656
|
+
}]
|
|
657
|
+
};
|
|
658
|
+
} catch (err) {
|
|
659
|
+
return {
|
|
660
|
+
isError: true,
|
|
661
|
+
content: [{
|
|
662
|
+
type: "text",
|
|
663
|
+
text: `Error: Failed to write configuration file: ${err.message}`
|
|
664
|
+
}]
|
|
665
|
+
};
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
|
|
435
669
|
if (name === "check_status") {
|
|
436
670
|
const license = checkLicenseStatus();
|
|
437
671
|
const tier = license.isPro ? 'PRO' : 'FREE';
|
|
@@ -439,12 +673,35 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
439
673
|
const profileList = license.isPro
|
|
440
674
|
? 'All 23 profiles active (General, Dev, Medical, Legal, Finance, HRβ¦)'
|
|
441
675
|
: 'General only β PRO unlocks 22 industry profiles';
|
|
442
|
-
const rulesStatus = license.isPro ? 'β
Active (privacyscrubber.json)' : 'π Locked β requires PRO';
|
|
443
676
|
const sizeLimit = license.isPro ? 'Unlimited' : '50,000 characters per request';
|
|
444
677
|
|
|
678
|
+
const configPath = resolveConfigPath();
|
|
679
|
+
let rulesStatus = '';
|
|
680
|
+
let configPathSnippet = 'Not found';
|
|
681
|
+
|
|
682
|
+
if (fs.existsSync(configPath)) {
|
|
683
|
+
const localPath = path.resolve(process.cwd(), 'privacyscrubber.json');
|
|
684
|
+
if (configPath === localPath) {
|
|
685
|
+
configPathSnippet = './privacyscrubber.json';
|
|
686
|
+
} else {
|
|
687
|
+
configPathSnippet = '~/privacyscrubber.json';
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
if (license.isPro) {
|
|
691
|
+
const rulesCount = loadCustomRules().length;
|
|
692
|
+
rulesStatus = `β
Active (${rulesCount} rule${rulesCount !== 1 ? 's' : ''})`;
|
|
693
|
+
} else {
|
|
694
|
+
rulesStatus = 'π Ignored (requires PRO)';
|
|
695
|
+
}
|
|
696
|
+
} else {
|
|
697
|
+
rulesStatus = 'Not found';
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
const metricsSummary = getSessionMetricsSummary();
|
|
701
|
+
|
|
445
702
|
const lines = [
|
|
446
703
|
'ββββββββββββββββββββββββββββββββββββββββββββββββββββ',
|
|
447
|
-
|
|
704
|
+
`β PrivacyScrubber MCP Server v${MCP_VERSION.padEnd(10)} β`,
|
|
448
705
|
'β βββββββββββββββββββββββββββββββββββββββββββββββββββ£',
|
|
449
706
|
`β ${tierIcon} Tier: ${tier.padEnd(43)}β`,
|
|
450
707
|
`β π Session requests: ${String(sessionRequestCount).padEnd(27)}β`,
|
|
@@ -452,6 +709,8 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
452
709
|
'β βββββββββββββββββββββββββββββββββββββββββββββββββββ£',
|
|
453
710
|
`β π·οΈ Profiles: ${profileList.substring(0,35).padEnd(35)}β`,
|
|
454
711
|
`β π Custom rules: ${rulesStatus.padEnd(31)}β`,
|
|
712
|
+
`β βοΈ Config file: ${configPathSnippet.padEnd(31)}β`,
|
|
713
|
+
`β π Metrics: ${metricsSummary.substring(0,36).padEnd(36)}β`,
|
|
455
714
|
'β βββββββββββββββββββββββββββββββββββββββββββββββββββ£',
|
|
456
715
|
];
|
|
457
716
|
|
|
@@ -495,17 +754,50 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
495
754
|
}
|
|
496
755
|
});
|
|
497
756
|
|
|
498
|
-
function
|
|
499
|
-
const license = checkLicenseStatus();
|
|
500
|
-
|
|
757
|
+
function resolveConfigPath() {
|
|
501
758
|
let configPath = path.resolve(process.cwd(), 'privacyscrubber.json');
|
|
502
|
-
|
|
503
759
|
if (!fs.existsSync(configPath)) {
|
|
504
760
|
const homeDir = process.env.HOME || process.env.USERPROFILE || '';
|
|
505
761
|
if (homeDir) {
|
|
506
762
|
configPath = path.resolve(homeDir, 'privacyscrubber.json');
|
|
507
763
|
}
|
|
508
764
|
}
|
|
765
|
+
return configPath;
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
function getSessionMetricsSummary() {
|
|
769
|
+
const counts = {};
|
|
770
|
+
Object.keys(sessionMap).forEach(token => {
|
|
771
|
+
const match = token.match(/^\[([A-Z_]+)_\d+\]$/);
|
|
772
|
+
if (match) {
|
|
773
|
+
const type = match[1];
|
|
774
|
+
counts[type] = (counts[type] || 0) + 1;
|
|
775
|
+
}
|
|
776
|
+
});
|
|
777
|
+
|
|
778
|
+
if (Object.keys(counts).length === 0) {
|
|
779
|
+
return '0 entities masked';
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
const parts = [];
|
|
783
|
+
if (counts.NAME) parts.push(`${counts.NAME} Name${counts.NAME > 1 ? 's' : ''}`);
|
|
784
|
+
if (counts.EMAIL) parts.push(`${counts.EMAIL} Email${counts.EMAIL > 1 ? 's' : ''}`);
|
|
785
|
+
if (counts.PHONE) parts.push(`${counts.PHONE} Phone${counts.PHONE > 1 ? 's' : ''}`);
|
|
786
|
+
if (counts.ID) parts.push(`${counts.ID} ID${counts.ID > 1 ? 's' : ''}`);
|
|
787
|
+
if (counts.CUSTOM) parts.push(`${counts.CUSTOM} Custom${counts.CUSTOM > 1 ? 's' : ''}`);
|
|
788
|
+
|
|
789
|
+
Object.entries(counts).forEach(([type, count]) => {
|
|
790
|
+
if (!['NAME', 'EMAIL', 'PHONE', 'ID', 'CUSTOM'].includes(type)) {
|
|
791
|
+
parts.push(`${count} ${type}${count > 1 ? 's' : ''}`);
|
|
792
|
+
}
|
|
793
|
+
});
|
|
794
|
+
|
|
795
|
+
return parts.join(', ');
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
function loadCustomRules() {
|
|
799
|
+
const license = checkLicenseStatus();
|
|
800
|
+
const configPath = resolveConfigPath();
|
|
509
801
|
|
|
510
802
|
if (fs.existsSync(configPath)) {
|
|
511
803
|
try {
|
|
@@ -520,7 +812,7 @@ function loadCustomRules() {
|
|
|
520
812
|
category: (r.label || r.category || "CUSTOM").toUpperCase()
|
|
521
813
|
})).filter(r => r.pattern);
|
|
522
814
|
} else {
|
|
523
|
-
process.stderr.write(
|
|
815
|
+
process.stderr.write(`${colors.yellowBold}β οΈ [PrivacyScrubber] Custom rules detected in privacyscrubber.json, but are ignored in the Free Tier.${colors.reset}\n${colors.cyan}π Set PRIVACYSCRUBBER_KEY to your PRO license key.${colors.reset}\n`);
|
|
524
816
|
}
|
|
525
817
|
}
|
|
526
818
|
} catch (e) {
|
|
@@ -530,11 +822,44 @@ function loadCustomRules() {
|
|
|
530
822
|
return [];
|
|
531
823
|
}
|
|
532
824
|
|
|
533
|
-
//
|
|
825
|
+
// Secrets scanner β uses the module-level DEVOPS_SECRETS_DETECTOR constant
|
|
826
|
+
function detectSecrets(text) {
|
|
827
|
+
const detected = [];
|
|
828
|
+
DEVOPS_SECRETS_DETECTOR.forEach(detector => {
|
|
829
|
+
// Reset lastIndex before every test β global regex retains state across calls
|
|
830
|
+
detector.regex.lastIndex = 0;
|
|
831
|
+
if (detector.regex.test(text)) {
|
|
832
|
+
detected.push(detector.name);
|
|
833
|
+
}
|
|
834
|
+
});
|
|
835
|
+
return detected;
|
|
836
|
+
}
|
|
837
|
+
|
|
534
838
|
function performSanitization(text, profile) {
|
|
535
839
|
const customRules = loadCustomRules();
|
|
536
840
|
const result = PrivacyScrubberCore.scrubText(text, customRules, {}, profile, sessionMap);
|
|
537
841
|
|
|
842
|
+
const license = checkLicenseStatus();
|
|
843
|
+
if (!license.isPro && result.tokenMap) {
|
|
844
|
+
// Re-scan each masked token's original value against the shared detector.
|
|
845
|
+
// If it was a raw secret, restore it in the output and remove from sessionMap
|
|
846
|
+
// so it is never accidentally revealed by reveal_text.
|
|
847
|
+
Object.entries(result.tokenMap).forEach(([token, original]) => {
|
|
848
|
+
let isSecret = false;
|
|
849
|
+
for (const detector of DEVOPS_SECRETS_DETECTOR) {
|
|
850
|
+
detector.regex.lastIndex = 0;
|
|
851
|
+
if (detector.regex.test(original)) {
|
|
852
|
+
isSecret = true;
|
|
853
|
+
break;
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
if (isSecret) {
|
|
857
|
+
result.scrubbedText = result.scrubbedText.replace(token, original);
|
|
858
|
+
delete result.tokenMap[token];
|
|
859
|
+
}
|
|
860
|
+
});
|
|
861
|
+
}
|
|
862
|
+
|
|
538
863
|
// Update our volatile map with new matches
|
|
539
864
|
if (result.tokenMap) {
|
|
540
865
|
Object.entries(result.tokenMap).forEach(([token, original]) => {
|
|
@@ -548,7 +873,7 @@ function performSanitization(text, profile) {
|
|
|
548
873
|
function truncateIfFree(text, isPro) {
|
|
549
874
|
const MAX_FREE_CHARS = 50000;
|
|
550
875
|
if (!isPro && text.length > MAX_FREE_CHARS) {
|
|
551
|
-
process.stderr.write(
|
|
876
|
+
process.stderr.write(`${colors.yellowBold}β οΈ [PrivacyScrubber] Input truncated to ${MAX_FREE_CHARS} characters (Free Tier Limit).${colors.reset}\n${colors.cyan}π Set PRIVACYSCRUBBER_KEY to your PRO license key for unlimited size.${colors.reset}\n`);
|
|
552
877
|
return { processedText: text.substring(0, MAX_FREE_CHARS), wasTruncated: true };
|
|
553
878
|
}
|
|
554
879
|
return { processedText: text, wasTruncated: false };
|
package/manifest.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"manifest_version": "0.3",
|
|
3
3
|
"name": "privacyscrubber/pii-masking-mcp",
|
|
4
|
-
"version": "1.6.
|
|
5
|
-
"description": "Zero-Trust
|
|
4
|
+
"version": "1.6.8",
|
|
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
|
"author": {
|
|
7
7
|
"name": "PrivacyScrubber",
|
|
8
8
|
"email": "legal@privacyscrubber.com"
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@privacyscrubber/mcp-server",
|
|
3
|
-
"version": "1.6.
|
|
3
|
+
"version": "1.6.8",
|
|
4
4
|
"mcpName": "io.github.moxno/privacyscrubber-mcp",
|
|
5
|
-
"description": "Zero-Trust
|
|
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",
|
|
7
7
|
"type": "module",
|
|
8
8
|
"publishConfig": {
|
|
@@ -26,19 +26,32 @@
|
|
|
26
26
|
},
|
|
27
27
|
"dependencies": {
|
|
28
28
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
29
|
-
"mammoth": "^1.8.0"
|
|
29
|
+
"mammoth": "^1.8.0",
|
|
30
|
+
"pdf-parse": "^1.1.1",
|
|
31
|
+
"xlsx": "^0.18.5"
|
|
30
32
|
},
|
|
31
33
|
"keywords": [
|
|
32
34
|
"mcp",
|
|
33
35
|
"mcp-server",
|
|
34
|
-
"
|
|
36
|
+
"cursor",
|
|
37
|
+
"cursor-mcp",
|
|
38
|
+
"windsurf",
|
|
39
|
+
"windsurf-mcp",
|
|
40
|
+
"claude-desktop",
|
|
35
41
|
"security",
|
|
36
|
-
"
|
|
37
|
-
"secrets-scanner",
|
|
42
|
+
"privacy",
|
|
38
43
|
"zero-trust",
|
|
39
44
|
"ztds",
|
|
40
|
-
"
|
|
41
|
-
"
|
|
45
|
+
"pii-redactor",
|
|
46
|
+
"secrets-scanner",
|
|
47
|
+
"data-loss-prevention",
|
|
48
|
+
"dlp",
|
|
49
|
+
"data-masking",
|
|
50
|
+
"compliance",
|
|
51
|
+
"hipaa",
|
|
52
|
+
"soc2",
|
|
53
|
+
"grc",
|
|
54
|
+
"ciso"
|
|
42
55
|
],
|
|
43
56
|
"author": "Ilya Sibiryakov (BrandMeWeb)",
|
|
44
57
|
"license": "MIT",
|