@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.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "serverInfo": {
3
3
  "name": "privacyscrubber/pii-masking-mcp",
4
- "version": "1.0.4"
4
+ "version": "1.6.7"
5
5
  },
6
6
  "tools": [
7
7
  {
package/README.md CHANGED
@@ -6,8 +6,8 @@
6
6
  [![Security: 100% Local](https://img.shields.io/badge/Security-100%25%20Local-emerald)](https://privacyscrubber.com)
7
7
  [![Parity: 100% Core Match](https://img.shields.io/badge/Parity-100%25%20Core%20Match-blueviolet)](https://privacyscrubber.com)
8
8
 
9
- **Zero-Trust Data Sanitization (ZTDS) Model Context Protocol (MCP) Server.**
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
- Standard use is free under the **Free Tier** (limits to the `General` PII profile). To unlock 22+ specialized industry profiles (DevOps, Medical, Legal, Finance) and custom regex rules, acquire a commercial license at [privacyscrubber.com/pricing](https://privacyscrubber.com/pricing).
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: "1.6.7",
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(`⚠️ [PrivacyScrubber] Advanced profile '${targetProfile}' is locked in the FREE tier. Falling back to 'General' profile.${reason}\nGet a PRO key at: https://privacyscrubber.com/pricing\n`);
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(`⚠️ [PrivacyScrubber] Advanced profile '${targetProfile}' is locked in the FREE tier. Falling back to 'General' profile.${reason}\nGet a PRO key at: https://privacyscrubber.com/pricing\n`);
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). To sanitize complex formats like PDF or DOCX, please use the web interface at: https://privacyscrubber.com/`
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(`⚠️ [PrivacyScrubber] Advanced profile '${targetProfile}' is locked in the FREE tier. Falling back to 'General' profile.${reason}\nGet a PRO key at: https://privacyscrubber.com/pricing\n`);
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
- 'β•‘ PrivacyScrubber MCP Server v1.6.7 β•‘',
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 loadCustomRules() {
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("⚠️ [PrivacyScrubber] Custom rules detected in privacyscrubber.json, but are ignored in the Free Tier. Set PRIVACYSCRUBBER_KEY to your PRO license key.\n");
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
- // Run helper
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(`⚠️ [PrivacyScrubber] Input truncated to ${MAX_FREE_CHARS} characters (Free Tier Limit). Set PRIVACYSCRUBBER_KEY to your PRO license key for unlimited size.\n`);
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.6",
5
- "description": "Zero-Trust Data Sanitization (ZTDS) MCP Server for AI IDEs and Claude Desktop. Locally scrubs PII, secrets, and credentials before sending context to LLMs.",
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.7",
3
+ "version": "1.6.8",
4
4
  "mcpName": "io.github.moxno/privacyscrubber-mcp",
5
- "description": "Zero-Trust Data Sanitization (ZTDS) MCP Server for AI IDEs and Claude Desktop. Locally scrubs PII, secrets, and credentials before sending context to LLMs.",
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
- "privacy",
36
+ "cursor",
37
+ "cursor-mcp",
38
+ "windsurf",
39
+ "windsurf-mcp",
40
+ "claude-desktop",
35
41
  "security",
36
- "pii-redactor",
37
- "secrets-scanner",
42
+ "privacy",
38
43
  "zero-trust",
39
44
  "ztds",
40
- "claude-desktop",
41
- "cursor"
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",