@privacyscrubber/mcp-server 2.4.5 → 2.4.7

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
@@ -43,6 +43,7 @@ if (rawCliArgs.includes('--help') || rawCliArgs.includes('-h') || rawCliArgs.inc
43
43
  console.log(`PrivacyScrubber MCP Server v${MCP_VERSION} — Zero-Trust Data Sanitization (ZTDS)`);
44
44
  console.log('\nUsage:');
45
45
  console.log(' npx @privacyscrubber/mcp-server Start MCP stdio server');
46
+ console.log(' npx @privacyscrubber/mcp-server --wrap <cmd> Transparent ZTDS proxy for any 3rd-party MCP server');
46
47
  console.log(' npx @privacyscrubber/mcp-server init Auto-configure Claude, Cursor, Windsurf, Cline');
47
48
  console.log(' npx @privacyscrubber/mcp-server uninstall Remove from IDE configurations');
48
49
  console.log(' npx @privacyscrubber/mcp-server trial Request 7-day B2B TEAMS/SDK evaluation key');
@@ -82,6 +83,10 @@ if (rawCliArgs.includes('--memo') || rawCliArgs.includes('memo')) {
82
83
  console.log(generateProcurementMemo({ targetTier: tierArg, format: 'text' }));
83
84
  process.exit(0);
84
85
  }
86
+ if (rawCliArgs.includes('--hook') || rawCliArgs.includes('--wrap')) {
87
+ await import('./pii-masking-run.js');
88
+ await new Promise(() => {});
89
+ }
85
90
  if (rawCliArgs.some(arg => ['init', 'install', '--install', 'setup', '--setup', 'uninstall', '--uninstall', '--remove'].includes(arg))) {
86
91
  const { runInstaller } = await import('./install.js');
87
92
  const isRemove = rawCliArgs.some(arg => ['uninstall', '--uninstall', '--remove'].includes(arg));
@@ -748,11 +753,27 @@ function buildCisoAuditTelemetry(currentTokenMap = {}) {
748
753
  trademark: 'ZTDS™ Reg. #182655957 (ILPO Cl 9 & 42)'
749
754
  };
750
755
 
756
+ const agenticProtocol = {
757
+ protocolsSupported: ['MCP-1.0 (stdio/SSE)', 'A2A-1.0 (Agent2Agent)', 'ZTDS-RFC-v1.0'],
758
+ agentCardUrl: 'https://privacyscrubber.com/.well-known/agent-card.json',
759
+ ztdsStandardCardUrl: 'https://ztds.ai/.well-known/agent-card.json',
760
+ regulatorySafeHarbor: [
761
+ 'GDPR-Art-4(5)-Pseudonymization',
762
+ 'GDPR-Art-25-Privacy-by-Design',
763
+ 'GDPR-Art-32-TOMs',
764
+ 'EU-AI-Act-Art-50',
765
+ 'EU-AI-Act-Art-17-EN-18286-QMS',
766
+ 'CNIL-ICO-Agentic-Memory-Compartmentalization',
767
+ 'IL-PPL-Amendment-13-Sec-17'
768
+ ],
769
+ zeroCleartextEgress: true
770
+ };
771
+
751
772
  const statutoryCitations = [
752
773
  {
753
- statute: 'EU AI Act (Art. 50)',
754
- scope: 'AI Transparency & Data Minimization',
755
- rule: 'Sanitization runs locally by design; reduces the content sent to external AI models. Informational mapping, not a compliance determination.'
774
+ statute: 'EU AI Act (Art. 50 & Art. 17 / EN 18286:2026 QMS)',
775
+ scope: 'AI Transparency, Data Minimization & Harmonized QMS Governance',
776
+ rule: 'Sanitization runs locally by design; reduces the content sent to external AI models and supports Article 17 Quality Management System (EN 18286:2026) data governance controls. Informational mapping, not a compliance determination.'
756
777
  },
757
778
  {
758
779
  statute: 'GDPR (Art. 4, 25, 28, 32)',
@@ -784,6 +805,7 @@ function buildCisoAuditTelemetry(currentTokenMap = {}) {
784
805
  frameworksList: Array.from(frameworksSet),
785
806
  statutoryCitations,
786
807
  ipHoldings,
808
+ agenticProtocol,
787
809
  multimodalEgress: '0.00 Bytes (Solid Blackout & Burn-in Verified)'
788
810
  };
789
811
  }
@@ -1364,6 +1386,15 @@ We request approval to acquire a **PrivacyScrubber TEAMS License** (\$99/month f
1364
1386
  `;
1365
1387
  }
1366
1388
 
1389
+ const MCP_SYSTEM_INSTRUCTIONS = [
1390
+ "Zero-Trust Data Sanitization (ZTDS) Protocol Active (100% local RAM, 0 network egress).",
1391
+ "AUTOMATIC INTERCEPTION RULES FOR AI AGENTS:",
1392
+ "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.",
1393
+ "2. SHELL & GIT: Route terminal commands that may output secrets or PII through 'guard_exec', and inspect diffs via 'guard_git_diff'.",
1394
+ "3. RAW TEXT & RAG: Sanitize raw user PII/secrets in prompts via 'sanitize_text' (or 'guard_rag_chunk' for vector chunks) before external reasoning.",
1395
+ "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."
1396
+ ].join("\n");
1397
+
1367
1398
  // Create the MCP server
1368
1399
  const server = new Server(
1369
1400
  {
@@ -1377,6 +1408,7 @@ const server = new Server(
1377
1408
  logging: {},
1378
1409
  resources: {},
1379
1410
  },
1411
+ instructions: MCP_SYSTEM_INSTRUCTIONS,
1380
1412
  }
1381
1413
  );
1382
1414
 
@@ -1387,6 +1419,13 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
1387
1419
  {
1388
1420
  name: "audit_context",
1389
1421
  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.",
1422
+ annotations: {
1423
+ title: "Audit Context for PII & Secrets",
1424
+ readOnlyHint: true,
1425
+ destructiveHint: false,
1426
+ idempotentHint: true,
1427
+ openWorldHint: false
1428
+ },
1390
1429
  inputSchema: {
1391
1430
  type: "object",
1392
1431
  properties: {
@@ -1414,6 +1453,13 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
1414
1453
  {
1415
1454
  name: "sanitize_text",
1416
1455
  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)",
1456
+ annotations: {
1457
+ title: "Sanitize Text in Local RAM",
1458
+ readOnlyHint: true,
1459
+ destructiveHint: false,
1460
+ idempotentHint: true,
1461
+ openWorldHint: false
1462
+ },
1417
1463
  inputSchema: {
1418
1464
  type: "object",
1419
1465
  properties: {
@@ -1446,44 +1492,16 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
1446
1492
  required: ["text"]
1447
1493
  }
1448
1494
  },
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
1495
  {
1485
1496
  name: "reveal_text",
1486
1497
  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).",
1498
+ annotations: {
1499
+ title: "Restore Tokens in Local RAM",
1500
+ readOnlyHint: true,
1501
+ destructiveHint: false,
1502
+ idempotentHint: true,
1503
+ openWorldHint: false
1504
+ },
1487
1505
  inputSchema: {
1488
1506
  type: "object",
1489
1507
  properties: {
@@ -1510,6 +1528,13 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
1510
1528
  {
1511
1529
  name: "stream_reveal_text",
1512
1530
  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.",
1531
+ annotations: {
1532
+ title: "Streaming Token Detokenizer",
1533
+ readOnlyHint: true,
1534
+ destructiveHint: false,
1535
+ idempotentHint: false,
1536
+ openWorldHint: false
1537
+ },
1513
1538
  inputSchema: {
1514
1539
  type: "object",
1515
1540
  properties: {
@@ -1535,43 +1560,20 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
1535
1560
  },
1536
1561
  {
1537
1562
  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.",
1563
+ 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.",
1564
+ annotations: {
1565
+ title: "Sanitize Local File in RAM",
1566
+ readOnlyHint: true,
1567
+ destructiveHint: false,
1568
+ idempotentHint: true,
1569
+ openWorldHint: false
1570
+ },
1569
1571
  inputSchema: {
1570
1572
  type: "object",
1571
1573
  properties: {
1572
1574
  file_path: {
1573
1575
  type: "string",
1574
- description: "Absolute path to the file to sanitize."
1576
+ description: "Absolute or workspace-relative path to the file to sanitize."
1575
1577
  },
1576
1578
  profile: {
1577
1579
  type: "string",
@@ -1596,6 +1598,13 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
1596
1598
  {
1597
1599
  name: "audit_directory_for_pii",
1598
1600
  description: "Scans a local directory for leaks of secrets, keys, and PII. Returns a summary report. Use this tool for Security Auditing.",
1601
+ annotations: {
1602
+ title: "Audit Directory for PII & Secrets",
1603
+ readOnlyHint: true,
1604
+ destructiveHint: false,
1605
+ idempotentHint: true,
1606
+ openWorldHint: false
1607
+ },
1599
1608
  inputSchema: {
1600
1609
  type: "object",
1601
1610
  properties: {
@@ -1627,6 +1636,13 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
1627
1636
  {
1628
1637
  name: "redact_file",
1629
1638
  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.",
1639
+ annotations: {
1640
+ title: "In-Place File Redaction",
1641
+ readOnlyHint: false,
1642
+ destructiveHint: true,
1643
+ idempotentHint: false,
1644
+ openWorldHint: false
1645
+ },
1630
1646
  inputSchema: {
1631
1647
  type: "object",
1632
1648
  properties: {
@@ -1657,6 +1673,13 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
1657
1673
  {
1658
1674
  name: "create_default_config",
1659
1675
  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.",
1676
+ annotations: {
1677
+ title: "Create Default Workspace Config",
1678
+ readOnlyHint: false,
1679
+ destructiveHint: false,
1680
+ idempotentHint: true,
1681
+ openWorldHint: false
1682
+ },
1660
1683
  inputSchema: {
1661
1684
  type: "object",
1662
1685
  properties: {},
@@ -1666,6 +1689,13 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
1666
1689
  {
1667
1690
  name: "generate_compliance_report",
1668
1691
  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.",
1692
+ annotations: {
1693
+ title: "Generate Compliance Audit Certificate",
1694
+ readOnlyHint: true,
1695
+ destructiveHint: false,
1696
+ idempotentHint: true,
1697
+ openWorldHint: false
1698
+ },
1669
1699
  inputSchema: {
1670
1700
  type: "object",
1671
1701
  properties: {
@@ -1693,6 +1723,13 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
1693
1723
  {
1694
1724
  name: "mark_false_positive",
1695
1725
  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.",
1726
+ annotations: {
1727
+ title: "Mark Token as False Positive",
1728
+ readOnlyHint: true,
1729
+ destructiveHint: false,
1730
+ idempotentHint: true,
1731
+ openWorldHint: false
1732
+ },
1696
1733
  inputSchema: {
1697
1734
  type: "object",
1698
1735
  properties: {
@@ -1711,6 +1748,13 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
1711
1748
  {
1712
1749
  name: "check_status",
1713
1750
  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.",
1751
+ annotations: {
1752
+ title: "Check MCP License & Session Status",
1753
+ readOnlyHint: true,
1754
+ destructiveHint: false,
1755
+ idempotentHint: true,
1756
+ openWorldHint: false
1757
+ },
1714
1758
  inputSchema: {
1715
1759
  type: "object",
1716
1760
  properties: {},
@@ -1720,6 +1764,13 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
1720
1764
  {
1721
1765
  name: "guard_exec",
1722
1766
  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.",
1767
+ annotations: {
1768
+ title: "Guard Shell Command Execution",
1769
+ readOnlyHint: false,
1770
+ destructiveHint: true,
1771
+ idempotentHint: false,
1772
+ openWorldHint: false
1773
+ },
1723
1774
  inputSchema: {
1724
1775
  type: "object",
1725
1776
  properties: {
@@ -1742,6 +1793,10 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
1742
1793
  timeout_ms: {
1743
1794
  type: "number",
1744
1795
  description: "Execution timeout in milliseconds. Defaults to 15000 (15s)."
1796
+ },
1797
+ session_id: {
1798
+ type: "string",
1799
+ description: "Optional session ID for multi-agent isolation. Defaults to 'default'."
1745
1800
  }
1746
1801
  },
1747
1802
  required: ["command"]
@@ -1749,7 +1804,14 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
1749
1804
  },
1750
1805
  {
1751
1806
  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.",
1807
+ 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.",
1808
+ annotations: {
1809
+ title: "Guard Local File Read",
1810
+ readOnlyHint: true,
1811
+ destructiveHint: false,
1812
+ idempotentHint: true,
1813
+ openWorldHint: false
1814
+ },
1753
1815
  inputSchema: {
1754
1816
  type: "object",
1755
1817
  properties: {
@@ -1768,6 +1830,10 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
1768
1830
  max_lines: {
1769
1831
  type: "number",
1770
1832
  description: "Optional line cap for large files. Defaults to 500."
1833
+ },
1834
+ session_id: {
1835
+ type: "string",
1836
+ description: "Optional session ID for multi-agent isolation. Defaults to 'default'."
1771
1837
  }
1772
1838
  },
1773
1839
  required: ["file_path"]
@@ -1776,6 +1842,13 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
1776
1842
  {
1777
1843
  name: "guard_git_diff",
1778
1844
  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.",
1845
+ annotations: {
1846
+ title: "Guard Git Diff Inspection",
1847
+ readOnlyHint: true,
1848
+ destructiveHint: false,
1849
+ idempotentHint: true,
1850
+ openWorldHint: false
1851
+ },
1779
1852
  inputSchema: {
1780
1853
  type: "object",
1781
1854
  properties: {
@@ -1806,6 +1879,13 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
1806
1879
  {
1807
1880
  name: "guard_apply_patch",
1808
1881
  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.",
1882
+ annotations: {
1883
+ title: "Guard Apply Unmasked Patch to Disk",
1884
+ readOnlyHint: false,
1885
+ destructiveHint: false,
1886
+ idempotentHint: true,
1887
+ openWorldHint: false
1888
+ },
1809
1889
  inputSchema: {
1810
1890
  type: "object",
1811
1891
  properties: {
@@ -1820,6 +1900,10 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
1820
1900
  create_backup: {
1821
1901
  type: "boolean",
1822
1902
  description: "Whether to create a .bak backup if target file already exists. Defaults to true."
1903
+ },
1904
+ session_id: {
1905
+ type: "string",
1906
+ description: "Optional session ID for multi-agent isolation. Defaults to 'default'."
1823
1907
  }
1824
1908
  },
1825
1909
  required: ["file_path", "content"]
@@ -1827,7 +1911,14 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
1827
1911
  },
1828
1912
  {
1829
1913
  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.",
1914
+ 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.",
1915
+ annotations: {
1916
+ title: "Scaffold AI Agent Security Rules",
1917
+ readOnlyHint: false,
1918
+ destructiveHint: false,
1919
+ idempotentHint: true,
1920
+ openWorldHint: false
1921
+ },
1831
1922
  inputSchema: {
1832
1923
  type: "object",
1833
1924
  properties: {
@@ -1835,7 +1926,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
1835
1926
  type: "array",
1836
1927
  items: {
1837
1928
  type: "string",
1838
- enum: ["all", "cursor", "windsurf", "claude_code", "copilot", "cline"]
1929
+ enum: ["all", "cursor", "windsurf", "claude_code", "agents_md", "codex", "copilot", "cline"]
1839
1930
  },
1840
1931
  description: "List of agent targets to generate rules for. Use ['all'] to generate rules for all supported AI agents."
1841
1932
  },
@@ -1850,6 +1941,13 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
1850
1941
  {
1851
1942
  name: "guard_unmask_args",
1852
1943
  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.",
1944
+ annotations: {
1945
+ title: "Unmask Tool Arguments in Local RAM",
1946
+ readOnlyHint: true,
1947
+ destructiveHint: false,
1948
+ idempotentHint: true,
1949
+ openWorldHint: false
1950
+ },
1853
1951
  inputSchema: {
1854
1952
  type: "object",
1855
1953
  properties: {
@@ -1867,6 +1965,13 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
1867
1965
  {
1868
1966
  name: "guard_session_info",
1869
1967
  description: "Zero-Trust Agentic Guard: Returns diagnostic metrics on active in-memory sessions, token counts, and memory footprint with zero plaintext leakage.",
1968
+ annotations: {
1969
+ title: "Inspect RAM Session Diagnostics",
1970
+ readOnlyHint: true,
1971
+ destructiveHint: false,
1972
+ idempotentHint: true,
1973
+ openWorldHint: false
1974
+ },
1870
1975
  inputSchema: {
1871
1976
  type: "object",
1872
1977
  properties: {
@@ -1881,6 +1986,13 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
1881
1986
  {
1882
1987
  name: "guard_session_reset",
1883
1988
  description: "Zero-Trust Agentic Guard: Explicitly purges the in-memory token map for a specific session_id or all sessions upon agent task completion.",
1989
+ annotations: {
1990
+ title: "Reset RAM Session Token Map",
1991
+ readOnlyHint: true,
1992
+ destructiveHint: false,
1993
+ idempotentHint: true,
1994
+ openWorldHint: false
1995
+ },
1884
1996
  inputSchema: {
1885
1997
  type: "object",
1886
1998
  properties: {
@@ -1899,6 +2011,13 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
1899
2011
  {
1900
2012
  name: "guard_rag_chunk",
1901
2013
  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.",
2014
+ annotations: {
2015
+ title: "Sanitize RAG Document Chunks",
2016
+ readOnlyHint: true,
2017
+ destructiveHint: false,
2018
+ idempotentHint: true,
2019
+ openWorldHint: false
2020
+ },
1902
2021
  inputSchema: {
1903
2022
  type: "object",
1904
2023
  properties: {
@@ -1939,6 +2058,13 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
1939
2058
  {
1940
2059
  name: "guard_rag_restore",
1941
2060
  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.",
2061
+ annotations: {
2062
+ title: "Restore RAG Vector Search Results",
2063
+ readOnlyHint: true,
2064
+ destructiveHint: false,
2065
+ idempotentHint: true,
2066
+ openWorldHint: false
2067
+ },
1942
2068
  inputSchema: {
1943
2069
  type: "object",
1944
2070
  properties: {
@@ -1972,6 +2098,13 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
1972
2098
  {
1973
2099
  name: "export_audit_log",
1974
2100
  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.",
2101
+ annotations: {
2102
+ title: "Export SIEM Compliance Audit Log",
2103
+ readOnlyHint: false,
2104
+ destructiveHint: false,
2105
+ idempotentHint: true,
2106
+ openWorldHint: false
2107
+ },
1975
2108
  inputSchema: {
1976
2109
  type: "object",
1977
2110
  properties: {
@@ -2006,6 +2139,13 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
2006
2139
  {
2007
2140
  name: "generate_procurement_memo",
2008
2141
  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).",
2142
+ annotations: {
2143
+ title: "Generate CISO Procurement & ROI Memo",
2144
+ readOnlyHint: true,
2145
+ destructiveHint: false,
2146
+ idempotentHint: true,
2147
+ openWorldHint: false
2148
+ },
2009
2149
  inputSchema: {
2010
2150
  type: "object",
2011
2151
  properties: {
@@ -2178,11 +2318,51 @@ server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
2178
2318
  throw new Error(`Resource not found: ${uri}`);
2179
2319
  });
2180
2320
 
2321
+ function getEffectiveWorkspaceRoots() {
2322
+ const roots = [];
2323
+ const addCandidate = (dir) => {
2324
+ if (!dir || typeof dir !== 'string') return;
2325
+ const trimmed = dir.trim();
2326
+ if (!trimmed) return;
2327
+ try {
2328
+ const resolved = fs.existsSync(trimmed) ? fs.realpathSync(trimmed) : path.resolve(trimmed);
2329
+ if (!roots.includes(resolved)) roots.push(resolved);
2330
+ } catch (_) {
2331
+ const fallback = path.resolve(trimmed);
2332
+ if (!roots.includes(fallback)) roots.push(fallback);
2333
+ }
2334
+ };
2335
+
2336
+ addCandidate(process.cwd());
2337
+ addCandidate(process.env.PRIVACYSCRUBBER_WORKSPACE);
2338
+ addCandidate(process.env.CURSOR_WORKSPACE_PATH);
2339
+ addCandidate(process.env.VSCODE_CWD);
2340
+ addCandidate(process.env.PROJECT_ROOT);
2341
+ addCandidate(process.env.INIT_CWD);
2342
+ if (process.env.WORKSPACE_FOLDER_PATHS) {
2343
+ for (const part of process.env.WORKSPACE_FOLDER_PATHS.split(path.delimiter)) {
2344
+ addCandidate(part);
2345
+ }
2346
+ }
2347
+ return roots;
2348
+ }
2349
+
2350
+ function isWithinAnyWorkspaceRoot(targetPath, roots) {
2351
+ for (const root of roots) {
2352
+ const rel = path.relative(root, targetPath);
2353
+ if (!rel.startsWith("..") && !path.isAbsolute(rel)) {
2354
+ return { ok: true, matchedRoot: root };
2355
+ }
2356
+ }
2357
+ return { ok: false, matchedRoot: roots[0] || process.cwd() };
2358
+ }
2359
+
2181
2360
  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)) {
2361
+ const roots = getEffectiveWorkspaceRoots();
2362
+ const primaryRoot = roots[0];
2363
+ const resolvedPath = path.resolve(primaryRoot, inputPath);
2364
+ const boundaryCheck = isWithinAnyWorkspaceRoot(resolvedPath, roots);
2365
+ if (!boundaryCheck.ok) {
2186
2366
  return {
2187
2367
  ok: false,
2188
2368
  error: `Error: Security policy violation — target '${paramName}' resolves outside the active workspace directory (path traversal blocked).`
@@ -2196,15 +2376,15 @@ function resolveWorkspaceSafePath(inputPath, paramName = "file_path") {
2196
2376
  }
2197
2377
  if (fs.existsSync(checkCursor)) {
2198
2378
  const realCheck = fs.realpathSync(checkCursor);
2199
- const relReal = path.relative(workspaceRoot, realCheck);
2200
- if (relReal.startsWith("..") || path.isAbsolute(relReal)) {
2379
+ const realBoundary = isWithinAnyWorkspaceRoot(realCheck, roots);
2380
+ if (!realBoundary.ok) {
2201
2381
  return {
2202
2382
  ok: false,
2203
2383
  error: `Error: Security policy violation — target path symlink escapes the active workspace directory.`
2204
2384
  };
2205
2385
  }
2206
2386
  }
2207
- return { ok: true, resolvedPath, workspaceRoot };
2387
+ return { ok: true, resolvedPath, workspaceRoot: boundaryCheck.matchedRoot };
2208
2388
  }
2209
2389
 
2210
2390
  // Handle tool execution calls
@@ -2239,18 +2419,21 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
2239
2419
  const result = PrivacyScrubberCore.scrubText(text, customRules, {}, normalizedProfile, dryRunMap, license.isPro);
2240
2420
  const telemetry = buildCisoAuditTelemetry(result.tokenMap || {});
2241
2421
 
2422
+ const structuredAudit = {
2423
+ riskLevel: telemetry.riskLevel,
2424
+ totalDetected: telemetry.totalDetected,
2425
+ entities: telemetry.entities,
2426
+ triggeredFrameworks: telemetry.triggeredFrameworks,
2427
+ profile: targetProfile,
2428
+ executionEngine: "ZTDS Client-Side RAM"
2429
+ };
2430
+
2242
2431
  if (format === "json") {
2243
2432
  return {
2433
+ structuredContent: structuredAudit,
2244
2434
  content: [{
2245
2435
  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)
2436
+ text: JSON.stringify(structuredAudit, null, 2)
2254
2437
  }]
2255
2438
  };
2256
2439
  }
@@ -2275,6 +2458,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
2275
2458
  }
2276
2459
 
2277
2460
  return {
2461
+ structuredContent: structuredAudit,
2278
2462
  content: [{ type: "text", text: report }]
2279
2463
  };
2280
2464
  }
@@ -2324,10 +2508,10 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
2324
2508
 
2325
2509
  const charLimit = (isAdvanced && !isAutoElevated && !license.isPro) ? 5000 : 15000;
2326
2510
  if (isAdvanced && !license.isPro) {
2327
- mcpLog(`${colors.yellowBold}[PrivacyScrubber] Profile '${targetProfile}' active on Free Tier (5,000 char trial quota).${colors.reset}\n${colors.cyan}[Upgrade] Commercial TEAMS ($99/mo flat) & Developer SDK ($299/mo): https://privacyscrubber.com/pricing?tier=teams&utm_source=mcp&utm_medium=profile_quota&utm_campaign=teams_upgrade${colors.reset}\n`);
2511
+ mcpLog(`${colors.yellowBold}[PrivacyScrubber] Profile '${targetProfile}' active on Free Tier (5,000 char trial quota).${colors.reset}\n${colors.cyan}[Upgrade] 7-Day B2B Trial / TEAMS ($99/mo flat) & Developer SDK ($299/mo): https://privacyscrubber.com/pricing?tier=teams&trial=true&profile=${encodeURIComponent(targetProfile)}&utm_source=mcp&utm_medium=profile_quota&utm_campaign=teams_upgrade (or run: npx @privacyscrubber/mcp-server trial)${colors.reset}\n`);
2328
2512
  }
2329
2513
 
2330
- const { processedText, wasTruncated } = truncateIfFree(text, license.isPro, charLimit);
2514
+ const { processedText, wasTruncated } = truncateIfFree(text, license.isPro, charLimit, finalProfile);
2331
2515
 
2332
2516
  const { scrubbedText, newTokens } = performSanitization(processedText, finalProfile, currentIgnoreList, currentSession);
2333
2517
 
@@ -2335,6 +2519,16 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
2335
2519
  const receiptMd = formatAuditReceipt(telemetry, compact, wasTruncated, charLimit);
2336
2520
 
2337
2521
  return {
2522
+ structuredContent: {
2523
+ scrubbedText,
2524
+ tokensRedacted: telemetry.totalCount,
2525
+ riskLevel: telemetry.riskLevel,
2526
+ entities: telemetry.entities,
2527
+ frameworks: telemetry.frameworksList,
2528
+ profile: finalProfile,
2529
+ sessionId: session_id,
2530
+ wasTruncated: Boolean(wasTruncated)
2531
+ },
2338
2532
  content: [
2339
2533
  { type: "text", text: scrubbedText + receiptMd }
2340
2534
  ]
@@ -2404,6 +2598,11 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
2404
2598
  restoredText = restoredText.replace(/\b(mysql|postgresql|postgres|redis|mongodb|https?|ftp|ssh|git|aws):\/\/\1:\/\//gi, '$1://');
2405
2599
 
2406
2600
  return {
2601
+ structuredContent: {
2602
+ restoredText,
2603
+ restoredCount: restored.restoredCount || 0,
2604
+ sessionId: session_id
2605
+ },
2407
2606
  content: [
2408
2607
  {
2409
2608
  type: "text",
@@ -2584,9 +2783,11 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
2584
2783
  const dataStart = localOffset + 30 + lNameLen + lExtraLen;
2585
2784
  const compData = docxBuf.subarray(dataStart, dataStart + compSize);
2586
2785
  if (method === 0) {
2587
- zipEntries[entryName] = compData.toString('utf8');
2786
+ if (compData.length <= 20 * 1024 * 1024) {
2787
+ zipEntries[entryName] = compData.toString('utf8');
2788
+ }
2588
2789
  } else if (method === 8) {
2589
- zipEntries[entryName] = zlib.inflateRawSync(compData).toString('utf8');
2790
+ zipEntries[entryName] = zlib.inflateRawSync(compData, { maxOutputLength: 20 * 1024 * 1024 }).toString('utf8');
2590
2791
  }
2591
2792
  }
2592
2793
  pos += 46 + nameLen + extraLen + commentLen;
@@ -2669,10 +2870,10 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
2669
2870
 
2670
2871
  const charLimit = (isAdvanced && !license.isPro) ? 5000 : 15000;
2671
2872
  if (isAdvanced && !license.isPro) {
2672
- mcpLog(`${colors.yellowBold}[PrivacyScrubber] Profile '${targetProfile}' active on Free Tier (5,000 char trial quota).${colors.reset}\n${colors.cyan}[Upgrade] Commercial TEAMS ($99/mo flat) & Developer SDK ($299/mo): https://privacyscrubber.com/pricing?tier=teams&utm_source=mcp&utm_medium=profile_quota&utm_campaign=teams_upgrade${colors.reset}\n`);
2873
+ mcpLog(`${colors.yellowBold}[PrivacyScrubber] Profile '${targetProfile}' active on Free Tier (5,000 char trial quota).${colors.reset}\n${colors.cyan}[Upgrade] 7-Day B2B Trial / TEAMS ($99/mo flat) & Developer SDK ($299/mo): https://privacyscrubber.com/pricing?tier=teams&trial=true&profile=${encodeURIComponent(targetProfile)}&utm_source=mcp&utm_medium=profile_quota&utm_campaign=teams_upgrade (or run: npx @privacyscrubber/mcp-server trial)${colors.reset}\n`);
2673
2874
  }
2674
2875
 
2675
- const { processedText: processedContent, wasTruncated } = truncateIfFree(content, license.isPro, charLimit);
2876
+ const { processedText: processedContent, wasTruncated } = truncateIfFree(content, license.isPro, charLimit, finalProfile);
2676
2877
 
2677
2878
  const sanitized = performSanitization(processedContent, finalProfile, targetIgnoreList, targetSession);
2678
2879
  const { scrubbedText, newTokens } = sanitized;
@@ -2900,16 +3101,26 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
2900
3101
 
2901
3102
  const charLimit = (isAdvanced && !isAutoElevated && !license.isPro) ? 5000 : 15000;
2902
3103
  if (isAdvanced && !license.isPro) {
2903
- mcpLog(`${colors.yellowBold}[PrivacyScrubber] Profile '${targetProfile}' active on Free Tier (5,000 char trial quota).${colors.reset}\n${colors.cyan}[Upgrade] Commercial TEAMS ($99/mo flat) & Developer SDK ($299/mo): https://privacyscrubber.com/pricing?tier=teams&utm_source=mcp&utm_medium=profile_quota&utm_campaign=teams_upgrade${colors.reset}\n`);
3104
+ mcpLog(`${colors.yellowBold}[PrivacyScrubber] Profile '${targetProfile}' active on Free Tier (5,000 char trial quota).${colors.reset}\n${colors.cyan}[Upgrade] 7-Day B2B Trial / TEAMS ($99/mo flat) & Developer SDK ($299/mo): https://privacyscrubber.com/pricing?tier=teams&trial=true&profile=${encodeURIComponent(targetProfile)}&utm_source=mcp&utm_medium=profile_quota&utm_campaign=teams_upgrade (or run: npx @privacyscrubber/mcp-server trial)${colors.reset}\n`);
2904
3105
  }
2905
3106
 
2906
- const { processedText: processedContent2, wasTruncated } = truncateIfFree(content, license.isPro, charLimit);
3107
+ const { processedText: processedContent2, wasTruncated } = truncateIfFree(content, license.isPro, charLimit, finalProfile);
2907
3108
 
2908
3109
  const { scrubbedText, newTokens } = performSanitization(processedContent2, finalProfile, targetIgnoreList, targetSession);
2909
3110
  const telemetry = buildCisoAuditTelemetry(newTokens);
2910
3111
  const receiptMd = formatAuditReceipt(telemetry, compact, wasTruncated, charLimit);
2911
3112
 
2912
3113
  return {
3114
+ structuredContent: {
3115
+ scrubbedText,
3116
+ tokensRedacted: telemetry.totalCount,
3117
+ riskLevel: telemetry.riskLevel,
3118
+ entities: telemetry.entities,
3119
+ frameworks: telemetry.frameworksList,
3120
+ profile: finalProfile,
3121
+ sessionId: session_id,
3122
+ wasTruncated: Boolean(wasTruncated)
3123
+ },
2913
3124
  content: [
2914
3125
  { type: "text", text: scrubbedText + receiptMd }
2915
3126
  ]
@@ -2931,7 +3142,11 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
2931
3142
  }
2932
3143
 
2933
3144
  try {
2934
- const stat = await fs.promises.stat(resolvedRedactPath);
3145
+ const lstat = await fs.promises.lstat(resolvedRedactPath);
3146
+ if (lstat.isSymbolicLink() || !lstat.isFile()) {
3147
+ return { isError: true, content: [{ type: "text", text: `Error: Target must be a regular non-symlink file.` }] };
3148
+ }
3149
+ const stat = lstat;
2935
3150
  if (stat.size > 50 * 1024 * 1024) {
2936
3151
  return { isError: true, content: [{ type: "text", text: `Error: File is too large (>50MB).` }] };
2937
3152
  }
@@ -3229,20 +3444,23 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
3229
3444
  async function walkDir(dir) {
3230
3445
  const entries = await fs.promises.readdir(dir, { withFileTypes: true });
3231
3446
  for (const entry of entries) {
3447
+ if (entry.isSymbolicLink && entry.isSymbolicLink()) continue;
3232
3448
  const fullPath = path.join(dir, entry.name);
3449
+ const safeChild = resolveWorkspaceSafePath(fullPath, "directory_path");
3450
+ if (!safeChild.ok) continue;
3233
3451
  if (entry.isDirectory()) {
3234
- if (!ignoreDirs.includes(entry.name)) await walkDir(fullPath);
3452
+ if (!ignoreDirs.includes(entry.name)) await walkDir(safeChild.resolvedPath);
3235
3453
  } else if (entry.isFile()) {
3236
3454
  if (extensions && extensions.length > 0) {
3237
3455
  const ext = path.extname(entry.name);
3238
3456
  if (!extensions.includes(ext)) continue;
3239
3457
  }
3240
- filesToScan.push(fullPath);
3458
+ filesToScan.push(safeChild.resolvedPath);
3241
3459
  }
3242
3460
  }
3243
3461
  }
3244
3462
 
3245
- await walkDir(directory_path);
3463
+ await walkDir(safeDir.resolvedPath);
3246
3464
 
3247
3465
  let reportLines = [`## Security Audit Report: ${directory_path}`];
3248
3466
  let filesWithIssues = 0;
@@ -3382,8 +3600,8 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
3382
3600
  const isAdvanced = targetProfile.toLowerCase() !== "general";
3383
3601
  const charLimit = (isAdvanced && !license.isPro) ? 5000 : 15000;
3384
3602
 
3385
- const { processedText: cleanStdout, wasTruncated: truncOut } = truncateIfFree(stdout, license.isPro, charLimit);
3386
- const { processedText: cleanStderr, wasTruncated: truncErr } = truncateIfFree(stderr, license.isPro, charLimit);
3603
+ const { processedText: cleanStdout, wasTruncated: truncOut } = truncateIfFree(stdout, license.isPro, charLimit, targetProfile);
3604
+ const { processedText: cleanStderr, wasTruncated: truncErr } = truncateIfFree(stderr, license.isPro, charLimit, targetProfile);
3387
3605
 
3388
3606
  const resCommand = performSanitization(command, targetProfile, targetIgnoreList, targetSession);
3389
3607
  const resStdout = performSanitization(cleanStdout, targetProfile, targetIgnoreList, targetSession);
@@ -3443,23 +3661,72 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
3443
3661
  };
3444
3662
  }
3445
3663
  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
- }
3664
+ const ext = path.extname(resolvedPath).toLowerCase();
3455
3665
  const license = checkLicenseStatus();
3456
3666
  const limitStatus = checkFreeTierLimit(license.isPro);
3457
3667
  if (limitStatus.blocked) {
3458
3668
  return buildDailyLimitBlockedResponse();
3459
3669
  }
3460
3670
 
3461
- let rawContent = rawBuffer.toString('utf8');
3462
- const ext = path.extname(resolvedPath).toLowerCase();
3671
+ let rawContent = '';
3672
+ if (ext === '.docx') {
3673
+ try {
3674
+ const mammoth = require('mammoth');
3675
+ const result = await mammoth.extractRawText({ buffer: rawBuffer });
3676
+ rawContent = result.value || '';
3677
+ } catch (docxErr) {
3678
+ return {
3679
+ isError: true,
3680
+ content: [{ type: "text", text: `Error: Failed to parse DOCX file: ${docxErr.message}` }]
3681
+ };
3682
+ }
3683
+ } else if (ext === '.pdf' || ext === '.xlsx' || ext === '.xls') {
3684
+ if (!license.isPro) {
3685
+ return {
3686
+ isError: true,
3687
+ content: [{
3688
+ type: "text",
3689
+ 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`
3690
+ }]
3691
+ };
3692
+ }
3693
+ if (ext === '.pdf') {
3694
+ try {
3695
+ const pdf = require('pdf-parse');
3696
+ const data = await pdf(rawBuffer);
3697
+ rawContent = data.text || '';
3698
+ } catch (pdfErr) {
3699
+ return {
3700
+ isError: true,
3701
+ content: [{ type: "text", text: `Error: Failed to parse PDF file: ${pdfErr.message}` }]
3702
+ };
3703
+ }
3704
+ } else {
3705
+ try {
3706
+ const XLSX = require('xlsx');
3707
+ const workbook = XLSX.read(rawBuffer, { type: 'buffer' });
3708
+ workbook.SheetNames.forEach(sheetName => {
3709
+ rawContent += `--- Sheet: ${sheetName} ---\n` + XLSX.utils.sheet_to_csv(workbook.Sheets[sheetName]) + '\n';
3710
+ });
3711
+ } catch (xlsxErr) {
3712
+ return {
3713
+ isError: true,
3714
+ content: [{ type: "text", text: `Error: Failed to parse Excel file: ${xlsxErr.message}` }]
3715
+ };
3716
+ }
3717
+ }
3718
+ } else {
3719
+ const checkLen = Math.min(rawBuffer.length, 8000);
3720
+ for (let i = 0; i < checkLen; i++) {
3721
+ if (rawBuffer[i] === 0) {
3722
+ return {
3723
+ isError: true,
3724
+ content: [{ type: "text", text: `Error: Binary file format detected. Only plain text, source code, and document formats (.docx, .pdf, .xlsx, .eml, .rtf) are supported.` }]
3725
+ };
3726
+ }
3727
+ }
3728
+ rawContent = rawBuffer.toString('utf8');
3729
+ }
3463
3730
  if (ext === '.eml') {
3464
3731
  const decodeQpUtf8 = (str) =>
3465
3732
  str.replace(/=\r?\n/g, '').replace(/(?:=[0-9A-Fa-f]{2})+/g, (seq) => {
@@ -3503,7 +3770,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
3503
3770
  const isAdvanced = targetProfile.toLowerCase() !== "general";
3504
3771
  const charLimit = (isAdvanced && !license.isPro) ? 5000 : 15000;
3505
3772
 
3506
- const { processedText, wasTruncated: wasCharTruncated } = truncateIfFree(capped, license.isPro, charLimit);
3773
+ const { processedText, wasTruncated: wasCharTruncated } = truncateIfFree(capped, license.isPro, charLimit, targetProfile);
3507
3774
  const currentSession = getSessionMap(session_id);
3508
3775
  const currentIgnoreList = getSessionIgnoreList(session_id);
3509
3776
  const { scrubbedText, newTokens } = performSanitization(processedText, targetProfile, currentIgnoreList, currentSession);
@@ -3567,7 +3834,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
3567
3834
  const isAdvanced = targetProfile.toLowerCase() !== "general";
3568
3835
  const charLimit = (isAdvanced && !license.isPro) ? 5000 : 15000;
3569
3836
 
3570
- const { processedText, wasTruncated } = truncateIfFree(diffOutput, license.isPro, charLimit);
3837
+ const { processedText, wasTruncated } = truncateIfFree(diffOutput, license.isPro, charLimit, targetProfile);
3571
3838
  const { scrubbedText, newTokens } = performSanitization(processedText, targetProfile, targetIgnoreList, targetSession);
3572
3839
 
3573
3840
  const telemetry = buildCisoAuditTelemetry(newTokens);
@@ -3599,28 +3866,26 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
3599
3866
  };
3600
3867
  }
3601
3868
 
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)) {
3869
+ const safeTarget = resolveWorkspaceSafePath(file_path, "file_path");
3870
+ if (!safeTarget.ok) {
3606
3871
  return {
3607
3872
  isError: true,
3608
- content: [{ type: "text", text: "Error: Security policy violation — target 'file_path' resolves outside the active workspace directory (path traversal blocked)." }]
3873
+ content: [{ type: "text", text: safeTarget.error }]
3609
3874
  };
3610
3875
  }
3611
- let checkCursor = resolvedPath;
3612
- while (!fs.existsSync(checkCursor)) {
3613
- const parentCursor = path.dirname(checkCursor);
3614
- if (parentCursor === checkCursor) break;
3615
- checkCursor = parentCursor;
3876
+ const resolvedPath = safeTarget.resolvedPath;
3877
+
3878
+ const license = checkLicenseStatus();
3879
+ const limitStatus = checkFreeTierLimit(license.isPro);
3880
+ if (limitStatus.blocked) {
3881
+ return buildDailyLimitBlockedResponse();
3616
3882
  }
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)) {
3883
+ if (fs.existsSync(resolvedPath)) {
3884
+ const existingLstat = fs.lstatSync(resolvedPath);
3885
+ if (existingLstat.isSymbolicLink() || !existingLstat.isFile()) {
3621
3886
  return {
3622
3887
  isError: true,
3623
- content: [{ type: "text", text: "Error: Security policy violation — target path symlink escapes the active workspace directory." }]
3888
+ content: [{ type: "text", text: "Error: Security policy violation — target must be a regular non-symlink file." }]
3624
3889
  };
3625
3890
  }
3626
3891
  }
@@ -3665,7 +3930,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
3665
3930
  }
3666
3931
  targetDir = safeWorkspace.resolvedPath;
3667
3932
  }
3668
- const targets = agent_types.includes("all") ? ["cursor", "windsurf", "claude_code", "copilot", "cline"] : agent_types;
3933
+ const targets = agent_types.includes("all") ? ["cursor", "windsurf", "claude_code", "copilot", "cline", "agents_md"] : agent_types;
3669
3934
 
3670
3935
  const ruleText = `# PrivacyScrubber Zero-Trust Agentic Guard (CISO Security Rules)
3671
3936
  # 100% Local In-Memory Data Sanitization. Zero Network Egress.
@@ -3689,6 +3954,14 @@ Before reading sensitive files, running terminal commands that may print credent
3689
3954
  const p = path.join(targetDir, '.cursorrules');
3690
3955
  fs.writeFileSync(p, ruleText, 'utf8');
3691
3956
  generatedFiles.push('.cursorrules');
3957
+
3958
+ const cursorRulesDir = path.join(targetDir, '.cursor', 'rules');
3959
+ try {
3960
+ if (!fs.existsSync(cursorRulesDir)) fs.mkdirSync(cursorRulesDir, { recursive: true });
3961
+ const mdcContent = `---\ndescription: PrivacyScrubber Zero-Trust Data Sanitization (ZTDS) Agentic Guard\nglobs: *\nalwaysApply: true\n---\n\n${ruleText}`;
3962
+ fs.writeFileSync(path.join(cursorRulesDir, 'privacyscrubber.mdc'), mdcContent, 'utf8');
3963
+ generatedFiles.push('.cursor/rules/privacyscrubber.mdc');
3964
+ } catch (_) {}
3692
3965
  }
3693
3966
  if (targets.includes("windsurf")) {
3694
3967
  const p = path.join(targetDir, '.windsurfrules');
@@ -3708,6 +3981,19 @@ Before reading sensitive files, running terminal commands that may print credent
3708
3981
  generatedFiles.push('CLAUDE.md');
3709
3982
  }
3710
3983
  }
3984
+ if (targets.includes("agents_md") || targets.includes("codex")) {
3985
+ const p = path.join(targetDir, 'AGENTS.md');
3986
+ if (fs.existsSync(p)) {
3987
+ const existing = fs.readFileSync(p, 'utf8');
3988
+ if (!existing.includes("Zero-Trust Agentic Guard")) {
3989
+ fs.writeFileSync(p, existing + "\n\n" + ruleText, 'utf8');
3990
+ generatedFiles.push('AGENTS.md (appended)');
3991
+ }
3992
+ } else {
3993
+ fs.writeFileSync(p, ruleText, 'utf8');
3994
+ generatedFiles.push('AGENTS.md');
3995
+ }
3996
+ }
3711
3997
  if (targets.includes("copilot")) {
3712
3998
  const copilotDir = path.join(targetDir, '.github');
3713
3999
  if (!fs.existsSync(copilotDir)) fs.mkdirSync(copilotDir, { recursive: true });
@@ -3721,7 +4007,57 @@ Before reading sensitive files, running terminal commands that may print credent
3721
4007
  generatedFiles.push('.clinerules');
3722
4008
  }
3723
4009
 
4010
+ if (args?.include_hooks !== false && (targets.includes("cursor") || targets.includes("claude_code") || agent_types.includes("all"))) {
4011
+ const hookCmd = "npx -y @privacyscrubber/mcp-server --hook";
4012
+ if (targets.includes("cursor") || agent_types.includes("all")) {
4013
+ try {
4014
+ const cursorHooksPath = path.join(targetDir, '.cursor', 'hooks.json');
4015
+ fs.mkdirSync(path.dirname(cursorHooksPath), { recursive: true });
4016
+ let cursorHooks = { version: 1, hooks: {} };
4017
+ if (fs.existsSync(cursorHooksPath)) {
4018
+ try { cursorHooks = JSON.parse(fs.readFileSync(cursorHooksPath, 'utf8')); } catch (_) {}
4019
+ }
4020
+ if (!cursorHooks.hooks || typeof cursorHooks.hooks !== 'object') cursorHooks.hooks = {};
4021
+ for (const ev of ['beforeReadFile', 'beforeShellExecution']) {
4022
+ if (!Array.isArray(cursorHooks.hooks[ev])) cursorHooks.hooks[ev] = [];
4023
+ if (!cursorHooks.hooks[ev].some(h => h && h.command && h.command.includes('privacyscrubber'))) {
4024
+ cursorHooks.hooks[ev].push({ command: hookCmd });
4025
+ }
4026
+ }
4027
+ fs.writeFileSync(cursorHooksPath, JSON.stringify(cursorHooks, null, 2) + '\n', 'utf8');
4028
+ generatedFiles.push('.cursor/hooks.json');
4029
+ } catch (_) {}
4030
+ }
4031
+ if (targets.includes("claude_code") || agent_types.includes("all")) {
4032
+ try {
4033
+ const claudeSettingsPath = path.join(targetDir, '.claude', 'settings.json');
4034
+ fs.mkdirSync(path.dirname(claudeSettingsPath), { recursive: true });
4035
+ let claudeSettings = {};
4036
+ if (fs.existsSync(claudeSettingsPath)) {
4037
+ try { claudeSettings = JSON.parse(fs.readFileSync(claudeSettingsPath, 'utf8')); } catch (_) {}
4038
+ }
4039
+ if (!claudeSettings.hooks || typeof claudeSettings.hooks !== 'object') claudeSettings.hooks = {};
4040
+ if (!Array.isArray(claudeSettings.hooks.PreToolUse)) claudeSettings.hooks.PreToolUse = [];
4041
+ const hasClaudeHook = claudeSettings.hooks.PreToolUse.some(entry =>
4042
+ Array.isArray(entry?.hooks) && entry.hooks.some(h => h && h.command && h.command.includes('privacyscrubber'))
4043
+ );
4044
+ if (!hasClaudeHook) {
4045
+ claudeSettings.hooks.PreToolUse.push({
4046
+ matcher: "Read|Bash",
4047
+ hooks: [{ type: "command", command: hookCmd }]
4048
+ });
4049
+ }
4050
+ fs.writeFileSync(claudeSettingsPath, JSON.stringify(claudeSettings, null, 2) + '\n', 'utf8');
4051
+ generatedFiles.push('.claude/settings.json');
4052
+ } catch (_) {}
4053
+ }
4054
+ }
4055
+
3724
4056
  return {
4057
+ structuredContent: {
4058
+ workspace: targetDir,
4059
+ generatedFiles
4060
+ },
3725
4061
  content: [{
3726
4062
  type: "text",
3727
4063
  text: `[Zero-Trust Agentic Guard: Agent Rules Generated]\n` +
@@ -3744,22 +4080,38 @@ Before reading sensitive files, running terminal commands that may print credent
3744
4080
  }
3745
4081
 
3746
4082
  const targetSession = getSessionMap(session_id);
4083
+ const seenUnmask = new WeakMap();
3747
4084
 
3748
4085
  function unmaskRecursive(val) {
3749
4086
  if (typeof val === 'string') {
3750
4087
  return reverseTokens(val, targetSession);
3751
4088
  }
3752
- if (Array.isArray(val)) {
3753
- return val.map(unmaskRecursive);
4089
+ if (val === null || typeof val !== 'object') return val;
4090
+ if (
4091
+ val instanceof Date ||
4092
+ val instanceof RegExp ||
4093
+ val instanceof Error ||
4094
+ ArrayBuffer.isView(val) ||
4095
+ (typeof ArrayBuffer !== 'undefined' && val instanceof ArrayBuffer)
4096
+ ) {
4097
+ return val;
3754
4098
  }
3755
- if (val !== null && typeof val === 'object') {
3756
- const res = {};
3757
- for (const [k, v] of Object.entries(val)) {
3758
- res[k] = unmaskRecursive(v);
4099
+ if (seenUnmask.has(val)) return seenUnmask.get(val);
4100
+ if (Array.isArray(val)) {
4101
+ const outArr = [];
4102
+ seenUnmask.set(val, outArr);
4103
+ for (let i = 0; i < val.length; i++) {
4104
+ outArr.push(unmaskRecursive(val[i]));
3759
4105
  }
3760
- return res;
4106
+ return outArr;
3761
4107
  }
3762
- return val;
4108
+ const res = {};
4109
+ seenUnmask.set(val, res);
4110
+ for (const [k, v] of Object.entries(val)) {
4111
+ if (k === '__proto__' || k === 'constructor' || k === 'prototype') continue;
4112
+ res[k] = unmaskRecursive(v);
4113
+ }
4114
+ return res;
3763
4115
  }
3764
4116
 
3765
4117
  const unmasked = unmaskRecursive(targetArgs);
@@ -3772,17 +4124,20 @@ Before reading sensitive files, running terminal commands that may print credent
3772
4124
  }
3773
4125
  }
3774
4126
 
4127
+ const unmaskPayload = {
4128
+ status: "success",
4129
+ success: true,
4130
+ session_id,
4131
+ tokens_restored: tokensRestored,
4132
+ unmasked_count: tokensRestored,
4133
+ unmasked_args: unmasked
4134
+ };
4135
+
3775
4136
  return {
4137
+ structuredContent: unmaskPayload,
3776
4138
  content: [{
3777
4139
  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)
4140
+ text: JSON.stringify(unmaskPayload, null, 2)
3786
4141
  }]
3787
4142
  };
3788
4143
  }
@@ -3796,6 +4151,7 @@ Before reading sensitive files, running terminal commands that may print credent
3796
4151
  const session = agentSessions.get(sid);
3797
4152
  if (!session) {
3798
4153
  return {
4154
+ structuredContent: { sessionId: sid, exists: false, totalTokens: 0 },
3799
4155
  content: [{
3800
4156
  type: "text",
3801
4157
  text: `[Zero-Trust Agentic Guard: Session Info]\nSession '${sid}' does not exist or has expired.`
@@ -3806,6 +4162,16 @@ Before reading sensitive files, running terminal commands that may print credent
3806
4162
  const telemetry = buildCisoAuditTelemetry(session.map);
3807
4163
  const streamBufferLen = session.streamDetokenizer ? session.streamDetokenizer.buffer.length : 0;
3808
4164
  return {
4165
+ structuredContent: {
4166
+ sessionId: sid,
4167
+ exists: true,
4168
+ createdAt: new Date(session.createdAt).toISOString(),
4169
+ lastActive: new Date(session.lastActive).toISOString(),
4170
+ totalTokens: tokenEntries.length,
4171
+ streamBufferBytes: streamBufferLen,
4172
+ entities: telemetry.entities,
4173
+ riskLevel: telemetry.riskLevel
4174
+ },
3809
4175
  content: [{
3810
4176
  type: "text",
3811
4177
  text: `[Zero-Trust Agentic Guard: Session Info - ${sid}]\n` +
@@ -3830,6 +4196,13 @@ Before reading sensitive files, running terminal commands that may print credent
3830
4196
  }
3831
4197
 
3832
4198
  return {
4199
+ structuredContent: {
4200
+ activeSessions: agentSessions.size,
4201
+ totalTokens,
4202
+ heapUsedMb: Number((memUsage.heapUsed / (1024 * 1024)).toFixed(2)),
4203
+ uptimeSeconds: Math.floor(process.uptime()),
4204
+ sessions: sessionList
4205
+ },
3833
4206
  content: [{
3834
4207
  type: "text",
3835
4208
  text: `[Zero-Trust Agentic Guard: Global Session Diagnostics]\n` +
@@ -3928,19 +4301,21 @@ Before reading sensitive files, running terminal commands that may print credent
3928
4301
 
3929
4302
  const telemetry = buildCisoAuditTelemetry(targetSession);
3930
4303
  const auditReceipt = formatAuditReceipt(telemetry);
4304
+ const ragChunkPayload = {
4305
+ status: "success",
4306
+ session_id,
4307
+ total_chunks: chunks.length,
4308
+ tokens_redacted_in_batch: totalTokensRedacted,
4309
+ total_session_tokens: Object.keys(targetSession).length,
4310
+ risk_level: telemetry.riskLevel,
4311
+ sanitized_chunks: sanitizedChunks
4312
+ };
3931
4313
 
3932
4314
  return {
4315
+ structuredContent: ragChunkPayload,
3933
4316
  content: [{
3934
4317
  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}`
4318
+ text: JSON.stringify(ragChunkPayload, null, 2) + `\n\n${auditReceipt}`
3944
4319
  }]
3945
4320
  };
3946
4321
  }
@@ -3988,16 +4363,19 @@ Before reading sensitive files, running terminal commands that may print credent
3988
4363
  }
3989
4364
  }
3990
4365
 
4366
+ const ragRestorePayload = {
4367
+ status: "success",
4368
+ session_id,
4369
+ total_results: results.length,
4370
+ tokens_restored: totalTokensRestored,
4371
+ restored_results: restoredResults
4372
+ };
4373
+
3991
4374
  return {
4375
+ structuredContent: ragRestorePayload,
3992
4376
  content: [{
3993
4377
  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)
4378
+ text: JSON.stringify(ragRestorePayload, null, 2)
4001
4379
  }]
4002
4380
  };
4003
4381
  }
@@ -4026,6 +4404,9 @@ Before reading sensitive files, running terminal commands that may print credent
4026
4404
  const sessionHash = receipt.receiptSha256;
4027
4405
 
4028
4406
  let outputText = "";
4407
+ const safeCompanyName = String(company_name || "Organization").replace(/[\r\n\x00-\x1f]/g, " ").trim().slice(0, 120);
4408
+ const safeDepartment = String(department || "Security & Compliance").replace(/[\r\n\x00-\x1f]/g, " ").trim().slice(0, 120);
4409
+
4029
4410
  const eventData = {
4030
4411
  vendor: "PrivacyScrubber",
4031
4412
  product: "ZTDS",
@@ -4036,8 +4417,8 @@ Before reading sensitive files, running terminal commands that may print credent
4036
4417
  entities: telemetry.entities,
4037
4418
  frameworks: telemetry.frameworksList,
4038
4419
  sessionHash,
4039
- organization: company_name,
4040
- department
4420
+ organization: safeCompanyName,
4421
+ department: safeDepartment
4041
4422
  };
4042
4423
 
4043
4424
  if (format === "cef") {
@@ -4045,19 +4426,35 @@ Before reading sensitive files, running terminal commands that may print credent
4045
4426
  } else if (format === "syslog") {
4046
4427
  outputText = formatSyslogEvent(eventData);
4047
4428
  } else if (format === "json" || format === "markdown") {
4048
- outputText = formatPsr2Report({ format, receipt, telemetry, company_name, department, utmSource: 'mcp_audit_export', utmMedium: format });
4429
+ outputText = formatPsr2Report({ format, receipt, telemetry, company_name: safeCompanyName, department: safeDepartment, utmSource: 'mcp_audit_export', utmMedium: format });
4049
4430
  } else {
4050
4431
  // Default: jsonl
4051
4432
  outputText = formatJsonlEvent(eventData);
4052
4433
  }
4053
4434
 
4054
4435
  if (typeof destination_file === 'string' && destination_file.trim()) {
4436
+ const safeDest = resolveWorkspaceSafePath(destination_file.trim(), "destination_file");
4437
+ if (!safeDest.ok) {
4438
+ return {
4439
+ isError: true,
4440
+ content: [{ type: "text", text: safeDest.error }]
4441
+ };
4442
+ }
4055
4443
  try {
4056
- const dir = path.dirname(destination_file);
4444
+ if (fs.existsSync(safeDest.resolvedPath)) {
4445
+ const st = fs.lstatSync(safeDest.resolvedPath);
4446
+ if (st.isSymbolicLink() || !st.isFile()) {
4447
+ return {
4448
+ isError: true,
4449
+ content: [{ type: "text", text: "Error: Security policy violation — destination_file must be a regular non-symlink file." }]
4450
+ };
4451
+ }
4452
+ }
4453
+ const dir = path.dirname(safeDest.resolvedPath);
4057
4454
  if (!fs.existsSync(dir)) {
4058
4455
  fs.mkdirSync(dir, { recursive: true });
4059
4456
  }
4060
- fs.appendFileSync(destination_file, outputText + '\n', 'utf8');
4457
+ fs.appendFileSync(safeDest.resolvedPath, outputText + '\n', 'utf8');
4061
4458
  } catch (e) {
4062
4459
  return {
4063
4460
  isError: true,
@@ -4067,6 +4464,15 @@ Before reading sensitive files, running terminal commands that may print credent
4067
4464
  }
4068
4465
 
4069
4466
  return {
4467
+ structuredContent: {
4468
+ format,
4469
+ riskLevel: telemetry.riskLevel,
4470
+ tokensMasked: telemetry.totalCount,
4471
+ entities: telemetry.entities,
4472
+ frameworks: telemetry.frameworksList,
4473
+ receiptSha256: sessionHash,
4474
+ timestamp
4475
+ },
4070
4476
  content: [{
4071
4477
  type: "text",
4072
4478
  text: outputText
@@ -4234,10 +4640,11 @@ export function performSanitization(text, profile, ignoreList = null, customSess
4234
4640
  return { scrubbedText: result.scrubbedText, newTokens };
4235
4641
  }
4236
4642
 
4237
- export function truncateIfFree(text, isPro, charLimit = 15000) {
4643
+ export function truncateIfFree(text, isPro, charLimit = 15000, profile = 'general') {
4238
4644
  if (!isPro && text.length > charLimit) {
4239
- mcpLog(`${colors.yellowBold}[PrivacyScrubber] Input truncated to ${charLimit.toLocaleString()} characters (Free Tier Limit).${colors.reset}\n${colors.cyan}[Upgrade] Set PRIVACYSCRUBBER_KEY to your PRO/TEAMS/SDK license key for unlimited throughput: https://privacyscrubber.com/pricing?tier=teams&utm_source=mcp&utm_medium=truncation&utm_campaign=teams_upgrade${colors.reset}\n`);
4240
- const upsellNotice = `\n\n[PrivacyScrubber Free Tier: Payload truncated to ${charLimit.toLocaleString()} chars. Upgrade to PRO ($15/mo or $110 Lifetime) or Developer SDK ($299/mo) for unlimited payload processing: https://privacyscrubber.com/pricing?tier=teams&utm_source=mcp&utm_medium=truncation&utm_campaign=teams_upgrade]`;
4645
+ const safeProf = encodeURIComponent(String(profile || 'general').toLowerCase());
4646
+ mcpLog(`${colors.yellowBold}[PrivacyScrubber] Input truncated to ${charLimit.toLocaleString()} characters (Free Tier Limit for profile '${profile || 'general'}').${colors.reset}\n${colors.cyan}[Upgrade] Set PRIVACYSCRUBBER_KEY to your PRO/TEAMS/SDK license key or start a 7-day B2B trial ('npx @privacyscrubber/mcp-server trial'): https://privacyscrubber.com/pricing?tier=teams&trial=true&profile=${safeProf}&utm_source=mcp&utm_medium=truncation&utm_campaign=teams_upgrade${colors.reset}\n`);
4647
+ const upsellNotice = `\n\n[PrivacyScrubber Free Tier: Payload truncated to ${charLimit.toLocaleString()} chars (${profile || 'general'}). Upgrade to PRO ($15/mo or $110 LT), TEAMS ($99/mo flat) or Developer SDK ($299/mo): https://privacyscrubber.com/pricing?tier=teams&trial=true&profile=${safeProf}&utm_source=mcp&utm_medium=truncation&utm_campaign=teams_upgrade | CLI Trial: npx @privacyscrubber/mcp-server trial]`;
4241
4648
  return { processedText: text.substring(0, charLimit), wasTruncated: true, upsellNotice, charLimit };
4242
4649
  }
4243
4650
  return { processedText: text, wasTruncated: false, upsellNotice: "", charLimit };