@posthog/mcp 0.14.1 → 0.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +64 -0
- 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 +23 -2
- package/dist/extensions/instrument-highlevel.mjs +25 -4
- package/dist/extensions/instrument-lowlevel.d.ts +4 -3
- package/dist/extensions/instrument-lowlevel.d.ts.map +1 -1
- package/dist/extensions/instrument-lowlevel.js +23 -2
- package/dist/extensions/instrument-lowlevel.mjs +25 -4
- package/dist/extensions/instrumentation.d.ts +33 -13
- package/dist/extensions/instrumentation.d.ts.map +1 -1
- package/dist/extensions/instrumentation.js +103 -9
- package/dist/extensions/instrumentation.mjs +99 -8
- package/dist/extensions/internal.js +9 -2
- package/dist/extensions/internal.mjs +9 -2
- package/dist/extensions/mcp-payloads.d.ts +19 -0
- package/dist/extensions/mcp-payloads.d.ts.map +1 -1
- package/dist/extensions/mcp-payloads.js +217 -7
- package/dist/extensions/mcp-payloads.mjs +210 -6
- 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.d.ts.map +1 -1
- package/dist/extensions/sanitization.js +2 -1
- package/dist/extensions/sanitization.mjs +3 -2
- package/dist/index.d.ts +5 -4
- 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 +114 -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 +45 -2
- package/src/extensions/instrument-lowlevel.ts +49 -5
- package/src/extensions/instrumentation.ts +170 -17
- package/src/extensions/internal.ts +13 -2
- package/src/extensions/mcp-payloads.ts +443 -7
- package/src/extensions/posthog-events.ts +1 -0
- package/src/extensions/posthog-mcp.ts +74 -3
- package/src/extensions/sanitization.ts +14 -6
- package/src/index.ts +24 -3
- package/src/types.ts +120 -0
- package/src/version.ts +1 -1
|
@@ -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
|
|
@@ -395,6 +423,111 @@ function publishFailedToolEvent(
|
|
|
395
423
|
}
|
|
396
424
|
}
|
|
397
425
|
|
|
426
|
+
type ResourceEventType = typeof MCPAnalyticsEventType.mcpResourcesList | typeof MCPAnalyticsEventType.mcpResourcesRead
|
|
427
|
+
|
|
428
|
+
interface TraceRequestParams {
|
|
429
|
+
server: MCPServerLike
|
|
430
|
+
originalHandler: MCPRequestHandler
|
|
431
|
+
request: MCPRequestLike
|
|
432
|
+
extra: CompatibleRequestHandlerExtra | undefined
|
|
433
|
+
eventType: ResourceEventType
|
|
434
|
+
logger: LoggerFn
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
/** One resource request's outcome: either the handler threw, or it returned. */
|
|
438
|
+
type ResourceOutcome = { error: unknown } | { result: unknown }
|
|
439
|
+
|
|
440
|
+
/**
|
|
441
|
+
* Stamps a resource request's outcome onto its prepared event and publishes it.
|
|
442
|
+
*
|
|
443
|
+
* A listing's result is captured as the event response; a read's is not. What
|
|
444
|
+
* `resources/list` and `resources/templates/list` return is discovery metadata —
|
|
445
|
+
* names, uris, mime types, the next cursor — which answers "what did this client
|
|
446
|
+
* actually see?", while a read returns the resource body itself, which analytics
|
|
447
|
+
* has no business holding.
|
|
448
|
+
*/
|
|
449
|
+
function publishResourceEvent(
|
|
450
|
+
server: MCPServerLike,
|
|
451
|
+
preparedEvent: PreparedToolEvent | null,
|
|
452
|
+
startTime: Date,
|
|
453
|
+
params: TraceRequestParams,
|
|
454
|
+
outcome: ResourceOutcome
|
|
455
|
+
): void {
|
|
456
|
+
if (!preparedEvent) {
|
|
457
|
+
return
|
|
458
|
+
}
|
|
459
|
+
const { event, requestAttribution } = preparedEvent
|
|
460
|
+
// Stamping is inside the `try` with the publish: `captureException` reads the
|
|
461
|
+
// thrown value's own `stack`, which an application error is free to define as
|
|
462
|
+
// a throwing getter. Outside, that would replace the resource error the caller
|
|
463
|
+
// is waiting on with ours.
|
|
464
|
+
try {
|
|
465
|
+
if ('error' in outcome) {
|
|
466
|
+
event.isError = true
|
|
467
|
+
event.error = captureException(outcome.error)
|
|
468
|
+
} else {
|
|
469
|
+
event.isError = false
|
|
470
|
+
if (params.eventType === MCPAnalyticsEventType.mcpResourcesList) {
|
|
471
|
+
event.response = outcome.result
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
event.duration = Date.now() - startTime.getTime()
|
|
475
|
+
captureEvent(server, event, params.logger, requestAttribution)
|
|
476
|
+
} catch (error) {
|
|
477
|
+
params.logger(`Warning: PostHog MCP analytics failed to publish ${params.request.method} analytics - ${error}`)
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
/** Builds the handler patch that captures one resource method, for either adapter. */
|
|
482
|
+
export function traceResourceRequest(eventType: ResourceEventType, logger: LoggerFn): HandlerPatch {
|
|
483
|
+
return (server, originalHandler, request, extra) =>
|
|
484
|
+
captureResourceRequest({ server, originalHandler, request, extra, eventType, logger })
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
/** Captures a non-tool MCP request without changing its result or error semantics. */
|
|
488
|
+
async function captureResourceRequest(params: TraceRequestParams): Promise<unknown> {
|
|
489
|
+
const { server, originalHandler, request, extra, eventType, logger } = params
|
|
490
|
+
const data = getServerTrackingData(server)
|
|
491
|
+
if (!data) {
|
|
492
|
+
logger(
|
|
493
|
+
'Warning: PostHog MCP analytics is unable to find server tracking data. Please ensure you have called instrument(server, options) before using resources.'
|
|
494
|
+
)
|
|
495
|
+
return await originalHandler(request, extra)
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
const startTime = new Date()
|
|
499
|
+
let preparedEvent: PreparedToolEvent | null = null
|
|
500
|
+
try {
|
|
501
|
+
const sessionId = getSessionId(server, extra)
|
|
502
|
+
const sessionInfo = getSessionInfo(server, data, sessionId)
|
|
503
|
+
const event: McpEvent = {
|
|
504
|
+
sessionId,
|
|
505
|
+
eventType,
|
|
506
|
+
parameters: buildCapturedMcpParameters(request),
|
|
507
|
+
resourceName: eventType === MCPAnalyticsEventType.mcpResourcesRead ? request.params?.uri : undefined,
|
|
508
|
+
timestamp: startTime,
|
|
509
|
+
}
|
|
510
|
+
stampClientIdentity(event, request, extra, server)
|
|
511
|
+
stampTransportIdentity(event, extra)
|
|
512
|
+
const identity = await handleIdentify(server, data, sessionId, request, sessionInfo, extra)
|
|
513
|
+
await applyResolvedMetadata(event, data, request, extra)
|
|
514
|
+
preparedEvent = { event, requestAttribution: withIdentity(sessionInfo, identity) }
|
|
515
|
+
} catch (error) {
|
|
516
|
+
logger(`Warning: PostHog MCP analytics could not prepare ${request.method} analytics - ${error}`)
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
let result: unknown
|
|
520
|
+
try {
|
|
521
|
+
result = await originalHandler(request, extra)
|
|
522
|
+
} catch (error) {
|
|
523
|
+
publishResourceEvent(server, preparedEvent, startTime, params, { error })
|
|
524
|
+
throw error
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
publishResourceEvent(server, preparedEvent, startTime, params, { result })
|
|
528
|
+
return result
|
|
529
|
+
}
|
|
530
|
+
|
|
398
531
|
// --- tools/list -----------------------------------------------------------
|
|
399
532
|
|
|
400
533
|
/**
|
|
@@ -508,26 +641,31 @@ export function patchRequestHandlers(server: MCPServerLike, patches: Record<stri
|
|
|
508
641
|
}
|
|
509
642
|
|
|
510
643
|
/**
|
|
511
|
-
* Ownership for
|
|
512
|
-
* `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.
|
|
513
646
|
*
|
|
514
|
-
*
|
|
515
|
-
*
|
|
516
|
-
*
|
|
517
|
-
*
|
|
518
|
-
*
|
|
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.
|
|
519
652
|
*
|
|
520
653
|
* `conversation_id` still comes from the cache on purpose: resolving it here too
|
|
521
654
|
* would start minting a handle, and appending its prompt-back block, on
|
|
522
655
|
* instances that today mint none. That changes session anchoring (ADR-0004)
|
|
523
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`.
|
|
524
661
|
*/
|
|
525
662
|
export function getVirtualToolParameterOwnership(
|
|
526
663
|
data: MCPAnalyticsData,
|
|
527
|
-
toolName: string
|
|
664
|
+
toolName: string,
|
|
665
|
+
virtualToolInputSchema: unknown
|
|
528
666
|
): AnalyticsParameterOwnership {
|
|
529
667
|
return {
|
|
530
|
-
...getAnalyticsParameterOwnership(
|
|
668
|
+
...getAnalyticsParameterOwnership(virtualToolInputSchema),
|
|
531
669
|
conversationId: data.toolAnalyticsParameterOwnership.get(toolName)?.conversationId === true,
|
|
532
670
|
}
|
|
533
671
|
}
|
|
@@ -703,6 +841,21 @@ async function getTracedToolsList(
|
|
|
703
841
|
}
|
|
704
842
|
}
|
|
705
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
|
+
|
|
706
859
|
// After the virtual tool is appended, so it advertises `llm_model` too.
|
|
707
860
|
// Its ownership was cached above from the un-injected descriptor, so the
|
|
708
861
|
// SDK already strips and captures the argument on its calls — without
|
|
@@ -226,15 +226,26 @@ export async function resolveEventProperties(
|
|
|
226
226
|
}
|
|
227
227
|
}
|
|
228
228
|
|
|
229
|
+
/**
|
|
230
|
+
* Names what a request is about, for `$identify`. `resources/read` addresses its
|
|
231
|
+
* subject by `uri` rather than `name`, so an identify published from a read used
|
|
232
|
+
* to report `Unknown`.
|
|
233
|
+
*/
|
|
229
234
|
function getRequestResourceName(request: unknown): string {
|
|
230
235
|
if (!request || typeof request !== 'object' || !('params' in request)) {
|
|
231
236
|
return 'Unknown'
|
|
232
237
|
}
|
|
233
238
|
|
|
234
239
|
const params = request.params
|
|
235
|
-
if (!params || typeof params !== 'object'
|
|
240
|
+
if (!params || typeof params !== 'object') {
|
|
236
241
|
return 'Unknown'
|
|
237
242
|
}
|
|
238
243
|
|
|
239
|
-
|
|
244
|
+
for (const key of ['name', 'uri'] as const) {
|
|
245
|
+
const value = (params as Record<string, unknown>)[key]
|
|
246
|
+
if (typeof value === 'string') {
|
|
247
|
+
return value
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
return 'Unknown'
|
|
240
251
|
}
|