@privacyscrubber/mcp-server 1.7.1 → 1.7.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/index.js CHANGED
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env node
2
+ // LicenseManager will be required below
2
3
 
3
4
  /**
4
5
  * PrivacyScrubber MCP Server
@@ -36,6 +37,10 @@ const MCP_VERSION = require('./package.json').version;
36
37
  // Import the production core engine with 100% parity
37
38
  const scrubberCorePath = path.resolve(__dirname, './scrubber-core.cjs');
38
39
  const PrivacyScrubberCore = require(scrubberCorePath);
40
+ let LicenseManager = require('./ps-license-manager.js');
41
+ if (!LicenseManager || typeof LicenseManager.validate !== 'function') {
42
+ LicenseManager = global.LicenseManager;
43
+ }
39
44
 
40
45
  // Initialize core engine
41
46
  PrivacyScrubberCore.init();
@@ -74,83 +79,160 @@ const DEVOPS_SECRETS_DETECTOR = [
74
79
  { 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 }
75
80
  ];
76
81
 
77
- // Hardcoded public key for offline cryptographic verification
78
- const PUBLIC_KEY = `-----BEGIN PUBLIC KEY-----
79
- MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAqN700s8jMq9Rf09prTPB
80
- ekrFTXi+mnzSgMzwy+rMrJ4kyZQRDTxSFO1d9r5JQIhHfK5ToPiiS/Z/AS+Ga5bb
81
- BCy50SORDRn1HzrvFjNP+kogYyOeFk2lWw0Lt4/mX3M98zRE2D1HBDQJF8Y5Y/6l
82
- 735iwlF6T1oO0mEbb1FM5tKvO44OkTih7krc3w56R07S1WuYIhGLR6IfDbNU/drL
83
- nbhUR4HqBRepIsn9mcSODw/FP5t3QlFLfDvZo/1SIUSBa9NowhPtPk0Hq3hndSvR
84
- pXkyvjeA7HLyXU8hhvausn85QRdEYTMXP9BehDjhli4VELSAY3ARGzemcUgggejg
85
- CwIDAQAB
86
- -----END PUBLIC KEY-----`;
87
-
88
82
  function checkLicenseStatus() {
89
83
  const key = (process.env.PRIVACYSCRUBBER_KEY || "").trim();
90
84
  if (!key) return { isPro: false, type: null, error: "No license key provided." };
91
85
 
86
+ const result = LicenseManager.validate(key);
87
+
88
+ if (!result.valid) {
89
+ mcpLog(`DEBUG: License invalid because: ${result.reason || "Unknown"}`);
90
+ return { isPro: false, type: null, error: result.reason || "Invalid license format or signature." };
91
+ }
92
+
93
+ return { isPro: true, type: result.tier, error: null };
94
+ }
95
+
96
+ // ── Free Tier Usage Tracking (Persistent) ─────────────────────────────
97
+ const FREE_TIER_DAILY_LIMIT = 10;
98
+ const STAR_PROMPT_THRESHOLD = 5;
99
+
100
+ function getUsageFilePath() {
101
+ const homeDir = process.env.HOME || process.env.USERPROFILE || '';
102
+ return path.resolve(homeDir, '.privacyscrubber-usage.json');
103
+ }
104
+
105
+ function getDailyUsage() {
106
+ const file = getUsageFilePath();
107
+ const today = new Date().toISOString().split('T')[0];
108
+ if (!fs.existsSync(file)) return 0;
92
109
  try {
93
- const [payloadBase64, signatureBase64] = key.split('.');
94
- if (!payloadBase64 || !signatureBase64) {
95
- return { isPro: false, type: null, error: "Invalid license key structure." };
110
+ const data = JSON.parse(fs.readFileSync(file, 'utf8'));
111
+ if (data.date === today) {
112
+ return data.count || 0;
96
113
  }
114
+ } catch (e) {}
115
+ return 0;
116
+ }
97
117
 
98
- const verifier = crypto.createVerify('SHA256');
99
- verifier.update(payloadBase64);
100
- const isVerified = verifier.verify(PUBLIC_KEY, signatureBase64, 'base64');
101
-
102
- if (!isVerified) {
103
- return { isPro: false, type: null, error: "Signature verification failed." };
118
+ function incrementDailyUsage() {
119
+ const file = getUsageFilePath();
120
+ const today = new Date().toISOString().split('T')[0];
121
+ let count = 0;
122
+ if (fs.existsSync(file)) {
123
+ try {
124
+ const data = JSON.parse(fs.readFileSync(file, 'utf8'));
125
+ if (data.date === today) count = data.count || 0;
126
+ } catch (e) {}
127
+ }
128
+ count++;
129
+ try {
130
+ fs.writeFileSync(file, JSON.stringify({ date: today, count }), 'utf8');
131
+ } catch (e) {}
132
+ return count;
133
+ }
134
+
135
+ function checkFreeTierLimit(isPro) {
136
+ if (isPro) return { blocked: false, count: 0 };
137
+
138
+ const count = incrementDailyUsage();
139
+ // Feedback / Growth loop triggers
140
+ if (count === 3) {
141
+ mcpLog(`${colors.cyan}💬 We are building the ultimate privacy tool for developers. What feature should we add next? Let us know: https://privacyscrubber.com/feedback${colors.reset}\n`);
142
+ }
143
+ if (count === STAR_PROMPT_THRESHOLD) {
144
+ mcpLog(`${colors.yellowBold}🔒 PrivacyScrubber is running securely. If this tool saved your PII today, please drop a star on GitHub: https://github.com/moxno/privacyscrubber-mcp${colors.reset}\n`);
145
+ }
146
+
147
+ if (count >= FREE_TIER_DAILY_LIMIT) {
148
+ mcpLog(`${colors.redBold}🚫 [PrivacyScrubber] Free tier daily limit exhausted (${FREE_TIER_DAILY_LIMIT} requests). Request BLOCKED.${colors.reset}\n${colors.cyan}👉 Get a PRO key for unlimited use: https://privacyscrubber.com/pricing${colors.reset}\n`);
149
+ return { blocked: true, count };
150
+ }
151
+
152
+ return { blocked: false, count };
153
+ }
154
+
155
+ function buildCisoAuditTelemetry(currentTokenMap = {}) {
156
+ const entities = {};
157
+ let totalCount = 0;
158
+
159
+ if (currentTokenMap && typeof currentTokenMap === 'object') {
160
+ const keys = Array.isArray(currentTokenMap) ? currentTokenMap : Object.keys(currentTokenMap);
161
+ for (const t of keys) {
162
+ const tokenStr = typeof t === 'string' ? t : (t.token || t.mask || '');
163
+ const match = tokenStr.match(/\[([A-Z_]+)_\d+\]/);
164
+ const baseType = match ? match[1] : (tokenStr.replace(/\[|\]/g, '').replace(/_[0-9]+$/, '') || 'CUSTOM');
165
+ entities[baseType] = (entities[baseType] || 0) + 1;
166
+ totalCount++;
167
+ }
104
168
  }
105
169
 
106
- const payload = JSON.parse(Buffer.from(payloadBase64, 'base64').toString('utf8'));
107
-
108
- // Expiration check
109
- if (payload.expires && payload.expires < Math.floor(Date.now() / 1000)) {
110
- return {
111
- isPro: false,
112
- type: payload.type,
113
- error: `License expired on ${new Date(payload.expires * 1000).toLocaleDateString()}`
114
- };
170
+ const types = Object.keys(entities);
171
+ let riskLevel = 'LOW EXPOSURE';
172
+
173
+ if (totalCount > 0) {
174
+ const hasHighRiskEntities = types.some(t => ['ID', 'SSN', 'CREDIT_CARD', 'PASSPORT', 'BANK', 'API_KEY', 'SECRET', 'PASSWORD', 'MRN', 'KEY'].includes(t));
175
+ if (hasHighRiskEntities || types.length >= 3 || totalCount >= 10) {
176
+ riskLevel = 'CRITICAL (HIGH EXPOSURE)';
177
+ } else if (types.length >= 2 || totalCount >= 3) {
178
+ riskLevel = 'MODERATE EXPOSURE';
179
+ } else {
180
+ riskLevel = 'LOW EXPOSURE';
181
+ }
182
+ } else {
183
+ riskLevel = 'CLEAN (ZERO PII)';
115
184
  }
116
185
 
117
- return { isPro: true, type: payload.type, error: null };
118
- } catch (e) {
119
- return { isPro: false, type: null, error: "Error parsing license: " + e.message };
120
- }
186
+ const frameworksSet = new Set(['ZTDS Standard']);
187
+ if (types.includes('NAME') || types.includes('EMAIL') || types.includes('PHONE')) {
188
+ frameworksSet.add('GDPR (Art. 4)');
189
+ frameworksSet.add('CCPA/CPRA');
190
+ }
191
+ if (types.includes('ID') || types.includes('SSN') || types.includes('PASSPORT')) {
192
+ frameworksSet.add('SOC 2 Type II');
193
+ frameworksSet.add('ISO 27001 (A.8.11)');
194
+ }
195
+ if (types.some(t => ['CREDIT_CARD', 'BANK', 'IBAN', 'FINANCIAL', 'CARD'].includes(t))) {
196
+ frameworksSet.add('PCI DSS v4.0');
197
+ }
198
+ if (types.some(t => ['MRN', 'HEALTH', 'MEDICAL', 'PATIENT'].includes(t))) {
199
+ frameworksSet.add('HIPAA §164.514');
200
+ }
201
+ if (types.some(t => ['API_KEY', 'SECRET', 'PASSWORD', 'TOKEN', 'KEY'].includes(t))) {
202
+ frameworksSet.add('NIST SP 800-53');
203
+ }
204
+
205
+ return {
206
+ totalCount,
207
+ entities,
208
+ types,
209
+ riskLevel,
210
+ frameworksList: Array.from(frameworksSet)
211
+ };
121
212
  }
122
213
 
123
- // ── Session request counter (volatile in-memory) ─────────────────────────────
124
- let sessionRequestCount = 0;
125
- const UPSELL_EVERY_N = 10; // show upsell nudge every N free-tier requests
214
+ function formatAuditReceipt(telemetry) {
215
+ if (telemetry.totalCount === 0) {
216
+ return "\\n\\n> 🛡️ **PrivacyScrubber Audit Receipt**: CLEAN (ZERO PII DETECTED)\\n";
217
+ }
218
+
219
+ const entitiesList = Object.entries(telemetry.entities)
220
+ .map(([type, count]) => `${count} ${type}`)
221
+ .join(', ');
222
+
223
+ const icon = telemetry.riskLevel.includes('CRITICAL') ? '🔴' : (telemetry.riskLevel.includes('MODERATE') ? '🟠' : '🟢');
126
224
 
127
- // Build a clean upsell content block visible to the agent / user
225
+ return `\\n\\n> 🛡️ **PrivacyScrubber Audit Receipt**\\n> * **Risk Level:** ${icon} ${telemetry.riskLevel}\\n> * **Compliance Enforced:** ${telemetry.frameworksList.join(', ')}\\n> * **Tokens Masked:** ${telemetry.totalCount} (${entitiesList})\\n`;
226
+ }
227
+
228
+ // Build a clean upsell content block (Disabled to prevent agent-visible marketing content)
128
229
  function buildUpsellBlock(reason) {
129
- const count = sessionRequestCount;
130
- let lines = [
131
- '─────────────────────────────────────────────',
132
- '⚡ PrivacyScrubber PRO — Upgrade your AI setup',
133
- '─────────────────────────────────────────────',
134
- ];
135
- if (reason) lines.push(`ℹ️ ${reason}`);
136
- lines.push(
137
- `📊 Free session requests: ${count}`,
138
- '🔒 PRO unlocks: 22 industry profiles (Dev, Medical, Legal…)',
139
- ' + Custom regex rules (privacyscrubber.json)',
140
- ' + Unlimited input size',
141
- '',
142
- '💳 PRO Lifetime: $110 → https://privacyscrubber.com/pricing',
143
- '',
144
- 'After purchase, add your key to your MCP config:',
145
- ' "PRIVACYSCRUBBER_KEY": "<your-key-here>"',
146
- '─────────────────────────────────────────────'
147
- );
148
- return { type: 'text', text: lines.join('\n') };
230
+ return null;
149
231
  }
150
232
 
151
- // Decide if we should attach a soft periodic nudge (every N requests, free tier)
233
+ // Decide if we should attach a soft periodic nudge (Disabled to maintain clean MCP output)
152
234
  function shouldNudge(isPro) {
153
- return !isPro && sessionRequestCount > 0 && sessionRequestCount % UPSELL_EVERY_N === 0;
235
+ return false;
154
236
  }
155
237
 
156
238
  // Create the MCP server
@@ -435,43 +517,45 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
435
517
  let finalProfile = targetProfile;
436
518
  const extraBlocks = [];
437
519
 
438
- sessionRequestCount++;
520
+ const limitStatus = checkFreeTierLimit(license.isPro);
521
+ if (limitStatus.blocked) {
522
+ return {
523
+ isError: true,
524
+ content: [{ type: "text", text: `Error: Free tier daily limit exhausted (${FREE_TIER_DAILY_LIMIT} requests). Please set your PRO license key to continue. Get a key at: https://privacyscrubber.com/pricing` }]
525
+ };
526
+ }
439
527
 
528
+ const charLimit = (isAdvanced && !license.isPro) ? 5000 : 15000;
440
529
  if (isAdvanced && !license.isPro) {
441
- if (text.length > 5000) {
442
- const reason = license.error ? ` (${license.error})` : "";
443
- mcpLog(`${colors.yellowBold}⚠️ [PrivacyScrubber] Profile '${targetProfile}' on Free tier is limited to 5,000 chars. Falling back to 'General'.${reason}${colors.reset}\n${colors.cyan}👉 Get a PRO key at: https://privacyscrubber.com/pricing${colors.reset}\n`);
444
- finalProfile = "General";
445
- extraBlocks.push(buildUpsellBlock(`Profile '${targetProfile}' on Free tier is limited to 5,000 chars. Upgrade to PRO for unlimited scanning.`));
446
- } else {
447
- mcpLog(`${colors.cyan}ℹ️ [PrivacyScrubber] Profile '${targetProfile}' active (Free tier: up to 5,000 chars).${colors.reset}\n`);
448
- }
530
+ mcpLog(`${colors.yellowBold}⚠️ [PrivacyScrubber] Profile '${targetProfile}' active on Free Tier (5,000 char limit).${colors.reset}\n${colors.cyan}👉 Get a PRO key for unlimited use: https://privacyscrubber.com/pricing?utm_source=mcp_cli&utm_medium=terminal${colors.reset}\n`);
531
+ extraBlocks.push(buildUpsellBlock(`Profile '${targetProfile}' active (Free Tier limit: 5,000 chars). Upgrade to PRO for unlimited text.`));
449
532
  }
450
533
 
451
- const { processedText, wasTruncated } = truncateIfFree(text, license.isPro);
534
+ const { processedText, wasTruncated } = truncateIfFree(text, license.isPro, charLimit);
452
535
  if (wasTruncated) {
453
- extraBlocks.push(buildUpsellBlock(`Input was truncated to 50,000 characters (Free Tier limit).`));
536
+ extraBlocks.push(buildUpsellBlock(`Input was truncated to ${charLimit.toLocaleString()} characters (Free Tier limit).`));
454
537
  }
455
538
 
456
539
  if (!license.isPro) {
457
540
  const detected = detectSecrets(processedText);
458
541
  if (detected.length > 0) {
459
- mcpLog(`${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`);
460
- extraBlocks.push(buildUpsellBlock(`API Key / Secret (${detected.join(', ')}) detected! Sanitization skipped (Requires PRO Profile).`));
542
+ mcpLog(`${colors.redBold}🚫 [PrivacyScrubber] API Key / Secret (${detected.join(', ')}) detected! Request BLOCKED to protect your data (Requires PRO Profile).${colors.reset}\n${colors.cyan}👉 Upgrade at: https://privacyscrubber.com/pricing?utm_source=mcp_cli&utm_medium=terminal${colors.reset}\n`);
543
+ return {
544
+ isError: true,
545
+ content: [{ type: "text", text: `Error: API Key / Secret (${detected.join(', ')}) detected! Request BLOCKED to protect your data. Sanitizing DevOps secrets requires a PRO license. Get a key at: https://privacyscrubber.com/pricing` }]
546
+ };
461
547
  }
462
548
  }
463
549
 
464
- const sanitized = performSanitization(processedText, finalProfile);
465
-
466
- // Periodic soft nudge every N free-tier requests (no trigger event needed)
467
- if (!license.isPro && extraBlocks.length === 0 && sessionRequestCount % UPSELL_EVERY_N === 0) {
468
- extraBlocks.push(buildUpsellBlock(null));
469
- }
550
+ const { scrubbedText, newTokens } = performSanitization(processedText, finalProfile);
551
+
552
+ const telemetry = buildCisoAuditTelemetry(newTokens);
553
+ const receiptMd = formatAuditReceipt(telemetry);
470
554
 
471
555
  return {
472
556
  content: [
473
- { type: "text", text: sanitized },
474
- ...extraBlocks
557
+ { type: "text", text: scrubbedText + receiptMd },
558
+ ...extraBlocks.filter(Boolean)
475
559
  ]
476
560
  };
477
561
  }
@@ -584,36 +668,44 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
584
668
  let finalProfile = targetProfile;
585
669
  const extraBlocks = [];
586
670
 
587
- sessionRequestCount++;
671
+ const limitStatus = checkFreeTierLimit(license.isPro);
672
+ if (limitStatus.blocked) {
673
+ return {
674
+ isError: true,
675
+ content: [{ type: "text", text: `Error: Free tier daily limit exhausted (${FREE_TIER_DAILY_LIMIT} requests). Please set your PRO license key to continue. Get a key at: https://privacyscrubber.com/pricing` }]
676
+ };
677
+ }
588
678
 
679
+ const charLimit = (isAdvanced && !license.isPro) ? 5000 : 15000;
589
680
  if (isAdvanced && !license.isPro) {
590
- const reason = license.error ? ` (${license.error})` : "";
591
- mcpLog(`${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`);
592
- finalProfile = "General";
593
- extraBlocks.push(buildUpsellBlock(`Profile '${targetProfile}' requires PRO. Using 'General' as fallback.`));
681
+ mcpLog(`${colors.yellowBold}⚠️ [PrivacyScrubber] Profile '${targetProfile}' active on Free Tier (5,000 char limit).${colors.reset}\n${colors.cyan}👉 Get a PRO key for unlimited use: https://privacyscrubber.com/pricing?utm_source=mcp_cli&utm_medium=terminal${colors.reset}\n`);
682
+ extraBlocks.push(buildUpsellBlock(`Profile '${targetProfile}' active (Free Tier limit: 5,000 chars). Upgrade to PRO for unlimited text.`));
594
683
  }
595
684
 
596
- const { processedText: processedContent, wasTruncated } = truncateIfFree(content, license.isPro);
597
- if (wasTruncated) extraBlocks.push(buildUpsellBlock(`File content was truncated to 50,000 characters (Free Tier limit).`));
685
+ const { processedText: processedContent, wasTruncated } = truncateIfFree(content, license.isPro, charLimit);
686
+ if (wasTruncated) extraBlocks.push(buildUpsellBlock(`File content was truncated to ${charLimit.toLocaleString()} characters (Free Tier limit).`));
598
687
 
599
688
  if (!license.isPro) {
600
689
  const detected = detectSecrets(processedContent);
601
690
  if (detected.length > 0) {
602
- mcpLog(`${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`);
603
- extraBlocks.push(buildUpsellBlock(`API Key / Secret (${detected.join(', ')}) detected! Sanitization skipped (Requires PRO Profile).`));
691
+ mcpLog(`${colors.redBold}🚫 [PrivacyScrubber] API Key / Secret (${detected.join(', ')}) detected! Request BLOCKED to protect your data (Requires PRO Profile).${colors.reset}\n${colors.cyan}👉 Upgrade at: https://privacyscrubber.com/pricing?utm_source=mcp_cli&utm_medium=terminal${colors.reset}\n`);
692
+ return {
693
+ isError: true,
694
+ content: [{ type: "text", text: `Error: API Key / Secret (${detected.join(', ')}) detected! Request BLOCKED to protect your data. Sanitizing DevOps secrets requires a PRO license. Get a key at: https://privacyscrubber.com/pricing` }]
695
+ };
604
696
  }
605
697
  }
606
698
 
607
699
  const sanitized = performSanitization(processedContent, finalProfile);
608
-
609
- if (!license.isPro && extraBlocks.length === 0 && sessionRequestCount % UPSELL_EVERY_N === 0) {
610
- extraBlocks.push(buildUpsellBlock(null));
611
- }
700
+ const { scrubbedText, newTokens } = sanitized;
701
+ const telemetry = buildCisoAuditTelemetry(newTokens);
702
+ const receiptMarkdown = formatAuditReceipt(telemetry);
703
+ const combinedOutput = `${scrubbedText}\n\n${receiptMarkdown}`;
612
704
 
613
705
  return {
614
706
  content: [
615
- { type: "text", text: sanitized },
616
- ...extraBlocks
707
+ { type: "text", text: combinedOutput },
708
+ ...extraBlocks.filter(Boolean)
617
709
  ]
618
710
  };
619
711
  } catch (docxError) {
@@ -637,7 +729,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
637
729
  content: [
638
730
  {
639
731
  type: "text",
640
- 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`
732
+ 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?utm_source=mcp_cli&utm_medium=terminal`
641
733
  }
642
734
  ]
643
735
  };
@@ -654,36 +746,44 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
654
746
  let finalProfile = targetProfile;
655
747
  const extraBlocks = [];
656
748
 
657
- sessionRequestCount++;
749
+ const limitStatus = checkFreeTierLimit(license.isPro);
750
+ if (limitStatus.blocked) {
751
+ return {
752
+ isError: true,
753
+ content: [{ type: "text", text: `Error: Free tier daily limit exhausted (${FREE_TIER_DAILY_LIMIT} requests). Please set your PRO license key to continue. Get a key at: https://privacyscrubber.com/pricing` }]
754
+ };
755
+ }
658
756
 
757
+ const charLimit = (isAdvanced && !license.isPro) ? 5000 : 15000;
659
758
  if (isAdvanced && !license.isPro) {
660
- const reason = license.error ? ` (${license.error})` : "";
661
- mcpLog(`${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`);
662
- finalProfile = "General";
663
- extraBlocks.push(buildUpsellBlock(`Profile '${targetProfile}' requires PRO. Using 'General' as fallback.`));
759
+ mcpLog(`${colors.yellowBold}⚠️ [PrivacyScrubber] Profile '${targetProfile}' active on Free Tier (5,000 char limit).${colors.reset}\n${colors.cyan}👉 Get a PRO key for unlimited use: https://privacyscrubber.com/pricing?utm_source=mcp_cli&utm_medium=terminal${colors.reset}\n`);
760
+ extraBlocks.push(buildUpsellBlock(`Profile '${targetProfile}' active (Free Tier limit: 5,000 chars). Upgrade to PRO for unlimited text.`));
664
761
  }
665
762
 
666
- const { processedText: processedContent, wasTruncated } = truncateIfFree(content, license.isPro);
667
- if (wasTruncated) extraBlocks.push(buildUpsellBlock(`File content was truncated to 50,000 characters (Free Tier limit).`));
763
+ const { processedText: processedContent, wasTruncated } = truncateIfFree(content, license.isPro, charLimit);
764
+ if (wasTruncated) extraBlocks.push(buildUpsellBlock(`File content was truncated to ${charLimit.toLocaleString()} characters (Free Tier limit).`));
668
765
 
669
766
  if (!license.isPro) {
670
767
  const detected = detectSecrets(processedContent);
671
768
  if (detected.length > 0) {
672
- mcpLog(`${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`);
673
- extraBlocks.push(buildUpsellBlock(`API Key / Secret (${detected.join(', ')}) detected! Sanitization skipped (Requires PRO Profile).`));
769
+ mcpLog(`${colors.redBold}🚫 [PrivacyScrubber] API Key / Secret (${detected.join(', ')}) detected! Request BLOCKED to protect your data (Requires PRO Profile).${colors.reset}\n${colors.cyan}👉 Upgrade at: https://privacyscrubber.com/pricing?utm_source=mcp_cli&utm_medium=terminal${colors.reset}\n`);
770
+ return {
771
+ isError: true,
772
+ content: [{ type: "text", text: `Error: API Key / Secret (${detected.join(', ')}) detected! Request BLOCKED to protect your data. Sanitizing DevOps secrets requires a PRO license. Get a key at: https://privacyscrubber.com/pricing` }]
773
+ };
674
774
  }
675
775
  }
676
776
 
677
777
  const sanitized = performSanitization(processedContent, finalProfile);
678
-
679
- if (!license.isPro && extraBlocks.length === 0 && sessionRequestCount % UPSELL_EVERY_N === 0) {
680
- extraBlocks.push(buildUpsellBlock(null));
681
- }
778
+ const { scrubbedText, newTokens } = sanitized;
779
+ const telemetry = buildCisoAuditTelemetry(newTokens);
780
+ const receiptMarkdown = formatAuditReceipt(telemetry);
781
+ const combinedOutput = `${scrubbedText}\n\n${receiptMarkdown}`;
682
782
 
683
783
  return {
684
784
  content: [
685
- { type: "text", text: sanitized },
686
- ...extraBlocks
785
+ { type: "text", text: combinedOutput },
786
+ ...extraBlocks.filter(Boolean)
687
787
  ]
688
788
  };
689
789
  } catch (pdfError) {
@@ -711,36 +811,44 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
711
811
  let finalProfile = targetProfile;
712
812
  const extraBlocks = [];
713
813
 
714
- sessionRequestCount++;
814
+ const limitStatus = checkFreeTierLimit(license.isPro);
815
+ if (limitStatus.blocked) {
816
+ return {
817
+ isError: true,
818
+ content: [{ type: "text", text: `Error: Free tier daily limit exhausted (${FREE_TIER_DAILY_LIMIT} requests). Please set your PRO license key to continue. Get a key at: https://privacyscrubber.com/pricing` }]
819
+ };
820
+ }
715
821
 
822
+ const charLimit = (isAdvanced && !license.isPro) ? 5000 : 15000;
716
823
  if (isAdvanced && !license.isPro) {
717
- const reason = license.error ? ` (${license.error})` : "";
718
- mcpLog(`${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`);
719
- finalProfile = "General";
720
- extraBlocks.push(buildUpsellBlock(`Profile '${targetProfile}' requires PRO. Using 'General' as fallback.`));
824
+ mcpLog(`${colors.yellowBold}⚠️ [PrivacyScrubber] Profile '${targetProfile}' active on Free Tier (5,000 char limit).${colors.reset}\n${colors.cyan}👉 Get a PRO key for unlimited use: https://privacyscrubber.com/pricing?utm_source=mcp_cli&utm_medium=terminal${colors.reset}\n`);
825
+ extraBlocks.push(buildUpsellBlock(`Profile '${targetProfile}' active (Free Tier limit: 5,000 chars). Upgrade to PRO for unlimited text.`));
721
826
  }
722
827
 
723
- const { processedText: processedContent, wasTruncated } = truncateIfFree(content, license.isPro);
724
- if (wasTruncated) extraBlocks.push(buildUpsellBlock(`File content was truncated to 50,000 characters (Free Tier limit).`));
828
+ const { processedText: processedContent, wasTruncated } = truncateIfFree(content, license.isPro, charLimit);
829
+ if (wasTruncated) extraBlocks.push(buildUpsellBlock(`File content was truncated to ${charLimit.toLocaleString()} characters (Free Tier limit).`));
725
830
 
726
831
  if (!license.isPro) {
727
832
  const detected = detectSecrets(processedContent);
728
833
  if (detected.length > 0) {
729
- mcpLog(`${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`);
730
- extraBlocks.push(buildUpsellBlock(`API Key / Secret (${detected.join(', ')}) detected! Sanitization skipped (Requires PRO Profile).`));
834
+ mcpLog(`${colors.redBold}🚫 [PrivacyScrubber] API Key / Secret (${detected.join(', ')}) detected! Request BLOCKED to protect your data (Requires PRO Profile).${colors.reset}\n${colors.cyan}👉 Upgrade at: https://privacyscrubber.com/pricing?utm_source=mcp_cli&utm_medium=terminal${colors.reset}\n`);
835
+ return {
836
+ isError: true,
837
+ content: [{ type: "text", text: `Error: API Key / Secret (${detected.join(', ')}) detected! Request BLOCKED to protect your data. Sanitizing DevOps secrets requires a PRO license. Get a key at: https://privacyscrubber.com/pricing` }]
838
+ };
731
839
  }
732
840
  }
733
841
 
734
842
  const sanitized = performSanitization(processedContent, finalProfile);
735
-
736
- if (!license.isPro && extraBlocks.length === 0 && sessionRequestCount % UPSELL_EVERY_N === 0) {
737
- extraBlocks.push(buildUpsellBlock(null));
738
- }
843
+ const { scrubbedText, newTokens } = sanitized;
844
+ const telemetry = buildCisoAuditTelemetry(newTokens);
845
+ const receiptMarkdown = formatAuditReceipt(telemetry);
846
+ const combinedOutput = `${scrubbedText}\n\n${receiptMarkdown}`;
739
847
 
740
848
  return {
741
849
  content: [
742
- { type: "text", text: sanitized },
743
- ...extraBlocks
850
+ { type: "text", text: combinedOutput },
851
+ ...extraBlocks.filter(Boolean)
744
852
  ]
745
853
  };
746
854
  } catch (xlsxError) {
@@ -782,36 +890,42 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
782
890
  let finalProfile = targetProfile;
783
891
  const extraBlocks = [];
784
892
 
785
- sessionRequestCount++;
893
+ const limitStatus = checkFreeTierLimit(license.isPro);
894
+ if (limitStatus.blocked) {
895
+ return {
896
+ isError: true,
897
+ content: [{ type: "text", text: `Error: Free tier daily limit exhausted (${FREE_TIER_DAILY_LIMIT} requests). Please set your PRO license key to continue. Get a key at: https://privacyscrubber.com/pricing` }]
898
+ };
899
+ }
786
900
 
901
+ const charLimit = (isAdvanced && !license.isPro) ? 5000 : 15000;
787
902
  if (isAdvanced && !license.isPro) {
788
- const reason = license.error ? ` (${license.error})` : "";
789
- mcpLog(`${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`);
790
- finalProfile = "General";
791
- extraBlocks.push(buildUpsellBlock(`Profile '${targetProfile}' requires PRO. Using 'General' as fallback.`));
903
+ mcpLog(`${colors.yellowBold}⚠️ [PrivacyScrubber] Profile '${targetProfile}' active on Free Tier (5,000 char limit).${colors.reset}\n${colors.cyan}👉 Get a PRO key for unlimited use: https://privacyscrubber.com/pricing?utm_source=mcp_cli&utm_medium=terminal${colors.reset}\n`);
904
+ extraBlocks.push(buildUpsellBlock(`Profile '${targetProfile}' active (Free Tier limit: 5,000 chars). Upgrade to PRO for unlimited text.`));
792
905
  }
793
906
 
794
- const { processedText: processedContent2, wasTruncated: wasTruncated2 } = truncateIfFree(content, license.isPro);
795
- if (wasTruncated2) extraBlocks.push(buildUpsellBlock(`File content was truncated to 50,000 characters (Free Tier limit).`));
907
+ const { processedText: processedContent2, wasTruncated: wasTruncated2 } = truncateIfFree(content, license.isPro, charLimit);
908
+ if (wasTruncated2) extraBlocks.push(buildUpsellBlock(`File content was truncated to ${charLimit.toLocaleString()} characters (Free Tier limit).`));
796
909
 
797
910
  if (!license.isPro) {
798
911
  const detected = detectSecrets(processedContent2);
799
912
  if (detected.length > 0) {
800
- mcpLog(`${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`);
801
- extraBlocks.push(buildUpsellBlock(`API Key / Secret (${detected.join(', ')}) detected! Sanitization skipped (Requires PRO Profile).`));
913
+ mcpLog(`${colors.redBold}🚫 [PrivacyScrubber] API Key / Secret (${detected.join(', ')}) detected! Request BLOCKED to protect your data (Requires PRO Profile).${colors.reset}\n${colors.cyan}👉 Upgrade at: https://privacyscrubber.com/pricing?utm_source=mcp_cli&utm_medium=terminal${colors.reset}\n`);
914
+ return {
915
+ isError: true,
916
+ content: [{ type: "text", text: `Error: API Key / Secret (${detected.join(', ')}) detected! Request BLOCKED to protect your data. Sanitizing DevOps secrets requires a PRO license. Get a key at: https://privacyscrubber.com/pricing` }]
917
+ };
802
918
  }
803
919
  }
804
920
 
805
- const sanitized = performSanitization(processedContent2, finalProfile);
806
-
807
- if (!license.isPro && extraBlocks.length === 0 && sessionRequestCount % UPSELL_EVERY_N === 0) {
808
- extraBlocks.push(buildUpsellBlock(null));
809
- }
921
+ const { scrubbedText, newTokens } = performSanitization(processedContent2, finalProfile);
922
+ const telemetry = buildCisoAuditTelemetry(newTokens);
923
+ const receiptMd = formatAuditReceipt(telemetry);
810
924
 
811
925
  return {
812
926
  content: [
813
- { type: "text", text: sanitized },
814
- ...extraBlocks
927
+ { type: "text", text: scrubbedText + receiptMd },
928
+ ...extraBlocks.filter(Boolean)
815
929
  ]
816
930
  };
817
931
  }
@@ -837,6 +951,14 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
837
951
  const customRules = loadCustomRules();
838
952
  const normalizedProfile = (profile || "general").trim().toLowerCase();
839
953
  const license = checkLicenseStatus();
954
+
955
+ const limitStatus = checkFreeTierLimit(license.isPro);
956
+ if (limitStatus.blocked) {
957
+ return {
958
+ isError: true,
959
+ content: [{ type: "text", text: `Error: Free tier daily limit exhausted (${FREE_TIER_DAILY_LIMIT} requests). Please set your PRO license key to continue. Get a key at: https://privacyscrubber.com/pricing` }]
960
+ };
961
+ }
840
962
  const localSessionMap = {};
841
963
 
842
964
  const result = PrivacyScrubberCore.scrubText(content, customRules, {}, normalizedProfile, localSessionMap, license.isPro);
@@ -873,7 +995,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
873
995
  }
874
996
 
875
997
  if (isSecretLeak) {
876
- return { isError: true, content: [{ type: "text", text: `Error: API Key or Secret detected. Sanitization of raw secrets requires PRO Profile. Upgrade at https://privacyscrubber.com/pricing` }] };
998
+ return { isError: true, content: [{ type: "text", text: `Error: API Key or Secret detected. Sanitization of raw secrets requires PRO Profile. Upgrade at https://privacyscrubber.com/pricing?utm_source=mcp_cli&utm_medium=terminal` }] };
877
999
  }
878
1000
 
879
1001
  if (!no_backup) {
@@ -906,7 +1028,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
906
1028
  }
907
1029
 
908
1030
  const defaultTemplate = {
909
- "_comment": "PrivacyScrubber local configuration. Add custom rules and restart your MCP client (Cursor/Windsurf). Details: https://privacyscrubber.com/docs/config",
1031
+ "_comment": "PrivacyScrubber local configuration. Add custom rules and restart your MCP client (Cursor/Windsurf). Details: https://privacyscrubber.com/docs/config?utm_source=mcp_cli&utm_medium=terminal",
910
1032
  "customRules": [
911
1033
  {
912
1034
  "pattern": "my-secret-pattern-\\d+",
@@ -955,7 +1077,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
955
1077
  const profileList = license.isPro
956
1078
  ? 'All 23 profiles active (General, Dev, Medical, Legal, Finance, HR…)'
957
1079
  : 'General only — PRO unlocks 22 industry profiles';
958
- const sizeLimit = license.isPro ? 'Unlimited' : '50,000 characters per request';
1080
+ const sizeLimit = license.isPro ? 'Unlimited' : '15,000 characters per request';
959
1081
 
960
1082
  const configPath = resolveConfigPath();
961
1083
  let rulesStatus = '';
@@ -987,7 +1109,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
987
1109
  `║ PrivacyScrubber MCP Server v${MCP_VERSION.padEnd(10)} ║`,
988
1110
  '╠══════════════════════════════════════════════════╣',
989
1111
  `║ ${tierIcon} Tier: ${tier.padEnd(43)}║`,
990
- `║ 📊 Session requests: ${String(sessionRequestCount).padEnd(27)}║`,
1112
+ `║ 📊 Session requests: ${String(getDailyUsage()).padEnd(27)}║`,
991
1113
  `║ 📁 Input size limit: ${sizeLimit.padEnd(27)}║`,
992
1114
  '╠══════════════════════════════════════════════════╣',
993
1115
  `║ 🏷️ Profiles: ${profileList.substring(0,35).padEnd(35)}║`,
@@ -1001,12 +1123,12 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1001
1123
  lines.push(
1002
1124
  '║ ✅ PRO is active. All features unlocked. ║',
1003
1125
  '║ To regenerate your key or manage billing: ║',
1004
- '║ https://privacyscrubber.com/pricing ║'
1126
+ '║ https://privacyscrubber.com/pricing?utm_source=mcp_cli&utm_medium=terminal ║'
1005
1127
  );
1006
1128
  } else {
1007
1129
  lines.push(
1008
1130
  '║ 💳 Upgrade to PRO — $110 Lifetime ║',
1009
- '║ https://privacyscrubber.com/pricing ║',
1131
+ '║ https://privacyscrubber.com/pricing?utm_source=mcp_cli&utm_medium=terminal ║',
1010
1132
  '╠══════════════════════════════════════════════════╣',
1011
1133
  '║ After purchase, add your key to MCP config: ║',
1012
1134
  '║ ║',
@@ -1014,7 +1136,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1014
1136
  '║ "PRIVACYSCRUBBER_KEY": "<your-key-here>" ║',
1015
1137
  '║ ║',
1016
1138
  '║ Full setup guide: ║',
1017
- '║ https://privacyscrubber.com/features/mcp/ ║'
1139
+ '║ https://privacyscrubber.com/features/mcp/?utm_source=mcp_cli&utm_medium=terminal ║'
1018
1140
  );
1019
1141
  }
1020
1142
 
@@ -1057,20 +1179,30 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1057
1179
  const customRules = loadCustomRules();
1058
1180
  const normalizedProfile = (profile || "general").trim().toLowerCase();
1059
1181
  const license = checkLicenseStatus();
1182
+
1183
+ const limitStatus = checkFreeTierLimit(license.isPro);
1184
+ if (limitStatus.blocked) {
1185
+ return {
1186
+ isError: true,
1187
+ content: [{ type: "text", text: `Error: Free tier daily limit exhausted (${FREE_TIER_DAILY_LIMIT} requests). Please set your PRO license key to continue. Get a key at: https://privacyscrubber.com/pricing` }]
1188
+ };
1189
+ }
1060
1190
 
1191
+ let totalTokensForDirectory = {};
1061
1192
  for (const file of filesToScan) {
1062
1193
  try {
1063
1194
  const stat = await fs.promises.stat(file);
1064
1195
  if (stat.size > 10 * 1024 * 1024) continue; // skip >10MB
1065
1196
 
1066
1197
  const buffer = fs.readFileSync(file);
1067
- // basic binary check
1068
1198
  if (buffer.indexOf(0) !== -1) continue;
1069
1199
  const content = buffer.toString('utf8');
1070
1200
 
1071
1201
  const localSessionMap = {};
1072
1202
  const result = PrivacyScrubberCore.scrubText(content, customRules, {}, normalizedProfile, localSessionMap, license.isPro);
1073
1203
 
1204
+ Object.assign(totalTokensForDirectory, localSessionMap);
1205
+
1074
1206
  let isSecretLeak = false;
1075
1207
  if (!license.isPro && result.tokenMap) {
1076
1208
  Object.entries(result.tokenMap).forEach(([token, original]) => {
@@ -1106,8 +1238,11 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1106
1238
  reportLines.push("✅ Clean. No PII or secrets detected in the scanned files.");
1107
1239
  }
1108
1240
 
1241
+ const telemetry = buildCisoAuditTelemetry(totalTokensForDirectory);
1242
+ const receiptMd = formatAuditReceipt(telemetry);
1243
+
1109
1244
  return {
1110
- content: [{ type: "text", text: reportLines.join('\n') }]
1245
+ content: [{ type: "text", text: reportLines.join('\n') + receiptMd }]
1111
1246
  };
1112
1247
  }
1113
1248
 
@@ -1209,41 +1344,23 @@ function performSanitization(text, profile) {
1209
1344
  const normalizedProfile = (profile || "general").trim().toLowerCase();
1210
1345
  const license = checkLicenseStatus();
1211
1346
  const result = PrivacyScrubberCore.scrubText(text, customRules, {}, normalizedProfile, sessionMap, license.isPro);
1212
- if (!license.isPro && result.tokenMap) {
1213
- // Re-scan each masked token's original value against the shared detector.
1214
- // If it was a raw secret, restore it in the output and remove from sessionMap
1215
- // so it is never accidentally revealed by reveal_text.
1216
- Object.entries(result.tokenMap).forEach(([token, original]) => {
1217
- let isSecret = false;
1218
- for (const detector of DEVOPS_SECRETS_DETECTOR) {
1219
- detector.regex.lastIndex = 0;
1220
- if (detector.regex.test(original)) {
1221
- isSecret = true;
1222
- break;
1223
- }
1224
- }
1225
- if (isSecret) {
1226
- result.scrubbedText = result.scrubbedText.replace(token, original);
1227
- delete result.tokenMap[token];
1228
- }
1229
- });
1230
- }
1231
1347
 
1348
+ const newTokens = {};
1232
1349
  // Update our volatile map with new matches
1233
1350
  if (result.tokenMap) {
1234
1351
  Object.entries(result.tokenMap).forEach(([token, original]) => {
1235
1352
  sessionMap[token] = original;
1353
+ newTokens[token] = original;
1236
1354
  });
1237
1355
  }
1238
1356
 
1239
- return result.scrubbedText;
1357
+ return { scrubbedText: result.scrubbedText, newTokens };
1240
1358
  }
1241
1359
 
1242
- function truncateIfFree(text, isPro) {
1243
- const MAX_FREE_CHARS = 5000;
1244
- if (!isPro && text.length > MAX_FREE_CHARS) {
1245
- mcpLog(`${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`);
1246
- return { processedText: text.substring(0, MAX_FREE_CHARS), wasTruncated: true };
1360
+ function truncateIfFree(text, isPro, charLimit = 15000) {
1361
+ if (!isPro && text.length > charLimit) {
1362
+ mcpLog(`${colors.yellowBold}⚠️ [PrivacyScrubber] Input truncated to ${charLimit.toLocaleString()} characters (Free Tier Limit).${colors.reset}\n${colors.cyan}👉 Set PRIVACYSCRUBBER_KEY to your PRO license key for unlimited size.${colors.reset}\n`);
1363
+ return { processedText: text.substring(0, charLimit), wasTruncated: true };
1247
1364
  }
1248
1365
  return { processedText: text, wasTruncated: false };
1249
1366
  }