@posthog/mcp 0.14.1 → 0.16.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.
Files changed (65) hide show
  1. package/LICENSE +64 -0
  2. package/README.md +64 -0
  3. package/dist/extensions/constants.d.ts +9 -0
  4. package/dist/extensions/constants.d.ts.map +1 -1
  5. package/dist/extensions/constants.js +9 -0
  6. package/dist/extensions/constants.mjs +9 -0
  7. package/dist/extensions/event-types.d.ts +1 -0
  8. package/dist/extensions/event-types.d.ts.map +1 -1
  9. package/dist/extensions/event-types.js +1 -0
  10. package/dist/extensions/event-types.mjs +1 -0
  11. package/dist/extensions/feedback.d.ts +41 -0
  12. package/dist/extensions/feedback.d.ts.map +1 -0
  13. package/dist/extensions/feedback.js +259 -0
  14. package/dist/extensions/feedback.mjs +200 -0
  15. package/dist/extensions/instrument-highlevel.d.ts.map +1 -1
  16. package/dist/extensions/instrument-highlevel.js +23 -2
  17. package/dist/extensions/instrument-highlevel.mjs +25 -4
  18. package/dist/extensions/instrument-lowlevel.d.ts +4 -3
  19. package/dist/extensions/instrument-lowlevel.d.ts.map +1 -1
  20. package/dist/extensions/instrument-lowlevel.js +23 -2
  21. package/dist/extensions/instrument-lowlevel.mjs +25 -4
  22. package/dist/extensions/instrumentation.d.ts +33 -13
  23. package/dist/extensions/instrumentation.d.ts.map +1 -1
  24. package/dist/extensions/instrumentation.js +103 -9
  25. package/dist/extensions/instrumentation.mjs +99 -8
  26. package/dist/extensions/internal.js +9 -2
  27. package/dist/extensions/internal.mjs +9 -2
  28. package/dist/extensions/mcp-payloads.d.ts +19 -0
  29. package/dist/extensions/mcp-payloads.d.ts.map +1 -1
  30. package/dist/extensions/mcp-payloads.js +217 -7
  31. package/dist/extensions/mcp-payloads.mjs +210 -6
  32. package/dist/extensions/posthog-events.d.ts.map +1 -1
  33. package/dist/extensions/posthog-events.js +1 -0
  34. package/dist/extensions/posthog-events.mjs +1 -0
  35. package/dist/extensions/posthog-mcp.d.ts +23 -3
  36. package/dist/extensions/posthog-mcp.d.ts.map +1 -1
  37. package/dist/extensions/posthog-mcp.js +27 -1
  38. package/dist/extensions/posthog-mcp.mjs +27 -1
  39. package/dist/extensions/sanitization.d.ts.map +1 -1
  40. package/dist/extensions/sanitization.js +2 -1
  41. package/dist/extensions/sanitization.mjs +3 -2
  42. package/dist/index.d.ts +5 -4
  43. package/dist/index.d.ts.map +1 -1
  44. package/dist/index.js +10 -0
  45. package/dist/index.mjs +5 -1
  46. package/dist/types.d.ts +114 -0
  47. package/dist/types.d.ts.map +1 -1
  48. package/dist/version.d.ts +1 -1
  49. package/dist/version.js +1 -1
  50. package/dist/version.mjs +1 -1
  51. package/package.json +4 -4
  52. package/src/extensions/constants.ts +9 -0
  53. package/src/extensions/event-types.ts +1 -0
  54. package/src/extensions/feedback.ts +349 -0
  55. package/src/extensions/instrument-highlevel.ts +45 -2
  56. package/src/extensions/instrument-lowlevel.ts +49 -5
  57. package/src/extensions/instrumentation.ts +170 -17
  58. package/src/extensions/internal.ts +13 -2
  59. package/src/extensions/mcp-payloads.ts +443 -7
  60. package/src/extensions/posthog-events.ts +1 -0
  61. package/src/extensions/posthog-mcp.ts +74 -3
  62. package/src/extensions/sanitization.ts +14 -6
  63. package/src/index.ts +24 -3
  64. package/src/types.ts +120 -0
  65. package/src/version.ts +1 -1
@@ -38,6 +38,18 @@ 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 URL_PATTERN = /[a-z][a-z0-9+.-]{0,63}:[^\s<>"]+/gi;
42
+ const URL_PATTERN_ONCE = new RegExp(URL_PATTERN.source, 'i');
43
+ const URL_AUTHORITY_SEARCH = /[a-z][a-z0-9+.-]{0,63}:\/\//i;
44
+ const URL_AUTHORITY_PATTERN = new RegExp(`^${URL_AUTHORITY_SEARCH.source}`, 'i');
45
+ const URL_AUTHORITY_SEARCH_ALL = new RegExp(URL_AUTHORITY_SEARCH.source, 'gi');
46
+ const URL_FIELD_CHARACTERS = '=&';
47
+ const LEGACY_FIELD_SEPARATOR = ';';
48
+ const URL_TRAILING_PUNCTUATION = ".,;:!?)]}'";
49
+ const MAX_URL_LENGTH = 8192;
50
+ const MAX_URL_QUERY_FIELDS = 128;
51
+ const SENSITIVE_QUERY_KEY_SEGMENT_PATTERN = /(^|[-_./;])(auth|token|secret|password|passwd|pwd|credential|signature|sig|key|hmac|sas|bearer|jwt|session|sessionid)([-_./;]|$)/i;
52
+ const SENSITIVE_QUERY_KEY_EXACT_PATTERN = /^(code|AWSAccessKeyId|GoogleAccessId|Policy)$/i;
41
53
  const UNICODE_SPACE_PATTERN = /[\u00A0\u1680\u2000-\u200A\u202F\u205F\u3000]/g;
42
54
  const EMAIL_PATTERN = /[A-Za-z0-9._%+-]{1,64}@[A-Za-z0-9.-]{1,255}\.[A-Za-z]{2,24}/g;
43
55
  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;
@@ -53,6 +65,9 @@ function isRecord(value) {
53
65
  function shouldRedactKey(key) {
54
66
  return SENSITIVE_KEY_PATTERN.test(key);
55
67
  }
68
+ function shouldRedactQueryKey(key) {
69
+ return shouldRedactKey(key) || SENSITIVE_QUERY_KEY_SEGMENT_PATTERN.test(key) || SENSITIVE_QUERY_KEY_EXACT_PATTERN.test(key);
70
+ }
56
71
  function isBase64DataUrl(value) {
57
72
  const prefix = BASE64_DATA_URL_PREFIX_PATTERN.exec(value);
58
73
  if (!prefix) return false;
@@ -64,10 +79,193 @@ function isBase64DataUrl(value) {
64
79
  }
65
80
  return BASE64_DATA_URL_PAYLOAD_PATTERN.test(payload.replace(/[\r\n]/g, ''));
66
81
  }
67
- function sanitizeString(value) {
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;
82
+ function exceedsUrlFieldLimit(fields) {
83
+ return fields.split('&', MAX_URL_QUERY_FIELDS + 1).length > MAX_URL_QUERY_FIELDS;
84
+ }
85
+ function hasLegacySensitiveField(value) {
86
+ return value.split(LEGACY_FIELD_SEPARATOR).some((piece)=>shouldRedactQueryKey(piece.split('=', 1)[0]));
87
+ }
88
+ function sanitizeUrlFieldValue(key, value, allowNestedUrls) {
89
+ if (shouldRedactQueryKey(key)) return REDACTED_VALUE;
90
+ if (value.includes(LEGACY_FIELD_SEPARATOR) && hasLegacySensitiveField(value)) return REDACTED_VALUE;
91
+ if (!URL_PATTERN_ONCE.test(value)) return value;
92
+ return allowNestedUrls ? sanitizeUrlsInString(value, {
93
+ allowNestedUrls: false,
94
+ stripPunctuation: true
95
+ }) : REDACTED_VALUE;
96
+ }
97
+ function sanitizeUrlFields(fields, allowNestedUrls) {
98
+ const sanitized = new URLSearchParams();
99
+ let changed = false;
100
+ let lastFieldSensitive = false;
101
+ for (const [key, value] of new URLSearchParams(fields)){
102
+ const sanitizedValue = sanitizeUrlFieldValue(key, value, allowNestedUrls);
103
+ changed ||= sanitizedValue !== value;
104
+ lastFieldSensitive = shouldRedactQueryKey(key) || sanitizedValue !== value;
105
+ sanitized.append(key, sanitizedValue);
106
+ }
107
+ return {
108
+ changed,
109
+ lastFieldSensitive,
110
+ serialized: sanitized.toString()
111
+ };
112
+ }
113
+ function splitTrailingPunctuation(value) {
114
+ let end = value.length;
115
+ while(end > 0 && URL_TRAILING_PUNCTUATION.includes(value[end - 1]))end--;
116
+ return {
117
+ address: value.slice(0, end),
118
+ suffix: value.slice(end)
119
+ };
120
+ }
121
+ function sanitizeFragmentTail(tail, headLastFieldSensitive, allowNestedUrls) {
122
+ if (!headLastFieldSensitive) return sanitizeFragmentPart(tail, allowNestedUrls);
123
+ return {
124
+ value: '' === tail ? '' : REDACTED_VALUE,
125
+ lastFieldSensitive: true
126
+ };
127
+ }
128
+ function splitFragmentParts(hash) {
129
+ const fragment = hash.slice(1);
130
+ const separator = fragment.indexOf('?');
131
+ if (separator < 0) return {
132
+ head: fragment,
133
+ tail: null
134
+ };
135
+ return {
136
+ head: fragment.slice(0, separator),
137
+ tail: fragment.slice(separator + 1)
138
+ };
139
+ }
140
+ function sanitizeFragmentPart(part, allowNestedUrls) {
141
+ if (!part.includes('=')) return {
142
+ value: sanitizeFragmentText(part, allowNestedUrls),
143
+ lastFieldSensitive: false
144
+ };
145
+ const fields = sanitizeUrlFields(part, allowNestedUrls);
146
+ return {
147
+ value: fields.changed ? fields.serialized : part,
148
+ lastFieldSensitive: fields.lastFieldSensitive
149
+ };
150
+ }
151
+ function sanitizeFragmentText(text, allowNestedUrls) {
152
+ if (!allowNestedUrls) return URL_PATTERN_ONCE.test(text) ? REDACTED_VALUE : text;
153
+ return sanitizeUrlsInString(text, {
154
+ allowNestedUrls: false,
155
+ stripPunctuation: true
156
+ });
157
+ }
158
+ function findEmbeddedAuthorityIndexes(value) {
159
+ const starts = [
160
+ ...value.matchAll(URL_AUTHORITY_SEARCH_ALL)
161
+ ].map((match)=>match.index);
162
+ const indexes = [];
163
+ const delimiters = {
164
+ query: false,
165
+ fragment: false,
166
+ fragmentQuery: false
167
+ };
168
+ let pending = 0;
169
+ let inFields = false;
170
+ let lastStructural = '';
171
+ for(let cursor = 0; cursor < value.length && pending < starts.length; cursor++){
172
+ if (starts[pending] === cursor) {
173
+ if (cursor > 0 && '=' !== lastStructural) indexes.push(cursor);
174
+ pending++;
175
+ }
176
+ const character = value[cursor];
177
+ if (isDelimiter(character, delimiters, lastStructural)) {
178
+ lastStructural = character;
179
+ inFields = true;
180
+ } else if (inFields && URL_FIELD_CHARACTERS.includes(character)) lastStructural = character;
181
+ }
182
+ return indexes;
183
+ }
184
+ function isDelimiter(character, seen, lastStructural) {
185
+ if ('#' === character) return seen.fragment ? false : seen.fragment = true;
186
+ if ('?' !== character) return false;
187
+ if (seen.fragment) {
188
+ if ('=' === lastStructural) return false;
189
+ return seen.fragmentQuery ? false : seen.fragmentQuery = true;
190
+ }
191
+ return seen.query ? false : seen.query = true;
192
+ }
193
+ function sanitizeUrl(value, mode) {
194
+ const embedded = findEmbeddedAuthorityIndexes(value);
195
+ if (0 === embedded.length) return sanitizeSingleUrl(value, mode);
196
+ let result = '';
197
+ let start = 0;
198
+ for (const index of embedded){
199
+ result += sanitizeSingleUrl(value.slice(start, index), {
200
+ ...mode,
201
+ stripPunctuation: false
202
+ });
203
+ start = index;
204
+ }
205
+ return result + sanitizeSingleUrl(value.slice(start), mode);
206
+ }
207
+ function sanitizeSingleUrl(value, mode) {
208
+ if (value.length > MAX_URL_LENGTH && URL_AUTHORITY_PATTERN.test(value)) return REDACTED_VALUE;
209
+ const { address, suffix } = mode.stripPunctuation ? splitTrailingPunctuation(value) : {
210
+ address: value,
211
+ suffix: ''
212
+ };
213
+ let url;
214
+ try {
215
+ url = new URL(address);
216
+ } catch {
217
+ return REDACTED_VALUE + suffix;
218
+ }
219
+ const query = url.search.slice(1);
220
+ const hasFragment = '' !== url.hash;
221
+ const fragment = splitFragmentParts(url.hash);
222
+ if (exceedsUrlFieldLimit(query) || exceedsUrlFieldLimit(fragment.head) || exceedsUrlFieldLimit(fragment.tail ?? '')) return REDACTED_VALUE + suffix;
223
+ let changed = false;
224
+ if (url.username || url.password) {
225
+ url.username = REDACTED_VALUE;
226
+ url.password = '';
227
+ changed = true;
228
+ }
229
+ const sanitizedQuery = sanitizeUrlFields(query, mode.allowNestedUrls);
230
+ if (sanitizedQuery.changed) {
231
+ url.search = sanitizedQuery.serialized;
232
+ changed = true;
233
+ }
234
+ const sanitizedHead = sanitizeFragmentPart(fragment.head, mode.allowNestedUrls);
235
+ const sanitizedTail = null === fragment.tail ? null : sanitizeFragmentTail(fragment.tail, sanitizedHead.lastFieldSensitive, mode.allowNestedUrls);
236
+ const sanitizedHash = null === sanitizedTail ? sanitizedHead.value : `${sanitizedHead.value}?${sanitizedTail.value}`;
237
+ if (sanitizedHash !== url.hash.slice(1)) {
238
+ url.hash = sanitizedHash;
239
+ changed = true;
240
+ }
241
+ const trailingFields = hasFragment ? sanitizedTail ?? sanitizedHead : sanitizedQuery;
242
+ return (changed ? url.toString() : address) + (trailingFields.lastFieldSensitive ? '' : suffix);
243
+ }
244
+ function sanitizeUrlsInString(text, mode) {
245
+ return text.replace(URL_PATTERN, (match)=>sanitizeUrl(match, {
246
+ ...mode,
247
+ stripPunctuation: mode.stripPunctuation && match.length !== text.length
248
+ }));
249
+ }
250
+ function isBinaryBlob(value) {
251
+ return value.length >= SIZE_GATE && (BASE64_PATTERN.test(value) || isBase64DataUrl(value) || BASE64URL_SPECIFIC_CHAR_PATTERN.test(value) && BASE64URL_PATTERN.test(value));
252
+ }
253
+ function redactCredentials(value) {
69
254
  return value.replace(POSTHOG_TOKEN_PATTERN, REDACTED_VALUE);
70
255
  }
256
+ function redactUrls(value) {
257
+ return sanitizeUrlsInString(value, {
258
+ allowNestedUrls: true,
259
+ stripPunctuation: true
260
+ });
261
+ }
262
+ function sanitizeString(value) {
263
+ return isBinaryBlob(value) ? BINARY_REDACTED_VALUE : redactUrls(redactCredentials(value));
264
+ }
265
+ function sanitizeFreeText(value) {
266
+ if (isBinaryBlob(value)) return BINARY_REDACTED_VALUE;
267
+ return redactUrls(redactPii(redactCredentials(value)));
268
+ }
71
269
  function passesLuhn(digits) {
72
270
  let sum = 0;
73
271
  let double = false;
@@ -121,13 +319,19 @@ function redactPii(value) {
121
319
  return result;
122
320
  }
123
321
  function sanitizeCapturedValue(value) {
322
+ return sanitizeValueWith(value, sanitizeString);
323
+ }
324
+ function sanitizeFreeTextValue(value) {
325
+ return sanitizeValueWith(value, sanitizeFreeText);
326
+ }
327
+ function sanitizeValueWith(value, sanitizeStringFn) {
124
328
  if (null == value) return value;
125
- if ('string' == typeof value) return sanitizeString(value);
126
- if (Array.isArray(value)) return value.map(sanitizeCapturedValue);
329
+ if ('string' == typeof value) return sanitizeStringFn(value);
330
+ if (Array.isArray(value)) return value.map((item)=>sanitizeValueWith(item, sanitizeStringFn));
127
331
  if (value instanceof Date) return value;
128
332
  if ('object' != typeof value) return value;
129
333
  const result = {};
130
- for (const [key, nestedValue] of Object.entries(value))result[key] = shouldRedactKey(key) ? REDACTED_VALUE : sanitizeCapturedValue(nestedValue);
334
+ for (const [key, nestedValue] of Object.entries(value))result[key] = shouldRedactKey(key) ? REDACTED_VALUE : sanitizeValueWith(nestedValue, sanitizeStringFn);
131
335
  return result;
132
336
  }
133
337
  function buildCapturedMcpArguments(argumentsValue) {
@@ -160,15 +364,21 @@ function buildCapturedMcpParameters(request) {
160
364
  __webpack_require__.d(__webpack_exports__, {
161
365
  buildCapturedMcpParameters: ()=>buildCapturedMcpParameters,
162
366
  redactPii: ()=>redactPii,
163
- sanitizeCapturedValue: ()=>sanitizeCapturedValue
367
+ sanitizeCapturedValue: ()=>sanitizeCapturedValue,
368
+ sanitizeFreeText: ()=>sanitizeFreeText,
369
+ sanitizeFreeTextValue: ()=>sanitizeFreeTextValue
164
370
  });
165
371
  exports.buildCapturedMcpParameters = __webpack_exports__.buildCapturedMcpParameters;
166
372
  exports.redactPii = __webpack_exports__.redactPii;
167
373
  exports.sanitizeCapturedValue = __webpack_exports__.sanitizeCapturedValue;
374
+ exports.sanitizeFreeText = __webpack_exports__.sanitizeFreeText;
375
+ exports.sanitizeFreeTextValue = __webpack_exports__.sanitizeFreeTextValue;
168
376
  for(var __rspack_i in __webpack_exports__)if (-1 === [
169
377
  "buildCapturedMcpParameters",
170
378
  "redactPii",
171
- "sanitizeCapturedValue"
379
+ "sanitizeCapturedValue",
380
+ "sanitizeFreeText",
381
+ "sanitizeFreeTextValue"
172
382
  ].indexOf(__rspack_i)) exports[__rspack_i] = __webpack_exports__[__rspack_i];
173
383
  Object.defineProperty(exports, '__esModule', {
174
384
  value: true
@@ -9,6 +9,18 @@ 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 URL_PATTERN = /[a-z][a-z0-9+.-]{0,63}:[^\s<>"]+/gi;
13
+ const URL_PATTERN_ONCE = new RegExp(URL_PATTERN.source, 'i');
14
+ const URL_AUTHORITY_SEARCH = /[a-z][a-z0-9+.-]{0,63}:\/\//i;
15
+ const URL_AUTHORITY_PATTERN = new RegExp(`^${URL_AUTHORITY_SEARCH.source}`, 'i');
16
+ const URL_AUTHORITY_SEARCH_ALL = new RegExp(URL_AUTHORITY_SEARCH.source, 'gi');
17
+ const URL_FIELD_CHARACTERS = '=&';
18
+ const LEGACY_FIELD_SEPARATOR = ';';
19
+ const URL_TRAILING_PUNCTUATION = ".,;:!?)]}'";
20
+ const MAX_URL_LENGTH = 8192;
21
+ const MAX_URL_QUERY_FIELDS = 128;
22
+ const SENSITIVE_QUERY_KEY_SEGMENT_PATTERN = /(^|[-_./;])(auth|token|secret|password|passwd|pwd|credential|signature|sig|key|hmac|sas|bearer|jwt|session|sessionid)([-_./;]|$)/i;
23
+ const SENSITIVE_QUERY_KEY_EXACT_PATTERN = /^(code|AWSAccessKeyId|GoogleAccessId|Policy)$/i;
12
24
  const UNICODE_SPACE_PATTERN = /[\u00A0\u1680\u2000-\u200A\u202F\u205F\u3000]/g;
13
25
  const EMAIL_PATTERN = /[A-Za-z0-9._%+-]{1,64}@[A-Za-z0-9.-]{1,255}\.[A-Za-z]{2,24}/g;
14
26
  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;
@@ -24,6 +36,9 @@ function isRecord(value) {
24
36
  function shouldRedactKey(key) {
25
37
  return SENSITIVE_KEY_PATTERN.test(key);
26
38
  }
39
+ function shouldRedactQueryKey(key) {
40
+ return shouldRedactKey(key) || SENSITIVE_QUERY_KEY_SEGMENT_PATTERN.test(key) || SENSITIVE_QUERY_KEY_EXACT_PATTERN.test(key);
41
+ }
27
42
  function isBase64DataUrl(value) {
28
43
  const prefix = BASE64_DATA_URL_PREFIX_PATTERN.exec(value);
29
44
  if (!prefix) return false;
@@ -35,10 +50,193 @@ function isBase64DataUrl(value) {
35
50
  }
36
51
  return BASE64_DATA_URL_PAYLOAD_PATTERN.test(payload.replace(/[\r\n]/g, ''));
37
52
  }
38
- function sanitizeString(value) {
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;
53
+ function exceedsUrlFieldLimit(fields) {
54
+ return fields.split('&', MAX_URL_QUERY_FIELDS + 1).length > MAX_URL_QUERY_FIELDS;
55
+ }
56
+ function hasLegacySensitiveField(value) {
57
+ return value.split(LEGACY_FIELD_SEPARATOR).some((piece)=>shouldRedactQueryKey(piece.split('=', 1)[0]));
58
+ }
59
+ function sanitizeUrlFieldValue(key, value, allowNestedUrls) {
60
+ if (shouldRedactQueryKey(key)) return REDACTED_VALUE;
61
+ if (value.includes(LEGACY_FIELD_SEPARATOR) && hasLegacySensitiveField(value)) return REDACTED_VALUE;
62
+ if (!URL_PATTERN_ONCE.test(value)) return value;
63
+ return allowNestedUrls ? sanitizeUrlsInString(value, {
64
+ allowNestedUrls: false,
65
+ stripPunctuation: true
66
+ }) : REDACTED_VALUE;
67
+ }
68
+ function sanitizeUrlFields(fields, allowNestedUrls) {
69
+ const sanitized = new URLSearchParams();
70
+ let changed = false;
71
+ let lastFieldSensitive = false;
72
+ for (const [key, value] of new URLSearchParams(fields)){
73
+ const sanitizedValue = sanitizeUrlFieldValue(key, value, allowNestedUrls);
74
+ changed ||= sanitizedValue !== value;
75
+ lastFieldSensitive = shouldRedactQueryKey(key) || sanitizedValue !== value;
76
+ sanitized.append(key, sanitizedValue);
77
+ }
78
+ return {
79
+ changed,
80
+ lastFieldSensitive,
81
+ serialized: sanitized.toString()
82
+ };
83
+ }
84
+ function splitTrailingPunctuation(value) {
85
+ let end = value.length;
86
+ while(end > 0 && URL_TRAILING_PUNCTUATION.includes(value[end - 1]))end--;
87
+ return {
88
+ address: value.slice(0, end),
89
+ suffix: value.slice(end)
90
+ };
91
+ }
92
+ function sanitizeFragmentTail(tail, headLastFieldSensitive, allowNestedUrls) {
93
+ if (!headLastFieldSensitive) return sanitizeFragmentPart(tail, allowNestedUrls);
94
+ return {
95
+ value: '' === tail ? '' : REDACTED_VALUE,
96
+ lastFieldSensitive: true
97
+ };
98
+ }
99
+ function splitFragmentParts(hash) {
100
+ const fragment = hash.slice(1);
101
+ const separator = fragment.indexOf('?');
102
+ if (separator < 0) return {
103
+ head: fragment,
104
+ tail: null
105
+ };
106
+ return {
107
+ head: fragment.slice(0, separator),
108
+ tail: fragment.slice(separator + 1)
109
+ };
110
+ }
111
+ function sanitizeFragmentPart(part, allowNestedUrls) {
112
+ if (!part.includes('=')) return {
113
+ value: sanitizeFragmentText(part, allowNestedUrls),
114
+ lastFieldSensitive: false
115
+ };
116
+ const fields = sanitizeUrlFields(part, allowNestedUrls);
117
+ return {
118
+ value: fields.changed ? fields.serialized : part,
119
+ lastFieldSensitive: fields.lastFieldSensitive
120
+ };
121
+ }
122
+ function sanitizeFragmentText(text, allowNestedUrls) {
123
+ if (!allowNestedUrls) return URL_PATTERN_ONCE.test(text) ? REDACTED_VALUE : text;
124
+ return sanitizeUrlsInString(text, {
125
+ allowNestedUrls: false,
126
+ stripPunctuation: true
127
+ });
128
+ }
129
+ function findEmbeddedAuthorityIndexes(value) {
130
+ const starts = [
131
+ ...value.matchAll(URL_AUTHORITY_SEARCH_ALL)
132
+ ].map((match)=>match.index);
133
+ const indexes = [];
134
+ const delimiters = {
135
+ query: false,
136
+ fragment: false,
137
+ fragmentQuery: false
138
+ };
139
+ let pending = 0;
140
+ let inFields = false;
141
+ let lastStructural = '';
142
+ for(let cursor = 0; cursor < value.length && pending < starts.length; cursor++){
143
+ if (starts[pending] === cursor) {
144
+ if (cursor > 0 && '=' !== lastStructural) indexes.push(cursor);
145
+ pending++;
146
+ }
147
+ const character = value[cursor];
148
+ if (isDelimiter(character, delimiters, lastStructural)) {
149
+ lastStructural = character;
150
+ inFields = true;
151
+ } else if (inFields && URL_FIELD_CHARACTERS.includes(character)) lastStructural = character;
152
+ }
153
+ return indexes;
154
+ }
155
+ function isDelimiter(character, seen, lastStructural) {
156
+ if ('#' === character) return seen.fragment ? false : seen.fragment = true;
157
+ if ('?' !== character) return false;
158
+ if (seen.fragment) {
159
+ if ('=' === lastStructural) return false;
160
+ return seen.fragmentQuery ? false : seen.fragmentQuery = true;
161
+ }
162
+ return seen.query ? false : seen.query = true;
163
+ }
164
+ function sanitizeUrl(value, mode) {
165
+ const embedded = findEmbeddedAuthorityIndexes(value);
166
+ if (0 === embedded.length) return sanitizeSingleUrl(value, mode);
167
+ let result = '';
168
+ let start = 0;
169
+ for (const index of embedded){
170
+ result += sanitizeSingleUrl(value.slice(start, index), {
171
+ ...mode,
172
+ stripPunctuation: false
173
+ });
174
+ start = index;
175
+ }
176
+ return result + sanitizeSingleUrl(value.slice(start), mode);
177
+ }
178
+ function sanitizeSingleUrl(value, mode) {
179
+ if (value.length > MAX_URL_LENGTH && URL_AUTHORITY_PATTERN.test(value)) return REDACTED_VALUE;
180
+ const { address, suffix } = mode.stripPunctuation ? splitTrailingPunctuation(value) : {
181
+ address: value,
182
+ suffix: ''
183
+ };
184
+ let url;
185
+ try {
186
+ url = new URL(address);
187
+ } catch {
188
+ return REDACTED_VALUE + suffix;
189
+ }
190
+ const query = url.search.slice(1);
191
+ const hasFragment = '' !== url.hash;
192
+ const fragment = splitFragmentParts(url.hash);
193
+ if (exceedsUrlFieldLimit(query) || exceedsUrlFieldLimit(fragment.head) || exceedsUrlFieldLimit(fragment.tail ?? '')) return REDACTED_VALUE + suffix;
194
+ let changed = false;
195
+ if (url.username || url.password) {
196
+ url.username = REDACTED_VALUE;
197
+ url.password = '';
198
+ changed = true;
199
+ }
200
+ const sanitizedQuery = sanitizeUrlFields(query, mode.allowNestedUrls);
201
+ if (sanitizedQuery.changed) {
202
+ url.search = sanitizedQuery.serialized;
203
+ changed = true;
204
+ }
205
+ const sanitizedHead = sanitizeFragmentPart(fragment.head, mode.allowNestedUrls);
206
+ const sanitizedTail = null === fragment.tail ? null : sanitizeFragmentTail(fragment.tail, sanitizedHead.lastFieldSensitive, mode.allowNestedUrls);
207
+ const sanitizedHash = null === sanitizedTail ? sanitizedHead.value : `${sanitizedHead.value}?${sanitizedTail.value}`;
208
+ if (sanitizedHash !== url.hash.slice(1)) {
209
+ url.hash = sanitizedHash;
210
+ changed = true;
211
+ }
212
+ const trailingFields = hasFragment ? sanitizedTail ?? sanitizedHead : sanitizedQuery;
213
+ return (changed ? url.toString() : address) + (trailingFields.lastFieldSensitive ? '' : suffix);
214
+ }
215
+ function sanitizeUrlsInString(text, mode) {
216
+ return text.replace(URL_PATTERN, (match)=>sanitizeUrl(match, {
217
+ ...mode,
218
+ stripPunctuation: mode.stripPunctuation && match.length !== text.length
219
+ }));
220
+ }
221
+ function isBinaryBlob(value) {
222
+ return value.length >= SIZE_GATE && (BASE64_PATTERN.test(value) || isBase64DataUrl(value) || BASE64URL_SPECIFIC_CHAR_PATTERN.test(value) && BASE64URL_PATTERN.test(value));
223
+ }
224
+ function redactCredentials(value) {
40
225
  return value.replace(POSTHOG_TOKEN_PATTERN, REDACTED_VALUE);
41
226
  }
227
+ function redactUrls(value) {
228
+ return sanitizeUrlsInString(value, {
229
+ allowNestedUrls: true,
230
+ stripPunctuation: true
231
+ });
232
+ }
233
+ function sanitizeString(value) {
234
+ return isBinaryBlob(value) ? BINARY_REDACTED_VALUE : redactUrls(redactCredentials(value));
235
+ }
236
+ function sanitizeFreeText(value) {
237
+ if (isBinaryBlob(value)) return BINARY_REDACTED_VALUE;
238
+ return redactUrls(redactPii(redactCredentials(value)));
239
+ }
42
240
  function passesLuhn(digits) {
43
241
  let sum = 0;
44
242
  let double = false;
@@ -92,13 +290,19 @@ function redactPii(value) {
92
290
  return result;
93
291
  }
94
292
  function sanitizeCapturedValue(value) {
293
+ return sanitizeValueWith(value, sanitizeString);
294
+ }
295
+ function sanitizeFreeTextValue(value) {
296
+ return sanitizeValueWith(value, sanitizeFreeText);
297
+ }
298
+ function sanitizeValueWith(value, sanitizeStringFn) {
95
299
  if (null == value) return value;
96
- if ('string' == typeof value) return sanitizeString(value);
97
- if (Array.isArray(value)) return value.map(sanitizeCapturedValue);
300
+ if ('string' == typeof value) return sanitizeStringFn(value);
301
+ if (Array.isArray(value)) return value.map((item)=>sanitizeValueWith(item, sanitizeStringFn));
98
302
  if (value instanceof Date) return value;
99
303
  if ('object' != typeof value) return value;
100
304
  const result = {};
101
- for (const [key, nestedValue] of Object.entries(value))result[key] = shouldRedactKey(key) ? REDACTED_VALUE : sanitizeCapturedValue(nestedValue);
305
+ for (const [key, nestedValue] of Object.entries(value))result[key] = shouldRedactKey(key) ? REDACTED_VALUE : sanitizeValueWith(nestedValue, sanitizeStringFn);
102
306
  return result;
103
307
  }
104
308
  function buildCapturedMcpArguments(argumentsValue) {
@@ -128,4 +332,4 @@ function buildCapturedMcpParameters(request) {
128
332
  request: capturedRequest
129
333
  };
130
334
  }
131
- export { buildCapturedMcpParameters, redactPii, sanitizeCapturedValue };
335
+ export { buildCapturedMcpParameters, redactPii, sanitizeCapturedValue, sanitizeFreeText, sanitizeFreeTextValue };
@@ -1 +1 @@
1
- {"version":3,"file":"posthog-events.d.ts","sourceRoot":"","sources":["../../src/extensions/posthog-events.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,UAAU,CAAA;AAyBrC,MAAM,WAAW,mBAAmB;IAClC,WAAW,EAAE,MAAM,CAAA;IACnB,KAAK,EAAE,MAAM,CAAA;IACb,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IACnC,SAAS,EAAE,MAAM,CAAA;IACjB,IAAI,EAAE,SAAS,CAAA;CAChB;AAED,MAAM,WAAW,gCAAgC;IAC/C,2FAA2F;IAC3F,0BAA0B,CAAC,EAAE,OAAO,CAAA;CACrC;AAED,wBAAgB,yBAAyB,CACvC,KAAK,EAAE,KAAK,EACZ,OAAO,GAAE,gCAAqC,GAC7C,mBAAmB,EAAE,CAQvB"}
1
+ {"version":3,"file":"posthog-events.d.ts","sourceRoot":"","sources":["../../src/extensions/posthog-events.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,UAAU,CAAA;AA0BrC,MAAM,WAAW,mBAAmB;IAClC,WAAW,EAAE,MAAM,CAAA;IACnB,KAAK,EAAE,MAAM,CAAA;IACb,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IACnC,SAAS,EAAE,MAAM,CAAA;IACjB,IAAI,EAAE,SAAS,CAAA;CAChB;AAED,MAAM,WAAW,gCAAgC;IAC/C,2FAA2F;IAC3F,0BAA0B,CAAC,EAAE,OAAO,CAAA;CACrC;AAED,wBAAgB,yBAAyB,CACvC,KAAK,EAAE,KAAK,EACZ,OAAO,GAAE,gCAAqC,GAC7C,mBAAmB,EAAE,CAQvB"}
@@ -35,6 +35,7 @@ const external_event_types_js_namespaceObject = require("./event-types.js");
35
35
  const BUILT_IN_EVENT_NAME_BY_TYPE = {
36
36
  [external_event_types_js_namespaceObject.MCPAnalyticsEventType.custom]: external_constants_js_namespaceObject.PostHogMCPAnalyticsEvent.Custom,
37
37
  [external_event_types_js_namespaceObject.MCPAnalyticsEventType.identify]: external_constants_js_namespaceObject.PostHogMCPAnalyticsEvent.Identify,
38
+ [external_event_types_js_namespaceObject.MCPAnalyticsEventType.mcpFeedback]: external_constants_js_namespaceObject.PostHogMCPAnalyticsEvent.Feedback,
38
39
  [external_event_types_js_namespaceObject.MCPAnalyticsEventType.mcpMissingCapability]: external_constants_js_namespaceObject.PostHogMCPAnalyticsEvent.MissingCapability,
39
40
  [external_event_types_js_namespaceObject.MCPAnalyticsEventType.mcpInitialize]: external_constants_js_namespaceObject.PostHogMCPAnalyticsEvent.Initialize,
40
41
  [external_event_types_js_namespaceObject.MCPAnalyticsEventType.mcpPromptsGet]: external_constants_js_namespaceObject.PostHogMCPAnalyticsEvent.PromptGet,
@@ -3,6 +3,7 @@ import { MCPAnalyticsEventType } from "./event-types.mjs";
3
3
  const BUILT_IN_EVENT_NAME_BY_TYPE = {
4
4
  [MCPAnalyticsEventType.custom]: PostHogMCPAnalyticsEvent.Custom,
5
5
  [MCPAnalyticsEventType.identify]: PostHogMCPAnalyticsEvent.Identify,
6
+ [MCPAnalyticsEventType.mcpFeedback]: PostHogMCPAnalyticsEvent.Feedback,
6
7
  [MCPAnalyticsEventType.mcpMissingCapability]: PostHogMCPAnalyticsEvent.MissingCapability,
7
8
  [MCPAnalyticsEventType.mcpInitialize]: PostHogMCPAnalyticsEvent.Initialize,
8
9
  [MCPAnalyticsEventType.mcpPromptsGet]: PostHogMCPAnalyticsEvent.PromptGet,
@@ -1,5 +1,5 @@
1
1
  import { PostHog, type PostHogOptions } from 'posthog-node';
2
- import type { InitializeCaptureData, MCPAnalyticsOptions, MissingCapabilityCaptureData, PreparedToolCall, PrepareToolCallOptions, PrepareToolListOptions, ToolCallCaptureData, ToolsListCaptureData } from '../types';
2
+ import type { FeedbackCaptureData, CollectFeedbackConfig, InitializeCaptureData, MCPAnalyticsOptions, MissingCapabilityCaptureData, PreparedToolCall, PrepareToolCallOptions, PrepareToolListOptions, ToolCallCaptureData, ToolsListCaptureData } from '../types';
3
3
  import { type ContextInjectableTool } from './context-parameters';
4
4
  /**
5
5
  * Options for {@link PostHogMCP}. A superset of `posthog-node`'s options, plus
@@ -13,6 +13,17 @@ export interface PostHogMCPOptions extends PostHogOptions {
13
13
  * can't drift. Defaults to `get_more_tools`.
14
14
  */
15
15
  missingCapabilityToolName?: string;
16
+ /**
17
+ * Enable + configure the `send_feedback` virtual tool: injected by
18
+ * {@link PostHogMCP.prepareToolList} (when its `collectFeedback` toggle is on)
19
+ * and detected by {@link PostHogMCP.prepareToolCall}. Set once here so
20
+ * injection and detection can't drift — without this option, `prepareToolCall`
21
+ * never flags a call as feedback, so a real tool that uses the name is not
22
+ * shadowed. Pick a `toolName` no real tool uses. `onFeedback` is ignored on
23
+ * this path — the host dispatcher routes reports itself via
24
+ * {@link PreparedToolCall.feedbackReport}.
25
+ */
26
+ collectFeedback?: CollectFeedbackConfig;
16
27
  /**
17
28
  * Capture the calling model from recognized client metadata, with an injected
18
29
  * `llm_model` argument as fallback. Off by default.
@@ -73,8 +84,9 @@ export declare class PostHogMCP extends PostHog {
73
84
  * Decorate your `tools/list` response with PostHog's analytics affordances:
74
85
  * injects the `context` argument into every tool (so agents state their intent,
75
86
  * captured as `$mcp_intent`), injects `llm_model` when the constructor's
76
- * `captureModel` option is enabled, and appends `get_more_tools` when
77
- * `reportMissing` is on. Returns a new array; your tools are untouched.
87
+ * `captureModel` option is enabled, appends `get_more_tools` when
88
+ * `reportMissing` is on, and appends `send_feedback` when `collectFeedback` is
89
+ * on. Returns a new array; your tools are untouched.
78
90
  *
79
91
  * The appended `get_more_tools` descriptor carries only the base MCP tool fields
80
92
  * (name, description, input schema) — not any framework-specific fields your
@@ -128,5 +140,13 @@ export declare class PostHogMCP extends PostHog {
128
140
  * to the agent with `getMoreToolsResult()`.
129
141
  */
130
142
  captureMissingCapability(data: MissingCapabilityCaptureData): void;
143
+ /**
144
+ * Capture a `send_feedback` call as an agent-feedback report. Emits
145
+ * `$mcp_feedback` with the report's `$mcp_feedback_*` properties and its
146
+ * summary/details as `$mcp_intent`. Reply to the agent with
147
+ * `sendFeedbackResult()` (or a custom text) after routing the report to your
148
+ * own feedback backend.
149
+ */
150
+ captureFeedback(data: FeedbackCaptureData): void;
131
151
  }
132
152
  //# sourceMappingURL=posthog-mcp.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"posthog-mcp.d.ts","sourceRoot":"","sources":["../../src/extensions/posthog-mcp.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,KAAK,cAAc,EAAE,MAAM,cAAc,CAAA;AAE3D,OAAO,KAAK,EACV,qBAAqB,EAErB,mBAAmB,EAGnB,4BAA4B,EAC5B,gBAAgB,EAChB,sBAAsB,EACtB,sBAAsB,EACtB,mBAAmB,EACnB,oBAAoB,EACrB,MAAM,UAAU,CAAA;AAEjB,OAAO,EAIL,KAAK,qBAAqB,EAC3B,MAAM,sBAAsB,CAAA;AAgB7B;;;GAGG;AACH,MAAM,WAAW,iBAAkB,SAAQ,cAAc;IACvD;;;;;OAKG;IACH,yBAAyB,CAAC,EAAE,MAAM,CAAA;IAClC;;;OAGG;IACH,YAAY,CAAC,EAAE,mBAAmB,CAAC,cAAc,CAAC,CAAA;CACnD;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AACH,qBAAa,UAAW,SAAQ,OAAO;;gBASzB,MAAM,EAAE,MAAM,EAAE,OAAO,GAAE,iBAAsB;IAO3D,8FAA8F;IAC9F,eAAe,CAAC,IAAI,EAAE,mBAAmB,GAAG,IAAI;IAkBhD,iEAAiE;IACjE,iBAAiB,CAAC,IAAI,EAAE,qBAAqB,GAAG,IAAI;IAUpD;;;;;OAKG;IACH,gBAAgB,CAAC,IAAI,EAAE,oBAAoB,GAAG,IAAI;IAclD;;;;;;;;;;;;;;;;;;;;;OAqBG;IACH,eAAe,CAAC,KAAK,SAAS,qBAAqB,EAAE,KAAK,EAAE,KAAK,EAAE,EAAE,OAAO,GAAE,sBAA2B,GAAG,KAAK,EAAE;IAiCnH;;;;;;;;;;;;;;;;;;;;;;;;;;;;OA4BG;IACH,eAAe,CACb,IAAI,EAAE,MAAM,EACZ,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC9B,OAAO,GAAE,sBAA2B,GACnC,gBAAgB;IA4BnB;;;;OAIG;IACH,wBAAwB,CAAC,IAAI,EAAE,4BAA4B,GAAG,IAAI;CAmBnE"}
1
+ {"version":3,"file":"posthog-mcp.d.ts","sourceRoot":"","sources":["../../src/extensions/posthog-mcp.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,KAAK,cAAc,EAAE,MAAM,cAAc,CAAA;AAE3D,OAAO,KAAK,EACV,mBAAmB,EAEnB,qBAAqB,EACrB,qBAAqB,EAErB,mBAAmB,EAGnB,4BAA4B,EAC5B,gBAAgB,EAChB,sBAAsB,EACtB,sBAAsB,EACtB,mBAAmB,EACnB,oBAAoB,EACrB,MAAM,UAAU,CAAA;AAUjB,OAAO,EAIL,KAAK,qBAAqB,EAC3B,MAAM,sBAAsB,CAAA;AAgB7B;;;GAGG;AACH,MAAM,WAAW,iBAAkB,SAAQ,cAAc;IACvD;;;;;OAKG;IACH,yBAAyB,CAAC,EAAE,MAAM,CAAA;IAClC;;;;;;;;;OASG;IACH,eAAe,CAAC,EAAE,qBAAqB,CAAA;IACvC;;;OAGG;IACH,YAAY,CAAC,EAAE,mBAAmB,CAAC,cAAc,CAAC,CAAA;CACnD;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AACH,qBAAa,UAAW,SAAQ,OAAO;;gBAazB,MAAM,EAAE,MAAM,EAAE,OAAO,GAAE,iBAAsB;IAoB3D,8FAA8F;IAC9F,eAAe,CAAC,IAAI,EAAE,mBAAmB,GAAG,IAAI;IAkBhD,iEAAiE;IACjE,iBAAiB,CAAC,IAAI,EAAE,qBAAqB,GAAG,IAAI;IAUpD;;;;;OAKG;IACH,gBAAgB,CAAC,IAAI,EAAE,oBAAoB,GAAG,IAAI;IAclD;;;;;;;;;;;;;;;;;;;;;;OAsBG;IACH,eAAe,CAAC,KAAK,SAAS,qBAAqB,EAAE,KAAK,EAAE,KAAK,EAAE,EAAE,OAAO,GAAE,sBAA2B,GAAG,KAAK,EAAE;IAyCnH;;;;;;;;;;;;;;;;;;;;;;;;;;;;OA4BG;IACH,eAAe,CACb,IAAI,EAAE,MAAM,EACZ,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC9B,OAAO,GAAE,sBAA2B,GACnC,gBAAgB;IA+BnB;;;;OAIG;IACH,wBAAwB,CAAC,IAAI,EAAE,4BAA4B,GAAG,IAAI;IASlE;;;;;;OAMG;IACH,eAAe,CAAC,IAAI,EAAE,mBAAmB,GAAG,IAAI;CAuBjD"}