@privacyscrubber/mcp-server 1.7.0 → 1.7.2
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 +6 -5
- package/index.js +430 -145
- package/install.js +63 -0
- package/manifest.json +1 -1
- package/package.json +5 -3
- package/pii-masking-run.js +2 -2
- 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/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
|
|
@@ -12,6 +13,10 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
|
|
|
12
13
|
import {
|
|
13
14
|
CallToolRequestSchema,
|
|
14
15
|
ListToolsRequestSchema,
|
|
16
|
+
ListPromptsRequestSchema,
|
|
17
|
+
GetPromptRequestSchema,
|
|
18
|
+
ListResourceTemplatesRequestSchema,
|
|
19
|
+
ReadResourceRequestSchema,
|
|
15
20
|
} from "@modelcontextprotocol/sdk/types.js";
|
|
16
21
|
import { createRequire } from 'module';
|
|
17
22
|
import fs from 'fs';
|
|
@@ -32,6 +37,7 @@ const MCP_VERSION = require('./package.json').version;
|
|
|
32
37
|
// Import the production core engine with 100% parity
|
|
33
38
|
const scrubberCorePath = path.resolve(__dirname, './scrubber-core.cjs');
|
|
34
39
|
const PrivacyScrubberCore = require(scrubberCorePath);
|
|
40
|
+
const LicenseManager = require('./ps-license-manager.js');
|
|
35
41
|
|
|
36
42
|
// Initialize core engine
|
|
37
43
|
PrivacyScrubberCore.init();
|
|
@@ -49,6 +55,18 @@ const colors = {
|
|
|
49
55
|
reset: '\x1b[0m'
|
|
50
56
|
};
|
|
51
57
|
|
|
58
|
+
// Helper to log to both standard terminal (with colors) and MCP logging protocol
|
|
59
|
+
function mcpLog(msg) {
|
|
60
|
+
const clean = msg.replace(/\x1b\[[0-9;]*m/g, '').trim();
|
|
61
|
+
const level = clean.includes('⚠️') || clean.includes('Error') ? "warning" : "info";
|
|
62
|
+
try {
|
|
63
|
+
server.sendLoggingMessage({ level, data: clean });
|
|
64
|
+
} catch(e) {
|
|
65
|
+
// Ignore if server isn't fully connected yet
|
|
66
|
+
}
|
|
67
|
+
process.stderr.write(msg);
|
|
68
|
+
}
|
|
69
|
+
|
|
52
70
|
// Secrets detection patterns — defined once here, shared by detectSecrets() and performSanitization()
|
|
53
71
|
const DEVOPS_SECRETS_DETECTOR = [
|
|
54
72
|
{ name: 'AWS Credentials', regex: /\b(?:AKIA|ASIA|AGPA|AIDA|AROA|AIPA)[A-Z0-9]{16}\b/g },
|
|
@@ -58,83 +76,31 @@ const DEVOPS_SECRETS_DETECTOR = [
|
|
|
58
76
|
{ 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
77
|
];
|
|
60
78
|
|
|
61
|
-
// Hardcoded public key for offline cryptographic verification
|
|
62
|
-
const PUBLIC_KEY = `-----BEGIN PUBLIC KEY-----
|
|
63
|
-
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAw3f37srO402PU4++Baf8
|
|
64
|
-
FG8LY4l/IA3NKLlBnYmNHRTjfI/O/w5PDZn1xPcUQevojA1J+A5moKcjXsJ5b21X
|
|
65
|
-
hJoYSkE4vLpcVYOt1FhRwEHs1APDSyss0HixboLz2eW2XQf2NbwajWtNlyxvgczO
|
|
66
|
-
KE6ClnLomtsaKywwqB4alzdYnnnFJttFPjwmgPSO7D9AgN9sYaVkXOaOFrIZ90Ng
|
|
67
|
-
TRhSHUeL7ReltWlCHwz9xf5m2FrKtxr2VBlEoyPjsFzalHMey1EX+yXe81zM7IIi
|
|
68
|
-
t1Z8agLzo7WIfNBAIWmRlerTplaFFZrQgdF5g/Y0n8IIMZOtadgoY8E855psDNZV
|
|
69
|
-
7wIDAQAB
|
|
70
|
-
-----END PUBLIC KEY-----`;
|
|
71
|
-
|
|
72
79
|
function checkLicenseStatus() {
|
|
73
80
|
const key = (process.env.PRIVACYSCRUBBER_KEY || "").trim();
|
|
74
81
|
if (!key) return { isPro: false, type: null, error: "No license key provided." };
|
|
75
82
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
const verifier = crypto.createVerify('SHA256');
|
|
83
|
-
verifier.update(payloadBase64);
|
|
84
|
-
const isVerified = verifier.verify(PUBLIC_KEY, signatureBase64, 'base64');
|
|
85
|
-
|
|
86
|
-
if (!isVerified) {
|
|
87
|
-
return { isPro: false, type: null, error: "Signature verification failed." };
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
const payload = JSON.parse(Buffer.from(payloadBase64, 'base64').toString('utf8'));
|
|
91
|
-
|
|
92
|
-
// Expiration check
|
|
93
|
-
if (payload.expires && payload.expires < Math.floor(Date.now() / 1000)) {
|
|
94
|
-
return {
|
|
95
|
-
isPro: false,
|
|
96
|
-
type: payload.type,
|
|
97
|
-
error: `License expired on ${new Date(payload.expires * 1000).toLocaleDateString()}`
|
|
98
|
-
};
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
return { isPro: true, type: payload.type, error: null };
|
|
102
|
-
} catch (e) {
|
|
103
|
-
return { isPro: false, type: null, error: "Error parsing license: " + e.message };
|
|
83
|
+
const result = LicenseManager.validate(key);
|
|
84
|
+
|
|
85
|
+
if (!result.valid) {
|
|
86
|
+
return { isPro: false, type: null, error: result.reason || "Invalid license format or signature." };
|
|
104
87
|
}
|
|
88
|
+
|
|
89
|
+
return { isPro: true, type: result.tier, error: null };
|
|
105
90
|
}
|
|
106
91
|
|
|
107
92
|
// ── Session request counter (volatile in-memory) ─────────────────────────────
|
|
108
93
|
let sessionRequestCount = 0;
|
|
109
94
|
const UPSELL_EVERY_N = 10; // show upsell nudge every N free-tier requests
|
|
110
95
|
|
|
111
|
-
// Build a clean upsell content block
|
|
96
|
+
// Build a clean upsell content block (Disabled to prevent agent-visible marketing content)
|
|
112
97
|
function buildUpsellBlock(reason) {
|
|
113
|
-
|
|
114
|
-
let lines = [
|
|
115
|
-
'─────────────────────────────────────────────',
|
|
116
|
-
'⚡ PrivacyScrubber PRO — Upgrade your AI setup',
|
|
117
|
-
'─────────────────────────────────────────────',
|
|
118
|
-
];
|
|
119
|
-
if (reason) lines.push(`ℹ️ ${reason}`);
|
|
120
|
-
lines.push(
|
|
121
|
-
`📊 Free session requests: ${count}`,
|
|
122
|
-
'🔒 PRO unlocks: 22 industry profiles (Dev, Medical, Legal…)',
|
|
123
|
-
' + Custom regex rules (privacyscrubber.json)',
|
|
124
|
-
' + Unlimited input size',
|
|
125
|
-
'',
|
|
126
|
-
'💳 PRO Lifetime: $110 → https://privacyscrubber.com/pricing',
|
|
127
|
-
'',
|
|
128
|
-
'After purchase, add your key to your MCP config:',
|
|
129
|
-
' "PRIVACYSCRUBBER_KEY": "<your-key-here>"',
|
|
130
|
-
'─────────────────────────────────────────────'
|
|
131
|
-
);
|
|
132
|
-
return { type: 'text', text: lines.join('\n') };
|
|
98
|
+
return null;
|
|
133
99
|
}
|
|
134
100
|
|
|
135
|
-
// Decide if we should attach a soft periodic nudge (
|
|
101
|
+
// Decide if we should attach a soft periodic nudge (Disabled to maintain clean MCP output)
|
|
136
102
|
function shouldNudge(isPro) {
|
|
137
|
-
return
|
|
103
|
+
return false;
|
|
138
104
|
}
|
|
139
105
|
|
|
140
106
|
// Create the MCP server
|
|
@@ -146,6 +112,9 @@ const server = new Server(
|
|
|
146
112
|
{
|
|
147
113
|
capabilities: {
|
|
148
114
|
tools: {},
|
|
115
|
+
prompts: {},
|
|
116
|
+
logging: {},
|
|
117
|
+
resources: {},
|
|
149
118
|
},
|
|
150
119
|
}
|
|
151
120
|
);
|
|
@@ -156,7 +125,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
156
125
|
tools: [
|
|
157
126
|
{
|
|
158
127
|
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.",
|
|
128
|
+
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
129
|
inputSchema: {
|
|
161
130
|
type: "object",
|
|
162
131
|
properties: {
|
|
@@ -174,7 +143,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
174
143
|
},
|
|
175
144
|
{
|
|
176
145
|
name: "reveal_text",
|
|
177
|
-
description: "
|
|
146
|
+
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
147
|
inputSchema: {
|
|
179
148
|
type: "object",
|
|
180
149
|
properties: {
|
|
@@ -204,6 +173,59 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
204
173
|
required: ["file_path"]
|
|
205
174
|
}
|
|
206
175
|
},
|
|
176
|
+
{
|
|
177
|
+
name: "audit_directory_for_pii",
|
|
178
|
+
description: "Scans a local directory for leaks of secrets, keys, and PII. Returns a summary report. Use this tool for Security Auditing.",
|
|
179
|
+
inputSchema: {
|
|
180
|
+
type: "object",
|
|
181
|
+
properties: {
|
|
182
|
+
directory_path: {
|
|
183
|
+
type: "string",
|
|
184
|
+
description: "Absolute path to the directory to audit."
|
|
185
|
+
},
|
|
186
|
+
profile: {
|
|
187
|
+
type: "string",
|
|
188
|
+
description: "The detection profile to use. Defaults to 'General'."
|
|
189
|
+
},
|
|
190
|
+
extensions: {
|
|
191
|
+
type: "array",
|
|
192
|
+
items: { type: "string" },
|
|
193
|
+
description: "List of file extensions to scan (e.g. ['.env', '.log', '.js']). If empty, scans all text files."
|
|
194
|
+
},
|
|
195
|
+
ignore_node_modules: {
|
|
196
|
+
type: "boolean",
|
|
197
|
+
description: "Whether to ignore 'node_modules' directories. Defaults to true."
|
|
198
|
+
}
|
|
199
|
+
},
|
|
200
|
+
required: ["directory_path"]
|
|
201
|
+
}
|
|
202
|
+
},
|
|
203
|
+
{
|
|
204
|
+
name: "redact_file",
|
|
205
|
+
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.",
|
|
206
|
+
inputSchema: {
|
|
207
|
+
type: "object",
|
|
208
|
+
properties: {
|
|
209
|
+
file_path: {
|
|
210
|
+
type: "string",
|
|
211
|
+
description: "Absolute path to the file to redact."
|
|
212
|
+
},
|
|
213
|
+
profile: {
|
|
214
|
+
type: "string",
|
|
215
|
+
description: "The detection profile to use. Defaults to 'General'."
|
|
216
|
+
},
|
|
217
|
+
dry_run: {
|
|
218
|
+
type: "boolean",
|
|
219
|
+
description: "If true, does not modify the file, only returns the metrics and a preview."
|
|
220
|
+
},
|
|
221
|
+
no_backup: {
|
|
222
|
+
type: "boolean",
|
|
223
|
+
description: "If true, does not create a .bak file. Use with extreme caution!"
|
|
224
|
+
}
|
|
225
|
+
},
|
|
226
|
+
required: ["file_path"]
|
|
227
|
+
}
|
|
228
|
+
},
|
|
207
229
|
{
|
|
208
230
|
name: "create_default_config",
|
|
209
231
|
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 +248,116 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
226
248
|
};
|
|
227
249
|
});
|
|
228
250
|
|
|
251
|
+
// List available prompts
|
|
252
|
+
server.setRequestHandler(ListPromptsRequestSchema, async () => {
|
|
253
|
+
return {
|
|
254
|
+
prompts: [
|
|
255
|
+
{
|
|
256
|
+
name: "secure_ai_workflow",
|
|
257
|
+
description: "Zero-Trust Data Sanitization workflow instruction. Use this to automatically scrub PII before processing data.",
|
|
258
|
+
},
|
|
259
|
+
{
|
|
260
|
+
name: "safe_redaction_workflow",
|
|
261
|
+
description: "Instructions for using the redact_file tool safely with QA checks.",
|
|
262
|
+
}
|
|
263
|
+
]
|
|
264
|
+
};
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
// Handle prompt execution
|
|
268
|
+
server.setRequestHandler(GetPromptRequestSchema, async (request) => {
|
|
269
|
+
if (request.params.name === "safe_redaction_workflow") {
|
|
270
|
+
return {
|
|
271
|
+
description: "Instructions for safely redacting files in-place.",
|
|
272
|
+
messages: [
|
|
273
|
+
{
|
|
274
|
+
role: "user",
|
|
275
|
+
content: {
|
|
276
|
+
type: "text",
|
|
277
|
+
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."
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
]
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
if (request.params.name !== "secure_ai_workflow") {
|
|
285
|
+
throw new Error(`Unknown prompt: ${request.params.name}`);
|
|
286
|
+
}
|
|
287
|
+
return {
|
|
288
|
+
description: "Instructions for securely processing data with PrivacyScrubber.",
|
|
289
|
+
messages: [
|
|
290
|
+
{
|
|
291
|
+
role: "user",
|
|
292
|
+
content: {
|
|
293
|
+
type: "text",
|
|
294
|
+
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."
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
]
|
|
298
|
+
};
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
// List Resource Templates
|
|
302
|
+
server.setRequestHandler(ListResourceTemplatesRequestSchema, async () => {
|
|
303
|
+
return {
|
|
304
|
+
resourceTemplates: [
|
|
305
|
+
{
|
|
306
|
+
uriTemplate: "mcp://privacyscrubber/profiles/{name}",
|
|
307
|
+
name: "PII Detection Profile Details",
|
|
308
|
+
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."
|
|
309
|
+
}
|
|
310
|
+
]
|
|
311
|
+
};
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
// Read Resource
|
|
315
|
+
server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
|
|
316
|
+
const uri = request.params.uri;
|
|
317
|
+
const match = uri.match(/^mcp:\/\/privacyscrubber\/profiles\/(.+)$/);
|
|
318
|
+
if (match) {
|
|
319
|
+
const profileName = match[1];
|
|
320
|
+
const descriptions = {
|
|
321
|
+
"general": "Scrubs basic PII: Names, Emails, Phone Numbers, Credit Cards, SSN, IPv4/IPv6, and standard API Keys.",
|
|
322
|
+
"dev": "Engineering profile. Scrubs AWS keys, JWTs, GCP keys, Stripe keys, database connection strings, and internal IP addresses.",
|
|
323
|
+
"engineering": "Engineering profile. Scrubs AWS keys, JWTs, GCP keys, Stripe keys, database connection strings, internal IPs, Docker paths, and K8s internal endpoints.",
|
|
324
|
+
"tech": "Technology profile. Scrubs cloud instance IDs, node/cluster IDs, environment tags, kubeconfig, and terraform states.",
|
|
325
|
+
"medical": "HIPAA compliance profile. Scrubs ICD-10 codes, medical record numbers (MRN), DEA numbers, NPI, patient names, and health conditions.",
|
|
326
|
+
"finance": "PCI/Finance profile. Scrubs IBAN, SWIFT codes, credit cards, routing numbers, and financial transaction IDs.",
|
|
327
|
+
"wealthmgmt": "Wealth Management profile. Scrubs ABA/Routing numbers, trust names, portfolio values, net worth, RMDs, and account numbers.",
|
|
328
|
+
"insurance": "Insurance profile. Scrubs claim numbers, policy numbers, settlement amounts, NAIC, adjustor IDs, and insured names.",
|
|
329
|
+
"accounting": "Accounting profile. Scrubs EIN, FEIN, Tax IDs, AGI, tax forms (1040, W-2, 1099), refund amounts, and PTINs.",
|
|
330
|
+
"legal": "Legal profile. Scrubs case numbers, matter numbers, attorney-client privilege markers, and litigation IDs.",
|
|
331
|
+
"hr": "Human Resources profile. Scrubs employee IDs (EEID), resumes, DOBs, tenant IDs, and home addresses.",
|
|
332
|
+
"security": "Security profile. Scrubs secrets, AWS keys, JWTs, Stripe keys, incident IDs, and breach reports.",
|
|
333
|
+
"marketing": "Marketing profile. Scrubs lead IDs, prospect IDs, GCLID/FBCLID, LTV/CAC amounts, cohort IDs, and segment IDs.",
|
|
334
|
+
"bizops": "BizOps profile. Scrubs deals, KPIs, vendor IDs, EBITDA/Revenue figures, NDAs, and M&A identifiers.",
|
|
335
|
+
"sales": "Sales profile. Scrubs opportunity IDs, DocuSign hashes, ARR/MRR/Quota amounts, and SFDC/HubSpot IDs.",
|
|
336
|
+
"support": "Support profile. Scrubs Zendesk/Jira ticket numbers, RMA/Return IDs, and loyalty/rewards numbers.",
|
|
337
|
+
"realestate": "Real Estate profile. Scrubs MLS numbers, LIS IDs, parcel numbers, rent/escrow amounts, and gate/lobby codes.",
|
|
338
|
+
"compliance": "Compliance profile. Scrubs GDPR/HIPAA/SOC2 audit IDs, DPA policy numbers, and SAR/DSAR request IDs.",
|
|
339
|
+
"ccpa": "CCPA profile. Scrubs driver licenses, precise geolocation (Lat/Long), CCPA/CPRA opt-out markers, and account numbers.",
|
|
340
|
+
"agents": "AI Agents profile. Scrubs agent IDs, vector IDs, task IDs, system prompts, and OpenAI API keys.",
|
|
341
|
+
"academic": "Academic profile. Scrubs student/alumni IDs, course numbers, FERPA/IRB IDs, and academic grades.",
|
|
342
|
+
"creative": "Creative profile. Scrubs project IDs, script drafts, spoiler/embargo tags, and ghostwriter names.",
|
|
343
|
+
"personal": "Personal profile. Scrubs birthdays, passwords, PINs, and emergency contacts/family phone numbers.",
|
|
344
|
+
"pharma": "Pharma/Clinical profile. Scrubs patient IDs, study protocols, IND/NDA numbers, IRB IDs, batch/lot serials, and dosages."
|
|
345
|
+
};
|
|
346
|
+
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.`;
|
|
347
|
+
|
|
348
|
+
return {
|
|
349
|
+
contents: [
|
|
350
|
+
{
|
|
351
|
+
uri,
|
|
352
|
+
mimeType: "text/markdown",
|
|
353
|
+
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.*`
|
|
354
|
+
}
|
|
355
|
+
]
|
|
356
|
+
};
|
|
357
|
+
}
|
|
358
|
+
throw new Error(`Resource not found: ${uri}`);
|
|
359
|
+
});
|
|
360
|
+
|
|
229
361
|
// Handle tool execution calls
|
|
230
362
|
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
231
363
|
const { name, arguments: args } = request.params;
|
|
@@ -255,23 +387,25 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
255
387
|
|
|
256
388
|
sessionRequestCount++;
|
|
257
389
|
|
|
390
|
+
const charLimit = (isAdvanced && !license.isPro) ? 5000 : 15000;
|
|
258
391
|
if (isAdvanced && !license.isPro) {
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
finalProfile = "General";
|
|
262
|
-
extraBlocks.push(buildUpsellBlock(`Profile '${targetProfile}' requires PRO. Using 'General' as fallback.`));
|
|
392
|
+
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`);
|
|
393
|
+
extraBlocks.push(buildUpsellBlock(`Profile '${targetProfile}' active (Free Tier limit: 5,000 chars). Upgrade to PRO for unlimited text.`));
|
|
263
394
|
}
|
|
264
395
|
|
|
265
|
-
const { processedText, wasTruncated } = truncateIfFree(text, license.isPro);
|
|
396
|
+
const { processedText, wasTruncated } = truncateIfFree(text, license.isPro, charLimit);
|
|
266
397
|
if (wasTruncated) {
|
|
267
|
-
extraBlocks.push(buildUpsellBlock(`Input was truncated to
|
|
398
|
+
extraBlocks.push(buildUpsellBlock(`Input was truncated to ${charLimit.toLocaleString()} characters (Free Tier limit).`));
|
|
268
399
|
}
|
|
269
400
|
|
|
270
401
|
if (!license.isPro) {
|
|
271
402
|
const detected = detectSecrets(processedText);
|
|
272
403
|
if (detected.length > 0) {
|
|
273
|
-
|
|
274
|
-
|
|
404
|
+
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`);
|
|
405
|
+
return {
|
|
406
|
+
isError: true,
|
|
407
|
+
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` }]
|
|
408
|
+
};
|
|
275
409
|
}
|
|
276
410
|
}
|
|
277
411
|
|
|
@@ -285,7 +419,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
285
419
|
return {
|
|
286
420
|
content: [
|
|
287
421
|
{ type: "text", text: sanitized },
|
|
288
|
-
...extraBlocks
|
|
422
|
+
...extraBlocks.filter(Boolean)
|
|
289
423
|
]
|
|
290
424
|
};
|
|
291
425
|
}
|
|
@@ -400,21 +534,23 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
400
534
|
|
|
401
535
|
sessionRequestCount++;
|
|
402
536
|
|
|
537
|
+
const charLimit = (isAdvanced && !license.isPro) ? 5000 : 15000;
|
|
403
538
|
if (isAdvanced && !license.isPro) {
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
finalProfile = "General";
|
|
407
|
-
extraBlocks.push(buildUpsellBlock(`Profile '${targetProfile}' requires PRO. Using 'General' as fallback.`));
|
|
539
|
+
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`);
|
|
540
|
+
extraBlocks.push(buildUpsellBlock(`Profile '${targetProfile}' active (Free Tier limit: 5,000 chars). Upgrade to PRO for unlimited text.`));
|
|
408
541
|
}
|
|
409
542
|
|
|
410
|
-
const { processedText: processedContent, wasTruncated } = truncateIfFree(content, license.isPro);
|
|
411
|
-
if (wasTruncated) extraBlocks.push(buildUpsellBlock(`File content was truncated to
|
|
543
|
+
const { processedText: processedContent, wasTruncated } = truncateIfFree(content, license.isPro, charLimit);
|
|
544
|
+
if (wasTruncated) extraBlocks.push(buildUpsellBlock(`File content was truncated to ${charLimit.toLocaleString()} characters (Free Tier limit).`));
|
|
412
545
|
|
|
413
546
|
if (!license.isPro) {
|
|
414
547
|
const detected = detectSecrets(processedContent);
|
|
415
548
|
if (detected.length > 0) {
|
|
416
|
-
|
|
417
|
-
|
|
549
|
+
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`);
|
|
550
|
+
return {
|
|
551
|
+
isError: true,
|
|
552
|
+
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` }]
|
|
553
|
+
};
|
|
418
554
|
}
|
|
419
555
|
}
|
|
420
556
|
|
|
@@ -427,7 +563,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
427
563
|
return {
|
|
428
564
|
content: [
|
|
429
565
|
{ type: "text", text: sanitized },
|
|
430
|
-
...extraBlocks
|
|
566
|
+
...extraBlocks.filter(Boolean)
|
|
431
567
|
]
|
|
432
568
|
};
|
|
433
569
|
} catch (docxError) {
|
|
@@ -451,7 +587,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
451
587
|
content: [
|
|
452
588
|
{
|
|
453
589
|
type: "text",
|
|
454
|
-
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`
|
|
590
|
+
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`
|
|
455
591
|
}
|
|
456
592
|
]
|
|
457
593
|
};
|
|
@@ -470,21 +606,23 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
470
606
|
|
|
471
607
|
sessionRequestCount++;
|
|
472
608
|
|
|
609
|
+
const charLimit = (isAdvanced && !license.isPro) ? 5000 : 15000;
|
|
473
610
|
if (isAdvanced && !license.isPro) {
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
finalProfile = "General";
|
|
477
|
-
extraBlocks.push(buildUpsellBlock(`Profile '${targetProfile}' requires PRO. Using 'General' as fallback.`));
|
|
611
|
+
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`);
|
|
612
|
+
extraBlocks.push(buildUpsellBlock(`Profile '${targetProfile}' active (Free Tier limit: 5,000 chars). Upgrade to PRO for unlimited text.`));
|
|
478
613
|
}
|
|
479
614
|
|
|
480
|
-
const { processedText: processedContent, wasTruncated } = truncateIfFree(content, license.isPro);
|
|
481
|
-
if (wasTruncated) extraBlocks.push(buildUpsellBlock(`File content was truncated to
|
|
615
|
+
const { processedText: processedContent, wasTruncated } = truncateIfFree(content, license.isPro, charLimit);
|
|
616
|
+
if (wasTruncated) extraBlocks.push(buildUpsellBlock(`File content was truncated to ${charLimit.toLocaleString()} characters (Free Tier limit).`));
|
|
482
617
|
|
|
483
618
|
if (!license.isPro) {
|
|
484
619
|
const detected = detectSecrets(processedContent);
|
|
485
620
|
if (detected.length > 0) {
|
|
486
|
-
|
|
487
|
-
|
|
621
|
+
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`);
|
|
622
|
+
return {
|
|
623
|
+
isError: true,
|
|
624
|
+
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` }]
|
|
625
|
+
};
|
|
488
626
|
}
|
|
489
627
|
}
|
|
490
628
|
|
|
@@ -497,7 +635,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
497
635
|
return {
|
|
498
636
|
content: [
|
|
499
637
|
{ type: "text", text: sanitized },
|
|
500
|
-
...extraBlocks
|
|
638
|
+
...extraBlocks.filter(Boolean)
|
|
501
639
|
]
|
|
502
640
|
};
|
|
503
641
|
} catch (pdfError) {
|
|
@@ -527,21 +665,23 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
527
665
|
|
|
528
666
|
sessionRequestCount++;
|
|
529
667
|
|
|
668
|
+
const charLimit = (isAdvanced && !license.isPro) ? 5000 : 15000;
|
|
530
669
|
if (isAdvanced && !license.isPro) {
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
finalProfile = "General";
|
|
534
|
-
extraBlocks.push(buildUpsellBlock(`Profile '${targetProfile}' requires PRO. Using 'General' as fallback.`));
|
|
670
|
+
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`);
|
|
671
|
+
extraBlocks.push(buildUpsellBlock(`Profile '${targetProfile}' active (Free Tier limit: 5,000 chars). Upgrade to PRO for unlimited text.`));
|
|
535
672
|
}
|
|
536
673
|
|
|
537
|
-
const { processedText: processedContent, wasTruncated } = truncateIfFree(content, license.isPro);
|
|
538
|
-
if (wasTruncated) extraBlocks.push(buildUpsellBlock(`File content was truncated to
|
|
674
|
+
const { processedText: processedContent, wasTruncated } = truncateIfFree(content, license.isPro, charLimit);
|
|
675
|
+
if (wasTruncated) extraBlocks.push(buildUpsellBlock(`File content was truncated to ${charLimit.toLocaleString()} characters (Free Tier limit).`));
|
|
539
676
|
|
|
540
677
|
if (!license.isPro) {
|
|
541
678
|
const detected = detectSecrets(processedContent);
|
|
542
679
|
if (detected.length > 0) {
|
|
543
|
-
|
|
544
|
-
|
|
680
|
+
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`);
|
|
681
|
+
return {
|
|
682
|
+
isError: true,
|
|
683
|
+
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` }]
|
|
684
|
+
};
|
|
545
685
|
}
|
|
546
686
|
}
|
|
547
687
|
|
|
@@ -554,7 +694,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
554
694
|
return {
|
|
555
695
|
content: [
|
|
556
696
|
{ type: "text", text: sanitized },
|
|
557
|
-
...extraBlocks
|
|
697
|
+
...extraBlocks.filter(Boolean)
|
|
558
698
|
]
|
|
559
699
|
};
|
|
560
700
|
} catch (xlsxError) {
|
|
@@ -598,21 +738,23 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
598
738
|
|
|
599
739
|
sessionRequestCount++;
|
|
600
740
|
|
|
741
|
+
const charLimit = (isAdvanced && !license.isPro) ? 5000 : 15000;
|
|
601
742
|
if (isAdvanced && !license.isPro) {
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
finalProfile = "General";
|
|
605
|
-
extraBlocks.push(buildUpsellBlock(`Profile '${targetProfile}' requires PRO. Using 'General' as fallback.`));
|
|
743
|
+
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`);
|
|
744
|
+
extraBlocks.push(buildUpsellBlock(`Profile '${targetProfile}' active (Free Tier limit: 5,000 chars). Upgrade to PRO for unlimited text.`));
|
|
606
745
|
}
|
|
607
746
|
|
|
608
|
-
const { processedText: processedContent2, wasTruncated: wasTruncated2 } = truncateIfFree(content, license.isPro);
|
|
609
|
-
if (wasTruncated2) extraBlocks.push(buildUpsellBlock(`File content was truncated to
|
|
747
|
+
const { processedText: processedContent2, wasTruncated: wasTruncated2 } = truncateIfFree(content, license.isPro, charLimit);
|
|
748
|
+
if (wasTruncated2) extraBlocks.push(buildUpsellBlock(`File content was truncated to ${charLimit.toLocaleString()} characters (Free Tier limit).`));
|
|
610
749
|
|
|
611
750
|
if (!license.isPro) {
|
|
612
751
|
const detected = detectSecrets(processedContent2);
|
|
613
752
|
if (detected.length > 0) {
|
|
614
|
-
|
|
615
|
-
|
|
753
|
+
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`);
|
|
754
|
+
return {
|
|
755
|
+
isError: true,
|
|
756
|
+
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` }]
|
|
757
|
+
};
|
|
616
758
|
}
|
|
617
759
|
}
|
|
618
760
|
|
|
@@ -625,11 +767,89 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
625
767
|
return {
|
|
626
768
|
content: [
|
|
627
769
|
{ type: "text", text: sanitized },
|
|
628
|
-
...extraBlocks
|
|
770
|
+
...extraBlocks.filter(Boolean)
|
|
629
771
|
]
|
|
630
772
|
};
|
|
631
773
|
}
|
|
632
774
|
|
|
775
|
+
if (name === "redact_file") {
|
|
776
|
+
const { file_path, profile, dry_run, no_backup } = args;
|
|
777
|
+
if (!file_path || !fs.existsSync(file_path)) {
|
|
778
|
+
return { isError: true, content: [{ type: "text", text: `Error: File not found at path: ${file_path}` }] };
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
try {
|
|
782
|
+
const stat = await fs.promises.stat(file_path);
|
|
783
|
+
if (stat.size > 50 * 1024 * 1024) {
|
|
784
|
+
return { isError: true, content: [{ type: "text", text: `Error: File is too large (>50MB).` }] };
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
const buffer = fs.readFileSync(file_path);
|
|
788
|
+
if (buffer.indexOf(0) !== -1) {
|
|
789
|
+
return { isError: true, content: [{ type: "text", text: `Error: Cannot redact binary file.` }] };
|
|
790
|
+
}
|
|
791
|
+
const content = buffer.toString('utf8');
|
|
792
|
+
|
|
793
|
+
const customRules = loadCustomRules();
|
|
794
|
+
const normalizedProfile = (profile || "general").trim().toLowerCase();
|
|
795
|
+
const license = checkLicenseStatus();
|
|
796
|
+
const localSessionMap = {};
|
|
797
|
+
|
|
798
|
+
const result = PrivacyScrubberCore.scrubText(content, customRules, {}, normalizedProfile, localSessionMap, license.isPro);
|
|
799
|
+
|
|
800
|
+
let isSecretLeak = false;
|
|
801
|
+
if (!license.isPro && result.tokenMap) {
|
|
802
|
+
Object.entries(result.tokenMap).forEach(([token, original]) => {
|
|
803
|
+
for (const detector of DEVOPS_SECRETS_DETECTOR) {
|
|
804
|
+
detector.regex.lastIndex = 0;
|
|
805
|
+
if (detector.regex.test(original)) {
|
|
806
|
+
isSecretLeak = true;
|
|
807
|
+
break;
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
});
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
const counts = {};
|
|
814
|
+
Object.keys(localSessionMap).forEach(token => {
|
|
815
|
+
const match = token.match(/^\[([A-Z_]+)_\d+\]$/);
|
|
816
|
+
if (match) {
|
|
817
|
+
counts[match[1]] = (counts[match[1]] || 0) + 1;
|
|
818
|
+
}
|
|
819
|
+
});
|
|
820
|
+
const summary = Object.entries(counts).map(([type, count]) => `${count} ${type}`).join(', ') || 'None';
|
|
821
|
+
|
|
822
|
+
if (dry_run) {
|
|
823
|
+
return {
|
|
824
|
+
content: [{
|
|
825
|
+
type: "text",
|
|
826
|
+
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.`
|
|
827
|
+
}]
|
|
828
|
+
};
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
if (isSecretLeak) {
|
|
832
|
+
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` }] };
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
if (!no_backup) {
|
|
836
|
+
fs.writeFileSync(`${file_path}.bak`, content, 'utf8');
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
fs.writeFileSync(file_path, result.scrubbedText, 'utf8');
|
|
840
|
+
|
|
841
|
+
return {
|
|
842
|
+
content: [{
|
|
843
|
+
type: "text",
|
|
844
|
+
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)."}`
|
|
845
|
+
}]
|
|
846
|
+
};
|
|
847
|
+
|
|
848
|
+
} catch (err) {
|
|
849
|
+
return { isError: true, content: [{ type: "text", text: `Error redacting file: ${err.message}` }] };
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
|
|
633
853
|
if (name === "create_default_config") {
|
|
634
854
|
const configPath = path.resolve(process.cwd(), 'privacyscrubber.json');
|
|
635
855
|
if (fs.existsSync(configPath)) {
|
|
@@ -642,7 +862,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
642
862
|
}
|
|
643
863
|
|
|
644
864
|
const defaultTemplate = {
|
|
645
|
-
"_comment": "PrivacyScrubber local configuration. Add custom rules and restart your MCP client (Cursor/Windsurf). Details: https://privacyscrubber.com/docs/config",
|
|
865
|
+
"_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",
|
|
646
866
|
"customRules": [
|
|
647
867
|
{
|
|
648
868
|
"pattern": "my-secret-pattern-\\d+",
|
|
@@ -691,7 +911,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
691
911
|
const profileList = license.isPro
|
|
692
912
|
? 'All 23 profiles active (General, Dev, Medical, Legal, Finance, HR…)'
|
|
693
913
|
: 'General only — PRO unlocks 22 industry profiles';
|
|
694
|
-
const sizeLimit = license.isPro ? 'Unlimited' : '
|
|
914
|
+
const sizeLimit = license.isPro ? 'Unlimited' : '15,000 characters per request';
|
|
695
915
|
|
|
696
916
|
const configPath = resolveConfigPath();
|
|
697
917
|
let rulesStatus = '';
|
|
@@ -737,12 +957,12 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
737
957
|
lines.push(
|
|
738
958
|
'║ ✅ PRO is active. All features unlocked. ║',
|
|
739
959
|
'║ To regenerate your key or manage billing: ║',
|
|
740
|
-
'║ https://privacyscrubber.com/pricing ║'
|
|
960
|
+
'║ https://privacyscrubber.com/pricing?utm_source=mcp_cli&utm_medium=terminal ║'
|
|
741
961
|
);
|
|
742
962
|
} else {
|
|
743
963
|
lines.push(
|
|
744
964
|
'║ 💳 Upgrade to PRO — $110 Lifetime ║',
|
|
745
|
-
'║ https://privacyscrubber.com/pricing ║',
|
|
965
|
+
'║ https://privacyscrubber.com/pricing?utm_source=mcp_cli&utm_medium=terminal ║',
|
|
746
966
|
'╠══════════════════════════════════════════════════╣',
|
|
747
967
|
'║ After purchase, add your key to MCP config: ║',
|
|
748
968
|
'║ ║',
|
|
@@ -750,7 +970,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
750
970
|
'║ "PRIVACYSCRUBBER_KEY": "<your-key-here>" ║',
|
|
751
971
|
'║ ║',
|
|
752
972
|
'║ Full setup guide: ║',
|
|
753
|
-
'║ https://privacyscrubber.com/features/mcp
|
|
973
|
+
'║ https://privacyscrubber.com/features/mcp/?utm_source=mcp_cli&utm_medium=terminal ║'
|
|
754
974
|
);
|
|
755
975
|
}
|
|
756
976
|
|
|
@@ -761,6 +981,92 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
761
981
|
};
|
|
762
982
|
}
|
|
763
983
|
|
|
984
|
+
if (name === "audit_directory_for_pii") {
|
|
985
|
+
const { directory_path, profile, extensions, ignore_node_modules } = args;
|
|
986
|
+
if (!directory_path || !fs.existsSync(directory_path)) {
|
|
987
|
+
return { isError: true, content: [{ type: "text", text: `Error: Directory not found at path: ${directory_path}` }] };
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
const filesToScan = [];
|
|
991
|
+
const ignoreDirs = ignore_node_modules !== false ? ['.git', 'node_modules', '.next', 'dist', 'build', '.cache'] : ['.git'];
|
|
992
|
+
|
|
993
|
+
async function walkDir(dir) {
|
|
994
|
+
const entries = await fs.promises.readdir(dir, { withFileTypes: true });
|
|
995
|
+
for (const entry of entries) {
|
|
996
|
+
const fullPath = path.join(dir, entry.name);
|
|
997
|
+
if (entry.isDirectory()) {
|
|
998
|
+
if (!ignoreDirs.includes(entry.name)) await walkDir(fullPath);
|
|
999
|
+
} else if (entry.isFile()) {
|
|
1000
|
+
if (extensions && extensions.length > 0) {
|
|
1001
|
+
const ext = path.extname(entry.name);
|
|
1002
|
+
if (!extensions.includes(ext)) continue;
|
|
1003
|
+
}
|
|
1004
|
+
filesToScan.push(fullPath);
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
await walkDir(directory_path);
|
|
1010
|
+
|
|
1011
|
+
let reportLines = [`## Security Audit Report: ${directory_path}`];
|
|
1012
|
+
let filesWithIssues = 0;
|
|
1013
|
+
const customRules = loadCustomRules();
|
|
1014
|
+
const normalizedProfile = (profile || "general").trim().toLowerCase();
|
|
1015
|
+
const license = checkLicenseStatus();
|
|
1016
|
+
|
|
1017
|
+
for (const file of filesToScan) {
|
|
1018
|
+
try {
|
|
1019
|
+
const stat = await fs.promises.stat(file);
|
|
1020
|
+
if (stat.size > 10 * 1024 * 1024) continue; // skip >10MB
|
|
1021
|
+
|
|
1022
|
+
const buffer = fs.readFileSync(file);
|
|
1023
|
+
// basic binary check
|
|
1024
|
+
if (buffer.indexOf(0) !== -1) continue;
|
|
1025
|
+
const content = buffer.toString('utf8');
|
|
1026
|
+
|
|
1027
|
+
const localSessionMap = {};
|
|
1028
|
+
const result = PrivacyScrubberCore.scrubText(content, customRules, {}, normalizedProfile, localSessionMap, license.isPro);
|
|
1029
|
+
|
|
1030
|
+
let isSecretLeak = false;
|
|
1031
|
+
if (!license.isPro && result.tokenMap) {
|
|
1032
|
+
Object.entries(result.tokenMap).forEach(([token, original]) => {
|
|
1033
|
+
for (const detector of DEVOPS_SECRETS_DETECTOR) {
|
|
1034
|
+
detector.regex.lastIndex = 0;
|
|
1035
|
+
if (detector.regex.test(original)) {
|
|
1036
|
+
isSecretLeak = true;
|
|
1037
|
+
break;
|
|
1038
|
+
}
|
|
1039
|
+
}
|
|
1040
|
+
});
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
if (Object.keys(localSessionMap).length > 0 || isSecretLeak) {
|
|
1044
|
+
filesWithIssues++;
|
|
1045
|
+
const counts = {};
|
|
1046
|
+
Object.keys(localSessionMap).forEach(token => {
|
|
1047
|
+
const match = token.match(/^\[([A-Z_]+)_\d+\]$/);
|
|
1048
|
+
if (match) {
|
|
1049
|
+
counts[match[1]] = (counts[match[1]] || 0) + 1;
|
|
1050
|
+
}
|
|
1051
|
+
});
|
|
1052
|
+
const summary = Object.entries(counts).map(([type, count]) => `${count} ${type}`).join(', ');
|
|
1053
|
+
let leakMsg = isSecretLeak ? " (⚠️ RAW SECRET DETECTED — REQUIRES PRO)" : "";
|
|
1054
|
+
reportLines.push(`- \`${path.relative(directory_path, file)}\`: ⚠️ Found ${summary}${leakMsg}`);
|
|
1055
|
+
}
|
|
1056
|
+
} catch (err) {
|
|
1057
|
+
// ignore file read errors
|
|
1058
|
+
}
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
if (filesWithIssues === 0) {
|
|
1062
|
+
reportLines.push("✅ Clean. No PII or secrets detected in the scanned files.");
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
return {
|
|
1066
|
+
content: [{ type: "text", text: reportLines.join('\n') }]
|
|
1067
|
+
};
|
|
1068
|
+
}
|
|
1069
|
+
|
|
764
1070
|
return {
|
|
765
1071
|
isError: true,
|
|
766
1072
|
content: [{ type: "text", text: `Unknown tool: ${name}` }]
|
|
@@ -831,11 +1137,11 @@ function loadCustomRules() {
|
|
|
831
1137
|
category: (r.label || r.category || "CUSTOM").toUpperCase()
|
|
832
1138
|
})).filter(r => r.pattern);
|
|
833
1139
|
} else {
|
|
834
|
-
|
|
1140
|
+
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`);
|
|
835
1141
|
}
|
|
836
1142
|
}
|
|
837
1143
|
} catch (e) {
|
|
838
|
-
|
|
1144
|
+
mcpLog(`Error parsing privacyscrubber.json: ${e.message}\n`);
|
|
839
1145
|
}
|
|
840
1146
|
}
|
|
841
1147
|
return [];
|
|
@@ -857,28 +1163,8 @@ function detectSecrets(text) {
|
|
|
857
1163
|
function performSanitization(text, profile) {
|
|
858
1164
|
const customRules = loadCustomRules();
|
|
859
1165
|
const normalizedProfile = (profile || "general").trim().toLowerCase();
|
|
860
|
-
const result = PrivacyScrubberCore.scrubText(text, customRules, {}, normalizedProfile, sessionMap);
|
|
861
|
-
|
|
862
1166
|
const license = checkLicenseStatus();
|
|
863
|
-
|
|
864
|
-
// Re-scan each masked token's original value against the shared detector.
|
|
865
|
-
// If it was a raw secret, restore it in the output and remove from sessionMap
|
|
866
|
-
// so it is never accidentally revealed by reveal_text.
|
|
867
|
-
Object.entries(result.tokenMap).forEach(([token, original]) => {
|
|
868
|
-
let isSecret = false;
|
|
869
|
-
for (const detector of DEVOPS_SECRETS_DETECTOR) {
|
|
870
|
-
detector.regex.lastIndex = 0;
|
|
871
|
-
if (detector.regex.test(original)) {
|
|
872
|
-
isSecret = true;
|
|
873
|
-
break;
|
|
874
|
-
}
|
|
875
|
-
}
|
|
876
|
-
if (isSecret) {
|
|
877
|
-
result.scrubbedText = result.scrubbedText.replace(token, original);
|
|
878
|
-
delete result.tokenMap[token];
|
|
879
|
-
}
|
|
880
|
-
});
|
|
881
|
-
}
|
|
1167
|
+
const result = PrivacyScrubberCore.scrubText(text, customRules, {}, normalizedProfile, sessionMap, license.isPro);
|
|
882
1168
|
|
|
883
1169
|
// Update our volatile map with new matches
|
|
884
1170
|
if (result.tokenMap) {
|
|
@@ -890,11 +1176,10 @@ function performSanitization(text, profile) {
|
|
|
890
1176
|
return result.scrubbedText;
|
|
891
1177
|
}
|
|
892
1178
|
|
|
893
|
-
function truncateIfFree(text, isPro) {
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
return { processedText: text.substring(0, MAX_FREE_CHARS), wasTruncated: true };
|
|
1179
|
+
function truncateIfFree(text, isPro, charLimit = 15000) {
|
|
1180
|
+
if (!isPro && text.length > charLimit) {
|
|
1181
|
+
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`);
|
|
1182
|
+
return { processedText: text.substring(0, charLimit), wasTruncated: true };
|
|
898
1183
|
}
|
|
899
1184
|
return { processedText: text, wasTruncated: false };
|
|
900
1185
|
}
|
|
@@ -902,8 +1187,8 @@ function truncateIfFree(text, isPro) {
|
|
|
902
1187
|
// Start the server transport
|
|
903
1188
|
const transport = new StdioServerTransport();
|
|
904
1189
|
server.connect(transport).then(() => {
|
|
905
|
-
|
|
906
|
-
|
|
1190
|
+
mcpLog(`${colors.greenBold}✅ PrivacyScrubber ZTDS MCP Server started successfully.${colors.reset}\n`);
|
|
1191
|
+
mcpLog(`${colors.cyan}⭐ Star us on GitHub: https://github.com/moxno/privacyscrubber-mcp${colors.reset}\n`);
|
|
907
1192
|
}).catch((error) => {
|
|
908
1193
|
console.error("Failed to connect MCP server transport:", error);
|
|
909
1194
|
process.exit(1);
|