@posthog/mcp 0.11.3 → 0.11.5

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 CHANGED
@@ -73,6 +73,27 @@ Details: [docs/ARCHITECTURE.md §4](./docs/ARCHITECTURE.md).
73
73
  `@modelcontextprotocol/{core,server,client}` v2 — and on both the high-level `McpServer` and the
74
74
  low-level `Server`. Shapes are detected at runtime, so neither major is a dependency here.
75
75
 
76
+ ### Sessions and client identity on a v2 server
77
+
78
+ Protocol revision is a property of each **request**, not of the server: a v2 server serves both
79
+ `2025-11-25` and `2026-07-28` traffic, and the SDK is instrumented once for both.
80
+
81
+ - **On `2026-07-28`** there is no `initialize` and no session header — the revision removed
82
+ protocol-level sessions, and this SDK will not mint one. Session correlation therefore comes from
83
+ `enableConversationId`, which is **opt-in**. Without it every request is its own `$session_id`.
84
+ - **On `2025-11-25`**, the session id and the client's name and version are exchanged once at
85
+ `initialize`. If your server builds a fresh `McpServer` per HTTP request — which
86
+ `createMcpHandler` does by default — the instance serving a later `tools/call` never saw that
87
+ handshake. The SDK bridges it by minting the `Mcp-Session-Id` token described above, which the
88
+ client replays on every request.
89
+
90
+ **That token only reaches the client if the transport builds response headers _after_ the handler
91
+ runs.** `@rekog/mcp-nest` with `enableJsonResponse: true` does; `createMcpHandler`'s legacy leg
92
+ does not, and there is no setting we can reach from inside the server. On that leg, expect
93
+ `$mcp_client_name` and `$mcp_client_version` to be absent for `2025-11-25` traffic — the protocol
94
+ version still arrives, because clients send it on the `MCP-Protocol-Version` header of every
95
+ request.
96
+
76
97
  ### Reading request headers in a callback
77
98
 
78
99
  If your `identify`, `intentFallback`, `eventProperties` or `beforeSend` reads HTTP headers, it has
@@ -95,6 +116,39 @@ identify: async (request, extra) => {
95
116
  }
96
117
  ```
97
118
 
119
+ ### What `$mcp_intent` records, and how to turn it off
120
+
121
+ `context` defaults to **on**: the SDK adds a `context` parameter to every tool it advertises, asks
122
+ the agent to say why it is calling, and records the answer as `$mcp_intent`. It is stripped before
123
+ your tool runs wherever the SDK can confirm the parameter is its own.
124
+
125
+ On a server that builds a fresh instance per HTTP request — `createMcpHandler`, or `@rekog/mcp-nest`
126
+ in its stateless mode — that confirmation is not available: ownership is learned while serving
127
+ `tools/list`, and the instance handling a `tools/call` never served one. There the SDK records the
128
+ argument but does **not** strip it, because deleting an argument your tool declared would cost you
129
+ the call, while an extra key usually costs nothing.
130
+
131
+ What matters is instance lifetime, not statelessness. A server that is stateless at the transport
132
+ (`sessionIdGenerator: undefined`) but keeps one long-lived server object learns ownership from the
133
+ first `tools/list` and keeps it, so none of the above applies to it.
134
+
135
+ The consequence worth knowing: if **your own** tool declares a parameter named `context` and the SDK
136
+ cannot tell that it is yours, its value is recorded as `$mcp_intent`. It never leaves your project,
137
+ and it is capped at 2048 characters. Two ways out, both one line:
138
+
139
+ ```ts
140
+ instrument(server, posthog, { context: false }) // no injection, no capture
141
+
142
+ instrument(server, posthog, { // keep it, drop the property
143
+ beforeSend: (event) => {
144
+ delete event.properties.$mcp_intent
145
+ return event
146
+ },
147
+ })
148
+ ```
149
+
150
+ `intentFallback` is the third option: supply the intent yourself when the agent did not send one.
151
+
98
152
  ### If you switched to `instrument(server.server)`
99
153
 
100
154
  Before v2 support landed, the compatibility gate rejected high-level v2 servers, and the usual
@@ -95,9 +95,7 @@ export declare function isToolAdvertised(server: MCPServerLike, toolName: string
95
95
  * Captures each `tools/list` and injects the SDK-managed tools (context
96
96
  * parameter, conversation id, `get_more_tools`) into the returned list.
97
97
  */
98
- export declare function handleListToolsRequest(server: MCPServerLike, originalListToolsHandler: MCPRequestHandler, request: MCPRequestLike, extra: CompatibleRequestHandlerExtra | undefined, logger: LoggerFn): Promise<{
99
- tools: CompatibleToolsListLike['tools'];
100
- }>;
98
+ export declare function handleListToolsRequest(server: MCPServerLike, originalListToolsHandler: MCPRequestHandler, request: MCPRequestLike, extra: CompatibleRequestHandlerExtra | undefined, logger: LoggerFn): Promise<CompatibleToolsListLike>;
101
99
  export declare function cacheToolDescriptions(cache: Map<string, string>, tools: CompatibleToolsListLike['tools'] | undefined): void;
102
100
  /**
103
101
  * Category declared on a tool's `_meta` block (the MCP spec allows arbitrary
@@ -1 +1 @@
1
- {"version":3,"file":"instrumentation.d.ts","sourceRoot":"","sources":["../../src/extensions/instrumentation.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"instrumentation.d.ts","sourceRoot":"","sources":["../../src/extensions/instrumentation.ts"],"names":[],"mappings":"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"}
@@ -56,10 +56,11 @@ async function captureToolCall(params) {
56
56
  const { server, data, request, extra, execute, parameterOwnership, eventType, explicitContextIntent, takeCapturedError } = params;
57
57
  const resolvedEventType = eventType ?? external_event_types_js_namespaceObject.MCPAnalyticsEventType.mcpToolsCall;
58
58
  const ownership = getActiveAnalyticsParameterOwnership(data, request.params?.name, parameterOwnership, resolvedEventType === external_event_types_js_namespaceObject.MCPAnalyticsEventType.mcpMissingCapability);
59
+ const canCaptureContextIntent = resolvedEventType !== external_event_types_js_namespaceObject.MCPAnalyticsEventType.mcpMissingCapability && (0, external_context_parameters_js_namespaceObject.isContextEnabled)(data.options.context) && (ownership.context || !ownership.contextOwnershipKnown);
59
60
  const conversation = (0, external_conversation_id_js_namespaceObject.resolveConversationId)(ownership.conversationId, request.params?.arguments);
60
61
  const downstreamRequest = cloneRequestWithoutOwnedAnalyticsArguments(request, ownership);
61
62
  const startTime = new Date();
62
- const preparedEvent = await prepareToolCallEvent(server, data, request, downstreamRequest, extra, startTime, conversation, ownership, resolvedEventType);
63
+ const preparedEvent = await prepareToolCallEvent(server, data, request, downstreamRequest, extra, startTime, conversation, ownership, resolvedEventType, canCaptureContextIntent);
63
64
  if (preparedEvent && explicitContextIntent) (0, external_intent_js_namespaceObject.setExplicitContextIntent)(preparedEvent.event, explicitContextIntent);
64
65
  let result;
65
66
  try {
@@ -69,13 +70,14 @@ async function captureToolCall(params) {
69
70
  throw error;
70
71
  }
71
72
  const finalResult = applyConversationInstructions(preparedEvent?.event ?? null, result, conversation, ownership);
72
- publishSuccessfulToolEvent(server, preparedEvent, finalResult, startTime, data.logger, takeCapturedError);
73
+ publishSuccessfulToolEvent(server, preparedEvent, finalResult, startTime, data.logger, takeCapturedError, result);
73
74
  return finalResult;
74
75
  }
75
76
  function getActiveAnalyticsParameterOwnership(data, toolName, override, isMissingCapabilityTool) {
76
77
  const listed = toolName ? data.toolAnalyticsParameterOwnership.get(toolName) : void 0;
77
78
  const ownership = override ?? listed;
78
79
  return {
80
+ contextOwnershipKnown: void 0 !== ownership,
79
81
  context: !isMissingCapabilityTool && (0, external_context_parameters_js_namespaceObject.isContextEnabled)(data.options.context) && ownership?.context === true,
80
82
  conversationId: true === data.options.enableConversationId && ownership?.conversationId === true,
81
83
  outputInstructions: true === data.options.enableConversationId && listed?.outputInstructions === true
@@ -93,7 +95,7 @@ function cloneRequestWithoutOwnedAnalyticsArguments(request, ownership) {
93
95
  }
94
96
  };
95
97
  }
96
- async function prepareToolCallEvent(server, data, request, downstreamRequest, extra, startTime, conversation, ownership, eventType) {
98
+ async function prepareToolCallEvent(server, data, request, downstreamRequest, extra, startTime, conversation, ownership, eventType, canCaptureContextIntent) {
97
99
  try {
98
100
  const sessionId = (0, external_session_js_namespaceObject.getSessionId)(server, extra, conversation.conversationId);
99
101
  const sessionInfo = (0, external_session_js_namespaceObject.getSessionInfo)(server, data, sessionId);
@@ -113,7 +115,7 @@ async function prepareToolCallEvent(server, data, request, downstreamRequest, ex
113
115
  (0, external_client_identity_js_namespaceObject.stampClientIdentity)(event, request, extra, server);
114
116
  (0, external_transport_identity_js_namespaceObject.stampTransportIdentity)(event, extra);
115
117
  await (0, external_tracing_helpers_js_namespaceObject.applyResolvedMetadata)(event, data, request, extra);
116
- (0, external_intent_js_namespaceObject.setEventIntent)(event, await (0, external_intent_js_namespaceObject.resolveToolCallIntent)(data, request, ownership.context, extra));
118
+ (0, external_intent_js_namespaceObject.setEventIntent)(event, await (0, external_intent_js_namespaceObject.resolveToolCallIntent)(data, request, canCaptureContextIntent, extra));
117
119
  return {
118
120
  event,
119
121
  requestAttribution
@@ -140,14 +142,14 @@ function applyConversationInstructions(event, result, conversation, ownership) {
140
142
  if (!delivered && conversation.minted && event) event.conversationId = void 0;
141
143
  return updated;
142
144
  }
143
- function publishSuccessfulToolEvent(server, preparedEvent, result, startTime, logger, takeCapturedError) {
145
+ function publishSuccessfulToolEvent(server, preparedEvent, result, startTime, logger, takeCapturedError, resultBeforeInstructions) {
144
146
  if (!preparedEvent) return;
145
147
  const { event, requestAttribution } = preparedEvent;
146
148
  try {
147
149
  if ((0, external_tracing_helpers_js_namespaceObject.isToolResultError)(result)) {
148
150
  event.isError = true;
149
151
  const capturedError = takeCapturedError?.();
150
- event.error = (0, external_exceptions_js_namespaceObject.captureException)(capturedError ?? result);
152
+ event.error = (0, external_exceptions_js_namespaceObject.captureException)(capturedError ?? resultBeforeInstructions ?? result);
151
153
  } else event.isError = false;
152
154
  event.response = result;
153
155
  event.duration = Date.now() - startTime.getTime();
@@ -223,7 +225,7 @@ async function handleListToolsRequest(server, originalListToolsHandler, request,
223
225
  const data = (0, external_internal_js_namespaceObject.getServerTrackingData)(server);
224
226
  const startTime = new Date();
225
227
  const sessionId = (0, external_session_js_namespaceObject.getSessionId)(server, extra);
226
- const requestAttribution = (0, external_session_js_namespaceObject.getSessionInfo)(server, data, sessionId);
228
+ const sessionInfo = (0, external_session_js_namespaceObject.getSessionInfo)(server, data, sessionId);
227
229
  const event = {
228
230
  sessionId,
229
231
  parameters: (0, external_mcp_payloads_js_namespaceObject.buildCapturedMcpParameters)(request),
@@ -232,13 +234,14 @@ async function handleListToolsRequest(server, originalListToolsHandler, request,
232
234
  };
233
235
  (0, external_client_identity_js_namespaceObject.stampClientIdentity)(event, request, extra, server);
234
236
  (0, external_transport_identity_js_namespaceObject.stampTransportIdentity)(event, extra);
237
+ const identity = data ? await (0, external_internal_js_namespaceObject.handleIdentify)(server, data, sessionId, request, sessionInfo, extra) : void 0;
238
+ const requestAttribution = (0, external_internal_js_namespaceObject.withIdentity)(sessionInfo, identity);
235
239
  if (data) await (0, external_tracing_helpers_js_namespaceObject.applyResolvedMetadata)(event, data, request, extra);
236
- const tools = await getTracedToolsList(server, originalListToolsHandler, request, extra, event, logger, requestAttribution);
240
+ const response = await getTracedToolsList(server, originalListToolsHandler, request, extra, event, logger, requestAttribution);
241
+ const tools = response.tools;
237
242
  if (!data) {
238
243
  logger('Warning: PostHog MCP analytics is unable to find server tracking data. Please ensure you have called instrument(server, options) before using tool calls.');
239
- return {
240
- tools
241
- };
244
+ return response;
242
245
  }
243
246
  if (0 === tools.length) {
244
247
  data.logger('Warning: No tools found in the original list. This is likely due to the tools not being registered before PostHog MCP analytics.instrument().');
@@ -246,20 +249,14 @@ async function handleListToolsRequest(server, originalListToolsHandler, request,
246
249
  event.isError = true;
247
250
  event.duration = Date.now() - startTime.getTime();
248
251
  (0, external_capture_js_namespaceObject.captureEvent)(server, event, data.logger, requestAttribution);
249
- return {
250
- tools
251
- };
252
+ return response;
252
253
  }
253
- event.response = {
254
- tools
255
- };
254
+ event.response = response;
256
255
  event.listedToolNames = collectListedToolNames(tools);
257
256
  event.isError = false;
258
257
  event.duration = Date.now() - startTime.getTime();
259
258
  (0, external_capture_js_namespaceObject.captureEvent)(server, event, data.logger, requestAttribution);
260
- return {
261
- tools
262
- };
259
+ return response;
263
260
  }
264
261
  function cacheToolAnalyticsParameterOwnership(cache, tools) {
265
262
  for (const tool of tools)if (tool?.name) cache.set(tool.name, (0, external_analytics_parameters_js_namespaceObject.getAnalyticsParameterOwnership)(tool.inputSchema, tool.outputSchema));
@@ -300,7 +297,10 @@ async function getTracedToolsList(server, originalListToolsHandler, request, ext
300
297
  cacheToolDescriptions(data.toolDescriptions, tools);
301
298
  cacheToolCategories(data.toolCategories, tools);
302
299
  }
303
- return tools;
300
+ return {
301
+ ...originalResponse,
302
+ tools
303
+ };
304
304
  } catch (error) {
305
305
  logger(`Warning: Original list tools handler failed, this suggests an error PostHog MCP analytics did not cause - ${error}`);
306
306
  event.error = (0, external_exceptions_js_namespaceObject.captureException)(error);
@@ -329,6 +329,7 @@ function mintStatelessSessionOnInitialize(server, data, request, extra) {
329
329
  try {
330
330
  const headers = (0, external_request_headers_js_namespaceObject.getRequestHeaders)(extra);
331
331
  if (!headers) return;
332
+ if ((0, external_session_js_namespaceObject.isModernEraRequest)(request, extra, server)) return;
332
333
  if ((0, external_session_token_js_namespaceObject.readMcpSessionHeader)(headers)) return;
333
334
  const transport = server.transport;
334
335
  if (!transport || extra?.sessionId || transport.sessionId) return;
@@ -12,7 +12,7 @@ import { getServerTrackingData, handleIdentify, setServerTrackingData, withIdent
12
12
  import { buildCapturedMcpParameters } from "./mcp-payloads.mjs";
13
13
  import { readRequestHandlerMethod } from "./mcp-sdk-compat.mjs";
14
14
  import { getRequestHeaders } from "./request-headers.mjs";
15
- import { getSessionId, getSessionInfo, newSessionId } from "./session.mjs";
15
+ import { getSessionId, getSessionInfo, isModernEraRequest, newSessionId } from "./session.mjs";
16
16
  import { encodeSessionId, readMcpSessionHeader, writeSessionIdToTransport } from "./session-token.mjs";
17
17
  import { getReportMissingToolDescriptor, resolveMissingCapabilityToolName } from "./tools.mjs";
18
18
  import { applyResolvedMetadata, isToolResultError } from "./tracing-helpers.mjs";
@@ -20,10 +20,11 @@ async function captureToolCall(params) {
20
20
  const { server, data, request, extra, execute, parameterOwnership, eventType, explicitContextIntent, takeCapturedError } = params;
21
21
  const resolvedEventType = eventType ?? MCPAnalyticsEventType.mcpToolsCall;
22
22
  const ownership = getActiveAnalyticsParameterOwnership(data, request.params?.name, parameterOwnership, resolvedEventType === MCPAnalyticsEventType.mcpMissingCapability);
23
+ const canCaptureContextIntent = resolvedEventType !== MCPAnalyticsEventType.mcpMissingCapability && isContextEnabled(data.options.context) && (ownership.context || !ownership.contextOwnershipKnown);
23
24
  const conversation = resolveConversationId(ownership.conversationId, request.params?.arguments);
24
25
  const downstreamRequest = cloneRequestWithoutOwnedAnalyticsArguments(request, ownership);
25
26
  const startTime = new Date();
26
- const preparedEvent = await prepareToolCallEvent(server, data, request, downstreamRequest, extra, startTime, conversation, ownership, resolvedEventType);
27
+ const preparedEvent = await prepareToolCallEvent(server, data, request, downstreamRequest, extra, startTime, conversation, ownership, resolvedEventType, canCaptureContextIntent);
27
28
  if (preparedEvent && explicitContextIntent) setExplicitContextIntent(preparedEvent.event, explicitContextIntent);
28
29
  let result;
29
30
  try {
@@ -33,13 +34,14 @@ async function captureToolCall(params) {
33
34
  throw error;
34
35
  }
35
36
  const finalResult = applyConversationInstructions(preparedEvent?.event ?? null, result, conversation, ownership);
36
- publishSuccessfulToolEvent(server, preparedEvent, finalResult, startTime, data.logger, takeCapturedError);
37
+ publishSuccessfulToolEvent(server, preparedEvent, finalResult, startTime, data.logger, takeCapturedError, result);
37
38
  return finalResult;
38
39
  }
39
40
  function getActiveAnalyticsParameterOwnership(data, toolName, override, isMissingCapabilityTool) {
40
41
  const listed = toolName ? data.toolAnalyticsParameterOwnership.get(toolName) : void 0;
41
42
  const ownership = override ?? listed;
42
43
  return {
44
+ contextOwnershipKnown: void 0 !== ownership,
43
45
  context: !isMissingCapabilityTool && isContextEnabled(data.options.context) && ownership?.context === true,
44
46
  conversationId: true === data.options.enableConversationId && ownership?.conversationId === true,
45
47
  outputInstructions: true === data.options.enableConversationId && listed?.outputInstructions === true
@@ -57,7 +59,7 @@ function cloneRequestWithoutOwnedAnalyticsArguments(request, ownership) {
57
59
  }
58
60
  };
59
61
  }
60
- async function prepareToolCallEvent(server, data, request, downstreamRequest, extra, startTime, conversation, ownership, eventType) {
62
+ async function prepareToolCallEvent(server, data, request, downstreamRequest, extra, startTime, conversation, ownership, eventType, canCaptureContextIntent) {
61
63
  try {
62
64
  const sessionId = getSessionId(server, extra, conversation.conversationId);
63
65
  const sessionInfo = getSessionInfo(server, data, sessionId);
@@ -77,7 +79,7 @@ async function prepareToolCallEvent(server, data, request, downstreamRequest, ex
77
79
  stampClientIdentity(event, request, extra, server);
78
80
  stampTransportIdentity(event, extra);
79
81
  await applyResolvedMetadata(event, data, request, extra);
80
- setEventIntent(event, await resolveToolCallIntent(data, request, ownership.context, extra));
82
+ setEventIntent(event, await resolveToolCallIntent(data, request, canCaptureContextIntent, extra));
81
83
  return {
82
84
  event,
83
85
  requestAttribution
@@ -104,14 +106,14 @@ function applyConversationInstructions(event, result, conversation, ownership) {
104
106
  if (!delivered && conversation.minted && event) event.conversationId = void 0;
105
107
  return updated;
106
108
  }
107
- function publishSuccessfulToolEvent(server, preparedEvent, result, startTime, logger, takeCapturedError) {
109
+ function publishSuccessfulToolEvent(server, preparedEvent, result, startTime, logger, takeCapturedError, resultBeforeInstructions) {
108
110
  if (!preparedEvent) return;
109
111
  const { event, requestAttribution } = preparedEvent;
110
112
  try {
111
113
  if (isToolResultError(result)) {
112
114
  event.isError = true;
113
115
  const capturedError = takeCapturedError?.();
114
- event.error = captureException(capturedError ?? result);
116
+ event.error = captureException(capturedError ?? resultBeforeInstructions ?? result);
115
117
  } else event.isError = false;
116
118
  event.response = result;
117
119
  event.duration = Date.now() - startTime.getTime();
@@ -187,7 +189,7 @@ async function handleListToolsRequest(server, originalListToolsHandler, request,
187
189
  const data = getServerTrackingData(server);
188
190
  const startTime = new Date();
189
191
  const sessionId = getSessionId(server, extra);
190
- const requestAttribution = getSessionInfo(server, data, sessionId);
192
+ const sessionInfo = getSessionInfo(server, data, sessionId);
191
193
  const event = {
192
194
  sessionId,
193
195
  parameters: buildCapturedMcpParameters(request),
@@ -196,13 +198,14 @@ async function handleListToolsRequest(server, originalListToolsHandler, request,
196
198
  };
197
199
  stampClientIdentity(event, request, extra, server);
198
200
  stampTransportIdentity(event, extra);
201
+ const identity = data ? await handleIdentify(server, data, sessionId, request, sessionInfo, extra) : void 0;
202
+ const requestAttribution = withIdentity(sessionInfo, identity);
199
203
  if (data) await applyResolvedMetadata(event, data, request, extra);
200
- const tools = await getTracedToolsList(server, originalListToolsHandler, request, extra, event, logger, requestAttribution);
204
+ const response = await getTracedToolsList(server, originalListToolsHandler, request, extra, event, logger, requestAttribution);
205
+ const tools = response.tools;
201
206
  if (!data) {
202
207
  logger('Warning: PostHog MCP analytics is unable to find server tracking data. Please ensure you have called instrument(server, options) before using tool calls.');
203
- return {
204
- tools
205
- };
208
+ return response;
206
209
  }
207
210
  if (0 === tools.length) {
208
211
  data.logger('Warning: No tools found in the original list. This is likely due to the tools not being registered before PostHog MCP analytics.instrument().');
@@ -210,20 +213,14 @@ async function handleListToolsRequest(server, originalListToolsHandler, request,
210
213
  event.isError = true;
211
214
  event.duration = Date.now() - startTime.getTime();
212
215
  captureEvent(server, event, data.logger, requestAttribution);
213
- return {
214
- tools
215
- };
216
+ return response;
216
217
  }
217
- event.response = {
218
- tools
219
- };
218
+ event.response = response;
220
219
  event.listedToolNames = collectListedToolNames(tools);
221
220
  event.isError = false;
222
221
  event.duration = Date.now() - startTime.getTime();
223
222
  captureEvent(server, event, data.logger, requestAttribution);
224
- return {
225
- tools
226
- };
223
+ return response;
227
224
  }
228
225
  function cacheToolAnalyticsParameterOwnership(cache, tools) {
229
226
  for (const tool of tools)if (tool?.name) cache.set(tool.name, getAnalyticsParameterOwnership(tool.inputSchema, tool.outputSchema));
@@ -264,7 +261,10 @@ async function getTracedToolsList(server, originalListToolsHandler, request, ext
264
261
  cacheToolDescriptions(data.toolDescriptions, tools);
265
262
  cacheToolCategories(data.toolCategories, tools);
266
263
  }
267
- return tools;
264
+ return {
265
+ ...originalResponse,
266
+ tools
267
+ };
268
268
  } catch (error) {
269
269
  logger(`Warning: Original list tools handler failed, this suggests an error PostHog MCP analytics did not cause - ${error}`);
270
270
  event.error = captureException(error);
@@ -293,6 +293,7 @@ function mintStatelessSessionOnInitialize(server, data, request, extra) {
293
293
  try {
294
294
  const headers = getRequestHeaders(extra);
295
295
  if (!headers) return;
296
+ if (isModernEraRequest(request, extra, server)) return;
296
297
  if (readMcpSessionHeader(headers)) return;
297
298
  const transport = server.transport;
298
299
  if (!transport || extra?.sessionId || transport.sessionId) return;
@@ -3,7 +3,7 @@ interface ResolvedIntent {
3
3
  intent: string;
4
4
  source: MCPAnalyticsIntentSource;
5
5
  }
6
- export declare function resolveToolCallIntent(data: MCPAnalyticsData, request: MCPRequestLike, analyticsOwnsContext: boolean, extra?: CompatibleRequestHandlerExtra): Promise<ResolvedIntent | null>;
6
+ export declare function resolveToolCallIntent(data: MCPAnalyticsData, request: MCPRequestLike, canCaptureContext: boolean, extra?: CompatibleRequestHandlerExtra): Promise<ResolvedIntent | null>;
7
7
  export declare function setEventIntent(event: McpEvent, resolvedIntent: ResolvedIntent | null): void;
8
8
  export declare function setExplicitContextIntent(event: McpEvent, context: string): void;
9
9
  export {};
@@ -1 +1 @@
1
- {"version":3,"file":"intent.d.ts","sourceRoot":"","sources":["../../src/extensions/intent.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EACV,6BAA6B,EAC7B,gBAAgB,EAChB,wBAAwB,EACxB,cAAc,EACd,QAAQ,EACT,MAAM,UAAU,CAAA;AAGjB,UAAU,cAAc;IACtB,MAAM,EAAE,MAAM,CAAA;IACd,MAAM,EAAE,wBAAwB,CAAA;CACjC;AAkCD,wBAAsB,qBAAqB,CACzC,IAAI,EAAE,gBAAgB,EACtB,OAAO,EAAE,cAAc,EACvB,oBAAoB,EAAE,OAAO,EAC7B,KAAK,CAAC,EAAE,6BAA6B,GACpC,OAAO,CAAC,cAAc,GAAG,IAAI,CAAC,CAOhC;AAED,wBAAgB,cAAc,CAAC,KAAK,EAAE,QAAQ,EAAE,cAAc,EAAE,cAAc,GAAG,IAAI,GAAG,IAAI,CAO3F;AAED,wBAAgB,wBAAwB,CAAC,KAAK,EAAE,QAAQ,EAAE,OAAO,EAAE,MAAM,GAAG,IAAI,CAQ/E"}
1
+ {"version":3,"file":"intent.d.ts","sourceRoot":"","sources":["../../src/extensions/intent.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EACV,6BAA6B,EAC7B,gBAAgB,EAChB,wBAAwB,EACxB,cAAc,EACd,QAAQ,EACT,MAAM,UAAU,CAAA;AAGjB,UAAU,cAAc;IACtB,MAAM,EAAE,MAAM,CAAA;IACd,MAAM,EAAE,wBAAwB,CAAA;CACjC;AAkCD,wBAAsB,qBAAqB,CACzC,IAAI,EAAE,gBAAgB,EACtB,OAAO,EAAE,cAAc,EACvB,iBAAiB,EAAE,OAAO,EAC1B,KAAK,CAAC,EAAE,6BAA6B,GACpC,OAAO,CAAC,cAAc,GAAG,IAAI,CAAC,CAOhC;AAED,wBAAgB,cAAc,CAAC,KAAK,EAAE,QAAQ,EAAE,cAAc,EAAE,cAAc,GAAG,IAAI,GAAG,IAAI,CAO3F;AAED,wBAAgB,wBAAwB,CAAC,KAAK,EAAE,QAAQ,EAAE,OAAO,EAAE,MAAM,GAAG,IAAI,CAQ/E"}
@@ -51,9 +51,9 @@ async function runIntentFallback(data, request, extra) {
51
51
  return null;
52
52
  }
53
53
  }
54
- async function resolveToolCallIntent(data, request, analyticsOwnsContext, extra) {
54
+ async function resolveToolCallIntent(data, request, canCaptureContext, extra) {
55
55
  const contextArgument = getContextArgument(request);
56
- if (analyticsOwnsContext && (0, external_context_parameters_js_namespaceObject.isContextEnabled)(data.options.context) && contextArgument) return {
56
+ if (canCaptureContext && (0, external_context_parameters_js_namespaceObject.isContextEnabled)(data.options.context) && contextArgument) return {
57
57
  intent: contextArgument,
58
58
  source: 'context_parameter'
59
59
  };
@@ -21,9 +21,9 @@ async function runIntentFallback(data, request, extra) {
21
21
  return null;
22
22
  }
23
23
  }
24
- async function resolveToolCallIntent(data, request, analyticsOwnsContext, extra) {
24
+ async function resolveToolCallIntent(data, request, canCaptureContext, extra) {
25
25
  const contextArgument = getContextArgument(request);
26
- if (analyticsOwnsContext && isContextEnabled(data.options.context) && contextArgument) return {
26
+ if (canCaptureContext && isContextEnabled(data.options.context) && contextArgument) return {
27
27
  intent: contextArgument,
28
28
  source: 'context_parameter'
29
29
  };
@@ -1,4 +1,28 @@
1
- import type { CompatibleRequestHandlerExtra, MCPAnalyticsData, MCPServerLike, SessionInfo } from '../types';
1
+ import type { CompatibleRequestHandlerExtra, MCPAnalyticsData, MCPRequestLike, MCPServerLike, SessionInfo } from '../types';
2
+ /**
3
+ * The revision that removed protocol-level sessions. A request declaring this or
4
+ * anything later must not be answered with an `Mcp-Session-Id`.
5
+ *
6
+ * Compared as a string, which is safe and stable because MCP revisions are
7
+ * ISO dates: any future revision sorts above this one and is treated as modern,
8
+ * which is the right default — sessions were removed, not re-added.
9
+ */
10
+ export declare const MODERN_PROTOCOL_REVISION = "2026-07-28";
11
+ /**
12
+ * Whether *this request* is governed by 2026-07-28 or later.
13
+ *
14
+ * Era is a property of the request, never of the installed SDK: one v2 server
15
+ * serves both, request by request, and v2's exported `LATEST_PROTOCOL_VERSION`
16
+ * still reads `2025-11-25`. So the version is resolved from the request itself —
17
+ * an `initialize` body declares the version it is asking for, and every other
18
+ * request carries it in the envelope, `_meta`, or the `MCP-Protocol-Version`
19
+ * header — and only then compared.
20
+ *
21
+ * Unknown means legacy. A request that declares nothing is a v1 client on a
22
+ * transport that has always had a session header, and taking it away from them
23
+ * would be the regression.
24
+ */
25
+ export declare function isModernEraRequest(request: MCPRequestLike, extra?: CompatibleRequestHandlerExtra, server?: MCPServerLike): boolean;
2
26
  export declare function newSessionId(): string;
3
27
  /**
4
28
  * Derives the SDK session id deterministically from the MCP sessionId, so the
@@ -1 +1 @@
1
- {"version":3,"file":"session.d.ts","sourceRoot":"","sources":["../../src/extensions/session.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EACV,6BAA6B,EAC7B,gBAAgB,EAChB,aAAa,EAEb,WAAW,EACZ,MAAM,UAAU,CAAA;AAQjB,wBAAgB,YAAY,IAAI,MAAM,CAErC;AAED;;;GAGG;AACH,wBAAgB,6BAA6B,CAAC,YAAY,EAAE,MAAM,GAAG,MAAM,CAE1E;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,+BAA+B,CAAC,cAAc,EAAE,MAAM,GAAG,MAAM,CAE9E;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,YAAY,CAC1B,MAAM,EAAE,aAAa,EACrB,KAAK,CAAC,EAAE,6BAA6B,EACrC,cAAc,CAAC,EAAE,MAAM,GACtB,MAAM,CAgCR;AAuED;;;;GAIG;AACH,wBAAgB,cAAc,CAC5B,MAAM,EAAE,aAAa,EACrB,IAAI,EAAE,gBAAgB,GAAG,SAAS,EAClC,SAAS,CAAC,EAAE,MAAM,GACjB,WAAW,CAmCb"}
1
+ {"version":3,"file":"session.d.ts","sourceRoot":"","sources":["../../src/extensions/session.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EACV,6BAA6B,EAC7B,gBAAgB,EAChB,cAAc,EACd,aAAa,EAEb,WAAW,EACZ,MAAM,UAAU,CAAA;AASjB;;;;;;;GAOG;AACH,eAAO,MAAM,wBAAwB,eAAe,CAAA;AAcpD;;;;;;;;;;;;;GAaG;AACH,wBAAgB,kBAAkB,CAChC,OAAO,EAAE,cAAc,EACvB,KAAK,CAAC,EAAE,6BAA6B,EACrC,MAAM,CAAC,EAAE,aAAa,GACrB,OAAO,CAYT;AAED,wBAAgB,YAAY,IAAI,MAAM,CAErC;AAED;;;GAGG;AACH,wBAAgB,6BAA6B,CAAC,YAAY,EAAE,MAAM,GAAG,MAAM,CAE1E;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,+BAA+B,CAAC,cAAc,EAAE,MAAM,GAAG,MAAM,CAE9E;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,YAAY,CAC1B,MAAM,EAAE,aAAa,EACrB,KAAK,CAAC,EAAE,6BAA6B,EACrC,cAAc,CAAC,EAAE,MAAM,GACtB,MAAM,CAgCR;AAuED;;;;GAIG;AACH,wBAAgB,cAAc,CAC5B,MAAM,EAAE,aAAa,EACrB,IAAI,EAAE,gBAAgB,GAAG,SAAS,EAClC,SAAS,CAAC,EAAE,MAAM,GACjB,WAAW,CAmCb"}
@@ -24,18 +24,35 @@ var __webpack_require__ = {};
24
24
  var __webpack_exports__ = {};
25
25
  __webpack_require__.r(__webpack_exports__);
26
26
  __webpack_require__.d(__webpack_exports__, {
27
+ isModernEraRequest: ()=>isModernEraRequest,
27
28
  deriveSessionIdFromConversation: ()=>deriveSessionIdFromConversation,
28
- getSessionId: ()=>getSessionId,
29
+ newSessionId: ()=>newSessionId,
30
+ MODERN_PROTOCOL_REVISION: ()=>MODERN_PROTOCOL_REVISION,
29
31
  deriveSessionIdFromMCPSession: ()=>deriveSessionIdFromMCPSession,
30
- getSessionInfo: ()=>getSessionInfo,
31
- newSessionId: ()=>newSessionId
32
+ getSessionId: ()=>getSessionId,
33
+ getSessionInfo: ()=>getSessionInfo
32
34
  });
33
35
  const external_version_js_namespaceObject = require("../version.js");
34
36
  const external_constants_js_namespaceObject = require("./constants.js");
35
37
  const external_ids_js_namespaceObject = require("./ids.js");
38
+ const external_client_identity_js_namespaceObject = require("./client-identity.js");
36
39
  const external_internal_js_namespaceObject = require("./internal.js");
37
40
  const external_request_headers_js_namespaceObject = require("./request-headers.js");
38
41
  const external_session_token_js_namespaceObject = require("./session-token.js");
42
+ const MODERN_PROTOCOL_REVISION = '2026-07-28';
43
+ const REVISION_SHAPE = /^\d{4}-\d{2}-\d{2}$/;
44
+ const DRAFT_REVISION = 'draft';
45
+ function isModernEraRequest(request, extra, server) {
46
+ const requested = request.params?.protocolVersion;
47
+ const version = ('string' == typeof requested && requested.length > 0 ? requested : void 0) ?? (0, external_client_identity_js_namespaceObject.resolveClientIdentity)({
48
+ request,
49
+ extra,
50
+ server
51
+ })?.protocolVersion;
52
+ if (!version) return false;
53
+ if (version === DRAFT_REVISION) return true;
54
+ return REVISION_SHAPE.test(version) && version >= MODERN_PROTOCOL_REVISION;
55
+ }
39
56
  function newSessionId() {
40
57
  return (0, external_ids_js_namespaceObject.newPrefixedId)('ses');
41
58
  }
@@ -111,16 +128,20 @@ function getSessionInfo(server, data, sessionId) {
111
128
  ...sessionInfo
112
129
  };
113
130
  }
131
+ exports.MODERN_PROTOCOL_REVISION = __webpack_exports__.MODERN_PROTOCOL_REVISION;
114
132
  exports.deriveSessionIdFromConversation = __webpack_exports__.deriveSessionIdFromConversation;
115
133
  exports.deriveSessionIdFromMCPSession = __webpack_exports__.deriveSessionIdFromMCPSession;
116
134
  exports.getSessionId = __webpack_exports__.getSessionId;
117
135
  exports.getSessionInfo = __webpack_exports__.getSessionInfo;
136
+ exports.isModernEraRequest = __webpack_exports__.isModernEraRequest;
118
137
  exports.newSessionId = __webpack_exports__.newSessionId;
119
138
  for(var __webpack_i__ in __webpack_exports__)if (-1 === [
139
+ "MODERN_PROTOCOL_REVISION",
120
140
  "deriveSessionIdFromConversation",
121
141
  "deriveSessionIdFromMCPSession",
122
142
  "getSessionId",
123
143
  "getSessionInfo",
144
+ "isModernEraRequest",
124
145
  "newSessionId"
125
146
  ].indexOf(__webpack_i__)) exports[__webpack_i__] = __webpack_exports__[__webpack_i__];
126
147
  Object.defineProperty(exports, '__esModule', {
@@ -1,9 +1,24 @@
1
- import { version } from "../version.mjs";
1
+ import { version as external_version_mjs_version } from "../version.mjs";
2
2
  import { INACTIVITY_TIMEOUT_IN_MINUTES } from "./constants.mjs";
3
3
  import { deterministicPrefixedId, newPrefixedId } from "./ids.mjs";
4
+ import { resolveClientIdentity } from "./client-identity.mjs";
4
5
  import { getServerTrackingData, setServerTrackingData } from "./internal.mjs";
5
6
  import { getRequestHeaders } from "./request-headers.mjs";
6
7
  import { decodeSessionId, readMcpSessionHeader } from "./session-token.mjs";
8
+ const MODERN_PROTOCOL_REVISION = '2026-07-28';
9
+ const REVISION_SHAPE = /^\d{4}-\d{2}-\d{2}$/;
10
+ const DRAFT_REVISION = 'draft';
11
+ function isModernEraRequest(request, extra, server) {
12
+ const requested = request.params?.protocolVersion;
13
+ const version = ('string' == typeof requested && requested.length > 0 ? requested : void 0) ?? resolveClientIdentity({
14
+ request,
15
+ extra,
16
+ server
17
+ })?.protocolVersion;
18
+ if (!version) return false;
19
+ if (version === DRAFT_REVISION) return true;
20
+ return REVISION_SHAPE.test(version) && version >= MODERN_PROTOCOL_REVISION;
21
+ }
7
22
  function newSessionId() {
8
23
  return newPrefixedId('ses');
9
24
  }
@@ -62,7 +77,7 @@ function getSessionInfo(server, data, sessionId) {
62
77
  const sessionInfo = {
63
78
  ipAddress: void 0,
64
79
  sdkLanguage: 'TypeScript',
65
- sdkVersion: version,
80
+ sdkVersion: external_version_mjs_version,
66
81
  serverName: server._serverInfo?.name,
67
82
  serverVersion: server._serverInfo?.version,
68
83
  clientName: clientInfo?.name,
@@ -79,4 +94,4 @@ function getSessionInfo(server, data, sessionId) {
79
94
  ...sessionInfo
80
95
  };
81
96
  }
82
- export { deriveSessionIdFromConversation, deriveSessionIdFromMCPSession, getSessionId, getSessionInfo, newSessionId };
97
+ export { MODERN_PROTOCOL_REVISION, deriveSessionIdFromConversation, deriveSessionIdFromMCPSession, getSessionId, getSessionInfo, isModernEraRequest, newSessionId };
package/dist/version.d.ts CHANGED
@@ -1,2 +1,2 @@
1
- export declare const version = "0.11.3";
1
+ export declare const version = "0.11.5";
2
2
  //# sourceMappingURL=version.d.ts.map
package/dist/version.js CHANGED
@@ -26,7 +26,7 @@ __webpack_require__.r(__webpack_exports__);
26
26
  __webpack_require__.d(__webpack_exports__, {
27
27
  version: ()=>version
28
28
  });
29
- const version = '0.11.3';
29
+ const version = '0.11.5';
30
30
  exports.version = __webpack_exports__.version;
31
31
  for(var __webpack_i__ in __webpack_exports__)if (-1 === [
32
32
  "version"
package/dist/version.mjs CHANGED
@@ -1,2 +1,2 @@
1
- const version = '0.11.3';
1
+ const version = '0.11.5';
2
2
  export { version };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@posthog/mcp",
3
- "version": "0.11.3",
3
+ "version": "0.11.5",
4
4
  "bugs": {
5
5
  "url": "https://github.com/PostHog/posthog-js/issues"
6
6
  },
@@ -28,7 +28,7 @@
28
28
  "module": "dist/index.mjs",
29
29
  "types": "dist/index.d.ts",
30
30
  "dependencies": {
31
- "@posthog/core": "^1.47.0"
31
+ "@posthog/core": "^1.48.0"
32
32
  },
33
33
  "devDependencies": {
34
34
  "@babel/preset-env": "^7.27.1",
@@ -41,7 +41,7 @@
41
41
  "jest": "29.7.0",
42
42
  "zod": "^3.25.0",
43
43
  "@posthog-tooling/tsconfig-base": "1.1.1",
44
- "posthog-node": "^5.48.1"
44
+ "posthog-node": "^5.49.0"
45
45
  },
46
46
  "peerDependencies": {
47
47
  "@modelcontextprotocol/sdk": ">=1.26.0",
@@ -35,7 +35,7 @@ import type { LoggerFn } from './logger'
35
35
  import { buildCapturedMcpParameters } from './mcp-payloads'
36
36
  import { readRequestHandlerMethod } from './mcp-sdk-compat'
37
37
  import { getRequestHeaders } from './request-headers'
38
- import { getSessionId, getSessionInfo, newSessionId } from './session'
38
+ import { getSessionId, getSessionInfo, isModernEraRequest, newSessionId } from './session'
39
39
  import { encodeSessionId, readMcpSessionHeader, writeSessionIdToTransport } from './session-token'
40
40
  import { getReportMissingToolDescriptor, resolveMissingCapabilityToolName } from './tools'
41
41
  import { applyResolvedMetadata, isToolResultError } from './tracing-helpers'
@@ -109,6 +109,14 @@ export async function captureToolCall(params: TraceToolCallParams): Promise<unkn
109
109
  parameterOwnership,
110
110
  resolvedEventType === MCPAnalyticsEventType.mcpMissingCapability
111
111
  )
112
+ // Reading the argument and removing it are separate questions: deleting one the
113
+ // application declared costs the customer their call, so the strip below still
114
+ // requires positive ownership, while reading it when ownership is unresolved
115
+ // costs at worst a mislabelled property. ADR-0011.
116
+ const canCaptureContextIntent =
117
+ resolvedEventType !== MCPAnalyticsEventType.mcpMissingCapability &&
118
+ isContextEnabled(data.options.context) &&
119
+ (ownership.context || !ownership.contextOwnershipKnown)
112
120
  const conversation = resolveConversationId(ownership.conversationId, request.params?.arguments)
113
121
  const downstreamRequest = cloneRequestWithoutOwnedAnalyticsArguments(request, ownership)
114
122
 
@@ -124,7 +132,8 @@ export async function captureToolCall(params: TraceToolCallParams): Promise<unkn
124
132
  startTime,
125
133
  conversation,
126
134
  ownership,
127
- resolvedEventType
135
+ resolvedEventType,
136
+ canCaptureContextIntent
128
137
  )
129
138
  if (preparedEvent && explicitContextIntent) {
130
139
  setExplicitContextIntent(preparedEvent.event, explicitContextIntent)
@@ -139,7 +148,9 @@ export async function captureToolCall(params: TraceToolCallParams): Promise<unkn
139
148
  }
140
149
 
141
150
  const finalResult = applyConversationInstructions(preparedEvent?.event ?? null, result, conversation, ownership)
142
- publishSuccessfulToolEvent(server, preparedEvent, finalResult, startTime, data.logger, takeCapturedError)
151
+ // `result`, not `finalResult`: the error is read from what the tool produced,
152
+ // before the conversation handle was written into it. See below.
153
+ publishSuccessfulToolEvent(server, preparedEvent, finalResult, startTime, data.logger, takeCapturedError, result)
143
154
  return finalResult
144
155
  }
145
156
 
@@ -148,15 +159,24 @@ interface PreparedToolEvent {
148
159
  requestAttribution: SessionInfo
149
160
  }
150
161
 
162
+ /**
163
+ * Ownership for one request, plus whether it could be resolved at all — "no
164
+ * answer" must not read the same as "the application owns it".
165
+ */
166
+ interface ActiveAnalyticsParameterOwnership extends AnalyticsParameterOwnership {
167
+ contextOwnershipKnown: boolean
168
+ }
169
+
151
170
  function getActiveAnalyticsParameterOwnership(
152
171
  data: MCPAnalyticsData,
153
172
  toolName: string | undefined,
154
173
  override: AnalyticsParameterOwnership | undefined,
155
174
  isMissingCapabilityTool: boolean
156
- ): AnalyticsParameterOwnership {
175
+ ): ActiveAnalyticsParameterOwnership {
157
176
  const listed = toolName ? data.toolAnalyticsParameterOwnership.get(toolName) : undefined
158
177
  const ownership = override ?? listed
159
178
  return {
179
+ contextOwnershipKnown: ownership !== undefined,
160
180
  context: !isMissingCapabilityTool && isContextEnabled(data.options.context) && ownership?.context === true,
161
181
  conversationId: data.options.enableConversationId === true && ownership?.conversationId === true,
162
182
  // Deliberately read off `listed`, never the override: only the advertised
@@ -197,7 +217,8 @@ async function prepareToolCallEvent(
197
217
  startTime: Date,
198
218
  conversation: ConversationIdResolution,
199
219
  ownership: AnalyticsParameterOwnership,
200
- eventType: MCPAnalyticsEventType
220
+ eventType: MCPAnalyticsEventType,
221
+ canCaptureContextIntent: boolean
201
222
  ): Promise<PreparedToolEvent | null> {
202
223
  try {
203
224
  const sessionId = getSessionId(server, extra, conversation.conversationId)
@@ -235,7 +256,7 @@ async function prepareToolCallEvent(
235
256
  stampTransportIdentity(event, extra)
236
257
 
237
258
  await applyResolvedMetadata(event, data, request, extra)
238
- setEventIntent(event, await resolveToolCallIntent(data, request, ownership.context, extra))
259
+ setEventIntent(event, await resolveToolCallIntent(data, request, canCaptureContextIntent, extra))
239
260
  return { event, requestAttribution }
240
261
  } catch (error) {
241
262
  data.logger(
@@ -288,13 +309,26 @@ function applyConversationInstructions(
288
309
  return updated
289
310
  }
290
311
 
312
+ /**
313
+ * @param result - what the caller receives, conversation handle included.
314
+ * @param resultBeforeInstructions - the same result as the tool produced it.
315
+ *
316
+ * The two differ once `enableConversationId` mints a handle, and the difference
317
+ * matters for errors. A tool that fails returns its message in `content`, and on
318
+ * MCP SDK v2 that flattened `isError` result is the only description of the
319
+ * failure we get — the throw never reaches our callback wrapper. Reading the
320
+ * error off the *delivered* result would therefore append the prompt-back to it,
321
+ * putting a fresh uuid inside `$mcp_error_message` on every failed call and
322
+ * splitting one recurring failure into as many groups as there were calls.
323
+ */
291
324
  function publishSuccessfulToolEvent(
292
325
  server: MCPServerLike,
293
326
  preparedEvent: PreparedToolEvent | null,
294
327
  result: unknown,
295
328
  startTime: Date,
296
329
  logger: LoggerFn,
297
- takeCapturedError?: () => unknown
330
+ takeCapturedError?: () => unknown,
331
+ resultBeforeInstructions?: unknown
298
332
  ): void {
299
333
  if (!preparedEvent) {
300
334
  return
@@ -304,7 +338,7 @@ function publishSuccessfulToolEvent(
304
338
  if (isToolResultError(result)) {
305
339
  event.isError = true
306
340
  const capturedError = takeCapturedError?.()
307
- event.error = captureException(capturedError ?? result)
341
+ event.error = captureException(capturedError ?? resultBeforeInstructions ?? result)
308
342
  } else {
309
343
  event.isError = false
310
344
  }
@@ -496,27 +530,39 @@ export async function handleListToolsRequest(
496
530
  request: MCPRequestLike,
497
531
  extra: CompatibleRequestHandlerExtra | undefined,
498
532
  logger: LoggerFn
499
- ): Promise<{ tools: CompatibleToolsListLike['tools'] }> {
533
+ ): Promise<CompatibleToolsListLike> {
500
534
  const data = getServerTrackingData(server)
501
535
  const startTime = new Date()
502
536
  const sessionId = getSessionId(server, extra)
503
- // Snapshot before metadata resolution or the list handler can yield to a
504
- // concurrent request using the same instrumented server.
505
- const requestAttribution = getSessionInfo(server, data, sessionId)
537
+ // Snapshot before identify, metadata resolution, or the list handler can yield
538
+ // to a concurrent request using the same instrumented server.
539
+ const sessionInfo = getSessionInfo(server, data, sessionId)
506
540
  const event: McpEvent = {
507
541
  sessionId,
508
542
  parameters: buildCapturedMcpParameters(request),
509
543
  eventType: MCPAnalyticsEventType.mcpToolsList,
510
544
  timestamp: startTime,
511
545
  }
546
+ // Stamp before the identify await below. The last link of the identity chain is
547
+ // the server's own `getClientVersion()`, which a concurrent `initialize` can
548
+ // replace while a slow identify callback is in flight — and `captureEvent`
549
+ // prefers a stamped value over the `sessionInfo` snapshot, so a late stamp
550
+ // would win with the wrong client's name.
512
551
  stampClientIdentity(event, request, extra, server)
513
552
  stampTransportIdentity(event, extra)
514
553
 
554
+ // `getSessionInfo` only surfaces an identity some earlier request already
555
+ // cached on this instance. On a per-request instance that cache is always
556
+ // empty, so without resolving `identify` here too, `tools/list` would be the
557
+ // one request path that never attributes to a person.
558
+ const identity = data ? await handleIdentify(server, data, sessionId, request, sessionInfo, extra) : undefined
559
+ const requestAttribution = withIdentity(sessionInfo, identity)
560
+
515
561
  if (data) {
516
562
  await applyResolvedMetadata(event, data, request, extra)
517
563
  }
518
564
 
519
- const tools = await getTracedToolsList(
565
+ const response = await getTracedToolsList(
520
566
  server,
521
567
  originalListToolsHandler,
522
568
  request,
@@ -525,12 +571,13 @@ export async function handleListToolsRequest(
525
571
  logger,
526
572
  requestAttribution
527
573
  )
574
+ const tools = response.tools
528
575
 
529
576
  if (!data) {
530
577
  logger(
531
578
  'Warning: PostHog MCP analytics is unable to find server tracking data. Please ensure you have called instrument(server, options) before using tool calls.'
532
579
  )
533
- return { tools }
580
+ return response
534
581
  }
535
582
 
536
583
  if (tools.length === 0) {
@@ -541,15 +588,15 @@ export async function handleListToolsRequest(
541
588
  event.isError = true
542
589
  event.duration = Date.now() - startTime.getTime()
543
590
  captureEvent(server, event, data.logger, requestAttribution)
544
- return { tools }
591
+ return response
545
592
  }
546
593
 
547
- event.response = { tools }
594
+ event.response = response
548
595
  event.listedToolNames = collectListedToolNames(tools)
549
596
  event.isError = false
550
597
  event.duration = Date.now() - startTime.getTime()
551
598
  captureEvent(server, event, data.logger, requestAttribution)
552
- return { tools }
599
+ return response
553
600
  }
554
601
 
555
602
  function cacheToolAnalyticsParameterOwnership(
@@ -580,7 +627,7 @@ async function getTracedToolsList(
580
627
  event: McpEvent,
581
628
  logger: LoggerFn,
582
629
  requestAttribution: SessionInfo
583
- ): Promise<CompatibleToolsListLike['tools']> {
630
+ ): Promise<CompatibleToolsListLike> {
584
631
  try {
585
632
  const data = getServerTrackingData(server)
586
633
  const originalResponse = (await originalListToolsHandler(request, extra)) as CompatibleToolsListLike
@@ -622,7 +669,8 @@ async function getTracedToolsList(
622
669
  cacheToolCategories(data.toolCategories, tools)
623
670
  }
624
671
 
625
- return tools
672
+ // Spread, never enumerate — fields later revisions add survive by default.
673
+ return { ...originalResponse, tools }
626
674
  } catch (error) {
627
675
  logger(
628
676
  `Warning: Original list tools handler failed, this suggests an error PostHog MCP analytics did not cause - ${error}`
@@ -699,6 +747,14 @@ function mintStatelessSessionOnInitialize(
699
747
  if (!headers) {
700
748
  return undefined // not an HTTP transport (stdio/in-memory) — nothing to mint into
701
749
  }
750
+ // 2026-07-28 removed sessions from the protocol: a server MUST NOT mint or
751
+ // echo `Mcp-Session-Id` under it. Today that era never reaches here anyway,
752
+ // because it has no `initialize` — but relying on that is relying on an SDK
753
+ // routing detail to keep us spec-compliant, so the era is asked directly.
754
+ // Per request, never per server: the same v2 server serves both.
755
+ if (isModernEraRequest(request, extra, server)) {
756
+ return undefined
757
+ }
702
758
  if (readMcpSessionHeader(headers)) {
703
759
  return undefined // client already replays a session id (ours or the transport's)
704
760
  }
@@ -52,11 +52,11 @@ async function runIntentFallback(
52
52
  export async function resolveToolCallIntent(
53
53
  data: MCPAnalyticsData,
54
54
  request: MCPRequestLike,
55
- analyticsOwnsContext: boolean,
55
+ canCaptureContext: boolean,
56
56
  extra?: CompatibleRequestHandlerExtra
57
57
  ): Promise<ResolvedIntent | null> {
58
58
  const contextArgument = getContextArgument(request)
59
- if (analyticsOwnsContext && isContextEnabled(data.options.context) && contextArgument) {
59
+ if (canCaptureContext && isContextEnabled(data.options.context) && contextArgument) {
60
60
  return { intent: contextArgument, source: 'context_parameter' }
61
61
  }
62
62
 
@@ -7,17 +7,73 @@ import { version } from '../version'
7
7
  import type {
8
8
  CompatibleRequestHandlerExtra,
9
9
  MCPAnalyticsData,
10
+ MCPRequestLike,
10
11
  MCPServerLike,
11
12
  ServerClientInfoLike,
12
13
  SessionInfo,
13
14
  } from '../types'
14
15
  import { INACTIVITY_TIMEOUT_IN_MINUTES } from './constants'
15
16
  import { deterministicPrefixedId, newPrefixedId } from './ids'
17
+ import { resolveClientIdentity } from './client-identity'
16
18
  import { getServerTrackingData, setServerTrackingData } from './internal'
17
19
  import { getRequestHeaders } from './request-headers'
18
20
  import { decodeSessionId, readMcpSessionHeader } from './session-token'
19
21
  import type { SessionTokenPayload } from './session-token'
20
22
 
23
+ /**
24
+ * The revision that removed protocol-level sessions. A request declaring this or
25
+ * anything later must not be answered with an `Mcp-Session-Id`.
26
+ *
27
+ * Compared as a string, which is safe and stable because MCP revisions are
28
+ * ISO dates: any future revision sorts above this one and is treated as modern,
29
+ * which is the right default — sessions were removed, not re-added.
30
+ */
31
+ export const MODERN_PROTOCOL_REVISION = '2026-07-28'
32
+
33
+ /**
34
+ * Revisions are ISO dates, and only a date-shaped token may be compared as one.
35
+ * Without this the comparison is lexicographic over arbitrary input, so any
36
+ * string starting above `'2'` — every junk value beginning with a letter —
37
+ * sorts as modern and silently loses the session header. Unknown must mean
38
+ * legacy, so anything that is not a date is not compared at all.
39
+ */
40
+ const REVISION_SHAPE = /^\d{4}-\d{2}-\d{2}$/
41
+
42
+ /** The spec's rolling draft sits ahead of every dated revision, sessions included. */
43
+ const DRAFT_REVISION = 'draft'
44
+
45
+ /**
46
+ * Whether *this request* is governed by 2026-07-28 or later.
47
+ *
48
+ * Era is a property of the request, never of the installed SDK: one v2 server
49
+ * serves both, request by request, and v2's exported `LATEST_PROTOCOL_VERSION`
50
+ * still reads `2025-11-25`. So the version is resolved from the request itself —
51
+ * an `initialize` body declares the version it is asking for, and every other
52
+ * request carries it in the envelope, `_meta`, or the `MCP-Protocol-Version`
53
+ * header — and only then compared.
54
+ *
55
+ * Unknown means legacy. A request that declares nothing is a v1 client on a
56
+ * transport that has always had a session header, and taking it away from them
57
+ * would be the regression.
58
+ */
59
+ export function isModernEraRequest(
60
+ request: MCPRequestLike,
61
+ extra?: CompatibleRequestHandlerExtra,
62
+ server?: MCPServerLike
63
+ ): boolean {
64
+ const requested = request.params?.protocolVersion
65
+ const version =
66
+ (typeof requested === 'string' && requested.length > 0 ? requested : undefined) ??
67
+ resolveClientIdentity({ request, extra, server })?.protocolVersion
68
+ if (!version) {
69
+ return false
70
+ }
71
+ if (version === DRAFT_REVISION) {
72
+ return true
73
+ }
74
+ return REVISION_SHAPE.test(version) && version >= MODERN_PROTOCOL_REVISION
75
+ }
76
+
21
77
  export function newSessionId(): string {
22
78
  return newPrefixedId('ses')
23
79
  }
package/src/version.ts CHANGED
@@ -1 +1 @@
1
- export const version = '0.11.3'
1
+ export const version = '0.11.5'