@privacyscrubber/mcp-server 2.4.7 → 2.5.0
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 +1 -1
- package/index.js +274 -75
- package/manifest.json +1 -1
- package/package.json +1 -1
- package/pii-masking-run.js +1 -1
- 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/README.md
CHANGED
|
@@ -485,7 +485,7 @@ Returns a visual dashboard showing your current tier, session request count, act
|
|
|
485
485
|
* **Response Example (Free Tier):**
|
|
486
486
|
```
|
|
487
487
|
╔══════════════════════════════════════════════════╗
|
|
488
|
-
║ PrivacyScrubber MCP Server v2.
|
|
488
|
+
║ PrivacyScrubber MCP Server v2.5.0 ║
|
|
489
489
|
╠══════════════════════════════════════════════════╣
|
|
490
490
|
║ 🔓 Tier: FREE ║
|
|
491
491
|
║ 📊 Session requests: 5 ║
|
package/index.js
CHANGED
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
ListToolsRequestSchema,
|
|
16
16
|
ListPromptsRequestSchema,
|
|
17
17
|
GetPromptRequestSchema,
|
|
18
|
+
ListResourcesRequestSchema,
|
|
18
19
|
ListResourceTemplatesRequestSchema,
|
|
19
20
|
ReadResourceRequestSchema,
|
|
20
21
|
} from "@modelcontextprotocol/sdk/types.js";
|
|
@@ -169,18 +170,41 @@ const agentSessions = new Map();
|
|
|
169
170
|
const MAX_CONCURRENT_SESSIONS = 200;
|
|
170
171
|
const MAX_SESSION_TOKENS = parseInt(process.env.PRIVACYSCRUBBER_MAX_SESSION_TOKENS || '5000', 10);
|
|
171
172
|
|
|
173
|
+
function zeroizeSessionEntry(sess) {
|
|
174
|
+
if (!sess || typeof sess !== 'object') return;
|
|
175
|
+
if (sess.map && typeof sess.map === 'object') {
|
|
176
|
+
for (const k of Object.keys(sess.map)) {
|
|
177
|
+
delete sess.map[k];
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
if (sess.ignoreList && typeof sess.ignoreList.clear === 'function') {
|
|
181
|
+
sess.ignoreList.clear();
|
|
182
|
+
}
|
|
183
|
+
if (sess.streamDetokenizer) {
|
|
184
|
+
sess.streamDetokenizer.reset();
|
|
185
|
+
if (sess.map) sess.streamDetokenizer.updateTokenMap(sess.map);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
172
189
|
function getSession(sessionId = 'default') {
|
|
173
190
|
const sid = (sessionId || 'default').toString().trim() || 'default';
|
|
174
191
|
let session = agentSessions.get(sid);
|
|
175
192
|
const now = Date.now();
|
|
176
193
|
if (!session) {
|
|
177
194
|
if (agentSessions.size >= MAX_CONCURRENT_SESSIONS) {
|
|
178
|
-
|
|
195
|
+
let oldestKey = null;
|
|
196
|
+
for (const k of agentSessions.keys()) {
|
|
197
|
+
if (k !== 'default') {
|
|
198
|
+
oldestKey = k;
|
|
199
|
+
break;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
if (!oldestKey) oldestKey = agentSessions.keys().next().value;
|
|
179
203
|
const oldestSession = agentSessions.get(oldestKey);
|
|
180
|
-
|
|
181
|
-
|
|
204
|
+
zeroizeSessionEntry(oldestSession);
|
|
205
|
+
if (oldestKey !== 'default') {
|
|
206
|
+
agentSessions.delete(oldestKey);
|
|
182
207
|
}
|
|
183
|
-
agentSessions.delete(oldestKey);
|
|
184
208
|
}
|
|
185
209
|
session = { map: {}, ignoreList: new Set(), streamDetokenizer: null, createdAt: now, lastActive: now };
|
|
186
210
|
agentSessions.set(sid, session);
|
|
@@ -227,10 +251,10 @@ export function buildStreamReplacer(tokenMap = {}, options = {}) {
|
|
|
227
251
|
const match = inner.match(/^([A-Za-z_0-9]+?)[-_ ]?(\d+)$/);
|
|
228
252
|
|
|
229
253
|
if (match) {
|
|
230
|
-
const label = escapeRegex(match[1]);
|
|
254
|
+
const label = escapeRegex(match[1]).replace(/[_-]/g, '\\\\?[_-]');
|
|
231
255
|
const idx = match[2];
|
|
232
|
-
const corePat = '[*`_~\\s]*' + label + '[*`_~]*[-_
|
|
233
|
-
const patternStr = '(?:\\[' + corePat + '
|
|
256
|
+
const corePat = '[*`_~\\s]*' + label + '[*`_~]*[-_\\\\\\s]*0*' + idx + '[*`_~.,;:!?\\s]*';
|
|
257
|
+
const patternStr = '(?:(?:\\[|\\\\u005[Bb]|%5[Bb]|&(?:amp;)?#0*91;|&(?:amp;)?#[xX]0*5[Bb];|&(?:amp;)?(?:lbrack|lsqb);|【|[|〔|「|『)' + corePat + '(?:\\]|\\\\u005[Dd]|%5[Dd]|&(?:amp;)?#0*93;|&(?:amp;)?#[xX]0*5[Dd];|&(?:amp;)?(?:rbrack|rsqb);|】|]|〕|」|』)|<' + corePat + '>|\\{\\{' + corePat + '\\}\\}|\\{' + corePat + '\\}|__' + corePat + '__|\\b' + label + '[-_\\s]+0*' + idx + '\\b)';
|
|
234
258
|
patterns.push(patternStr);
|
|
235
259
|
const normKey = (match[1] + '_' + parseInt(idx, 10)).toUpperCase();
|
|
236
260
|
lookup.set(normKey, val);
|
|
@@ -260,13 +284,13 @@ export function buildStreamReplacer(tokenMap = {}, options = {}) {
|
|
|
260
284
|
restored = lookup.get(coreMatch.toUpperCase());
|
|
261
285
|
} else {
|
|
262
286
|
const inner = coreMatch
|
|
263
|
-
.replace(/^(
|
|
264
|
-
.replace(
|
|
265
|
-
.replace(/[*`_~.,;:!?\s]+/g, ' ')
|
|
287
|
+
.replace(/^(?:\[|\\u005[Bb]|%5[Bb]|&(?:amp;)?#0*91;|&(?:amp;)?#[xX]0*5[Bb];|&(?:amp;)?(?:lbrack|lsqb);|【|[|〔|「|『|<|\{\{?|__)|(?:\]|\\u005[Dd]|%5[Dd]|&(?:amp;)?#0*93;|&(?:amp;)?#[xX]0*5[Dd];|&(?:amp;)?(?:rbrack|rsqb);|】|]|〕|」|』|>|\}\}?|__)$/gi, '')
|
|
288
|
+
.replace(/^[*`_~\s]+|[.,;:!?*`_~\s]+$/g, '')
|
|
266
289
|
.trim();
|
|
267
|
-
const m = inner.match(/^([A-Za-z_0-9]+?)[-_
|
|
290
|
+
const m = inner.match(/^([A-Za-z_0-9]+?)[-_\\\s]*(\d+)$/);
|
|
268
291
|
if (m) {
|
|
269
|
-
const
|
|
292
|
+
const cleanLabel = m[1].replace(/\\([_-])/g, '$1');
|
|
293
|
+
const normKey = (cleanLabel + '_' + parseInt(m[2], 10)).toUpperCase();
|
|
270
294
|
if (lookup.has(normKey)) {
|
|
271
295
|
restored = lookup.get(normKey);
|
|
272
296
|
}
|
|
@@ -355,17 +379,36 @@ export class StreamingDetokenizer {
|
|
|
355
379
|
this.pendingBuffer += text;
|
|
356
380
|
|
|
357
381
|
const tailLen = 160;
|
|
358
|
-
const
|
|
359
|
-
|
|
360
|
-
|
|
382
|
+
const tailRegex = /(?:(?:\[|【|[|〔|「|『|<|\{\{?|__|%5[Bb]|\\u005[Bb]|&(?:amp;)?(?:#0*91;|#[xX]0*5[Bb];|lbrack;))[A-Za-z0-9_\-\u0410-\u044F\u0401\u0451 *`~.\\]{0,128}(?:%5?|&(?:a(?:m(?:p(?:;)?)?)?)?(?:#(?:0*9?3?|[xX]0*5?[Dd]?)?|r(?:b(?:r(?:a(?:c(?:k)?)?)?)?)?)?)?|%[0-9A-Fa-f]?|\\(?:u(?:0(?:0(?:5)?)?)?)?|&(?:a(?:m(?:p(?:;)?)?)?)?(?:#(?:0*9?1?|[xX]0*5?)?|l(?:b(?:r(?:a(?:c(?:k)?)?)?)?)?)?|_)$/;
|
|
383
|
+
let tail = this.pendingBuffer.length > tailLen ? this.pendingBuffer.slice(-tailLen) : this.pendingBuffer;
|
|
384
|
+
let offset = this.pendingBuffer.length > tailLen ? this.pendingBuffer.length - tailLen : 0;
|
|
385
|
+
let m = tail.match(tailRegex);
|
|
361
386
|
|
|
362
387
|
if (m) {
|
|
363
388
|
const candidate = m[0];
|
|
364
|
-
const
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
389
|
+
const uClose = candidate.startsWith('__') ? candidate.indexOf('__', 2) : -1;
|
|
390
|
+
const uniMatch = candidate.match(/\\u005[Dd]/);
|
|
391
|
+
const closeLen = uClose !== -1 ? uClose + 2 : (uniMatch ? uniMatch.index + 6 : -1);
|
|
392
|
+
if (closeLen !== -1) {
|
|
393
|
+
const closedToken = candidate.slice(0, closeLen);
|
|
394
|
+
if (this.replacer(closedToken) !== closedToken) {
|
|
395
|
+
const splitAfterToken = offset + m.index + closeLen;
|
|
396
|
+
const prefixEmitted = this.replacer(this.pendingBuffer.slice(0, splitAfterToken));
|
|
397
|
+
this.pendingBuffer = this.pendingBuffer.slice(splitAfterToken);
|
|
398
|
+
if (!this.pendingBuffer) return prefixEmitted;
|
|
399
|
+
tail = this.pendingBuffer.length > tailLen ? this.pendingBuffer.slice(-tailLen) : this.pendingBuffer;
|
|
400
|
+
offset = this.pendingBuffer.length > tailLen ? this.pendingBuffer.length - tailLen : 0;
|
|
401
|
+
m = tail.match(tailRegex);
|
|
402
|
+
if (!m) {
|
|
403
|
+
const rest = this.replacer(this.pendingBuffer);
|
|
404
|
+
this.pendingBuffer = '';
|
|
405
|
+
return prefixEmitted + rest;
|
|
406
|
+
}
|
|
407
|
+
const sIdx = offset + m.index;
|
|
408
|
+
const safe = this.pendingBuffer.slice(0, sIdx);
|
|
409
|
+
this.pendingBuffer = this.pendingBuffer.slice(sIdx);
|
|
410
|
+
return prefixEmitted + (safe ? this.replacer(safe) : '');
|
|
411
|
+
}
|
|
369
412
|
}
|
|
370
413
|
const splitIdx = offset + m.index;
|
|
371
414
|
const safeToEmit = this.pendingBuffer.slice(0, splitIdx);
|
|
@@ -429,15 +472,21 @@ export class StreamingDetokenizer {
|
|
|
429
472
|
}
|
|
430
473
|
|
|
431
474
|
async* wrapStream(asyncIterable) {
|
|
475
|
+
let lastOpenAIChunk = null;
|
|
476
|
+
let lastResponsesDeltaChunk = null;
|
|
477
|
+
let lastAnthropicDeltaChunk = null;
|
|
432
478
|
for await (const chunk of asyncIterable) {
|
|
433
479
|
if (chunk === null || chunk === undefined) continue;
|
|
434
480
|
if (chunk.choices && Array.isArray(chunk.choices) && chunk.choices[0]?.delta?.content !== undefined) {
|
|
481
|
+
lastOpenAIChunk = chunk;
|
|
435
482
|
const delta = chunk.choices[0].delta.content;
|
|
436
483
|
if (typeof delta === 'string') {
|
|
437
484
|
const restored = this.feed(delta);
|
|
438
485
|
if (restored) {
|
|
439
|
-
chunk.choices
|
|
440
|
-
|
|
486
|
+
const clonedChoices = chunk.choices.map((c, idx) =>
|
|
487
|
+
idx === 0 ? { ...c, delta: { ...c.delta, content: restored } } : c
|
|
488
|
+
);
|
|
489
|
+
yield { ...chunk, choices: clonedChoices };
|
|
441
490
|
}
|
|
442
491
|
} else {
|
|
443
492
|
yield chunk;
|
|
@@ -445,6 +494,53 @@ export class StreamingDetokenizer {
|
|
|
445
494
|
continue;
|
|
446
495
|
}
|
|
447
496
|
|
|
497
|
+
if (chunk && typeof chunk === 'object' && typeof chunk.type === 'string' && chunk.type.startsWith('response.')) {
|
|
498
|
+
if (chunk.type === 'response.output_text.delta' && typeof chunk.delta === 'string') {
|
|
499
|
+
lastResponsesDeltaChunk = chunk;
|
|
500
|
+
const restored = this.feed(chunk.delta);
|
|
501
|
+
if (restored) {
|
|
502
|
+
yield { ...chunk, delta: restored };
|
|
503
|
+
}
|
|
504
|
+
continue;
|
|
505
|
+
}
|
|
506
|
+
if (chunk.type === 'response.output_text.done' && typeof chunk.text === 'string') {
|
|
507
|
+
const remaining = this.flush();
|
|
508
|
+
if (remaining && lastResponsesDeltaChunk) {
|
|
509
|
+
yield { ...lastResponsesDeltaChunk, delta: remaining };
|
|
510
|
+
}
|
|
511
|
+
yield { ...chunk, text: this.replacer(chunk.text) };
|
|
512
|
+
continue;
|
|
513
|
+
}
|
|
514
|
+
yield chunk;
|
|
515
|
+
continue;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
if (chunk && typeof chunk === 'object' && typeof chunk.type === 'string' && (
|
|
519
|
+
chunk.type === 'content_block_delta' ||
|
|
520
|
+
chunk.type === 'content_block_start' ||
|
|
521
|
+
chunk.type === 'content_block_stop' ||
|
|
522
|
+
chunk.type === 'message_start' ||
|
|
523
|
+
chunk.type === 'message_delta' ||
|
|
524
|
+
chunk.type === 'message_stop'
|
|
525
|
+
)) {
|
|
526
|
+
if (chunk.type === 'content_block_delta' && chunk.delta && chunk.delta.type === 'text_delta' && typeof chunk.delta.text === 'string') {
|
|
527
|
+
lastAnthropicDeltaChunk = chunk;
|
|
528
|
+
const restored = this.feed(chunk.delta.text);
|
|
529
|
+
if (restored) {
|
|
530
|
+
yield { ...chunk, delta: { ...chunk.delta, text: restored } };
|
|
531
|
+
}
|
|
532
|
+
continue;
|
|
533
|
+
}
|
|
534
|
+
if ((chunk.type === 'content_block_stop' || chunk.type === 'message_stop') && this.pendingBuffer) {
|
|
535
|
+
const remaining = this.flush();
|
|
536
|
+
if (remaining && lastAnthropicDeltaChunk) {
|
|
537
|
+
yield { ...lastAnthropicDeltaChunk, delta: { ...lastAnthropicDeltaChunk.delta, text: remaining } };
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
yield chunk;
|
|
541
|
+
continue;
|
|
542
|
+
}
|
|
543
|
+
|
|
448
544
|
let text = '';
|
|
449
545
|
if (typeof chunk === 'string') {
|
|
450
546
|
text = chunk;
|
|
@@ -463,7 +559,20 @@ export class StreamingDetokenizer {
|
|
|
463
559
|
}
|
|
464
560
|
|
|
465
561
|
const finalChunk = this.flush();
|
|
466
|
-
if (finalChunk)
|
|
562
|
+
if (finalChunk) {
|
|
563
|
+
if (lastOpenAIChunk) {
|
|
564
|
+
const clonedChoices = lastOpenAIChunk.choices.map((c, idx) =>
|
|
565
|
+
idx === 0 ? { ...c, delta: { ...c.delta, content: finalChunk } } : c
|
|
566
|
+
);
|
|
567
|
+
yield { ...lastOpenAIChunk, choices: clonedChoices };
|
|
568
|
+
} else if (lastResponsesDeltaChunk) {
|
|
569
|
+
yield { ...lastResponsesDeltaChunk, delta: finalChunk };
|
|
570
|
+
} else if (lastAnthropicDeltaChunk) {
|
|
571
|
+
yield { ...lastAnthropicDeltaChunk, delta: { ...lastAnthropicDeltaChunk.delta, text: finalChunk } };
|
|
572
|
+
} else {
|
|
573
|
+
yield finalChunk;
|
|
574
|
+
}
|
|
575
|
+
}
|
|
467
576
|
}
|
|
468
577
|
}
|
|
469
578
|
|
|
@@ -688,7 +797,7 @@ function buildDailyLimitBlockedResponse() {
|
|
|
688
797
|
};
|
|
689
798
|
}
|
|
690
799
|
|
|
691
|
-
function buildCisoAuditTelemetry(currentTokenMap = {}) {
|
|
800
|
+
function buildCisoAuditTelemetry(currentTokenMap = {}, extraStats = {}) {
|
|
692
801
|
const entities = {};
|
|
693
802
|
let totalCount = 0;
|
|
694
803
|
|
|
@@ -697,8 +806,9 @@ function buildCisoAuditTelemetry(currentTokenMap = {}) {
|
|
|
697
806
|
for (const t of keys) {
|
|
698
807
|
const tokenStr = typeof t === 'string' ? t : (t && (t.token || t.mask)) ? String(t.token || t.mask) : '';
|
|
699
808
|
if (!tokenStr) continue;
|
|
700
|
-
const match = tokenStr.match(/\[([A-
|
|
701
|
-
const
|
|
809
|
+
const match = tokenStr.match(/\[([A-Za-z0-9_]+)_\d+\]/);
|
|
810
|
+
const rawBase = match ? match[1] : tokenStr.replace(/\[|\]|<|>|\{|\}|__/g, '').replace(/_[0-9]+$/, '');
|
|
811
|
+
const baseType = (rawBase.replace(/[^A-Za-z0-9_]/g, '') || 'CUSTOM').toUpperCase();
|
|
702
812
|
entities[baseType] = (entities[baseType] || 0) + 1;
|
|
703
813
|
totalCount++;
|
|
704
814
|
}
|
|
@@ -708,7 +818,7 @@ function buildCisoAuditTelemetry(currentTokenMap = {}) {
|
|
|
708
818
|
let riskLevel = 'LOW EXPOSURE';
|
|
709
819
|
|
|
710
820
|
if (totalCount > 0) {
|
|
711
|
-
const hasHighRiskEntities = types.some(t => ['ID', 'SSN', 'CREDIT_CARD', 'PASSPORT', 'BANK', 'API_KEY', 'SECRET', 'PASSWORD', 'MRN', 'KEY'].includes(t));
|
|
821
|
+
const hasHighRiskEntities = types.some(t => ['ID', 'SSN', 'CREDIT_CARD', 'PASSPORT', 'BANK', 'API_KEY', 'SECRET', 'PASSWORD', 'MRN', 'KEY', 'PHI', 'PRIVILEGE', 'AWS_KEY', 'STRIPE_KEY', 'JWT_TOKEN', 'API_TOKEN'].includes(t));
|
|
712
822
|
if (hasHighRiskEntities || types.length >= 3 || totalCount >= 10) {
|
|
713
823
|
riskLevel = 'CRITICAL (HIGH EXPOSURE)';
|
|
714
824
|
} else if (types.length >= 2 || totalCount >= 3) {
|
|
@@ -733,10 +843,10 @@ function buildCisoAuditTelemetry(currentTokenMap = {}) {
|
|
|
733
843
|
frameworksSet.add('PCI DSS v4.0');
|
|
734
844
|
frameworksSet.add('IRC § 7216 & GLBA');
|
|
735
845
|
}
|
|
736
|
-
if (types.some(t => ['MRN', 'HEALTH', 'MEDICAL', 'PATIENT', 'DIAGNOSIS', 'NPI', 'DEA'].includes(t))) {
|
|
846
|
+
if (types.some(t => ['PHI', 'MRN', 'HEALTH', 'MEDICAL', 'PATIENT', 'DIAGNOSIS', 'NPI', 'DEA'].includes(t))) {
|
|
737
847
|
frameworksSet.add('HIPAA §164.514');
|
|
738
848
|
}
|
|
739
|
-
if (types.some(t => ['CASE_NUMBER', 'DOCKET', 'PRIVILEGED', 'LEGAL', 'ATTORNEY', 'COURT', 'CONTRACT', 'NDA'].includes(t))) {
|
|
849
|
+
if (types.some(t => ['PRIVILEGE', 'CASE_NUMBER', 'DOCKET', 'PRIVILEGED', 'LEGAL', 'ATTORNEY', 'COURT', 'CONTRACT', 'NDA'].includes(t))) {
|
|
740
850
|
frameworksSet.add('FRE 502 (Privilege)');
|
|
741
851
|
}
|
|
742
852
|
if (types.some(t => ['API_KEY', 'SECRET', 'PASSWORD', 'TOKEN', 'KEY', 'AWS_KEY', 'JWT_TOKEN', 'API_TOKEN', 'CREDENTIAL'].includes(t))) {
|
|
@@ -746,6 +856,26 @@ function buildCisoAuditTelemetry(currentTokenMap = {}) {
|
|
|
746
856
|
frameworksSet.add('EU AI Act (Art. 50)');
|
|
747
857
|
}
|
|
748
858
|
|
|
859
|
+
const frameworksList = Array.from(frameworksSet);
|
|
860
|
+
const getFrameworkSlug = (name) => {
|
|
861
|
+
const lower = name.toLowerCase();
|
|
862
|
+
if (lower.includes('gdpr')) return 'gdpr';
|
|
863
|
+
if (lower.includes('ccpa')) return 'ccpa';
|
|
864
|
+
if (lower.includes('soc')) return 'soc2';
|
|
865
|
+
if (lower.includes('iso')) return 'iso27001';
|
|
866
|
+
if (lower.includes('pci')) return 'pci';
|
|
867
|
+
if (lower.includes('hipaa')) return 'hipaa';
|
|
868
|
+
if (lower.includes('nist')) return 'nist';
|
|
869
|
+
if (lower.includes('irc') || lower.includes('7216')) return 'irc7216';
|
|
870
|
+
if (lower.includes('fre') || lower.includes('502')) return 'fre502';
|
|
871
|
+
if (lower.includes('ai act')) return 'euaiact';
|
|
872
|
+
return lower.replace(/[^a-z0-9]/g, '');
|
|
873
|
+
};
|
|
874
|
+
const triggeredFrameworks = frameworksList.map(name => ({
|
|
875
|
+
name,
|
|
876
|
+
id: getFrameworkSlug(name)
|
|
877
|
+
}));
|
|
878
|
+
|
|
749
879
|
const ipHoldings = {
|
|
750
880
|
standard: 'IETF draft-sibiryakov-ztds-protocol-01',
|
|
751
881
|
standardUrl: 'https://datatracker.ietf.org/doc/draft-sibiryakov-ztds-protocol/',
|
|
@@ -797,12 +927,33 @@ function buildCisoAuditTelemetry(currentTokenMap = {}) {
|
|
|
797
927
|
}
|
|
798
928
|
];
|
|
799
929
|
|
|
930
|
+
const rawLicenseTier = String(extraStats?.licenseTier || '').trim().toUpperCase();
|
|
931
|
+
const isOrganizationalScope = ['TEAMS', 'SDK', 'OEM', 'ENTERPRISE', 'ENTERPRISE_AIRGAP'].includes(rawLicenseTier);
|
|
932
|
+
const auditScope = isOrganizationalScope ? {
|
|
933
|
+
tier: rawLicenseTier,
|
|
934
|
+
isOrganizational: true,
|
|
935
|
+
badge: 'ORGANIZATIONAL MULTI-SEAT COMPLIANCE SCOPE',
|
|
936
|
+
coverage: 'Covers department, clinic, or firm personnel under unified ZTDS policy governance.',
|
|
937
|
+
upgradeRecommended: false,
|
|
938
|
+
upgradeUrl: null
|
|
939
|
+
} : {
|
|
940
|
+
tier: rawLicenseTier || 'SINGLE_OPERATOR',
|
|
941
|
+
isOrganizational: false,
|
|
942
|
+
badge: 'SINGLE-OPERATOR PERSONAL SCOPE',
|
|
943
|
+
coverage: 'Valid for individual workstation only. Multi-employee clinics, CPA firms, and departments require TEAMS ($99/mo flat, unlimited seats) for organizational audit coverage.',
|
|
944
|
+
upgradeRecommended: true,
|
|
945
|
+
upgradeUrl: 'https://privacyscrubber.com/pricing/?tier=teams&utm_source=audit_receipt&utm_medium=scope_upsell'
|
|
946
|
+
};
|
|
947
|
+
|
|
800
948
|
return {
|
|
801
949
|
totalCount,
|
|
950
|
+
totalDetected: totalCount,
|
|
802
951
|
entities,
|
|
803
952
|
types,
|
|
804
953
|
riskLevel,
|
|
805
|
-
frameworksList
|
|
954
|
+
frameworksList,
|
|
955
|
+
triggeredFrameworks,
|
|
956
|
+
auditScope,
|
|
806
957
|
statutoryCitations,
|
|
807
958
|
ipHoldings,
|
|
808
959
|
agenticProtocol,
|
|
@@ -2252,6 +2403,60 @@ server.setRequestHandler(GetPromptRequestSchema, async (request) => {
|
|
|
2252
2403
|
};
|
|
2253
2404
|
});
|
|
2254
2405
|
|
|
2406
|
+
const PROFILE_RESOURCE_DESCRIPTIONS = {
|
|
2407
|
+
"general": "Scrubs basic PII: Names, Emails, Phone Numbers, Credit Cards, SSN, IPv4/IPv6, and standard API Keys.",
|
|
2408
|
+
"dev": "Engineering profile. Scrubs AWS keys, JWTs, GCP keys, Stripe keys, database connection strings, and internal IP addresses.",
|
|
2409
|
+
"engineering": "Engineering profile. Scrubs AWS keys, JWTs, GCP keys, Stripe keys, database connection strings, internal IPs, Docker paths, and K8s internal endpoints.",
|
|
2410
|
+
"tech": "Technology profile. Scrubs cloud instance IDs, node/cluster IDs, environment tags, kubeconfig, and terraform states.",
|
|
2411
|
+
"medical": "HIPAA compliance profile. Scrubs ICD-10 codes, medical record numbers (MRN), DEA numbers, NPI, patient names, and health conditions.",
|
|
2412
|
+
"finance": "PCI/Finance profile. Scrubs IBAN, SWIFT codes, credit cards, routing numbers, and financial transaction IDs.",
|
|
2413
|
+
"wealthmgmt": "Wealth Management profile. Scrubs ABA/Routing numbers, trust names, portfolio values, net worth, RMDs, and account numbers.",
|
|
2414
|
+
"insurance": "Insurance profile. Scrubs claim numbers, policy numbers, settlement amounts, NAIC, adjustor IDs, and insured names.",
|
|
2415
|
+
"accounting": "Accounting profile. Scrubs EIN, FEIN, Tax IDs, AGI, tax forms (1040, W-2, 1099), refund amounts, and PTINs.",
|
|
2416
|
+
"legal": "Legal profile. Scrubs case numbers, matter numbers, attorney-client privilege markers, and litigation IDs.",
|
|
2417
|
+
"hr": "Human Resources profile. Scrubs employee IDs (EEID), resumes, DOBs, tenant IDs, and home addresses.",
|
|
2418
|
+
"security": "Security profile. Scrubs secrets, AWS keys, JWTs, Stripe keys, incident IDs, and breach reports.",
|
|
2419
|
+
"marketing": "Marketing profile. Scrubs lead IDs, prospect IDs, GCLID/FBCLID, LTV/CAC amounts, cohort IDs, and segment IDs.",
|
|
2420
|
+
"bizops": "BizOps profile. Scrubs deals, KPIs, vendor IDs, EBITDA/Revenue figures, NDAs, and M&A identifiers.",
|
|
2421
|
+
"sales": "Sales profile. Scrubs opportunity IDs, DocuSign hashes, ARR/MRR/Quota amounts, and SFDC/HubSpot IDs.",
|
|
2422
|
+
"support": "Support profile. Scrubs Zendesk/Jira ticket numbers, RMA/Return IDs, and loyalty/rewards numbers.",
|
|
2423
|
+
"realestate": "Real Estate profile. Scrubs MLS numbers, LIS IDs, parcel numbers, rent/escrow amounts, and gate/lobby codes.",
|
|
2424
|
+
"compliance": "Compliance profile. Scrubs GDPR/HIPAA/SOC2 audit IDs, DPA policy numbers, and SAR/DSAR request IDs.",
|
|
2425
|
+
"ccpa": "CCPA profile. Scrubs driver licenses, precise geolocation (Lat/Long), CCPA/CPRA opt-out markers, and account numbers.",
|
|
2426
|
+
"agents": "AI Agents profile. Scrubs agent IDs, vector IDs, task IDs, system prompts, and OpenAI API keys.",
|
|
2427
|
+
"academic": "Academic profile. Scrubs student/alumni IDs, course numbers, FERPA/IRB IDs, and academic grades.",
|
|
2428
|
+
"creative": "Creative profile. Scrubs project IDs, script drafts, spoiler/embargo tags, and ghostwriter names.",
|
|
2429
|
+
"personal": "Personal profile. Scrubs birthdays, passwords, PINs, and emergency contacts/family phone numbers.",
|
|
2430
|
+
"pharma": "Pharma/Clinical profile. Scrubs patient IDs, study protocols, IND/NDA numbers, IRB IDs, batch/lot serials, and dosages.",
|
|
2431
|
+
"underwriting": "Loan Underwriting & Mortgage profile. Scrubs borrower names, SSNs, EINs, addresses while preserving wages and YTD figures.",
|
|
2432
|
+
"automotive": "Automotive & EV profile. Scrubs VINs, ECU IDs, telematics, and repair orders while preserving DTCs and ASIL-D standards.",
|
|
2433
|
+
"energy": "Energy & Utilities profile. Scrubs ESI IDs, meter serials, SCADA points, PLC tags, and substation circuits.",
|
|
2434
|
+
"hospitality": "Hospitality & Travel profile. Scrubs PNRs, e-tickets, boarding pass barcodes, and guest folios.",
|
|
2435
|
+
"biotech": "Biotech & Genomics profile. Scrubs accessions, patient phenotypes, HGVS variants, DNA/RNA samples, and flowcell IDs.",
|
|
2436
|
+
"telecom": "Call Centers & Telephony profile. Scrubs CTI interaction IDs, Amazon Connect/Genesys sessions, agent extensions, ANI/DNIS, IVR DTMF, and audio transcript diarization headers."
|
|
2437
|
+
};
|
|
2438
|
+
|
|
2439
|
+
// List Resources
|
|
2440
|
+
server.setRequestHandler(ListResourcesRequestSchema, async () => {
|
|
2441
|
+
const featuredProfiles = ["general", "dev", "medical", "finance", "legal", "security", "compliance", "hr", "agents"];
|
|
2442
|
+
return {
|
|
2443
|
+
resources: [
|
|
2444
|
+
{
|
|
2445
|
+
uri: "mcp://privacyscrubber/profiles/catalog",
|
|
2446
|
+
name: "ZTDS 30 Industry Profiles Catalog",
|
|
2447
|
+
description: "Complete directory of all 30 specialized PrivacyScrubber PII & secrets detection profiles and regulatory coverage.",
|
|
2448
|
+
mimeType: "text/markdown"
|
|
2449
|
+
},
|
|
2450
|
+
...featuredProfiles.map((name) => ({
|
|
2451
|
+
uri: `mcp://privacyscrubber/profiles/${name}`,
|
|
2452
|
+
name: `Profile: ${name.charAt(0).toUpperCase() + name.slice(1)}`,
|
|
2453
|
+
description: PROFILE_RESOURCE_DESCRIPTIONS[name],
|
|
2454
|
+
mimeType: "text/markdown"
|
|
2455
|
+
}))
|
|
2456
|
+
]
|
|
2457
|
+
};
|
|
2458
|
+
});
|
|
2459
|
+
|
|
2255
2460
|
// List Resource Templates
|
|
2256
2461
|
server.setRequestHandler(ListResourceTemplatesRequestSchema, async () => {
|
|
2257
2462
|
return {
|
|
@@ -2268,42 +2473,24 @@ server.setRequestHandler(ListResourceTemplatesRequestSchema, async () => {
|
|
|
2268
2473
|
// Read Resource
|
|
2269
2474
|
server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
|
|
2270
2475
|
const uri = request.params.uri;
|
|
2476
|
+
if (uri === "mcp://privacyscrubber/profiles/catalog") {
|
|
2477
|
+
const lines = Object.entries(PROFILE_RESOURCE_DESCRIPTIONS).map(
|
|
2478
|
+
([k, v]) => `- **\`${k}\`**: ${v}`
|
|
2479
|
+
);
|
|
2480
|
+
return {
|
|
2481
|
+
contents: [
|
|
2482
|
+
{
|
|
2483
|
+
uri,
|
|
2484
|
+
mimeType: "text/markdown",
|
|
2485
|
+
text: `# PrivacyScrubber ZTDS Profiles Catalog (30 Specialized Industry Profiles)\n\n${lines.join("\n")}\n\n*Usage: Pass \`"profile": "<name>"\` or \`"sector": "<name>"\` to \`sanitize_text\`, \`sanitize_file\`, or \`guard_read_file\`.*`
|
|
2486
|
+
}
|
|
2487
|
+
]
|
|
2488
|
+
};
|
|
2489
|
+
}
|
|
2271
2490
|
const match = uri.match(/^mcp:\/\/privacyscrubber\/profiles\/(.+)$/);
|
|
2272
2491
|
if (match) {
|
|
2273
2492
|
const profileName = match[1];
|
|
2274
|
-
const
|
|
2275
|
-
"general": "Scrubs basic PII: Names, Emails, Phone Numbers, Credit Cards, SSN, IPv4/IPv6, and standard API Keys.",
|
|
2276
|
-
"dev": "Engineering profile. Scrubs AWS keys, JWTs, GCP keys, Stripe keys, database connection strings, and internal IP addresses.",
|
|
2277
|
-
"engineering": "Engineering profile. Scrubs AWS keys, JWTs, GCP keys, Stripe keys, database connection strings, internal IPs, Docker paths, and K8s internal endpoints.",
|
|
2278
|
-
"tech": "Technology profile. Scrubs cloud instance IDs, node/cluster IDs, environment tags, kubeconfig, and terraform states.",
|
|
2279
|
-
"medical": "HIPAA compliance profile. Scrubs ICD-10 codes, medical record numbers (MRN), DEA numbers, NPI, patient names, and health conditions.",
|
|
2280
|
-
"finance": "PCI/Finance profile. Scrubs IBAN, SWIFT codes, credit cards, routing numbers, and financial transaction IDs.",
|
|
2281
|
-
"wealthmgmt": "Wealth Management profile. Scrubs ABA/Routing numbers, trust names, portfolio values, net worth, RMDs, and account numbers.",
|
|
2282
|
-
"insurance": "Insurance profile. Scrubs claim numbers, policy numbers, settlement amounts, NAIC, adjustor IDs, and insured names.",
|
|
2283
|
-
"accounting": "Accounting profile. Scrubs EIN, FEIN, Tax IDs, AGI, tax forms (1040, W-2, 1099), refund amounts, and PTINs.",
|
|
2284
|
-
"legal": "Legal profile. Scrubs case numbers, matter numbers, attorney-client privilege markers, and litigation IDs.",
|
|
2285
|
-
"hr": "Human Resources profile. Scrubs employee IDs (EEID), resumes, DOBs, tenant IDs, and home addresses.",
|
|
2286
|
-
"security": "Security profile. Scrubs secrets, AWS keys, JWTs, Stripe keys, incident IDs, and breach reports.",
|
|
2287
|
-
"marketing": "Marketing profile. Scrubs lead IDs, prospect IDs, GCLID/FBCLID, LTV/CAC amounts, cohort IDs, and segment IDs.",
|
|
2288
|
-
"bizops": "BizOps profile. Scrubs deals, KPIs, vendor IDs, EBITDA/Revenue figures, NDAs, and M&A identifiers.",
|
|
2289
|
-
"sales": "Sales profile. Scrubs opportunity IDs, DocuSign hashes, ARR/MRR/Quota amounts, and SFDC/HubSpot IDs.",
|
|
2290
|
-
"support": "Support profile. Scrubs Zendesk/Jira ticket numbers, RMA/Return IDs, and loyalty/rewards numbers.",
|
|
2291
|
-
"realestate": "Real Estate profile. Scrubs MLS numbers, LIS IDs, parcel numbers, rent/escrow amounts, and gate/lobby codes.",
|
|
2292
|
-
"compliance": "Compliance profile. Scrubs GDPR/HIPAA/SOC2 audit IDs, DPA policy numbers, and SAR/DSAR request IDs.",
|
|
2293
|
-
"ccpa": "CCPA profile. Scrubs driver licenses, precise geolocation (Lat/Long), CCPA/CPRA opt-out markers, and account numbers.",
|
|
2294
|
-
"agents": "AI Agents profile. Scrubs agent IDs, vector IDs, task IDs, system prompts, and OpenAI API keys.",
|
|
2295
|
-
"academic": "Academic profile. Scrubs student/alumni IDs, course numbers, FERPA/IRB IDs, and academic grades.",
|
|
2296
|
-
"creative": "Creative profile. Scrubs project IDs, script drafts, spoiler/embargo tags, and ghostwriter names.",
|
|
2297
|
-
"personal": "Personal profile. Scrubs birthdays, passwords, PINs, and emergency contacts/family phone numbers.",
|
|
2298
|
-
"pharma": "Pharma/Clinical profile. Scrubs patient IDs, study protocols, IND/NDA numbers, IRB IDs, batch/lot serials, and dosages.",
|
|
2299
|
-
"underwriting": "Loan Underwriting & Mortgage profile. Scrubs borrower names, SSNs, EINs, addresses while preserving wages and YTD figures.",
|
|
2300
|
-
"automotive": "Automotive & EV profile. Scrubs VINs, ECU IDs, telematics, and repair orders while preserving DTCs and ASIL-D standards.",
|
|
2301
|
-
"energy": "Energy & Utilities profile. Scrubs ESI IDs, meter serials, SCADA points, PLC tags, and substation circuits.",
|
|
2302
|
-
"hospitality": "Hospitality & Travel profile. Scrubs PNRs, e-tickets, boarding pass barcodes, and guest folios.",
|
|
2303
|
-
"biotech": "Biotech & Genomics profile. Scrubs accessions, patient phenotypes, HGVS variants, DNA/RNA samples, and flowcell IDs.",
|
|
2304
|
-
"telecom": "Call Centers & Telephony profile. Scrubs CTI interaction IDs, Amazon Connect/Genesys sessions, agent extensions, ANI/DNIS, IVR DTMF, and audio transcript diarization headers."
|
|
2305
|
-
};
|
|
2306
|
-
const desc = descriptions[profileName.toLowerCase()] || `The '${profileName}' profile is a specialized detection ruleset tuned for specific industry compliance. It detects and sanitizes domain-specific identifiers.`;
|
|
2493
|
+
const desc = PROFILE_RESOURCE_DESCRIPTIONS[profileName.toLowerCase()] || `The '${profileName}' profile is a specialized detection ruleset tuned for specific industry compliance. It detects and sanitizes domain-specific identifiers.`;
|
|
2307
2494
|
|
|
2308
2495
|
return {
|
|
2309
2496
|
contents: [
|
|
@@ -2423,6 +2610,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
2423
2610
|
riskLevel: telemetry.riskLevel,
|
|
2424
2611
|
totalDetected: telemetry.totalDetected,
|
|
2425
2612
|
entities: telemetry.entities,
|
|
2613
|
+
frameworksList: telemetry.frameworksList,
|
|
2426
2614
|
triggeredFrameworks: telemetry.triggeredFrameworks,
|
|
2427
2615
|
profile: targetProfile,
|
|
2428
2616
|
executionEngine: "ZTDS Client-Side RAM"
|
|
@@ -2442,14 +2630,14 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
2442
2630
|
report += `* **Status / Risk Level:** \`${telemetry.riskLevel}\`\n`;
|
|
2443
2631
|
report += `* **Total Entities Detected:** \`${telemetry.totalDetected}\`\n`;
|
|
2444
2632
|
report += `* **Active Profile:** \`${targetProfile}\` (Engine: ZTDS Client-Side RAM)\n`;
|
|
2445
|
-
report += `* **Triggered Compliance Frameworks:** ${telemetry.
|
|
2633
|
+
report += `* **Triggered Compliance Frameworks:** ${(telemetry.frameworksList || []).map(f => `\`${f}\``).join(', ')}\n\n`;
|
|
2446
2634
|
|
|
2447
2635
|
if (telemetry.totalDetected > 0) {
|
|
2448
2636
|
report += `#### Entity Breakdown\n`;
|
|
2449
2637
|
report += `| Entity Type | Occurrences | Exposure Severity |\n`;
|
|
2450
2638
|
report += `| :--- | :--- | :--- |\n`;
|
|
2451
2639
|
for (const [entityType, count] of Object.entries(telemetry.entities)) {
|
|
2452
|
-
const isHigh = ['ID', 'SSN', 'CREDIT_CARD', 'API_KEY', 'SECRET', 'PASSWORD', 'MRN', 'KEY'].includes(entityType);
|
|
2640
|
+
const isHigh = ['ID', 'SSN', 'CREDIT_CARD', 'PASSPORT', 'BANK', 'API_KEY', 'SECRET', 'PASSWORD', 'MRN', 'KEY', 'PHI', 'PRIVILEGE', 'AWS_KEY', 'STRIPE_KEY', 'JWT_TOKEN', 'API_TOKEN'].includes(entityType);
|
|
2453
2641
|
report += `| \`${entityType}\` | ${count} | ${isHigh ? 'High Risk' : 'Standard'} |\n`;
|
|
2454
2642
|
}
|
|
2455
2643
|
report += `\n> [!CAUTION]\n> **Action Recommended:** This context contains sensitive data. Call \`sanitize_text\` before transmitting to any external LLM model.\n`;
|
|
@@ -4219,14 +4407,19 @@ Before reading sensitive files, running terminal commands that may print credent
|
|
|
4219
4407
|
const { session_id, all = false, reset_all = false } = args || {};
|
|
4220
4408
|
if (all || reset_all) {
|
|
4221
4409
|
const count = agentSessions.size;
|
|
4410
|
+
const defaultSess = agentSessions.get('default');
|
|
4222
4411
|
for (const sess of agentSessions.values()) {
|
|
4223
|
-
|
|
4224
|
-
sess.streamDetokenizer.reset();
|
|
4225
|
-
}
|
|
4412
|
+
zeroizeSessionEntry(sess);
|
|
4226
4413
|
}
|
|
4227
4414
|
agentSessions.clear();
|
|
4228
|
-
|
|
4229
|
-
|
|
4415
|
+
const now = Date.now();
|
|
4416
|
+
if (defaultSess) {
|
|
4417
|
+
defaultSess.createdAt = now;
|
|
4418
|
+
defaultSess.lastActive = now;
|
|
4419
|
+
agentSessions.set('default', defaultSess);
|
|
4420
|
+
} else {
|
|
4421
|
+
getSession('default');
|
|
4422
|
+
}
|
|
4230
4423
|
return {
|
|
4231
4424
|
content: [{
|
|
4232
4425
|
type: "text",
|
|
@@ -4235,14 +4428,20 @@ Before reading sensitive files, running terminal commands that may print credent
|
|
|
4235
4428
|
};
|
|
4236
4429
|
}
|
|
4237
4430
|
|
|
4238
|
-
const sid = (session_id || 'default').toString().trim();
|
|
4431
|
+
const sid = (session_id || 'default').toString().trim() || 'default';
|
|
4239
4432
|
const sess = agentSessions.get(sid);
|
|
4240
|
-
|
|
4241
|
-
|
|
4433
|
+
const existed = Boolean(sess);
|
|
4434
|
+
if (sess) {
|
|
4435
|
+
zeroizeSessionEntry(sess);
|
|
4242
4436
|
}
|
|
4243
|
-
const existed = agentSessions.delete(sid);
|
|
4244
4437
|
if (sid === 'default') {
|
|
4245
|
-
|
|
4438
|
+
if (sess) {
|
|
4439
|
+
sess.lastActive = Date.now();
|
|
4440
|
+
} else {
|
|
4441
|
+
getSession('default');
|
|
4442
|
+
}
|
|
4443
|
+
} else {
|
|
4444
|
+
agentSessions.delete(sid);
|
|
4246
4445
|
}
|
|
4247
4446
|
return {
|
|
4248
4447
|
content: [{
|
package/manifest.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"manifest_version": "0.3",
|
|
3
3
|
"name": "privacyscrubber/pii-masking-mcp",
|
|
4
|
-
"version": "2.
|
|
4
|
+
"version": "2.5.0",
|
|
5
5
|
"description": "CISO-Approved Zero-Trust PII & Secrets Redaction MCP Server for Cursor, Windsurf, and Claude Desktop. Prevent API leaks and comply with HIPAA/SOC 2 locally.",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "PrivacyScrubber",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@privacyscrubber/mcp-server",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.5.0",
|
|
4
4
|
"mcpName": "io.github.moxno/privacyscrubber-mcp",
|
|
5
5
|
"description": "CISO-Approved Zero-Trust PII & Secrets Redaction MCP Server for Cursor, Windsurf, and Claude Desktop. Prevent API leaks and comply with HIPAA/SOC 2 locally.",
|
|
6
6
|
"main": "index.js",
|
package/pii-masking-run.js
CHANGED
|
@@ -27,7 +27,7 @@ const cmdIndex = args.indexOf("--");
|
|
|
27
27
|
|
|
28
28
|
if (args.includes("--version") || args.includes("-v")) {
|
|
29
29
|
const pkgPath = path.resolve(__dirname, './package.json');
|
|
30
|
-
let version = '2.
|
|
30
|
+
let version = '2.5.0';
|
|
31
31
|
try { version = JSON.parse(fs.readFileSync(pkgPath, 'utf8')).version; } catch (_) {}
|
|
32
32
|
console.log(`ps-guard / pii-masking-run v${version} (PrivacyScrubber MCP)`);
|
|
33
33
|
process.exit(0);
|