@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 +8 -0
- package/dist/extensions/constants.d.ts +1 -1
- package/dist/extensions/constants.d.ts.map +1 -1
- package/dist/extensions/constants.js +1 -1
- package/dist/extensions/constants.mjs +1 -1
- package/dist/extensions/mcp-payloads.d.ts +10 -0
- package/dist/extensions/mcp-payloads.d.ts.map +1 -1
- package/dist/extensions/mcp-payloads.js +64 -0
- package/dist/extensions/mcp-payloads.mjs +62 -1
- package/dist/extensions/sanitization.d.ts.map +1 -1
- package/dist/extensions/sanitization.js +1 -1
- package/dist/extensions/sanitization.mjs +2 -2
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/version.mjs +1 -1
- package/package.json +4 -4
- package/src/extensions/constants.ts +1 -1
- package/src/extensions/mcp-payloads.ts +129 -0
- package/src/extensions/sanitization.ts +8 -5
- package/src/version.ts +1 -1
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
|
|
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,
|
|
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 =
|
|
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 =
|
|
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":"
|
|
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,
|
|
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.
|
|
1
|
+
export declare const version = "0.13.1";
|
|
2
2
|
//# sourceMappingURL=version.d.ts.map
|
package/dist/version.js
CHANGED
package/dist/version.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
const version = '0.13.
|
|
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.
|
|
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.
|
|
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
|
-
"
|
|
56
|
-
"posthog-
|
|
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
|
|
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
|
-
//
|
|
37
|
-
//
|
|
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.
|
|
1
|
+
export const version = '0.13.1'
|