@privacyscrubber/mcp-server 1.6.9 → 1.7.1
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/README.md +1 -0
- package/index.js +400 -32
- package/manifest.json +1 -1
- package/package.json +4 -2
- package/pii-masking-run.js +238 -0
- package/scrubber-core.cjs +1 -1
- package/server.json +3 -3
- package/.well-known/mcp/server-card.json +0 -78
- package/LICENSE +0 -21
package/README.md
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# @privacyscrubber/mcp-server
|
|
2
2
|
|
|
3
3
|
[](https://www.npmjs.com/package/@privacyscrubber/mcp-server)
|
|
4
|
+
[](https://www.npmjs.com/package/@privacyscrubber/mcp-server)
|
|
4
5
|
[](https://opensource.org/licenses/MIT)
|
|
5
6
|
[](https://smithery.ai/servers/privacyscrubber/pii-masking-mcp)
|
|
6
7
|
[](https://privacyscrubber.com)
|
package/index.js
CHANGED
|
@@ -12,6 +12,10 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
|
|
|
12
12
|
import {
|
|
13
13
|
CallToolRequestSchema,
|
|
14
14
|
ListToolsRequestSchema,
|
|
15
|
+
ListPromptsRequestSchema,
|
|
16
|
+
GetPromptRequestSchema,
|
|
17
|
+
ListResourceTemplatesRequestSchema,
|
|
18
|
+
ReadResourceRequestSchema,
|
|
15
19
|
} from "@modelcontextprotocol/sdk/types.js";
|
|
16
20
|
import { createRequire } from 'module';
|
|
17
21
|
import fs from 'fs';
|
|
@@ -49,6 +53,18 @@ const colors = {
|
|
|
49
53
|
reset: '\x1b[0m'
|
|
50
54
|
};
|
|
51
55
|
|
|
56
|
+
// Helper to log to both standard terminal (with colors) and MCP logging protocol
|
|
57
|
+
function mcpLog(msg) {
|
|
58
|
+
const clean = msg.replace(/\x1b\[[0-9;]*m/g, '').trim();
|
|
59
|
+
const level = clean.includes('⚠️') || clean.includes('Error') ? "warning" : "info";
|
|
60
|
+
try {
|
|
61
|
+
server.sendLoggingMessage({ level, data: clean });
|
|
62
|
+
} catch(e) {
|
|
63
|
+
// Ignore if server isn't fully connected yet
|
|
64
|
+
}
|
|
65
|
+
process.stderr.write(msg);
|
|
66
|
+
}
|
|
67
|
+
|
|
52
68
|
// Secrets detection patterns — defined once here, shared by detectSecrets() and performSanitization()
|
|
53
69
|
const DEVOPS_SECRETS_DETECTOR = [
|
|
54
70
|
{ name: 'AWS Credentials', regex: /\b(?:AKIA|ASIA|AGPA|AIDA|AROA|AIPA)[A-Z0-9]{16}\b/g },
|
|
@@ -60,13 +76,13 @@ const DEVOPS_SECRETS_DETECTOR = [
|
|
|
60
76
|
|
|
61
77
|
// Hardcoded public key for offline cryptographic verification
|
|
62
78
|
const PUBLIC_KEY = `-----BEGIN PUBLIC KEY-----
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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
|
|
70
86
|
-----END PUBLIC KEY-----`;
|
|
71
87
|
|
|
72
88
|
function checkLicenseStatus() {
|
|
@@ -146,6 +162,9 @@ const server = new Server(
|
|
|
146
162
|
{
|
|
147
163
|
capabilities: {
|
|
148
164
|
tools: {},
|
|
165
|
+
prompts: {},
|
|
166
|
+
logging: {},
|
|
167
|
+
resources: {},
|
|
149
168
|
},
|
|
150
169
|
}
|
|
151
170
|
);
|
|
@@ -156,7 +175,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
156
175
|
tools: [
|
|
157
176
|
{
|
|
158
177
|
name: "sanitize_text",
|
|
159
|
-
description: "Locally scrubs PII, secrets, and credentials (like API keys, passwords, emails, phones, names) from code, logs, or text. Replaces them with safe placeholders (e.g., [EMAIL_1], [API_KEY_1]). Keep your data secure before passing it to any LLM.",
|
|
178
|
+
description: "STEP 1: Call this first. You MUST NOT process raw user data before calling this. Locally scrubs PII, secrets, and credentials (like API keys, passwords, emails, phones, names) from code, logs, or text. Replaces them with safe placeholders (e.g., [EMAIL_1], [API_KEY_1]). Keep your data secure before passing it to any LLM.",
|
|
160
179
|
inputSchema: {
|
|
161
180
|
type: "object",
|
|
162
181
|
properties: {
|
|
@@ -174,7 +193,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
174
193
|
},
|
|
175
194
|
{
|
|
176
195
|
name: "reveal_text",
|
|
177
|
-
description: "
|
|
196
|
+
description: "STEP 3: Call this last. You MUST pass your final generated response through this tool to restore tokens (e.g., [EMAIL_1]) back with the original private data from the local volatile RAM-only session map before showing it to the user.",
|
|
178
197
|
inputSchema: {
|
|
179
198
|
type: "object",
|
|
180
199
|
properties: {
|
|
@@ -204,6 +223,59 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
204
223
|
required: ["file_path"]
|
|
205
224
|
}
|
|
206
225
|
},
|
|
226
|
+
{
|
|
227
|
+
name: "audit_directory_for_pii",
|
|
228
|
+
description: "Scans a local directory for leaks of secrets, keys, and PII. Returns a summary report. Use this tool for Security Auditing.",
|
|
229
|
+
inputSchema: {
|
|
230
|
+
type: "object",
|
|
231
|
+
properties: {
|
|
232
|
+
directory_path: {
|
|
233
|
+
type: "string",
|
|
234
|
+
description: "Absolute path to the directory to audit."
|
|
235
|
+
},
|
|
236
|
+
profile: {
|
|
237
|
+
type: "string",
|
|
238
|
+
description: "The detection profile to use. Defaults to 'General'."
|
|
239
|
+
},
|
|
240
|
+
extensions: {
|
|
241
|
+
type: "array",
|
|
242
|
+
items: { type: "string" },
|
|
243
|
+
description: "List of file extensions to scan (e.g. ['.env', '.log', '.js']). If empty, scans all text files."
|
|
244
|
+
},
|
|
245
|
+
ignore_node_modules: {
|
|
246
|
+
type: "boolean",
|
|
247
|
+
description: "Whether to ignore 'node_modules' directories. Defaults to true."
|
|
248
|
+
}
|
|
249
|
+
},
|
|
250
|
+
required: ["directory_path"]
|
|
251
|
+
}
|
|
252
|
+
},
|
|
253
|
+
{
|
|
254
|
+
name: "redact_file",
|
|
255
|
+
description: "Action/Redact: In-place redaction of a local file. Replaces PII and secrets with tokens and saves the file. By default, creates a .bak backup. Use dry_run=true to test without modifying.",
|
|
256
|
+
inputSchema: {
|
|
257
|
+
type: "object",
|
|
258
|
+
properties: {
|
|
259
|
+
file_path: {
|
|
260
|
+
type: "string",
|
|
261
|
+
description: "Absolute path to the file to redact."
|
|
262
|
+
},
|
|
263
|
+
profile: {
|
|
264
|
+
type: "string",
|
|
265
|
+
description: "The detection profile to use. Defaults to 'General'."
|
|
266
|
+
},
|
|
267
|
+
dry_run: {
|
|
268
|
+
type: "boolean",
|
|
269
|
+
description: "If true, does not modify the file, only returns the metrics and a preview."
|
|
270
|
+
},
|
|
271
|
+
no_backup: {
|
|
272
|
+
type: "boolean",
|
|
273
|
+
description: "If true, does not create a .bak file. Use with extreme caution!"
|
|
274
|
+
}
|
|
275
|
+
},
|
|
276
|
+
required: ["file_path"]
|
|
277
|
+
}
|
|
278
|
+
},
|
|
207
279
|
{
|
|
208
280
|
name: "create_default_config",
|
|
209
281
|
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.",
|
|
@@ -226,6 +298,116 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
226
298
|
};
|
|
227
299
|
});
|
|
228
300
|
|
|
301
|
+
// List available prompts
|
|
302
|
+
server.setRequestHandler(ListPromptsRequestSchema, async () => {
|
|
303
|
+
return {
|
|
304
|
+
prompts: [
|
|
305
|
+
{
|
|
306
|
+
name: "secure_ai_workflow",
|
|
307
|
+
description: "Zero-Trust Data Sanitization workflow instruction. Use this to automatically scrub PII before processing data.",
|
|
308
|
+
},
|
|
309
|
+
{
|
|
310
|
+
name: "safe_redaction_workflow",
|
|
311
|
+
description: "Instructions for using the redact_file tool safely with QA checks.",
|
|
312
|
+
}
|
|
313
|
+
]
|
|
314
|
+
};
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
// Handle prompt execution
|
|
318
|
+
server.setRequestHandler(GetPromptRequestSchema, async (request) => {
|
|
319
|
+
if (request.params.name === "safe_redaction_workflow") {
|
|
320
|
+
return {
|
|
321
|
+
description: "Instructions for safely redacting files in-place.",
|
|
322
|
+
messages: [
|
|
323
|
+
{
|
|
324
|
+
role: "user",
|
|
325
|
+
content: {
|
|
326
|
+
type: "text",
|
|
327
|
+
text: "When you need to redact or scrub a file on disk permanently, follow this quality-assured workflow:\n1. Run `redact_file` with `dry_run: true` to preview the changes and check how many PII tokens will be replaced.\n2. Verify the metrics. If the numbers look correct and no unintended data is being scrubbed, run `redact_file` with `dry_run: false` to apply the redaction.\n3. Ensure you have backups. `redact_file` creates a `.bak` backup by default. NEVER set `no_backup: true` unless the user explicitly asks to destroy the original data without a backup."
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
]
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
if (request.params.name !== "secure_ai_workflow") {
|
|
335
|
+
throw new Error(`Unknown prompt: ${request.params.name}`);
|
|
336
|
+
}
|
|
337
|
+
return {
|
|
338
|
+
description: "Instructions for securely processing data with PrivacyScrubber.",
|
|
339
|
+
messages: [
|
|
340
|
+
{
|
|
341
|
+
role: "user",
|
|
342
|
+
content: {
|
|
343
|
+
type: "text",
|
|
344
|
+
text: "You are a secure, privacy-first AI agent. Whenever you are asked to process, analyze, or rewrite text or files that may contain PII (Personally Identifiable Information), secrets, or credentials, you MUST follow this Zero-Trust workflow:\n\n1. STEP 1: Call the `sanitize_text` or `sanitize_file` tool first to scrub the input. DO NOT process the raw user data.\n2. STEP 2: Perform your analysis or task on the sanitized output provided by the tool.\n3. STEP 3: Before returning your final answer to the user, you MUST call the `reveal_text` tool, passing your final answer. This will restore the original private data (e.g., replacing [EMAIL_1] with the real email).\n4. STEP 4: Return the result of the `reveal_text` tool to the user."
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
]
|
|
348
|
+
};
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
// List Resource Templates
|
|
352
|
+
server.setRequestHandler(ListResourceTemplatesRequestSchema, async () => {
|
|
353
|
+
return {
|
|
354
|
+
resourceTemplates: [
|
|
355
|
+
{
|
|
356
|
+
uriTemplate: "mcp://privacyscrubber/profiles/{name}",
|
|
357
|
+
name: "PII Detection Profile Details",
|
|
358
|
+
description: "Returns the description and target entities of a specific PrivacyScrubber profile (e.g., 'Medical', 'Finance', 'General'). Use this to understand what a profile does."
|
|
359
|
+
}
|
|
360
|
+
]
|
|
361
|
+
};
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
// Read Resource
|
|
365
|
+
server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
|
|
366
|
+
const uri = request.params.uri;
|
|
367
|
+
const match = uri.match(/^mcp:\/\/privacyscrubber\/profiles\/(.+)$/);
|
|
368
|
+
if (match) {
|
|
369
|
+
const profileName = match[1];
|
|
370
|
+
const descriptions = {
|
|
371
|
+
"general": "Scrubs basic PII: Names, Emails, Phone Numbers, Credit Cards, SSN, IPv4/IPv6, and standard API Keys.",
|
|
372
|
+
"dev": "Engineering profile. Scrubs AWS keys, JWTs, GCP keys, Stripe keys, database connection strings, and internal IP addresses.",
|
|
373
|
+
"engineering": "Engineering profile. Scrubs AWS keys, JWTs, GCP keys, Stripe keys, database connection strings, internal IPs, Docker paths, and K8s internal endpoints.",
|
|
374
|
+
"tech": "Technology profile. Scrubs cloud instance IDs, node/cluster IDs, environment tags, kubeconfig, and terraform states.",
|
|
375
|
+
"medical": "HIPAA compliance profile. Scrubs ICD-10 codes, medical record numbers (MRN), DEA numbers, NPI, patient names, and health conditions.",
|
|
376
|
+
"finance": "PCI/Finance profile. Scrubs IBAN, SWIFT codes, credit cards, routing numbers, and financial transaction IDs.",
|
|
377
|
+
"wealthmgmt": "Wealth Management profile. Scrubs ABA/Routing numbers, trust names, portfolio values, net worth, RMDs, and account numbers.",
|
|
378
|
+
"insurance": "Insurance profile. Scrubs claim numbers, policy numbers, settlement amounts, NAIC, adjustor IDs, and insured names.",
|
|
379
|
+
"accounting": "Accounting profile. Scrubs EIN, FEIN, Tax IDs, AGI, tax forms (1040, W-2, 1099), refund amounts, and PTINs.",
|
|
380
|
+
"legal": "Legal profile. Scrubs case numbers, matter numbers, attorney-client privilege markers, and litigation IDs.",
|
|
381
|
+
"hr": "Human Resources profile. Scrubs employee IDs (EEID), resumes, DOBs, tenant IDs, and home addresses.",
|
|
382
|
+
"security": "Security profile. Scrubs secrets, AWS keys, JWTs, Stripe keys, incident IDs, and breach reports.",
|
|
383
|
+
"marketing": "Marketing profile. Scrubs lead IDs, prospect IDs, GCLID/FBCLID, LTV/CAC amounts, cohort IDs, and segment IDs.",
|
|
384
|
+
"bizops": "BizOps profile. Scrubs deals, KPIs, vendor IDs, EBITDA/Revenue figures, NDAs, and M&A identifiers.",
|
|
385
|
+
"sales": "Sales profile. Scrubs opportunity IDs, DocuSign hashes, ARR/MRR/Quota amounts, and SFDC/HubSpot IDs.",
|
|
386
|
+
"support": "Support profile. Scrubs Zendesk/Jira ticket numbers, RMA/Return IDs, and loyalty/rewards numbers.",
|
|
387
|
+
"realestate": "Real Estate profile. Scrubs MLS numbers, LIS IDs, parcel numbers, rent/escrow amounts, and gate/lobby codes.",
|
|
388
|
+
"compliance": "Compliance profile. Scrubs GDPR/HIPAA/SOC2 audit IDs, DPA policy numbers, and SAR/DSAR request IDs.",
|
|
389
|
+
"ccpa": "CCPA profile. Scrubs driver licenses, precise geolocation (Lat/Long), CCPA/CPRA opt-out markers, and account numbers.",
|
|
390
|
+
"agents": "AI Agents profile. Scrubs agent IDs, vector IDs, task IDs, system prompts, and OpenAI API keys.",
|
|
391
|
+
"academic": "Academic profile. Scrubs student/alumni IDs, course numbers, FERPA/IRB IDs, and academic grades.",
|
|
392
|
+
"creative": "Creative profile. Scrubs project IDs, script drafts, spoiler/embargo tags, and ghostwriter names.",
|
|
393
|
+
"personal": "Personal profile. Scrubs birthdays, passwords, PINs, and emergency contacts/family phone numbers.",
|
|
394
|
+
"pharma": "Pharma/Clinical profile. Scrubs patient IDs, study protocols, IND/NDA numbers, IRB IDs, batch/lot serials, and dosages."
|
|
395
|
+
};
|
|
396
|
+
const desc = descriptions[profileName.toLowerCase()] || `The '${profileName}' profile is a PRO-tier detection ruleset tuned for specific industry compliance. It detects and sanitizes domain-specific identifiers.`;
|
|
397
|
+
|
|
398
|
+
return {
|
|
399
|
+
contents: [
|
|
400
|
+
{
|
|
401
|
+
uri,
|
|
402
|
+
mimeType: "text/markdown",
|
|
403
|
+
text: `# Profile: ${profileName}\n\n**Description:** ${desc}\n\n*Note: To use this profile, pass \`"profile": "${profileName}"\` to the \`sanitize_text\` or \`sanitize_file\` tools. Advanced profiles require a PRO license.*`
|
|
404
|
+
}
|
|
405
|
+
]
|
|
406
|
+
};
|
|
407
|
+
}
|
|
408
|
+
throw new Error(`Resource not found: ${uri}`);
|
|
409
|
+
});
|
|
410
|
+
|
|
229
411
|
// Handle tool execution calls
|
|
230
412
|
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
231
413
|
const { name, arguments: args } = request.params;
|
|
@@ -256,10 +438,14 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
256
438
|
sessionRequestCount++;
|
|
257
439
|
|
|
258
440
|
if (isAdvanced && !license.isPro) {
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
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
|
+
}
|
|
263
449
|
}
|
|
264
450
|
|
|
265
451
|
const { processedText, wasTruncated } = truncateIfFree(text, license.isPro);
|
|
@@ -270,7 +456,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
270
456
|
if (!license.isPro) {
|
|
271
457
|
const detected = detectSecrets(processedText);
|
|
272
458
|
if (detected.length > 0) {
|
|
273
|
-
|
|
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`);
|
|
274
460
|
extraBlocks.push(buildUpsellBlock(`API Key / Secret (${detected.join(', ')}) detected! Sanitization skipped (Requires PRO Profile).`));
|
|
275
461
|
}
|
|
276
462
|
}
|
|
@@ -316,11 +502,15 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
316
502
|
}
|
|
317
503
|
|
|
318
504
|
const restored = PrivacyScrubberCore.unscrubText(text, sessionMap);
|
|
505
|
+
let restoredText = restored.restoredText;
|
|
506
|
+
// Deduplicate common double-prefixed schemas resulting from LLM prefix reconstruction
|
|
507
|
+
restoredText = restoredText.replace(/\b(mysql|postgresql|postgres|redis|mongodb|https?|ftp|ssh|git|aws):\/\/\1:\/\//gi, '$1://');
|
|
508
|
+
|
|
319
509
|
return {
|
|
320
510
|
content: [
|
|
321
511
|
{
|
|
322
512
|
type: "text",
|
|
323
|
-
text:
|
|
513
|
+
text: restoredText
|
|
324
514
|
}
|
|
325
515
|
]
|
|
326
516
|
};
|
|
@@ -398,7 +588,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
398
588
|
|
|
399
589
|
if (isAdvanced && !license.isPro) {
|
|
400
590
|
const reason = license.error ? ` (${license.error})` : "";
|
|
401
|
-
|
|
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`);
|
|
402
592
|
finalProfile = "General";
|
|
403
593
|
extraBlocks.push(buildUpsellBlock(`Profile '${targetProfile}' requires PRO. Using 'General' as fallback.`));
|
|
404
594
|
}
|
|
@@ -409,7 +599,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
409
599
|
if (!license.isPro) {
|
|
410
600
|
const detected = detectSecrets(processedContent);
|
|
411
601
|
if (detected.length > 0) {
|
|
412
|
-
|
|
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`);
|
|
413
603
|
extraBlocks.push(buildUpsellBlock(`API Key / Secret (${detected.join(', ')}) detected! Sanitization skipped (Requires PRO Profile).`));
|
|
414
604
|
}
|
|
415
605
|
}
|
|
@@ -468,7 +658,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
468
658
|
|
|
469
659
|
if (isAdvanced && !license.isPro) {
|
|
470
660
|
const reason = license.error ? ` (${license.error})` : "";
|
|
471
|
-
|
|
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`);
|
|
472
662
|
finalProfile = "General";
|
|
473
663
|
extraBlocks.push(buildUpsellBlock(`Profile '${targetProfile}' requires PRO. Using 'General' as fallback.`));
|
|
474
664
|
}
|
|
@@ -479,7 +669,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
479
669
|
if (!license.isPro) {
|
|
480
670
|
const detected = detectSecrets(processedContent);
|
|
481
671
|
if (detected.length > 0) {
|
|
482
|
-
|
|
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`);
|
|
483
673
|
extraBlocks.push(buildUpsellBlock(`API Key / Secret (${detected.join(', ')}) detected! Sanitization skipped (Requires PRO Profile).`));
|
|
484
674
|
}
|
|
485
675
|
}
|
|
@@ -525,7 +715,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
525
715
|
|
|
526
716
|
if (isAdvanced && !license.isPro) {
|
|
527
717
|
const reason = license.error ? ` (${license.error})` : "";
|
|
528
|
-
|
|
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`);
|
|
529
719
|
finalProfile = "General";
|
|
530
720
|
extraBlocks.push(buildUpsellBlock(`Profile '${targetProfile}' requires PRO. Using 'General' as fallback.`));
|
|
531
721
|
}
|
|
@@ -536,7 +726,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
536
726
|
if (!license.isPro) {
|
|
537
727
|
const detected = detectSecrets(processedContent);
|
|
538
728
|
if (detected.length > 0) {
|
|
539
|
-
|
|
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`);
|
|
540
730
|
extraBlocks.push(buildUpsellBlock(`API Key / Secret (${detected.join(', ')}) detected! Sanitization skipped (Requires PRO Profile).`));
|
|
541
731
|
}
|
|
542
732
|
}
|
|
@@ -596,7 +786,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
596
786
|
|
|
597
787
|
if (isAdvanced && !license.isPro) {
|
|
598
788
|
const reason = license.error ? ` (${license.error})` : "";
|
|
599
|
-
|
|
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`);
|
|
600
790
|
finalProfile = "General";
|
|
601
791
|
extraBlocks.push(buildUpsellBlock(`Profile '${targetProfile}' requires PRO. Using 'General' as fallback.`));
|
|
602
792
|
}
|
|
@@ -607,7 +797,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
607
797
|
if (!license.isPro) {
|
|
608
798
|
const detected = detectSecrets(processedContent2);
|
|
609
799
|
if (detected.length > 0) {
|
|
610
|
-
|
|
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`);
|
|
611
801
|
extraBlocks.push(buildUpsellBlock(`API Key / Secret (${detected.join(', ')}) detected! Sanitization skipped (Requires PRO Profile).`));
|
|
612
802
|
}
|
|
613
803
|
}
|
|
@@ -626,6 +816,84 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
626
816
|
};
|
|
627
817
|
}
|
|
628
818
|
|
|
819
|
+
if (name === "redact_file") {
|
|
820
|
+
const { file_path, profile, dry_run, no_backup } = args;
|
|
821
|
+
if (!file_path || !fs.existsSync(file_path)) {
|
|
822
|
+
return { isError: true, content: [{ type: "text", text: `Error: File not found at path: ${file_path}` }] };
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
try {
|
|
826
|
+
const stat = await fs.promises.stat(file_path);
|
|
827
|
+
if (stat.size > 50 * 1024 * 1024) {
|
|
828
|
+
return { isError: true, content: [{ type: "text", text: `Error: File is too large (>50MB).` }] };
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
const buffer = fs.readFileSync(file_path);
|
|
832
|
+
if (buffer.indexOf(0) !== -1) {
|
|
833
|
+
return { isError: true, content: [{ type: "text", text: `Error: Cannot redact binary file.` }] };
|
|
834
|
+
}
|
|
835
|
+
const content = buffer.toString('utf8');
|
|
836
|
+
|
|
837
|
+
const customRules = loadCustomRules();
|
|
838
|
+
const normalizedProfile = (profile || "general").trim().toLowerCase();
|
|
839
|
+
const license = checkLicenseStatus();
|
|
840
|
+
const localSessionMap = {};
|
|
841
|
+
|
|
842
|
+
const result = PrivacyScrubberCore.scrubText(content, customRules, {}, normalizedProfile, localSessionMap, license.isPro);
|
|
843
|
+
|
|
844
|
+
let isSecretLeak = false;
|
|
845
|
+
if (!license.isPro && result.tokenMap) {
|
|
846
|
+
Object.entries(result.tokenMap).forEach(([token, original]) => {
|
|
847
|
+
for (const detector of DEVOPS_SECRETS_DETECTOR) {
|
|
848
|
+
detector.regex.lastIndex = 0;
|
|
849
|
+
if (detector.regex.test(original)) {
|
|
850
|
+
isSecretLeak = true;
|
|
851
|
+
break;
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
});
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
const counts = {};
|
|
858
|
+
Object.keys(localSessionMap).forEach(token => {
|
|
859
|
+
const match = token.match(/^\[([A-Z_]+)_\d+\]$/);
|
|
860
|
+
if (match) {
|
|
861
|
+
counts[match[1]] = (counts[match[1]] || 0) + 1;
|
|
862
|
+
}
|
|
863
|
+
});
|
|
864
|
+
const summary = Object.entries(counts).map(([type, count]) => `${count} ${type}`).join(', ') || 'None';
|
|
865
|
+
|
|
866
|
+
if (dry_run) {
|
|
867
|
+
return {
|
|
868
|
+
content: [{
|
|
869
|
+
type: "text",
|
|
870
|
+
text: `DRY RUN: ⚠️ Would redact ${Object.keys(localSessionMap).length} tokens (${summary}).\n${isSecretLeak ? "⚠️ RAW SECRET DETECTED (Requires PRO to actually redact)\n" : ""}Run again with dry_run: false to apply changes.`
|
|
871
|
+
}]
|
|
872
|
+
};
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
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` }] };
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
if (!no_backup) {
|
|
880
|
+
fs.writeFileSync(`${file_path}.bak`, content, 'utf8');
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
fs.writeFileSync(file_path, result.scrubbedText, 'utf8');
|
|
884
|
+
|
|
885
|
+
return {
|
|
886
|
+
content: [{
|
|
887
|
+
type: "text",
|
|
888
|
+
text: `✅ File successfully redacted.\nReplaced ${Object.keys(localSessionMap).length} tokens (${summary}).\n${!no_backup ? `Backup saved to ${file_path}.bak` : "No backup created (no_backup=true)."}`
|
|
889
|
+
}]
|
|
890
|
+
};
|
|
891
|
+
|
|
892
|
+
} catch (err) {
|
|
893
|
+
return { isError: true, content: [{ type: "text", text: `Error redacting file: ${err.message}` }] };
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
|
|
629
897
|
if (name === "create_default_config") {
|
|
630
898
|
const configPath = path.resolve(process.cwd(), 'privacyscrubber.json');
|
|
631
899
|
if (fs.existsSync(configPath)) {
|
|
@@ -645,6 +913,19 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
645
913
|
"label": "CUSTOM_TAG",
|
|
646
914
|
"_comment": "pattern must be a valid Javascript regex string. label is the tag replacement (e.g. CUSTOM_TAG)"
|
|
647
915
|
}
|
|
916
|
+
],
|
|
917
|
+
"exclusions": [
|
|
918
|
+
"localhost",
|
|
919
|
+
"127.0.0.1",
|
|
920
|
+
"0.0.0.0",
|
|
921
|
+
"process.env",
|
|
922
|
+
"PORT",
|
|
923
|
+
"node_modules",
|
|
924
|
+
"PrivacyScrubber",
|
|
925
|
+
"console.log",
|
|
926
|
+
"console.error",
|
|
927
|
+
"utf-8",
|
|
928
|
+
"utf8"
|
|
648
929
|
]
|
|
649
930
|
};
|
|
650
931
|
|
|
@@ -695,7 +976,8 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
695
976
|
rulesStatus = '🔒 Ignored (requires PRO)';
|
|
696
977
|
}
|
|
697
978
|
} else {
|
|
698
|
-
rulesStatus = '
|
|
979
|
+
rulesStatus = 'None';
|
|
980
|
+
configPathSnippet = 'Run create_default_config';
|
|
699
981
|
}
|
|
700
982
|
|
|
701
983
|
const metricsSummary = getSessionMetricsSummary();
|
|
@@ -743,6 +1025,92 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
743
1025
|
};
|
|
744
1026
|
}
|
|
745
1027
|
|
|
1028
|
+
if (name === "audit_directory_for_pii") {
|
|
1029
|
+
const { directory_path, profile, extensions, ignore_node_modules } = args;
|
|
1030
|
+
if (!directory_path || !fs.existsSync(directory_path)) {
|
|
1031
|
+
return { isError: true, content: [{ type: "text", text: `Error: Directory not found at path: ${directory_path}` }] };
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
const filesToScan = [];
|
|
1035
|
+
const ignoreDirs = ignore_node_modules !== false ? ['.git', 'node_modules', '.next', 'dist', 'build', '.cache'] : ['.git'];
|
|
1036
|
+
|
|
1037
|
+
async function walkDir(dir) {
|
|
1038
|
+
const entries = await fs.promises.readdir(dir, { withFileTypes: true });
|
|
1039
|
+
for (const entry of entries) {
|
|
1040
|
+
const fullPath = path.join(dir, entry.name);
|
|
1041
|
+
if (entry.isDirectory()) {
|
|
1042
|
+
if (!ignoreDirs.includes(entry.name)) await walkDir(fullPath);
|
|
1043
|
+
} else if (entry.isFile()) {
|
|
1044
|
+
if (extensions && extensions.length > 0) {
|
|
1045
|
+
const ext = path.extname(entry.name);
|
|
1046
|
+
if (!extensions.includes(ext)) continue;
|
|
1047
|
+
}
|
|
1048
|
+
filesToScan.push(fullPath);
|
|
1049
|
+
}
|
|
1050
|
+
}
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
await walkDir(directory_path);
|
|
1054
|
+
|
|
1055
|
+
let reportLines = [`## Security Audit Report: ${directory_path}`];
|
|
1056
|
+
let filesWithIssues = 0;
|
|
1057
|
+
const customRules = loadCustomRules();
|
|
1058
|
+
const normalizedProfile = (profile || "general").trim().toLowerCase();
|
|
1059
|
+
const license = checkLicenseStatus();
|
|
1060
|
+
|
|
1061
|
+
for (const file of filesToScan) {
|
|
1062
|
+
try {
|
|
1063
|
+
const stat = await fs.promises.stat(file);
|
|
1064
|
+
if (stat.size > 10 * 1024 * 1024) continue; // skip >10MB
|
|
1065
|
+
|
|
1066
|
+
const buffer = fs.readFileSync(file);
|
|
1067
|
+
// basic binary check
|
|
1068
|
+
if (buffer.indexOf(0) !== -1) continue;
|
|
1069
|
+
const content = buffer.toString('utf8');
|
|
1070
|
+
|
|
1071
|
+
const localSessionMap = {};
|
|
1072
|
+
const result = PrivacyScrubberCore.scrubText(content, customRules, {}, normalizedProfile, localSessionMap, license.isPro);
|
|
1073
|
+
|
|
1074
|
+
let isSecretLeak = false;
|
|
1075
|
+
if (!license.isPro && result.tokenMap) {
|
|
1076
|
+
Object.entries(result.tokenMap).forEach(([token, original]) => {
|
|
1077
|
+
for (const detector of DEVOPS_SECRETS_DETECTOR) {
|
|
1078
|
+
detector.regex.lastIndex = 0;
|
|
1079
|
+
if (detector.regex.test(original)) {
|
|
1080
|
+
isSecretLeak = true;
|
|
1081
|
+
break;
|
|
1082
|
+
}
|
|
1083
|
+
}
|
|
1084
|
+
});
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
if (Object.keys(localSessionMap).length > 0 || isSecretLeak) {
|
|
1088
|
+
filesWithIssues++;
|
|
1089
|
+
const counts = {};
|
|
1090
|
+
Object.keys(localSessionMap).forEach(token => {
|
|
1091
|
+
const match = token.match(/^\[([A-Z_]+)_\d+\]$/);
|
|
1092
|
+
if (match) {
|
|
1093
|
+
counts[match[1]] = (counts[match[1]] || 0) + 1;
|
|
1094
|
+
}
|
|
1095
|
+
});
|
|
1096
|
+
const summary = Object.entries(counts).map(([type, count]) => `${count} ${type}`).join(', ');
|
|
1097
|
+
let leakMsg = isSecretLeak ? " (⚠️ RAW SECRET DETECTED — REQUIRES PRO)" : "";
|
|
1098
|
+
reportLines.push(`- \`${path.relative(directory_path, file)}\`: ⚠️ Found ${summary}${leakMsg}`);
|
|
1099
|
+
}
|
|
1100
|
+
} catch (err) {
|
|
1101
|
+
// ignore file read errors
|
|
1102
|
+
}
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
if (filesWithIssues === 0) {
|
|
1106
|
+
reportLines.push("✅ Clean. No PII or secrets detected in the scanned files.");
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
return {
|
|
1110
|
+
content: [{ type: "text", text: reportLines.join('\n') }]
|
|
1111
|
+
};
|
|
1112
|
+
}
|
|
1113
|
+
|
|
746
1114
|
return {
|
|
747
1115
|
isError: true,
|
|
748
1116
|
content: [{ type: "text", text: `Unknown tool: ${name}` }]
|
|
@@ -813,11 +1181,11 @@ function loadCustomRules() {
|
|
|
813
1181
|
category: (r.label || r.category || "CUSTOM").toUpperCase()
|
|
814
1182
|
})).filter(r => r.pattern);
|
|
815
1183
|
} else {
|
|
816
|
-
|
|
1184
|
+
mcpLog(`${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`);
|
|
817
1185
|
}
|
|
818
1186
|
}
|
|
819
1187
|
} catch (e) {
|
|
820
|
-
|
|
1188
|
+
mcpLog(`Error parsing privacyscrubber.json: ${e.message}\n`);
|
|
821
1189
|
}
|
|
822
1190
|
}
|
|
823
1191
|
return [];
|
|
@@ -838,9 +1206,9 @@ function detectSecrets(text) {
|
|
|
838
1206
|
|
|
839
1207
|
function performSanitization(text, profile) {
|
|
840
1208
|
const customRules = loadCustomRules();
|
|
841
|
-
const
|
|
842
|
-
|
|
1209
|
+
const normalizedProfile = (profile || "general").trim().toLowerCase();
|
|
843
1210
|
const license = checkLicenseStatus();
|
|
1211
|
+
const result = PrivacyScrubberCore.scrubText(text, customRules, {}, normalizedProfile, sessionMap, license.isPro);
|
|
844
1212
|
if (!license.isPro && result.tokenMap) {
|
|
845
1213
|
// Re-scan each masked token's original value against the shared detector.
|
|
846
1214
|
// If it was a raw secret, restore it in the output and remove from sessionMap
|
|
@@ -872,9 +1240,9 @@ function performSanitization(text, profile) {
|
|
|
872
1240
|
}
|
|
873
1241
|
|
|
874
1242
|
function truncateIfFree(text, isPro) {
|
|
875
|
-
const MAX_FREE_CHARS =
|
|
1243
|
+
const MAX_FREE_CHARS = 5000;
|
|
876
1244
|
if (!isPro && text.length > MAX_FREE_CHARS) {
|
|
877
|
-
|
|
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`);
|
|
878
1246
|
return { processedText: text.substring(0, MAX_FREE_CHARS), wasTruncated: true };
|
|
879
1247
|
}
|
|
880
1248
|
return { processedText: text, wasTruncated: false };
|
|
@@ -883,8 +1251,8 @@ function truncateIfFree(text, isPro) {
|
|
|
883
1251
|
// Start the server transport
|
|
884
1252
|
const transport = new StdioServerTransport();
|
|
885
1253
|
server.connect(transport).then(() => {
|
|
886
|
-
|
|
887
|
-
|
|
1254
|
+
mcpLog(`${colors.greenBold}✅ PrivacyScrubber ZTDS MCP Server started successfully.${colors.reset}\n`);
|
|
1255
|
+
mcpLog(`${colors.cyan}⭐ Star us on GitHub: https://github.com/moxno/privacyscrubber-mcp${colors.reset}\n`);
|
|
888
1256
|
}).catch((error) => {
|
|
889
1257
|
console.error("Failed to connect MCP server transport:", error);
|
|
890
1258
|
process.exit(1);
|
package/manifest.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"manifest_version": "0.3",
|
|
3
3
|
"name": "privacyscrubber/pii-masking-mcp",
|
|
4
|
-
"version": "1.
|
|
4
|
+
"version": "1.7.1",
|
|
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
|
"author": {
|
|
7
7
|
"name": "PrivacyScrubber",
|