@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/README.md +14 -7
- package/index.js +578 -171
- package/install.js +94 -3
- package/manifest.json +8 -10
- package/package.json +1 -3
- package/pii-masking-run.js +427 -45
- package/ps-license-manager.cjs +1 -1
- package/ps-pii-engine.cjs +1 -1
- package/scrubber-core.cjs +1 -1
- package/server.json +3 -3
- package/ps-license-manager.js +0 -11
- package/ps-pii-engine.js +0 -11
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 &
|
|
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
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
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
|
|
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
|
|
2183
|
-
const
|
|
2184
|
-
const
|
|
2185
|
-
|
|
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
|
|
2200
|
-
if (
|
|
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]
|
|
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
|
-
|
|
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]
|
|
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]
|
|
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
|
|
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(
|
|
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(
|
|
3458
|
+
filesToScan.push(safeChild.resolvedPath);
|
|
3241
3459
|
}
|
|
3242
3460
|
}
|
|
3243
3461
|
}
|
|
3244
3462
|
|
|
3245
|
-
await walkDir(
|
|
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
|
|
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 =
|
|
3462
|
-
|
|
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
|
|
3603
|
-
|
|
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:
|
|
3873
|
+
content: [{ type: "text", text: safeTarget.error }]
|
|
3609
3874
|
};
|
|
3610
3875
|
}
|
|
3611
|
-
|
|
3612
|
-
|
|
3613
|
-
|
|
3614
|
-
|
|
3615
|
-
|
|
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(
|
|
3618
|
-
const
|
|
3619
|
-
|
|
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
|
|
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 (
|
|
3753
|
-
|
|
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
|
|
3756
|
-
|
|
3757
|
-
|
|
3758
|
-
|
|
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
|
|
4106
|
+
return outArr;
|
|
3761
4107
|
}
|
|
3762
|
-
|
|
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:
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
4240
|
-
|
|
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 };
|