@privacyscrubber/mcp-server 1.6.7 β†’ 1.6.9

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.9"
5
5
  },
6
6
  "tools": [
7
7
  {
@@ -16,7 +16,7 @@
16
16
  },
17
17
  "profile": {
18
18
  "type": "string",
19
- "description": "The detection profile to use. Available: 'General' (Free), or PRO profiles. Defaults to 'General'."
19
+ "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'."
20
20
  }
21
21
  },
22
22
  "required": ["text"]
@@ -48,11 +48,29 @@
48
48
  },
49
49
  "profile": {
50
50
  "type": "string",
51
- "description": "The detection profile to use. Available: 'General' (Free), or PRO profiles. Defaults to 'General'."
51
+ "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'."
52
52
  }
53
53
  },
54
54
  "required": ["file_path"]
55
55
  }
56
+ },
57
+ {
58
+ "name": "create_default_config",
59
+ "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.",
60
+ "inputSchema": {
61
+ "type": "object",
62
+ "properties": {},
63
+ "required": []
64
+ }
65
+ },
66
+ {
67
+ "name": "check_status",
68
+ "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.",
69
+ "inputSchema": {
70
+ "type": "object",
71
+ "properties": {},
72
+ "required": []
73
+ }
56
74
  }
57
75
  ],
58
76
  "resources": [],
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,25 @@ 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
+ greenBold: '\x1b[1;32m',
48
+ redBold: '\x1b[1;31m',
49
+ reset: '\x1b[0m'
50
+ };
51
+
52
+ // Secrets detection patterns β€” defined once here, shared by detectSecrets() and performSanitization()
53
+ const DEVOPS_SECRETS_DETECTOR = [
54
+ { name: 'AWS Credentials', regex: /\b(?:AKIA|ASIA|AGPA|AIDA|AROA|AIPA)[A-Z0-9]{16}\b/g },
55
+ { name: 'JSON Web Token (JWT)', regex: /\beyJ[a-zA-Z0-9_-]+\.[a-zA-Z0-9_-]+\.[a-zA-Z0-9_-]+\b/g },
56
+ { 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 },
57
+ { name: 'Stripe API Key', regex: /\b(?:[rs]k)_(?:test|live)_[a-zA-Z0-9]{24,}\b/g },
58
+ { 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 }
59
+ ];
60
+
37
61
  // Hardcoded public key for offline cryptographic verification
38
62
  const PUBLIC_KEY = `-----BEGIN PUBLIC KEY-----
39
63
  MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAw3f37srO402PU4++Baf8
@@ -117,7 +141,7 @@ function shouldNudge(isPro) {
117
141
  const server = new Server(
118
142
  {
119
143
  name: "privacyscrubber/pii-masking-mcp",
120
- version: "1.6.7",
144
+ version: MCP_VERSION,
121
145
  },
122
146
  {
123
147
  capabilities: {
@@ -180,6 +204,15 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
180
204
  required: ["file_path"]
181
205
  }
182
206
  },
207
+ {
208
+ name: "create_default_config",
209
+ 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.",
210
+ inputSchema: {
211
+ type: "object",
212
+ properties: {},
213
+ required: []
214
+ }
215
+ },
183
216
  {
184
217
  name: "check_status",
185
218
  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 +257,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
224
257
 
225
258
  if (isAdvanced && !license.isPro) {
226
259
  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`);
260
+ 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
261
  finalProfile = "General";
229
262
  extraBlocks.push(buildUpsellBlock(`Profile '${targetProfile}' requires PRO. Using 'General' as fallback.`));
230
263
  }
@@ -234,6 +267,14 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
234
267
  extraBlocks.push(buildUpsellBlock(`Input was truncated to 50,000 characters (Free Tier limit).`));
235
268
  }
236
269
 
270
+ if (!license.isPro) {
271
+ const detected = detectSecrets(processedText);
272
+ if (detected.length > 0) {
273
+ 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`);
274
+ extraBlocks.push(buildUpsellBlock(`API Key / Secret (${detected.join(', ')}) detected! Sanitization skipped (Requires PRO Profile).`));
275
+ }
276
+ }
277
+
237
278
  const sanitized = performSanitization(processedText, finalProfile);
238
279
 
239
280
  // Periodic soft nudge every N free-tier requests (no trigger event needed)
@@ -263,6 +304,17 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
263
304
  content: [{ type: "text", text: `Error: Parameter 'text' must be a string, got '${typeof text}'.` }]
264
305
  };
265
306
  }
307
+
308
+ // Warn early if session has no tokens β€” reveal would be a no-op
309
+ if (Object.keys(sessionMap).length === 0) {
310
+ return {
311
+ content: [{
312
+ type: "text",
313
+ 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.`
314
+ }]
315
+ };
316
+ }
317
+
266
318
  const restored = PrivacyScrubberCore.unscrubText(text, sessionMap);
267
319
  return {
268
320
  content: [
@@ -346,7 +398,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
346
398
 
347
399
  if (isAdvanced && !license.isPro) {
348
400
  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`);
401
+ 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
402
  finalProfile = "General";
351
403
  extraBlocks.push(buildUpsellBlock(`Profile '${targetProfile}' requires PRO. Using 'General' as fallback.`));
352
404
  }
@@ -354,6 +406,14 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
354
406
  const { processedText: processedContent, wasTruncated } = truncateIfFree(content, license.isPro);
355
407
  if (wasTruncated) extraBlocks.push(buildUpsellBlock(`File content was truncated to 50,000 characters (Free Tier limit).`));
356
408
 
409
+ if (!license.isPro) {
410
+ const detected = detectSecrets(processedContent);
411
+ if (detected.length > 0) {
412
+ 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`);
413
+ extraBlocks.push(buildUpsellBlock(`API Key / Secret (${detected.join(', ')}) detected! Sanitization skipped (Requires PRO Profile).`));
414
+ }
415
+ }
416
+
357
417
  const sanitized = performSanitization(processedContent, finalProfile);
358
418
 
359
419
  if (!license.isPro && extraBlocks.length === 0 && sessionRequestCount % UPSELL_EVERY_N === 0) {
@@ -375,7 +435,133 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
375
435
  }
376
436
 
377
437
  const buffer = fs.readFileSync(resolvedPath);
378
-
438
+
439
+ const isPdf = resolvedPath.toLowerCase().endsWith(".pdf");
440
+ const isExcel = resolvedPath.toLowerCase().endsWith(".xlsx") || resolvedPath.toLowerCase().endsWith(".xls");
441
+
442
+ if (isPdf || isExcel) {
443
+ const license = checkLicenseStatus();
444
+ if (!license.isPro) {
445
+ return {
446
+ isError: true,
447
+ content: [
448
+ {
449
+ type: "text",
450
+ 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`
451
+ }
452
+ ]
453
+ };
454
+ }
455
+
456
+ if (isPdf) {
457
+ try {
458
+ const pdf = require('pdf-parse');
459
+ const data = await pdf(buffer);
460
+ const content = data.text;
461
+ const targetProfile = profile.trim();
462
+ const isAdvanced = targetProfile.toLowerCase() !== "general";
463
+
464
+ let finalProfile = targetProfile;
465
+ const extraBlocks = [];
466
+
467
+ sessionRequestCount++;
468
+
469
+ if (isAdvanced && !license.isPro) {
470
+ const reason = license.error ? ` (${license.error})` : "";
471
+ 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`);
472
+ finalProfile = "General";
473
+ extraBlocks.push(buildUpsellBlock(`Profile '${targetProfile}' requires PRO. Using 'General' as fallback.`));
474
+ }
475
+
476
+ const { processedText: processedContent, wasTruncated } = truncateIfFree(content, license.isPro);
477
+ if (wasTruncated) extraBlocks.push(buildUpsellBlock(`File content was truncated to 50,000 characters (Free Tier limit).`));
478
+
479
+ if (!license.isPro) {
480
+ const detected = detectSecrets(processedContent);
481
+ if (detected.length > 0) {
482
+ 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`);
483
+ extraBlocks.push(buildUpsellBlock(`API Key / Secret (${detected.join(', ')}) detected! Sanitization skipped (Requires PRO Profile).`));
484
+ }
485
+ }
486
+
487
+ const sanitized = performSanitization(processedContent, finalProfile);
488
+
489
+ if (!license.isPro && extraBlocks.length === 0 && sessionRequestCount % UPSELL_EVERY_N === 0) {
490
+ extraBlocks.push(buildUpsellBlock(null));
491
+ }
492
+
493
+ return {
494
+ content: [
495
+ { type: "text", text: sanitized },
496
+ ...extraBlocks
497
+ ]
498
+ };
499
+ } catch (pdfError) {
500
+ return {
501
+ isError: true,
502
+ content: [{ type: "text", text: `Error: Failed to parse PDF file: ${pdfError.message}` }]
503
+ };
504
+ }
505
+ }
506
+
507
+ if (isExcel) {
508
+ try {
509
+ const XLSX = require('xlsx');
510
+ const workbook = XLSX.read(buffer, { type: 'buffer' });
511
+ let content = '';
512
+ workbook.SheetNames.forEach(sheetName => {
513
+ const worksheet = workbook.Sheets[sheetName];
514
+ content += `--- Sheet: ${sheetName} ---\n`;
515
+ content += XLSX.utils.sheet_to_csv(worksheet) + '\n';
516
+ });
517
+
518
+ const targetProfile = profile.trim();
519
+ const isAdvanced = targetProfile.toLowerCase() !== "general";
520
+
521
+ let finalProfile = targetProfile;
522
+ const extraBlocks = [];
523
+
524
+ sessionRequestCount++;
525
+
526
+ if (isAdvanced && !license.isPro) {
527
+ const reason = license.error ? ` (${license.error})` : "";
528
+ 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`);
529
+ finalProfile = "General";
530
+ extraBlocks.push(buildUpsellBlock(`Profile '${targetProfile}' requires PRO. Using 'General' as fallback.`));
531
+ }
532
+
533
+ const { processedText: processedContent, wasTruncated } = truncateIfFree(content, license.isPro);
534
+ if (wasTruncated) extraBlocks.push(buildUpsellBlock(`File content was truncated to 50,000 characters (Free Tier limit).`));
535
+
536
+ if (!license.isPro) {
537
+ const detected = detectSecrets(processedContent);
538
+ if (detected.length > 0) {
539
+ 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`);
540
+ extraBlocks.push(buildUpsellBlock(`API Key / Secret (${detected.join(', ')}) detected! Sanitization skipped (Requires PRO Profile).`));
541
+ }
542
+ }
543
+
544
+ const sanitized = performSanitization(processedContent, finalProfile);
545
+
546
+ if (!license.isPro && extraBlocks.length === 0 && sessionRequestCount % UPSELL_EVERY_N === 0) {
547
+ extraBlocks.push(buildUpsellBlock(null));
548
+ }
549
+
550
+ return {
551
+ content: [
552
+ { type: "text", text: sanitized },
553
+ ...extraBlocks
554
+ ]
555
+ };
556
+ } catch (xlsxError) {
557
+ return {
558
+ isError: true,
559
+ content: [{ type: "text", text: `Error: Failed to parse Excel file: ${xlsxError.message}` }]
560
+ };
561
+ }
562
+ }
563
+ }
564
+
379
565
  // Check for null bytes in the first 8KB to detect binary files
380
566
  let isBinary = false;
381
567
  const checkLen = Math.min(buffer.length, 8000);
@@ -392,7 +578,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
392
578
  content: [
393
579
  {
394
580
  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/`
581
+ 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
582
  }
397
583
  ]
398
584
  };
@@ -410,7 +596,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
410
596
 
411
597
  if (isAdvanced && !license.isPro) {
412
598
  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`);
599
+ 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
600
  finalProfile = "General";
415
601
  extraBlocks.push(buildUpsellBlock(`Profile '${targetProfile}' requires PRO. Using 'General' as fallback.`));
416
602
  }
@@ -418,6 +604,14 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
418
604
  const { processedText: processedContent2, wasTruncated: wasTruncated2 } = truncateIfFree(content, license.isPro);
419
605
  if (wasTruncated2) extraBlocks.push(buildUpsellBlock(`File content was truncated to 50,000 characters (Free Tier limit).`));
420
606
 
607
+ if (!license.isPro) {
608
+ const detected = detectSecrets(processedContent2);
609
+ if (detected.length > 0) {
610
+ 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`);
611
+ extraBlocks.push(buildUpsellBlock(`API Key / Secret (${detected.join(', ')}) detected! Sanitization skipped (Requires PRO Profile).`));
612
+ }
613
+ }
614
+
421
615
  const sanitized = performSanitization(processedContent2, finalProfile);
422
616
 
423
617
  if (!license.isPro && extraBlocks.length === 0 && sessionRequestCount % UPSELL_EVERY_N === 0) {
@@ -432,6 +626,47 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
432
626
  };
433
627
  }
434
628
 
629
+ if (name === "create_default_config") {
630
+ const configPath = path.resolve(process.cwd(), 'privacyscrubber.json');
631
+ if (fs.existsSync(configPath)) {
632
+ return {
633
+ content: [{
634
+ type: "text",
635
+ text: `ℹ️ Configuration file 'privacyscrubber.json' already exists in the active workspace root directory:\n ${configPath}\nNo changes were made.`
636
+ }]
637
+ };
638
+ }
639
+
640
+ const defaultTemplate = {
641
+ "_comment": "PrivacyScrubber local configuration. Add custom rules and restart your MCP client (Cursor/Windsurf). Details: https://privacyscrubber.com/docs/config",
642
+ "customRules": [
643
+ {
644
+ "pattern": "my-secret-pattern-\\d+",
645
+ "label": "CUSTOM_TAG",
646
+ "_comment": "pattern must be a valid Javascript regex string. label is the tag replacement (e.g. CUSTOM_TAG)"
647
+ }
648
+ ]
649
+ };
650
+
651
+ try {
652
+ fs.writeFileSync(configPath, JSON.stringify(defaultTemplate, null, 2) + '\n', 'utf8');
653
+ return {
654
+ content: [{
655
+ type: "text",
656
+ 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.`
657
+ }]
658
+ };
659
+ } catch (err) {
660
+ return {
661
+ isError: true,
662
+ content: [{
663
+ type: "text",
664
+ text: `Error: Failed to write configuration file: ${err.message}`
665
+ }]
666
+ };
667
+ }
668
+ }
669
+
435
670
  if (name === "check_status") {
436
671
  const license = checkLicenseStatus();
437
672
  const tier = license.isPro ? 'PRO' : 'FREE';
@@ -439,12 +674,35 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
439
674
  const profileList = license.isPro
440
675
  ? 'All 23 profiles active (General, Dev, Medical, Legal, Finance, HR…)'
441
676
  : 'General only β€” PRO unlocks 22 industry profiles';
442
- const rulesStatus = license.isPro ? 'βœ… Active (privacyscrubber.json)' : 'πŸ”’ Locked β€” requires PRO';
443
677
  const sizeLimit = license.isPro ? 'Unlimited' : '50,000 characters per request';
444
678
 
679
+ const configPath = resolveConfigPath();
680
+ let rulesStatus = '';
681
+ let configPathSnippet = 'Not found';
682
+
683
+ if (fs.existsSync(configPath)) {
684
+ const localPath = path.resolve(process.cwd(), 'privacyscrubber.json');
685
+ if (configPath === localPath) {
686
+ configPathSnippet = './privacyscrubber.json';
687
+ } else {
688
+ configPathSnippet = '~/privacyscrubber.json';
689
+ }
690
+
691
+ if (license.isPro) {
692
+ const rulesCount = loadCustomRules().length;
693
+ rulesStatus = `βœ… Active (${rulesCount} rule${rulesCount !== 1 ? 's' : ''})`;
694
+ } else {
695
+ rulesStatus = 'πŸ”’ Ignored (requires PRO)';
696
+ }
697
+ } else {
698
+ rulesStatus = 'Not found';
699
+ }
700
+
701
+ const metricsSummary = getSessionMetricsSummary();
702
+
445
703
  const lines = [
446
704
  '╔══════════════════════════════════════════════════╗',
447
- 'β•‘ PrivacyScrubber MCP Server v1.6.7 β•‘',
705
+ `β•‘ PrivacyScrubber MCP Server v${MCP_VERSION.padEnd(10)} β•‘`,
448
706
  '╠══════════════════════════════════════════════════╣',
449
707
  `β•‘ ${tierIcon} Tier: ${tier.padEnd(43)}β•‘`,
450
708
  `β•‘ πŸ“Š Session requests: ${String(sessionRequestCount).padEnd(27)}β•‘`,
@@ -452,6 +710,8 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
452
710
  '╠══════════════════════════════════════════════════╣',
453
711
  `β•‘ 🏷️ Profiles: ${profileList.substring(0,35).padEnd(35)}β•‘`,
454
712
  `β•‘ πŸ“‹ Custom rules: ${rulesStatus.padEnd(31)}β•‘`,
713
+ `β•‘ βš™οΈ Config file: ${configPathSnippet.padEnd(31)}β•‘`,
714
+ `β•‘ πŸ“ˆ Metrics: ${metricsSummary.substring(0,36).padEnd(36)}β•‘`,
455
715
  '╠══════════════════════════════════════════════════╣',
456
716
  ];
457
717
 
@@ -495,17 +755,50 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
495
755
  }
496
756
  });
497
757
 
498
- function loadCustomRules() {
499
- const license = checkLicenseStatus();
500
-
758
+ function resolveConfigPath() {
501
759
  let configPath = path.resolve(process.cwd(), 'privacyscrubber.json');
502
-
503
760
  if (!fs.existsSync(configPath)) {
504
761
  const homeDir = process.env.HOME || process.env.USERPROFILE || '';
505
762
  if (homeDir) {
506
763
  configPath = path.resolve(homeDir, 'privacyscrubber.json');
507
764
  }
508
765
  }
766
+ return configPath;
767
+ }
768
+
769
+ function getSessionMetricsSummary() {
770
+ const counts = {};
771
+ Object.keys(sessionMap).forEach(token => {
772
+ const match = token.match(/^\[([A-Z_]+)_\d+\]$/);
773
+ if (match) {
774
+ const type = match[1];
775
+ counts[type] = (counts[type] || 0) + 1;
776
+ }
777
+ });
778
+
779
+ if (Object.keys(counts).length === 0) {
780
+ return '0 entities masked';
781
+ }
782
+
783
+ const parts = [];
784
+ if (counts.NAME) parts.push(`${counts.NAME} Name${counts.NAME > 1 ? 's' : ''}`);
785
+ if (counts.EMAIL) parts.push(`${counts.EMAIL} Email${counts.EMAIL > 1 ? 's' : ''}`);
786
+ if (counts.PHONE) parts.push(`${counts.PHONE} Phone${counts.PHONE > 1 ? 's' : ''}`);
787
+ if (counts.ID) parts.push(`${counts.ID} ID${counts.ID > 1 ? 's' : ''}`);
788
+ if (counts.CUSTOM) parts.push(`${counts.CUSTOM} Custom${counts.CUSTOM > 1 ? 's' : ''}`);
789
+
790
+ Object.entries(counts).forEach(([type, count]) => {
791
+ if (!['NAME', 'EMAIL', 'PHONE', 'ID', 'CUSTOM'].includes(type)) {
792
+ parts.push(`${count} ${type}${count > 1 ? 's' : ''}`);
793
+ }
794
+ });
795
+
796
+ return parts.join(', ');
797
+ }
798
+
799
+ function loadCustomRules() {
800
+ const license = checkLicenseStatus();
801
+ const configPath = resolveConfigPath();
509
802
 
510
803
  if (fs.existsSync(configPath)) {
511
804
  try {
@@ -520,7 +813,7 @@ function loadCustomRules() {
520
813
  category: (r.label || r.category || "CUSTOM").toUpperCase()
521
814
  })).filter(r => r.pattern);
522
815
  } 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");
816
+ 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
817
  }
525
818
  }
526
819
  } catch (e) {
@@ -530,11 +823,44 @@ function loadCustomRules() {
530
823
  return [];
531
824
  }
532
825
 
533
- // Run helper
826
+ // Secrets scanner β€” uses the module-level DEVOPS_SECRETS_DETECTOR constant
827
+ function detectSecrets(text) {
828
+ const detected = [];
829
+ DEVOPS_SECRETS_DETECTOR.forEach(detector => {
830
+ // Reset lastIndex before every test β€” global regex retains state across calls
831
+ detector.regex.lastIndex = 0;
832
+ if (detector.regex.test(text)) {
833
+ detected.push(detector.name);
834
+ }
835
+ });
836
+ return detected;
837
+ }
838
+
534
839
  function performSanitization(text, profile) {
535
840
  const customRules = loadCustomRules();
536
841
  const result = PrivacyScrubberCore.scrubText(text, customRules, {}, profile, sessionMap);
537
842
 
843
+ const license = checkLicenseStatus();
844
+ if (!license.isPro && result.tokenMap) {
845
+ // Re-scan each masked token's original value against the shared detector.
846
+ // If it was a raw secret, restore it in the output and remove from sessionMap
847
+ // so it is never accidentally revealed by reveal_text.
848
+ Object.entries(result.tokenMap).forEach(([token, original]) => {
849
+ let isSecret = false;
850
+ for (const detector of DEVOPS_SECRETS_DETECTOR) {
851
+ detector.regex.lastIndex = 0;
852
+ if (detector.regex.test(original)) {
853
+ isSecret = true;
854
+ break;
855
+ }
856
+ }
857
+ if (isSecret) {
858
+ result.scrubbedText = result.scrubbedText.replace(token, original);
859
+ delete result.tokenMap[token];
860
+ }
861
+ });
862
+ }
863
+
538
864
  // Update our volatile map with new matches
539
865
  if (result.tokenMap) {
540
866
  Object.entries(result.tokenMap).forEach(([token, original]) => {
@@ -548,7 +874,7 @@ function performSanitization(text, profile) {
548
874
  function truncateIfFree(text, isPro) {
549
875
  const MAX_FREE_CHARS = 50000;
550
876
  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`);
877
+ 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
878
  return { processedText: text.substring(0, MAX_FREE_CHARS), wasTruncated: true };
553
879
  }
554
880
  return { processedText: text, wasTruncated: false };
@@ -556,7 +882,10 @@ function truncateIfFree(text, isPro) {
556
882
 
557
883
  // Start the server transport
558
884
  const transport = new StdioServerTransport();
559
- server.connect(transport).catch((error) => {
885
+ server.connect(transport).then(() => {
886
+ process.stderr.write(`${colors.greenBold}βœ… PrivacyScrubber ZTDS MCP Server started successfully.${colors.reset}\n`);
887
+ process.stderr.write(`${colors.cyan}⭐ Star us on GitHub: https://github.com/moxno/privacyscrubber-mcp${colors.reset}\n`);
888
+ }).catch((error) => {
560
889
  console.error("Failed to connect MCP server transport:", error);
561
890
  process.exit(1);
562
891
  });