@posthog/mcp 0.21.2 → 0.21.4

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.
@@ -1 +1 @@
1
- {"version":3,"file":"feedback.d.ts","sourceRoot":"","sources":["../../src/extensions/feedback.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,sBAAsB,EAEtB,cAAc,EAGd,qBAAqB,EACrB,wBAAwB,EACxB,uBAAuB,EACvB,UAAU,EACX,MAAM,UAAU,CAAA;AAEjB,OAAO,EAAO,KAAK,QAAQ,EAAE,MAAM,UAAU,CAAA;AAG7C,eAAO,MAAM,uBAAuB,EAAG,eAAwB,CAAA;AAsF/D,KAAK,sBAAsB,GAAG,uBAAuB,CAAC,OAAO,CAAC,CAAC,MAAM,CAAC,CAAA;AAEtE,4FAA4F;AAC5F,wBAAgB,6BAA6B,CAC3C,MAAM,EAAE,qBAAqB,GAAG,SAAS,GACxC,sBAAsB,GAAG,SAAS,CAKpC;AAED;;;;;GAKG;AACH,wBAAgB,yBAAyB,CAAC,OAAO,GAAE,sBAA2B,GAAG,sBAAsB,CA6CtG;AAkCD;;;;;;GAMG;AACH,wBAAgB,mBAAmB,CACjC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,EACzC,OAAO,GAAE,sBAA2B,GACnC,cAAc,CAoBhB;AAED,iEAAiE;AACjE,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,cAAc,GAAG,MAAM,CAElE;AAuCD,uFAAuF;AACvF,wBAAgB,4BAA4B,CAAC,MAAM,EAAE,cAAc,GAAG,UAAU,CAqC/E;AAED;;;;;GAKG;AACH,wBAAgB,kBAAkB,IAAI,wBAAwB,CAS7D;AAED;;;;;GAKG;AACH,wBAAsB,cAAc,CAClC,MAAM,EAAE,cAAc,EACtB,OAAO,GAAE,sBAA2B,EACpC,MAAM,GAAE,QAAc,GACrB,OAAO,CAAC,wBAAwB,CAAC,CAiBnC"}
1
+ {"version":3,"file":"feedback.d.ts","sourceRoot":"","sources":["../../src/extensions/feedback.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,sBAAsB,EAEtB,cAAc,EAGd,qBAAqB,EACrB,wBAAwB,EACxB,uBAAuB,EACvB,UAAU,EACX,MAAM,UAAU,CAAA;AAEjB,OAAO,EAAO,KAAK,QAAQ,EAAE,MAAM,UAAU,CAAA;AAI7C,eAAO,MAAM,uBAAuB,EAAG,eAAwB,CAAA;AAqF/D,KAAK,sBAAsB,GAAG,uBAAuB,CAAC,OAAO,CAAC,CAAC,MAAM,CAAC,CAAA;AAEtE,4FAA4F;AAC5F,wBAAgB,6BAA6B,CAC3C,MAAM,EAAE,qBAAqB,GAAG,SAAS,GACxC,sBAAsB,GAAG,SAAS,CAKpC;AAED;;;;;GAKG;AACH,wBAAgB,yBAAyB,CAAC,OAAO,GAAE,sBAA2B,GAAG,sBAAsB,CA6CtG;AAkCD;;;;;;GAMG;AACH,wBAAgB,mBAAmB,CACjC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,EACzC,OAAO,GAAE,sBAA2B,GACnC,cAAc,CAoBhB;AAED,iEAAiE;AACjE,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,cAAc,GAAG,MAAM,CAElE;AAuCD,uFAAuF;AACvF,wBAAgB,4BAA4B,CAAC,MAAM,EAAE,cAAc,GAAG,UAAU,CAqC/E;AAED;;;;;GAKG;AACH,wBAAgB,kBAAkB,IAAI,wBAAwB,CAS7D;AAED;;;;;GAKG;AACH,wBAAsB,cAAc,CAClC,MAAM,EAAE,cAAc,EACtB,OAAO,GAAE,sBAA2B,EACpC,MAAM,GAAE,QAAc,GACrB,OAAO,CAAC,wBAAwB,CAAC,CAiBnC"}
@@ -40,6 +40,7 @@ __webpack_require__.d(__webpack_exports__, {
40
40
  const external_constants_js_namespaceObject = require("./constants.js");
41
41
  const external_logger_js_namespaceObject = require("./logger.js");
42
42
  const external_mcp_payloads_js_namespaceObject = require("./mcp-payloads.js");
43
+ const external_truncation_js_namespaceObject = require("./truncation.js");
43
44
  const SEND_FEEDBACK_TOOL_NAME = 'send_feedback';
44
45
  const FEEDBACK_TYPES = [
45
46
  'missing_capability',
@@ -55,7 +56,6 @@ const SENTIMENTS = [
55
56
  ];
56
57
  const MAX_FEEDBACK_TEXT_LENGTH = 2048;
57
58
  const MAX_FEEDBACK_TOOL_NAME_LENGTH = 256;
58
- const TRUNCATION_SUFFIX = '...';
59
59
  const DEFAULT_FEEDBACK_DESCRIPTION = 'Send feedback about this server to its developers. Most important: report a missing capability whenever no available tool fits your task, even if you can work around it (feedback_type "missing_capability"). Also welcome: a tool that failed or confused you, an unhelpful error, or something that worked well. This records the feedback; it does not add or change tools. Do not include user PII or sensitive content.';
60
60
  const CORE_FEEDBACK_SCHEMA_PROPERTIES = {
61
61
  feedback_type: {
@@ -179,7 +179,7 @@ function buildFeedbackIntent(report) {
179
179
  ].filter(Boolean).join('\n\n');
180
180
  }
181
181
  function truncateFeedbackText(value, maxLength) {
182
- return value.length > maxLength ? value.slice(0, maxLength) + TRUNCATION_SUFFIX : value;
182
+ return value.length > maxLength ? value.slice(0, maxLength) + external_truncation_js_namespaceObject.TRUNCATION_SUFFIX : value;
183
183
  }
184
184
  function captureFreeText(value) {
185
185
  return truncateFeedbackText((0, external_mcp_payloads_js_namespaceObject.sanitizeFreeText)(value), MAX_FEEDBACK_TEXT_LENGTH);
@@ -1,6 +1,7 @@
1
1
  import { PostHogMCPAnalyticsProperty } from "./constants.mjs";
2
2
  import { log } from "./logger.mjs";
3
3
  import { sanitizeFreeText, sanitizeFreeTextValue } from "./mcp-payloads.mjs";
4
+ import { TRUNCATION_SUFFIX } from "./truncation.mjs";
4
5
  const SEND_FEEDBACK_TOOL_NAME = 'send_feedback';
5
6
  const FEEDBACK_TYPES = [
6
7
  'missing_capability',
@@ -16,7 +17,6 @@ const SENTIMENTS = [
16
17
  ];
17
18
  const MAX_FEEDBACK_TEXT_LENGTH = 2048;
18
19
  const MAX_FEEDBACK_TOOL_NAME_LENGTH = 256;
19
- const TRUNCATION_SUFFIX = '...';
20
20
  const DEFAULT_FEEDBACK_DESCRIPTION = 'Send feedback about this server to its developers. Most important: report a missing capability whenever no available tool fits your task, even if you can work around it (feedback_type "missing_capability"). Also welcome: a tool that failed or confused you, an unhelpful error, or something that worked well. This records the feedback; it does not add or change tools. Do not include user PII or sensitive content.';
21
21
  const CORE_FEEDBACK_SCHEMA_PROPERTIES = {
22
22
  feedback_type: {
@@ -1 +1 @@
1
- {"version":3,"file":"mcp-payloads.d.ts","sourceRoot":"","sources":["../../src/extensions/mcp-payloads.ts"],"names":[],"mappings":"AAoHA,KAAK,UAAU,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;AAyYzC;;;;;;;;;;;GAWG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAKtD;AA6DD;;;;;;;;GAQG;AACH,wBAAgB,SAAS,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAU/C;AAED,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAE7D;AAED;;;;GAIG;AACH,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAE7D;AAyDD,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":"AA4HA,KAAK,UAAU,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;AA2azC;;;;;;;;;;;GAWG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAKtD;AA6DD;;;;;;;;GAQG;AACH,wBAAgB,SAAS,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAU/C;AAED,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAE7D;AAED;;;;GAIG;AACH,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAE7D;AAyDD,wBAAgB,0BAA0B,CAAC,OAAO,EAAE,OAAO,GAAG,UAAU,CAiBvE"}
@@ -27,6 +27,14 @@ var __webpack_require__ = {};
27
27
  })();
28
28
  var __webpack_exports__ = {};
29
29
  __webpack_require__.r(__webpack_exports__);
30
+ __webpack_require__.d(__webpack_exports__, {
31
+ buildCapturedMcpParameters: ()=>buildCapturedMcpParameters,
32
+ redactPii: ()=>redactPii,
33
+ sanitizeCapturedValue: ()=>sanitizeCapturedValue,
34
+ sanitizeFreeText: ()=>sanitizeFreeText,
35
+ sanitizeFreeTextValue: ()=>sanitizeFreeTextValue
36
+ });
37
+ const external_truncation_js_namespaceObject = require("./truncation.js");
30
38
  const CONTEXT_ARGUMENT_NAME = 'context';
31
39
  const REDACTED_VALUE = '[redacted]';
32
40
  const BINARY_REDACTED_VALUE = '[binary data redacted - not supported by PostHog MCP analytics]';
@@ -36,9 +44,12 @@ const BASE64URL_SPECIFIC_CHAR_PATTERN = /[-_]/;
36
44
  const BASE64_DATA_URL_PREFIX_PATTERN = /^data:[^,\s]*;base64,/i;
37
45
  const BASE64_DATA_URL_PAYLOAD_PATTERN = /^[A-Za-z0-9+/_-]+={0,2}$/;
38
46
  const SIZE_GATE = 10240;
47
+ const SCANNED_HEAD_LENGTH = 4 * external_truncation_js_namespaceObject.MAX_STRING_LENGTH;
48
+ const MIN_REDACTED_HEAD_LENGTH = 2 * external_truncation_js_namespaceObject.MAX_STRING_LENGTH;
39
49
  const POSTHOG_TOKEN_PATTERN = /\bph[a-z]_[A-Za-z0-9_-]{20,}\b/g;
40
50
  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
51
  const URL_PATTERN = /[a-z][a-z0-9+.-]{0,63}:[^\s<>"]+/gi;
52
+ const URL_END_PATTERN = /[\s<>"]/;
42
53
  const URL_PATTERN_ONCE = new RegExp(URL_PATTERN.source, 'i');
43
54
  const URL_AUTHORITY_SEARCH = /[a-z][a-z0-9+.-]{0,63}:\/\//i;
44
55
  const URL_AUTHORITY_PATTERN = new RegExp(`^${URL_AUTHORITY_SEARCH.source}`, 'i');
@@ -156,6 +167,7 @@ function sanitizeFragmentText(text, allowNestedUrls) {
156
167
  });
157
168
  }
158
169
  function findEmbeddedAuthorityIndexes(value) {
170
+ if (!value.includes('://')) return [];
159
171
  const starts = [
160
172
  ...value.matchAll(URL_AUTHORITY_SEARCH_ALL)
161
173
  ].map((match)=>match.index);
@@ -259,12 +271,24 @@ function redactUrls(value) {
259
271
  stripPunctuation: true
260
272
  });
261
273
  }
274
+ function redactCapturedHead(value, redact) {
275
+ if (value.length > SCANNED_HEAD_LENGTH) {
276
+ const head = redact(value.slice(0, headLength(value)));
277
+ if (head.length >= MIN_REDACTED_HEAD_LENGTH) return head + external_truncation_js_namespaceObject.TRUNCATION_SUFFIX;
278
+ }
279
+ return redact(value);
280
+ }
281
+ function headLength(value) {
282
+ let end = SCANNED_HEAD_LENGTH;
283
+ while(end > 0 && !URL_END_PATTERN.test(value[end - 1]))end--;
284
+ return end;
285
+ }
262
286
  function sanitizeString(value) {
263
- return isBinaryBlob(value) ? BINARY_REDACTED_VALUE : redactUrls(redactCredentials(value));
287
+ return isBinaryBlob(value) ? BINARY_REDACTED_VALUE : redactCapturedHead(value, (head)=>redactUrls(redactCredentials(head)));
264
288
  }
265
289
  function sanitizeFreeText(value) {
266
290
  if (isBinaryBlob(value)) return BINARY_REDACTED_VALUE;
267
- return redactUrls(redactPii(redactCredentials(value)));
291
+ return redactCapturedHead(value, (head)=>redactUrls(redactPii(redactCredentials(head))));
268
292
  }
269
293
  function passesLuhn(digits) {
270
294
  let sum = 0;
@@ -361,13 +385,6 @@ function buildCapturedMcpParameters(request) {
361
385
  request: capturedRequest
362
386
  };
363
387
  }
364
- __webpack_require__.d(__webpack_exports__, {
365
- buildCapturedMcpParameters: ()=>buildCapturedMcpParameters,
366
- redactPii: ()=>redactPii,
367
- sanitizeCapturedValue: ()=>sanitizeCapturedValue,
368
- sanitizeFreeText: ()=>sanitizeFreeText,
369
- sanitizeFreeTextValue: ()=>sanitizeFreeTextValue
370
- });
371
388
  exports.buildCapturedMcpParameters = __webpack_exports__.buildCapturedMcpParameters;
372
389
  exports.redactPii = __webpack_exports__.redactPii;
373
390
  exports.sanitizeCapturedValue = __webpack_exports__.sanitizeCapturedValue;
@@ -1,3 +1,4 @@
1
+ import { MAX_STRING_LENGTH, TRUNCATION_SUFFIX } from "./truncation.mjs";
1
2
  const CONTEXT_ARGUMENT_NAME = 'context';
2
3
  const REDACTED_VALUE = '[redacted]';
3
4
  const BINARY_REDACTED_VALUE = '[binary data redacted - not supported by PostHog MCP analytics]';
@@ -7,9 +8,12 @@ const BASE64URL_SPECIFIC_CHAR_PATTERN = /[-_]/;
7
8
  const BASE64_DATA_URL_PREFIX_PATTERN = /^data:[^,\s]*;base64,/i;
8
9
  const BASE64_DATA_URL_PAYLOAD_PATTERN = /^[A-Za-z0-9+/_-]+={0,2}$/;
9
10
  const SIZE_GATE = 10240;
11
+ const SCANNED_HEAD_LENGTH = 4 * MAX_STRING_LENGTH;
12
+ const MIN_REDACTED_HEAD_LENGTH = 2 * MAX_STRING_LENGTH;
10
13
  const POSTHOG_TOKEN_PATTERN = /\bph[a-z]_[A-Za-z0-9_-]{20,}\b/g;
11
14
  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
15
  const URL_PATTERN = /[a-z][a-z0-9+.-]{0,63}:[^\s<>"]+/gi;
16
+ const URL_END_PATTERN = /[\s<>"]/;
13
17
  const URL_PATTERN_ONCE = new RegExp(URL_PATTERN.source, 'i');
14
18
  const URL_AUTHORITY_SEARCH = /[a-z][a-z0-9+.-]{0,63}:\/\//i;
15
19
  const URL_AUTHORITY_PATTERN = new RegExp(`^${URL_AUTHORITY_SEARCH.source}`, 'i');
@@ -127,6 +131,7 @@ function sanitizeFragmentText(text, allowNestedUrls) {
127
131
  });
128
132
  }
129
133
  function findEmbeddedAuthorityIndexes(value) {
134
+ if (!value.includes('://')) return [];
130
135
  const starts = [
131
136
  ...value.matchAll(URL_AUTHORITY_SEARCH_ALL)
132
137
  ].map((match)=>match.index);
@@ -230,12 +235,24 @@ function redactUrls(value) {
230
235
  stripPunctuation: true
231
236
  });
232
237
  }
238
+ function redactCapturedHead(value, redact) {
239
+ if (value.length > SCANNED_HEAD_LENGTH) {
240
+ const head = redact(value.slice(0, headLength(value)));
241
+ if (head.length >= MIN_REDACTED_HEAD_LENGTH) return head + TRUNCATION_SUFFIX;
242
+ }
243
+ return redact(value);
244
+ }
245
+ function headLength(value) {
246
+ let end = SCANNED_HEAD_LENGTH;
247
+ while(end > 0 && !URL_END_PATTERN.test(value[end - 1]))end--;
248
+ return end;
249
+ }
233
250
  function sanitizeString(value) {
234
- return isBinaryBlob(value) ? BINARY_REDACTED_VALUE : redactUrls(redactCredentials(value));
251
+ return isBinaryBlob(value) ? BINARY_REDACTED_VALUE : redactCapturedHead(value, (head)=>redactUrls(redactCredentials(head)));
235
252
  }
236
253
  function sanitizeFreeText(value) {
237
254
  if (isBinaryBlob(value)) return BINARY_REDACTED_VALUE;
238
- return redactUrls(redactPii(redactCredentials(value)));
255
+ return redactCapturedHead(value, (head)=>redactUrls(redactPii(redactCredentials(head))));
239
256
  }
240
257
  function passesLuhn(digits) {
241
258
  let sum = 0;
@@ -3,6 +3,7 @@ export declare const MAX_DEPTH = 10;
3
3
  export declare const MAX_BREADTH = 100;
4
4
  export declare const MAX_STRING_LENGTH = 32768;
5
5
  export declare const MAX_EVENT_BYTES = 102400;
6
+ export declare const TRUNCATION_SUFFIX = "...";
6
7
  /**
7
8
  * Recursively normalizes a value, handling:
8
9
  * - String truncation (> MAX_STRING_LENGTH)
@@ -1 +1 @@
1
- {"version":3,"file":"truncation.d.ts","sourceRoot":"","sources":["../../src/extensions/truncation.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAmB,KAAK,EAAc,QAAQ,EAAE,MAAM,UAAU,CAAA;AAG5E,eAAO,MAAM,SAAS,KAAK,CAAA;AAC3B,eAAO,MAAM,WAAW,MAAM,CAAA;AAC9B,eAAO,MAAM,iBAAiB,QAAS,CAAA;AACvC,eAAO,MAAM,eAAe,SAAU,CAAA;AAgBtC;;;;;;;;GAQG;AACH,wBAAgB,SAAS,CACvB,KAAK,EAAE,OAAO,EACd,KAAK,GAAE,MAAkB,EACzB,UAAU,GAAE,MAAoB,EAChC,eAAe,GAAE,MAA0B,GAC1C,OAAO,CAGT;AAqXD;;;;;;;GAOG;AACH,wBAAgB,aAAa,CAAC,CAAC,SAAS,KAAK,GAAG,QAAQ,EAAE,KAAK,EAAE,CAAC,GAAG,CAAC,CA2CrE"}
1
+ {"version":3,"file":"truncation.d.ts","sourceRoot":"","sources":["../../src/extensions/truncation.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAmB,KAAK,EAAc,QAAQ,EAAE,MAAM,UAAU,CAAA;AAG5E,eAAO,MAAM,SAAS,KAAK,CAAA;AAC3B,eAAO,MAAM,WAAW,MAAM,CAAA;AAC9B,eAAO,MAAM,iBAAiB,QAAS,CAAA;AACvC,eAAO,MAAM,eAAe,SAAU,CAAA;AAWtC,eAAO,MAAM,iBAAiB,QAAQ,CAAA;AAKtC;;;;;;;;GAQG;AACH,wBAAgB,SAAS,CACvB,KAAK,EAAE,OAAO,EACd,KAAK,GAAE,MAAkB,EACzB,UAAU,GAAE,MAAoB,EAChC,eAAe,GAAE,MAA0B,GAC1C,OAAO,CAGT;AA+XD;;;;;;;GAOG;AACH,wBAAgB,aAAa,CAAC,CAAC,SAAS,KAAK,GAAG,QAAQ,EAAE,KAAK,EAAE,CAAC,GAAG,CAAC,CA2CrE"}
@@ -216,26 +216,38 @@ function setNestedValue(obj, path, value) {
216
216
  const finalKey = path.at(-1);
217
217
  if (void 0 !== finalKey && null != current && 'object' == typeof current) current[finalKey] = value;
218
218
  }
219
+ const REDUCIBLE_FIELDS = [
220
+ 'parameters',
221
+ 'response',
222
+ 'identifyActorData',
223
+ 'error'
224
+ ];
219
225
  function truncateToSize(event) {
220
226
  if (jsonByteSize(event) <= MAX_EVENT_BYTES) return event;
221
- for(let depth = MAX_DEPTH - 1; depth >= 1; depth--){
222
- const reduced = {
223
- ...event
224
- };
225
- if (null != reduced.parameters) reduced.parameters = normalize(reduced.parameters, depth);
226
- if (null != reduced.response) reduced.response = normalize(reduced.response, depth);
227
- if (null != reduced.identifyActorData) reduced.identifyActorData = normalize(reduced.identifyActorData, depth);
228
- if (null != reduced.error) reduced.error = normalize(reduced.error, depth);
227
+ const nesting = Math.max(...REDUCIBLE_FIELDS.map((field)=>nestingDepth(event[field])));
228
+ let atDepthOne;
229
+ for(let depth = nesting - 1; depth >= 1; depth--){
230
+ const reduced = normalizeReducibleFields(event, depth);
229
231
  if (jsonByteSize(reduced) <= MAX_EVENT_BYTES) return reduced;
232
+ if (1 === depth) atDepthOne = reduced;
233
+ }
234
+ return truncateLargestFields(atDepthOne ?? normalizeReducibleFields(event, 1), MAX_EVENT_BYTES);
235
+ }
236
+ function nestingDepth(value, limit = MAX_DEPTH) {
237
+ if (0 === limit || null === value || 'object' != typeof value) return 0;
238
+ let deepest = 0;
239
+ for (const item of Array.isArray(value) ? value : Object.values(value)){
240
+ deepest = Math.max(deepest, nestingDepth(item, limit - 1));
241
+ if (deepest === limit - 1) break;
230
242
  }
231
- const minimal = {
243
+ return deepest + 1;
244
+ }
245
+ function normalizeReducibleFields(event, depth) {
246
+ const reduced = {
232
247
  ...event
233
248
  };
234
- if (null != minimal.parameters) minimal.parameters = normalize(minimal.parameters, 1);
235
- if (null != minimal.response) minimal.response = normalize(minimal.response, 1);
236
- if (null != minimal.identifyActorData) minimal.identifyActorData = normalize(minimal.identifyActorData, 1);
237
- if (null != minimal.error) minimal.error = normalize(minimal.error, 1);
238
- return truncateLargestFields(minimal, MAX_EVENT_BYTES);
249
+ for (const field of REDUCIBLE_FIELDS)if (null != reduced[field]) reduced[field] = normalize(reduced[field], depth);
250
+ return reduced;
239
251
  }
240
252
  function truncateEvent(event) {
241
253
  const result = {
@@ -266,12 +278,14 @@ __webpack_require__.d(__webpack_exports__, {
266
278
  MAX_BREADTH: MAX_BREADTH,
267
279
  MAX_DEPTH: MAX_DEPTH,
268
280
  MAX_EVENT_BYTES: MAX_EVENT_BYTES,
269
- MAX_STRING_LENGTH: MAX_STRING_LENGTH
281
+ MAX_STRING_LENGTH: MAX_STRING_LENGTH,
282
+ TRUNCATION_SUFFIX: TRUNCATION_SUFFIX
270
283
  });
271
284
  exports.MAX_BREADTH = __webpack_exports__.MAX_BREADTH;
272
285
  exports.MAX_DEPTH = __webpack_exports__.MAX_DEPTH;
273
286
  exports.MAX_EVENT_BYTES = __webpack_exports__.MAX_EVENT_BYTES;
274
287
  exports.MAX_STRING_LENGTH = __webpack_exports__.MAX_STRING_LENGTH;
288
+ exports.TRUNCATION_SUFFIX = __webpack_exports__.TRUNCATION_SUFFIX;
275
289
  exports.normalize = __webpack_exports__.normalize;
276
290
  exports.truncateEvent = __webpack_exports__.truncateEvent;
277
291
  for(var __rspack_i in __webpack_exports__)if (-1 === [
@@ -279,6 +293,7 @@ for(var __rspack_i in __webpack_exports__)if (-1 === [
279
293
  "MAX_DEPTH",
280
294
  "MAX_EVENT_BYTES",
281
295
  "MAX_STRING_LENGTH",
296
+ "TRUNCATION_SUFFIX",
282
297
  "normalize",
283
298
  "truncateEvent"
284
299
  ].indexOf(__rspack_i)) exports[__rspack_i] = __webpack_exports__[__rspack_i];
@@ -187,26 +187,38 @@ function setNestedValue(obj, path, value) {
187
187
  const finalKey = path.at(-1);
188
188
  if (void 0 !== finalKey && null != current && 'object' == typeof current) current[finalKey] = value;
189
189
  }
190
+ const REDUCIBLE_FIELDS = [
191
+ 'parameters',
192
+ 'response',
193
+ 'identifyActorData',
194
+ 'error'
195
+ ];
190
196
  function truncateToSize(event) {
191
197
  if (jsonByteSize(event) <= MAX_EVENT_BYTES) return event;
192
- for(let depth = MAX_DEPTH - 1; depth >= 1; depth--){
193
- const reduced = {
194
- ...event
195
- };
196
- if (null != reduced.parameters) reduced.parameters = normalize(reduced.parameters, depth);
197
- if (null != reduced.response) reduced.response = normalize(reduced.response, depth);
198
- if (null != reduced.identifyActorData) reduced.identifyActorData = normalize(reduced.identifyActorData, depth);
199
- if (null != reduced.error) reduced.error = normalize(reduced.error, depth);
198
+ const nesting = Math.max(...REDUCIBLE_FIELDS.map((field)=>nestingDepth(event[field])));
199
+ let atDepthOne;
200
+ for(let depth = nesting - 1; depth >= 1; depth--){
201
+ const reduced = normalizeReducibleFields(event, depth);
200
202
  if (jsonByteSize(reduced) <= MAX_EVENT_BYTES) return reduced;
203
+ if (1 === depth) atDepthOne = reduced;
204
+ }
205
+ return truncateLargestFields(atDepthOne ?? normalizeReducibleFields(event, 1), MAX_EVENT_BYTES);
206
+ }
207
+ function nestingDepth(value, limit = MAX_DEPTH) {
208
+ if (0 === limit || null === value || 'object' != typeof value) return 0;
209
+ let deepest = 0;
210
+ for (const item of Array.isArray(value) ? value : Object.values(value)){
211
+ deepest = Math.max(deepest, nestingDepth(item, limit - 1));
212
+ if (deepest === limit - 1) break;
201
213
  }
202
- const minimal = {
214
+ return deepest + 1;
215
+ }
216
+ function normalizeReducibleFields(event, depth) {
217
+ const reduced = {
203
218
  ...event
204
219
  };
205
- if (null != minimal.parameters) minimal.parameters = normalize(minimal.parameters, 1);
206
- if (null != minimal.response) minimal.response = normalize(minimal.response, 1);
207
- if (null != minimal.identifyActorData) minimal.identifyActorData = normalize(minimal.identifyActorData, 1);
208
- if (null != minimal.error) minimal.error = normalize(minimal.error, 1);
209
- return truncateLargestFields(minimal, MAX_EVENT_BYTES);
220
+ for (const field of REDUCIBLE_FIELDS)if (null != reduced[field]) reduced[field] = normalize(reduced[field], depth);
221
+ return reduced;
210
222
  }
211
223
  function truncateEvent(event) {
212
224
  const result = {
@@ -230,4 +242,4 @@ function truncateEvent(event) {
230
242
  if (null != result.error) result.error = normalize(result.error);
231
243
  return truncateToSize(result);
232
244
  }
233
- export { MAX_BREADTH, MAX_DEPTH, MAX_EVENT_BYTES, MAX_STRING_LENGTH, normalize, truncateEvent };
245
+ export { MAX_BREADTH, MAX_DEPTH, MAX_EVENT_BYTES, MAX_STRING_LENGTH, TRUNCATION_SUFFIX, normalize, truncateEvent };
package/dist/version.d.ts CHANGED
@@ -1,2 +1,2 @@
1
- export declare const version = "0.21.2";
1
+ export declare const version = "0.21.4";
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.21.2";
30
+ const version = "0.21.4";
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.21.2";
1
+ const version = "0.21.4";
2
2
  export { version };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@posthog/mcp",
3
- "version": "0.21.2",
3
+ "version": "0.21.4",
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.55.2"
31
+ "@posthog/core": "^1.55.3"
32
32
  },
33
33
  "devDependencies": {
34
34
  "@babel/preset-env": "^7.27.1",
@@ -53,8 +53,8 @@
53
53
  "zod4": "npm:zod@^4.4.3",
54
54
  "@vitest/coverage-v8": "^1.6.1",
55
55
  "vitest": "^1.6.1",
56
- "@posthog-tooling/tsconfig-base": "1.1.1",
57
- "posthog-node": "^5.55.0"
56
+ "posthog-node": "^5.55.0",
57
+ "@posthog-tooling/tsconfig-base": "1.1.1"
58
58
  },
59
59
  "peerDependencies": {
60
60
  "@modelcontextprotocol/sdk": ">=1.26.0",
@@ -12,6 +12,7 @@ import type {
12
12
  import { PostHogMCPAnalyticsProperty } from './constants'
13
13
  import { log, type LoggerFn } from './logger'
14
14
  import { sanitizeFreeText, sanitizeFreeTextValue } from './mcp-payloads'
15
+ import { TRUNCATION_SUFFIX } from './truncation'
15
16
 
16
17
  export const SEND_FEEDBACK_TOOL_NAME = 'send_feedback' as const
17
18
 
@@ -21,7 +22,6 @@ const SENTIMENTS: readonly FeedbackSentiment[] = ['positive', 'neutral', 'negati
21
22
  // Free-text fields are agent-narrated, like `$mcp_intent`; bound them the same way.
22
23
  const MAX_FEEDBACK_TEXT_LENGTH = 2048
23
24
  const MAX_FEEDBACK_TOOL_NAME_LENGTH = 256
24
- const TRUNCATION_SUFFIX = '...'
25
25
 
26
26
  const DEFAULT_FEEDBACK_DESCRIPTION =
27
27
  'Send feedback about this server to its developers. Most important: report a missing capability whenever no ' +
@@ -3,6 +3,8 @@
3
3
  // Copyright (c) 2025 AgentCat, Inc. (formerly MCPcat)
4
4
  // Licensed under the MIT License: https://github.com/agentcathq/agentcat-typescript-sdk/blob/main/LICENSE
5
5
 
6
+ import { MAX_STRING_LENGTH, TRUNCATION_SUFFIX } from './truncation'
7
+
6
8
  const CONTEXT_ARGUMENT_NAME = 'context'
7
9
  const REDACTED_VALUE = '[redacted]'
8
10
  const BINARY_REDACTED_VALUE = '[binary data redacted - not supported by PostHog MCP analytics]'
@@ -12,6 +14,10 @@ const BASE64URL_SPECIFIC_CHAR_PATTERN = /[-_]/
12
14
  const BASE64_DATA_URL_PREFIX_PATTERN = /^data:[^,\s]*;base64,/i
13
15
  const BASE64_DATA_URL_PAYLOAD_PATTERN = /^[A-Za-z0-9+/_-]+={0,2}$/
14
16
  const SIZE_GATE = 10_240
17
+ const SCANNED_HEAD_LENGTH = 4 * MAX_STRING_LENGTH
18
+ // Twice what truncation keeps, so a match split where the head was cut sits past
19
+ // the part truncation keeps.
20
+ const MIN_REDACTED_HEAD_LENGTH = 2 * MAX_STRING_LENGTH
15
21
  const POSTHOG_TOKEN_PATTERN = /\bph[a-z]_[A-Za-z0-9_-]{20,}\b/g
16
22
  const SENSITIVE_KEY_PATTERN =
17
23
  /^(authorization|cookie|set-cookie|x-api-key|api[-_]?key|api[-_]?token|access[-_]?token|refresh[-_]?token|token|password|secret|client[-_]?secret|private[-_]?key)$/i
@@ -33,6 +39,8 @@ const SENSITIVE_KEY_PATTERN =
33
39
  // harmless: a match with nothing to redact is returned byte-for-byte, so the
34
40
  // text around it is never rewritten.
35
41
  const URL_PATTERN = /[a-z][a-z0-9+.-]{0,63}:[^\s<>"]+/gi
42
+ // A character no URL match can contain, so a cut just after one splits no URL.
43
+ const URL_END_PATTERN = /[\s<>"]/
36
44
  /** The same pattern without `g`, for asking whether a value holds a URL at all. */
37
45
  const URL_PATTERN_ONCE = new RegExp(URL_PATTERN.source, 'i')
38
46
  /** Finds the first scheme that brings a real authority, i.e. what the pattern required before it went authority-less. */
@@ -315,6 +323,11 @@ function sanitizeFragmentText(text: string, allowNestedUrls: boolean): string {
315
323
  * addresses together, because every one of them scanned back to the same `?`.
316
324
  */
317
325
  function findEmbeddedAuthorityIndexes(value: string): number[] {
326
+ // The search tries up to 64 scheme characters at every position, which a long
327
+ // base64 `data:` URI turns into seconds; without `://` it cannot match at all.
328
+ if (!value.includes('://')) {
329
+ return []
330
+ }
318
331
  const starts = [...value.matchAll(URL_AUTHORITY_SEARCH_ALL)].map((match) => match.index)
319
332
  const indexes: number[] = []
320
333
  const delimiters = { query: false, fragment: false, fragmentQuery: false }
@@ -503,8 +516,37 @@ function redactUrls(value: string): string {
503
516
  return sanitizeUrlsInString(value, { allowNestedUrls: true, stripPunctuation: true })
504
517
  }
505
518
 
519
+ /**
520
+ * Runs `redact` over only the head of `value` that truncation can keep, so a
521
+ * multi-megabyte tool result costs about the same as the part captured. The head
522
+ * ends where no URL crosses it, because a URL is redacted or kept on the whole
523
+ * address. Redaction shrinks text, so a head that ends up too short to cover
524
+ * truncation's cut falls back to the whole string. Either way the kept part
525
+ * reads exactly as if the whole string were redacted.
526
+ */
527
+ function redactCapturedHead(value: string, redact: (value: string) => string): string {
528
+ if (value.length > SCANNED_HEAD_LENGTH) {
529
+ const head = redact(value.slice(0, headLength(value)))
530
+ if (head.length >= MIN_REDACTED_HEAD_LENGTH) {
531
+ return head + TRUNCATION_SUFFIX
532
+ }
533
+ }
534
+ return redact(value)
535
+ }
536
+
537
+ /** The scanned head, shortened to end just after the last character that ends a URL. */
538
+ function headLength(value: string): number {
539
+ let end = SCANNED_HEAD_LENGTH
540
+ while (end > 0 && !URL_END_PATTERN.test(value[end - 1])) {
541
+ end--
542
+ }
543
+ return end
544
+ }
545
+
506
546
  function sanitizeString(value: string): string {
507
- return isBinaryBlob(value) ? BINARY_REDACTED_VALUE : redactUrls(redactCredentials(value))
547
+ return isBinaryBlob(value)
548
+ ? BINARY_REDACTED_VALUE
549
+ : redactCapturedHead(value, (head) => redactUrls(redactCredentials(head)))
508
550
  }
509
551
 
510
552
  /**
@@ -523,7 +565,7 @@ export function sanitizeFreeText(value: string): string {
523
565
  if (isBinaryBlob(value)) {
524
566
  return BINARY_REDACTED_VALUE
525
567
  }
526
- return redactUrls(redactPii(redactCredentials(value)))
568
+ return redactCapturedHead(value, (head) => redactUrls(redactPii(redactCredentials(head))))
527
569
  }
528
570
 
529
571
  function passesLuhn(digits: string): boolean {
@@ -20,7 +20,7 @@ const MAX_STACK_FRAMES = 50
20
20
  const MAX_CONTENT_TEXT_LENGTH = 32_768
21
21
 
22
22
  // --- Truncation markers ---
23
- const TRUNCATION_SUFFIX = '...'
23
+ export const TRUNCATION_SUFFIX = '...'
24
24
 
25
25
  type MutableEvent = Partial<Event | McpEvent> & Record<string, unknown>
26
26
  type MutableRecord = Record<string, unknown>
@@ -366,6 +366,8 @@ function setNestedValue(obj: unknown, path: string[], value: unknown): void {
366
366
  }
367
367
  }
368
368
 
369
+ const REDUCIBLE_FIELDS = ['parameters', 'response', 'identifyActorData', 'error'] as const
370
+
369
371
  /**
370
372
  * Ensures an event fits within MAX_EVENT_BYTES by progressively reducing
371
373
  * normalization depth, then truncating largest string fields as a last resort.
@@ -376,43 +378,51 @@ function truncateToSize(event: MutableEvent): MutableEvent {
376
378
  return event
377
379
  }
378
380
 
379
- // Progressive depth reduction
380
- for (let depth = MAX_DEPTH - 1; depth >= 1; depth--) {
381
- const reduced: MutableEvent = { ...event }
382
- if (reduced.parameters != null) {
383
- reduced.parameters = normalize(reduced.parameters, depth)
384
- }
385
- if (reduced.response != null) {
386
- reduced.response = normalize(reduced.response, depth)
387
- }
388
- if (reduced.identifyActorData != null) {
389
- reduced.identifyActorData = normalize(reduced.identifyActorData, depth) as Event['identifyActorData']
390
- }
391
- if (reduced.error != null) {
392
- reduced.error = normalize(reduced.error, depth) as Event['error']
393
- }
394
-
381
+ // Progressive depth reduction, from the first depth that collapses anything.
382
+ // Any deeper depth leaves the event as it is, and it is already too large.
383
+ const nesting = Math.max(...REDUCIBLE_FIELDS.map((field) => nestingDepth(event[field])))
384
+ let atDepthOne: MutableEvent | undefined
385
+ for (let depth = nesting - 1; depth >= 1; depth--) {
386
+ const reduced = normalizeReducibleFields(event, depth)
395
387
  if (jsonByteSize(reduced) <= MAX_EVENT_BYTES) {
396
388
  return reduced
397
389
  }
390
+ if (depth === 1) {
391
+ atDepthOne = reduced
392
+ }
398
393
  }
399
394
 
400
- // Last resort: truncate largest string fields
401
- const minimal: MutableEvent = { ...event }
402
- if (minimal.parameters != null) {
403
- minimal.parameters = normalize(minimal.parameters, 1)
404
- }
405
- if (minimal.response != null) {
406
- minimal.response = normalize(minimal.response, 1)
407
- }
408
- if (minimal.identifyActorData != null) {
409
- minimal.identifyActorData = normalize(minimal.identifyActorData, 1) as Event['identifyActorData']
410
- }
411
- if (minimal.error != null) {
412
- minimal.error = normalize(minimal.error, 1) as Event['error']
395
+ // Last resort: truncate largest string fields. Reuse the loop's depth-1
396
+ // result when it already computed one, instead of normalizing again.
397
+ return truncateLargestFields(atDepthOne ?? normalizeReducibleFields(event, 1), MAX_EVENT_BYTES)
398
+ }
399
+
400
+ /**
401
+ * The number of object and array levels in `value`, up to `limit`. `normalize`
402
+ * at this depth or deeper collapses nothing.
403
+ */
404
+ function nestingDepth(value: unknown, limit: number = MAX_DEPTH): number {
405
+ if (limit === 0 || value === null || typeof value !== 'object') {
406
+ return 0
407
+ }
408
+ let deepest = 0
409
+ for (const item of Array.isArray(value) ? value : Object.values(value)) {
410
+ deepest = Math.max(deepest, nestingDepth(item, limit - 1))
411
+ if (deepest === limit - 1) {
412
+ break
413
+ }
413
414
  }
415
+ return deepest + 1
416
+ }
414
417
 
415
- return truncateLargestFields(minimal, MAX_EVENT_BYTES)
418
+ function normalizeReducibleFields(event: MutableEvent, depth: number): MutableEvent {
419
+ const reduced: MutableEvent = { ...event }
420
+ for (const field of REDUCIBLE_FIELDS) {
421
+ if (reduced[field] != null) {
422
+ ;(reduced as MutableRecord)[field] = normalize(reduced[field], depth)
423
+ }
424
+ }
425
+ return reduced
416
426
  }
417
427
 
418
428
  /**
package/src/version.ts CHANGED
@@ -1 +1 @@
1
- export const version = "0.21.2"
1
+ export const version = "0.21.4"