@posthog/mcp 0.12.0 → 0.13.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +30 -2
- package/dist/extensions/analytics-parameters.js +14 -10
- package/dist/extensions/capture.js +12 -8
- package/dist/extensions/client-identity.js +18 -14
- package/dist/extensions/compatibility.js +16 -12
- package/dist/extensions/constants.d.ts +1 -1
- package/dist/extensions/constants.d.ts.map +1 -1
- package/dist/extensions/constants.js +25 -21
- package/dist/extensions/constants.mjs +3 -3
- package/dist/extensions/context-parameters.js +13 -9
- package/dist/extensions/conversation-id.js +18 -14
- package/dist/extensions/detect.js +23 -19
- package/dist/extensions/event-types.js +15 -11
- package/dist/extensions/exceptions.js +12 -8
- package/dist/extensions/headers.js +16 -12
- package/dist/extensions/ids.js +12 -8
- package/dist/extensions/instrument-highlevel.js +12 -8
- package/dist/extensions/instrument-lowlevel.js +12 -8
- package/dist/extensions/instrumentation.js +19 -15
- package/dist/extensions/intent.js +14 -10
- package/dist/extensions/internal.js +17 -13
- package/dist/extensions/lib-identity.js +12 -8
- package/dist/extensions/logger.js +17 -13
- package/dist/extensions/mcp-payloads.d.ts +10 -0
- package/dist/extensions/mcp-payloads.d.ts.map +1 -1
- package/dist/extensions/mcp-payloads.js +80 -12
- package/dist/extensions/mcp-payloads.mjs +62 -1
- package/dist/extensions/mcp-sdk-compat.js +23 -19
- package/dist/extensions/model-parameters.d.ts.map +1 -1
- package/dist/extensions/model-parameters.js +21 -14
- package/dist/extensions/model-parameters.mjs +6 -3
- package/dist/extensions/output-instructions.js +15 -11
- package/dist/extensions/posthog-events.js +12 -8
- package/dist/extensions/posthog-mcp.d.ts +23 -12
- package/dist/extensions/posthog-mcp.d.ts.map +1 -1
- package/dist/extensions/posthog-mcp.js +49 -11
- package/dist/extensions/posthog-mcp.mjs +37 -3
- package/dist/extensions/request-headers.js +15 -11
- package/dist/extensions/sanitization.d.ts +2 -2
- package/dist/extensions/sanitization.d.ts.map +1 -1
- package/dist/extensions/sanitization.js +14 -9
- package/dist/extensions/sanitization.mjs +3 -2
- package/dist/extensions/session-token.js +17 -13
- package/dist/extensions/session-token.mjs +2 -2
- package/dist/extensions/session.js +16 -12
- package/dist/extensions/sink.js +14 -10
- package/dist/extensions/tools.js +15 -11
- package/dist/extensions/tracing-helpers.js +15 -11
- package/dist/extensions/transport-identity.js +15 -11
- package/dist/extensions/truncation.js +21 -16
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +22 -18
- package/dist/index.mjs +7 -7
- package/dist/types.d.ts +28 -4
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +2 -2
- package/dist/types.mjs +1 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +15 -11
- package/dist/version.mjs +1 -1
- package/package.json +12 -12
- package/src/extensions/constants.ts +1 -1
- package/src/extensions/mcp-payloads.ts +129 -0
- package/src/extensions/model-parameters.ts +7 -3
- package/src/extensions/posthog-mcp.ts +84 -12
- package/src/extensions/sanitization.ts +14 -7
- package/src/index.ts +1 -0
- package/src/types.ts +27 -4
- package/src/version.ts +1 -1
|
@@ -16,6 +16,55 @@ const POSTHOG_TOKEN_PATTERN = /\bph[a-z]_[A-Za-z0-9_-]{20,}\b/g
|
|
|
16
16
|
const SENSITIVE_KEY_PATTERN =
|
|
17
17
|
/^(authorization|cookie|set-cookie|x-api-key|api[-_]?key|api[-_]?token|access[-_]?token|refresh[-_]?token|token|password|secret|client[-_]?secret|private[-_]?key)$/i
|
|
18
18
|
|
|
19
|
+
// PII redaction for the agent-narrated intent string only. `$mcp_intent` is free
|
|
20
|
+
// text the calling LLM writes into the injected `context` argument, so it can
|
|
21
|
+
// carry personal data the model read aloud despite being told not to. We redact
|
|
22
|
+
// well-defined *structured identifiers* — the kind regex can match with high
|
|
23
|
+
// precision. Person names and postal addresses are deliberately out of scope:
|
|
24
|
+
// they need an NER model that a client SDK cannot ship, and naive patterns would
|
|
25
|
+
// over-redact ordinary prose. Patterns are ordered so an earlier pass never eats
|
|
26
|
+
// digits a later pass needs (email before phone, IPs before phone, cards before
|
|
27
|
+
// the generic phone pass). See `redactPii`.
|
|
28
|
+
// Horizontal Unicode spaces (NBSP, narrow NBSP, ideographic space, ...) are what
|
|
29
|
+
// appear when text is copied from web pages or PDFs. `redactPii` normalizes them
|
|
30
|
+
// to an ASCII space first so the separator-based card/phone/SSN candidates match
|
|
31
|
+
// them instead of leaking the identifier they group.
|
|
32
|
+
const UNICODE_SPACE_PATTERN = /[\u00A0\u1680\u2000-\u200A\u202F\u205F\u3000]/g
|
|
33
|
+
// Quantifiers are bounded to RFC-ish limits (local-part <=64, domain <=255,
|
|
34
|
+
// TLD <=24) rather than open-ended `+`. Unbounded `+` here is quadratic: on a
|
|
35
|
+
// long run of local-part chars with no valid `.tld`, `replace` rescans from
|
|
36
|
+
// every start position. `$mcp_intent` is attacker-influenceable free text seen
|
|
37
|
+
// before truncation, so an open-ended pattern is a reachable event-loop stall.
|
|
38
|
+
const EMAIL_PATTERN = /[A-Za-z0-9._%+-]{1,64}@[A-Za-z0-9.-]{1,255}\.[A-Za-z]{2,24}/g
|
|
39
|
+
const IPV4_PATTERN = /\b(?:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)\.){3}(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)\b/g
|
|
40
|
+
// Four forms: full 8-group, `::`-terminated (`2001:db8::`), a middle `::`
|
|
41
|
+
// (`2001:db8::8a2e:1`), and a leading `::` (`::1`). The compressed branches use a
|
|
42
|
+
// `(?<![\w:])` boundary so a hex-looking C++ scope like `std::bad` — whose left
|
|
43
|
+
// side is not a valid hex group — is not mistaken for an address, while a
|
|
44
|
+
// genuinely address-shaped `dead::beef` still matches.
|
|
45
|
+
const IPV6_PATTERN =
|
|
46
|
+
/\b(?:[0-9A-Fa-f]{1,4}:){7}[0-9A-Fa-f]{1,4}\b|(?<![\w:])(?:[0-9A-Fa-f]{1,4}:){1,7}:(?![\w:])|(?<![\w:])(?:[0-9A-Fa-f]{1,4}:){1,6}:[0-9A-Fa-f]{1,4}(?::[0-9A-Fa-f]{1,4}){0,5}(?![\w])|(?<![\w:])::(?:[0-9A-Fa-f]{1,4}(?::[0-9A-Fa-f]{1,4}){0,6})(?![\w])/g
|
|
47
|
+
// A separator (space, dot, or dash) is required between the 3-2-4 groups so bare
|
|
48
|
+
// 9-digit IDs are never mistaken for an SSN.
|
|
49
|
+
const US_SSN_PATTERN = /\b\d{3}[ .-]\d{2}[ .-]\d{4}\b/g
|
|
50
|
+
// A run of >=13 digits optionally grouped by a single space, dot, dash, or
|
|
51
|
+
// slash. This only marks the numeric region; `redactCardInMatch` then looks for
|
|
52
|
+
// the actual card as a run of whole separator-delimited groups that passes Luhn,
|
|
53
|
+
// so an adjacent field such as an expiry (`4111 1111 1111 1111 12/30`) is not
|
|
54
|
+
// absorbed into a failing check that would leak the card.
|
|
55
|
+
const CREDIT_CARD_CANDIDATE_PATTERN = /\b\d(?:[ ./-]?\d){12,}\b/g
|
|
56
|
+
// Matches each separator-delimited digit group inside a card candidate.
|
|
57
|
+
const DIGIT_GROUP_PATTERN = /\d+/g
|
|
58
|
+
// Phone matching is structural rather than "any 10–15 digits", so dates
|
|
59
|
+
// (`2024-01-15 12:30`) and dotted versions are not mistaken for numbers. Two
|
|
60
|
+
// forms: a North-American 3-3-4 grouping, and an international number that must
|
|
61
|
+
// start with `+` and a country code. The area code is either `(415)` (the
|
|
62
|
+
// separator after it is optional, so `(415)555-0142` matches) or a bare `415`
|
|
63
|
+
// that must be followed by a separator (space, dot, dash, or slash) — so a bare
|
|
64
|
+
// digit run is never taken for a phone number.
|
|
65
|
+
const PHONE_NANP_PATTERN = /(?<![\w+])(?:\+?1[ ./-]?)?(?:\(\d{3}\)[ ./-]?|\d{3}[ ./-])\d{3}[ ./-]\d{4}(?![\w])/g
|
|
66
|
+
const PHONE_INTL_PATTERN = /(?<!\w)\+\d{1,3}(?:[ ./()-]{0,2}\d){7,13}(?![\w])/g
|
|
67
|
+
|
|
19
68
|
type JsonRecord = Record<string, unknown>
|
|
20
69
|
|
|
21
70
|
function isRecord(value: unknown): value is JsonRecord {
|
|
@@ -54,6 +103,86 @@ function sanitizeString(value: string): string {
|
|
|
54
103
|
return value.replace(POSTHOG_TOKEN_PATTERN, REDACTED_VALUE)
|
|
55
104
|
}
|
|
56
105
|
|
|
106
|
+
function passesLuhn(digits: string): boolean {
|
|
107
|
+
let sum = 0
|
|
108
|
+
let double = false
|
|
109
|
+
for (let index = digits.length - 1; index >= 0; index--) {
|
|
110
|
+
let digit = digits.charCodeAt(index) - 48
|
|
111
|
+
if (digit < 0 || digit > 9) {
|
|
112
|
+
return false
|
|
113
|
+
}
|
|
114
|
+
if (double) {
|
|
115
|
+
digit *= 2
|
|
116
|
+
if (digit > 9) {
|
|
117
|
+
digit -= 9
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
sum += digit
|
|
121
|
+
double = !double
|
|
122
|
+
}
|
|
123
|
+
return sum % 10 === 0
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// A card candidate can span more than one card plus adjacent fields (e.g. an
|
|
127
|
+
// expiry, or a second card). This redacts every card in it: scanning
|
|
128
|
+
// left-to-right over the whole separator-delimited digit groups, at each start it
|
|
129
|
+
// takes the longest run whose joined digits are 13–19 long and pass Luhn, redacts
|
|
130
|
+
// just that span, and resumes after it; a group that begins no valid run is kept
|
|
131
|
+
// and skipped. Checking group-aligned runs rather than arbitrary digit windows
|
|
132
|
+
// keeps the false-positive rate at Luhn's own ~1-in-10, instead of letting a
|
|
133
|
+
// chance-valid sub-window of an ordinary long ID trigger redaction.
|
|
134
|
+
function redactCardInMatch(match: string): string {
|
|
135
|
+
const groups: { digits: string; start: number; end: number }[] = []
|
|
136
|
+
for (let m = DIGIT_GROUP_PATTERN.exec(match); m !== null; m = DIGIT_GROUP_PATTERN.exec(match)) {
|
|
137
|
+
groups.push({ digits: m[0], start: m.index, end: m.index + m[0].length })
|
|
138
|
+
}
|
|
139
|
+
let output = ''
|
|
140
|
+
let cursor = 0
|
|
141
|
+
let first = 0
|
|
142
|
+
while (first < groups.length) {
|
|
143
|
+
let digits = ''
|
|
144
|
+
let matchedLast = -1
|
|
145
|
+
for (let last = first; last < groups.length; last++) {
|
|
146
|
+
digits += groups[last].digits
|
|
147
|
+
if (digits.length > 19) {
|
|
148
|
+
break
|
|
149
|
+
}
|
|
150
|
+
if (digits.length >= 13 && passesLuhn(digits)) {
|
|
151
|
+
matchedLast = last
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
if (matchedLast >= 0) {
|
|
155
|
+
output += match.slice(cursor, groups[first].start) + REDACTED_VALUE
|
|
156
|
+
cursor = groups[matchedLast].end
|
|
157
|
+
first = matchedLast + 1
|
|
158
|
+
} else {
|
|
159
|
+
first++
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
return output + match.slice(cursor)
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Redacts structured personal identifiers (emails, IP addresses, credit-card
|
|
167
|
+
* numbers, US SSNs, and phone numbers) from a free-text string. Intended for the
|
|
168
|
+
* agent-narrated `$mcp_intent` value only — not for structured tool parameters or
|
|
169
|
+
* responses, where the same shapes are often legitimate data. Horizontal Unicode
|
|
170
|
+
* spaces are first normalized to an ASCII space so copy-pasted identifiers still
|
|
171
|
+
* match. Returns a new string; leaves the input's identifiers untouched when
|
|
172
|
+
* nothing matches.
|
|
173
|
+
*/
|
|
174
|
+
export function redactPii(value: string): string {
|
|
175
|
+
let result = value.replace(UNICODE_SPACE_PATTERN, ' ')
|
|
176
|
+
result = result.replace(EMAIL_PATTERN, REDACTED_VALUE)
|
|
177
|
+
result = result.replace(IPV4_PATTERN, REDACTED_VALUE)
|
|
178
|
+
result = result.replace(IPV6_PATTERN, REDACTED_VALUE)
|
|
179
|
+
result = result.replace(CREDIT_CARD_CANDIDATE_PATTERN, redactCardInMatch)
|
|
180
|
+
result = result.replace(US_SSN_PATTERN, REDACTED_VALUE)
|
|
181
|
+
result = result.replace(PHONE_NANP_PATTERN, REDACTED_VALUE)
|
|
182
|
+
result = result.replace(PHONE_INTL_PATTERN, REDACTED_VALUE)
|
|
183
|
+
return result
|
|
184
|
+
}
|
|
185
|
+
|
|
57
186
|
export function sanitizeCapturedValue(value: unknown): unknown {
|
|
58
187
|
if (value == null) {
|
|
59
188
|
return value
|
|
@@ -74,7 +74,10 @@ export function addModelParameterToTools<TTool extends ModelInjectableTool>(
|
|
|
74
74
|
* not become a property value queries would group by.
|
|
75
75
|
*/
|
|
76
76
|
export function getModelArgument(request: MCPRequestLike): string | undefined {
|
|
77
|
-
|
|
77
|
+
return normalizeModel(request.params?.arguments?.llm_model)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function normalizeModel(model: unknown): string | undefined {
|
|
78
81
|
if (typeof model !== 'string') {
|
|
79
82
|
return undefined
|
|
80
83
|
}
|
|
@@ -86,9 +89,10 @@ export function getModelArgument(request: MCPRequestLike): string | undefined {
|
|
|
86
89
|
}
|
|
87
90
|
|
|
88
91
|
export function setEventModel(event: McpEvent, model: string | undefined): void {
|
|
89
|
-
|
|
92
|
+
const normalizedModel = normalizeModel(model)
|
|
93
|
+
if (!normalizedModel) {
|
|
90
94
|
return
|
|
91
95
|
}
|
|
92
|
-
event.llmModel =
|
|
96
|
+
event.llmModel = normalizedModel
|
|
93
97
|
event.llmModelSource = 'self_reported'
|
|
94
98
|
}
|
|
@@ -3,14 +3,17 @@ import { PostHog, type PostHogOptions } from 'posthog-node'
|
|
|
3
3
|
import type {
|
|
4
4
|
InitializeCaptureData,
|
|
5
5
|
JsonRecord,
|
|
6
|
+
MCPAnalyticsOptions,
|
|
6
7
|
McpCaptureCommon,
|
|
7
8
|
McpEvent,
|
|
8
9
|
MissingCapabilityCaptureData,
|
|
9
10
|
PreparedToolCall,
|
|
11
|
+
PrepareToolCallOptions,
|
|
10
12
|
PrepareToolListOptions,
|
|
11
13
|
ToolCallCaptureData,
|
|
12
14
|
ToolsListCaptureData,
|
|
13
15
|
} from '../types'
|
|
16
|
+
import { analyticsOwnsParameter, stripOwnedAnalyticsArguments } from './analytics-parameters'
|
|
14
17
|
import {
|
|
15
18
|
addContextParameterToTools,
|
|
16
19
|
getContextDescription,
|
|
@@ -22,6 +25,13 @@ import { captureException } from './exceptions'
|
|
|
22
25
|
import { normalizeHeaderString } from './headers'
|
|
23
26
|
import { applyMcpLibIdentity } from './lib-identity'
|
|
24
27
|
import { log } from './logger'
|
|
28
|
+
import {
|
|
29
|
+
addModelParameterToTool,
|
|
30
|
+
getModelArgument,
|
|
31
|
+
getModelDescription,
|
|
32
|
+
isCaptureModelEnabled,
|
|
33
|
+
setEventModel,
|
|
34
|
+
} from './model-parameters'
|
|
25
35
|
import { McpEventSink } from './sink'
|
|
26
36
|
import { GET_MORE_TOOLS_NAME, getReportMissingToolDescriptor } from './tools'
|
|
27
37
|
|
|
@@ -37,6 +47,11 @@ export interface PostHogMCPOptions extends PostHogOptions {
|
|
|
37
47
|
* can't drift. Defaults to `get_more_tools`.
|
|
38
48
|
*/
|
|
39
49
|
missingCapabilityToolName?: string
|
|
50
|
+
/**
|
|
51
|
+
* Inject a required `llm_model` argument into advertised tools and capture
|
|
52
|
+
* the agent's self-reported value. Off by default.
|
|
53
|
+
*/
|
|
54
|
+
captureModel?: MCPAnalyticsOptions['captureModel']
|
|
40
55
|
}
|
|
41
56
|
|
|
42
57
|
/**
|
|
@@ -57,7 +72,10 @@ export interface PostHogMCPOptions extends PostHogOptions {
|
|
|
57
72
|
* ```ts
|
|
58
73
|
* import { PostHogMCP } from "@posthog/mcp"
|
|
59
74
|
*
|
|
60
|
-
* const posthog = new PostHogMCP("phc_your_project_token", {
|
|
75
|
+
* const posthog = new PostHogMCP("phc_your_project_token", {
|
|
76
|
+
* host: "https://us.i.posthog.com",
|
|
77
|
+
* captureModel: true,
|
|
78
|
+
* })
|
|
61
79
|
*
|
|
62
80
|
* posthog.captureToolCall({
|
|
63
81
|
* toolName: "search_docs",
|
|
@@ -78,10 +96,13 @@ export class PostHogMCP extends PostHog {
|
|
|
78
96
|
// The get_more_tools name lives here (not on the per-call options) so that
|
|
79
97
|
// prepareToolList (inject) and prepareToolCall (detect) always agree.
|
|
80
98
|
readonly #missingCapabilityToolName: string
|
|
99
|
+
readonly #captureModel: MCPAnalyticsOptions['captureModel']
|
|
100
|
+
readonly #modelParameterOwnership = new Map<string, boolean>()
|
|
81
101
|
|
|
82
102
|
constructor(apiKey: string, options: PostHogMCPOptions = {}) {
|
|
83
103
|
super(apiKey, options)
|
|
84
104
|
this.#missingCapabilityToolName = options.missingCapabilityToolName ?? GET_MORE_TOOLS_NAME
|
|
105
|
+
this.#captureModel = options.captureModel
|
|
85
106
|
applyMcpLibIdentity(this)
|
|
86
107
|
}
|
|
87
108
|
|
|
@@ -97,6 +118,10 @@ export class PostHogMCP extends PostHog {
|
|
|
97
118
|
event.isError = data.isError
|
|
98
119
|
event.errorType = data.errorType
|
|
99
120
|
applyIntent(event, data.intent, data.intentSource)
|
|
121
|
+
setEventModel(event, data.llmModel)
|
|
122
|
+
if (event.llmModel && data.llmModelSource) {
|
|
123
|
+
event.llmModelSource = data.llmModelSource
|
|
124
|
+
}
|
|
100
125
|
if (data.isError) {
|
|
101
126
|
event.error = captureException(data.error ?? `Tool ${data.toolName} returned an error`)
|
|
102
127
|
}
|
|
@@ -137,9 +162,9 @@ export class PostHogMCP extends PostHog {
|
|
|
137
162
|
/**
|
|
138
163
|
* Decorate your `tools/list` response with PostHog's analytics affordances:
|
|
139
164
|
* injects the `context` argument into every tool (so agents state their intent,
|
|
140
|
-
* captured as `$mcp_intent`)
|
|
141
|
-
* `
|
|
142
|
-
*
|
|
165
|
+
* captured as `$mcp_intent`), injects `llm_model` when the constructor's
|
|
166
|
+
* `captureModel` option is enabled, and appends `get_more_tools` when
|
|
167
|
+
* `reportMissing` is on. Returns a new array; your tools are untouched.
|
|
143
168
|
*
|
|
144
169
|
* The appended `get_more_tools` descriptor carries only the base MCP tool fields
|
|
145
170
|
* (name, description, input schema) — not any framework-specific fields your
|
|
@@ -165,18 +190,41 @@ export class PostHogMCP extends PostHog {
|
|
|
165
190
|
if (options.reportMissing && !prepared.some((tool) => tool?.name === this.#missingCapabilityToolName)) {
|
|
166
191
|
prepared = [...prepared, getReportMissingToolDescriptor(this.#missingCapabilityToolName) as TTool]
|
|
167
192
|
}
|
|
193
|
+
|
|
194
|
+
if (isCaptureModelEnabled(this.#captureModel)) {
|
|
195
|
+
this.#modelParameterOwnership.clear()
|
|
196
|
+
const ownershipByName = new Map<string, boolean>()
|
|
197
|
+
for (const tool of prepared) {
|
|
198
|
+
if (typeof tool.name !== 'string') {
|
|
199
|
+
continue
|
|
200
|
+
}
|
|
201
|
+
const ownsModel = analyticsOwnsParameter(tool.inputSchema, 'llm_model')
|
|
202
|
+
ownershipByName.set(tool.name, (ownershipByName.get(tool.name) ?? true) && ownsModel)
|
|
203
|
+
}
|
|
204
|
+
for (const [toolName, ownsModel] of ownershipByName) {
|
|
205
|
+
this.#modelParameterOwnership.set(toolName, ownsModel)
|
|
206
|
+
}
|
|
207
|
+
const modelDescription = getModelDescription(this.#captureModel)
|
|
208
|
+
prepared = prepared.map((tool) =>
|
|
209
|
+
typeof tool.name === 'string' && ownershipByName.get(tool.name) === false
|
|
210
|
+
? tool
|
|
211
|
+
: addModelParameterToTool(tool, modelDescription)
|
|
212
|
+
)
|
|
213
|
+
}
|
|
168
214
|
return prepared
|
|
169
215
|
}
|
|
170
216
|
|
|
171
217
|
/**
|
|
172
|
-
* Read an incoming `tools/call` before you dispatch it: pulls
|
|
173
|
-
*
|
|
174
|
-
* arguments (so your handler and its schema validation never see it), and flags
|
|
218
|
+
* Read an incoming `tools/call` before you dispatch it: pulls analytics values
|
|
219
|
+
* from SDK-owned arguments, strips those arguments before validation, and flags
|
|
175
220
|
* whether the call targeted the `get_more_tools` virtual tool.
|
|
176
221
|
*
|
|
177
|
-
* Pass the returned
|
|
222
|
+
* Pass the returned intent and model fields to {@link captureToolCall}, and
|
|
178
223
|
* dispatch the returned `args` to your tool.
|
|
179
224
|
*
|
|
225
|
+
* On stateless or multi-replica servers, pass the original tool descriptor
|
|
226
|
+
* so ownership does not depend on which process served `tools/list`.
|
|
227
|
+
*
|
|
180
228
|
* This only extracts the explicit `context` argument (`intentSource:
|
|
181
229
|
* 'context_parameter'`); it does not infer intent. If you run your own
|
|
182
230
|
* inference, pass that string with `intentSource: 'inferred'` straight to
|
|
@@ -185,22 +233,42 @@ export class PostHogMCP extends PostHog {
|
|
|
185
233
|
*
|
|
186
234
|
* @example
|
|
187
235
|
* ```ts
|
|
188
|
-
* const { intent, intentSource, args, isMissingCapability } =
|
|
236
|
+
* const { intent, intentSource, llmModel, llmModelSource, args, isMissingCapability } =
|
|
237
|
+
* posthog.prepareToolCall(name, rawArgs)
|
|
189
238
|
* if (isMissingCapability) {
|
|
190
|
-
* posthog.captureMissingCapability({ context: intent, ...identity })
|
|
239
|
+
* posthog.captureMissingCapability({ context: intent, llmModel, llmModelSource, ...identity })
|
|
191
240
|
* return getMoreToolsResult()
|
|
192
241
|
* }
|
|
193
242
|
* const result = await runTool(name, args)
|
|
194
243
|
* posthog.captureToolCall({ toolName: name, intent, intentSource, ...identity })
|
|
195
244
|
* ```
|
|
196
245
|
*/
|
|
197
|
-
prepareToolCall(
|
|
246
|
+
prepareToolCall(
|
|
247
|
+
name: string,
|
|
248
|
+
args?: Record<string, unknown>,
|
|
249
|
+
options: PrepareToolCallOptions = {}
|
|
250
|
+
): PreparedToolCall {
|
|
198
251
|
const rawContext = args?.context
|
|
199
252
|
const intent = typeof rawContext === 'string' && rawContext.trim() ? rawContext.trim() : undefined
|
|
253
|
+
const ownsModel =
|
|
254
|
+
isCaptureModelEnabled(this.#captureModel) &&
|
|
255
|
+
(options.originalTool
|
|
256
|
+
? analyticsOwnsParameter(options.originalTool.inputSchema, 'llm_model')
|
|
257
|
+
: this.#modelParameterOwnership.get(name) === true)
|
|
258
|
+
const llmModel = ownsModel ? getModelArgument({ params: { arguments: args } }) : undefined
|
|
259
|
+
const strippedArgs = stripContext(args)
|
|
200
260
|
return {
|
|
201
261
|
intent,
|
|
202
262
|
intentSource: intent ? 'context_parameter' : undefined,
|
|
203
|
-
|
|
263
|
+
llmModel,
|
|
264
|
+
llmModelSource: llmModel ? 'self_reported' : undefined,
|
|
265
|
+
args: ownsModel
|
|
266
|
+
? (stripOwnedAnalyticsArguments(strippedArgs, {
|
|
267
|
+
context: false,
|
|
268
|
+
conversationId: false,
|
|
269
|
+
llmModel: true,
|
|
270
|
+
}) as Record<string, unknown> | undefined)
|
|
271
|
+
: strippedArgs,
|
|
204
272
|
isMissingCapability: name === this.#missingCapabilityToolName,
|
|
205
273
|
}
|
|
206
274
|
}
|
|
@@ -215,6 +283,10 @@ export class PostHogMCP extends PostHog {
|
|
|
215
283
|
event.resourceName = this.#missingCapabilityToolName
|
|
216
284
|
event.parameters = data.parameters
|
|
217
285
|
applyIntent(event, data.context, 'context_parameter')
|
|
286
|
+
setEventModel(event, data.llmModel)
|
|
287
|
+
if (event.llmModel && data.llmModelSource) {
|
|
288
|
+
event.llmModelSource = data.llmModelSource
|
|
289
|
+
}
|
|
218
290
|
this.#emit(event)
|
|
219
291
|
}
|
|
220
292
|
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
// Licensed under the MIT License: https://github.com/agentcathq/agentcat-typescript-sdk/blob/main/LICENSE
|
|
5
5
|
|
|
6
6
|
import type { ErrorProperties, Event, McpEvent } from '../types'
|
|
7
|
-
import { sanitizeCapturedValue } from './mcp-payloads'
|
|
7
|
+
import { redactPii, sanitizeCapturedValue } from './mcp-payloads'
|
|
8
8
|
|
|
9
9
|
type SanitizedRecord = Record<string, unknown>
|
|
10
10
|
|
|
@@ -15,8 +15,8 @@ function isRecord(value: unknown): value is SanitizedRecord {
|
|
|
15
15
|
/**
|
|
16
16
|
* Sanitizes an event by redacting non-text content blocks from responses
|
|
17
17
|
* and large base64-encoded strings from parameters, and applying the same
|
|
18
|
-
* string redaction (PostHog tokens, base64 blobs, sensitive keys) to
|
|
19
|
-
*
|
|
18
|
+
* string redaction (PostHog tokens, base64 blobs, sensitive keys) to values
|
|
19
|
+
* supplied by the agent.
|
|
20
20
|
*
|
|
21
21
|
* This is a synchronous operation that returns a new object without mutating the original.
|
|
22
22
|
* It should run after customer redaction in the event pipeline.
|
|
@@ -32,11 +32,18 @@ export function sanitizeEvent<T extends Event | McpEvent>(event: T): T {
|
|
|
32
32
|
result.parameters = sanitizeParameters(result.parameters)
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
-
// The intent comes straight from an agent-narrated `context` string, so it
|
|
36
|
-
//
|
|
37
|
-
//
|
|
35
|
+
// The intent comes straight from an agent-narrated `context` string, so it can
|
|
36
|
+
// contain a secret the LLM read aloud or personal data it narrated about the
|
|
37
|
+
// user. Redact it like any other captured value, then strip structured PII
|
|
38
|
+
// (emails, phone numbers, IPs, cards, SSNs) rather than shipping it raw as
|
|
39
|
+
// `$mcp_intent`. PII redaction is scoped to the intent only — structured tool
|
|
40
|
+
// parameters and responses often hold the same shapes as legitimate data.
|
|
38
41
|
if (result.userIntent != null) {
|
|
39
|
-
result.userIntent = sanitizeCapturedValue(result.userIntent) as string
|
|
42
|
+
result.userIntent = redactPii(sanitizeCapturedValue(result.userIntent) as string)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (result.llmModel != null) {
|
|
46
|
+
result.llmModel = sanitizeCapturedValue(result.llmModel) as string
|
|
40
47
|
}
|
|
41
48
|
|
|
42
49
|
if (result.error != null) {
|
package/src/index.ts
CHANGED
package/src/types.ts
CHANGED
|
@@ -511,6 +511,13 @@ export interface ToolCallCaptureData extends McpCaptureCommon {
|
|
|
511
511
|
* the host derived it. Defaults to `context_parameter` when an intent is set.
|
|
512
512
|
*/
|
|
513
513
|
intentSource?: MCPAnalyticsIntentSource
|
|
514
|
+
/**
|
|
515
|
+
* The calling agent's self-reported model id -> `$mcp_llm_model`. On the
|
|
516
|
+
* custom-dispatcher path, read it from {@link PostHogMCP.prepareToolCall}.
|
|
517
|
+
*/
|
|
518
|
+
llmModel?: string
|
|
519
|
+
/** How the model id was obtained -> `$mcp_llm_model_source`. */
|
|
520
|
+
llmModelSource?: MCPAnalyticsModelSource
|
|
514
521
|
/** Captured call arguments → `$mcp_parameters` (sanitized + truncated). */
|
|
515
522
|
parameters?: unknown
|
|
516
523
|
/** Captured tool result → `$mcp_response` (sanitized + truncated). */
|
|
@@ -586,18 +593,30 @@ export interface PrepareToolListOptions {
|
|
|
586
593
|
reportMissing?: boolean
|
|
587
594
|
}
|
|
588
595
|
|
|
596
|
+
/** Options for {@link PostHogMCP.prepareToolCall}. */
|
|
597
|
+
export interface PrepareToolCallOptions {
|
|
598
|
+
/**
|
|
599
|
+
* The tool descriptor before PostHog preparation. Pass this on stateless or
|
|
600
|
+
* multi-replica servers so SDK argument ownership is resolved per request.
|
|
601
|
+
*/
|
|
602
|
+
originalTool?: { inputSchema?: unknown }
|
|
603
|
+
}
|
|
604
|
+
|
|
589
605
|
/**
|
|
590
606
|
* Result of {@link PostHogMCP.prepareToolCall}: the intent pulled off the
|
|
591
|
-
* incoming call, the arguments with
|
|
592
|
-
*
|
|
593
|
-
* the `get_more_tools` virtual tool.
|
|
607
|
+
* incoming call, the arguments with SDK-owned analytics fields removed, and
|
|
608
|
+
* whether the call targeted the `get_more_tools` virtual tool.
|
|
594
609
|
*/
|
|
595
610
|
export interface PreparedToolCall {
|
|
596
611
|
/** The agent's stated intent (the `context` argument), if present. */
|
|
597
612
|
intent?: string
|
|
598
613
|
/** Where the intent came from. Always `context_parameter` here when set. */
|
|
599
614
|
intentSource?: MCPAnalyticsIntentSource
|
|
600
|
-
/** The
|
|
615
|
+
/** The agent's self-reported model id, when `captureModel` is enabled and owned by the SDK. */
|
|
616
|
+
llmModel?: string
|
|
617
|
+
/** How the model id was obtained. Always `self_reported` when present. */
|
|
618
|
+
llmModelSource?: MCPAnalyticsModelSource
|
|
619
|
+
/** The call arguments with SDK-owned `context` and `llm_model` keys removed. */
|
|
601
620
|
args?: Record<string, unknown>
|
|
602
621
|
/** True when `name` is the `get_more_tools` virtual tool. */
|
|
603
622
|
isMissingCapability: boolean
|
|
@@ -610,6 +629,10 @@ export interface MissingCapabilityCaptureData extends McpCaptureCommon {
|
|
|
610
629
|
* on the `get_more_tools` call) → `$mcp_intent`.
|
|
611
630
|
*/
|
|
612
631
|
context?: string
|
|
632
|
+
/** The calling agent's self-reported model id -> `$mcp_llm_model`. */
|
|
633
|
+
llmModel?: string
|
|
634
|
+
/** How the model id was obtained -> `$mcp_llm_model_source`. */
|
|
635
|
+
llmModelSource?: MCPAnalyticsModelSource
|
|
613
636
|
/** Captured call arguments → `$mcp_parameters` (sanitized + truncated). */
|
|
614
637
|
parameters?: unknown
|
|
615
638
|
}
|
package/src/version.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const version = '0.
|
|
1
|
+
export const version = '0.13.1'
|