@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 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.4.7 ║
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
- const oldestKey = agentSessions.keys().next().value;
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
- if (oldestSession && oldestSession.streamDetokenizer) {
181
- oldestSession.streamDetokenizer.reset();
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 + '[*`_~]*[-_\\s]*0*' + idx + '[*`_~.,;:!?\\s]*';
233
- const patternStr = '(?:\\[' + corePat + '\\]|<' + corePat + '>|\\{\\{' + corePat + '\\}\\}|\\{' + corePat + '\\}|__' + corePat + '__|[【[〔「『]' + corePat + '[】]〕」』]|%5B' + corePat + '%5D|&#(?:91|091|x5[bB]);' + corePat + '&#(?:93|093|x5[dD]);|&(?:lbrack|lsqb);' + corePat + '&(?:rbrack|rsqb);|\\\\u005[bB]' + corePat + '\\\\u005[dD]|\\b' + label + '[-_\\s]+0*' + idx + '\\b)';
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(/^(?:%5B|&#(?:91|091|x5[bB]);|&(?:lbrack|lsqb);|\\u005[bB]|\[|<|\{\{?|__|【|[|〔|「|『)/i, '')
264
- .replace(/(?:%5D|&#(?:93|093|x5[dD]);|&(?:rbrack|rsqb);|\\u005[dD]|\]|>|\}\}?|__|】|]|〕|」|』)$/i, '')
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]+?)[-_ ]?(\d+)$/);
290
+ const m = inner.match(/^([A-Za-z_0-9]+?)[-_\\\s]*(\d+)$/);
268
291
  if (m) {
269
- const normKey = (m[1] + '_' + parseInt(m[2], 10)).toUpperCase();
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 tail = this.pendingBuffer.length > tailLen ? this.pendingBuffer.slice(-tailLen) : this.pendingBuffer;
359
- const offset = this.pendingBuffer.length > tailLen ? this.pendingBuffer.length - tailLen : 0;
360
- const m = tail.match(/(?:(?:\[|【|[|〔|「|『|<|\{\{?|__?|%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)?)?)?)?)?)?)$/);
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 isClosedUnderscoreOrUnicode = (candidate.length >= 4 && candidate.startsWith('__') && candidate.endsWith('__')) || /\\u005[Dd]/.test(candidate);
365
- if (isClosedUnderscoreOrUnicode && this.replacer(candidate) !== candidate) {
366
- const toEmit = this.pendingBuffer;
367
- this.pendingBuffer = '';
368
- return this.replacer(toEmit);
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[0].delta.content = restored;
440
- yield chunk;
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) yield 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-Z_]+)_\d+\]/);
701
- const baseType = match ? match[1] : (tokenStr.replace(/\[|\]/g, '').replace(/_[0-9]+$/, '') || 'CUSTOM');
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: Array.from(frameworksSet),
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 descriptions = {
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.triggeredFrameworks.map(f => `\`${f}\``).join(', ')}\n\n`;
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
- if (sess.streamDetokenizer) {
4224
- sess.streamDetokenizer.reset();
4225
- }
4412
+ zeroizeSessionEntry(sess);
4226
4413
  }
4227
4414
  agentSessions.clear();
4228
- getSessionMap('default');
4229
- sessionIgnoreList.clear();
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
- if (sess && sess.streamDetokenizer) {
4241
- sess.streamDetokenizer.reset();
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
- getSessionMap('default');
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.7",
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.4.7",
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",
@@ -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.4.7';
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);