@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/README.md +50 -11
- package/index.js +881 -79
- package/manifest.json +84 -8
- package/package.json +3 -2
- package/pii-masking-run.js +103 -26
- package/ps-license-manager.cjs +261 -11
- package/ps-license-manager.js +4 -0
- package/ps-pii-engine.cjs +450 -96
- package/ps-pii-engine.js +450 -96
- package/scrubber-core.cjs +31 -25
- package/server.json +27 -4
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
|
|
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)
|
|
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][
|
|
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 = /(?:\[
|
|
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(/^\[|<|\{\{
|
|
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 + '[-_
|
|
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
|
|
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
|
-
|
|
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("
|
|
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.
|
|
11
|
+
"version": "2.4.1",
|
|
10
12
|
"packages": [
|
|
11
13
|
{
|
|
12
14
|
"registryType": "npm",
|
|
13
15
|
"identifier": "@privacyscrubber/mcp-server",
|
|
14
|
-
"version": "2.
|
|
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
|
-
|
|
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
|
}
|