@posthog/mcp 0.3.0 → 0.4.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.
Files changed (44) hide show
  1. package/dist/extensions/conversation-id.d.ts +2 -2
  2. package/dist/extensions/conversation-id.d.ts.map +1 -1
  3. package/dist/extensions/conversation-id.js +4 -4
  4. package/dist/extensions/conversation-id.mjs +4 -4
  5. package/dist/extensions/instrument-highlevel.d.ts.map +1 -1
  6. package/dist/extensions/instrument-highlevel.js +4 -3
  7. package/dist/extensions/instrument-highlevel.mjs +5 -4
  8. package/dist/extensions/instrument-lowlevel.js +1 -1
  9. package/dist/extensions/instrument-lowlevel.mjs +2 -2
  10. package/dist/extensions/instrumentation.d.ts.map +1 -1
  11. package/dist/extensions/instrumentation.js +5 -4
  12. package/dist/extensions/instrumentation.mjs +6 -5
  13. package/dist/extensions/posthog-mcp.d.ts +80 -2
  14. package/dist/extensions/posthog-mcp.d.ts.map +1 -1
  15. package/dist/extensions/posthog-mcp.js +57 -3
  16. package/dist/extensions/posthog-mcp.mjs +57 -3
  17. package/dist/extensions/sanitization.d.ts +3 -1
  18. package/dist/extensions/sanitization.d.ts.map +1 -1
  19. package/dist/extensions/sanitization.js +1 -0
  20. package/dist/extensions/sanitization.mjs +1 -0
  21. package/dist/extensions/tools.d.ts +16 -1
  22. package/dist/extensions/tools.d.ts.map +1 -1
  23. package/dist/extensions/tools.js +18 -6
  24. package/dist/extensions/tools.mjs +11 -5
  25. package/dist/index.d.ts +5 -2
  26. package/dist/index.d.ts.map +1 -1
  27. package/dist/index.js +14 -3
  28. package/dist/index.mjs +3 -1
  29. package/dist/types.d.ts +78 -0
  30. package/dist/types.d.ts.map +1 -1
  31. package/dist/version.d.ts +1 -1
  32. package/dist/version.js +1 -1
  33. package/dist/version.mjs +1 -1
  34. package/package.json +3 -3
  35. package/src/extensions/conversation-id.ts +8 -4
  36. package/src/extensions/instrument-highlevel.ts +7 -4
  37. package/src/extensions/instrument-lowlevel.ts +2 -2
  38. package/src/extensions/instrumentation.ts +7 -5
  39. package/src/extensions/posthog-mcp.ts +168 -2
  40. package/src/extensions/sanitization.ts +10 -1
  41. package/src/extensions/tools.ts +23 -5
  42. package/src/index.ts +11 -1
  43. package/src/types.ts +82 -0
  44. package/src/version.ts +1 -1
@@ -1,10 +1,41 @@
1
- import { PostHog } from 'posthog-node'
1
+ import { PostHog, type PostHogOptions } from 'posthog-node'
2
2
 
3
- import type { InitializeCaptureData, JsonRecord, McpCaptureCommon, McpEvent, ToolCallCaptureData } from '../types'
3
+ import type {
4
+ InitializeCaptureData,
5
+ JsonRecord,
6
+ McpCaptureCommon,
7
+ McpEvent,
8
+ MissingCapabilityCaptureData,
9
+ PreparedToolCall,
10
+ PrepareToolListOptions,
11
+ ToolCallCaptureData,
12
+ ToolsListCaptureData,
13
+ } from '../types'
14
+ import {
15
+ addContextParameterToTools,
16
+ getContextDescription,
17
+ isContextEnabled,
18
+ type ContextInjectableTool,
19
+ } from './context-parameters'
4
20
  import { MCPAnalyticsEventType } from './event-types'
5
21
  import { captureException } from './exceptions'
6
22
  import { log } from './logger'
7
23
  import { McpEventSink } from './sink'
24
+ import { GET_MORE_TOOLS_NAME, getReportMissingToolDescriptor } from './tools'
25
+
26
+ /**
27
+ * Options for {@link PostHogMCP}. A superset of `posthog-node`'s options, plus
28
+ * MCP-specific knobs.
29
+ */
30
+ export interface PostHogMCPOptions extends PostHogOptions {
31
+ /**
32
+ * Name of the virtual "report a missing capability" tool injected by
33
+ * {@link PostHogMCP.prepareToolList} and detected by
34
+ * {@link PostHogMCP.prepareToolCall}. Set once here so injection and detection
35
+ * can't drift. Defaults to `get_more_tools`.
36
+ */
37
+ missingCapabilityToolName?: string
38
+ }
8
39
 
9
40
  /**
10
41
  * A `posthog-node` client with first-class MCP analytics. Use this when there is
@@ -46,6 +77,15 @@ export class PostHogMCP extends PostHog {
46
77
  // `beforeSend` and batching apply.
47
78
  readonly #sink = new McpEventSink(this)
48
79
 
80
+ // The get_more_tools name lives here (not on the per-call options) so that
81
+ // prepareToolList (inject) and prepareToolCall (detect) always agree.
82
+ readonly #missingCapabilityToolName: string
83
+
84
+ constructor(apiKey: string, options: PostHogMCPOptions = {}) {
85
+ super(apiKey, options)
86
+ this.#missingCapabilityToolName = options.missingCapabilityToolName ?? GET_MORE_TOOLS_NAME
87
+ }
88
+
49
89
  /** Capture a tool invocation. Emits `$mcp_tool_call` (+ an `$exception` sibling on error). */
50
90
  captureToolCall(data: ToolCallCaptureData): void {
51
91
  const event = baseEvent(MCPAnalyticsEventType.mcpToolsCall, data)
@@ -56,6 +96,7 @@ export class PostHogMCP extends PostHog {
56
96
  event.response = data.response
57
97
  event.duration = data.durationMs
58
98
  event.isError = data.isError
99
+ applyIntent(event, data.intent, data.intentSource)
59
100
  if (data.isError) {
60
101
  event.error = captureException(data.error ?? `Tool ${data.toolName} returned an error`)
61
102
  }
@@ -73,6 +114,109 @@ export class PostHogMCP extends PostHog {
73
114
  this.#emit(event)
74
115
  }
75
116
 
117
+ /**
118
+ * Capture a `tools/list` response. Emits `$mcp_tools_list` carrying the
119
+ * advertised tool names (`$mcp_listed_tool_names`), which powers
120
+ * "advertised but never called" analysis. Pass the names you're about to
121
+ * return — typically the result of {@link prepareToolList}.
122
+ */
123
+ captureToolsList(data: ToolsListCaptureData): void {
124
+ const event = baseEvent(MCPAnalyticsEventType.mcpToolsList, data)
125
+ event.listedToolNames = data.toolNames
126
+ event.parameters = data.parameters
127
+ event.response = data.response
128
+ event.duration = data.durationMs
129
+ event.isError = data.isError
130
+ if (data.isError) {
131
+ event.error = captureException(data.error ?? 'tools/list failed')
132
+ }
133
+ this.#emit(event)
134
+ }
135
+
136
+ /**
137
+ * Decorate your `tools/list` response with PostHog's analytics affordances:
138
+ * injects the `context` argument into every tool (so agents state their intent,
139
+ * captured as `$mcp_intent`) and, when `reportMissing` is on, appends the
140
+ * `get_more_tools` virtual tool (rename it via the `missingCapabilityToolName`
141
+ * constructor option). Returns a new array; your tools are untouched.
142
+ *
143
+ * The appended `get_more_tools` descriptor carries only the base MCP tool fields
144
+ * (name, description, input schema) — not any framework-specific fields your
145
+ * `TTool` may add (e.g. a `handler`). It is meant to be detected via
146
+ * {@link prepareToolCall}'s `isMissingCapability`, not dispatched through a handler.
147
+ *
148
+ * Call this when there is no `Server` to wrap — it does for a custom dispatcher
149
+ * what `instrument()` does for a `Server`. Pair it with {@link prepareToolCall}
150
+ * on the inbound side.
151
+ *
152
+ * @example
153
+ * ```ts
154
+ * // building your tools/list response
155
+ * return { tools: posthog.prepareToolList(myTools, { reportMissing: true }) }
156
+ * ```
157
+ */
158
+ prepareToolList<TTool extends ContextInjectableTool>(tools: TTool[], options: PrepareToolListOptions = {}): TTool[] {
159
+ const contextOption = options.context ?? true
160
+ let prepared = isContextEnabled(contextOption)
161
+ ? addContextParameterToTools(tools, getContextDescription(contextOption))
162
+ : [...tools]
163
+
164
+ if (options.reportMissing && !prepared.some((tool) => tool?.name === this.#missingCapabilityToolName)) {
165
+ prepared = [...prepared, getReportMissingToolDescriptor(this.#missingCapabilityToolName) as TTool]
166
+ }
167
+ return prepared
168
+ }
169
+
170
+ /**
171
+ * Read an incoming `tools/call` before you dispatch it: pulls the agent's
172
+ * intent off the injected `context` argument, strips `context` from the
173
+ * arguments (so your handler and its schema validation never see it), and flags
174
+ * whether the call targeted the `get_more_tools` virtual tool.
175
+ *
176
+ * Pass the returned `intent` / `intentSource` to {@link captureToolCall}, and
177
+ * dispatch the returned `args` to your tool.
178
+ *
179
+ * This only extracts the explicit `context` argument (`intentSource:
180
+ * 'context_parameter'`); it does not infer intent. If you run your own
181
+ * inference, pass that string with `intentSource: 'inferred'` straight to
182
+ * {@link captureToolCall} (the `instrument()` path's `intentFallback`
183
+ * equivalent).
184
+ *
185
+ * @example
186
+ * ```ts
187
+ * const { intent, intentSource, args, isMissingCapability } = posthog.prepareToolCall(name, rawArgs)
188
+ * if (isMissingCapability) {
189
+ * posthog.captureMissingCapability({ context: intent, ...identity })
190
+ * return getMoreToolsResult()
191
+ * }
192
+ * const result = await runTool(name, args)
193
+ * posthog.captureToolCall({ toolName: name, intent, intentSource, ...identity })
194
+ * ```
195
+ */
196
+ prepareToolCall(name: string, args?: Record<string, unknown>): PreparedToolCall {
197
+ const rawContext = args?.context
198
+ const intent = typeof rawContext === 'string' && rawContext.trim() ? rawContext.trim() : undefined
199
+ return {
200
+ intent,
201
+ intentSource: intent ? 'context_parameter' : undefined,
202
+ args: stripContext(args),
203
+ isMissingCapability: name === this.#missingCapabilityToolName,
204
+ }
205
+ }
206
+
207
+ /**
208
+ * Capture a `get_more_tools` call as a missing-capability report. Emits
209
+ * `$mcp_missing_capability` with the agent's description as `$mcp_intent`. Reply
210
+ * to the agent with `getMoreToolsResult()`.
211
+ */
212
+ captureMissingCapability(data: MissingCapabilityCaptureData): void {
213
+ const event = baseEvent(MCPAnalyticsEventType.mcpMissingCapability, data)
214
+ event.resourceName = this.#missingCapabilityToolName
215
+ event.parameters = data.parameters
216
+ applyIntent(event, data.context, 'context_parameter')
217
+ this.#emit(event)
218
+ }
219
+
76
220
  /**
77
221
  * Fire-and-forget, mirroring posthog-node's `capture()`: the event is enqueued
78
222
  * on the client, not awaited. Never throws — a failure to record analytics
@@ -106,3 +250,25 @@ function baseEvent(eventType: MCPAnalyticsEventType, common: McpCaptureCommon):
106
250
  }
107
251
  return event
108
252
  }
253
+
254
+ /**
255
+ * Set the agent intent on an event → `$mcp_intent` / `$mcp_intent_source`. No-op
256
+ * for blank intents, so a missing `context` argument simply leaves them off.
257
+ */
258
+ function applyIntent(event: McpEvent, intent: string | undefined, source: McpEvent['userIntentSource']): void {
259
+ const trimmed = typeof intent === 'string' ? intent.trim() : ''
260
+ if (!trimmed) {
261
+ return
262
+ }
263
+ event.userIntent = trimmed
264
+ event.userIntentSource = source ?? 'context_parameter'
265
+ }
266
+
267
+ /** Return a shallow copy of the arguments with the injected `context` key removed. */
268
+ function stripContext(args: Record<string, unknown> | undefined): Record<string, unknown> | undefined {
269
+ if (!args || !('context' in args)) {
270
+ return args
271
+ }
272
+ const { context: _context, ...rest } = args
273
+ return rest
274
+ }
@@ -13,7 +13,9 @@ function isRecord(value: unknown): value is SanitizedRecord {
13
13
 
14
14
  /**
15
15
  * Sanitizes an event by redacting non-text content blocks from responses
16
- * and large base64-encoded strings from parameters.
16
+ * and large base64-encoded strings from parameters, and applying the same
17
+ * string redaction (PostHog tokens, base64 blobs, sensitive keys) to the
18
+ * agent-supplied intent.
17
19
  *
18
20
  * This is a synchronous operation that returns a new object without mutating the original.
19
21
  * It should run after customer redaction in the event pipeline.
@@ -29,6 +31,13 @@ export function sanitizeEvent<T extends Event | McpEvent>(event: T): T {
29
31
  result.parameters = sanitizeParameters(result.parameters)
30
32
  }
31
33
 
34
+ // The intent comes straight from an agent-narrated `context` string, so it
35
+ // can contain a secret the LLM read aloud. Redact it like any other captured
36
+ // value rather than shipping it raw as `$mcp_intent`.
37
+ if (result.userIntent != null) {
38
+ result.userIntent = sanitizeCapturedValue(result.userIntent) as string
39
+ }
40
+
32
41
  return result
33
42
  }
34
43
 
@@ -7,11 +7,20 @@ import { log } from './logger'
7
7
 
8
8
  export const GET_MORE_TOOLS_NAME = 'get_more_tools' as const
9
9
 
10
+ /**
11
+ * The configured name of the `get_more_tools` virtual tool, falling back to the
12
+ * default. Resolve through here everywhere (inject + detect) so a custom name
13
+ * can't drift between call sites.
14
+ */
15
+ export function resolveMissingCapabilityToolName(options?: { missingCapabilityToolName?: string }): string {
16
+ return options?.missingCapabilityToolName ?? GET_MORE_TOOLS_NAME
17
+ }
18
+
10
19
  type ReportMissingToolDescriptor = ListToolsResult['tools'][number]
11
20
 
12
- export function getReportMissingToolDescriptor(): ReportMissingToolDescriptor {
21
+ export function getReportMissingToolDescriptor(name: string = GET_MORE_TOOLS_NAME): ReportMissingToolDescriptor {
13
22
  return {
14
- name: GET_MORE_TOOLS_NAME,
23
+ name,
15
24
  description:
16
25
  'Check for additional tools whenever your task might benefit from specialized capabilities - even if existing tools could work as a fallback.',
17
26
  inputSchema: {
@@ -40,9 +49,13 @@ export function getReportMissingToolDescriptor(): ReportMissingToolDescriptor {
40
49
  }
41
50
  }
42
51
 
43
- export function handleReportMissing(args: { context: string }): CallToolResult {
44
- log(`Missing tool reported: ${JSON.stringify(args)}`)
45
-
52
+ /**
53
+ * The canned acknowledgement returned to the agent after it calls
54
+ * `get_more_tools`. Reply with this from your dispatcher so the agent knows the
55
+ * report was recorded (custom dispatcher path); the `instrument()` path returns
56
+ * it automatically.
57
+ */
58
+ export function getMoreToolsResult(): CallToolResult {
46
59
  return {
47
60
  content: [
48
61
  {
@@ -52,3 +65,8 @@ export function handleReportMissing(args: { context: string }): CallToolResult {
52
65
  ],
53
66
  }
54
67
  }
68
+
69
+ export function handleReportMissing(args: { context: string }): CallToolResult {
70
+ log(`Missing tool reported: ${JSON.stringify(args)}`)
71
+ return getMoreToolsResult()
72
+ }
package/src/index.ts CHANGED
@@ -166,7 +166,13 @@ export {
166
166
  PostHogMCPAnalyticsEvent,
167
167
  PostHogMCPAnalyticsProperty,
168
168
  } from './extensions/constants'
169
- export { PostHogMCP } from './extensions/posthog-mcp'
169
+ export { PostHogMCP, type PostHogMCPOptions } from './extensions/posthog-mcp'
170
+ export { getMoreToolsResult } from './extensions/tools'
171
+ export { setLogger } from './extensions/logger'
172
+ // Re-export the posthog-node client so a single import works:
173
+ // import { PostHog, instrument } from "@posthog/mcp"
174
+ // posthog-node stays a peer dependency, so this resolves the host app's installed copy.
175
+ export { PostHog, type PostHogOptions } from 'posthog-node'
170
176
  export type {
171
177
  BeforeSendFn,
172
178
  CaptureEventData,
@@ -176,7 +182,11 @@ export type {
176
182
  MCPAnalyticsContextOptions,
177
183
  MCPAnalyticsIntentSource,
178
184
  MCPAnalyticsOptions,
185
+ MissingCapabilityCaptureData,
186
+ PreparedToolCall,
187
+ PrepareToolListOptions,
179
188
  ToolCallCaptureData,
189
+ ToolsListCaptureData,
180
190
  UserIdentity,
181
191
  } from './types'
182
192
  export type IdentifyFunction = MCPAnalyticsOptions['identify']
package/src/types.ts CHANGED
@@ -50,6 +50,12 @@ export interface MCPAnalyticsOptions {
50
50
  logger?: LoggerFn
51
51
  /** Enable the `get_more_tools` virtual tool so agents can report missing functionality. */
52
52
  reportMissing?: boolean
53
+ /**
54
+ * Rename the `get_more_tools` virtual tool (the `reportMissing` feature).
55
+ * Defaults to `get_more_tools`. Set once here so the tool is advertised and
56
+ * detected under the same name.
57
+ */
58
+ missingCapabilityToolName?: string
53
59
  /** Enables the `conversation_id` tool parameter + prompt-back loop. */
54
60
  enableConversationId?: boolean
55
61
  /**
@@ -283,6 +289,19 @@ export interface ToolCallCaptureData extends McpCaptureCommon {
283
289
  toolDescription?: string
284
290
  /** Product category the tool belongs to (e.g. "Logs") → `$mcp_tool_category`. */
285
291
  category?: string
292
+ /**
293
+ * The agent's stated intent for this call → `$mcp_intent`. On the custom
294
+ * dispatcher path, read it off the incoming call with
295
+ * {@link PostHogMCP.prepareToolCall} (which pulls the injected `context`
296
+ * argument). Omitted when no intent was captured.
297
+ */
298
+ intent?: string
299
+ /**
300
+ * Where {@link ToolCallCaptureData.intent} came from → `$mcp_intent_source`.
301
+ * `context_parameter` when the client supplied it on the call, `inferred` when
302
+ * the host derived it. Defaults to `context_parameter` when an intent is set.
303
+ */
304
+ intentSource?: MCPAnalyticsIntentSource
286
305
  /** Captured call arguments → `$mcp_parameters` (sanitized + truncated). */
287
306
  parameters?: unknown
288
307
  /** Captured tool result → `$mcp_response` (sanitized + truncated). */
@@ -313,3 +332,66 @@ export interface InitializeCaptureData extends McpCaptureCommon {
313
332
  /** Wall-clock duration → `$mcp_duration_ms`. */
314
333
  durationMs?: number
315
334
  }
335
+
336
+ /** Payload for {@link PostHogMCP.captureToolsList}. Emits `$mcp_tools_list`. */
337
+ export interface ToolsListCaptureData extends McpCaptureCommon {
338
+ /**
339
+ * Names of the tools advertised in this `tools/list` response →
340
+ * `$mcp_listed_tool_names`. Powers "advertised but never called" analysis.
341
+ */
342
+ toolNames?: string[]
343
+ /** Captured request params → `$mcp_parameters` (sanitized + truncated). */
344
+ parameters?: unknown
345
+ /** Captured listing result → `$mcp_response` (sanitized + truncated). */
346
+ response?: unknown
347
+ /** Wall-clock duration → `$mcp_duration_ms`. */
348
+ durationMs?: number
349
+ /** Whether building the listing failed → `$mcp_is_error`. */
350
+ isError?: boolean
351
+ /** The thrown value when `isError` is true → fans out an `$exception` sibling. */
352
+ error?: unknown
353
+ }
354
+
355
+ /** Options for {@link PostHogMCP.prepareToolList}. */
356
+ export interface PrepareToolListOptions {
357
+ /**
358
+ * Inject the `context` argument into every tool so agents state their intent
359
+ * (captured as `$mcp_intent`). `true` (default) uses the standard prompt;
360
+ * pass `{ description }` to customize it, or `false` to skip injection.
361
+ */
362
+ context?: boolean | MCPAnalyticsContextOptions
363
+ /**
364
+ * Append the `get_more_tools` virtual tool so agents can report a missing
365
+ * capability. Defaults to `false`. When the agent calls it, route the call to
366
+ * {@link PostHogMCP.captureMissingCapability} and reply with `getMoreToolsResult()`.
367
+ */
368
+ reportMissing?: boolean
369
+ }
370
+
371
+ /**
372
+ * Result of {@link PostHogMCP.prepareToolCall}: the intent pulled off the
373
+ * incoming call, the arguments with the injected `context` removed (so your tool
374
+ * handler and its schema validation never see it), and whether the call targeted
375
+ * the `get_more_tools` virtual tool.
376
+ */
377
+ export interface PreparedToolCall {
378
+ /** The agent's stated intent (the `context` argument), if present. */
379
+ intent?: string
380
+ /** Where the intent came from. Always `context_parameter` here when set. */
381
+ intentSource?: MCPAnalyticsIntentSource
382
+ /** The call arguments with the injected `context` key stripped out. */
383
+ args?: Record<string, unknown>
384
+ /** True when `name` is the `get_more_tools` virtual tool. */
385
+ isMissingCapability: boolean
386
+ }
387
+
388
+ /** Payload for {@link PostHogMCP.captureMissingCapability}. Emits `$mcp_missing_capability`. */
389
+ export interface MissingCapabilityCaptureData extends McpCaptureCommon {
390
+ /**
391
+ * The agent's description of the capability it wanted (the `context` argument
392
+ * on the `get_more_tools` call) → `$mcp_intent`.
393
+ */
394
+ context?: string
395
+ /** Captured call arguments → `$mcp_parameters` (sanitized + truncated). */
396
+ parameters?: unknown
397
+ }
package/src/version.ts CHANGED
@@ -1 +1 @@
1
- export const version = '0.3.0'
1
+ export const version = '0.4.1'