@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/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 = require('./ps-license-manager.js');
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 both standard terminal (with colors) and MCP logging protocol
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 "\\n\\n> 🛡️ **PrivacyScrubber Audit Receipt**: CLEAN (ZERO PII DETECTED)\\n";
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 `\\n\\n> 🛡️ **PrivacyScrubber Audit Receipt**\\n> * **Risk Level:** ${icon} ${telemetry.riskLevel}\\n> * **Compliance Enforced:** ${telemetry.frameworksList.join(', ')}\\n> * **Tokens Masked:** ${telemetry.totalCount} (${entitiesList})\\n`;
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);