@posthog/mcp 0.18.0 → 0.19.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.
Files changed (42) hide show
  1. package/dist/extensions/constants.d.ts +3 -1
  2. package/dist/extensions/constants.d.ts.map +1 -1
  3. package/dist/extensions/constants.js +3 -1
  4. package/dist/extensions/constants.mjs +3 -1
  5. package/dist/extensions/instrument-highlevel.d.ts.map +1 -1
  6. package/dist/extensions/instrument-highlevel.js +1 -0
  7. package/dist/extensions/instrument-highlevel.mjs +1 -0
  8. package/dist/extensions/instrumentation.d.ts +1 -0
  9. package/dist/extensions/instrumentation.d.ts.map +1 -1
  10. package/dist/extensions/instrumentation.js +27 -2
  11. package/dist/extensions/instrumentation.mjs +28 -3
  12. package/dist/extensions/internal.d.ts +9 -8
  13. package/dist/extensions/internal.d.ts.map +1 -1
  14. package/dist/extensions/internal.js +17 -12
  15. package/dist/extensions/internal.mjs +15 -13
  16. package/dist/extensions/mcp-sdk-compat.d.ts +7 -0
  17. package/dist/extensions/mcp-sdk-compat.d.ts.map +1 -1
  18. package/dist/extensions/mcp-sdk-compat.js +21 -3
  19. package/dist/extensions/mcp-sdk-compat.mjs +17 -2
  20. package/dist/extensions/tool-input.d.ts +9 -0
  21. package/dist/extensions/tool-input.d.ts.map +1 -0
  22. package/dist/extensions/tool-input.js +117 -0
  23. package/dist/extensions/tool-input.mjs +79 -0
  24. package/dist/index.d.ts +2 -1
  25. package/dist/index.d.ts.map +1 -1
  26. package/dist/index.js +5 -0
  27. package/dist/index.mjs +3 -1
  28. package/dist/types.d.ts +31 -1
  29. package/dist/types.d.ts.map +1 -1
  30. package/dist/version.d.ts +1 -1
  31. package/dist/version.js +1 -1
  32. package/dist/version.mjs +1 -1
  33. package/package.json +3 -3
  34. package/src/extensions/constants.ts +3 -1
  35. package/src/extensions/instrument-highlevel.ts +1 -0
  36. package/src/extensions/instrumentation.ts +37 -1
  37. package/src/extensions/internal.ts +18 -17
  38. package/src/extensions/mcp-sdk-compat.ts +38 -1
  39. package/src/extensions/tool-input.ts +92 -0
  40. package/src/index.ts +6 -1
  41. package/src/types.ts +32 -1
  42. package/src/version.ts +1 -1
@@ -109,7 +109,44 @@ function isZodTypeLike(value: unknown): boolean {
109
109
  }
110
110
 
111
111
  export function isZodRawShapeCompat(schema: unknown): schema is Record<string, unknown> {
112
- return !!schema && typeof schema === 'object' && Object.values(schema).some(isZodTypeLike)
112
+ // A Zod v4 pipe exposes its `in` and `out` schemas as own fields, so a Zod schema is never a raw shape
113
+ return !!schema && typeof schema === 'object' && !isZodTypeLike(schema) && Object.values(schema).some(isZodTypeLike)
114
+ }
115
+
116
+ interface ZodWrapperDef {
117
+ type?: unknown
118
+ schema?: unknown
119
+ in?: unknown
120
+ out?: unknown
121
+ innerType?: unknown
122
+ }
123
+
124
+ const MAX_UNWRAP_DEPTH = 8
125
+
126
+ function zodDef(schema: unknown): ZodWrapperDef | undefined {
127
+ return (isZ4Schema(schema) ? (schema as ZodV4Internal)._zod?.def : (schema as ZodV3Internal)._def) as
128
+ | ZodWrapperDef
129
+ | undefined
130
+ }
131
+
132
+ /**
133
+ * Follows Zod wrappers to the schema that parses the caller's input: v3 effects
134
+ * (refine, transform, preprocess) and pipelines, v4 pipes (which include
135
+ * transforms), and optional, nullable, default, catch, and readonly wrappers.
136
+ * A v4 `z.preprocess` is a pipe whose input side is the transform, so its output side is followed.
137
+ */
138
+ export function unwrapInputSchema(schema: unknown): unknown {
139
+ let current = schema
140
+ for (let depth = 0; depth < MAX_UNWRAP_DEPTH && isZodTypeLike(current); depth++) {
141
+ const def = zodDef(current)
142
+ const pipeInput = def?.in !== undefined && zodDef(def.in)?.type === 'transform' ? def.out : def?.in
143
+ const inner = def?.schema ?? pipeInput ?? def?.innerType
144
+ if (!inner) {
145
+ break
146
+ }
147
+ current = inner
148
+ }
149
+ return current
113
150
  }
114
151
 
115
152
  export function getObjectShape(schema: unknown): Record<string, unknown> | undefined {
@@ -0,0 +1,92 @@
1
+ import type { InputAliasMap, JsonRecord, ShouldRecordInputKeyFn, ToolInputOptions } from '../types'
2
+ import { PostHogMCPAnalyticsProperty } from './constants'
3
+ import { getObjectShape, isZodRawShapeCompat, unwrapInputSchema } from './mcp-sdk-compat'
4
+
5
+ const MAX_INPUT_KEYS = 20
6
+ const MAX_KEY_LENGTH = 64
7
+ const ANALYTICS_KEYS = new Set(['context', 'llm_model', 'conversation_id'])
8
+
9
+ function isRecord(value: unknown): value is Record<string, unknown> {
10
+ return value !== null && typeof value === 'object' && !Array.isArray(value)
11
+ }
12
+
13
+ function declaredProperties(schema: unknown): Record<string, unknown> | undefined {
14
+ if (!isRecord(schema)) return undefined
15
+ const properties = schema.properties
16
+ if (isZodRawShapeCompat(schema)) return schema
17
+ return getObjectShape(unwrapInputSchema(schema)) ?? (isRecord(properties) ? properties : undefined)
18
+ }
19
+
20
+ const recordDeclaredOnly: ShouldRecordInputKeyFn = (_key, { declared }) => declared
21
+
22
+ function shouldRecord(fn: ShouldRecordInputKeyFn, key: string, declared: boolean): boolean {
23
+ try {
24
+ return fn(key, { declared }) === true
25
+ } catch {
26
+ return false
27
+ }
28
+ }
29
+
30
+ function aliasNames(aliases: InputAliasMap | undefined): string[] {
31
+ if (!isRecord(aliases)) return []
32
+ return Object.values(aliases).flatMap((names) =>
33
+ Array.isArray(names) ? names.filter((name): name is string => typeof name === 'string') : []
34
+ )
35
+ }
36
+
37
+ /**
38
+ * Each alias the server needed: the canonical name is absent and this is the first of its
39
+ * aliases present, the same order a normalizer that fills the canonical from its aliases uses.
40
+ */
41
+ function describeAliasesUsed(aliases: InputAliasMap | undefined, input: Record<string, unknown>): string[] {
42
+ if (!isRecord(aliases)) return []
43
+ const used: string[] = []
44
+ for (const [canonical, names] of Object.entries(aliases)) {
45
+ if (!Array.isArray(names) || Object.prototype.hasOwnProperty.call(input, canonical)) continue
46
+ const alias = names.find((name) => typeof name === 'string' && Object.prototype.hasOwnProperty.call(input, name))
47
+ if (alias && alias.length <= MAX_KEY_LENGTH && canonical.length <= MAX_KEY_LENGTH) {
48
+ used.push(`${alias}:${canonical}`)
49
+ }
50
+ }
51
+ return used.sort().slice(0, MAX_INPUT_KEYS)
52
+ }
53
+
54
+ /**
55
+ * Describe the original arguments without reading their values.
56
+ * Pass a server-owned JSON Schema or Zod object schema, never a schema from the caller.
57
+ * By default unknown names become `[redacted]` because an argument name can contain private data;
58
+ * `shouldRecordInputKey` replaces that rule.
59
+ */
60
+ export function getToolInputProperties(input: unknown, inputSchema?: unknown, options?: ToolInputOptions): JsonRecord {
61
+ try {
62
+ if (!isRecord(input)) return {}
63
+ const prototype = Object.getPrototypeOf(input)
64
+ if (prototype !== null && prototype !== Object.prototype) return {}
65
+ const properties = declaredProperties(inputSchema)
66
+ const known = new Set([...Object.keys(properties ?? {}), ...aliasNames(options?.inputAliases)])
67
+ const keys = Object.keys(input).filter((key) => known.has(key) || !ANALYTICS_KEYS.has(key))
68
+ const record = options?.shouldRecordInputKey ?? recordDeclaredOnly
69
+ const declared: string[] = []
70
+ const undeclared: string[] = []
71
+ let hasRedacted = false
72
+ for (const key of keys) {
73
+ const isDeclared = known.has(key)
74
+ if (key.length <= MAX_KEY_LENGTH && shouldRecord(record, key, isDeclared)) {
75
+ ;(isDeclared ? declared : undeclared).push(key)
76
+ } else {
77
+ hasRedacted = true
78
+ }
79
+ }
80
+ const visibleKeys = [...declared.sort(), ...undeclared.sort()].slice(0, MAX_INPUT_KEYS)
81
+ if (hasRedacted && visibleKeys.length < MAX_INPUT_KEYS) {
82
+ visibleKeys.push('[redacted]')
83
+ }
84
+ const aliasesUsed = describeAliasesUsed(options?.inputAliases, input)
85
+ return {
86
+ [PostHogMCPAnalyticsProperty.InputKeys]: visibleKeys,
87
+ ...(aliasesUsed.length > 0 ? { [PostHogMCPAnalyticsProperty.InputAliasesUsed]: aliasesUsed } : {}),
88
+ }
89
+ } catch {
90
+ return {}
91
+ }
92
+ }
package/src/index.ts CHANGED
@@ -7,7 +7,7 @@ import type { PostHog } from 'posthog-node'
7
7
  import { isCompatibleServerType, isHighLevelServer } from './extensions/compatibility'
8
8
  import { McpEventSink } from './extensions/sink'
9
9
  import { MCPAnalyticsEventType } from './extensions/event-types'
10
- import { IdentityCache, getServerTrackingData, setServerTrackingData } from './extensions/internal'
10
+ import { BoundedCache, IdentityCache, getServerTrackingData, setServerTrackingData } from './extensions/internal'
11
11
  import { createLogger } from './extensions/logger'
12
12
  import { captureEvent } from './extensions/capture'
13
13
  import { applyMcpLibIdentity } from './extensions/lib-identity'
@@ -161,6 +161,7 @@ function buildTrackingData(
161
161
  toolAnalyticsParameterOwnership: new Map(),
162
162
  toolCategories: new Map<string, string>(),
163
163
  toolDescriptions: new Map<string, string>(),
164
+ toolInputSchemas: new BoundedCache(),
164
165
  sessionInfo: getSessionInfo(lowLevelServer, undefined),
165
166
  options: {
166
167
  ...DEFAULT_OPTIONS,
@@ -230,6 +231,7 @@ export {
230
231
  // Host callbacks receive the SDK's `extra`/`ctx` unchanged, and the two SDK
231
232
  // majors carry HTTP headers in different places and shapes. This reads either.
232
233
  export { getRequestHeaders } from './extensions/request-headers'
234
+ export { getToolInputProperties } from './extensions/tool-input'
233
235
  export { PostHogMCP, type PostHogMCPOptions } from './extensions/posthog-mcp'
234
236
  export { getMoreToolsResult } from './extensions/tools'
235
237
  export { sendFeedbackResult, SEND_FEEDBACK_TOOL_NAME } from './extensions/feedback'
@@ -249,6 +251,7 @@ export type {
249
251
  CaptureEventData,
250
252
  CollectFeedbackConfig,
251
253
  InitializeCaptureData,
254
+ InputAliasMap,
252
255
  McpAnalytics,
253
256
  McpCaptureCommon,
254
257
  MCPAnalyticsContextOptions,
@@ -262,7 +265,9 @@ export type {
262
265
  PrepareToolCallOptions,
263
266
  PrepareToolListOptions,
264
267
  RequestHeaderBag,
268
+ ShouldRecordInputKeyFn,
265
269
  ToolCallCaptureData,
270
+ ToolInputOptions,
266
271
  ToolsListCaptureData,
267
272
  UserIdentity,
268
273
  } from './types'
package/src/types.ts CHANGED
@@ -6,7 +6,7 @@
6
6
  import type { ErrorTracking } from '@posthog/core'
7
7
  import type { AnalyticsInjectableJsonSchema } from './extensions/analytics-parameters'
8
8
  import type { MCPAnalyticsEventType } from './extensions/event-types'
9
- import type { IdentityCache } from './extensions/internal'
9
+ import type { BoundedCache, IdentityCache } from './extensions/internal'
10
10
  import type { PostHogCaptureEvent } from './extensions/posthog-events'
11
11
  import type { McpEventSink } from './extensions/sink'
12
12
  import type { LoggerFn } from './extensions/logger'
@@ -186,6 +186,12 @@ export interface MCPAnalyticsOptions {
186
186
  * suppress specific events. A throw drops that event.
187
187
  */
188
188
  beforeSend?: BeforeSendFn
189
+ /**
190
+ * Decide which argument names `$mcp_input_keys` records on tool-call events.
191
+ * By default only names the tool's input schema declares are recorded; every
192
+ * other name becomes one `[redacted]` entry, because a name can carry private data.
193
+ */
194
+ shouldRecordInputKey?: ShouldRecordInputKeyFn
189
195
  /**
190
196
  * Attach extra event properties on every auto-captured event. Spread into the PostHog
191
197
  * event properties as-is; values must be JSON-serializable.
@@ -304,6 +310,30 @@ export type RegisteredTool = {
304
310
  */
305
311
  export type BeforeSendFn = (event: PostHogCaptureEvent) => MaybePromise<PostHogCaptureEvent | null | undefined>
306
312
 
313
+ /**
314
+ * Decides whether one top-level argument name appears in `$mcp_input_keys`.
315
+ * `declared` is true when the server's input schema declares the name.
316
+ * Return `true` to record the name; any other result, or a throw, records `[redacted]`.
317
+ */
318
+ export type ShouldRecordInputKeyFn = (key: string, details: { declared: boolean }) => boolean
319
+
320
+ export interface ToolInputOptions {
321
+ /**
322
+ * Replace the default rule, which records only declared names. The SDK still
323
+ * drops names longer than 64 characters and records at most 20 names.
324
+ */
325
+ shouldRecordInputKey?: ShouldRecordInputKeyFn
326
+ /**
327
+ * The alternative argument names the server accepts, as canonical name to aliases in the
328
+ * order the server tries them, for example `{ id: ['experimentId'] }`. Must be owned by the
329
+ * server, never taken from the caller. Alias names count as declared in `$mcp_input_keys`,
330
+ * and `$mcp_input_aliases_used` records each alias the server needed, as `alias:canonical`.
331
+ */
332
+ inputAliases?: InputAliasMap
333
+ }
334
+
335
+ export type InputAliasMap = Readonly<Record<string, readonly string[]>>
336
+
307
337
  export interface Event {
308
338
  actorId?: string
309
339
  clientName?: string
@@ -515,6 +545,7 @@ export interface MCPAnalyticsData {
515
545
  toolAnalyticsParameterOwnership: Map<string, AnalyticsParameterOwnership>
516
546
  toolCategories: Map<string, string>
517
547
  toolDescriptions: Map<string, string>
548
+ toolInputSchemas: BoundedCache<Map<string, unknown>>
518
549
  }
519
550
 
520
551
  export interface CaptureEventData {
package/src/version.ts CHANGED
@@ -1 +1 @@
1
- export const version = '0.18.0'
1
+ export const version = '0.19.0'