@privacyscrubber/mcp-server 2.2.2 โ†’ 2.2.5

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 os from 'os';
27
28
  import { execSync } from 'child_process';
28
29
 
29
30
  const require = createRequire(import.meta.url);
@@ -35,6 +36,15 @@ const __dirname = path.dirname(__filename);
35
36
  // Never hardcode the version โ€” bump package.json at root instead.
36
37
  const MCP_VERSION = require('./package.json').version;
37
38
 
39
+ // CLI Command Intercept (init / install / setup / auto-configure)
40
+ const rawCliArgs = process.argv.slice(2);
41
+ if (rawCliArgs.some(arg => ['init', 'install', '--install', 'setup', '--setup'].includes(arg))) {
42
+ const { runInstaller } = await import('./install.js');
43
+ const forwardedArgs = rawCliArgs.filter(a => !['init', 'install', '--install', 'setup', '--setup'].includes(a));
44
+ await runInstaller(forwardedArgs);
45
+ process.exit(0);
46
+ }
47
+
38
48
  // Import the production core engine with 100% parity
39
49
  const scrubberCorePath = path.resolve(__dirname, './scrubber-core.cjs');
40
50
  const PrivacyScrubberCore = require(scrubberCorePath);
@@ -53,8 +63,39 @@ if (!LicenseManager || typeof LicenseManager.validate !== 'function') {
53
63
  // Initialize core engine
54
64
  PrivacyScrubberCore.init();
55
65
 
56
- // Volatile in-memory token map
57
- const sessionMap = {};
66
+ // Volatile in-memory token map per session (multi-task agent isolation)
67
+ const agentSessions = new Map();
68
+ const MAX_CONCURRENT_SESSIONS = 200;
69
+
70
+ function getSessionMap(sessionId = 'default') {
71
+ const sid = (sessionId || 'default').toString().trim() || 'default';
72
+ let session = agentSessions.get(sid);
73
+ const now = Date.now();
74
+ if (!session) {
75
+ if (agentSessions.size >= MAX_CONCURRENT_SESSIONS) {
76
+ const oldestKey = agentSessions.keys().next().value;
77
+ agentSessions.delete(oldestKey);
78
+ }
79
+ session = { map: {}, createdAt: now, lastActive: now };
80
+ agentSessions.set(sid, session);
81
+ }
82
+ session.lastActive = now;
83
+ return session.map;
84
+ }
85
+
86
+ // Global default sessionMap maintains 100% backward compatibility
87
+ const sessionMap = getSessionMap('default');
88
+
89
+ function reverseTokens(text, map) {
90
+ if (!text || typeof text !== 'string' || !map) return text;
91
+ let result = text;
92
+ for (const [token, original] of Object.entries(map)) {
93
+ if (result.includes(token)) {
94
+ result = result.replaceAll(token, original);
95
+ }
96
+ }
97
+ return result;
98
+ }
58
99
 
59
100
  // Volatile in-memory false positive ignore list (values excluded from future scrubs)
60
101
  const sessionIgnoreList = new Set();
@@ -259,6 +300,114 @@ function formatAuditReceipt(telemetry, compact = false, wasTruncated = false, ch
259
300
  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${truncationLine}> * โญ **Star on GitHub:** [moxno/privacyscrubber-mcp](https://github.com/moxno/privacyscrubber-mcp) (\`gh repo star moxno/privacyscrubber-mcp\`) | **SDK & Enterprise:** [privacyscrubber.com/pricing](https://privacyscrubber.com/pricing)\n`;
260
301
  }
261
302
 
303
+ function formatCefEvent(eventData = {}) {
304
+ const {
305
+ vendor = 'PrivacyScrubber',
306
+ product = 'ZTDS',
307
+ version = MCP_VERSION,
308
+ eventClassId = 'PII_REDACTION',
309
+ eventName = 'Zero-Trust PII Sanitization Executed',
310
+ riskLevel = 'LOW EXPOSURE',
311
+ tokensMasked = 0,
312
+ entities = {},
313
+ frameworks = ['ZTDS Standard'],
314
+ sessionHash = '',
315
+ source = 'local_ram',
316
+ action = 'redact_and_mask',
317
+ organization = '',
318
+ department = ''
319
+ } = eventData;
320
+
321
+ let severity = 1;
322
+ if (riskLevel.includes('CRITICAL')) severity = 9;
323
+ else if (riskLevel.includes('MODERATE')) severity = 6;
324
+ else if (riskLevel.includes('LOW')) severity = 3;
325
+ else severity = 0;
326
+
327
+ const entitiesStr = Object.entries(entities).map(([k, v]) => `${k}:${v}`).join(',') || 'none';
328
+ const frameworksStr = Array.isArray(frameworks) ? frameworks.join(',') : String(frameworks);
329
+ const safeMsg = `Sanitized ${tokensMasked} entity tokens in volatile RAM. 0 bytes network egress.`;
330
+
331
+ const extension = [
332
+ `src=${source}`,
333
+ `act=${action}`,
334
+ `cs1Label=RiskLevel cs1=${riskLevel.replace(/\s+/g, '_')}`,
335
+ `cn1Label=TokensMasked cn1=${tokensMasked}`,
336
+ `cs2Label=Frameworks cs2=${frameworksStr.replace(/\s+/g, '_')}`,
337
+ `cs3Label=Entities cs3=${entitiesStr}`,
338
+ sessionHash ? `cs4Label=SessionHash cs4=${sessionHash.substring(0, 16)}` : '',
339
+ organization ? `suser=${organization.replace(/\s+/g, '_')}` : '',
340
+ department ? `cs5Label=Department cs5=${department.replace(/\s+/g, '_')}` : '',
341
+ `msg=${safeMsg}`
342
+ ].filter(Boolean).join(' ');
343
+
344
+ return `CEF:0|${vendor}|${product}|${version}|${eventClassId}|${eventName}|${severity}|${extension}`;
345
+ }
346
+
347
+ function formatSyslogEvent(eventData = {}) {
348
+ const {
349
+ timestamp = new Date().toISOString(),
350
+ hostname = (typeof os !== 'undefined' && typeof os.hostname === 'function' ? os.hostname() : 'localhost'),
351
+ pid = (typeof process !== 'undefined' && process.pid ? process.pid : 1),
352
+ riskLevel = 'LOW EXPOSURE',
353
+ tokensMasked = 0,
354
+ frameworks = ['ZTDS Standard'],
355
+ sessionHash = '',
356
+ organization = ''
357
+ } = eventData;
358
+
359
+ let pri = 134; // local0.info
360
+ if (riskLevel.includes('CRITICAL')) pri = 132; // local0.warning
361
+ else if (riskLevel.includes('MODERATE')) pri = 133; // local0.notice
362
+
363
+ const frameworksStr = Array.isArray(frameworks) ? frameworks.join(';') : String(frameworks);
364
+ const hashPrefix = sessionHash.substring(0, 16) || 'none';
365
+ const orgStr = organization ? ` org="${organization.replace(/"/g, '')}"` : '';
366
+
367
+ const sd = `[ztds@49152 risk="${riskLevel}" tokens="${tokensMasked}" frameworks="${frameworksStr}" session="${hashPrefix}"${orgStr}]`;
368
+ const msg = `Zero-Trust Data Sanitization executed in RAM. ${tokensMasked} token(s) masked. 0 bytes network egress.`;
369
+
370
+ return `<${pri}>1 ${timestamp} ${hostname} privacyscrubber ${pid} ZTDS - ${sd} ${msg}`;
371
+ }
372
+
373
+ function formatJsonlEvent(eventData = {}) {
374
+ const {
375
+ timestamp = new Date().toISOString(),
376
+ version = MCP_VERSION,
377
+ protocol = 'ZTDS',
378
+ event = 'sanitization',
379
+ riskLevel = 'LOW EXPOSURE',
380
+ tokensMasked = 0,
381
+ entities = {},
382
+ frameworks = ['ZTDS Standard'],
383
+ sessionHash = '',
384
+ organization = '',
385
+ department = '',
386
+ executionMs = 0,
387
+ metadata = {}
388
+ } = eventData;
389
+
390
+ const record = {
391
+ timestamp,
392
+ version,
393
+ protocol,
394
+ event,
395
+ risk_level: riskLevel,
396
+ tokens_masked: tokensMasked,
397
+ entities,
398
+ frameworks,
399
+ session_hash: sessionHash,
400
+ zero_egress: true,
401
+ verification_mode: "100% Offline (In-Memory RAM)",
402
+ ...(organization && { organization }),
403
+ ...(department && { department }),
404
+ ...(executionMs && { execution_ms: executionMs }),
405
+ ...(metadata && Object.keys(metadata).length > 0 && { metadata })
406
+ };
407
+
408
+ return JSON.stringify(record);
409
+ }
410
+
262
411
  // Create the MCP server
263
412
  const server = new Server(
264
413
  {
@@ -279,6 +428,29 @@ const server = new Server(
279
428
  server.setRequestHandler(ListToolsRequestSchema, async () => {
280
429
  return {
281
430
  tools: [
431
+ {
432
+ name: "audit_context",
433
+ description: "STEP 0 (Pre-Flight): Non-destructive security inspection of raw prompts or document chunks before sending to LLMs. Evaluates PII, secrets, risk severity (CLEAN, LOW, MODERATE, CRITICAL), and triggered regulatory frameworks (GDPR, HIPAA, SOC 2, PCI DSS) with zero text mutation.",
434
+ inputSchema: {
435
+ type: "object",
436
+ properties: {
437
+ text: {
438
+ type: "string",
439
+ description: "The raw text, code, prompt, or logs to audit."
440
+ },
441
+ profile: {
442
+ type: "string",
443
+ description: "The detection profile to use (e.g. 'General', 'Dev', 'Medical', 'Finance', 'Legal'). Defaults to 'General'."
444
+ },
445
+ format: {
446
+ type: "string",
447
+ description: "Output format: 'detailed' (default markdown), 'summary', or 'json'.",
448
+ enum: ["detailed", "summary", "json"]
449
+ }
450
+ },
451
+ required: ["text"]
452
+ }
453
+ },
282
454
  {
283
455
  name: "sanitize_text",
284
456
  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. (For in-code backend services or RAG vector pipelines outside of MCP, use '@privacyscrubber/sdk': npm i @privacyscrubber/sdk)",
@@ -306,6 +478,28 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
306
478
  required: ["text"]
307
479
  }
308
480
  },
481
+ {
482
+ name: "scrub_text",
483
+ description: "Alias for 'sanitize_text'. Locally scrubs PII and secrets before LLM ingestion.",
484
+ inputSchema: {
485
+ type: "object",
486
+ properties: {
487
+ text: {
488
+ type: "string",
489
+ description: "The raw text, code, or logs to sanitize."
490
+ },
491
+ profile: {
492
+ type: "string",
493
+ description: "The detection profile to use. Defaults to 'General'."
494
+ },
495
+ compact: {
496
+ type: "boolean",
497
+ description: "Optional. Compact 1-line audit summary."
498
+ }
499
+ },
500
+ required: ["text"]
501
+ }
502
+ },
309
503
  {
310
504
  name: "reveal_text",
311
505
  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.",
@@ -332,11 +526,33 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
332
526
  },
333
527
  profile: {
334
528
  type: "string",
335
- description: "The detection profile to use. Available: 'General' (Free), or PRO profiles: 'Dev' (Engineering/Code), 'Medical', 'Pharma', 'Biotech', 'Telecom', 'Legal', 'Compliance', 'CCPA', 'Finance', 'Bizops', 'Sales', 'WealthMgmt', 'Insurance', 'Accounting', 'Underwriting', 'Automotive', 'Energy', 'Hospitality', 'HR', 'Security', 'Marketing', 'Support', 'RealEstate', 'Agents', 'Academic', 'Creative', 'Tech', 'Personal'. Defaults to 'General'."
529
+ description: "The detection profile to use. Defaults to 'General'."
336
530
  },
337
531
  compact: {
338
532
  type: "boolean",
339
- description: "Optional. When true, returns a compact 1-line audit summary saving token overhead in AI IDEs (Cursor, Claude Desktop)."
533
+ description: "Optional. Compact 1-line audit summary."
534
+ }
535
+ },
536
+ required: ["file_path"]
537
+ }
538
+ },
539
+ {
540
+ name: "scrub_file",
541
+ description: "Alias for 'sanitize_file'. Reads and sanitizes a local file.",
542
+ inputSchema: {
543
+ type: "object",
544
+ properties: {
545
+ file_path: {
546
+ type: "string",
547
+ description: "Absolute path to the file to sanitize."
548
+ },
549
+ profile: {
550
+ type: "string",
551
+ description: "The detection profile to use. Defaults to 'General'."
552
+ },
553
+ compact: {
554
+ type: "boolean",
555
+ description: "Optional. Compact 1-line audit summary."
340
556
  }
341
557
  },
342
558
  required: ["file_path"]
@@ -563,6 +779,158 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
563
779
  },
564
780
  required: []
565
781
  }
782
+ },
783
+ {
784
+ name: "guard_unmask_args",
785
+ description: "Zero-Trust Agentic Guard: Detokenizes parameters, JSON objects, or arguments in local RAM before dispatching to local tools or APIs without sending cleartext secrets back to the LLM.",
786
+ inputSchema: {
787
+ type: "object",
788
+ properties: {
789
+ tool_args: {
790
+ description: "Arbitrary JSON object, array, or string containing token placeholders to detokenize in local RAM."
791
+ },
792
+ session_id: {
793
+ type: "string",
794
+ description: "Optional session ID for isolated multi-task agent memory. Defaults to 'default'."
795
+ }
796
+ },
797
+ required: ["tool_args"]
798
+ }
799
+ },
800
+ {
801
+ name: "guard_session_info",
802
+ description: "Zero-Trust Agentic Guard: Returns diagnostic metrics on active in-memory sessions, token counts, and memory footprint with zero plaintext leakage.",
803
+ inputSchema: {
804
+ type: "object",
805
+ properties: {
806
+ session_id: {
807
+ type: "string",
808
+ description: "Optional specific session ID to inspect."
809
+ }
810
+ },
811
+ required: []
812
+ }
813
+ },
814
+ {
815
+ name: "guard_session_reset",
816
+ description: "Zero-Trust Agentic Guard: Explicitly purges the in-memory token map for a specific session_id or all sessions upon agent task completion.",
817
+ inputSchema: {
818
+ type: "object",
819
+ properties: {
820
+ session_id: {
821
+ type: "string",
822
+ description: "The session ID to purge. Defaults to 'default'."
823
+ },
824
+ all: {
825
+ type: "boolean",
826
+ description: "Whether to purge ALL active agent sessions in memory. Defaults to false."
827
+ }
828
+ },
829
+ required: []
830
+ }
831
+ },
832
+ {
833
+ name: "guard_rag_chunk",
834
+ description: "Zero-Trust Agentic Guard: Sanitizes an array of document chunks or knowledge base records before sending to embedding models or vector databases (Chroma, Pinecone, Qdrant). Keeps authentic data in local RAM with zero network leakage.",
835
+ inputSchema: {
836
+ type: "object",
837
+ properties: {
838
+ chunks: {
839
+ type: "array",
840
+ description: "Array of chunk objects ({ id?: string, text: string, metadata?: object }) or array of chunk strings to sanitize.",
841
+ items: {
842
+ anyOf: [
843
+ { type: "string" },
844
+ {
845
+ type: "object",
846
+ properties: {
847
+ id: { type: "string" },
848
+ text: { type: "string" },
849
+ pageContent: { type: "string" },
850
+ metadata: { type: "object" }
851
+ }
852
+ }
853
+ ]
854
+ }
855
+ },
856
+ profile: {
857
+ type: "string",
858
+ description: "The detection profile to use. Defaults to 'General'."
859
+ },
860
+ session_id: {
861
+ type: "string",
862
+ description: "Optional session ID for isolated multi-task agent memory. Defaults to 'default'."
863
+ }
864
+ },
865
+ required: ["chunks"]
866
+ }
867
+ },
868
+ {
869
+ name: "guard_rag_restore",
870
+ description: "Zero-Trust Agentic Guard: Re-hydrates retrieved vector search results or chunks by restoring token placeholders with authentic data from the volatile RAM session before presenting to the user.",
871
+ inputSchema: {
872
+ type: "object",
873
+ properties: {
874
+ results: {
875
+ type: "array",
876
+ description: "Array of retrieved search result objects ({ id?: string, text?: string, pageContent?: string, score?: number, metadata?: object }) or strings containing tokens to restore.",
877
+ items: {
878
+ anyOf: [
879
+ { type: "string" },
880
+ {
881
+ type: "object",
882
+ properties: {
883
+ id: { type: "string" },
884
+ text: { type: "string" },
885
+ pageContent: { type: "string" },
886
+ score: { type: "number" },
887
+ metadata: { type: "object" }
888
+ }
889
+ }
890
+ ]
891
+ }
892
+ },
893
+ session_id: {
894
+ type: "string",
895
+ description: "Optional session ID for isolated multi-task agent memory. Defaults to 'default'."
896
+ }
897
+ },
898
+ required: ["results"]
899
+ }
900
+ },
901
+ {
902
+ name: "export_audit_log",
903
+ description: "Zero-Trust Enterprise Compliance: Exports an immutable, tamper-evident audit log in SIEM standard formats (CEF / ArcSight, RFC 5424 Syslog, JSONL, or JSON) with zero plaintext PII leakage. Can write directly to a local log file or stream.",
904
+ inputSchema: {
905
+ type: "object",
906
+ properties: {
907
+ format: {
908
+ type: "string",
909
+ enum: ["jsonl", "cef", "syslog", "json", "markdown"],
910
+ description: "Audit log format: 'jsonl' (Datadog/ELK), 'cef' (ArcSight/Splunk), 'syslog' (RFC 5424), 'json', or 'markdown'. Defaults to 'jsonl'."
911
+ },
912
+ session_id: {
913
+ type: "string",
914
+ description: "Optional session ID for isolated multi-task agent memory. Defaults to 'default'."
915
+ },
916
+ destination_file: {
917
+ type: "string",
918
+ description: "Optional local file path to append the audit log event to (e.g. '/var/log/privacyscrubber.log')."
919
+ },
920
+ company_name: {
921
+ type: "string",
922
+ description: "Optional company name for enterprise audit attribution. Defaults to 'PrivacyScrubber Client'."
923
+ },
924
+ department: {
925
+ type: "string",
926
+ description: "Optional department or auditor identifier. Defaults to 'SecOps / Compliance'."
927
+ },
928
+ text: {
929
+ type: "string",
930
+ description: "Optional raw text to sanitize and include in the exported audit log event."
931
+ }
932
+ }
933
+ }
566
934
  }
567
935
  ]
568
936
  };
@@ -709,8 +1077,75 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
709
1077
  const toolStart = Date.now();
710
1078
 
711
1079
  try {
712
- if (name === "sanitize_text") {
713
- const { text, profile = "General", ignore_list, compact = false } = args || {};
1080
+ if (name === "audit_context") {
1081
+ const { text, profile = "General", format = "detailed" } = args || {};
1082
+
1083
+ if (text === undefined || text === null) {
1084
+ return {
1085
+ isError: true,
1086
+ content: [{ type: "text", text: "Error: Missing required parameter 'text'. Provide the string to audit." }]
1087
+ };
1088
+ }
1089
+ if (typeof text !== "string") {
1090
+ return {
1091
+ isError: true,
1092
+ content: [{ type: "text", text: `Error: Parameter 'text' must be a string, got '${typeof text}'.` }]
1093
+ };
1094
+ }
1095
+
1096
+ const targetProfile = (profile || "General").trim();
1097
+ const customRules = loadCustomRules();
1098
+ const normalizedProfile = targetProfile.toLowerCase();
1099
+ const license = checkLicenseStatus();
1100
+
1101
+ // Read-only dry-run map: NEVER mutates global sessionMap
1102
+ const dryRunMap = {};
1103
+ const result = PrivacyScrubberCore.scrubText(text, customRules, {}, normalizedProfile, dryRunMap, license.isPro);
1104
+ const telemetry = buildCisoAuditTelemetry(result.tokenMap || {});
1105
+
1106
+ if (format === "json") {
1107
+ return {
1108
+ content: [{
1109
+ type: "text",
1110
+ text: JSON.stringify({
1111
+ riskLevel: telemetry.riskLevel,
1112
+ totalDetected: telemetry.totalDetected,
1113
+ entities: telemetry.entities,
1114
+ triggeredFrameworks: telemetry.triggeredFrameworks,
1115
+ profile: targetProfile,
1116
+ executionEngine: "ZTDS Client-Side RAM"
1117
+ }, null, 2)
1118
+ }]
1119
+ };
1120
+ }
1121
+
1122
+ let report = `### PrivacyScrubber Pre-Flight Context Audit\n\n`;
1123
+ report += `* **Status / Risk Level:** \`${telemetry.riskLevel}\`\n`;
1124
+ report += `* **Total Entities Detected:** \`${telemetry.totalDetected}\`\n`;
1125
+ report += `* **Active Profile:** \`${targetProfile}\` (Engine: ZTDS Client-Side RAM)\n`;
1126
+ report += `* **Triggered Compliance Frameworks:** ${telemetry.triggeredFrameworks.map(f => `\`${f}\``).join(', ')}\n\n`;
1127
+
1128
+ if (telemetry.totalDetected > 0) {
1129
+ report += `#### Entity Breakdown\n`;
1130
+ report += `| Entity Type | Occurrences | Exposure Severity |\n`;
1131
+ report += `| :--- | :--- | :--- |\n`;
1132
+ for (const [entityType, count] of Object.entries(telemetry.entities)) {
1133
+ const isHigh = ['ID', 'SSN', 'CREDIT_CARD', 'API_KEY', 'SECRET', 'PASSWORD', 'MRN', 'KEY'].includes(entityType);
1134
+ report += `| \`${entityType}\` | ${count} | ${isHigh ? 'High Risk' : 'Standard'} |\n`;
1135
+ }
1136
+ report += `\n> [!CAUTION]\n> **Action Recommended:** This context contains sensitive data. Call \`sanitize_text\` before transmitting to any external LLM model.\n`;
1137
+ } else {
1138
+ report += `> [!NOTE]\n> **Status:** Clean. Zero sensitive entities detected under the \`${targetProfile}\` profile.\n`;
1139
+ }
1140
+
1141
+ return {
1142
+ content: [{ type: "text", text: report }]
1143
+ };
1144
+ }
1145
+
1146
+ if (name === "sanitize_text" || name === "scrub_text") {
1147
+ const { text, profile = "General", ignore_list, compact = false, session_id = "default" } = args || {};
1148
+ const currentSession = getSessionMap(session_id);
714
1149
 
715
1150
  // Merge per-call ignore_list into persistent sessionIgnoreList
716
1151
  if (Array.isArray(ignore_list)) {
@@ -760,7 +1195,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
760
1195
 
761
1196
  const { processedText, wasTruncated } = truncateIfFree(text, license.isPro, charLimit);
762
1197
 
763
- const { scrubbedText, newTokens } = performSanitization(processedText, finalProfile, sessionIgnoreList);
1198
+ const { scrubbedText, newTokens } = performSanitization(processedText, finalProfile, sessionIgnoreList, currentSession);
764
1199
 
765
1200
  const telemetry = buildCisoAuditTelemetry(newTokens);
766
1201
  const receiptMd = formatAuditReceipt(telemetry, compact, wasTruncated, charLimit);
@@ -773,7 +1208,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
773
1208
  }
774
1209
 
775
1210
  if (name === "reveal_text") {
776
- const { text } = args || {};
1211
+ const { text, session_id = "default" } = args || {};
777
1212
  if (text === undefined || text === null) {
778
1213
  return {
779
1214
  isError: true,
@@ -787,17 +1222,18 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
787
1222
  };
788
1223
  }
789
1224
 
1225
+ const currentSession = getSessionMap(session_id);
790
1226
  // Warn early if session has no tokens โ€” reveal would be a no-op
791
- if (Object.keys(sessionMap).length === 0) {
1227
+ if (Object.keys(currentSession).length === 0) {
792
1228
  return {
793
1229
  content: [{
794
1230
  type: "text",
795
- text: `โš ๏ธ Session map is empty โ€” no tokens to restore. Call 'sanitize_text' or 'sanitize_file' first to build the token map, then pass the AI's response here.`
1231
+ text: `โš ๏ธ Session map${session_id !== 'default' ? ` ('${session_id}')` : ''} is empty โ€” no tokens to restore. Call 'sanitize_text' or 'sanitize_file' first to build the token map, then pass the AI's response here.`
796
1232
  }]
797
1233
  };
798
1234
  }
799
1235
 
800
- const restored = PrivacyScrubberCore.unscrubText(text, sessionMap);
1236
+ const restored = PrivacyScrubberCore.unscrubText(text, currentSession);
801
1237
  let restoredText = restored.restoredText;
802
1238
  // Deduplicate common double-prefixed schemas resulting from LLM prefix reconstruction
803
1239
  restoredText = restoredText.replace(/\b(mysql|postgresql|postgres|redis|mongodb|https?|ftp|ssh|git|aws):\/\/\1:\/\//gi, '$1://');
@@ -844,7 +1280,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
844
1280
  };
845
1281
  }
846
1282
 
847
- if (name === "sanitize_file") {
1283
+ if (name === "sanitize_file" || name === "scrub_file") {
848
1284
  const rawPath = args.file_path || args.filePath;
849
1285
  if (!rawPath) {
850
1286
  return {
@@ -1234,8 +1670,8 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1234
1670
  const tier = license.isPro ? 'PRO' : 'FREE';
1235
1671
  const tierIcon = license.isPro ? 'โœ…' : '๐Ÿ”“';
1236
1672
  const profileList = license.isPro
1237
- ? 'All 25 profiles active (General, Dev, Medical, Legal, Finance, HRโ€ฆ)'
1238
- : 'General only โ€” PRO unlocks 25 industry profiles';
1673
+ ? 'All 30 specialized profiles active (General, Dev, Medical, Legal, Finance, HRโ€ฆ)'
1674
+ : 'General only โ€” PRO unlocks all 30 specialized industry profiles';
1239
1675
  const sizeLimit = license.isPro ? 'Unlimited' : '15,000 characters per request';
1240
1676
 
1241
1677
  const configPath = resolveConfigPath();
@@ -1492,7 +1928,7 @@ ${telemetry.frameworksList.map(f => `- **${f}**`).join('\n')}
1492
1928
  }
1493
1929
 
1494
1930
  if (name === "guard_exec") {
1495
- const { command, cwd, profile = "Dev", timeout_ms = 15000 } = args || {};
1931
+ const { command, cwd, profile = "Dev", timeout_ms = 15000, session_id = "default" } = args || {};
1496
1932
  if (!command || typeof command !== "string") {
1497
1933
  return {
1498
1934
  isError: true,
@@ -1508,13 +1944,17 @@ ${telemetry.frameworksList.map(f => `- **${f}**`).join('\n')}
1508
1944
  };
1509
1945
  }
1510
1946
 
1947
+ const targetSession = getSessionMap(session_id);
1948
+ // Transparent in-memory unmasking of any token placeholders before local execution
1949
+ const rawExecutableCommand = reverseTokens(command, targetSession);
1950
+
1511
1951
  const execCwd = cwd ? path.resolve(cwd) : process.cwd();
1512
1952
  let stdout = '';
1513
1953
  let stderr = '';
1514
1954
  let exitCode = 0;
1515
1955
 
1516
1956
  try {
1517
- stdout = execSync(command, {
1957
+ stdout = execSync(rawExecutableCommand, {
1518
1958
  cwd: execCwd,
1519
1959
  timeout: Math.min(Math.max(timeout_ms, 1000), 60000),
1520
1960
  maxBuffer: 10 * 1024 * 1024,
@@ -1534,9 +1974,9 @@ ${telemetry.frameworksList.map(f => `- **${f}**`).join('\n')}
1534
1974
  const { processedText: cleanStdout, wasTruncated: truncOut } = truncateIfFree(stdout, license.isPro, charLimit);
1535
1975
  const { processedText: cleanStderr, wasTruncated: truncErr } = truncateIfFree(stderr, license.isPro, charLimit);
1536
1976
 
1537
- const resCommand = performSanitization(command, targetProfile, sessionIgnoreList);
1538
- const resStdout = performSanitization(cleanStdout, targetProfile, sessionIgnoreList);
1539
- const resStderr = performSanitization(cleanStderr, targetProfile, sessionIgnoreList);
1977
+ const resCommand = performSanitization(command, targetProfile, sessionIgnoreList, targetSession);
1978
+ const resStdout = performSanitization(cleanStdout, targetProfile, sessionIgnoreList, targetSession);
1979
+ const resStderr = performSanitization(cleanStderr, targetProfile, sessionIgnoreList, targetSession);
1540
1980
 
1541
1981
  const allNewTokens = { ...resCommand.newTokens, ...resStdout.newTokens, ...resStderr.newTokens };
1542
1982
  const tokenCount = Object.keys(allNewTokens).length;
@@ -1670,7 +2110,7 @@ ${telemetry.frameworksList.map(f => `- **${f}**`).join('\n')}
1670
2110
  }
1671
2111
 
1672
2112
  if (name === "guard_apply_patch") {
1673
- const { file_path, content, create_backup = true } = args || {};
2113
+ const { file_path, content, create_backup = true, session_id = "default" } = args || {};
1674
2114
  if (!file_path || typeof file_path !== "string") {
1675
2115
  return {
1676
2116
  isError: true,
@@ -1685,7 +2125,8 @@ ${telemetry.frameworksList.map(f => `- **${f}**`).join('\n')}
1685
2125
  }
1686
2126
 
1687
2127
  const resolvedPath = path.resolve(process.cwd(), file_path);
1688
- const restored = PrivacyScrubberCore.unscrubText(content, sessionMap);
2128
+ const targetSession = getSessionMap(session_id);
2129
+ const restored = PrivacyScrubberCore.unscrubText(content, targetSession);
1689
2130
  const authenticContent = restored.restoredText;
1690
2131
 
1691
2132
  let backupCreated = false;
@@ -1785,6 +2226,377 @@ Before reading sensitive files, running terminal commands that may print credent
1785
2226
  };
1786
2227
  }
1787
2228
 
2229
+ if (name === "guard_unmask_args") {
2230
+ const targetArgs = (args?.tool_args !== undefined) ? args.tool_args : args?.args;
2231
+ const { session_id = "default" } = args || {};
2232
+ if (targetArgs === undefined || targetArgs === null) {
2233
+ return {
2234
+ isError: true,
2235
+ content: [{ type: "text", text: "Error: Missing required parameter 'tool_args' (or 'args')." }]
2236
+ };
2237
+ }
2238
+
2239
+ const targetSession = getSessionMap(session_id);
2240
+
2241
+ function unmaskRecursive(val) {
2242
+ if (typeof val === 'string') {
2243
+ return reverseTokens(val, targetSession);
2244
+ }
2245
+ if (Array.isArray(val)) {
2246
+ return val.map(unmaskRecursive);
2247
+ }
2248
+ if (val !== null && typeof val === 'object') {
2249
+ const res = {};
2250
+ for (const [k, v] of Object.entries(val)) {
2251
+ res[k] = unmaskRecursive(v);
2252
+ }
2253
+ return res;
2254
+ }
2255
+ return val;
2256
+ }
2257
+
2258
+ const unmasked = unmaskRecursive(targetArgs);
2259
+ const originalStr = typeof targetArgs === 'string' ? targetArgs : JSON.stringify(targetArgs);
2260
+
2261
+ let tokensRestored = 0;
2262
+ for (const token of Object.keys(targetSession)) {
2263
+ if (originalStr.includes(token)) {
2264
+ tokensRestored += (originalStr.split(token).length - 1);
2265
+ }
2266
+ }
2267
+
2268
+ return {
2269
+ content: [{
2270
+ type: "text",
2271
+ text: JSON.stringify({
2272
+ status: "success",
2273
+ success: true,
2274
+ session_id,
2275
+ tokens_restored: tokensRestored,
2276
+ unmasked_count: tokensRestored,
2277
+ unmasked_args: unmasked
2278
+ }, null, 2)
2279
+ }]
2280
+ };
2281
+ }
2282
+
2283
+ if (name === "guard_session_info") {
2284
+ const { session_id } = args || {};
2285
+ const memUsage = process.memoryUsage();
2286
+
2287
+ if (session_id) {
2288
+ const sid = session_id.toString().trim();
2289
+ const session = agentSessions.get(sid);
2290
+ if (!session) {
2291
+ return {
2292
+ content: [{
2293
+ type: "text",
2294
+ text: `[Zero-Trust Agentic Guard: Session Info]\nSession '${sid}' does not exist or has expired.`
2295
+ }]
2296
+ };
2297
+ }
2298
+ const tokenEntries = Object.entries(session.map);
2299
+ const telemetry = buildCisoAuditTelemetry(session.map);
2300
+ return {
2301
+ content: [{
2302
+ type: "text",
2303
+ text: `[Zero-Trust Agentic Guard: Session Info - ${sid}]\n` +
2304
+ `Created: ${new Date(session.createdAt).toISOString()}\n` +
2305
+ `Last Active: ${new Date(session.lastActive).toISOString()}\n` +
2306
+ `Total Tokens Stored in RAM: ${tokenEntries.length}\n` +
2307
+ `Entity Types: ${Object.keys(telemetry.entities).join(', ') || 'None'}\n` +
2308
+ `Risk Level: ${telemetry.riskLevel}\n` +
2309
+ `Status: Volatile in-memory mapping active (0 bytes disk/network).`
2310
+ }]
2311
+ };
2312
+ }
2313
+
2314
+ let totalTokens = 0;
2315
+ const sessionList = [];
2316
+ for (const [sid, sess] of agentSessions.entries()) {
2317
+ const count = Object.keys(sess.map).length;
2318
+ totalTokens += count;
2319
+ sessionList.push({ id: sid, tokens: count, lastActive: new Date(sess.lastActive).toISOString() });
2320
+ }
2321
+
2322
+ return {
2323
+ content: [{
2324
+ type: "text",
2325
+ text: `[Zero-Trust Agentic Guard: Global Session Diagnostics]\n` +
2326
+ `Total Active Agent Sessions: ${agentSessions.size}\n` +
2327
+ `Total Tokens in Memory: ${totalTokens}\n` +
2328
+ `Process Heap Used: ${(memUsage.heapUsed / (1024 * 1024)).toFixed(2)} MB\n` +
2329
+ `Uptime: ${Math.floor(process.uptime())} seconds\n` +
2330
+ `Active Sessions: ${sessionList.map(s => `${s.id} (${s.tokens} tokens)`).join(', ') || 'None'}`
2331
+ }]
2332
+ };
2333
+ }
2334
+
2335
+ if (name === "guard_session_reset") {
2336
+ const { session_id, all = false, reset_all = false } = args || {};
2337
+ if (all || reset_all) {
2338
+ const count = agentSessions.size;
2339
+ agentSessions.clear();
2340
+ getSessionMap('default');
2341
+ sessionIgnoreList.clear();
2342
+ return {
2343
+ content: [{
2344
+ type: "text",
2345
+ text: `[Zero-Trust Agentic Guard] All ${count} active in-memory agent sessions have been completely purged from RAM.`
2346
+ }]
2347
+ };
2348
+ }
2349
+
2350
+ const sid = (session_id || 'default').toString().trim();
2351
+ const existed = agentSessions.delete(sid);
2352
+ if (sid === 'default') {
2353
+ getSessionMap('default');
2354
+ }
2355
+ return {
2356
+ content: [{
2357
+ type: "text",
2358
+ text: existed
2359
+ ? `[Zero-Trust Agentic Guard] Volatile RAM session '${sid}' successfully purged.`
2360
+ : `[Zero-Trust Agentic Guard] Session '${sid}' was not found.`
2361
+ }]
2362
+ };
2363
+ }
2364
+
2365
+ if (name === "guard_rag_chunk") {
2366
+ const { chunks, profile = "General", session_id = "default" } = args || {};
2367
+ if (!Array.isArray(chunks)) {
2368
+ return {
2369
+ isError: true,
2370
+ content: [{ type: "text", text: "Error: Missing or invalid parameter 'chunks'. Must be an array of chunk objects or strings." }]
2371
+ };
2372
+ }
2373
+
2374
+ const targetSession = getSessionMap(session_id);
2375
+ let totalTokensRedacted = 0;
2376
+ const sanitizedChunks = [];
2377
+
2378
+ for (let i = 0; i < chunks.length; i++) {
2379
+ const item = chunks[i];
2380
+ if (typeof item === 'string') {
2381
+ const res = performSanitization(item, profile, null, targetSession);
2382
+ totalTokensRedacted += Object.keys(res.newTokens || {}).length;
2383
+ sanitizedChunks.push(res.scrubbedText);
2384
+ } else if (item && typeof item === 'object') {
2385
+ const copy = { ...item };
2386
+ const rawText = copy.text || copy.pageContent || copy.document || '';
2387
+ const res = performSanitization(rawText, profile, null, targetSession);
2388
+ totalTokensRedacted += Object.keys(res.newTokens || {}).length;
2389
+
2390
+ if (copy.text !== undefined) copy.text = res.scrubbedText;
2391
+ if (copy.pageContent !== undefined) copy.pageContent = res.scrubbedText;
2392
+ if (copy.document !== undefined) copy.document = res.scrubbedText;
2393
+ if (copy.text === undefined && copy.pageContent === undefined && copy.document === undefined) {
2394
+ copy.text = res.scrubbedText;
2395
+ }
2396
+
2397
+ copy.metadata = {
2398
+ ...(copy.metadata || {}),
2399
+ _ztds_sanitized: true,
2400
+ _ztds_tokens_masked: Object.keys(res.newTokens || {}).length
2401
+ };
2402
+ sanitizedChunks.push(copy);
2403
+ } else {
2404
+ sanitizedChunks.push(item);
2405
+ }
2406
+ }
2407
+
2408
+ const telemetry = buildCisoAuditTelemetry(targetSession);
2409
+ const auditReceipt = formatAuditReceipt(telemetry);
2410
+
2411
+ return {
2412
+ content: [{
2413
+ type: "text",
2414
+ text: JSON.stringify({
2415
+ status: "success",
2416
+ session_id,
2417
+ total_chunks: chunks.length,
2418
+ tokens_redacted_in_batch: totalTokensRedacted,
2419
+ total_session_tokens: Object.keys(targetSession).length,
2420
+ risk_level: telemetry.riskLevel,
2421
+ sanitized_chunks: sanitizedChunks
2422
+ }, null, 2) + `\n\n${auditReceipt}`
2423
+ }]
2424
+ };
2425
+ }
2426
+
2427
+ if (name === "guard_rag_restore") {
2428
+ const { results, session_id = "default" } = args || {};
2429
+ if (!Array.isArray(results)) {
2430
+ return {
2431
+ isError: true,
2432
+ content: [{ type: "text", text: "Error: Missing or invalid parameter 'results'. Must be an array of search result objects or strings." }]
2433
+ };
2434
+ }
2435
+
2436
+ const targetSession = getSessionMap(session_id);
2437
+ let totalTokensRestored = 0;
2438
+ const restoredResults = [];
2439
+
2440
+ for (let i = 0; i < results.length; i++) {
2441
+ const item = results[i];
2442
+ if (typeof item === 'string') {
2443
+ const restored = reverseTokens(item, targetSession);
2444
+ for (const token of Object.keys(targetSession)) {
2445
+ if (item.includes(token)) totalTokensRestored++;
2446
+ }
2447
+ restoredResults.push(restored);
2448
+ } else if (item && typeof item === 'object') {
2449
+ const copy = { ...item };
2450
+ const rawText = copy.text || copy.pageContent || copy.document || '';
2451
+ const restored = reverseTokens(rawText, targetSession);
2452
+ for (const token of Object.keys(targetSession)) {
2453
+ if (rawText.includes(token)) totalTokensRestored++;
2454
+ }
2455
+
2456
+ if (copy.text !== undefined) copy.text = restored;
2457
+ if (copy.pageContent !== undefined) copy.pageContent = restored;
2458
+ if (copy.document !== undefined) copy.document = restored;
2459
+ if (copy.text === undefined && copy.pageContent === undefined && copy.document === undefined) {
2460
+ copy.text = restored;
2461
+ }
2462
+ restoredResults.push(copy);
2463
+ } else {
2464
+ restoredResults.push(item);
2465
+ }
2466
+ }
2467
+
2468
+ return {
2469
+ content: [{
2470
+ type: "text",
2471
+ text: JSON.stringify({
2472
+ status: "success",
2473
+ session_id,
2474
+ total_results: results.length,
2475
+ tokens_restored: totalTokensRestored,
2476
+ restored_results: restoredResults
2477
+ }, null, 2)
2478
+ }]
2479
+ };
2480
+ }
2481
+
2482
+ if (name === "export_audit_log") {
2483
+ const {
2484
+ format = "jsonl",
2485
+ session_id = "default",
2486
+ destination_file,
2487
+ company_name = "PrivacyScrubber Client",
2488
+ department = "SecOps / Compliance",
2489
+ text
2490
+ } = args || {};
2491
+
2492
+ const targetSession = getSessionMap(session_id);
2493
+
2494
+ // If text is provided, perform sanitization first to ensure session and telemetry are populated
2495
+ if (typeof text === 'string' && text.trim()) {
2496
+ performSanitization(text, "general", null, targetSession);
2497
+ }
2498
+
2499
+ const telemetry = buildCisoAuditTelemetry(targetSession);
2500
+
2501
+ const sessionHash = crypto.createHash('sha256')
2502
+ .update(JSON.stringify(targetSession) + Date.now().toString())
2503
+ .digest('hex');
2504
+
2505
+ const timestamp = new Date().toISOString();
2506
+
2507
+ let outputText = "";
2508
+ const eventData = {
2509
+ vendor: "PrivacyScrubber",
2510
+ product: "ZTDS",
2511
+ version: MCP_VERSION,
2512
+ timestamp,
2513
+ riskLevel: telemetry.riskLevel,
2514
+ tokensMasked: telemetry.totalCount,
2515
+ entities: telemetry.entities,
2516
+ frameworks: telemetry.frameworksList,
2517
+ sessionHash,
2518
+ organization: company_name,
2519
+ department
2520
+ };
2521
+
2522
+ if (format === "cef") {
2523
+ outputText = formatCefEvent(eventData);
2524
+ } else if (format === "syslog") {
2525
+ outputText = formatSyslogEvent(eventData);
2526
+ } else if (format === "json") {
2527
+ outputText = JSON.stringify({
2528
+ protocol: "Zero-Trust Data Sanitization (ZTDS)",
2529
+ certificate: `ZTDS-CERT-${sessionHash.substring(0, 16).toUpperCase()}`,
2530
+ company: company_name,
2531
+ department,
2532
+ timestamp,
2533
+ session_hash: sessionHash,
2534
+ verification_mode: "100% Offline (Local In-Memory RAM)",
2535
+ compliance_status: "VERIFIED PASS",
2536
+ risk_level: telemetry.riskLevel,
2537
+ frameworks_enforced: telemetry.frameworksList,
2538
+ total_masked_tokens: telemetry.totalCount,
2539
+ entities_breakdown: telemetry.entities,
2540
+ zero_egress_verified: true,
2541
+ verification_url: `https://privacyscrubber.com/features/audit-receipt/#verify?hash=${sessionHash.substring(0, 16)}`
2542
+ }, null, 2);
2543
+ } else if (format === "markdown") {
2544
+ const entitySummary = Object.entries(telemetry.entities)
2545
+ .map(([t, count]) => `[${t}]: ${count}`)
2546
+ .join(', ') || 'None (Clean)';
2547
+ outputText = `# ๐Ÿ›ก๏ธ Zero-Trust Data Sanitization Compliance Certificate
2548
+ **Certificate ID:** \`ZTDS-CERT-${sessionHash.substring(0, 16).toUpperCase()}\`
2549
+ **Organization:** ${company_name} (${department})
2550
+ **Timestamp:** ${timestamp}
2551
+ **Verification Mode:** 100% Local In-Memory Processing (Air-Gapped)
2552
+ **Status:** **VERIFIED PASS** (Zero Network Egress)
2553
+
2554
+ ---
2555
+
2556
+ ### ๐Ÿ“Š Sanitization Metrics & Risk Assessment
2557
+ * **Overall Risk Rating:** **${telemetry.riskLevel}**
2558
+ * **Total Sensitive Entities Masked:** \`${telemetry.totalCount}\`
2559
+ * **Entity Breakdown:** ${entitySummary}
2560
+ * **Network Data Transmitted:** \`0.00 KB (Zero-Trust Local RAM)\`
2561
+
2562
+ ### ๐Ÿ“œ Regulatory Frameworks Enforced
2563
+ ${telemetry.frameworksList.map(f => `- **${f}**`).join('\n')}
2564
+
2565
+ ### ๐Ÿ”’ CISO Compliance Declaration
2566
+ 1. **EU AI Act (Art. 50) & GDPR (Art. 25 & 32):** Data minimization and local pseudonymization enforced prior to model interaction.
2567
+ 2. **HIPAA Safe Harbor (ยง164.514) / SOC 2 Type II:** All direct and indirect identifiers sanitized locally without cloud processor liability.
2568
+ 3. **Cryptographic Verification:** Tamper-evident session verification hash: \`${sessionHash}\`
2569
+
2570
+ *Certified Offline by PrivacyScrubber Engine v${MCP_VERSION}*
2571
+ *Verify at: https://privacyscrubber.com/features/audit-receipt/*`;
2572
+ } else {
2573
+ // Default: jsonl
2574
+ outputText = formatJsonlEvent(eventData);
2575
+ }
2576
+
2577
+ if (typeof destination_file === 'string' && destination_file.trim()) {
2578
+ try {
2579
+ const dir = path.dirname(destination_file);
2580
+ if (!fs.existsSync(dir)) {
2581
+ fs.mkdirSync(dir, { recursive: true });
2582
+ }
2583
+ fs.appendFileSync(destination_file, outputText + '\n', 'utf8');
2584
+ } catch (e) {
2585
+ return {
2586
+ isError: true,
2587
+ content: [{ type: "text", text: `Error writing audit log to destination_file: ${e.message}` }]
2588
+ };
2589
+ }
2590
+ }
2591
+
2592
+ return {
2593
+ content: [{
2594
+ type: "text",
2595
+ text: outputText
2596
+ }]
2597
+ };
2598
+ }
2599
+
1788
2600
  return {
1789
2601
  isError: true,
1790
2602
  content: [{ type: "text", text: `Unknown tool: ${name}` }]
@@ -1878,17 +2690,18 @@ function detectSecrets(text) {
1878
2690
  return detected;
1879
2691
  }
1880
2692
 
1881
- function performSanitization(text, profile, ignoreList = null) {
2693
+ function performSanitization(text, profile, ignoreList = null, customSessionMap = null) {
1882
2694
  const customRules = loadCustomRules();
1883
2695
  const normalizedProfile = (profile || "general").trim().toLowerCase();
1884
2696
  const license = checkLicenseStatus();
1885
- const result = PrivacyScrubberCore.scrubText(text, customRules, {}, normalizedProfile, sessionMap, license.isPro, ignoreList);
2697
+ const targetMap = customSessionMap || sessionMap;
2698
+ const result = PrivacyScrubberCore.scrubText(text, customRules, {}, normalizedProfile, targetMap, license.isPro, ignoreList);
1886
2699
 
1887
2700
  const newTokens = {};
1888
2701
  // Update our volatile map with new matches
1889
2702
  if (result.tokenMap) {
1890
2703
  Object.entries(result.tokenMap).forEach(([token, original]) => {
1891
- sessionMap[token] = original;
2704
+ targetMap[token] = original;
1892
2705
  newTokens[token] = original;
1893
2706
  });
1894
2707
  }