@privacyscrubber/mcp-server 2.3.9 → 2.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/scrubber-core.cjs CHANGED
@@ -13,6 +13,8 @@
13
13
 
14
14
  function getEngine() {
15
15
  if (typeof window !== 'undefined' && window.PrivacyScrubberEngine) return window.PrivacyScrubberEngine;
16
+ if (typeof globalThis !== 'undefined' && globalThis.PrivacyScrubberEngine) return globalThis.PrivacyScrubberEngine;
17
+ if (typeof self !== 'undefined' && self.PrivacyScrubberEngine) return self.PrivacyScrubberEngine;
16
18
  if (typeof global !== 'undefined' && global.PrivacyScrubberEngine) return global.PrivacyScrubberEngine;
17
19
  if (typeof require === 'function') {
18
20
  try { return require('./ps-pii-engine.cjs'); } catch(e) {}
@@ -37,11 +39,15 @@
37
39
 
38
40
  function stitchOrphanedNameLines(text, profile) {
39
41
  if (!text || !text.includes('\n')) return text;
40
- const prof = (profile || 'general').toLowerCase();
42
+ const prof = (typeof profile === 'string' ? profile : 'general').toLowerCase();
41
43
  if (prof === 'medical' || prof === 'general') {
42
- text = text.replace(/(Patient\s+Name|Emergency\s+Contact|In\s+Case\s+of\s+Emergency|ICE(?:\s+Contact)?):\s*\r?\n+([A-Z\p{Lu}][A-Za-z0-9&.,'’ \t-]*?[A-Za-z0-9\p{Lu}\p{Ll}])(?=\r?\n|$)/gui, '$1: $2');
44
+ text = text.replace(/(Patient\s+Name|Emergency\s+Contact|In\s+Case\s+of\s+Emergency|ICE(?:\s+Contact)?|Subject\s+Personnel|Personnel|Employee):\s*\r?\n+([A-Z\p{Lu}][A-Za-z0-9&.,'’ \t-]*?[A-Za-z0-9\p{Lu}\p{Ll}])(?=\r?\n|$)/gui, '$1: $2');
43
45
  } else if (prof === 'legal') {
44
- text = text.replace(/Defendant:\s*\r?\n+([A-Z][a-zA-Z]+\s[A-Z][a-zA-Z]+)/g, 'Defendant: $1');
46
+ text = text.replace(/Defendant:\s*\r?\n+([A-Z\p{Lu}][\p{L}'’–-]+\s+[A-Z\p{Lu}][\p{L}'’–-]+)/gu, 'Defendant: $1');
47
+ } else if (prof === 'personal' || prof === 'vip' || prof === 'executive') {
48
+ text = text.replace(/(Close\s+Protection\s+Officer|CPO|Bodyguard|Detail\s+Leader|Shift\s+Leader|Protection\s+Agent|Estate\s+Manager|Private\s+Butler|Chauffeur|Personal\s+Driver):\s*\r?\n+([A-Z\p{Lu}][\p{L}'’–-]+\s+[A-Z\p{Lu}][\p{L}'’–-]+)/gu, '$1: $2');
49
+ } else if (prof === 'agents' || prof === 'ai_agents') {
50
+ text = text.replace(/(AI\s+Research\s+Scientist|Lead\s+ML\s+Engineer|Machine\s+Learning\s+Engineer|Prompt\s+Engineer|Agent\s+Architect|Agentic\s+Systems\s+Lead|AI\s+Safety\s+Researcher|AI\s+Red\s+Teamer|LLM\s+Security\s+Architect|Agent\s+Ops\s+Engineer|Principal\s+AI\s+Engineer):\s*\r?\n+([A-Z\p{Lu}][\p{L}'’–-]+\s+[A-Z\p{Lu}][\p{L}'’–-]+)/gu, '$1: $2');
45
51
  }
46
52
  return text;
47
53
  }
@@ -59,6 +65,7 @@ function stitchOrphanedNameLines(text, profile) {
59
65
  */
60
66
  function scrubText(text, customRules = [], tokenLabelMap = {}, profile = 'General', existingSessionMap = {}, isPro = false, ignoreList = null) {
61
67
  if (!text) return { scrubbedText: "", tokenMap: {}, count: 0, uniqueUnmasked: new Set(), trialMeta: null, executionMs: 0 };
68
+ if (typeof profile !== 'string' || !profile) profile = 'General';
62
69
 
63
70
  const executionStartMs = (typeof performance !== 'undefined' && typeof performance.now === 'function') ? performance.now() : Date.now();
64
71
  // Stitch PDF-split name fragments across line breaks (all profiles)
@@ -124,7 +131,7 @@ function scrubText(text, customRules = [], tokenLabelMap = {}, profile = 'Genera
124
131
  // Pre-scan input text for pre-existing tokens ([TYPE_N], <TYPE_N>, {{TYPE_N}}, __TYPE_N__)
125
132
  // to advance counters and avoid counter collisions or overwriting literal quoted tokens
126
133
  if (typeof textToProcess === 'string' && textToProcess) {
127
- const existingTokenRegex = /(?:\[|<|\{\{|__)([A-Za-z0-9_]+)_(\d+)(?:\]|>|\}\}|__)/g;
134
+ const existingTokenRegex = /(?:\[|\\u005[Bb]|%5[Bb]|&(?:amp;)?#0*91;|【|[|<|\{\{?|__)([A-Za-z0-9_]+)_(\d+)(?:\]|\\u005[Dd]|%5[Dd]|&(?:amp;)?#0*93;|】|]|>|\}\}?|__)/g;
128
135
  let tMatch;
129
136
  while ((tMatch = existingTokenRegex.exec(textToProcess)) !== null) {
130
137
  const rawLabel = tMatch[1].toUpperCase();
@@ -377,17 +384,17 @@ function buildRestorationRegexAndRules(tokenMap) {
377
384
  const regexParts = [];
378
385
 
379
386
  sortedKeys.forEach(k => {
380
- const inner = k.replace(/^\[|<|\{\{|__|\]|>|\}\}|__/g, '');
387
+ const inner = k.replace(/^\[|<|\{\{?|__|\]|>|\}\}?|__/g, '');
381
388
  const match = inner.match(/^([A-Za-z_0-9]+?)[-_]?(\d+)$/);
382
389
  if (match) {
383
390
  const label = match[1];
384
391
  const baseIndex = parseInt(match[2], 10);
385
392
  const aliases = getLabelAliases(label);
386
393
 
387
- const escapedAliases = aliases.map(a => a.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'));
394
+ const escapedAliases = aliases.map(a => a.replace(/[.*+?^${}()|[\]\\]/g, '\\$&').replace(/[_-]/g, '\\\\?[_-]'));
388
395
  const aliasesGroup = `(?:${escapedAliases.join('|')})`;
389
396
 
390
- const looseRegex = '(?:\\[?\\s*' + aliasesGroup + '[-_\\s]*0*' + baseIndex + '\\s*\\]?|<\\s*' + aliasesGroup + '[-_\\s]*0*' + baseIndex + '\\s*>|\\{\\{\\s*' + aliasesGroup + '[-_\\s]*0*' + baseIndex + '\\s*\\}\\}|__\\s*' + aliasesGroup + '[-_\\s]*0*' + baseIndex + '\\s*__)(?:\'s|’s|s|[а-яёА-ЯЁ]{1,3})?';
397
+ const looseRegex = '(?:(?:\\[|\\\\u005[Bb]|%5[Bb]|&(?:amp;)?#0*91;|&(?:amp;)?#[xX]0*5[Bb];|&(?:amp;)?lbrack;|【|[|〔|「|『)?\\s*[*`~]*' + aliasesGroup + '[-_\\\\\\s]*0*' + baseIndex + '[*`~]*[.,;:!?]?\\s*(?:\\]|\\\\u005[Dd]|%5[Dd]|&(?:amp;)?#0*93;|&(?:amp;)?#[xX]0*5[Dd];|&(?:amp;)?rbrack;|】|]|〕|」|』)?|<\\s*' + aliasesGroup + '[-_\\\\\\s]*0*' + baseIndex + '[.,;:!?]?\\s*>|\\{\\{?\\s*' + aliasesGroup + '[-_\\\\\\s]*0*' + baseIndex + '[.,;:!?]?\\s*\\}\\}?|__\\s*' + aliasesGroup + '[-_\\\\\\s]*0*' + baseIndex + '\\s*__)(?:\'s|’s|s|[а-яёА-ЯЁ]{1,3})?';
391
398
  looseRules.push({ patternStr: looseRegex, regex: new RegExp('^' + looseRegex + '$', 'i'), originalKey: k });
392
399
  regexParts.push(looseRegex);
393
400
  } else {
@@ -437,7 +444,14 @@ function cleanAIPromptPrefix(text) {
437
444
  cleaned = cleaned.replace(/^(?:Here (?:is|are) (?:the )?(?:redacted|scrubbed|sanitized|processed|clean|updated|modified) (?:text|output|version|data).*?[:\n]+|\*\*Scrubbed Text\*\*[:\n]+|### Scrubbed Text[:\n]+)/i, '');
438
445
  cleaned = cleaned.replace(/^\s*Edit\s*\n+/i, "");
439
446
  cleaned = cleaned.replace(/\s*\bEdit\s+in\s+a\s+page\b\s*$/i, "");
440
- // 3. Strip stray leading colons, semicolons, or separators left by stripped icons/artifact headers
447
+ // 3. Strip echoed system instructions and PrivacyScrubber / ZTDS pseudonymization disclaimers
448
+ cleaned = cleaned.replace(/^\s*(?:Understood|Acknowledged|Got it|Sure|OK|Okay|Note taken|Understood[.,!])[^\n]*\n+/i, "");
449
+ cleaned = cleaned.replace(/(?:^|\n*)\s*(?:----------------------\s*\n*)?\[(?:ZTDS(?:™|\(TM\))?\s*Protocol|Privacy Note|Context: identifiers|Privacy Scrubber Mode|SYSTEM INSTRUCTION: DATA PRIVACY MODE)[\s\S]*?(?:in your response\.?\]|(?:\]\s*(?:\n*----------------------\s*)?))(?:\n*|$)/gi, "\n\n");
450
+ cleaned = cleaned.replace(/(?:^|\n*)\s*(?:----------------------\s*\n*)?\[(?:ZTDS(?:™|\(TM\))?\s*Protocol|Privacy Note|Context: identifiers|Privacy Scrubber Mode|SYSTEM INSTRUCTION: DATA PRIVACY MODE|Secured by PrivacyScrubber)[^\n\]]*\]\s*(?:\n*----------------------\s*)?(?:\n*|$)/gi, "\n\n");
451
+ cleaned = cleaned.replace(/(?:^|\n*)\s*(?:Direct identifiers in this prompt were (?:pseudonymized|client-side sanitized)|I will retain all bracketed tokens)[^\n]*(?:\n*|$)/gi, "\n\n");
452
+ // 4. Strip stray leading colons, semicolons, or separators left by stripped icons/artifact headers
453
+ cleaned = cleaned.replace(/^(?:\s*----------------------\s*\n+)+/, "");
454
+ cleaned = cleaned.replace(/(?:\n+\s*----------------------\s*)+$/, "");
441
455
  cleaned = cleaned.replace(/^[:;|\-\—\–]+(?=\n|$)/, "");
442
456
  cleaned = cleaned.replace(/^[:;]+\s*/, "");
443
457
  return cleaned.trim();
@@ -1554,27 +1568,17 @@ function hydrateRegex(rule) {
1554
1568
  if (isCurrentActive) item.classList.add('active');
1555
1569
 
1556
1570
  let labelText = p.label;
1557
- item.innerText = labelText;
1558
-
1559
- if (!isPro && p.id !== 'general') {
1560
- item.style.opacity = '0.5';
1561
- item.style.cursor = 'not-allowed';
1562
- item.title = 'PRO Feature';
1563
- item.innerText = labelText + ' 🔒';
1564
- } else if (isCurrentActive) {
1571
+ if (isCurrentActive) {
1565
1572
  item.innerText = labelText + ' ✓';
1573
+ } else if (!isPro && p.id !== 'general') {
1574
+ item.innerText = labelText + ' (5k Trial)';
1575
+ item.title = 'Free 5,000-character trial per scrub';
1576
+ } else {
1577
+ item.innerText = labelText;
1566
1578
  }
1567
1579
 
1568
1580
  item.addEventListener('click', (ev) => {
1569
1581
  ev.stopPropagation();
1570
- if (!isPro && p.id !== 'general') {
1571
- if (typeof showInPageToast === 'function') {
1572
- showInPageToast('Specialized Profiles require PRO upgrade.', 'warning');
1573
- }
1574
- menu.remove();
1575
- return;
1576
- }
1577
-
1578
1582
  chrome.storage.local.set({ ps_active_profile: p.id }, () => {
1579
1583
  if (typeof showInPageToast === 'function') {
1580
1584
  showInPageToast(`Profile Switched to ${p.label}`, 'success');
@@ -1644,7 +1648,7 @@ function hydrateRegex(rule) {
1644
1648
  if (window.showInPageToast) window.showInPageToast("✓ Session exported successfully", "success");
1645
1649
  } catch (e) {
1646
1650
  console.error("PrivacyScrubber Export Error:", e);
1647
- if (window.showInPageToast) window.showInPageToast("❌ Export failed.", "error");
1651
+ if (window.showInPageToast) window.showInPageToast("Export failed.", "error");
1648
1652
  }
1649
1653
  }
1650
1654
 
@@ -1771,6 +1775,8 @@ function hydrateRegex(rule) {
1771
1775
  }
1772
1776
  if (typeof globalThis !== 'undefined') {
1773
1777
  globalThis.PrivacyScrubberCore = coreInstance;
1778
+ globalThis.scrubText = scrubText;
1779
+ globalThis.unscrubText = coreInstance.unscrubText;
1774
1780
  }
1775
1781
  if (typeof module !== 'undefined' && module.exports) {
1776
1782
  module.exports = {
package/server.json CHANGED
@@ -1,27 +1,50 @@
1
1
  {
2
2
  "$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
3
3
  "name": "io.github.moxno/privacyscrubber-mcp",
4
+ "title": "PrivacyScrubber PII Masking MCP",
4
5
  "description": "Zero-Trust PII and secrets sanitizer for Cursor, Claude, and AI agents. 100% in-memory.",
6
+ "websiteUrl": "https://privacyscrubber.com/pii-mcp/?utm_source=mcp_registry&utm_medium=server_json&utm_campaign=mcp_docs",
5
7
  "repository": {
6
8
  "url": "https://github.com/moxno/privacyscrubber-mcp",
7
9
  "source": "github"
8
10
  },
9
- "version": "2.3.9",
11
+ "version": "2.4.1",
10
12
  "packages": [
11
13
  {
12
14
  "registryType": "npm",
13
15
  "identifier": "@privacyscrubber/mcp-server",
14
- "version": "2.3.9",
16
+ "version": "2.4.1",
17
+ "registryBaseUrl": "https://registry.npmjs.org",
18
+ "runtimeHint": "npx",
15
19
  "transport": {
16
20
  "type": "stdio"
17
21
  },
18
22
  "environmentVariables": [
19
23
  {
24
+ "name": "PRIVACYSCRUBBER_KEY",
20
25
  "description": "Optional PRO or TEAMS license key to unlock specialized detection profiles.",
21
26
  "isRequired": false,
22
27
  "format": "string",
23
- "isSecret": true,
24
- "name": "PRIVACYSCRUBBER_KEY"
28
+ "isSecret": true
29
+ }
30
+ ]
31
+ },
32
+ {
33
+ "registryType": "pypi",
34
+ "identifier": "privacyscrubber-mcp",
35
+ "version": "2.4.1",
36
+ "registryBaseUrl": "https://pypi.org",
37
+ "runtimeHint": "uvx",
38
+ "transport": {
39
+ "type": "stdio"
40
+ },
41
+ "environmentVariables": [
42
+ {
43
+ "name": "PRIVACYSCRUBBER_KEY",
44
+ "description": "Optional PRO or TEAMS license key to unlock specialized detection profiles.",
45
+ "isRequired": false,
46
+ "format": "string",
47
+ "isSecret": true
25
48
  }
26
49
  ]
27
50
  }