@privacyscrubber/mcp-server 2.4.5 → 2.4.6

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
@@ -82,6 +82,9 @@ if (rawCliArgs.includes('--memo') || rawCliArgs.includes('memo')) {
82
82
  console.log(generateProcurementMemo({ targetTier: tierArg, format: 'text' }));
83
83
  process.exit(0);
84
84
  }
85
+ if (rawCliArgs.includes('--hook')) {
86
+ await import('./pii-masking-run.js');
87
+ }
85
88
  if (rawCliArgs.some(arg => ['init', 'install', '--install', 'setup', '--setup', 'uninstall', '--uninstall', '--remove'].includes(arg))) {
86
89
  const { runInstaller } = await import('./install.js');
87
90
  const isRemove = rawCliArgs.some(arg => ['uninstall', '--uninstall', '--remove'].includes(arg));
@@ -1364,6 +1367,15 @@ We request approval to acquire a **PrivacyScrubber TEAMS License** (\$99/month f
1364
1367
  `;
1365
1368
  }
1366
1369
 
1370
+ const MCP_SYSTEM_INSTRUCTIONS = [
1371
+ "Zero-Trust Data Sanitization (ZTDS) Protocol Active (100% local RAM, 0 network egress).",
1372
+ "AUTOMATIC INTERCEPTION RULES FOR AI AGENTS:",
1373
+ "1. FILE READS: Before reading .env, configs, logs, database dumps, or documents (DOCX/PDF/XLSX/CSV/JSON), call 'guard_read_file' (or 'sanitize_file') instead of native file read tools.",
1374
+ "2. SHELL & GIT: Route terminal commands that may output secrets or PII through 'guard_exec', and inspect diffs via 'guard_git_diff'.",
1375
+ "3. RAW TEXT & RAG: Sanitize raw user PII/secrets in prompts via 'sanitize_text' (or 'guard_rag_chunk' for vector chunks) before external reasoning.",
1376
+ "4. LOCAL RESTORATION: Keep placeholders like [EMAIL_1], [API_KEY_1], [NAME_1] intact during reasoning. Call 'reveal_text' to restore final answers, 'guard_apply_patch' to write unmasked code/configs to disk, or 'guard_unmask_args' for local tool calls."
1377
+ ].join("\n");
1378
+
1367
1379
  // Create the MCP server
1368
1380
  const server = new Server(
1369
1381
  {
@@ -1377,6 +1389,7 @@ const server = new Server(
1377
1389
  logging: {},
1378
1390
  resources: {},
1379
1391
  },
1392
+ instructions: MCP_SYSTEM_INSTRUCTIONS,
1380
1393
  }
1381
1394
  );
1382
1395
 
@@ -1387,6 +1400,13 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
1387
1400
  {
1388
1401
  name: "audit_context",
1389
1402
  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.",
1403
+ annotations: {
1404
+ title: "Audit Context for PII & Secrets",
1405
+ readOnlyHint: true,
1406
+ destructiveHint: false,
1407
+ idempotentHint: true,
1408
+ openWorldHint: false
1409
+ },
1390
1410
  inputSchema: {
1391
1411
  type: "object",
1392
1412
  properties: {
@@ -1414,6 +1434,13 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
1414
1434
  {
1415
1435
  name: "sanitize_text",
1416
1436
  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. Supports General and all 30 specialized industry profiles in RAM session map. (For in-code backend services or RAG vector pipelines outside of MCP, use '@privacyscrubber/sdk': npm i @privacyscrubber/sdk)",
1437
+ annotations: {
1438
+ title: "Sanitize Text in Local RAM",
1439
+ readOnlyHint: true,
1440
+ destructiveHint: false,
1441
+ idempotentHint: true,
1442
+ openWorldHint: false
1443
+ },
1417
1444
  inputSchema: {
1418
1445
  type: "object",
1419
1446
  properties: {
@@ -1446,44 +1473,16 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
1446
1473
  required: ["text"]
1447
1474
  }
1448
1475
  },
1449
- {
1450
- name: "scrub_text",
1451
- description: "Alias for 'sanitize_text'. Locally scrubs PII, secrets, and credentials from prompts or text before LLM ingestion. Supports General and all 30 specialized industry profiles in RAM session map.",
1452
- inputSchema: {
1453
- type: "object",
1454
- properties: {
1455
- text: {
1456
- type: "string",
1457
- description: "The raw text, code, or logs to sanitize."
1458
- },
1459
- profile: {
1460
- type: "string",
1461
- description: PROFILE_PARAM_DESCRIPTION
1462
- },
1463
- sector: {
1464
- type: "string",
1465
- description: SECTOR_PARAM_DESCRIPTION
1466
- },
1467
- compact: {
1468
- type: "boolean",
1469
- description: "Optional. Compact 1-line audit summary."
1470
- },
1471
- ignore_list: {
1472
- type: "array",
1473
- items: { type: "string" },
1474
- description: "Optional list of plaintext values to skip during detection."
1475
- },
1476
- session_id: {
1477
- type: "string",
1478
- description: "Optional session ID for multi-agent isolation. Defaults to 'default'."
1479
- }
1480
- },
1481
- required: ["text"]
1482
- }
1483
- },
1484
1476
  {
1485
1477
  name: "reveal_text",
1486
1478
  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. Supports both standard batch mode and real-time streaming mode (stream: true).",
1479
+ annotations: {
1480
+ title: "Restore Tokens in Local RAM",
1481
+ readOnlyHint: true,
1482
+ destructiveHint: false,
1483
+ idempotentHint: true,
1484
+ openWorldHint: false
1485
+ },
1487
1486
  inputSchema: {
1488
1487
  type: "object",
1489
1488
  properties: {
@@ -1510,6 +1509,13 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
1510
1509
  {
1511
1510
  name: "stream_reveal_text",
1512
1511
  description: "STEP 3 (Streaming): Real-time streaming token detokenizer for LLM Server-Sent Events (SSE) and stdio chunk pipelines. Buffers split token candidates across chunk boundaries (e.g. '[EM' + 'AIL_1]') so no characters or tokens are corrupted. Pass 'flush: true' on the final chunk.",
1512
+ annotations: {
1513
+ title: "Streaming Token Detokenizer",
1514
+ readOnlyHint: true,
1515
+ destructiveHint: false,
1516
+ idempotentHint: false,
1517
+ openWorldHint: false
1518
+ },
1513
1519
  inputSchema: {
1514
1520
  type: "object",
1515
1521
  properties: {
@@ -1535,43 +1541,20 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
1535
1541
  },
1536
1542
  {
1537
1543
  name: "sanitize_file",
1538
- description: "Reads a local file, sanitizes its contents using the selected profile, and outputs the safe version for AI analysis. Securely keeps original identifiers in memory.",
1539
- inputSchema: {
1540
- type: "object",
1541
- properties: {
1542
- file_path: {
1543
- type: "string",
1544
- description: "Absolute path to the file to sanitize."
1545
- },
1546
- profile: {
1547
- type: "string",
1548
- description: PROFILE_PARAM_DESCRIPTION
1549
- },
1550
- sector: {
1551
- type: "string",
1552
- description: SECTOR_PARAM_DESCRIPTION
1553
- },
1554
- compact: {
1555
- type: "boolean",
1556
- description: "Optional. Compact 1-line audit summary."
1557
- },
1558
- session_id: {
1559
- type: "string",
1560
- description: "Optional session ID for multi-agent isolation. Defaults to 'default'."
1561
- }
1562
- },
1563
- required: ["file_path"]
1564
- }
1565
- },
1566
- {
1567
- name: "scrub_file",
1568
- description: "Alias for 'sanitize_file'. Reads and sanitizes a local file.",
1544
+ description: "Reads a local file (including plain text, source code, .eml, .rtf, .docx, .pdf, .xlsx), sanitizes its contents using the selected profile, and outputs the safe version for AI analysis. Securely keeps original identifiers in memory.",
1545
+ annotations: {
1546
+ title: "Sanitize Local File in RAM",
1547
+ readOnlyHint: true,
1548
+ destructiveHint: false,
1549
+ idempotentHint: true,
1550
+ openWorldHint: false
1551
+ },
1569
1552
  inputSchema: {
1570
1553
  type: "object",
1571
1554
  properties: {
1572
1555
  file_path: {
1573
1556
  type: "string",
1574
- description: "Absolute path to the file to sanitize."
1557
+ description: "Absolute or workspace-relative path to the file to sanitize."
1575
1558
  },
1576
1559
  profile: {
1577
1560
  type: "string",
@@ -1596,6 +1579,13 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
1596
1579
  {
1597
1580
  name: "audit_directory_for_pii",
1598
1581
  description: "Scans a local directory for leaks of secrets, keys, and PII. Returns a summary report. Use this tool for Security Auditing.",
1582
+ annotations: {
1583
+ title: "Audit Directory for PII & Secrets",
1584
+ readOnlyHint: true,
1585
+ destructiveHint: false,
1586
+ idempotentHint: true,
1587
+ openWorldHint: false
1588
+ },
1599
1589
  inputSchema: {
1600
1590
  type: "object",
1601
1591
  properties: {
@@ -1627,6 +1617,13 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
1627
1617
  {
1628
1618
  name: "redact_file",
1629
1619
  description: "Action/Redact: In-place redaction of a local file. Replaces PII and secrets with tokens and saves the file. By default, creates a .bak backup. Use dry_run=true to test without modifying.",
1620
+ annotations: {
1621
+ title: "In-Place File Redaction",
1622
+ readOnlyHint: false,
1623
+ destructiveHint: true,
1624
+ idempotentHint: false,
1625
+ openWorldHint: false
1626
+ },
1630
1627
  inputSchema: {
1631
1628
  type: "object",
1632
1629
  properties: {
@@ -1657,6 +1654,13 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
1657
1654
  {
1658
1655
  name: "create_default_config",
1659
1656
  description: "Creates a default 'privacyscrubber.json' configuration file in the active workspace root directory if one does not exist. Includes template structures for custom regex rules and exclusion bypass patterns.",
1657
+ annotations: {
1658
+ title: "Create Default Workspace Config",
1659
+ readOnlyHint: false,
1660
+ destructiveHint: false,
1661
+ idempotentHint: true,
1662
+ openWorldHint: false
1663
+ },
1660
1664
  inputSchema: {
1661
1665
  type: "object",
1662
1666
  properties: {},
@@ -1666,6 +1670,13 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
1666
1670
  {
1667
1671
  name: "generate_compliance_report",
1668
1672
  description: "Generates an official Zero-Trust Compliance Audit Certificate (GDPR, HIPAA, EU AI Act, SOC 2) for the current MCP session. Returns cryptographic session hash, masked entities breakdown, and compliance certification.",
1673
+ annotations: {
1674
+ title: "Generate Compliance Audit Certificate",
1675
+ readOnlyHint: true,
1676
+ destructiveHint: false,
1677
+ idempotentHint: true,
1678
+ openWorldHint: false
1679
+ },
1669
1680
  inputSchema: {
1670
1681
  type: "object",
1671
1682
  properties: {
@@ -1693,6 +1704,13 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
1693
1704
  {
1694
1705
  name: "mark_false_positive",
1695
1706
  description: "Marks a previously detected token as a false positive. The original plaintext value will be excluded from all future sanitize_text calls in this session. Returns the restored original value and updated ignore list size.",
1707
+ annotations: {
1708
+ title: "Mark Token as False Positive",
1709
+ readOnlyHint: true,
1710
+ destructiveHint: false,
1711
+ idempotentHint: true,
1712
+ openWorldHint: false
1713
+ },
1696
1714
  inputSchema: {
1697
1715
  type: "object",
1698
1716
  properties: {
@@ -1711,6 +1729,13 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
1711
1729
  {
1712
1730
  name: "check_status",
1713
1731
  description: "Returns the current PrivacyScrubber MCP tier, session usage, available profiles, and PRO upgrade instructions. Call this to see your license status or get setup help.",
1732
+ annotations: {
1733
+ title: "Check MCP License & Session Status",
1734
+ readOnlyHint: true,
1735
+ destructiveHint: false,
1736
+ idempotentHint: true,
1737
+ openWorldHint: false
1738
+ },
1714
1739
  inputSchema: {
1715
1740
  type: "object",
1716
1741
  properties: {},
@@ -1720,6 +1745,13 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
1720
1745
  {
1721
1746
  name: "guard_exec",
1722
1747
  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.",
1748
+ annotations: {
1749
+ title: "Guard Shell Command Execution",
1750
+ readOnlyHint: false,
1751
+ destructiveHint: true,
1752
+ idempotentHint: false,
1753
+ openWorldHint: false
1754
+ },
1723
1755
  inputSchema: {
1724
1756
  type: "object",
1725
1757
  properties: {
@@ -1742,6 +1774,10 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
1742
1774
  timeout_ms: {
1743
1775
  type: "number",
1744
1776
  description: "Execution timeout in milliseconds. Defaults to 15000 (15s)."
1777
+ },
1778
+ session_id: {
1779
+ type: "string",
1780
+ description: "Optional session ID for multi-agent isolation. Defaults to 'default'."
1745
1781
  }
1746
1782
  },
1747
1783
  required: ["command"]
@@ -1749,7 +1785,14 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
1749
1785
  },
1750
1786
  {
1751
1787
  name: "guard_read_file",
1752
- 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.",
1788
+ description: "Zero-Trust Agentic Guard: Preferred over native file read tools. Reads any sensitive local file (.env, config, credentials, source code, database dumps, .docx, .pdf, .xlsx, .eml, .rtf), sanitizes all credentials and PII in volatile RAM, and returns safe tokenized content for AI agent context.",
1789
+ annotations: {
1790
+ title: "Guard Local File Read",
1791
+ readOnlyHint: true,
1792
+ destructiveHint: false,
1793
+ idempotentHint: true,
1794
+ openWorldHint: false
1795
+ },
1753
1796
  inputSchema: {
1754
1797
  type: "object",
1755
1798
  properties: {
@@ -1768,6 +1811,10 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
1768
1811
  max_lines: {
1769
1812
  type: "number",
1770
1813
  description: "Optional line cap for large files. Defaults to 500."
1814
+ },
1815
+ session_id: {
1816
+ type: "string",
1817
+ description: "Optional session ID for multi-agent isolation. Defaults to 'default'."
1771
1818
  }
1772
1819
  },
1773
1820
  required: ["file_path"]
@@ -1776,6 +1823,13 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
1776
1823
  {
1777
1824
  name: "guard_git_diff",
1778
1825
  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.",
1826
+ annotations: {
1827
+ title: "Guard Git Diff Inspection",
1828
+ readOnlyHint: true,
1829
+ destructiveHint: false,
1830
+ idempotentHint: true,
1831
+ openWorldHint: false
1832
+ },
1779
1833
  inputSchema: {
1780
1834
  type: "object",
1781
1835
  properties: {
@@ -1806,6 +1860,13 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
1806
1860
  {
1807
1861
  name: "guard_apply_patch",
1808
1862
  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.",
1863
+ annotations: {
1864
+ title: "Guard Apply Unmasked Patch to Disk",
1865
+ readOnlyHint: false,
1866
+ destructiveHint: false,
1867
+ idempotentHint: true,
1868
+ openWorldHint: false
1869
+ },
1809
1870
  inputSchema: {
1810
1871
  type: "object",
1811
1872
  properties: {
@@ -1820,6 +1881,10 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
1820
1881
  create_backup: {
1821
1882
  type: "boolean",
1822
1883
  description: "Whether to create a .bak backup if target file already exists. Defaults to true."
1884
+ },
1885
+ session_id: {
1886
+ type: "string",
1887
+ description: "Optional session ID for multi-agent isolation. Defaults to 'default'."
1823
1888
  }
1824
1889
  },
1825
1890
  required: ["file_path", "content"]
@@ -1827,7 +1892,14 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
1827
1892
  },
1828
1893
  {
1829
1894
  name: "create_agent_rules",
1830
- 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.",
1895
+ description: "Zero-Trust Agentic Guard: Scaffolds CISO-grade AI Agent Rules in workspace (.cursor/rules/privacyscrubber.mdc, .cursorrules, .windsurfrules, CLAUDE.md, AGENTS.md, .github/copilot-instructions.md, .clinerules). Guides autonomous agents to route all file reads, git diffs, and shell commands through PrivacyScrubber Guard tools.",
1896
+ annotations: {
1897
+ title: "Scaffold AI Agent Security Rules",
1898
+ readOnlyHint: false,
1899
+ destructiveHint: false,
1900
+ idempotentHint: true,
1901
+ openWorldHint: false
1902
+ },
1831
1903
  inputSchema: {
1832
1904
  type: "object",
1833
1905
  properties: {
@@ -1835,7 +1907,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
1835
1907
  type: "array",
1836
1908
  items: {
1837
1909
  type: "string",
1838
- enum: ["all", "cursor", "windsurf", "claude_code", "copilot", "cline"]
1910
+ enum: ["all", "cursor", "windsurf", "claude_code", "agents_md", "codex", "copilot", "cline"]
1839
1911
  },
1840
1912
  description: "List of agent targets to generate rules for. Use ['all'] to generate rules for all supported AI agents."
1841
1913
  },
@@ -1850,6 +1922,13 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
1850
1922
  {
1851
1923
  name: "guard_unmask_args",
1852
1924
  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.",
1925
+ annotations: {
1926
+ title: "Unmask Tool Arguments in Local RAM",
1927
+ readOnlyHint: true,
1928
+ destructiveHint: false,
1929
+ idempotentHint: true,
1930
+ openWorldHint: false
1931
+ },
1853
1932
  inputSchema: {
1854
1933
  type: "object",
1855
1934
  properties: {
@@ -1867,6 +1946,13 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
1867
1946
  {
1868
1947
  name: "guard_session_info",
1869
1948
  description: "Zero-Trust Agentic Guard: Returns diagnostic metrics on active in-memory sessions, token counts, and memory footprint with zero plaintext leakage.",
1949
+ annotations: {
1950
+ title: "Inspect RAM Session Diagnostics",
1951
+ readOnlyHint: true,
1952
+ destructiveHint: false,
1953
+ idempotentHint: true,
1954
+ openWorldHint: false
1955
+ },
1870
1956
  inputSchema: {
1871
1957
  type: "object",
1872
1958
  properties: {
@@ -1881,6 +1967,13 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
1881
1967
  {
1882
1968
  name: "guard_session_reset",
1883
1969
  description: "Zero-Trust Agentic Guard: Explicitly purges the in-memory token map for a specific session_id or all sessions upon agent task completion.",
1970
+ annotations: {
1971
+ title: "Reset RAM Session Token Map",
1972
+ readOnlyHint: true,
1973
+ destructiveHint: false,
1974
+ idempotentHint: true,
1975
+ openWorldHint: false
1976
+ },
1884
1977
  inputSchema: {
1885
1978
  type: "object",
1886
1979
  properties: {
@@ -1899,6 +1992,13 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
1899
1992
  {
1900
1993
  name: "guard_rag_chunk",
1901
1994
  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.",
1995
+ annotations: {
1996
+ title: "Sanitize RAG Document Chunks",
1997
+ readOnlyHint: true,
1998
+ destructiveHint: false,
1999
+ idempotentHint: true,
2000
+ openWorldHint: false
2001
+ },
1902
2002
  inputSchema: {
1903
2003
  type: "object",
1904
2004
  properties: {
@@ -1939,6 +2039,13 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
1939
2039
  {
1940
2040
  name: "guard_rag_restore",
1941
2041
  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.",
2042
+ annotations: {
2043
+ title: "Restore RAG Vector Search Results",
2044
+ readOnlyHint: true,
2045
+ destructiveHint: false,
2046
+ idempotentHint: true,
2047
+ openWorldHint: false
2048
+ },
1942
2049
  inputSchema: {
1943
2050
  type: "object",
1944
2051
  properties: {
@@ -1972,6 +2079,13 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
1972
2079
  {
1973
2080
  name: "export_audit_log",
1974
2081
  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.",
2082
+ annotations: {
2083
+ title: "Export SIEM Compliance Audit Log",
2084
+ readOnlyHint: false,
2085
+ destructiveHint: false,
2086
+ idempotentHint: true,
2087
+ openWorldHint: false
2088
+ },
1975
2089
  inputSchema: {
1976
2090
  type: "object",
1977
2091
  properties: {
@@ -2006,6 +2120,13 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
2006
2120
  {
2007
2121
  name: "generate_procurement_memo",
2008
2122
  description: "Zero-Trust Enterprise Procurement: Generates an executive-ready Procurement & ROI Memorandum for Chief Information Security Officers (CISO), Data Protection Officers (DPO), and VP of Engineering. Compares TCO of traditional Cloud DLP ($1.5k–$4k/mo) vs PrivacyScrubber TEAMS ($99/mo flat) or Developer SDK ($299/mo flat), citing statutory frameworks (GDPR Art. 4/28, HIPAA §164.514, SOC 2 Type II, PCI DSS v4.0, NIST SP 800-53, EU AI Act Art. 50).",
2123
+ annotations: {
2124
+ title: "Generate CISO Procurement & ROI Memo",
2125
+ readOnlyHint: true,
2126
+ destructiveHint: false,
2127
+ idempotentHint: true,
2128
+ openWorldHint: false
2129
+ },
2009
2130
  inputSchema: {
2010
2131
  type: "object",
2011
2132
  properties: {
@@ -2178,11 +2299,51 @@ server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
2178
2299
  throw new Error(`Resource not found: ${uri}`);
2179
2300
  });
2180
2301
 
2302
+ function getEffectiveWorkspaceRoots() {
2303
+ const roots = [];
2304
+ const addCandidate = (dir) => {
2305
+ if (!dir || typeof dir !== 'string') return;
2306
+ const trimmed = dir.trim();
2307
+ if (!trimmed) return;
2308
+ try {
2309
+ const resolved = fs.existsSync(trimmed) ? fs.realpathSync(trimmed) : path.resolve(trimmed);
2310
+ if (!roots.includes(resolved)) roots.push(resolved);
2311
+ } catch (_) {
2312
+ const fallback = path.resolve(trimmed);
2313
+ if (!roots.includes(fallback)) roots.push(fallback);
2314
+ }
2315
+ };
2316
+
2317
+ addCandidate(process.cwd());
2318
+ addCandidate(process.env.PRIVACYSCRUBBER_WORKSPACE);
2319
+ addCandidate(process.env.CURSOR_WORKSPACE_PATH);
2320
+ addCandidate(process.env.VSCODE_CWD);
2321
+ addCandidate(process.env.PROJECT_ROOT);
2322
+ addCandidate(process.env.INIT_CWD);
2323
+ if (process.env.WORKSPACE_FOLDER_PATHS) {
2324
+ for (const part of process.env.WORKSPACE_FOLDER_PATHS.split(path.delimiter)) {
2325
+ addCandidate(part);
2326
+ }
2327
+ }
2328
+ return roots;
2329
+ }
2330
+
2331
+ function isWithinAnyWorkspaceRoot(targetPath, roots) {
2332
+ for (const root of roots) {
2333
+ const rel = path.relative(root, targetPath);
2334
+ if (!rel.startsWith("..") && !path.isAbsolute(rel)) {
2335
+ return { ok: true, matchedRoot: root };
2336
+ }
2337
+ }
2338
+ return { ok: false, matchedRoot: roots[0] || process.cwd() };
2339
+ }
2340
+
2181
2341
  function resolveWorkspaceSafePath(inputPath, paramName = "file_path") {
2182
- const workspaceRoot = fs.existsSync(process.cwd()) ? fs.realpathSync(process.cwd()) : path.resolve(process.cwd());
2183
- const resolvedPath = path.resolve(workspaceRoot, inputPath);
2184
- const relToRoot = path.relative(workspaceRoot, resolvedPath);
2185
- if (relToRoot.startsWith("..") || path.isAbsolute(relToRoot)) {
2342
+ const roots = getEffectiveWorkspaceRoots();
2343
+ const primaryRoot = roots[0];
2344
+ const resolvedPath = path.resolve(primaryRoot, inputPath);
2345
+ const boundaryCheck = isWithinAnyWorkspaceRoot(resolvedPath, roots);
2346
+ if (!boundaryCheck.ok) {
2186
2347
  return {
2187
2348
  ok: false,
2188
2349
  error: `Error: Security policy violation — target '${paramName}' resolves outside the active workspace directory (path traversal blocked).`
@@ -2196,15 +2357,15 @@ function resolveWorkspaceSafePath(inputPath, paramName = "file_path") {
2196
2357
  }
2197
2358
  if (fs.existsSync(checkCursor)) {
2198
2359
  const realCheck = fs.realpathSync(checkCursor);
2199
- const relReal = path.relative(workspaceRoot, realCheck);
2200
- if (relReal.startsWith("..") || path.isAbsolute(relReal)) {
2360
+ const realBoundary = isWithinAnyWorkspaceRoot(realCheck, roots);
2361
+ if (!realBoundary.ok) {
2201
2362
  return {
2202
2363
  ok: false,
2203
2364
  error: `Error: Security policy violation — target path symlink escapes the active workspace directory.`
2204
2365
  };
2205
2366
  }
2206
2367
  }
2207
- return { ok: true, resolvedPath, workspaceRoot };
2368
+ return { ok: true, resolvedPath, workspaceRoot: boundaryCheck.matchedRoot };
2208
2369
  }
2209
2370
 
2210
2371
  // Handle tool execution calls
@@ -2239,18 +2400,21 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
2239
2400
  const result = PrivacyScrubberCore.scrubText(text, customRules, {}, normalizedProfile, dryRunMap, license.isPro);
2240
2401
  const telemetry = buildCisoAuditTelemetry(result.tokenMap || {});
2241
2402
 
2403
+ const structuredAudit = {
2404
+ riskLevel: telemetry.riskLevel,
2405
+ totalDetected: telemetry.totalDetected,
2406
+ entities: telemetry.entities,
2407
+ triggeredFrameworks: telemetry.triggeredFrameworks,
2408
+ profile: targetProfile,
2409
+ executionEngine: "ZTDS Client-Side RAM"
2410
+ };
2411
+
2242
2412
  if (format === "json") {
2243
2413
  return {
2414
+ structuredContent: structuredAudit,
2244
2415
  content: [{
2245
2416
  type: "text",
2246
- text: JSON.stringify({
2247
- riskLevel: telemetry.riskLevel,
2248
- totalDetected: telemetry.totalDetected,
2249
- entities: telemetry.entities,
2250
- triggeredFrameworks: telemetry.triggeredFrameworks,
2251
- profile: targetProfile,
2252
- executionEngine: "ZTDS Client-Side RAM"
2253
- }, null, 2)
2417
+ text: JSON.stringify(structuredAudit, null, 2)
2254
2418
  }]
2255
2419
  };
2256
2420
  }
@@ -2275,6 +2439,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
2275
2439
  }
2276
2440
 
2277
2441
  return {
2442
+ structuredContent: structuredAudit,
2278
2443
  content: [{ type: "text", text: report }]
2279
2444
  };
2280
2445
  }
@@ -2335,6 +2500,16 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
2335
2500
  const receiptMd = formatAuditReceipt(telemetry, compact, wasTruncated, charLimit);
2336
2501
 
2337
2502
  return {
2503
+ structuredContent: {
2504
+ scrubbedText,
2505
+ tokensRedacted: telemetry.totalCount,
2506
+ riskLevel: telemetry.riskLevel,
2507
+ entities: telemetry.entities,
2508
+ frameworks: telemetry.frameworksList,
2509
+ profile: finalProfile,
2510
+ sessionId: session_id,
2511
+ wasTruncated: Boolean(wasTruncated)
2512
+ },
2338
2513
  content: [
2339
2514
  { type: "text", text: scrubbedText + receiptMd }
2340
2515
  ]
@@ -2404,6 +2579,11 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
2404
2579
  restoredText = restoredText.replace(/\b(mysql|postgresql|postgres|redis|mongodb|https?|ftp|ssh|git|aws):\/\/\1:\/\//gi, '$1://');
2405
2580
 
2406
2581
  return {
2582
+ structuredContent: {
2583
+ restoredText,
2584
+ restoredCount: restored.restoredCount || 0,
2585
+ sessionId: session_id
2586
+ },
2407
2587
  content: [
2408
2588
  {
2409
2589
  type: "text",
@@ -2584,9 +2764,11 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
2584
2764
  const dataStart = localOffset + 30 + lNameLen + lExtraLen;
2585
2765
  const compData = docxBuf.subarray(dataStart, dataStart + compSize);
2586
2766
  if (method === 0) {
2587
- zipEntries[entryName] = compData.toString('utf8');
2767
+ if (compData.length <= 20 * 1024 * 1024) {
2768
+ zipEntries[entryName] = compData.toString('utf8');
2769
+ }
2588
2770
  } else if (method === 8) {
2589
- zipEntries[entryName] = zlib.inflateRawSync(compData).toString('utf8');
2771
+ zipEntries[entryName] = zlib.inflateRawSync(compData, { maxOutputLength: 20 * 1024 * 1024 }).toString('utf8');
2590
2772
  }
2591
2773
  }
2592
2774
  pos += 46 + nameLen + extraLen + commentLen;
@@ -2910,6 +3092,16 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
2910
3092
  const receiptMd = formatAuditReceipt(telemetry, compact, wasTruncated, charLimit);
2911
3093
 
2912
3094
  return {
3095
+ structuredContent: {
3096
+ scrubbedText,
3097
+ tokensRedacted: telemetry.totalCount,
3098
+ riskLevel: telemetry.riskLevel,
3099
+ entities: telemetry.entities,
3100
+ frameworks: telemetry.frameworksList,
3101
+ profile: finalProfile,
3102
+ sessionId: session_id,
3103
+ wasTruncated: Boolean(wasTruncated)
3104
+ },
2913
3105
  content: [
2914
3106
  { type: "text", text: scrubbedText + receiptMd }
2915
3107
  ]
@@ -2931,7 +3123,11 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
2931
3123
  }
2932
3124
 
2933
3125
  try {
2934
- const stat = await fs.promises.stat(resolvedRedactPath);
3126
+ const lstat = await fs.promises.lstat(resolvedRedactPath);
3127
+ if (lstat.isSymbolicLink() || !lstat.isFile()) {
3128
+ return { isError: true, content: [{ type: "text", text: `Error: Target must be a regular non-symlink file.` }] };
3129
+ }
3130
+ const stat = lstat;
2935
3131
  if (stat.size > 50 * 1024 * 1024) {
2936
3132
  return { isError: true, content: [{ type: "text", text: `Error: File is too large (>50MB).` }] };
2937
3133
  }
@@ -3229,20 +3425,23 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
3229
3425
  async function walkDir(dir) {
3230
3426
  const entries = await fs.promises.readdir(dir, { withFileTypes: true });
3231
3427
  for (const entry of entries) {
3428
+ if (entry.isSymbolicLink && entry.isSymbolicLink()) continue;
3232
3429
  const fullPath = path.join(dir, entry.name);
3430
+ const safeChild = resolveWorkspaceSafePath(fullPath, "directory_path");
3431
+ if (!safeChild.ok) continue;
3233
3432
  if (entry.isDirectory()) {
3234
- if (!ignoreDirs.includes(entry.name)) await walkDir(fullPath);
3433
+ if (!ignoreDirs.includes(entry.name)) await walkDir(safeChild.resolvedPath);
3235
3434
  } else if (entry.isFile()) {
3236
3435
  if (extensions && extensions.length > 0) {
3237
3436
  const ext = path.extname(entry.name);
3238
3437
  if (!extensions.includes(ext)) continue;
3239
3438
  }
3240
- filesToScan.push(fullPath);
3439
+ filesToScan.push(safeChild.resolvedPath);
3241
3440
  }
3242
3441
  }
3243
3442
  }
3244
3443
 
3245
- await walkDir(directory_path);
3444
+ await walkDir(safeDir.resolvedPath);
3246
3445
 
3247
3446
  let reportLines = [`## Security Audit Report: ${directory_path}`];
3248
3447
  let filesWithIssues = 0;
@@ -3443,23 +3642,72 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
3443
3642
  };
3444
3643
  }
3445
3644
  const rawBuffer = fs.readFileSync(resolvedPath);
3446
- const checkLen = Math.min(rawBuffer.length, 8000);
3447
- for (let i = 0; i < checkLen; i++) {
3448
- if (rawBuffer[i] === 0) {
3449
- return {
3450
- isError: true,
3451
- content: [{ type: "text", text: `Error: Binary file format detected. Use sanitize_file for DOCX/PDF/XLSX documents.` }]
3452
- };
3453
- }
3454
- }
3645
+ const ext = path.extname(resolvedPath).toLowerCase();
3455
3646
  const license = checkLicenseStatus();
3456
3647
  const limitStatus = checkFreeTierLimit(license.isPro);
3457
3648
  if (limitStatus.blocked) {
3458
3649
  return buildDailyLimitBlockedResponse();
3459
3650
  }
3460
3651
 
3461
- let rawContent = rawBuffer.toString('utf8');
3462
- const ext = path.extname(resolvedPath).toLowerCase();
3652
+ let rawContent = '';
3653
+ if (ext === '.docx') {
3654
+ try {
3655
+ const mammoth = require('mammoth');
3656
+ const result = await mammoth.extractRawText({ buffer: rawBuffer });
3657
+ rawContent = result.value || '';
3658
+ } catch (docxErr) {
3659
+ return {
3660
+ isError: true,
3661
+ content: [{ type: "text", text: `Error: Failed to parse DOCX file: ${docxErr.message}` }]
3662
+ };
3663
+ }
3664
+ } else if (ext === '.pdf' || ext === '.xlsx' || ext === '.xls') {
3665
+ if (!license.isPro) {
3666
+ return {
3667
+ isError: true,
3668
+ content: [{
3669
+ type: "text",
3670
+ text: `🔒 [PrivacyScrubber PRO / TEAMS] PDF and Excel file sanitization is a PRO feature (${ext === '.pdf' ? 'PDF' : 'Excel/XLSX'} detected).\n\n👉 7-Day Free B2B Trial (No card): https://privacyscrubber.com/pricing/?tier=teams&trial=true&utm_source=mcp_cli&utm_medium=file_type&utm_campaign=b2b_trial`
3671
+ }]
3672
+ };
3673
+ }
3674
+ if (ext === '.pdf') {
3675
+ try {
3676
+ const pdf = require('pdf-parse');
3677
+ const data = await pdf(rawBuffer);
3678
+ rawContent = data.text || '';
3679
+ } catch (pdfErr) {
3680
+ return {
3681
+ isError: true,
3682
+ content: [{ type: "text", text: `Error: Failed to parse PDF file: ${pdfErr.message}` }]
3683
+ };
3684
+ }
3685
+ } else {
3686
+ try {
3687
+ const XLSX = require('xlsx');
3688
+ const workbook = XLSX.read(rawBuffer, { type: 'buffer' });
3689
+ workbook.SheetNames.forEach(sheetName => {
3690
+ rawContent += `--- Sheet: ${sheetName} ---\n` + XLSX.utils.sheet_to_csv(workbook.Sheets[sheetName]) + '\n';
3691
+ });
3692
+ } catch (xlsxErr) {
3693
+ return {
3694
+ isError: true,
3695
+ content: [{ type: "text", text: `Error: Failed to parse Excel file: ${xlsxErr.message}` }]
3696
+ };
3697
+ }
3698
+ }
3699
+ } else {
3700
+ const checkLen = Math.min(rawBuffer.length, 8000);
3701
+ for (let i = 0; i < checkLen; i++) {
3702
+ if (rawBuffer[i] === 0) {
3703
+ return {
3704
+ isError: true,
3705
+ content: [{ type: "text", text: `Error: Binary file format detected. Only plain text, source code, and document formats (.docx, .pdf, .xlsx, .eml, .rtf) are supported.` }]
3706
+ };
3707
+ }
3708
+ }
3709
+ rawContent = rawBuffer.toString('utf8');
3710
+ }
3463
3711
  if (ext === '.eml') {
3464
3712
  const decodeQpUtf8 = (str) =>
3465
3713
  str.replace(/=\r?\n/g, '').replace(/(?:=[0-9A-Fa-f]{2})+/g, (seq) => {
@@ -3599,28 +3847,26 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
3599
3847
  };
3600
3848
  }
3601
3849
 
3602
- const workspaceRoot = fs.existsSync(process.cwd()) ? fs.realpathSync(process.cwd()) : path.resolve(process.cwd());
3603
- const resolvedPath = path.resolve(workspaceRoot, file_path);
3604
- const relToRoot = path.relative(workspaceRoot, resolvedPath);
3605
- if (relToRoot.startsWith("..") || path.isAbsolute(relToRoot)) {
3850
+ const safeTarget = resolveWorkspaceSafePath(file_path, "file_path");
3851
+ if (!safeTarget.ok) {
3606
3852
  return {
3607
3853
  isError: true,
3608
- content: [{ type: "text", text: "Error: Security policy violation — target 'file_path' resolves outside the active workspace directory (path traversal blocked)." }]
3854
+ content: [{ type: "text", text: safeTarget.error }]
3609
3855
  };
3610
3856
  }
3611
- let checkCursor = resolvedPath;
3612
- while (!fs.existsSync(checkCursor)) {
3613
- const parentCursor = path.dirname(checkCursor);
3614
- if (parentCursor === checkCursor) break;
3615
- checkCursor = parentCursor;
3857
+ const resolvedPath = safeTarget.resolvedPath;
3858
+
3859
+ const license = checkLicenseStatus();
3860
+ const limitStatus = checkFreeTierLimit(license.isPro);
3861
+ if (limitStatus.blocked) {
3862
+ return buildDailyLimitBlockedResponse();
3616
3863
  }
3617
- if (fs.existsSync(checkCursor)) {
3618
- const realCheck = fs.realpathSync(checkCursor);
3619
- const relReal = path.relative(workspaceRoot, realCheck);
3620
- if (relReal.startsWith("..") || path.isAbsolute(relReal)) {
3864
+ if (fs.existsSync(resolvedPath)) {
3865
+ const existingLstat = fs.lstatSync(resolvedPath);
3866
+ if (existingLstat.isSymbolicLink() || !existingLstat.isFile()) {
3621
3867
  return {
3622
3868
  isError: true,
3623
- content: [{ type: "text", text: "Error: Security policy violation — target path symlink escapes the active workspace directory." }]
3869
+ content: [{ type: "text", text: "Error: Security policy violation — target must be a regular non-symlink file." }]
3624
3870
  };
3625
3871
  }
3626
3872
  }
@@ -3665,7 +3911,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
3665
3911
  }
3666
3912
  targetDir = safeWorkspace.resolvedPath;
3667
3913
  }
3668
- const targets = agent_types.includes("all") ? ["cursor", "windsurf", "claude_code", "copilot", "cline"] : agent_types;
3914
+ const targets = agent_types.includes("all") ? ["cursor", "windsurf", "claude_code", "copilot", "cline", "agents_md"] : agent_types;
3669
3915
 
3670
3916
  const ruleText = `# PrivacyScrubber Zero-Trust Agentic Guard (CISO Security Rules)
3671
3917
  # 100% Local In-Memory Data Sanitization. Zero Network Egress.
@@ -3689,6 +3935,14 @@ Before reading sensitive files, running terminal commands that may print credent
3689
3935
  const p = path.join(targetDir, '.cursorrules');
3690
3936
  fs.writeFileSync(p, ruleText, 'utf8');
3691
3937
  generatedFiles.push('.cursorrules');
3938
+
3939
+ const cursorRulesDir = path.join(targetDir, '.cursor', 'rules');
3940
+ try {
3941
+ if (!fs.existsSync(cursorRulesDir)) fs.mkdirSync(cursorRulesDir, { recursive: true });
3942
+ const mdcContent = `---\ndescription: PrivacyScrubber Zero-Trust Data Sanitization (ZTDS) Agentic Guard\nglobs: *\nalwaysApply: true\n---\n\n${ruleText}`;
3943
+ fs.writeFileSync(path.join(cursorRulesDir, 'privacyscrubber.mdc'), mdcContent, 'utf8');
3944
+ generatedFiles.push('.cursor/rules/privacyscrubber.mdc');
3945
+ } catch (_) {}
3692
3946
  }
3693
3947
  if (targets.includes("windsurf")) {
3694
3948
  const p = path.join(targetDir, '.windsurfrules');
@@ -3708,6 +3962,19 @@ Before reading sensitive files, running terminal commands that may print credent
3708
3962
  generatedFiles.push('CLAUDE.md');
3709
3963
  }
3710
3964
  }
3965
+ if (targets.includes("agents_md") || targets.includes("codex")) {
3966
+ const p = path.join(targetDir, 'AGENTS.md');
3967
+ if (fs.existsSync(p)) {
3968
+ const existing = fs.readFileSync(p, 'utf8');
3969
+ if (!existing.includes("Zero-Trust Agentic Guard")) {
3970
+ fs.writeFileSync(p, existing + "\n\n" + ruleText, 'utf8');
3971
+ generatedFiles.push('AGENTS.md (appended)');
3972
+ }
3973
+ } else {
3974
+ fs.writeFileSync(p, ruleText, 'utf8');
3975
+ generatedFiles.push('AGENTS.md');
3976
+ }
3977
+ }
3711
3978
  if (targets.includes("copilot")) {
3712
3979
  const copilotDir = path.join(targetDir, '.github');
3713
3980
  if (!fs.existsSync(copilotDir)) fs.mkdirSync(copilotDir, { recursive: true });
@@ -3721,7 +3988,57 @@ Before reading sensitive files, running terminal commands that may print credent
3721
3988
  generatedFiles.push('.clinerules');
3722
3989
  }
3723
3990
 
3991
+ if (args?.include_hooks !== false && (targets.includes("cursor") || targets.includes("claude_code") || agent_types.includes("all"))) {
3992
+ const hookCmd = "npx -y @privacyscrubber/mcp-server --hook";
3993
+ if (targets.includes("cursor") || agent_types.includes("all")) {
3994
+ try {
3995
+ const cursorHooksPath = path.join(targetDir, '.cursor', 'hooks.json');
3996
+ fs.mkdirSync(path.dirname(cursorHooksPath), { recursive: true });
3997
+ let cursorHooks = { version: 1, hooks: {} };
3998
+ if (fs.existsSync(cursorHooksPath)) {
3999
+ try { cursorHooks = JSON.parse(fs.readFileSync(cursorHooksPath, 'utf8')); } catch (_) {}
4000
+ }
4001
+ if (!cursorHooks.hooks || typeof cursorHooks.hooks !== 'object') cursorHooks.hooks = {};
4002
+ for (const ev of ['beforeReadFile', 'beforeShellExecution']) {
4003
+ if (!Array.isArray(cursorHooks.hooks[ev])) cursorHooks.hooks[ev] = [];
4004
+ if (!cursorHooks.hooks[ev].some(h => h && h.command && h.command.includes('privacyscrubber'))) {
4005
+ cursorHooks.hooks[ev].push({ command: hookCmd });
4006
+ }
4007
+ }
4008
+ fs.writeFileSync(cursorHooksPath, JSON.stringify(cursorHooks, null, 2) + '\n', 'utf8');
4009
+ generatedFiles.push('.cursor/hooks.json');
4010
+ } catch (_) {}
4011
+ }
4012
+ if (targets.includes("claude_code") || agent_types.includes("all")) {
4013
+ try {
4014
+ const claudeSettingsPath = path.join(targetDir, '.claude', 'settings.json');
4015
+ fs.mkdirSync(path.dirname(claudeSettingsPath), { recursive: true });
4016
+ let claudeSettings = {};
4017
+ if (fs.existsSync(claudeSettingsPath)) {
4018
+ try { claudeSettings = JSON.parse(fs.readFileSync(claudeSettingsPath, 'utf8')); } catch (_) {}
4019
+ }
4020
+ if (!claudeSettings.hooks || typeof claudeSettings.hooks !== 'object') claudeSettings.hooks = {};
4021
+ if (!Array.isArray(claudeSettings.hooks.PreToolUse)) claudeSettings.hooks.PreToolUse = [];
4022
+ const hasClaudeHook = claudeSettings.hooks.PreToolUse.some(entry =>
4023
+ Array.isArray(entry?.hooks) && entry.hooks.some(h => h && h.command && h.command.includes('privacyscrubber'))
4024
+ );
4025
+ if (!hasClaudeHook) {
4026
+ claudeSettings.hooks.PreToolUse.push({
4027
+ matcher: "Read|Bash",
4028
+ hooks: [{ type: "command", command: hookCmd }]
4029
+ });
4030
+ }
4031
+ fs.writeFileSync(claudeSettingsPath, JSON.stringify(claudeSettings, null, 2) + '\n', 'utf8');
4032
+ generatedFiles.push('.claude/settings.json');
4033
+ } catch (_) {}
4034
+ }
4035
+ }
4036
+
3724
4037
  return {
4038
+ structuredContent: {
4039
+ workspace: targetDir,
4040
+ generatedFiles
4041
+ },
3725
4042
  content: [{
3726
4043
  type: "text",
3727
4044
  text: `[Zero-Trust Agentic Guard: Agent Rules Generated]\n` +
@@ -3744,22 +4061,38 @@ Before reading sensitive files, running terminal commands that may print credent
3744
4061
  }
3745
4062
 
3746
4063
  const targetSession = getSessionMap(session_id);
4064
+ const seenUnmask = new WeakMap();
3747
4065
 
3748
4066
  function unmaskRecursive(val) {
3749
4067
  if (typeof val === 'string') {
3750
4068
  return reverseTokens(val, targetSession);
3751
4069
  }
3752
- if (Array.isArray(val)) {
3753
- return val.map(unmaskRecursive);
4070
+ if (val === null || typeof val !== 'object') return val;
4071
+ if (
4072
+ val instanceof Date ||
4073
+ val instanceof RegExp ||
4074
+ val instanceof Error ||
4075
+ ArrayBuffer.isView(val) ||
4076
+ (typeof ArrayBuffer !== 'undefined' && val instanceof ArrayBuffer)
4077
+ ) {
4078
+ return val;
3754
4079
  }
3755
- if (val !== null && typeof val === 'object') {
3756
- const res = {};
3757
- for (const [k, v] of Object.entries(val)) {
3758
- res[k] = unmaskRecursive(v);
4080
+ if (seenUnmask.has(val)) return seenUnmask.get(val);
4081
+ if (Array.isArray(val)) {
4082
+ const outArr = [];
4083
+ seenUnmask.set(val, outArr);
4084
+ for (let i = 0; i < val.length; i++) {
4085
+ outArr.push(unmaskRecursive(val[i]));
3759
4086
  }
3760
- return res;
4087
+ return outArr;
3761
4088
  }
3762
- return val;
4089
+ const res = {};
4090
+ seenUnmask.set(val, res);
4091
+ for (const [k, v] of Object.entries(val)) {
4092
+ if (k === '__proto__' || k === 'constructor' || k === 'prototype') continue;
4093
+ res[k] = unmaskRecursive(v);
4094
+ }
4095
+ return res;
3763
4096
  }
3764
4097
 
3765
4098
  const unmasked = unmaskRecursive(targetArgs);
@@ -3772,17 +4105,20 @@ Before reading sensitive files, running terminal commands that may print credent
3772
4105
  }
3773
4106
  }
3774
4107
 
4108
+ const unmaskPayload = {
4109
+ status: "success",
4110
+ success: true,
4111
+ session_id,
4112
+ tokens_restored: tokensRestored,
4113
+ unmasked_count: tokensRestored,
4114
+ unmasked_args: unmasked
4115
+ };
4116
+
3775
4117
  return {
4118
+ structuredContent: unmaskPayload,
3776
4119
  content: [{
3777
4120
  type: "text",
3778
- text: JSON.stringify({
3779
- status: "success",
3780
- success: true,
3781
- session_id,
3782
- tokens_restored: tokensRestored,
3783
- unmasked_count: tokensRestored,
3784
- unmasked_args: unmasked
3785
- }, null, 2)
4121
+ text: JSON.stringify(unmaskPayload, null, 2)
3786
4122
  }]
3787
4123
  };
3788
4124
  }
@@ -3796,6 +4132,7 @@ Before reading sensitive files, running terminal commands that may print credent
3796
4132
  const session = agentSessions.get(sid);
3797
4133
  if (!session) {
3798
4134
  return {
4135
+ structuredContent: { sessionId: sid, exists: false, totalTokens: 0 },
3799
4136
  content: [{
3800
4137
  type: "text",
3801
4138
  text: `[Zero-Trust Agentic Guard: Session Info]\nSession '${sid}' does not exist or has expired.`
@@ -3806,6 +4143,16 @@ Before reading sensitive files, running terminal commands that may print credent
3806
4143
  const telemetry = buildCisoAuditTelemetry(session.map);
3807
4144
  const streamBufferLen = session.streamDetokenizer ? session.streamDetokenizer.buffer.length : 0;
3808
4145
  return {
4146
+ structuredContent: {
4147
+ sessionId: sid,
4148
+ exists: true,
4149
+ createdAt: new Date(session.createdAt).toISOString(),
4150
+ lastActive: new Date(session.lastActive).toISOString(),
4151
+ totalTokens: tokenEntries.length,
4152
+ streamBufferBytes: streamBufferLen,
4153
+ entities: telemetry.entities,
4154
+ riskLevel: telemetry.riskLevel
4155
+ },
3809
4156
  content: [{
3810
4157
  type: "text",
3811
4158
  text: `[Zero-Trust Agentic Guard: Session Info - ${sid}]\n` +
@@ -3830,6 +4177,13 @@ Before reading sensitive files, running terminal commands that may print credent
3830
4177
  }
3831
4178
 
3832
4179
  return {
4180
+ structuredContent: {
4181
+ activeSessions: agentSessions.size,
4182
+ totalTokens,
4183
+ heapUsedMb: Number((memUsage.heapUsed / (1024 * 1024)).toFixed(2)),
4184
+ uptimeSeconds: Math.floor(process.uptime()),
4185
+ sessions: sessionList
4186
+ },
3833
4187
  content: [{
3834
4188
  type: "text",
3835
4189
  text: `[Zero-Trust Agentic Guard: Global Session Diagnostics]\n` +
@@ -3928,19 +4282,21 @@ Before reading sensitive files, running terminal commands that may print credent
3928
4282
 
3929
4283
  const telemetry = buildCisoAuditTelemetry(targetSession);
3930
4284
  const auditReceipt = formatAuditReceipt(telemetry);
4285
+ const ragChunkPayload = {
4286
+ status: "success",
4287
+ session_id,
4288
+ total_chunks: chunks.length,
4289
+ tokens_redacted_in_batch: totalTokensRedacted,
4290
+ total_session_tokens: Object.keys(targetSession).length,
4291
+ risk_level: telemetry.riskLevel,
4292
+ sanitized_chunks: sanitizedChunks
4293
+ };
3931
4294
 
3932
4295
  return {
4296
+ structuredContent: ragChunkPayload,
3933
4297
  content: [{
3934
4298
  type: "text",
3935
- text: JSON.stringify({
3936
- status: "success",
3937
- session_id,
3938
- total_chunks: chunks.length,
3939
- tokens_redacted_in_batch: totalTokensRedacted,
3940
- total_session_tokens: Object.keys(targetSession).length,
3941
- risk_level: telemetry.riskLevel,
3942
- sanitized_chunks: sanitizedChunks
3943
- }, null, 2) + `\n\n${auditReceipt}`
4299
+ text: JSON.stringify(ragChunkPayload, null, 2) + `\n\n${auditReceipt}`
3944
4300
  }]
3945
4301
  };
3946
4302
  }
@@ -3988,16 +4344,19 @@ Before reading sensitive files, running terminal commands that may print credent
3988
4344
  }
3989
4345
  }
3990
4346
 
4347
+ const ragRestorePayload = {
4348
+ status: "success",
4349
+ session_id,
4350
+ total_results: results.length,
4351
+ tokens_restored: totalTokensRestored,
4352
+ restored_results: restoredResults
4353
+ };
4354
+
3991
4355
  return {
4356
+ structuredContent: ragRestorePayload,
3992
4357
  content: [{
3993
4358
  type: "text",
3994
- text: JSON.stringify({
3995
- status: "success",
3996
- session_id,
3997
- total_results: results.length,
3998
- tokens_restored: totalTokensRestored,
3999
- restored_results: restoredResults
4000
- }, null, 2)
4359
+ text: JSON.stringify(ragRestorePayload, null, 2)
4001
4360
  }]
4002
4361
  };
4003
4362
  }
@@ -4026,6 +4385,9 @@ Before reading sensitive files, running terminal commands that may print credent
4026
4385
  const sessionHash = receipt.receiptSha256;
4027
4386
 
4028
4387
  let outputText = "";
4388
+ const safeCompanyName = String(company_name || "Organization").replace(/[\r\n\x00-\x1f]/g, " ").trim().slice(0, 120);
4389
+ const safeDepartment = String(department || "Security & Compliance").replace(/[\r\n\x00-\x1f]/g, " ").trim().slice(0, 120);
4390
+
4029
4391
  const eventData = {
4030
4392
  vendor: "PrivacyScrubber",
4031
4393
  product: "ZTDS",
@@ -4036,8 +4398,8 @@ Before reading sensitive files, running terminal commands that may print credent
4036
4398
  entities: telemetry.entities,
4037
4399
  frameworks: telemetry.frameworksList,
4038
4400
  sessionHash,
4039
- organization: company_name,
4040
- department
4401
+ organization: safeCompanyName,
4402
+ department: safeDepartment
4041
4403
  };
4042
4404
 
4043
4405
  if (format === "cef") {
@@ -4045,19 +4407,35 @@ Before reading sensitive files, running terminal commands that may print credent
4045
4407
  } else if (format === "syslog") {
4046
4408
  outputText = formatSyslogEvent(eventData);
4047
4409
  } else if (format === "json" || format === "markdown") {
4048
- outputText = formatPsr2Report({ format, receipt, telemetry, company_name, department, utmSource: 'mcp_audit_export', utmMedium: format });
4410
+ outputText = formatPsr2Report({ format, receipt, telemetry, company_name: safeCompanyName, department: safeDepartment, utmSource: 'mcp_audit_export', utmMedium: format });
4049
4411
  } else {
4050
4412
  // Default: jsonl
4051
4413
  outputText = formatJsonlEvent(eventData);
4052
4414
  }
4053
4415
 
4054
4416
  if (typeof destination_file === 'string' && destination_file.trim()) {
4417
+ const safeDest = resolveWorkspaceSafePath(destination_file.trim(), "destination_file");
4418
+ if (!safeDest.ok) {
4419
+ return {
4420
+ isError: true,
4421
+ content: [{ type: "text", text: safeDest.error }]
4422
+ };
4423
+ }
4055
4424
  try {
4056
- const dir = path.dirname(destination_file);
4425
+ if (fs.existsSync(safeDest.resolvedPath)) {
4426
+ const st = fs.lstatSync(safeDest.resolvedPath);
4427
+ if (st.isSymbolicLink() || !st.isFile()) {
4428
+ return {
4429
+ isError: true,
4430
+ content: [{ type: "text", text: "Error: Security policy violation — destination_file must be a regular non-symlink file." }]
4431
+ };
4432
+ }
4433
+ }
4434
+ const dir = path.dirname(safeDest.resolvedPath);
4057
4435
  if (!fs.existsSync(dir)) {
4058
4436
  fs.mkdirSync(dir, { recursive: true });
4059
4437
  }
4060
- fs.appendFileSync(destination_file, outputText + '\n', 'utf8');
4438
+ fs.appendFileSync(safeDest.resolvedPath, outputText + '\n', 'utf8');
4061
4439
  } catch (e) {
4062
4440
  return {
4063
4441
  isError: true,
@@ -4067,6 +4445,15 @@ Before reading sensitive files, running terminal commands that may print credent
4067
4445
  }
4068
4446
 
4069
4447
  return {
4448
+ structuredContent: {
4449
+ format,
4450
+ riskLevel: telemetry.riskLevel,
4451
+ tokensMasked: telemetry.totalCount,
4452
+ entities: telemetry.entities,
4453
+ frameworks: telemetry.frameworksList,
4454
+ receiptSha256: sessionHash,
4455
+ timestamp
4456
+ },
4070
4457
  content: [{
4071
4458
  type: "text",
4072
4459
  text: outputText