@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.
- package/.well-known/mcp/server-card.json +21 -3
- package/README.md +17 -5
- package/bundle.mcpb +0 -0
- package/index.js +345 -16
- package/manifest.json +5 -43
- package/package.json +21 -8
- package/scrubber-core.cjs +1 -1
- package/server.json +2 -2
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"serverInfo": {
|
|
3
3
|
"name": "privacyscrubber/pii-masking-mcp",
|
|
4
|
-
"version": "1.
|
|
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
|
[](https://privacyscrubber.com)
|
|
7
7
|
[](https://privacyscrubber.com)
|
|
8
8
|
|
|
9
|
-
**Zero-Trust
|
|
10
|
-
Locally scrubs PII, secrets, credentials, and custom regex rules from files and text contexts before they reach remote LLM providers.
|
|
9
|
+
**CISO-Approved Zero-Trust PII & Secrets Redaction MCP Server for Cursor, Windsurf, and Claude Desktop.**
|
|
10
|
+
Locally scrubs PII, secrets, credentials, and custom regex rules from files and text contexts before they reach remote LLM providers to prevent API leaks and ensure HIPAA/SOC 2 compliance at the developer endpoint.
|
|
11
11
|
|
|
12
12
|
---
|
|
13
13
|
|
|
@@ -149,11 +149,23 @@ Looking for real-time protection directly inside your web browser?
|
|
|
149
149
|
* **Chrome Extension:** Get the [PrivacyScrubber Chrome Extension](https://chromewebstore.google.com/detail/privacyscrubber-%E2%80%94-pii-red/pimoejgefeilajmmbpghifdmhdlkgjol) to sanitize prompts directly inside ChatGPT, Claude, and Gemini in real-time.
|
|
150
150
|
* **Web Sandbox:** Use the zero-server browser sanitization tools at [PrivacyScrubber Homepage](https://privacyscrubber.com/).
|
|
151
151
|
|
|
152
|
-
---
|
|
153
|
-
|
|
154
152
|
## π License & Commercial Upgrade
|
|
155
153
|
|
|
156
|
-
|
|
154
|
+
By default, the server runs under the **Free Tier** (restricted to 50,000 characters per request and the basic `General` PII profile). To unlock advanced engineering, medical, legal, and financial PII profiles, as well as team-wide custom rules, you can purchase a commercial license.
|
|
155
|
+
|
|
156
|
+
### Feature Comparison
|
|
157
|
+
|
|
158
|
+
| Feature | Free Tier | PRO Tier | TEAMS Tier |
|
|
159
|
+
| :--- | :--- | :--- | :--- |
|
|
160
|
+
| **Volatile Tokenization** | β
Yes | β
Yes | β
Yes |
|
|
161
|
+
| **Standard PII Masking** | β
Yes | β
Yes | β
Yes |
|
|
162
|
+
| **Max Character Length** | 50,000 chars | βΎοΈ Unlimited | βΎοΈ Unlimited |
|
|
163
|
+
| **Industry Profiles** | General Only | 22+ Profiles | 22+ Profiles |
|
|
164
|
+
| **Custom Regex Rules** | β Locked | βΎοΈ Unlimited | βΎοΈ Unlimited |
|
|
165
|
+
| **Team Rules Sync (GPO)** | β No | β No | β
Yes (Shared Link) |
|
|
166
|
+
| **Licensing Cost** | $0 | **$110 Lifetime** | **$99/mo Flat Rate** |
|
|
167
|
+
|
|
168
|
+
π **[Acquire a PRO / TEAMS License Key at privacyscrubber.com/pricing](https://privacyscrubber.com/pricing)**
|
|
157
169
|
|
|
158
170
|
---
|
|
159
171
|
|
package/bundle.mcpb
CHANGED
|
Binary file
|
package/index.js
CHANGED
|
@@ -24,6 +24,11 @@ const require = createRequire(import.meta.url);
|
|
|
24
24
|
const __filename = fileURLToPath(import.meta.url);
|
|
25
25
|
const __dirname = path.dirname(__filename);
|
|
26
26
|
|
|
27
|
+
// Version is sourced from mcp-server/package.json.
|
|
28
|
+
// build.js syncs mcp-server/package.json from root package.json on every build.
|
|
29
|
+
// Never hardcode the version β bump package.json at root instead.
|
|
30
|
+
const MCP_VERSION = require('./package.json').version;
|
|
31
|
+
|
|
27
32
|
// Import the production core engine with 100% parity
|
|
28
33
|
const scrubberCorePath = path.resolve(__dirname, './scrubber-core.cjs');
|
|
29
34
|
const PrivacyScrubberCore = require(scrubberCorePath);
|
|
@@ -34,6 +39,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:
|
|
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(
|
|
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(
|
|
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)
|
|
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(
|
|
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
|
-
|
|
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
|
|
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(
|
|
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
|
-
//
|
|
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(
|
|
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).
|
|
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
|
});
|