@privacyscrubber/mcp-server 1.7.2 → 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.
@@ -0,0 +1,10 @@
1
+ {
2
+ "type": "object",
3
+ "properties": {
4
+ "PRIVACYSCRUBBER_KEY": {
5
+ "type": "string",
6
+ "title": "PrivacyScrubber License Key",
7
+ "description": "Optional PRO or TEAMS license key. Leave blank to run on the Free tier."
8
+ }
9
+ }
10
+ }
package/index.js CHANGED
@@ -37,7 +37,10 @@ const MCP_VERSION = require('./package.json').version;
37
37
  // Import the production core engine with 100% parity
38
38
  const scrubberCorePath = path.resolve(__dirname, './scrubber-core.cjs');
39
39
  const PrivacyScrubberCore = require(scrubberCorePath);
40
- const LicenseManager = require('./ps-license-manager.js');
40
+ let LicenseManager = require('./ps-license-manager.js');
41
+ if (!LicenseManager || typeof LicenseManager.validate !== 'function') {
42
+ LicenseManager = global.LicenseManager;
43
+ }
41
44
 
42
45
  // Initialize core engine
43
46
  PrivacyScrubberCore.init();
@@ -83,15 +86,144 @@ function checkLicenseStatus() {
83
86
  const result = LicenseManager.validate(key);
84
87
 
85
88
  if (!result.valid) {
89
+ mcpLog(`DEBUG: License invalid because: ${result.reason || "Unknown"}`);
86
90
  return { isPro: false, type: null, error: result.reason || "Invalid license format or signature." };
87
91
  }
88
92
 
89
93
  return { isPro: true, type: result.tier, error: null };
90
94
  }
91
95
 
92
- // ── Session request counter (volatile in-memory) ─────────────────────────────
93
- let sessionRequestCount = 0;
94
- const UPSELL_EVERY_N = 10; // show upsell nudge every N free-tier requests
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;
109
+ try {
110
+ const data = JSON.parse(fs.readFileSync(file, 'utf8'));
111
+ if (data.date === today) {
112
+ return data.count || 0;
113
+ }
114
+ } catch (e) {}
115
+ return 0;
116
+ }
117
+
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
+ }
168
+ }
169
+
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)';
184
+ }
185
+
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
+ };
212
+ }
213
+
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') ? '🟠' : '🟢');
224
+
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
+ }
95
227
 
96
228
  // Build a clean upsell content block (Disabled to prevent agent-visible marketing content)
97
229
  function buildUpsellBlock(reason) {
@@ -385,7 +517,13 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
385
517
  let finalProfile = targetProfile;
386
518
  const extraBlocks = [];
387
519
 
388
- 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
+ }
389
527
 
390
528
  const charLimit = (isAdvanced && !license.isPro) ? 5000 : 15000;
391
529
  if (isAdvanced && !license.isPro) {
@@ -409,16 +547,14 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
409
547
  }
410
548
  }
411
549
 
412
- const sanitized = performSanitization(processedText, finalProfile);
413
-
414
- // Periodic soft nudge every N free-tier requests (no trigger event needed)
415
- if (!license.isPro && extraBlocks.length === 0 && sessionRequestCount % UPSELL_EVERY_N === 0) {
416
- extraBlocks.push(buildUpsellBlock(null));
417
- }
550
+ const { scrubbedText, newTokens } = performSanitization(processedText, finalProfile);
551
+
552
+ const telemetry = buildCisoAuditTelemetry(newTokens);
553
+ const receiptMd = formatAuditReceipt(telemetry);
418
554
 
419
555
  return {
420
556
  content: [
421
- { type: "text", text: sanitized },
557
+ { type: "text", text: scrubbedText + receiptMd },
422
558
  ...extraBlocks.filter(Boolean)
423
559
  ]
424
560
  };
@@ -532,7 +668,13 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
532
668
  let finalProfile = targetProfile;
533
669
  const extraBlocks = [];
534
670
 
535
- 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
+ }
536
678
 
537
679
  const charLimit = (isAdvanced && !license.isPro) ? 5000 : 15000;
538
680
  if (isAdvanced && !license.isPro) {
@@ -555,14 +697,14 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
555
697
  }
556
698
 
557
699
  const sanitized = performSanitization(processedContent, finalProfile);
558
-
559
- if (!license.isPro && extraBlocks.length === 0 && sessionRequestCount % UPSELL_EVERY_N === 0) {
560
- extraBlocks.push(buildUpsellBlock(null));
561
- }
700
+ const { scrubbedText, newTokens } = sanitized;
701
+ const telemetry = buildCisoAuditTelemetry(newTokens);
702
+ const receiptMarkdown = formatAuditReceipt(telemetry);
703
+ const combinedOutput = `${scrubbedText}\n\n${receiptMarkdown}`;
562
704
 
563
705
  return {
564
706
  content: [
565
- { type: "text", text: sanitized },
707
+ { type: "text", text: combinedOutput },
566
708
  ...extraBlocks.filter(Boolean)
567
709
  ]
568
710
  };
@@ -604,7 +746,13 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
604
746
  let finalProfile = targetProfile;
605
747
  const extraBlocks = [];
606
748
 
607
- 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
+ }
608
756
 
609
757
  const charLimit = (isAdvanced && !license.isPro) ? 5000 : 15000;
610
758
  if (isAdvanced && !license.isPro) {
@@ -627,14 +775,14 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
627
775
  }
628
776
 
629
777
  const sanitized = performSanitization(processedContent, finalProfile);
630
-
631
- if (!license.isPro && extraBlocks.length === 0 && sessionRequestCount % UPSELL_EVERY_N === 0) {
632
- extraBlocks.push(buildUpsellBlock(null));
633
- }
778
+ const { scrubbedText, newTokens } = sanitized;
779
+ const telemetry = buildCisoAuditTelemetry(newTokens);
780
+ const receiptMarkdown = formatAuditReceipt(telemetry);
781
+ const combinedOutput = `${scrubbedText}\n\n${receiptMarkdown}`;
634
782
 
635
783
  return {
636
784
  content: [
637
- { type: "text", text: sanitized },
785
+ { type: "text", text: combinedOutput },
638
786
  ...extraBlocks.filter(Boolean)
639
787
  ]
640
788
  };
@@ -663,7 +811,13 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
663
811
  let finalProfile = targetProfile;
664
812
  const extraBlocks = [];
665
813
 
666
- 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
+ }
667
821
 
668
822
  const charLimit = (isAdvanced && !license.isPro) ? 5000 : 15000;
669
823
  if (isAdvanced && !license.isPro) {
@@ -686,14 +840,14 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
686
840
  }
687
841
 
688
842
  const sanitized = performSanitization(processedContent, finalProfile);
689
-
690
- if (!license.isPro && extraBlocks.length === 0 && sessionRequestCount % UPSELL_EVERY_N === 0) {
691
- extraBlocks.push(buildUpsellBlock(null));
692
- }
843
+ const { scrubbedText, newTokens } = sanitized;
844
+ const telemetry = buildCisoAuditTelemetry(newTokens);
845
+ const receiptMarkdown = formatAuditReceipt(telemetry);
846
+ const combinedOutput = `${scrubbedText}\n\n${receiptMarkdown}`;
693
847
 
694
848
  return {
695
849
  content: [
696
- { type: "text", text: sanitized },
850
+ { type: "text", text: combinedOutput },
697
851
  ...extraBlocks.filter(Boolean)
698
852
  ]
699
853
  };
@@ -736,7 +890,13 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
736
890
  let finalProfile = targetProfile;
737
891
  const extraBlocks = [];
738
892
 
739
- 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
+ }
740
900
 
741
901
  const charLimit = (isAdvanced && !license.isPro) ? 5000 : 15000;
742
902
  if (isAdvanced && !license.isPro) {
@@ -758,15 +918,13 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
758
918
  }
759
919
  }
760
920
 
761
- const sanitized = performSanitization(processedContent2, finalProfile);
762
-
763
- if (!license.isPro && extraBlocks.length === 0 && sessionRequestCount % UPSELL_EVERY_N === 0) {
764
- extraBlocks.push(buildUpsellBlock(null));
765
- }
921
+ const { scrubbedText, newTokens } = performSanitization(processedContent2, finalProfile);
922
+ const telemetry = buildCisoAuditTelemetry(newTokens);
923
+ const receiptMd = formatAuditReceipt(telemetry);
766
924
 
767
925
  return {
768
926
  content: [
769
- { type: "text", text: sanitized },
927
+ { type: "text", text: scrubbedText + receiptMd },
770
928
  ...extraBlocks.filter(Boolean)
771
929
  ]
772
930
  };
@@ -793,6 +951,14 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
793
951
  const customRules = loadCustomRules();
794
952
  const normalizedProfile = (profile || "general").trim().toLowerCase();
795
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
+ }
796
962
  const localSessionMap = {};
797
963
 
798
964
  const result = PrivacyScrubberCore.scrubText(content, customRules, {}, normalizedProfile, localSessionMap, license.isPro);
@@ -943,7 +1109,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
943
1109
  `║ PrivacyScrubber MCP Server v${MCP_VERSION.padEnd(10)} ║`,
944
1110
  '╠══════════════════════════════════════════════════╣',
945
1111
  `║ ${tierIcon} Tier: ${tier.padEnd(43)}║`,
946
- `║ 📊 Session requests: ${String(sessionRequestCount).padEnd(27)}║`,
1112
+ `║ 📊 Session requests: ${String(getDailyUsage()).padEnd(27)}║`,
947
1113
  `║ 📁 Input size limit: ${sizeLimit.padEnd(27)}║`,
948
1114
  '╠══════════════════════════════════════════════════╣',
949
1115
  `║ 🏷️ Profiles: ${profileList.substring(0,35).padEnd(35)}║`,
@@ -1013,20 +1179,30 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1013
1179
  const customRules = loadCustomRules();
1014
1180
  const normalizedProfile = (profile || "general").trim().toLowerCase();
1015
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
+ }
1016
1190
 
1191
+ let totalTokensForDirectory = {};
1017
1192
  for (const file of filesToScan) {
1018
1193
  try {
1019
1194
  const stat = await fs.promises.stat(file);
1020
1195
  if (stat.size > 10 * 1024 * 1024) continue; // skip >10MB
1021
1196
 
1022
1197
  const buffer = fs.readFileSync(file);
1023
- // basic binary check
1024
1198
  if (buffer.indexOf(0) !== -1) continue;
1025
1199
  const content = buffer.toString('utf8');
1026
1200
 
1027
1201
  const localSessionMap = {};
1028
1202
  const result = PrivacyScrubberCore.scrubText(content, customRules, {}, normalizedProfile, localSessionMap, license.isPro);
1029
1203
 
1204
+ Object.assign(totalTokensForDirectory, localSessionMap);
1205
+
1030
1206
  let isSecretLeak = false;
1031
1207
  if (!license.isPro && result.tokenMap) {
1032
1208
  Object.entries(result.tokenMap).forEach(([token, original]) => {
@@ -1062,8 +1238,11 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1062
1238
  reportLines.push("✅ Clean. No PII or secrets detected in the scanned files.");
1063
1239
  }
1064
1240
 
1241
+ const telemetry = buildCisoAuditTelemetry(totalTokensForDirectory);
1242
+ const receiptMd = formatAuditReceipt(telemetry);
1243
+
1065
1244
  return {
1066
- content: [{ type: "text", text: reportLines.join('\n') }]
1245
+ content: [{ type: "text", text: reportLines.join('\n') + receiptMd }]
1067
1246
  };
1068
1247
  }
1069
1248
 
@@ -1166,14 +1345,16 @@ function performSanitization(text, profile) {
1166
1345
  const license = checkLicenseStatus();
1167
1346
  const result = PrivacyScrubberCore.scrubText(text, customRules, {}, normalizedProfile, sessionMap, license.isPro);
1168
1347
 
1348
+ const newTokens = {};
1169
1349
  // Update our volatile map with new matches
1170
1350
  if (result.tokenMap) {
1171
1351
  Object.entries(result.tokenMap).forEach(([token, original]) => {
1172
1352
  sessionMap[token] = original;
1353
+ newTokens[token] = original;
1173
1354
  });
1174
1355
  }
1175
1356
 
1176
- return result.scrubbedText;
1357
+ return { scrubbedText: result.scrubbedText, newTokens };
1177
1358
  }
1178
1359
 
1179
1360
  function truncateIfFree(text, isPro, charLimit = 15000) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@privacyscrubber/mcp-server",
3
- "version": "1.7.2",
3
+ "version": "1.7.6",
4
4
  "mcpName": "io.github.moxno/privacyscrubber-mcp",
5
5
  "description": "CISO-Approved Zero-Trust PII & Secrets Redaction MCP Server for Cursor, Windsurf, and Claude Desktop. Prevent API leaks and comply with HIPAA/SOC 2 locally.",
6
6
  "main": "index.js",
@@ -13,6 +13,10 @@
13
13
  "install.js",
14
14
  "pii-masking-run.js",
15
15
  "scrubber-core.cjs",
16
+ "ps-license-manager.js",
17
+ "ps-pii-engine.cjs",
18
+ "ps-pii-engine.js",
19
+ "config-schema.json",
16
20
  "smithery.yaml",
17
21
  "server.json",
18
22
  "manifest.json",