@posthog/mcp 0.15.0 → 0.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +64 -0
- package/dist/extensions/constants.d.ts +9 -0
- package/dist/extensions/constants.d.ts.map +1 -1
- package/dist/extensions/constants.js +9 -0
- package/dist/extensions/constants.mjs +9 -0
- package/dist/extensions/event-types.d.ts +1 -0
- package/dist/extensions/event-types.d.ts.map +1 -1
- package/dist/extensions/event-types.js +1 -0
- package/dist/extensions/event-types.mjs +1 -0
- package/dist/extensions/feedback.d.ts +41 -0
- package/dist/extensions/feedback.d.ts.map +1 -0
- package/dist/extensions/feedback.js +259 -0
- package/dist/extensions/feedback.mjs +200 -0
- package/dist/extensions/instrument-highlevel.d.ts.map +1 -1
- package/dist/extensions/instrument-highlevel.js +19 -1
- package/dist/extensions/instrument-highlevel.mjs +20 -2
- package/dist/extensions/instrument-lowlevel.d.ts.map +1 -1
- package/dist/extensions/instrument-lowlevel.js +19 -1
- package/dist/extensions/instrument-lowlevel.mjs +20 -2
- package/dist/extensions/instrumentation.d.ts +30 -13
- package/dist/extensions/instrumentation.d.ts.map +1 -1
- package/dist/extensions/instrumentation.js +27 -7
- package/dist/extensions/instrumentation.mjs +27 -7
- package/dist/extensions/mcp-payloads.d.ts +9 -3
- package/dist/extensions/mcp-payloads.d.ts.map +1 -1
- package/dist/extensions/mcp-payloads.js +16 -7
- package/dist/extensions/mcp-payloads.mjs +11 -5
- package/dist/extensions/posthog-events.d.ts.map +1 -1
- package/dist/extensions/posthog-events.js +1 -0
- package/dist/extensions/posthog-events.mjs +1 -0
- package/dist/extensions/posthog-mcp.d.ts +23 -3
- package/dist/extensions/posthog-mcp.d.ts.map +1 -1
- package/dist/extensions/posthog-mcp.js +27 -1
- package/dist/extensions/posthog-mcp.mjs +27 -1
- package/dist/extensions/sanitization.js +1 -1
- package/dist/extensions/sanitization.mjs +2 -2
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +10 -0
- package/dist/index.mjs +5 -1
- package/dist/types.d.ts +113 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/version.mjs +1 -1
- package/package.json +4 -4
- package/src/extensions/constants.ts +9 -0
- package/src/extensions/event-types.ts +1 -0
- package/src/extensions/feedback.ts +349 -0
- package/src/extensions/instrument-highlevel.ts +39 -2
- package/src/extensions/instrument-lowlevel.ts +39 -2
- package/src/extensions/instrumentation.ts +65 -17
- package/src/extensions/mcp-payloads.ts +19 -6
- package/src/extensions/posthog-events.ts +1 -0
- package/src/extensions/posthog-mcp.ts +74 -3
- package/src/extensions/sanitization.ts +3 -3
- package/src/index.ts +21 -0
- package/src/types.ts +119 -0
- package/src/version.ts +1 -1
|
@@ -11,6 +11,15 @@ import type {
|
|
|
11
11
|
RegisteredTool,
|
|
12
12
|
ToolCallback,
|
|
13
13
|
} from '../types'
|
|
14
|
+
import {
|
|
15
|
+
buildFeedbackEventProperties,
|
|
16
|
+
buildFeedbackIntent,
|
|
17
|
+
getFeedbackToolDescriptor,
|
|
18
|
+
handleFeedback,
|
|
19
|
+
parseFeedbackReport,
|
|
20
|
+
resolveCollectFeedbackOptions,
|
|
21
|
+
SEND_FEEDBACK_TOOL_NAME,
|
|
22
|
+
} from './feedback'
|
|
14
23
|
import {
|
|
15
24
|
analyticsOwnsParameter,
|
|
16
25
|
getAnalyticsParameterOwnership,
|
|
@@ -22,7 +31,7 @@ import { MCPAnalyticsEventType } from './event-types'
|
|
|
22
31
|
import { getServerTrackingData } from './internal'
|
|
23
32
|
import type { LoggerFn } from './logger'
|
|
24
33
|
import { createWrappedTool, getToolFunction, hasToolFunction } from './mcp-sdk-compat'
|
|
25
|
-
import { handleReportMissing, resolveMissingCapabilityToolName } from './tools'
|
|
34
|
+
import { getReportMissingToolDescriptor, handleReportMissing, resolveMissingCapabilityToolName } from './tools'
|
|
26
35
|
import {
|
|
27
36
|
handleInitializeRequest,
|
|
28
37
|
handleListToolsRequest,
|
|
@@ -251,11 +260,39 @@ async function handleToolCallRequest(
|
|
|
251
260
|
extra,
|
|
252
261
|
eventType: MCPAnalyticsEventType.mcpMissingCapability,
|
|
253
262
|
explicitContextIntent: context,
|
|
254
|
-
parameterOwnership: getVirtualToolParameterOwnership(
|
|
263
|
+
parameterOwnership: getVirtualToolParameterOwnership(
|
|
264
|
+
data,
|
|
265
|
+
toolName,
|
|
266
|
+
getReportMissingToolDescriptor(toolName).inputSchema
|
|
267
|
+
),
|
|
255
268
|
execute: async () => handleReportMissing({ context }, data.logger),
|
|
256
269
|
})
|
|
257
270
|
}
|
|
258
271
|
|
|
272
|
+
const feedbackOptions = resolveCollectFeedbackOptions(data.options.collectFeedback)
|
|
273
|
+
const isFeedbackCandidate =
|
|
274
|
+
feedbackOptions !== undefined && toolName === (feedbackOptions.toolName ?? SEND_FEEDBACK_TOOL_NAME)
|
|
275
|
+
|
|
276
|
+
if (isFeedbackCandidate && (await isToolAdvertised(server, toolName, extra, data.logger)) === false) {
|
|
277
|
+
const report = parseFeedbackReport(request.params?.arguments, feedbackOptions)
|
|
278
|
+
return await captureToolCall({
|
|
279
|
+
server,
|
|
280
|
+
data,
|
|
281
|
+
request,
|
|
282
|
+
extra,
|
|
283
|
+
eventType: MCPAnalyticsEventType.mcpFeedback,
|
|
284
|
+
explicitContextIntent: buildFeedbackIntent(report),
|
|
285
|
+
omitCapturedParameters: true,
|
|
286
|
+
extraEventProperties: buildFeedbackEventProperties(report),
|
|
287
|
+
parameterOwnership: getVirtualToolParameterOwnership(
|
|
288
|
+
data,
|
|
289
|
+
toolName,
|
|
290
|
+
getFeedbackToolDescriptor(feedbackOptions).inputSchema
|
|
291
|
+
),
|
|
292
|
+
execute: async () => handleFeedback(report, feedbackOptions, data.logger),
|
|
293
|
+
})
|
|
294
|
+
}
|
|
295
|
+
|
|
259
296
|
const registeredTool = toolName ? highLevelServer._registeredTools[toolName] : undefined
|
|
260
297
|
|
|
261
298
|
// Strip SDK-owned arguments before the MCP SDK validates the registered Zod
|
|
@@ -4,10 +4,19 @@
|
|
|
4
4
|
// Licensed under the MIT License: https://github.com/agentcathq/agentcat-typescript-sdk/blob/main/LICENSE
|
|
5
5
|
|
|
6
6
|
import type { CompatibleRequestHandlerExtra, MCPRequestLike, MCPServerLike } from '../types'
|
|
7
|
+
import {
|
|
8
|
+
buildFeedbackEventProperties,
|
|
9
|
+
buildFeedbackIntent,
|
|
10
|
+
getFeedbackToolDescriptor,
|
|
11
|
+
handleFeedback,
|
|
12
|
+
parseFeedbackReport,
|
|
13
|
+
resolveCollectFeedbackOptions,
|
|
14
|
+
SEND_FEEDBACK_TOOL_NAME,
|
|
15
|
+
} from './feedback'
|
|
7
16
|
import { MCPAnalyticsEventType } from './event-types'
|
|
8
17
|
import { getServerTrackingData } from './internal'
|
|
9
18
|
import type { LoggerFn } from './logger'
|
|
10
|
-
import { handleReportMissing, resolveMissingCapabilityToolName } from './tools'
|
|
19
|
+
import { getReportMissingToolDescriptor, handleReportMissing, resolveMissingCapabilityToolName } from './tools'
|
|
11
20
|
import {
|
|
12
21
|
handleInitializeRequest,
|
|
13
22
|
handleListToolsRequest,
|
|
@@ -98,11 +107,39 @@ async function handleToolCallRequest(
|
|
|
98
107
|
extra,
|
|
99
108
|
eventType: MCPAnalyticsEventType.mcpMissingCapability,
|
|
100
109
|
explicitContextIntent: context,
|
|
101
|
-
parameterOwnership: getVirtualToolParameterOwnership(
|
|
110
|
+
parameterOwnership: getVirtualToolParameterOwnership(
|
|
111
|
+
data,
|
|
112
|
+
toolName,
|
|
113
|
+
getReportMissingToolDescriptor(toolName).inputSchema
|
|
114
|
+
),
|
|
102
115
|
execute: async () => handleReportMissing({ context }, data.logger),
|
|
103
116
|
})
|
|
104
117
|
}
|
|
105
118
|
|
|
119
|
+
const feedbackOptions = resolveCollectFeedbackOptions(data.options.collectFeedback)
|
|
120
|
+
const isFeedbackCandidate =
|
|
121
|
+
feedbackOptions !== undefined && toolName === (feedbackOptions.toolName ?? SEND_FEEDBACK_TOOL_NAME)
|
|
122
|
+
|
|
123
|
+
if (isFeedbackCandidate && (await isToolAdvertised(server, toolName, extra, data.logger)) === false) {
|
|
124
|
+
const report = parseFeedbackReport(request.params?.arguments, feedbackOptions)
|
|
125
|
+
return await captureToolCall({
|
|
126
|
+
server,
|
|
127
|
+
data,
|
|
128
|
+
request,
|
|
129
|
+
extra,
|
|
130
|
+
eventType: MCPAnalyticsEventType.mcpFeedback,
|
|
131
|
+
explicitContextIntent: buildFeedbackIntent(report),
|
|
132
|
+
omitCapturedParameters: true,
|
|
133
|
+
extraEventProperties: buildFeedbackEventProperties(report),
|
|
134
|
+
parameterOwnership: getVirtualToolParameterOwnership(
|
|
135
|
+
data,
|
|
136
|
+
toolName,
|
|
137
|
+
getFeedbackToolDescriptor(feedbackOptions).inputSchema
|
|
138
|
+
),
|
|
139
|
+
execute: async () => handleFeedback(report, feedbackOptions, data.logger),
|
|
140
|
+
})
|
|
141
|
+
}
|
|
142
|
+
|
|
106
143
|
return await captureToolCall({
|
|
107
144
|
server,
|
|
108
145
|
data,
|
|
@@ -7,6 +7,7 @@ import type {
|
|
|
7
7
|
AnalyticsParameterOwnership,
|
|
8
8
|
CompatibleRequestHandlerExtra,
|
|
9
9
|
CompatibleToolsListLike,
|
|
10
|
+
JsonRecord,
|
|
10
11
|
MCPAnalyticsData,
|
|
11
12
|
MCPRequestLike,
|
|
12
13
|
MCPServerLike,
|
|
@@ -44,6 +45,7 @@ import { readRequestHandlerMethod } from './mcp-sdk-compat'
|
|
|
44
45
|
import { getRequestHeaders } from './request-headers'
|
|
45
46
|
import { getSessionId, getSessionInfo, isModernEraRequest, newSessionId } from './session'
|
|
46
47
|
import { encodeSessionId, readMcpSessionHeader, writeSessionIdToTransport } from './session-token'
|
|
48
|
+
import { getFeedbackToolDescriptor, resolveCollectFeedbackOptions, SEND_FEEDBACK_TOOL_NAME } from './feedback'
|
|
47
49
|
import { getReportMissingToolDescriptor, resolveMissingCapabilityToolName } from './tools'
|
|
48
50
|
import { applyResolvedMetadata, isToolResultError } from './tracing-helpers'
|
|
49
51
|
|
|
@@ -70,16 +72,29 @@ interface TraceToolCallParams {
|
|
|
70
72
|
parameterOwnership?: AnalyticsParameterOwnership
|
|
71
73
|
/**
|
|
72
74
|
* Event type to capture. Defaults to a tool call; the `get_more_tools` virtual
|
|
73
|
-
* tool passes `mcpMissingCapability`
|
|
74
|
-
* a
|
|
75
|
+
* tool passes `mcpMissingCapability` and `send_feedback` passes
|
|
76
|
+
* `mcpFeedback`, so they record a capability gap / a feedback report
|
|
77
|
+
* rather than a tool invocation.
|
|
75
78
|
*/
|
|
76
79
|
eventType?: MCPAnalyticsEventType
|
|
77
80
|
/**
|
|
78
81
|
* When set, used verbatim as the captured intent (source `context_parameter`)
|
|
79
|
-
* instead of running `resolveToolCallIntent`. Used by the
|
|
80
|
-
*
|
|
82
|
+
* instead of running `resolveToolCallIntent`. Used by the virtual tools, which
|
|
83
|
+
* carry their intent in their own arguments.
|
|
81
84
|
*/
|
|
82
85
|
explicitContextIntent?: string
|
|
86
|
+
/**
|
|
87
|
+
* Extra event properties spread onto the captured event, after the host's
|
|
88
|
+
* `eventProperties`. Used by `send_feedback` for its `$mcp_feedback_*` fields.
|
|
89
|
+
*/
|
|
90
|
+
extraEventProperties?: JsonRecord
|
|
91
|
+
/**
|
|
92
|
+
* Drop the generically captured `$mcp_parameters` from the event. Used by
|
|
93
|
+
* `send_feedback`: its arguments are agent-narrated free text, so the
|
|
94
|
+
* PII-redacted `$mcp_feedback_*` properties are the captured surface — the
|
|
95
|
+
* raw arguments would bypass that redaction and record undeclared fields.
|
|
96
|
+
*/
|
|
97
|
+
omitCapturedParameters?: boolean
|
|
83
98
|
/**
|
|
84
99
|
* Optional accessor for an error the executor captured out-of-band. The
|
|
85
100
|
* high-level SDK turns thrown tool errors into `isError: true` results before
|
|
@@ -107,21 +122,28 @@ export async function captureToolCall(params: TraceToolCallParams): Promise<unkn
|
|
|
107
122
|
parameterOwnership,
|
|
108
123
|
eventType,
|
|
109
124
|
explicitContextIntent,
|
|
125
|
+
extraEventProperties,
|
|
126
|
+
omitCapturedParameters,
|
|
110
127
|
takeCapturedError,
|
|
111
128
|
} = params
|
|
112
129
|
const resolvedEventType = eventType ?? MCPAnalyticsEventType.mcpToolsCall
|
|
130
|
+
// The SDK's own virtual tools carry their intent in their own arguments, so
|
|
131
|
+
// the injected `context` parameter neither exists on them nor gets read.
|
|
132
|
+
const isVirtualAnalyticsTool =
|
|
133
|
+
resolvedEventType === MCPAnalyticsEventType.mcpMissingCapability ||
|
|
134
|
+
resolvedEventType === MCPAnalyticsEventType.mcpFeedback
|
|
113
135
|
const ownership = getActiveAnalyticsParameterOwnership(
|
|
114
136
|
data,
|
|
115
137
|
request.params?.name,
|
|
116
138
|
parameterOwnership,
|
|
117
|
-
|
|
139
|
+
isVirtualAnalyticsTool
|
|
118
140
|
)
|
|
119
141
|
// Reading the argument and removing it are separate questions: deleting one the
|
|
120
142
|
// application declared costs the customer their call, so the strip below still
|
|
121
143
|
// requires positive ownership, while reading it when ownership is unresolved
|
|
122
144
|
// costs at worst a mislabelled property. ADR-0011.
|
|
123
145
|
const canCaptureContextIntent =
|
|
124
|
-
|
|
146
|
+
!isVirtualAnalyticsTool &&
|
|
125
147
|
isContextEnabled(data.options.context) &&
|
|
126
148
|
(ownership.context || !ownership.contextOwnershipKnown)
|
|
127
149
|
const conversation = resolveConversationId(ownership.conversationId, request.params?.arguments)
|
|
@@ -145,6 +167,12 @@ export async function captureToolCall(params: TraceToolCallParams): Promise<unkn
|
|
|
145
167
|
if (preparedEvent && explicitContextIntent) {
|
|
146
168
|
setExplicitContextIntent(preparedEvent.event, explicitContextIntent)
|
|
147
169
|
}
|
|
170
|
+
if (preparedEvent && extraEventProperties) {
|
|
171
|
+
preparedEvent.event.properties = { ...preparedEvent.event.properties, ...extraEventProperties }
|
|
172
|
+
}
|
|
173
|
+
if (preparedEvent && omitCapturedParameters) {
|
|
174
|
+
preparedEvent.event.parameters = undefined
|
|
175
|
+
}
|
|
148
176
|
|
|
149
177
|
let result: unknown
|
|
150
178
|
try {
|
|
@@ -178,13 +206,13 @@ function getActiveAnalyticsParameterOwnership(
|
|
|
178
206
|
data: MCPAnalyticsData,
|
|
179
207
|
toolName: string | undefined,
|
|
180
208
|
override: AnalyticsParameterOwnership | undefined,
|
|
181
|
-
|
|
209
|
+
isVirtualAnalyticsTool: boolean
|
|
182
210
|
): ActiveAnalyticsParameterOwnership {
|
|
183
211
|
const listed = toolName ? data.toolAnalyticsParameterOwnership.get(toolName) : undefined
|
|
184
212
|
const ownership = override ?? listed
|
|
185
213
|
return {
|
|
186
214
|
contextOwnershipKnown: ownership !== undefined,
|
|
187
|
-
context: !
|
|
215
|
+
context: !isVirtualAnalyticsTool && isContextEnabled(data.options.context) && ownership?.context === true,
|
|
188
216
|
conversationId: data.options.enableConversationId === true && ownership?.conversationId === true,
|
|
189
217
|
llmModel: isCaptureModelEnabled(data.options.captureModel) && ownership?.llmModel === true,
|
|
190
218
|
// Deliberately read off `listed`, never the override: only the advertised
|
|
@@ -613,26 +641,31 @@ export function patchRequestHandlers(server: MCPServerLike, patches: Record<stri
|
|
|
613
641
|
}
|
|
614
642
|
|
|
615
643
|
/**
|
|
616
|
-
* Ownership for
|
|
617
|
-
* `tools/list` cache.
|
|
644
|
+
* Ownership for an SDK-owned virtual tool (`get_more_tools`, `send_feedback`),
|
|
645
|
+
* resolved from its own descriptor rather than the `tools/list` cache.
|
|
618
646
|
*
|
|
619
|
-
*
|
|
620
|
-
*
|
|
621
|
-
*
|
|
622
|
-
*
|
|
623
|
-
*
|
|
647
|
+
* A virtual-tool branch is only entered when the application does not advertise
|
|
648
|
+
* a tool by this name, so the descriptor is the SDK's own and what it declares
|
|
649
|
+
* is known statically. An instance that never served a listing — a per-request
|
|
650
|
+
* `McpServer`/`Server`, the topology in ADR-0011 — would otherwise read every
|
|
651
|
+
* injected parameter as not-ours and neither capture nor strip it.
|
|
624
652
|
*
|
|
625
653
|
* `conversation_id` still comes from the cache on purpose: resolving it here too
|
|
626
654
|
* would start minting a handle, and appending its prompt-back block, on
|
|
627
655
|
* instances that today mint none. That changes session anchoring (ADR-0004)
|
|
628
656
|
* rather than closing this gap.
|
|
657
|
+
*
|
|
658
|
+
* `virtualToolInputSchema` is required (not defaulted to one specific virtual
|
|
659
|
+
* tool's descriptor) so every call site names the tool it means; each caller
|
|
660
|
+
* passes its own descriptor's `inputSchema`.
|
|
629
661
|
*/
|
|
630
662
|
export function getVirtualToolParameterOwnership(
|
|
631
663
|
data: MCPAnalyticsData,
|
|
632
|
-
toolName: string
|
|
664
|
+
toolName: string,
|
|
665
|
+
virtualToolInputSchema: unknown
|
|
633
666
|
): AnalyticsParameterOwnership {
|
|
634
667
|
return {
|
|
635
|
-
...getAnalyticsParameterOwnership(
|
|
668
|
+
...getAnalyticsParameterOwnership(virtualToolInputSchema),
|
|
636
669
|
conversationId: data.toolAnalyticsParameterOwnership.get(toolName)?.conversationId === true,
|
|
637
670
|
}
|
|
638
671
|
}
|
|
@@ -808,6 +841,21 @@ async function getTracedToolsList(
|
|
|
808
841
|
}
|
|
809
842
|
}
|
|
810
843
|
|
|
844
|
+
const feedbackOptions = resolveCollectFeedbackOptions(data.options.collectFeedback)
|
|
845
|
+
if (feedbackOptions) {
|
|
846
|
+
const feedbackToolName = feedbackOptions.toolName ?? SEND_FEEDBACK_TOOL_NAME
|
|
847
|
+
const alreadyPresent = tools.some((tool) => tool?.name === feedbackToolName)
|
|
848
|
+
if (alreadyPresent) {
|
|
849
|
+
data.logger(
|
|
850
|
+
`Warning: Cannot inject agent-feedback tool "${feedbackToolName}" because a real tool already uses that name. The real tool will not be intercepted.`
|
|
851
|
+
)
|
|
852
|
+
} else {
|
|
853
|
+
const virtualTool = getFeedbackToolDescriptor(feedbackOptions)
|
|
854
|
+
tools.push(virtualTool)
|
|
855
|
+
cacheToolAnalyticsParameterOwnership(data.toolAnalyticsParameterOwnership, [virtualTool])
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
|
|
811
859
|
// After the virtual tool is appended, so it advertises `llm_model` too.
|
|
812
860
|
// Its ownership was cached above from the un-injected descriptor, so the
|
|
813
861
|
// SDK already strips and captures the argument on its calls — without
|
|
@@ -508,8 +508,8 @@ function sanitizeString(value: string): string {
|
|
|
508
508
|
}
|
|
509
509
|
|
|
510
510
|
/**
|
|
511
|
-
* Sanitizes
|
|
512
|
-
* captured string gets.
|
|
511
|
+
* Sanitizes agent-narrated free text (`$mcp_intent`, the `send_feedback`
|
|
512
|
+
* fields): structured PII on top of the passes every captured string gets.
|
|
513
513
|
*
|
|
514
514
|
* Every step of the order is load-bearing. The binary gate reads the value as it
|
|
515
515
|
* arrived, because splicing `[redacted]` into a blob — a Luhn-valid run inside
|
|
@@ -519,7 +519,7 @@ function sanitizeString(value: string): string {
|
|
|
519
519
|
* URL pass, because the URL rewrite percent-encodes the `@` that the email
|
|
520
520
|
* pattern anchors on.
|
|
521
521
|
*/
|
|
522
|
-
export function
|
|
522
|
+
export function sanitizeFreeText(value: string): string {
|
|
523
523
|
if (isBinaryBlob(value)) {
|
|
524
524
|
return BINARY_REDACTED_VALUE
|
|
525
525
|
}
|
|
@@ -607,16 +607,29 @@ export function redactPii(value: string): string {
|
|
|
607
607
|
}
|
|
608
608
|
|
|
609
609
|
export function sanitizeCapturedValue(value: unknown): unknown {
|
|
610
|
+
return sanitizeValueWith(value, sanitizeString)
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
/**
|
|
614
|
+
* {@link sanitizeCapturedValue} with the intent-grade string pass
|
|
615
|
+
* ({@link sanitizeFreeText}) on every string leaf, so nested agent-narrated
|
|
616
|
+
* values get structured-PII redaction in the load-bearing order too.
|
|
617
|
+
*/
|
|
618
|
+
export function sanitizeFreeTextValue(value: unknown): unknown {
|
|
619
|
+
return sanitizeValueWith(value, sanitizeFreeText)
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
function sanitizeValueWith(value: unknown, sanitizeStringFn: (value: string) => string): unknown {
|
|
610
623
|
if (value == null) {
|
|
611
624
|
return value
|
|
612
625
|
}
|
|
613
626
|
|
|
614
627
|
if (typeof value === 'string') {
|
|
615
|
-
return
|
|
628
|
+
return sanitizeStringFn(value)
|
|
616
629
|
}
|
|
617
630
|
|
|
618
631
|
if (Array.isArray(value)) {
|
|
619
|
-
return value.map(
|
|
632
|
+
return value.map((item) => sanitizeValueWith(item, sanitizeStringFn))
|
|
620
633
|
}
|
|
621
634
|
|
|
622
635
|
if (value instanceof Date) {
|
|
@@ -629,7 +642,7 @@ export function sanitizeCapturedValue(value: unknown): unknown {
|
|
|
629
642
|
|
|
630
643
|
const result: JsonRecord = {}
|
|
631
644
|
for (const [key, nestedValue] of Object.entries(value)) {
|
|
632
|
-
result[key] = shouldRedactKey(key) ? REDACTED_VALUE :
|
|
645
|
+
result[key] = shouldRedactKey(key) ? REDACTED_VALUE : sanitizeValueWith(nestedValue, sanitizeStringFn)
|
|
633
646
|
}
|
|
634
647
|
return result
|
|
635
648
|
}
|
|
@@ -10,6 +10,7 @@ import { MCPAnalyticsEventType } from './event-types'
|
|
|
10
10
|
const BUILT_IN_EVENT_NAME_BY_TYPE = {
|
|
11
11
|
[MCPAnalyticsEventType.custom]: PostHogMCPAnalyticsEvent.Custom,
|
|
12
12
|
[MCPAnalyticsEventType.identify]: PostHogMCPAnalyticsEvent.Identify,
|
|
13
|
+
[MCPAnalyticsEventType.mcpFeedback]: PostHogMCPAnalyticsEvent.Feedback,
|
|
13
14
|
[MCPAnalyticsEventType.mcpMissingCapability]: PostHogMCPAnalyticsEvent.MissingCapability,
|
|
14
15
|
[MCPAnalyticsEventType.mcpInitialize]: PostHogMCPAnalyticsEvent.Initialize,
|
|
15
16
|
[MCPAnalyticsEventType.mcpPromptsGet]: PostHogMCPAnalyticsEvent.PromptGet,
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { PostHog, type PostHogOptions } from 'posthog-node'
|
|
2
2
|
|
|
3
3
|
import type {
|
|
4
|
+
FeedbackCaptureData,
|
|
5
|
+
CollectFeedbackOptions,
|
|
6
|
+
CollectFeedbackConfig,
|
|
4
7
|
InitializeCaptureData,
|
|
5
8
|
JsonRecord,
|
|
6
9
|
MCPAnalyticsOptions,
|
|
@@ -13,6 +16,14 @@ import type {
|
|
|
13
16
|
ToolCallCaptureData,
|
|
14
17
|
ToolsListCaptureData,
|
|
15
18
|
} from '../types'
|
|
19
|
+
import {
|
|
20
|
+
buildFeedbackEventProperties,
|
|
21
|
+
buildFeedbackIntent,
|
|
22
|
+
getFeedbackToolDescriptor,
|
|
23
|
+
parseFeedbackReport,
|
|
24
|
+
resolveCollectFeedbackOptions,
|
|
25
|
+
SEND_FEEDBACK_TOOL_NAME,
|
|
26
|
+
} from './feedback'
|
|
16
27
|
import { analyticsOwnsParameter, stripOwnedAnalyticsArguments } from './analytics-parameters'
|
|
17
28
|
import {
|
|
18
29
|
addContextParameterToTools,
|
|
@@ -47,6 +58,17 @@ export interface PostHogMCPOptions extends PostHogOptions {
|
|
|
47
58
|
* can't drift. Defaults to `get_more_tools`.
|
|
48
59
|
*/
|
|
49
60
|
missingCapabilityToolName?: string
|
|
61
|
+
/**
|
|
62
|
+
* Enable + configure the `send_feedback` virtual tool: injected by
|
|
63
|
+
* {@link PostHogMCP.prepareToolList} (when its `collectFeedback` toggle is on)
|
|
64
|
+
* and detected by {@link PostHogMCP.prepareToolCall}. Set once here so
|
|
65
|
+
* injection and detection can't drift — without this option, `prepareToolCall`
|
|
66
|
+
* never flags a call as feedback, so a real tool that uses the name is not
|
|
67
|
+
* shadowed. Pick a `toolName` no real tool uses. `onFeedback` is ignored on
|
|
68
|
+
* this path — the host dispatcher routes reports itself via
|
|
69
|
+
* {@link PreparedToolCall.feedbackReport}.
|
|
70
|
+
*/
|
|
71
|
+
collectFeedback?: CollectFeedbackConfig
|
|
50
72
|
/**
|
|
51
73
|
* Capture the calling model from recognized client metadata, with an injected
|
|
52
74
|
* `llm_model` argument as fallback. Off by default.
|
|
@@ -93,19 +115,36 @@ export interface PostHogMCPOptions extends PostHogOptions {
|
|
|
93
115
|
export class PostHogMCP extends PostHog {
|
|
94
116
|
readonly #sink = new McpEventSink(this)
|
|
95
117
|
|
|
96
|
-
// The
|
|
118
|
+
// The virtual-tool config lives here (not on the per-call options) so that
|
|
97
119
|
// prepareToolList (inject) and prepareToolCall (detect) always agree.
|
|
98
120
|
readonly #missingCapabilityToolName: string
|
|
121
|
+
// `undefined` is the enable switch's off state: without it, prepareToolCall
|
|
122
|
+
// must never claim a call named like the virtual tool — the host may have a
|
|
123
|
+
// real tool by that name, and flagging it would shadow the real handler.
|
|
124
|
+
readonly #feedbackOptions: CollectFeedbackOptions | undefined
|
|
99
125
|
readonly #captureModel: MCPAnalyticsOptions['captureModel']
|
|
100
126
|
readonly #modelParameterOwnership = new Map<string, boolean>()
|
|
101
127
|
|
|
102
128
|
constructor(apiKey: string, options: PostHogMCPOptions = {}) {
|
|
103
129
|
super(apiKey, options)
|
|
104
130
|
this.#missingCapabilityToolName = options.missingCapabilityToolName ?? GET_MORE_TOOLS_NAME
|
|
131
|
+
this.#feedbackOptions = resolveCollectFeedbackOptions(options.collectFeedback)
|
|
132
|
+
// Fail fast on a config error (reserved extra key, undeclared extraRequired)
|
|
133
|
+
// instead of first surfacing it when a tools/list is served.
|
|
134
|
+
getFeedbackToolDescriptor(this.#feedbackOptions)
|
|
135
|
+
if (this.#feedbackOptions?.onFeedback) {
|
|
136
|
+
log(
|
|
137
|
+
'Warning: collectFeedback.onFeedback is ignored on the PostHogMCP path - route reports from your dispatcher via prepareToolCall().feedbackReport instead.'
|
|
138
|
+
)
|
|
139
|
+
}
|
|
105
140
|
this.#captureModel = options.captureModel
|
|
106
141
|
applyMcpLibIdentity(this)
|
|
107
142
|
}
|
|
108
143
|
|
|
144
|
+
get #feedbackToolName(): string {
|
|
145
|
+
return this.#feedbackOptions?.toolName ?? SEND_FEEDBACK_TOOL_NAME
|
|
146
|
+
}
|
|
147
|
+
|
|
109
148
|
/** Capture a tool invocation. Emits `$mcp_tool_call` (+ an `$exception` sibling on error). */
|
|
110
149
|
captureToolCall(data: ToolCallCaptureData): void {
|
|
111
150
|
const event = baseEvent(MCPAnalyticsEventType.mcpToolsCall, data)
|
|
@@ -160,8 +199,9 @@ export class PostHogMCP extends PostHog {
|
|
|
160
199
|
* Decorate your `tools/list` response with PostHog's analytics affordances:
|
|
161
200
|
* injects the `context` argument into every tool (so agents state their intent,
|
|
162
201
|
* captured as `$mcp_intent`), injects `llm_model` when the constructor's
|
|
163
|
-
* `captureModel` option is enabled,
|
|
164
|
-
* `reportMissing` is on
|
|
202
|
+
* `captureModel` option is enabled, appends `get_more_tools` when
|
|
203
|
+
* `reportMissing` is on, and appends `send_feedback` when `collectFeedback` is
|
|
204
|
+
* on. Returns a new array; your tools are untouched.
|
|
165
205
|
*
|
|
166
206
|
* The appended `get_more_tools` descriptor carries only the base MCP tool fields
|
|
167
207
|
* (name, description, input schema) — not any framework-specific fields your
|
|
@@ -188,6 +228,14 @@ export class PostHogMCP extends PostHog {
|
|
|
188
228
|
prepared = [...prepared, getReportMissingToolDescriptor(this.#missingCapabilityToolName) as TTool]
|
|
189
229
|
}
|
|
190
230
|
|
|
231
|
+
if (
|
|
232
|
+
options.collectFeedback &&
|
|
233
|
+
this.#feedbackOptions !== undefined &&
|
|
234
|
+
!prepared.some((tool) => tool?.name === this.#feedbackToolName)
|
|
235
|
+
) {
|
|
236
|
+
prepared = [...prepared, getFeedbackToolDescriptor(this.#feedbackOptions) as TTool]
|
|
237
|
+
}
|
|
238
|
+
|
|
191
239
|
if (isCaptureModelEnabled(this.#captureModel)) {
|
|
192
240
|
this.#modelParameterOwnership.clear()
|
|
193
241
|
const ownershipByName = new Map<string, boolean>()
|
|
@@ -256,6 +304,7 @@ export class PostHogMCP extends PostHog {
|
|
|
256
304
|
? resolveModel({ params: { arguments: args, _meta: options.requestMeta } }, ownsModel)
|
|
257
305
|
: undefined
|
|
258
306
|
const strippedArgs = stripContext(args)
|
|
307
|
+
const isFeedback = this.#feedbackOptions !== undefined && name === this.#feedbackToolName
|
|
259
308
|
return {
|
|
260
309
|
intent,
|
|
261
310
|
intentSource: intent ? 'context_parameter' : undefined,
|
|
@@ -269,6 +318,8 @@ export class PostHogMCP extends PostHog {
|
|
|
269
318
|
}) as Record<string, unknown> | undefined)
|
|
270
319
|
: strippedArgs,
|
|
271
320
|
isMissingCapability: name === this.#missingCapabilityToolName,
|
|
321
|
+
isFeedback,
|
|
322
|
+
feedbackReport: isFeedback ? parseFeedbackReport(args, this.#feedbackOptions) : undefined,
|
|
272
323
|
}
|
|
273
324
|
}
|
|
274
325
|
|
|
@@ -286,6 +337,26 @@ export class PostHogMCP extends PostHog {
|
|
|
286
337
|
this.#emit(event)
|
|
287
338
|
}
|
|
288
339
|
|
|
340
|
+
/**
|
|
341
|
+
* Capture a `send_feedback` call as an agent-feedback report. Emits
|
|
342
|
+
* `$mcp_feedback` with the report's `$mcp_feedback_*` properties and its
|
|
343
|
+
* summary/details as `$mcp_intent`. Reply to the agent with
|
|
344
|
+
* `sendFeedbackResult()` (or a custom text) after routing the report to your
|
|
345
|
+
* own feedback backend.
|
|
346
|
+
*/
|
|
347
|
+
captureFeedback(data: FeedbackCaptureData): void {
|
|
348
|
+
const event = baseEvent(MCPAnalyticsEventType.mcpFeedback, data)
|
|
349
|
+
event.resourceName = this.#feedbackToolName
|
|
350
|
+
// Deliberately no `$mcp_parameters`: the arguments are agent-narrated free
|
|
351
|
+
// text, and the PII-redacted `$mcp_feedback_*` properties are the captured
|
|
352
|
+
// surface. Raw arguments would bypass that redaction. Feedback properties
|
|
353
|
+
// win over the caller's, matching the instrument() path's spread order.
|
|
354
|
+
event.properties = { ...event.properties, ...buildFeedbackEventProperties(data.report) }
|
|
355
|
+
applyIntent(event, buildFeedbackIntent(data.report), 'context_parameter')
|
|
356
|
+
setEventModel(event, data.llmModel, data.llmModelSource)
|
|
357
|
+
this.#emit(event)
|
|
358
|
+
}
|
|
359
|
+
|
|
289
360
|
/**
|
|
290
361
|
* Fire-and-forget, mirroring posthog-node's `capture()`: the event is enqueued
|
|
291
362
|
* on the client, not awaited. Never throws — a failure to record analytics
|
|
@@ -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,
|
|
7
|
+
import { sanitizeCapturedValue, sanitizeFreeText } from './mcp-payloads'
|
|
8
8
|
|
|
9
9
|
type SanitizedRecord = Record<string, unknown>
|
|
10
10
|
|
|
@@ -41,13 +41,13 @@ export function sanitizeEvent<T extends Event | McpEvent>(event: T): T {
|
|
|
41
41
|
|
|
42
42
|
// The intent comes straight from an agent-narrated `context` string, so it can
|
|
43
43
|
// contain a secret the LLM read aloud or personal data it narrated about the
|
|
44
|
-
// user. `
|
|
44
|
+
// user. `sanitizeFreeText` adds structured PII redaction (emails, phone numbers,
|
|
45
45
|
// IPs, cards, SSNs) to the passes every captured value gets, in the one order
|
|
46
46
|
// that works — see its doc comment. PII redaction is scoped to the intent only:
|
|
47
47
|
// structured tool parameters and responses often hold the same shapes as
|
|
48
48
|
// legitimate data.
|
|
49
49
|
if (result.userIntent != null) {
|
|
50
|
-
result.userIntent =
|
|
50
|
+
result.userIntent = sanitizeFreeText(result.userIntent)
|
|
51
51
|
}
|
|
52
52
|
|
|
53
53
|
if (result.llmModel != null) {
|
package/src/index.ts
CHANGED
|
@@ -14,6 +14,7 @@ import { applyMcpLibIdentity } from './extensions/lib-identity'
|
|
|
14
14
|
import { deriveSessionIdFromMCPSession, getSessionInfo, newSessionId } from './extensions/session'
|
|
15
15
|
import { instrumentLowLevelServer } from './extensions/instrument-lowlevel'
|
|
16
16
|
import { instrumentHighLevelServer } from './extensions/instrument-highlevel'
|
|
17
|
+
import { getFeedbackToolDescriptor, resolveCollectFeedbackOptions } from './extensions/feedback'
|
|
17
18
|
import type {
|
|
18
19
|
CaptureEventData,
|
|
19
20
|
HighLevelMCPServerLike,
|
|
@@ -52,6 +53,17 @@ import type {
|
|
|
52
53
|
*/
|
|
53
54
|
function instrument(server: unknown, posthog: PostHog, options: MCPAnalyticsOptions = {}): McpAnalytics {
|
|
54
55
|
const logger = createLogger(options?.logger)
|
|
56
|
+
|
|
57
|
+
// Fail fast on a `collectFeedback` config error (reserved extra key,
|
|
58
|
+
// undeclared extraRequired). Above the graceful-degradation try so it throws
|
|
59
|
+
// out of instrument() like PostHogMCP's constructor does — inside the catch it
|
|
60
|
+
// would silently disable ALL analytics for the server, not just feedback.
|
|
61
|
+
// `options?.` — untyped JavaScript can pass null options (see logger-isolation.test.ts).
|
|
62
|
+
const feedbackOptions = resolveCollectFeedbackOptions(options?.collectFeedback)
|
|
63
|
+
if (feedbackOptions) {
|
|
64
|
+
getFeedbackToolDescriptor(feedbackOptions)
|
|
65
|
+
}
|
|
66
|
+
|
|
55
67
|
try {
|
|
56
68
|
if (!posthog) {
|
|
57
69
|
logger('Warning: No PostHog client passed to instrument(). Events will not be sent anywhere.')
|
|
@@ -129,6 +141,7 @@ function getLowLevelServer(server: MCPServerLike | HighLevelMCPServerLike): MCPS
|
|
|
129
141
|
*/
|
|
130
142
|
const DEFAULT_OPTIONS = {
|
|
131
143
|
reportMissing: false,
|
|
144
|
+
collectFeedback: false,
|
|
132
145
|
enableConversationId: false,
|
|
133
146
|
} satisfies Partial<MCPAnalyticsOptions>
|
|
134
147
|
|
|
@@ -215,14 +228,22 @@ export {
|
|
|
215
228
|
export { getRequestHeaders } from './extensions/request-headers'
|
|
216
229
|
export { PostHogMCP, type PostHogMCPOptions } from './extensions/posthog-mcp'
|
|
217
230
|
export { getMoreToolsResult } from './extensions/tools'
|
|
231
|
+
export { sendFeedbackResult, SEND_FEEDBACK_TOOL_NAME } from './extensions/feedback'
|
|
218
232
|
export { setLogger } from './extensions/logger'
|
|
219
233
|
// Re-export the posthog-node client so a single import works:
|
|
220
234
|
// import { PostHog, instrument } from "@posthog/mcp"
|
|
221
235
|
// posthog-node stays a peer dependency, so this resolves the host app's installed copy.
|
|
222
236
|
export { PostHog, type PostHogOptions } from 'posthog-node'
|
|
223
237
|
export type {
|
|
238
|
+
FeedbackCaptureData,
|
|
239
|
+
FeedbackExtraPropertySchema,
|
|
240
|
+
CollectFeedbackOptions,
|
|
241
|
+
FeedbackReport,
|
|
242
|
+
FeedbackSentiment,
|
|
243
|
+
FeedbackType,
|
|
224
244
|
BeforeSendFn,
|
|
225
245
|
CaptureEventData,
|
|
246
|
+
CollectFeedbackConfig,
|
|
226
247
|
InitializeCaptureData,
|
|
227
248
|
McpAnalytics,
|
|
228
249
|
McpCaptureCommon,
|