@privacyscrubber/mcp-server 2.1.0 → 2.2.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 +125 -22
- package/index.js +486 -21
- package/manifest.json +4 -2
- package/package.json +7 -2
- package/pii-masking-run.js +213 -54
- package/ps-license-manager.cjs +257 -0
- package/ps-pii-engine.cjs +79 -36
- package/ps-pii-engine.js +79 -36
- package/scrubber-core.cjs +6 -6
- package/server.json +3 -3
package/index.js
CHANGED
|
@@ -24,6 +24,7 @@ import path from 'path';
|
|
|
24
24
|
import { fileURLToPath } from 'url';
|
|
25
25
|
|
|
26
26
|
import crypto from 'crypto';
|
|
27
|
+
import { execSync } from 'child_process';
|
|
27
28
|
|
|
28
29
|
const require = createRequire(import.meta.url);
|
|
29
30
|
const __filename = fileURLToPath(import.meta.url);
|
|
@@ -37,7 +38,14 @@ const MCP_VERSION = require('./package.json').version;
|
|
|
37
38
|
// Import the production core engine with 100% parity
|
|
38
39
|
const scrubberCorePath = path.resolve(__dirname, './scrubber-core.cjs');
|
|
39
40
|
const PrivacyScrubberCore = require(scrubberCorePath);
|
|
40
|
-
let LicenseManager
|
|
41
|
+
let LicenseManager;
|
|
42
|
+
try {
|
|
43
|
+
LicenseManager = require('./ps-license-manager.cjs');
|
|
44
|
+
} catch (e) {
|
|
45
|
+
try {
|
|
46
|
+
LicenseManager = require('./ps-license-manager.js');
|
|
47
|
+
} catch (err) {}
|
|
48
|
+
}
|
|
41
49
|
if (!LicenseManager || typeof LicenseManager.validate !== 'function') {
|
|
42
50
|
LicenseManager = global.LicenseManager;
|
|
43
51
|
}
|
|
@@ -61,15 +69,8 @@ const colors = {
|
|
|
61
69
|
reset: '\x1b[0m'
|
|
62
70
|
};
|
|
63
71
|
|
|
64
|
-
// Helper to log to
|
|
72
|
+
// Helper to log to stderr (preserves stdout strictly for JSON-RPC MCP protocol messages)
|
|
65
73
|
function mcpLog(msg) {
|
|
66
|
-
const clean = msg.replace(/\x1b\[[0-9;]*m/g, '').trim();
|
|
67
|
-
const level = clean.includes('⚠️') || clean.includes('Error') ? "warning" : "info";
|
|
68
|
-
try {
|
|
69
|
-
server.sendLoggingMessage({ level, data: clean });
|
|
70
|
-
} catch(e) {
|
|
71
|
-
// Ignore if server isn't fully connected yet
|
|
72
|
-
}
|
|
73
74
|
process.stderr.write(msg);
|
|
74
75
|
}
|
|
75
76
|
|
|
@@ -221,9 +222,20 @@ function buildCisoAuditTelemetry(currentTokenMap = {}) {
|
|
|
221
222
|
};
|
|
222
223
|
}
|
|
223
224
|
|
|
224
|
-
function formatAuditReceipt(telemetry) {
|
|
225
|
+
function formatAuditReceipt(telemetry, compact = false) {
|
|
226
|
+
const isCompact = compact === true || process.env.PRIVACYSCRUBBER_COMPACT_RECEIPT === '1' || process.env.PRIVACYSCRUBBER_COMPACT_RECEIPT === 'true';
|
|
227
|
+
if (isCompact) {
|
|
228
|
+
if (telemetry.totalCount === 0) {
|
|
229
|
+
return "\n\n> [ZTDS: CLEAN (ZERO PII DETECTED)]\n";
|
|
230
|
+
}
|
|
231
|
+
const entitiesList = Object.entries(telemetry.entities)
|
|
232
|
+
.map(([type, count]) => `${count} ${type}`)
|
|
233
|
+
.join(', ');
|
|
234
|
+
return `\n\n> [ZTDS: ${telemetry.totalCount} masked (${entitiesList}) | Risk: ${telemetry.riskLevel} | Compliance: ${telemetry.frameworksList.join(', ')}]\n`;
|
|
235
|
+
}
|
|
236
|
+
|
|
225
237
|
if (telemetry.totalCount === 0) {
|
|
226
|
-
return "
|
|
238
|
+
return "\n\n> 🛡️ **PrivacyScrubber Audit Receipt**: CLEAN (ZERO PII DETECTED)\n> * ⭐ **Star on GitHub:** [moxno/privacyscrubber-mcp](https://github.com/moxno/privacyscrubber-mcp) | **SDK & Enterprise:** [privacyscrubber.com/pricing](https://privacyscrubber.com/pricing)\n";
|
|
227
239
|
}
|
|
228
240
|
|
|
229
241
|
const entitiesList = Object.entries(telemetry.entities)
|
|
@@ -232,7 +244,7 @@ function formatAuditReceipt(telemetry) {
|
|
|
232
244
|
|
|
233
245
|
const icon = telemetry.riskLevel.includes('CRITICAL') ? '🔴' : (telemetry.riskLevel.includes('MODERATE') ? '🟠' : '🟢');
|
|
234
246
|
|
|
235
|
-
return
|
|
247
|
+
return `\n\n> 🛡️ **PrivacyScrubber Audit Receipt**\n> * **Risk Level:** ${icon} ${telemetry.riskLevel}\n> * **Compliance Enforced:** ${telemetry.frameworksList.join(', ')}\n> * **Tokens Masked:** ${telemetry.totalCount} (${entitiesList})\n> * ⭐ **Star on GitHub:** [moxno/privacyscrubber-mcp](https://github.com/moxno/privacyscrubber-mcp) | **SDK & Enterprise:** [privacyscrubber.com/pricing](https://privacyscrubber.com/pricing)\n`;
|
|
236
248
|
}
|
|
237
249
|
|
|
238
250
|
// Create the MCP server
|
|
@@ -269,6 +281,10 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
269
281
|
type: "string",
|
|
270
282
|
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'."
|
|
271
283
|
},
|
|
284
|
+
compact: {
|
|
285
|
+
type: "boolean",
|
|
286
|
+
description: "Optional. When true, returns a compact 1-line audit summary saving token overhead in AI IDEs (Cursor, Claude Desktop)."
|
|
287
|
+
},
|
|
272
288
|
ignore_list: {
|
|
273
289
|
type: "array",
|
|
274
290
|
items: { type: "string" },
|
|
@@ -305,6 +321,10 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
305
321
|
profile: {
|
|
306
322
|
type: "string",
|
|
307
323
|
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'."
|
|
324
|
+
},
|
|
325
|
+
compact: {
|
|
326
|
+
type: "boolean",
|
|
327
|
+
description: "Optional. When true, returns a compact 1-line audit summary saving token overhead in AI IDEs (Cursor, Claude Desktop)."
|
|
308
328
|
}
|
|
309
329
|
},
|
|
310
330
|
required: ["file_path"]
|
|
@@ -417,6 +437,120 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
417
437
|
properties: {},
|
|
418
438
|
required: []
|
|
419
439
|
}
|
|
440
|
+
},
|
|
441
|
+
{
|
|
442
|
+
name: "guard_exec",
|
|
443
|
+
description: "Zero-Trust Agentic Guard: Safely executes a shell/terminal command locally in isolated process, sanitizes stdout and stderr in volatile RAM, tokenizes any API keys, credentials, database URIs, or PII before sending output to LLM. Maps original values in memory for safe local restoration.",
|
|
444
|
+
inputSchema: {
|
|
445
|
+
type: "object",
|
|
446
|
+
properties: {
|
|
447
|
+
command: {
|
|
448
|
+
type: "string",
|
|
449
|
+
description: "The shell command to execute (e.g. 'cat .env', 'git diff', 'docker logs web', 'env')."
|
|
450
|
+
},
|
|
451
|
+
cwd: {
|
|
452
|
+
type: "string",
|
|
453
|
+
description: "Optional working directory. Defaults to current workspace root."
|
|
454
|
+
},
|
|
455
|
+
profile: {
|
|
456
|
+
type: "string",
|
|
457
|
+
description: "Detection profile. Available: 'General', 'Dev' (recommended for code/secrets), 'Security', 'Finance', etc. Defaults to 'Dev'."
|
|
458
|
+
},
|
|
459
|
+
timeout_ms: {
|
|
460
|
+
type: "number",
|
|
461
|
+
description: "Execution timeout in milliseconds. Defaults to 15000 (15s)."
|
|
462
|
+
}
|
|
463
|
+
},
|
|
464
|
+
required: ["command"]
|
|
465
|
+
}
|
|
466
|
+
},
|
|
467
|
+
{
|
|
468
|
+
name: "guard_read_file",
|
|
469
|
+
description: "Zero-Trust Agentic Guard: Reads a sensitive local file (.env, config, credentials, source code, database dumps), sanitizes all credentials and PII in volatile RAM, and returns safe tokenized content for AI agent context.",
|
|
470
|
+
inputSchema: {
|
|
471
|
+
type: "object",
|
|
472
|
+
properties: {
|
|
473
|
+
file_path: {
|
|
474
|
+
type: "string",
|
|
475
|
+
description: "Absolute or relative path to the file to inspect."
|
|
476
|
+
},
|
|
477
|
+
profile: {
|
|
478
|
+
type: "string",
|
|
479
|
+
description: "Detection profile. Defaults to 'Dev'."
|
|
480
|
+
},
|
|
481
|
+
max_lines: {
|
|
482
|
+
type: "number",
|
|
483
|
+
description: "Optional line cap for large files. Defaults to 500."
|
|
484
|
+
}
|
|
485
|
+
},
|
|
486
|
+
required: ["file_path"]
|
|
487
|
+
}
|
|
488
|
+
},
|
|
489
|
+
{
|
|
490
|
+
name: "guard_git_diff",
|
|
491
|
+
description: "Zero-Trust Agentic Guard: Inspects unstaged or staged git diffs locally, sanitizes any newly added or modified credentials, tokens, or PII in RAM, and returns safe diff for AI commit generation or code review.",
|
|
492
|
+
inputSchema: {
|
|
493
|
+
type: "object",
|
|
494
|
+
properties: {
|
|
495
|
+
staged: {
|
|
496
|
+
type: "boolean",
|
|
497
|
+
description: "If true, runs 'git diff --cached'. If false, runs 'git diff'. Defaults to false."
|
|
498
|
+
},
|
|
499
|
+
cwd: {
|
|
500
|
+
type: "string",
|
|
501
|
+
description: "Optional repository directory. Defaults to current workspace root."
|
|
502
|
+
},
|
|
503
|
+
profile: {
|
|
504
|
+
type: "string",
|
|
505
|
+
description: "Detection profile. Defaults to 'Dev'."
|
|
506
|
+
}
|
|
507
|
+
},
|
|
508
|
+
required: []
|
|
509
|
+
}
|
|
510
|
+
},
|
|
511
|
+
{
|
|
512
|
+
name: "guard_apply_patch",
|
|
513
|
+
description: "Zero-Trust Agentic Guard: Reverses token placeholders ([API_KEY_1], [SECRET_1]) in AI-generated code or text, restores authentic values from local volatile RAM, and writes real content directly to target file on disk. Remote LLM never learns actual secrets.",
|
|
514
|
+
inputSchema: {
|
|
515
|
+
type: "object",
|
|
516
|
+
properties: {
|
|
517
|
+
file_path: {
|
|
518
|
+
type: "string",
|
|
519
|
+
description: "Absolute path to the destination file to create or update."
|
|
520
|
+
},
|
|
521
|
+
content: {
|
|
522
|
+
type: "string",
|
|
523
|
+
description: "Code or text content containing token placeholders to de-anonymize locally before write."
|
|
524
|
+
},
|
|
525
|
+
create_backup: {
|
|
526
|
+
type: "boolean",
|
|
527
|
+
description: "Whether to create a .bak backup if target file already exists. Defaults to true."
|
|
528
|
+
}
|
|
529
|
+
},
|
|
530
|
+
required: ["file_path", "content"]
|
|
531
|
+
}
|
|
532
|
+
},
|
|
533
|
+
{
|
|
534
|
+
name: "create_agent_rules",
|
|
535
|
+
description: "Zero-Trust Agentic Guard: Scaffolds CISO-grade AI Agent Rules in workspace (.cursorrules, .windsurfrules, CLAUDE.md, .github/copilot-instructions.md, .clinerules). Guides autonomous agents to route all file reads, git diffs, and shell commands through PrivacyScrubber Guard tools.",
|
|
536
|
+
inputSchema: {
|
|
537
|
+
type: "object",
|
|
538
|
+
properties: {
|
|
539
|
+
agent_types: {
|
|
540
|
+
type: "array",
|
|
541
|
+
items: {
|
|
542
|
+
type: "string",
|
|
543
|
+
enum: ["all", "cursor", "windsurf", "claude_code", "copilot", "cline"]
|
|
544
|
+
},
|
|
545
|
+
description: "List of agent targets to generate rules for. Use ['all'] to generate rules for all supported AI agents."
|
|
546
|
+
},
|
|
547
|
+
workspace_dir: {
|
|
548
|
+
type: "string",
|
|
549
|
+
description: "Target workspace root directory. Defaults to current directory."
|
|
550
|
+
}
|
|
551
|
+
},
|
|
552
|
+
required: []
|
|
553
|
+
}
|
|
420
554
|
}
|
|
421
555
|
]
|
|
422
556
|
};
|
|
@@ -433,6 +567,10 @@ server.setRequestHandler(ListPromptsRequestSchema, async () => {
|
|
|
433
567
|
{
|
|
434
568
|
name: "safe_redaction_workflow",
|
|
435
569
|
description: "Instructions for using the redact_file tool safely with QA checks.",
|
|
570
|
+
},
|
|
571
|
+
{
|
|
572
|
+
name: "agentic_guard_instructions",
|
|
573
|
+
description: "Zero-Trust Agentic Guard instructions for autonomous AI agents (Cursor, Windsurf, Claude Code).",
|
|
436
574
|
}
|
|
437
575
|
]
|
|
438
576
|
};
|
|
@@ -455,6 +593,21 @@ server.setRequestHandler(GetPromptRequestSchema, async (request) => {
|
|
|
455
593
|
};
|
|
456
594
|
}
|
|
457
595
|
|
|
596
|
+
if (request.params.name === "agentic_guard_instructions") {
|
|
597
|
+
return {
|
|
598
|
+
description: "Instructions for autonomous AI agents operating under Zero-Trust Agentic Guard.",
|
|
599
|
+
messages: [
|
|
600
|
+
{
|
|
601
|
+
role: "user",
|
|
602
|
+
content: {
|
|
603
|
+
type: "text",
|
|
604
|
+
text: "You are an autonomous AI software engineer operating under strict Zero-Trust Data Sanitization (ZTDS) policies. Sensitive files, logs, and shell output must never leak API keys, credentials, or PII to external model APIs.\n\nFollow these mandatory agent protocols:\n1. Terminal Commands: When running commands whose output might contain credentials or PII (cat .env, docker logs, env, git diff, psql), use `guard_exec` to sanitize output in local RAM before it enters your context.\n2. Reading Secrets/Config: When reading configuration files, API keys, or database schemas, call `guard_read_file` instead of raw file readers.\n3. Code Review & Commits: Use `guard_git_diff` to review changes safely with credentials masked as tokens.\n4. Writing Code: When writing code containing token placeholders (e.g. [API_KEY_1]), use `guard_apply_patch` to restore authentic values locally on disk without exposing them to the cloud.\n5. AI Answers: Pass final text containing tokens through `reveal_text` before returning it to the user."
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
]
|
|
608
|
+
};
|
|
609
|
+
}
|
|
610
|
+
|
|
458
611
|
if (request.params.name !== "secure_ai_workflow") {
|
|
459
612
|
throw new Error(`Unknown prompt: ${request.params.name}`);
|
|
460
613
|
}
|
|
@@ -539,7 +692,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
539
692
|
|
|
540
693
|
try {
|
|
541
694
|
if (name === "sanitize_text") {
|
|
542
|
-
const { text, profile = "General", ignore_list } = args || {};
|
|
695
|
+
const { text, profile = "General", ignore_list, compact = false } = args || {};
|
|
543
696
|
|
|
544
697
|
// Merge per-call ignore_list into persistent sessionIgnoreList
|
|
545
698
|
if (Array.isArray(ignore_list)) {
|
|
@@ -563,6 +716,16 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
563
716
|
const isAdvanced = targetProfile.toLowerCase() !== "general";
|
|
564
717
|
const license = checkLicenseStatus();
|
|
565
718
|
let finalProfile = targetProfile;
|
|
719
|
+
let isAutoElevated = false;
|
|
720
|
+
|
|
721
|
+
// Heuristic auto-detection: if profile is General and text contains obvious DevOps credentials or code syntax, auto-activate Dev
|
|
722
|
+
const DEV_HEURISTIC_REGEX = /(?:AKIA[0-9A-Z]{16}|ghp_[a-zA-Z0-9]{36}|sk-[a-zA-Z0-9]{20,}|-----BEGIN (?:RSA )?PRIVATE KEY-----|Bearer\s+eyJ[a-zA-Z0-9_-]+\.[a-zA-Z0-9_-]+|(?:postgres|postgresql|mysql|mongodb|redis):\/\/[^\s"'>]+|export\s+(?:AWS_|DATABASE_|API_KEY|SECRET)|const\s+\w+\s*=\s*require\(|import\s+.*\s+from\s+['"])/i;
|
|
723
|
+
|
|
724
|
+
if (!isAdvanced && DEV_HEURISTIC_REGEX.test(text)) {
|
|
725
|
+
finalProfile = "Dev";
|
|
726
|
+
isAutoElevated = true;
|
|
727
|
+
mcpLog(`${colors.cyan}🔍 [PrivacyScrubber] Auto-detected code/DevOps secrets. Activated 'Dev' profile rules.${colors.reset}\n`);
|
|
728
|
+
}
|
|
566
729
|
|
|
567
730
|
const limitStatus = checkFreeTierLimit(license.isPro);
|
|
568
731
|
if (limitStatus.blocked) {
|
|
@@ -572,7 +735,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
572
735
|
};
|
|
573
736
|
}
|
|
574
737
|
|
|
575
|
-
const charLimit = (isAdvanced && !license.isPro) ? 5000 : 15000;
|
|
738
|
+
const charLimit = (isAdvanced && !isAutoElevated && !license.isPro) ? 5000 : 15000;
|
|
576
739
|
if (isAdvanced && !license.isPro) {
|
|
577
740
|
mcpLog(`${colors.yellowBold}⚠️ [PrivacyScrubber] Profile '${targetProfile}' active on Free Tier (5,000 char limit).${colors.reset}\n`);
|
|
578
741
|
}
|
|
@@ -582,7 +745,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
582
745
|
const { scrubbedText, newTokens } = performSanitization(processedText, finalProfile, sessionIgnoreList);
|
|
583
746
|
|
|
584
747
|
const telemetry = buildCisoAuditTelemetry(newTokens);
|
|
585
|
-
const receiptMd = formatAuditReceipt(telemetry);
|
|
748
|
+
const receiptMd = formatAuditReceipt(telemetry, compact);
|
|
586
749
|
|
|
587
750
|
return {
|
|
588
751
|
content: [
|
|
@@ -676,7 +839,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
676
839
|
]
|
|
677
840
|
};
|
|
678
841
|
}
|
|
679
|
-
const { profile = "General" } = args;
|
|
842
|
+
const { profile = "General", compact = false } = args;
|
|
680
843
|
const filePath = rawPath;
|
|
681
844
|
|
|
682
845
|
const resolvedPath = path.resolve(filePath);
|
|
@@ -747,7 +910,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
747
910
|
const sanitized = performSanitization(processedContent, finalProfile, sessionIgnoreList);
|
|
748
911
|
const { scrubbedText, newTokens } = sanitized;
|
|
749
912
|
const telemetry = buildCisoAuditTelemetry(newTokens);
|
|
750
|
-
const receiptMarkdown = formatAuditReceipt(telemetry);
|
|
913
|
+
const receiptMarkdown = formatAuditReceipt(telemetry, compact);
|
|
751
914
|
const combinedOutput = `${scrubbedText}\n\n${receiptMarkdown}`;
|
|
752
915
|
|
|
753
916
|
return {
|
|
@@ -793,7 +956,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
793
956
|
const sanitized = performSanitization(content, finalProfile, sessionIgnoreList);
|
|
794
957
|
const { scrubbedText, newTokens } = sanitized;
|
|
795
958
|
const telemetry = buildCisoAuditTelemetry(newTokens);
|
|
796
|
-
const receiptMarkdown = formatAuditReceipt(telemetry);
|
|
959
|
+
const receiptMarkdown = formatAuditReceipt(telemetry, compact);
|
|
797
960
|
const combinedOutput = `${scrubbedText}\n\n${receiptMarkdown}`;
|
|
798
961
|
|
|
799
962
|
return {
|
|
@@ -826,7 +989,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
826
989
|
const sanitized = performSanitization(content, finalProfile, sessionIgnoreList);
|
|
827
990
|
const { scrubbedText, newTokens } = sanitized;
|
|
828
991
|
const telemetry = buildCisoAuditTelemetry(newTokens);
|
|
829
|
-
const receiptMarkdown = formatAuditReceipt(telemetry);
|
|
992
|
+
const receiptMarkdown = formatAuditReceipt(telemetry, compact);
|
|
830
993
|
const combinedOutput = `${scrubbedText}\n\n${receiptMarkdown}`;
|
|
831
994
|
|
|
832
995
|
return {
|
|
@@ -870,6 +1033,17 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
870
1033
|
const isAdvanced = targetProfile.toLowerCase() !== "general";
|
|
871
1034
|
const license = checkLicenseStatus();
|
|
872
1035
|
let finalProfile = targetProfile;
|
|
1036
|
+
let isAutoElevated = false;
|
|
1037
|
+
|
|
1038
|
+
const ext = path.extname(resolvedPath).toLowerCase();
|
|
1039
|
+
const CODE_EXTENSIONS = new Set(['.js', '.ts', '.jsx', '.tsx', '.py', '.go', '.rs', '.java', '.rb', '.php', '.cs', '.sh', '.bash', '.zsh', '.env', '.yaml', '.yml', '.toml', '.sql']);
|
|
1040
|
+
const DEV_HEURISTIC_REGEX = /(?:AKIA[0-9A-Z]{16}|ghp_[a-zA-Z0-9]{36}|sk-[a-zA-Z0-9]{20,}|-----BEGIN (?:RSA )?PRIVATE KEY-----|Bearer\s+eyJ[a-zA-Z0-9_-]+\.[a-zA-Z0-9_-]+|(?:postgres|postgresql|mysql|mongodb|redis):\/\/[^\s"'>]+|export\s+(?:AWS_|DATABASE_|API_KEY|SECRET)|const\s+\w+\s*=\s*require\(|import\s+.*\s+from\s+['"])/i;
|
|
1041
|
+
|
|
1042
|
+
if (!isAdvanced && (CODE_EXTENSIONS.has(ext) || DEV_HEURISTIC_REGEX.test(content))) {
|
|
1043
|
+
finalProfile = "Dev";
|
|
1044
|
+
isAutoElevated = true;
|
|
1045
|
+
mcpLog(`${colors.cyan}🔍 [PrivacyScrubber] Auto-detected code/DevOps file '${path.basename(resolvedPath)}'. Activated 'Dev' profile rules.${colors.reset}\n`);
|
|
1046
|
+
}
|
|
873
1047
|
|
|
874
1048
|
const limitStatus = checkFreeTierLimit(license.isPro);
|
|
875
1049
|
if (limitStatus.blocked) {
|
|
@@ -879,7 +1053,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
879
1053
|
};
|
|
880
1054
|
}
|
|
881
1055
|
|
|
882
|
-
const charLimit = (isAdvanced && !license.isPro) ? 5000 : 15000;
|
|
1056
|
+
const charLimit = (isAdvanced && !isAutoElevated && !license.isPro) ? 5000 : 15000;
|
|
883
1057
|
if (isAdvanced && !license.isPro) {
|
|
884
1058
|
mcpLog(`${colors.yellowBold}⚠️ [PrivacyScrubber] Profile '${targetProfile}' active on Free Tier (5,000 char limit).${colors.reset}\n`);
|
|
885
1059
|
}
|
|
@@ -888,7 +1062,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
888
1062
|
|
|
889
1063
|
const { scrubbedText, newTokens } = performSanitization(processedContent2, finalProfile, sessionIgnoreList);
|
|
890
1064
|
const telemetry = buildCisoAuditTelemetry(newTokens);
|
|
891
|
-
const receiptMd = formatAuditReceipt(telemetry);
|
|
1065
|
+
const receiptMd = formatAuditReceipt(telemetry, compact);
|
|
892
1066
|
|
|
893
1067
|
return {
|
|
894
1068
|
content: [
|
|
@@ -1288,6 +1462,296 @@ ${telemetry.frameworksList.map(f => `- **${f}**`).join('\n')}
|
|
|
1288
1462
|
};
|
|
1289
1463
|
}
|
|
1290
1464
|
|
|
1465
|
+
if (name === "guard_exec") {
|
|
1466
|
+
const { command, cwd, profile = "Dev", timeout_ms = 15000 } = args || {};
|
|
1467
|
+
if (!command || typeof command !== "string") {
|
|
1468
|
+
return {
|
|
1469
|
+
isError: true,
|
|
1470
|
+
content: [{ type: "text", text: "Error: Missing required parameter 'command'." }]
|
|
1471
|
+
};
|
|
1472
|
+
}
|
|
1473
|
+
const license = checkLicenseStatus();
|
|
1474
|
+
const limitStatus = checkFreeTierLimit(license.isPro);
|
|
1475
|
+
if (limitStatus.blocked) {
|
|
1476
|
+
return {
|
|
1477
|
+
isError: true,
|
|
1478
|
+
content: [{ type: "text", text: `Error: Free tier daily limit exhausted (${FREE_TIER_DAILY_LIMIT} requests). Get a PRO key at https://privacyscrubber.com/pricing` }]
|
|
1479
|
+
};
|
|
1480
|
+
}
|
|
1481
|
+
|
|
1482
|
+
const execCwd = cwd ? path.resolve(cwd) : process.cwd();
|
|
1483
|
+
let stdout = '';
|
|
1484
|
+
let stderr = '';
|
|
1485
|
+
let exitCode = 0;
|
|
1486
|
+
|
|
1487
|
+
try {
|
|
1488
|
+
stdout = execSync(command, {
|
|
1489
|
+
cwd: execCwd,
|
|
1490
|
+
timeout: Math.min(Math.max(timeout_ms, 1000), 60000),
|
|
1491
|
+
maxBuffer: 10 * 1024 * 1024,
|
|
1492
|
+
encoding: 'utf8',
|
|
1493
|
+
stdio: ['pipe', 'pipe', 'pipe']
|
|
1494
|
+
});
|
|
1495
|
+
} catch (err) {
|
|
1496
|
+
stdout = err.stdout ? err.stdout.toString() : '';
|
|
1497
|
+
stderr = err.stderr ? err.stderr.toString() : (err.message || '');
|
|
1498
|
+
exitCode = typeof err.status === 'number' ? err.status : 1;
|
|
1499
|
+
}
|
|
1500
|
+
|
|
1501
|
+
const targetProfile = (profile || "Dev").trim();
|
|
1502
|
+
const isAdvanced = targetProfile.toLowerCase() !== "general";
|
|
1503
|
+
const charLimit = (isAdvanced && !license.isPro) ? 5000 : 15000;
|
|
1504
|
+
|
|
1505
|
+
const { processedText: cleanStdout } = truncateIfFree(stdout, license.isPro, charLimit);
|
|
1506
|
+
const { processedText: cleanStderr } = truncateIfFree(stderr, license.isPro, charLimit);
|
|
1507
|
+
|
|
1508
|
+
const resCommand = performSanitization(command, targetProfile, sessionIgnoreList);
|
|
1509
|
+
const resStdout = performSanitization(cleanStdout, targetProfile, sessionIgnoreList);
|
|
1510
|
+
const resStderr = performSanitization(cleanStderr, targetProfile, sessionIgnoreList);
|
|
1511
|
+
|
|
1512
|
+
const allNewTokens = { ...resCommand.newTokens, ...resStdout.newTokens, ...resStderr.newTokens };
|
|
1513
|
+
const tokenCount = Object.keys(allNewTokens).length;
|
|
1514
|
+
const telemetry = buildCisoAuditTelemetry(allNewTokens);
|
|
1515
|
+
const receiptMd = formatAuditReceipt(telemetry);
|
|
1516
|
+
|
|
1517
|
+
const responseText = `[Zero-Trust Agentic Guard: Exec: ${command.split(' ')[0]}]\n` +
|
|
1518
|
+
`Command: ${resCommand.scrubbedText}\n` +
|
|
1519
|
+
`Exit Code: ${exitCode}\n` +
|
|
1520
|
+
`Tokens Redacted: ${tokenCount}\n\n` +
|
|
1521
|
+
(resStdout.scrubbedText ? `--- STDOUT ---\n${resStdout.scrubbedText}\n\n` : '') +
|
|
1522
|
+
(resStderr.scrubbedText ? `--- STDERR ---\n${resStderr.scrubbedText}\n\n` : '') +
|
|
1523
|
+
receiptMd;
|
|
1524
|
+
|
|
1525
|
+
return {
|
|
1526
|
+
content: [{ type: "text", text: responseText }]
|
|
1527
|
+
};
|
|
1528
|
+
}
|
|
1529
|
+
|
|
1530
|
+
if (name === "guard_read_file") {
|
|
1531
|
+
const { file_path, profile = "Dev", max_lines = 500 } = args || {};
|
|
1532
|
+
if (!file_path || typeof file_path !== "string") {
|
|
1533
|
+
return {
|
|
1534
|
+
isError: true,
|
|
1535
|
+
content: [{ type: "text", text: "Error: Missing required parameter 'file_path'." }]
|
|
1536
|
+
};
|
|
1537
|
+
}
|
|
1538
|
+
const resolvedPath = path.resolve(process.cwd(), file_path);
|
|
1539
|
+
if (!fs.existsSync(resolvedPath)) {
|
|
1540
|
+
return {
|
|
1541
|
+
isError: true,
|
|
1542
|
+
content: [{ type: "text", text: `Error: File not found at path: ${resolvedPath}` }]
|
|
1543
|
+
};
|
|
1544
|
+
}
|
|
1545
|
+
const stat = fs.statSync(resolvedPath);
|
|
1546
|
+
if (stat.isDirectory()) {
|
|
1547
|
+
return {
|
|
1548
|
+
isError: true,
|
|
1549
|
+
content: [{ type: "text", text: `Error: Path is a directory. Use audit_directory_for_pii to scan directories.` }]
|
|
1550
|
+
};
|
|
1551
|
+
}
|
|
1552
|
+
const license = checkLicenseStatus();
|
|
1553
|
+
const limitStatus = checkFreeTierLimit(license.isPro);
|
|
1554
|
+
if (limitStatus.blocked) {
|
|
1555
|
+
return {
|
|
1556
|
+
isError: true,
|
|
1557
|
+
content: [{ type: "text", text: `Error: Free tier daily limit exhausted. Get a PRO key at https://privacyscrubber.com/pricing` }]
|
|
1558
|
+
};
|
|
1559
|
+
}
|
|
1560
|
+
|
|
1561
|
+
let rawContent = fs.readFileSync(resolvedPath, 'utf8');
|
|
1562
|
+
const lines = rawContent.split(/\r?\n/);
|
|
1563
|
+
const capped = lines.slice(0, max_lines).join('\n');
|
|
1564
|
+
const wasTruncated = lines.length > max_lines;
|
|
1565
|
+
|
|
1566
|
+
const targetProfile = (profile || "Dev").trim();
|
|
1567
|
+
const isAdvanced = targetProfile.toLowerCase() !== "general";
|
|
1568
|
+
const charLimit = (isAdvanced && !license.isPro) ? 5000 : 15000;
|
|
1569
|
+
|
|
1570
|
+
const { processedText } = truncateIfFree(capped, license.isPro, charLimit);
|
|
1571
|
+
const { scrubbedText, newTokens } = performSanitization(processedText, targetProfile, sessionIgnoreList);
|
|
1572
|
+
|
|
1573
|
+
const telemetry = buildCisoAuditTelemetry(newTokens);
|
|
1574
|
+
const receiptMd = formatAuditReceipt(telemetry);
|
|
1575
|
+
|
|
1576
|
+
const responseText = `[Zero-Trust Agentic Guard: File Read: ${path.basename(resolvedPath)}]\n` +
|
|
1577
|
+
`Path: ${resolvedPath}\n` +
|
|
1578
|
+
`Total Lines: ${lines.length}${wasTruncated ? ` (Displaying first ${max_lines} lines)` : ''}\n` +
|
|
1579
|
+
`Tokens Redacted: ${Object.keys(newTokens).length}\n\n` +
|
|
1580
|
+
`--- SANITIZED CONTENT ---\n` +
|
|
1581
|
+
scrubbedText + '\n\n' +
|
|
1582
|
+
receiptMd;
|
|
1583
|
+
|
|
1584
|
+
return {
|
|
1585
|
+
content: [{ type: "text", text: responseText }]
|
|
1586
|
+
};
|
|
1587
|
+
}
|
|
1588
|
+
|
|
1589
|
+
if (name === "guard_git_diff") {
|
|
1590
|
+
const { staged = false, cwd, profile = "Dev" } = args || {};
|
|
1591
|
+
const repoCwd = cwd ? path.resolve(cwd) : process.cwd();
|
|
1592
|
+
const license = checkLicenseStatus();
|
|
1593
|
+
const limitStatus = checkFreeTierLimit(license.isPro);
|
|
1594
|
+
if (limitStatus.blocked) {
|
|
1595
|
+
return {
|
|
1596
|
+
isError: true,
|
|
1597
|
+
content: [{ type: "text", text: `Error: Free tier daily limit exhausted. Get a PRO key at https://privacyscrubber.com/pricing` }]
|
|
1598
|
+
};
|
|
1599
|
+
}
|
|
1600
|
+
|
|
1601
|
+
const gitCmd = staged ? "git diff --cached" : "git diff";
|
|
1602
|
+
let diffOutput = '';
|
|
1603
|
+
try {
|
|
1604
|
+
diffOutput = execSync(gitCmd, { cwd: repoCwd, encoding: 'utf8', maxBuffer: 64 * 1024 * 1024 });
|
|
1605
|
+
} catch (err) {
|
|
1606
|
+
if (err.code === 'ENOBUFS' && err.stdout) {
|
|
1607
|
+
diffOutput = err.stdout.toString('utf8');
|
|
1608
|
+
} else {
|
|
1609
|
+
return {
|
|
1610
|
+
isError: true,
|
|
1611
|
+
content: [{ type: "text", text: `Error executing '${gitCmd}': ${err.message}` }]
|
|
1612
|
+
};
|
|
1613
|
+
}
|
|
1614
|
+
}
|
|
1615
|
+
|
|
1616
|
+
if (!diffOutput.trim()) {
|
|
1617
|
+
return {
|
|
1618
|
+
content: [{ type: "text", text: `[Zero-Trust Agentic Guard: Git Diff]\nNo git diff changes detected (${staged ? 'staged' : 'unstaged'}).` }]
|
|
1619
|
+
};
|
|
1620
|
+
}
|
|
1621
|
+
|
|
1622
|
+
const targetProfile = (profile || "Dev").trim();
|
|
1623
|
+
const isAdvanced = targetProfile.toLowerCase() !== "general";
|
|
1624
|
+
const charLimit = (isAdvanced && !license.isPro) ? 5000 : 15000;
|
|
1625
|
+
|
|
1626
|
+
const { processedText } = truncateIfFree(diffOutput, license.isPro, charLimit);
|
|
1627
|
+
const { scrubbedText, newTokens } = performSanitization(processedText, targetProfile, sessionIgnoreList);
|
|
1628
|
+
|
|
1629
|
+
const telemetry = buildCisoAuditTelemetry(newTokens);
|
|
1630
|
+
const receiptMd = formatAuditReceipt(telemetry);
|
|
1631
|
+
|
|
1632
|
+
const responseText = `[Zero-Trust Agentic Guard: Git Diff (${staged ? 'Staged' : 'Unstaged'})]\n` +
|
|
1633
|
+
`Tokens Redacted: ${Object.keys(newTokens).length}\n\n` +
|
|
1634
|
+
`--- SANITIZED DIFF ---\n` +
|
|
1635
|
+
scrubbedText + '\n\n' +
|
|
1636
|
+
receiptMd;
|
|
1637
|
+
|
|
1638
|
+
return {
|
|
1639
|
+
content: [{ type: "text", text: responseText }]
|
|
1640
|
+
};
|
|
1641
|
+
}
|
|
1642
|
+
|
|
1643
|
+
if (name === "guard_apply_patch") {
|
|
1644
|
+
const { file_path, content, create_backup = true } = args || {};
|
|
1645
|
+
if (!file_path || typeof file_path !== "string") {
|
|
1646
|
+
return {
|
|
1647
|
+
isError: true,
|
|
1648
|
+
content: [{ type: "text", text: "Error: Missing required parameter 'file_path'." }]
|
|
1649
|
+
};
|
|
1650
|
+
}
|
|
1651
|
+
if (content === undefined || content === null || typeof content !== "string") {
|
|
1652
|
+
return {
|
|
1653
|
+
isError: true,
|
|
1654
|
+
content: [{ type: "text", text: "Error: Missing required parameter 'content' (must be string)." }]
|
|
1655
|
+
};
|
|
1656
|
+
}
|
|
1657
|
+
|
|
1658
|
+
const resolvedPath = path.resolve(process.cwd(), file_path);
|
|
1659
|
+
const restored = PrivacyScrubberCore.unscrubText(content, sessionMap);
|
|
1660
|
+
const authenticContent = restored.restoredText;
|
|
1661
|
+
|
|
1662
|
+
let backupCreated = false;
|
|
1663
|
+
if (create_backup && fs.existsSync(resolvedPath)) {
|
|
1664
|
+
const backupPath = `${resolvedPath}.bak`;
|
|
1665
|
+
fs.copyFileSync(resolvedPath, backupPath);
|
|
1666
|
+
backupCreated = true;
|
|
1667
|
+
}
|
|
1668
|
+
|
|
1669
|
+
const parentDir = path.dirname(resolvedPath);
|
|
1670
|
+
if (!fs.existsSync(parentDir)) {
|
|
1671
|
+
fs.mkdirSync(parentDir, { recursive: true });
|
|
1672
|
+
}
|
|
1673
|
+
|
|
1674
|
+
fs.writeFileSync(resolvedPath, authenticContent, 'utf8');
|
|
1675
|
+
|
|
1676
|
+
return {
|
|
1677
|
+
content: [{
|
|
1678
|
+
type: "text",
|
|
1679
|
+
text: `[Zero-Trust Agentic Guard: Patch Applied Successfully]\n` +
|
|
1680
|
+
`Destination: ${resolvedPath}\n` +
|
|
1681
|
+
`Tokens Restored Locally: ${restored.restoredCount || 0}\n` +
|
|
1682
|
+
`Backup Created: ${backupCreated ? `${resolvedPath}.bak` : 'No'}\n` +
|
|
1683
|
+
`Status: File written with 100% authentic local values. Remote LLM received zero secrets.`
|
|
1684
|
+
}]
|
|
1685
|
+
};
|
|
1686
|
+
}
|
|
1687
|
+
|
|
1688
|
+
if (name === "create_agent_rules") {
|
|
1689
|
+
const { agent_types = ["all"], workspace_dir } = args || {};
|
|
1690
|
+
const targetDir = workspace_dir ? path.resolve(workspace_dir) : process.cwd();
|
|
1691
|
+
const targets = agent_types.includes("all") ? ["cursor", "windsurf", "claude_code", "copilot", "cline"] : agent_types;
|
|
1692
|
+
|
|
1693
|
+
const ruleText = `# PrivacyScrubber Zero-Trust Agentic Guard (CISO Security Rules)
|
|
1694
|
+
# 100% Local In-Memory Data Sanitization. Zero Network Egress.
|
|
1695
|
+
|
|
1696
|
+
## Agent Mandate
|
|
1697
|
+
You are an autonomous AI assistant operating under strict Zero-Trust Data Sanitization (ZTDS) policies.
|
|
1698
|
+
Before reading sensitive files, running terminal commands that may print credentials, or committing code:
|
|
1699
|
+
|
|
1700
|
+
1. **Terminal Commands:** When running shell commands whose output may contain API keys, database connection strings, or customer PII (e.g. \`cat .env\`, \`docker logs\`, \`env\`, \`git diff\`), invoke the \`guard_exec\` tool instead of raw terminal execution.
|
|
1701
|
+
2. **Reading Credentials/Config:** When reading \`.env*\`, config files, database credentials, or secret keys, always call \`guard_read_file\` so sensitive entities are tokenized locally in RAM before entering your context window.
|
|
1702
|
+
3. **Git Code Review:** Use \`guard_git_diff\` to inspect changes without exposing plaintext secrets to the cloud.
|
|
1703
|
+
4. **Applying Patches:** When writing files containing token placeholders (e.g. \`[API_KEY_1]\`, \`[DB_PASSWORD_1]\`), use \`guard_apply_patch\` to automatically restore authentic secrets locally to disk without sending them back to the LLM.
|
|
1704
|
+
5. **Final Output:** Pass any final synthesized text containing token placeholders through \`reveal_text\` before presenting it to the user.
|
|
1705
|
+
`;
|
|
1706
|
+
|
|
1707
|
+
const generatedFiles = [];
|
|
1708
|
+
if (targets.includes("cursor")) {
|
|
1709
|
+
const p = path.join(targetDir, '.cursorrules');
|
|
1710
|
+
fs.writeFileSync(p, ruleText, 'utf8');
|
|
1711
|
+
generatedFiles.push('.cursorrules');
|
|
1712
|
+
}
|
|
1713
|
+
if (targets.includes("windsurf")) {
|
|
1714
|
+
const p = path.join(targetDir, '.windsurfrules');
|
|
1715
|
+
fs.writeFileSync(p, ruleText, 'utf8');
|
|
1716
|
+
generatedFiles.push('.windsurfrules');
|
|
1717
|
+
}
|
|
1718
|
+
if (targets.includes("claude_code")) {
|
|
1719
|
+
const p = path.join(targetDir, 'CLAUDE.md');
|
|
1720
|
+
if (fs.existsSync(p)) {
|
|
1721
|
+
const existing = fs.readFileSync(p, 'utf8');
|
|
1722
|
+
if (!existing.includes("Zero-Trust Agentic Guard")) {
|
|
1723
|
+
fs.writeFileSync(p, existing + "\n\n" + ruleText, 'utf8');
|
|
1724
|
+
generatedFiles.push('CLAUDE.md (appended)');
|
|
1725
|
+
}
|
|
1726
|
+
} else {
|
|
1727
|
+
fs.writeFileSync(p, ruleText, 'utf8');
|
|
1728
|
+
generatedFiles.push('CLAUDE.md');
|
|
1729
|
+
}
|
|
1730
|
+
}
|
|
1731
|
+
if (targets.includes("copilot")) {
|
|
1732
|
+
const copilotDir = path.join(targetDir, '.github');
|
|
1733
|
+
if (!fs.existsSync(copilotDir)) fs.mkdirSync(copilotDir, { recursive: true });
|
|
1734
|
+
const p = path.join(copilotDir, 'copilot-instructions.md');
|
|
1735
|
+
fs.writeFileSync(p, ruleText, 'utf8');
|
|
1736
|
+
generatedFiles.push('.github/copilot-instructions.md');
|
|
1737
|
+
}
|
|
1738
|
+
if (targets.includes("cline")) {
|
|
1739
|
+
const p = path.join(targetDir, '.clinerules');
|
|
1740
|
+
fs.writeFileSync(p, ruleText, 'utf8');
|
|
1741
|
+
generatedFiles.push('.clinerules');
|
|
1742
|
+
}
|
|
1743
|
+
|
|
1744
|
+
return {
|
|
1745
|
+
content: [{
|
|
1746
|
+
type: "text",
|
|
1747
|
+
text: `[Zero-Trust Agentic Guard: Agent Rules Generated]\n` +
|
|
1748
|
+
`Workspace: ${targetDir}\n` +
|
|
1749
|
+
`Files Created/Updated:\n${generatedFiles.map(f => `- ${f}`).join('\n')}\n\n` +
|
|
1750
|
+
`Autonomous AI agents in this repository will now automatically use PrivacyScrubber Guard tools.`
|
|
1751
|
+
}]
|
|
1752
|
+
};
|
|
1753
|
+
}
|
|
1754
|
+
|
|
1291
1755
|
return {
|
|
1292
1756
|
isError: true,
|
|
1293
1757
|
content: [{ type: "text", text: `Unknown tool: ${name}` }]
|
|
@@ -1413,6 +1877,7 @@ server.connect(transport).then(() => {
|
|
|
1413
1877
|
mcpLog(`${colors.greenBold}✅ PrivacyScrubber ZTDS MCP Server v${MCP_VERSION} started successfully.${colors.reset}\n`);
|
|
1414
1878
|
mcpLog(`${colors.cyan}📦 Need programmatic in-code redaction? Try: npm install @privacyscrubber/sdk${colors.reset}\n`);
|
|
1415
1879
|
mcpLog(`${colors.yellowBold}⭐ Star us on GitHub: https://github.com/moxno/privacyscrubber-mcp${colors.reset}\n`);
|
|
1880
|
+
mcpLog(`${colors.cyan}👉 Developer SDK & Enterprise: https://privacyscrubber.com/pricing${colors.reset}\n`);
|
|
1416
1881
|
}).catch((error) => {
|
|
1417
1882
|
console.error("Failed to connect MCP server transport:", error);
|
|
1418
1883
|
process.exit(1);
|