@posthog/mcp 0.13.0 → 0.13.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 CHANGED
@@ -150,6 +150,14 @@ instrument(server, posthog, {
150
150
 
151
151
  `intentFallback` is the third option: supply the intent yourself when the agent did not send one.
152
152
 
153
+ Whatever the agent narrates, the SDK redacts structured personal identifiers — email addresses, phone
154
+ numbers, IPv4/IPv6 addresses, Luhn-valid card numbers, and US SSNs — from `$mcp_intent` before it is
155
+ sent. This is always on and needs no configuration. It is best-effort for those well-defined shapes,
156
+ not for free-form personal data such as names or postal addresses, which a regex cannot catch without
157
+ over-redacting ordinary prose. It also applies only to `$mcp_intent`: structured tool `arguments` and
158
+ results are left as-is, because the same shapes are often legitimate data there. If you need a stronger
159
+ guarantee, `context: false` and the `beforeSend` hook above remain the ways to drop the field entirely.
160
+
153
161
  ### What `$mcp_llm_model` records, and when it stays empty
154
162
 
155
163
  `captureModel` is **off** by default. Turn it on and the SDK adds a required `llm_model` parameter to
@@ -1,5 +1,5 @@
1
1
  export declare const INACTIVITY_TIMEOUT_IN_MINUTES = 30;
2
- export declare const DEFAULT_CONTEXT_PARAMETER_DESCRIPTION = "Explain why you are calling this tool and how it fits into the user's overall goal. This parameter is used for analytics and user intent tracking. YOU MUST provide 15-25 words (count carefully). NEVER use first person ('I', 'we', 'you') - maintain third-person perspective. NEVER include sensitive information such as credentials, passwords, or personal data. Example (20 words): \"Searching across the organization's repositories to find all open issues related to performance complaints and latency issues for team prioritization.\"";
2
+ export declare const DEFAULT_CONTEXT_PARAMETER_DESCRIPTION = "Explain in 15-25 words, in third person, why this tool is called and how it supports the user's goal. For analytics only. You MUST describe only the abstract purpose of the tool call. NEVER include, repeat, paraphrase, or infer personal, sensitive, or identifying information from the user request or tool results, including names, emails, phone numbers, IPs, IDs, or credentials. You MUST generalize specific entities into roles such as \"a user\", \"the customer\", or \"an account\". Example: \"Retrieving a customer's recent orders to investigate a billing issue and help support determine the appropriate resolution.\"";
3
3
  export declare const DEFAULT_MODEL_PARAMETER_DESCRIPTION = "The exact model identifier you (the assistant) are running as, taken from your system prompt or environment (e.g. \"claude-opus-4-8\", \"gpt-5.2\"). Used for analytics only. If you do not know your model identifier with certainty, pass \"unknown\" \u2014 never guess.";
4
4
  export declare const DEFAULT_CONVERSATION_ID_DESCRIPTION = "Echo the conversation_id from the server's previous response. The server provides it on the first call \u2014 never invent one, and do not issue parallel tool calls until you have it.";
5
5
  export declare const POSTHOG_MCP_ANALYTICS_SOURCE = "posthog_mcp_analytics";
@@ -1 +1 @@
1
- {"version":3,"file":"constants.d.ts","sourceRoot":"","sources":["../../src/extensions/constants.ts"],"names":[],"mappings":"AAKA,eAAO,MAAM,6BAA6B,KAAK,CAAA;AAE/C,eAAO,MAAM,qCAAqC,2hBAAyhB,CAAA;AAE3kB,eAAO,MAAM,mCAAmC,gRAAqQ,CAAA;AAErT,eAAO,MAAM,mCAAmC,4LACsI,CAAA;AAEtL,eAAO,MAAM,4BAA4B,0BAA0B,CAAA;AAMnE,eAAO,MAAM,oBAAoB,qBAAqB,CAAA;AAItD,eAAO,MAAM,wBAAwB;;;;;;;;;;;;CAY3B,CAAA;AAEV,MAAM,MAAM,wBAAwB,GAAG,CAAC,OAAO,wBAAwB,CAAC,CAAC,MAAM,OAAO,wBAAwB,CAAC,CAAA;AAE/G,eAAO,MAAM,2BAA2B;;;;;;;;;;;;;;;;;;;;;;;;;;CA0B9B,CAAA;AAEV,MAAM,MAAM,2BAA2B,GAAG,CAAC,OAAO,2BAA2B,CAAC,CAAC,MAAM,OAAO,2BAA2B,CAAC,CAAA"}
1
+ {"version":3,"file":"constants.d.ts","sourceRoot":"","sources":["../../src/extensions/constants.ts"],"names":[],"mappings":"AAKA,eAAO,MAAM,6BAA6B,KAAK,CAAA;AAE/C,eAAO,MAAM,qCAAqC,onBAA4mB,CAAA;AAE9pB,eAAO,MAAM,mCAAmC,gRAAqQ,CAAA;AAErT,eAAO,MAAM,mCAAmC,4LACsI,CAAA;AAEtL,eAAO,MAAM,4BAA4B,0BAA0B,CAAA;AAMnE,eAAO,MAAM,oBAAoB,qBAAqB,CAAA;AAItD,eAAO,MAAM,wBAAwB;;;;;;;;;;;;CAY3B,CAAA;AAEV,MAAM,MAAM,wBAAwB,GAAG,CAAC,OAAO,wBAAwB,CAAC,CAAC,MAAM,OAAO,wBAAwB,CAAC,CAAA;AAE/G,eAAO,MAAM,2BAA2B;;;;;;;;;;;;;;;;;;;;;;;;;;CA0B9B,CAAA;AAEV,MAAM,MAAM,2BAA2B,GAAG,CAAC,OAAO,2BAA2B,CAAC,CAAC,MAAM,OAAO,2BAA2B,CAAC,CAAA"}
@@ -28,7 +28,7 @@ var __webpack_require__ = {};
28
28
  var __webpack_exports__ = {};
29
29
  __webpack_require__.r(__webpack_exports__);
30
30
  const INACTIVITY_TIMEOUT_IN_MINUTES = 30;
31
- const DEFAULT_CONTEXT_PARAMETER_DESCRIPTION = "Explain why you are calling this tool and how it fits into the user's overall goal. This parameter is used for analytics and user intent tracking. YOU MUST provide 15-25 words (count carefully). NEVER use first person ('I', 'we', 'you') - maintain third-person perspective. NEVER include sensitive information such as credentials, passwords, or personal data. Example (20 words): \"Searching across the organization's repositories to find all open issues related to performance complaints and latency issues for team prioritization.\"";
31
+ const DEFAULT_CONTEXT_PARAMETER_DESCRIPTION = 'Explain in 15-25 words, in third person, why this tool is called and how it supports the user\'s goal. For analytics only. You MUST describe only the abstract purpose of the tool call. NEVER include, repeat, paraphrase, or infer personal, sensitive, or identifying information from the user request or tool results, including names, emails, phone numbers, IPs, IDs, or credentials. You MUST generalize specific entities into roles such as "a user", "the customer", or "an account". Example: "Retrieving a customer\'s recent orders to investigate a billing issue and help support determine the appropriate resolution."';
32
32
  const DEFAULT_MODEL_PARAMETER_DESCRIPTION = 'The exact model identifier you (the assistant) are running as, taken from your system prompt or environment (e.g. "claude-opus-4-8", "gpt-5.2"). Used for analytics only. If you do not know your model identifier with certainty, pass "unknown" — never guess.';
33
33
  const DEFAULT_CONVERSATION_ID_DESCRIPTION = "Echo the conversation_id from the server's previous response. The server provides it on the first call — never invent one, and do not issue parallel tool calls until you have it.";
34
34
  const POSTHOG_MCP_ANALYTICS_SOURCE = 'posthog_mcp_analytics';
@@ -1,5 +1,5 @@
1
1
  const INACTIVITY_TIMEOUT_IN_MINUTES = 30;
2
- const DEFAULT_CONTEXT_PARAMETER_DESCRIPTION = "Explain why you are calling this tool and how it fits into the user's overall goal. This parameter is used for analytics and user intent tracking. YOU MUST provide 15-25 words (count carefully). NEVER use first person ('I', 'we', 'you') - maintain third-person perspective. NEVER include sensitive information such as credentials, passwords, or personal data. Example (20 words): \"Searching across the organization's repositories to find all open issues related to performance complaints and latency issues for team prioritization.\"";
2
+ const DEFAULT_CONTEXT_PARAMETER_DESCRIPTION = 'Explain in 15-25 words, in third person, why this tool is called and how it supports the user\'s goal. For analytics only. You MUST describe only the abstract purpose of the tool call. NEVER include, repeat, paraphrase, or infer personal, sensitive, or identifying information from the user request or tool results, including names, emails, phone numbers, IPs, IDs, or credentials. You MUST generalize specific entities into roles such as "a user", "the customer", or "an account". Example: "Retrieving a customer\'s recent orders to investigate a billing issue and help support determine the appropriate resolution."';
3
3
  const DEFAULT_MODEL_PARAMETER_DESCRIPTION = 'The exact model identifier you (the assistant) are running as, taken from your system prompt or environment (e.g. "claude-opus-4-8", "gpt-5.2"). Used for analytics only. If you do not know your model identifier with certainty, pass "unknown" — never guess.';
4
4
  const DEFAULT_CONVERSATION_ID_DESCRIPTION = "Echo the conversation_id from the server's previous response. The server provides it on the first call — never invent one, and do not issue parallel tool calls until you have it.";
5
5
  const POSTHOG_MCP_ANALYTICS_SOURCE = 'posthog_mcp_analytics';
@@ -1,4 +1,14 @@
1
1
  type JsonRecord = Record<string, unknown>;
2
+ /**
3
+ * Redacts structured personal identifiers (emails, IP addresses, credit-card
4
+ * numbers, US SSNs, and phone numbers) from a free-text string. Intended for the
5
+ * agent-narrated `$mcp_intent` value only — not for structured tool parameters or
6
+ * responses, where the same shapes are often legitimate data. Horizontal Unicode
7
+ * spaces are first normalized to an ASCII space so copy-pasted identifiers still
8
+ * match. Returns a new string; leaves the input's identifiers untouched when
9
+ * nothing matches.
10
+ */
11
+ export declare function redactPii(value: string): string;
2
12
  export declare function sanitizeCapturedValue(value: unknown): unknown;
3
13
  export declare function buildCapturedMcpParameters(request: unknown): JsonRecord;
4
14
  export {};
@@ -1 +1 @@
1
- {"version":3,"file":"mcp-payloads.d.ts","sourceRoot":"","sources":["../../src/extensions/mcp-payloads.ts"],"names":[],"mappings":"AAkBA,KAAK,UAAU,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;AAsCzC,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CA0B7D;AA6BD,wBAAgB,0BAA0B,CAAC,OAAO,EAAE,OAAO,GAAG,UAAU,CAiBvE"}
1
+ {"version":3,"file":"mcp-payloads.d.ts","sourceRoot":"","sources":["../../src/extensions/mcp-payloads.ts"],"names":[],"mappings":"AAmEA,KAAK,UAAU,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;AAiGzC;;;;;;;;GAQG;AACH,wBAAgB,SAAS,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAU/C;AAED,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CA0B7D;AA6BD,wBAAgB,0BAA0B,CAAC,OAAO,EAAE,OAAO,GAAG,UAAU,CAiBvE"}
@@ -38,6 +38,15 @@ const BASE64_DATA_URL_PAYLOAD_PATTERN = /^[A-Za-z0-9+/_-]+={0,2}$/;
38
38
  const SIZE_GATE = 10240;
39
39
  const POSTHOG_TOKEN_PATTERN = /\bph[a-z]_[A-Za-z0-9_-]{20,}\b/g;
40
40
  const SENSITIVE_KEY_PATTERN = /^(authorization|cookie|set-cookie|x-api-key|api[-_]?key|api[-_]?token|access[-_]?token|refresh[-_]?token|token|password|secret|client[-_]?secret|private[-_]?key)$/i;
41
+ const UNICODE_SPACE_PATTERN = /[\u00A0\u1680\u2000-\u200A\u202F\u205F\u3000]/g;
42
+ const EMAIL_PATTERN = /[A-Za-z0-9._%+-]{1,64}@[A-Za-z0-9.-]{1,255}\.[A-Za-z]{2,24}/g;
43
+ const IPV4_PATTERN = /\b(?:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)\.){3}(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)\b/g;
44
+ const IPV6_PATTERN = /\b(?:[0-9A-Fa-f]{1,4}:){7}[0-9A-Fa-f]{1,4}\b|(?<![\w:])(?:[0-9A-Fa-f]{1,4}:){1,7}:(?![\w:])|(?<![\w:])(?:[0-9A-Fa-f]{1,4}:){1,6}:[0-9A-Fa-f]{1,4}(?::[0-9A-Fa-f]{1,4}){0,5}(?![\w])|(?<![\w:])::(?:[0-9A-Fa-f]{1,4}(?::[0-9A-Fa-f]{1,4}){0,6})(?![\w])/g;
45
+ const US_SSN_PATTERN = /\b\d{3}[ .-]\d{2}[ .-]\d{4}\b/g;
46
+ const CREDIT_CARD_CANDIDATE_PATTERN = /\b\d(?:[ ./-]?\d){12,}\b/g;
47
+ const DIGIT_GROUP_PATTERN = /\d+/g;
48
+ const PHONE_NANP_PATTERN = /(?<![\w+])(?:\+?1[ ./-]?)?(?:\(\d{3}\)[ ./-]?|\d{3}[ ./-])\d{3}[ ./-]\d{4}(?![\w])/g;
49
+ const PHONE_INTL_PATTERN = /(?<!\w)\+\d{1,3}(?:[ ./()-]{0,2}\d){7,13}(?![\w])/g;
41
50
  function isRecord(value) {
42
51
  return !!value && 'object' == typeof value && !Array.isArray(value);
43
52
  }
@@ -59,6 +68,58 @@ function sanitizeString(value) {
59
68
  if (value.length >= SIZE_GATE && (BASE64_PATTERN.test(value) || isBase64DataUrl(value) || BASE64URL_SPECIFIC_CHAR_PATTERN.test(value) && BASE64URL_PATTERN.test(value))) return BINARY_REDACTED_VALUE;
60
69
  return value.replace(POSTHOG_TOKEN_PATTERN, REDACTED_VALUE);
61
70
  }
71
+ function passesLuhn(digits) {
72
+ let sum = 0;
73
+ let double = false;
74
+ for(let index = digits.length - 1; index >= 0; index--){
75
+ let digit = digits.charCodeAt(index) - 48;
76
+ if (digit < 0 || digit > 9) return false;
77
+ if (double) {
78
+ digit *= 2;
79
+ if (digit > 9) digit -= 9;
80
+ }
81
+ sum += digit;
82
+ double = !double;
83
+ }
84
+ return sum % 10 === 0;
85
+ }
86
+ function redactCardInMatch(match) {
87
+ const groups = [];
88
+ for(let m = DIGIT_GROUP_PATTERN.exec(match); null !== m; m = DIGIT_GROUP_PATTERN.exec(match))groups.push({
89
+ digits: m[0],
90
+ start: m.index,
91
+ end: m.index + m[0].length
92
+ });
93
+ let output = '';
94
+ let cursor = 0;
95
+ let first = 0;
96
+ while(first < groups.length){
97
+ let digits = '';
98
+ let matchedLast = -1;
99
+ for(let last = first; last < groups.length; last++){
100
+ digits += groups[last].digits;
101
+ if (digits.length > 19) break;
102
+ if (digits.length >= 13 && passesLuhn(digits)) matchedLast = last;
103
+ }
104
+ if (matchedLast >= 0) {
105
+ output += match.slice(cursor, groups[first].start) + REDACTED_VALUE;
106
+ cursor = groups[matchedLast].end;
107
+ first = matchedLast + 1;
108
+ } else first++;
109
+ }
110
+ return output + match.slice(cursor);
111
+ }
112
+ function redactPii(value) {
113
+ let result = value.replace(UNICODE_SPACE_PATTERN, ' ');
114
+ result = result.replace(EMAIL_PATTERN, REDACTED_VALUE);
115
+ result = result.replace(IPV4_PATTERN, REDACTED_VALUE);
116
+ result = result.replace(IPV6_PATTERN, REDACTED_VALUE);
117
+ result = result.replace(CREDIT_CARD_CANDIDATE_PATTERN, redactCardInMatch);
118
+ result = result.replace(US_SSN_PATTERN, REDACTED_VALUE);
119
+ result = result.replace(PHONE_NANP_PATTERN, REDACTED_VALUE);
120
+ result = result.replace(PHONE_INTL_PATTERN, REDACTED_VALUE);
121
+ return result;
122
+ }
62
123
  function sanitizeCapturedValue(value) {
63
124
  if (null == value) return value;
64
125
  if ('string' == typeof value) return sanitizeString(value);
@@ -98,12 +159,15 @@ function buildCapturedMcpParameters(request) {
98
159
  }
99
160
  __webpack_require__.d(__webpack_exports__, {
100
161
  buildCapturedMcpParameters: ()=>buildCapturedMcpParameters,
162
+ redactPii: ()=>redactPii,
101
163
  sanitizeCapturedValue: ()=>sanitizeCapturedValue
102
164
  });
103
165
  exports.buildCapturedMcpParameters = __webpack_exports__.buildCapturedMcpParameters;
166
+ exports.redactPii = __webpack_exports__.redactPii;
104
167
  exports.sanitizeCapturedValue = __webpack_exports__.sanitizeCapturedValue;
105
168
  for(var __rspack_i in __webpack_exports__)if (-1 === [
106
169
  "buildCapturedMcpParameters",
170
+ "redactPii",
107
171
  "sanitizeCapturedValue"
108
172
  ].indexOf(__rspack_i)) exports[__rspack_i] = __webpack_exports__[__rspack_i];
109
173
  Object.defineProperty(exports, '__esModule', {
@@ -9,6 +9,15 @@ const BASE64_DATA_URL_PAYLOAD_PATTERN = /^[A-Za-z0-9+/_-]+={0,2}$/;
9
9
  const SIZE_GATE = 10240;
10
10
  const POSTHOG_TOKEN_PATTERN = /\bph[a-z]_[A-Za-z0-9_-]{20,}\b/g;
11
11
  const SENSITIVE_KEY_PATTERN = /^(authorization|cookie|set-cookie|x-api-key|api[-_]?key|api[-_]?token|access[-_]?token|refresh[-_]?token|token|password|secret|client[-_]?secret|private[-_]?key)$/i;
12
+ const UNICODE_SPACE_PATTERN = /[\u00A0\u1680\u2000-\u200A\u202F\u205F\u3000]/g;
13
+ const EMAIL_PATTERN = /[A-Za-z0-9._%+-]{1,64}@[A-Za-z0-9.-]{1,255}\.[A-Za-z]{2,24}/g;
14
+ const IPV4_PATTERN = /\b(?:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)\.){3}(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)\b/g;
15
+ const IPV6_PATTERN = /\b(?:[0-9A-Fa-f]{1,4}:){7}[0-9A-Fa-f]{1,4}\b|(?<![\w:])(?:[0-9A-Fa-f]{1,4}:){1,7}:(?![\w:])|(?<![\w:])(?:[0-9A-Fa-f]{1,4}:){1,6}:[0-9A-Fa-f]{1,4}(?::[0-9A-Fa-f]{1,4}){0,5}(?![\w])|(?<![\w:])::(?:[0-9A-Fa-f]{1,4}(?::[0-9A-Fa-f]{1,4}){0,6})(?![\w])/g;
16
+ const US_SSN_PATTERN = /\b\d{3}[ .-]\d{2}[ .-]\d{4}\b/g;
17
+ const CREDIT_CARD_CANDIDATE_PATTERN = /\b\d(?:[ ./-]?\d){12,}\b/g;
18
+ const DIGIT_GROUP_PATTERN = /\d+/g;
19
+ const PHONE_NANP_PATTERN = /(?<![\w+])(?:\+?1[ ./-]?)?(?:\(\d{3}\)[ ./-]?|\d{3}[ ./-])\d{3}[ ./-]\d{4}(?![\w])/g;
20
+ const PHONE_INTL_PATTERN = /(?<!\w)\+\d{1,3}(?:[ ./()-]{0,2}\d){7,13}(?![\w])/g;
12
21
  function isRecord(value) {
13
22
  return !!value && 'object' == typeof value && !Array.isArray(value);
14
23
  }
@@ -30,6 +39,58 @@ function sanitizeString(value) {
30
39
  if (value.length >= SIZE_GATE && (BASE64_PATTERN.test(value) || isBase64DataUrl(value) || BASE64URL_SPECIFIC_CHAR_PATTERN.test(value) && BASE64URL_PATTERN.test(value))) return BINARY_REDACTED_VALUE;
31
40
  return value.replace(POSTHOG_TOKEN_PATTERN, REDACTED_VALUE);
32
41
  }
42
+ function passesLuhn(digits) {
43
+ let sum = 0;
44
+ let double = false;
45
+ for(let index = digits.length - 1; index >= 0; index--){
46
+ let digit = digits.charCodeAt(index) - 48;
47
+ if (digit < 0 || digit > 9) return false;
48
+ if (double) {
49
+ digit *= 2;
50
+ if (digit > 9) digit -= 9;
51
+ }
52
+ sum += digit;
53
+ double = !double;
54
+ }
55
+ return sum % 10 === 0;
56
+ }
57
+ function redactCardInMatch(match) {
58
+ const groups = [];
59
+ for(let m = DIGIT_GROUP_PATTERN.exec(match); null !== m; m = DIGIT_GROUP_PATTERN.exec(match))groups.push({
60
+ digits: m[0],
61
+ start: m.index,
62
+ end: m.index + m[0].length
63
+ });
64
+ let output = '';
65
+ let cursor = 0;
66
+ let first = 0;
67
+ while(first < groups.length){
68
+ let digits = '';
69
+ let matchedLast = -1;
70
+ for(let last = first; last < groups.length; last++){
71
+ digits += groups[last].digits;
72
+ if (digits.length > 19) break;
73
+ if (digits.length >= 13 && passesLuhn(digits)) matchedLast = last;
74
+ }
75
+ if (matchedLast >= 0) {
76
+ output += match.slice(cursor, groups[first].start) + REDACTED_VALUE;
77
+ cursor = groups[matchedLast].end;
78
+ first = matchedLast + 1;
79
+ } else first++;
80
+ }
81
+ return output + match.slice(cursor);
82
+ }
83
+ function redactPii(value) {
84
+ let result = value.replace(UNICODE_SPACE_PATTERN, ' ');
85
+ result = result.replace(EMAIL_PATTERN, REDACTED_VALUE);
86
+ result = result.replace(IPV4_PATTERN, REDACTED_VALUE);
87
+ result = result.replace(IPV6_PATTERN, REDACTED_VALUE);
88
+ result = result.replace(CREDIT_CARD_CANDIDATE_PATTERN, redactCardInMatch);
89
+ result = result.replace(US_SSN_PATTERN, REDACTED_VALUE);
90
+ result = result.replace(PHONE_NANP_PATTERN, REDACTED_VALUE);
91
+ result = result.replace(PHONE_INTL_PATTERN, REDACTED_VALUE);
92
+ return result;
93
+ }
33
94
  function sanitizeCapturedValue(value) {
34
95
  if (null == value) return value;
35
96
  if ('string' == typeof value) return sanitizeString(value);
@@ -67,4 +128,4 @@ function buildCapturedMcpParameters(request) {
67
128
  request: capturedRequest
68
129
  };
69
130
  }
70
- export { buildCapturedMcpParameters, sanitizeCapturedValue };
131
+ export { buildCapturedMcpParameters, redactPii, sanitizeCapturedValue };
@@ -1 +1 @@
1
- {"version":3,"file":"sanitization.d.ts","sourceRoot":"","sources":["../../src/extensions/sanitization.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAmB,KAAK,EAAE,QAAQ,EAAE,MAAM,UAAU,CAAA;AAShE;;;;;;;;GAQG;AACH,wBAAgB,aAAa,CAAC,CAAC,SAAS,KAAK,GAAG,QAAQ,EAAE,KAAK,EAAE,CAAC,GAAG,CAAC,CA2BrE"}
1
+ {"version":3,"file":"sanitization.d.ts","sourceRoot":"","sources":["../../src/extensions/sanitization.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAmB,KAAK,EAAE,QAAQ,EAAE,MAAM,UAAU,CAAA;AAShE;;;;;;;;GAQG;AACH,wBAAgB,aAAa,CAAC,CAAC,SAAS,KAAK,GAAG,QAAQ,EAAE,KAAK,EAAE,CAAC,GAAG,CAAC,CA8BrE"}
@@ -40,7 +40,7 @@ function sanitizeEvent(event) {
40
40
  };
41
41
  if (null != result.response) result.response = sanitizeResponse(result.response);
42
42
  if (null != result.parameters) result.parameters = sanitizeParameters(result.parameters);
43
- if (null != result.userIntent) result.userIntent = (0, external_mcp_payloads_js_namespaceObject.sanitizeCapturedValue)(result.userIntent);
43
+ if (null != result.userIntent) result.userIntent = (0, external_mcp_payloads_js_namespaceObject.redactPii)((0, external_mcp_payloads_js_namespaceObject.sanitizeCapturedValue)(result.userIntent));
44
44
  if (null != result.llmModel) result.llmModel = (0, external_mcp_payloads_js_namespaceObject.sanitizeCapturedValue)(result.llmModel);
45
45
  if (null != result.error) result.error = sanitizeExceptionValues(result.error);
46
46
  return result;
@@ -1,4 +1,4 @@
1
- import { sanitizeCapturedValue } from "./mcp-payloads.mjs";
1
+ import { redactPii, sanitizeCapturedValue } from "./mcp-payloads.mjs";
2
2
  function isRecord(value) {
3
3
  return !!value && 'object' == typeof value && !Array.isArray(value);
4
4
  }
@@ -8,7 +8,7 @@ function sanitizeEvent(event) {
8
8
  };
9
9
  if (null != result.response) result.response = sanitizeResponse(result.response);
10
10
  if (null != result.parameters) result.parameters = sanitizeParameters(result.parameters);
11
- if (null != result.userIntent) result.userIntent = sanitizeCapturedValue(result.userIntent);
11
+ if (null != result.userIntent) result.userIntent = redactPii(sanitizeCapturedValue(result.userIntent));
12
12
  if (null != result.llmModel) result.llmModel = sanitizeCapturedValue(result.llmModel);
13
13
  if (null != result.error) result.error = sanitizeExceptionValues(result.error);
14
14
  return result;
package/dist/version.d.ts CHANGED
@@ -1,2 +1,2 @@
1
- export declare const version = "0.13.0";
1
+ export declare const version = "0.13.1";
2
2
  //# sourceMappingURL=version.d.ts.map
package/dist/version.js CHANGED
@@ -27,7 +27,7 @@ var __webpack_require__ = {};
27
27
  })();
28
28
  var __webpack_exports__ = {};
29
29
  __webpack_require__.r(__webpack_exports__);
30
- const version = '0.13.0';
30
+ const version = '0.13.1';
31
31
  __webpack_require__.d(__webpack_exports__, {}, {
32
32
  version: version
33
33
  });
package/dist/version.mjs CHANGED
@@ -1,2 +1,2 @@
1
- const version = '0.13.0';
1
+ const version = '0.13.1';
2
2
  export { version };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@posthog/mcp",
3
- "version": "0.13.0",
3
+ "version": "0.13.1",
4
4
  "bugs": {
5
5
  "url": "https://github.com/PostHog/posthog-js/issues"
6
6
  },
@@ -28,7 +28,7 @@
28
28
  "module": "dist/index.mjs",
29
29
  "types": "dist/index.d.ts",
30
30
  "dependencies": {
31
- "@posthog/core": "^1.50.3"
31
+ "@posthog/core": "^1.50.6"
32
32
  },
33
33
  "devDependencies": {
34
34
  "@babel/preset-env": "^7.27.1",
@@ -52,8 +52,8 @@
52
52
  "zod4": "npm:zod@^4.4.3",
53
53
  "@vitest/coverage-v8": "^1.6.1",
54
54
  "vitest": "^1.6.1",
55
- "@posthog-tooling/tsconfig-base": "1.1.1",
56
- "posthog-node": "^5.51.6"
55
+ "posthog-node": "^5.51.7",
56
+ "@posthog-tooling/tsconfig-base": "1.1.1"
57
57
  },
58
58
  "peerDependencies": {
59
59
  "@modelcontextprotocol/sdk": ">=1.26.0",
@@ -5,7 +5,7 @@
5
5
 
6
6
  export const INACTIVITY_TIMEOUT_IN_MINUTES = 30
7
7
 
8
- export const DEFAULT_CONTEXT_PARAMETER_DESCRIPTION = `Explain why you are calling this tool and how it fits into the user's overall goal. This parameter is used for analytics and user intent tracking. YOU MUST provide 15-25 words (count carefully). NEVER use first person ('I', 'we', 'you') - maintain third-person perspective. NEVER include sensitive information such as credentials, passwords, or personal data. Example (20 words): "Searching across the organization's repositories to find all open issues related to performance complaints and latency issues for team prioritization."`
8
+ export const DEFAULT_CONTEXT_PARAMETER_DESCRIPTION = `Explain in 15-25 words, in third person, why this tool is called and how it supports the user's goal. For analytics only. You MUST describe only the abstract purpose of the tool call. NEVER include, repeat, paraphrase, or infer personal, sensitive, or identifying information from the user request or tool results, including names, emails, phone numbers, IPs, IDs, or credentials. You MUST generalize specific entities into roles such as "a user", "the customer", or "an account". Example: "Retrieving a customer's recent orders to investigate a billing issue and help support determine the appropriate resolution."`
9
9
 
10
10
  export const DEFAULT_MODEL_PARAMETER_DESCRIPTION = `The exact model identifier you (the assistant) are running as, taken from your system prompt or environment (e.g. "claude-opus-4-8", "gpt-5.2"). Used for analytics only. If you do not know your model identifier with certainty, pass "unknown" — never guess.`
11
11
 
@@ -16,6 +16,55 @@ const POSTHOG_TOKEN_PATTERN = /\bph[a-z]_[A-Za-z0-9_-]{20,}\b/g
16
16
  const SENSITIVE_KEY_PATTERN =
17
17
  /^(authorization|cookie|set-cookie|x-api-key|api[-_]?key|api[-_]?token|access[-_]?token|refresh[-_]?token|token|password|secret|client[-_]?secret|private[-_]?key)$/i
18
18
 
19
+ // PII redaction for the agent-narrated intent string only. `$mcp_intent` is free
20
+ // text the calling LLM writes into the injected `context` argument, so it can
21
+ // carry personal data the model read aloud despite being told not to. We redact
22
+ // well-defined *structured identifiers* — the kind regex can match with high
23
+ // precision. Person names and postal addresses are deliberately out of scope:
24
+ // they need an NER model that a client SDK cannot ship, and naive patterns would
25
+ // over-redact ordinary prose. Patterns are ordered so an earlier pass never eats
26
+ // digits a later pass needs (email before phone, IPs before phone, cards before
27
+ // the generic phone pass). See `redactPii`.
28
+ // Horizontal Unicode spaces (NBSP, narrow NBSP, ideographic space, ...) are what
29
+ // appear when text is copied from web pages or PDFs. `redactPii` normalizes them
30
+ // to an ASCII space first so the separator-based card/phone/SSN candidates match
31
+ // them instead of leaking the identifier they group.
32
+ const UNICODE_SPACE_PATTERN = /[\u00A0\u1680\u2000-\u200A\u202F\u205F\u3000]/g
33
+ // Quantifiers are bounded to RFC-ish limits (local-part <=64, domain <=255,
34
+ // TLD <=24) rather than open-ended `+`. Unbounded `+` here is quadratic: on a
35
+ // long run of local-part chars with no valid `.tld`, `replace` rescans from
36
+ // every start position. `$mcp_intent` is attacker-influenceable free text seen
37
+ // before truncation, so an open-ended pattern is a reachable event-loop stall.
38
+ const EMAIL_PATTERN = /[A-Za-z0-9._%+-]{1,64}@[A-Za-z0-9.-]{1,255}\.[A-Za-z]{2,24}/g
39
+ const IPV4_PATTERN = /\b(?:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)\.){3}(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)\b/g
40
+ // Four forms: full 8-group, `::`-terminated (`2001:db8::`), a middle `::`
41
+ // (`2001:db8::8a2e:1`), and a leading `::` (`::1`). The compressed branches use a
42
+ // `(?<![\w:])` boundary so a hex-looking C++ scope like `std::bad` — whose left
43
+ // side is not a valid hex group — is not mistaken for an address, while a
44
+ // genuinely address-shaped `dead::beef` still matches.
45
+ const IPV6_PATTERN =
46
+ /\b(?:[0-9A-Fa-f]{1,4}:){7}[0-9A-Fa-f]{1,4}\b|(?<![\w:])(?:[0-9A-Fa-f]{1,4}:){1,7}:(?![\w:])|(?<![\w:])(?:[0-9A-Fa-f]{1,4}:){1,6}:[0-9A-Fa-f]{1,4}(?::[0-9A-Fa-f]{1,4}){0,5}(?![\w])|(?<![\w:])::(?:[0-9A-Fa-f]{1,4}(?::[0-9A-Fa-f]{1,4}){0,6})(?![\w])/g
47
+ // A separator (space, dot, or dash) is required between the 3-2-4 groups so bare
48
+ // 9-digit IDs are never mistaken for an SSN.
49
+ const US_SSN_PATTERN = /\b\d{3}[ .-]\d{2}[ .-]\d{4}\b/g
50
+ // A run of >=13 digits optionally grouped by a single space, dot, dash, or
51
+ // slash. This only marks the numeric region; `redactCardInMatch` then looks for
52
+ // the actual card as a run of whole separator-delimited groups that passes Luhn,
53
+ // so an adjacent field such as an expiry (`4111 1111 1111 1111 12/30`) is not
54
+ // absorbed into a failing check that would leak the card.
55
+ const CREDIT_CARD_CANDIDATE_PATTERN = /\b\d(?:[ ./-]?\d){12,}\b/g
56
+ // Matches each separator-delimited digit group inside a card candidate.
57
+ const DIGIT_GROUP_PATTERN = /\d+/g
58
+ // Phone matching is structural rather than "any 10–15 digits", so dates
59
+ // (`2024-01-15 12:30`) and dotted versions are not mistaken for numbers. Two
60
+ // forms: a North-American 3-3-4 grouping, and an international number that must
61
+ // start with `+` and a country code. The area code is either `(415)` (the
62
+ // separator after it is optional, so `(415)555-0142` matches) or a bare `415`
63
+ // that must be followed by a separator (space, dot, dash, or slash) — so a bare
64
+ // digit run is never taken for a phone number.
65
+ const PHONE_NANP_PATTERN = /(?<![\w+])(?:\+?1[ ./-]?)?(?:\(\d{3}\)[ ./-]?|\d{3}[ ./-])\d{3}[ ./-]\d{4}(?![\w])/g
66
+ const PHONE_INTL_PATTERN = /(?<!\w)\+\d{1,3}(?:[ ./()-]{0,2}\d){7,13}(?![\w])/g
67
+
19
68
  type JsonRecord = Record<string, unknown>
20
69
 
21
70
  function isRecord(value: unknown): value is JsonRecord {
@@ -54,6 +103,86 @@ function sanitizeString(value: string): string {
54
103
  return value.replace(POSTHOG_TOKEN_PATTERN, REDACTED_VALUE)
55
104
  }
56
105
 
106
+ function passesLuhn(digits: string): boolean {
107
+ let sum = 0
108
+ let double = false
109
+ for (let index = digits.length - 1; index >= 0; index--) {
110
+ let digit = digits.charCodeAt(index) - 48
111
+ if (digit < 0 || digit > 9) {
112
+ return false
113
+ }
114
+ if (double) {
115
+ digit *= 2
116
+ if (digit > 9) {
117
+ digit -= 9
118
+ }
119
+ }
120
+ sum += digit
121
+ double = !double
122
+ }
123
+ return sum % 10 === 0
124
+ }
125
+
126
+ // A card candidate can span more than one card plus adjacent fields (e.g. an
127
+ // expiry, or a second card). This redacts every card in it: scanning
128
+ // left-to-right over the whole separator-delimited digit groups, at each start it
129
+ // takes the longest run whose joined digits are 13–19 long and pass Luhn, redacts
130
+ // just that span, and resumes after it; a group that begins no valid run is kept
131
+ // and skipped. Checking group-aligned runs rather than arbitrary digit windows
132
+ // keeps the false-positive rate at Luhn's own ~1-in-10, instead of letting a
133
+ // chance-valid sub-window of an ordinary long ID trigger redaction.
134
+ function redactCardInMatch(match: string): string {
135
+ const groups: { digits: string; start: number; end: number }[] = []
136
+ for (let m = DIGIT_GROUP_PATTERN.exec(match); m !== null; m = DIGIT_GROUP_PATTERN.exec(match)) {
137
+ groups.push({ digits: m[0], start: m.index, end: m.index + m[0].length })
138
+ }
139
+ let output = ''
140
+ let cursor = 0
141
+ let first = 0
142
+ while (first < groups.length) {
143
+ let digits = ''
144
+ let matchedLast = -1
145
+ for (let last = first; last < groups.length; last++) {
146
+ digits += groups[last].digits
147
+ if (digits.length > 19) {
148
+ break
149
+ }
150
+ if (digits.length >= 13 && passesLuhn(digits)) {
151
+ matchedLast = last
152
+ }
153
+ }
154
+ if (matchedLast >= 0) {
155
+ output += match.slice(cursor, groups[first].start) + REDACTED_VALUE
156
+ cursor = groups[matchedLast].end
157
+ first = matchedLast + 1
158
+ } else {
159
+ first++
160
+ }
161
+ }
162
+ return output + match.slice(cursor)
163
+ }
164
+
165
+ /**
166
+ * Redacts structured personal identifiers (emails, IP addresses, credit-card
167
+ * numbers, US SSNs, and phone numbers) from a free-text string. Intended for the
168
+ * agent-narrated `$mcp_intent` value only — not for structured tool parameters or
169
+ * responses, where the same shapes are often legitimate data. Horizontal Unicode
170
+ * spaces are first normalized to an ASCII space so copy-pasted identifiers still
171
+ * match. Returns a new string; leaves the input's identifiers untouched when
172
+ * nothing matches.
173
+ */
174
+ export function redactPii(value: string): string {
175
+ let result = value.replace(UNICODE_SPACE_PATTERN, ' ')
176
+ result = result.replace(EMAIL_PATTERN, REDACTED_VALUE)
177
+ result = result.replace(IPV4_PATTERN, REDACTED_VALUE)
178
+ result = result.replace(IPV6_PATTERN, REDACTED_VALUE)
179
+ result = result.replace(CREDIT_CARD_CANDIDATE_PATTERN, redactCardInMatch)
180
+ result = result.replace(US_SSN_PATTERN, REDACTED_VALUE)
181
+ result = result.replace(PHONE_NANP_PATTERN, REDACTED_VALUE)
182
+ result = result.replace(PHONE_INTL_PATTERN, REDACTED_VALUE)
183
+ return result
184
+ }
185
+
57
186
  export function sanitizeCapturedValue(value: unknown): unknown {
58
187
  if (value == null) {
59
188
  return value
@@ -4,7 +4,7 @@
4
4
  // Licensed under the MIT License: https://github.com/agentcathq/agentcat-typescript-sdk/blob/main/LICENSE
5
5
 
6
6
  import type { ErrorProperties, Event, McpEvent } from '../types'
7
- import { sanitizeCapturedValue } from './mcp-payloads'
7
+ import { redactPii, sanitizeCapturedValue } from './mcp-payloads'
8
8
 
9
9
  type SanitizedRecord = Record<string, unknown>
10
10
 
@@ -32,11 +32,14 @@ export function sanitizeEvent<T extends Event | McpEvent>(event: T): T {
32
32
  result.parameters = sanitizeParameters(result.parameters)
33
33
  }
34
34
 
35
- // The intent comes straight from an agent-narrated `context` string, so it
36
- // can contain a secret the LLM read aloud. Redact it like any other captured
37
- // value rather than shipping it raw as `$mcp_intent`.
35
+ // The intent comes straight from an agent-narrated `context` string, so it can
36
+ // contain a secret the LLM read aloud or personal data it narrated about the
37
+ // user. Redact it like any other captured value, then strip structured PII
38
+ // (emails, phone numbers, IPs, cards, SSNs) rather than shipping it raw as
39
+ // `$mcp_intent`. PII redaction is scoped to the intent only — structured tool
40
+ // parameters and responses often hold the same shapes as legitimate data.
38
41
  if (result.userIntent != null) {
39
- result.userIntent = sanitizeCapturedValue(result.userIntent) as string
42
+ result.userIntent = redactPii(sanitizeCapturedValue(result.userIntent) as string)
40
43
  }
41
44
 
42
45
  if (result.llmModel != null) {
package/src/version.ts CHANGED
@@ -1 +1 @@
1
- export const version = '0.13.0'
1
+ export const version = '0.13.1'