@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.
- package/dist/extensions/feedback.d.ts.map +1 -1
- package/dist/extensions/feedback.js +2 -2
- package/dist/extensions/feedback.mjs +1 -1
- package/dist/extensions/mcp-payloads.d.ts.map +1 -1
- package/dist/extensions/mcp-payloads.js +26 -9
- package/dist/extensions/mcp-payloads.mjs +19 -2
- package/dist/extensions/truncation.d.ts +1 -0
- package/dist/extensions/truncation.d.ts.map +1 -1
- package/dist/extensions/truncation.js +30 -15
- package/dist/extensions/truncation.mjs +27 -15
- 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/feedback.ts +1 -1
- package/src/extensions/mcp-payloads.ts +44 -2
- package/src/extensions/truncation.ts +41 -31
- package/src/version.ts +1 -1
|
@@ -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;
|
|
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":"
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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;
|
|
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
|
-
|
|
222
|
-
|
|
223
|
-
|
|
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
|
-
|
|
243
|
+
return deepest + 1;
|
|
244
|
+
}
|
|
245
|
+
function normalizeReducibleFields(event, depth) {
|
|
246
|
+
const reduced = {
|
|
232
247
|
...event
|
|
233
248
|
};
|
|
234
|
-
if (null !=
|
|
235
|
-
|
|
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
|
-
|
|
193
|
-
|
|
194
|
-
|
|
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
|
-
|
|
214
|
+
return deepest + 1;
|
|
215
|
+
}
|
|
216
|
+
function normalizeReducibleFields(event, depth) {
|
|
217
|
+
const reduced = {
|
|
203
218
|
...event
|
|
204
219
|
};
|
|
205
|
-
if (null !=
|
|
206
|
-
|
|
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.
|
|
1
|
+
export declare const version = "0.21.4";
|
|
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.21.
|
|
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.
|
|
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.
|
|
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
|
-
"
|
|
57
|
-
"posthog-
|
|
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)
|
|
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(
|
|
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
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
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
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
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
|
-
|
|
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.
|
|
1
|
+
export const version = "0.21.4"
|