@posthog/mcp 0.10.7 → 0.10.9

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 (51) hide show
  1. package/dist/extensions/analytics-parameters.d.ts +7 -2
  2. package/dist/extensions/analytics-parameters.d.ts.map +1 -1
  3. package/dist/extensions/analytics-parameters.js +4 -2
  4. package/dist/extensions/analytics-parameters.mjs +4 -2
  5. package/dist/extensions/conversation-id.d.ts +37 -8
  6. package/dist/extensions/conversation-id.d.ts.map +1 -1
  7. package/dist/extensions/conversation-id.js +8 -13
  8. package/dist/extensions/conversation-id.mjs +8 -13
  9. package/dist/extensions/instrument-highlevel.d.ts.map +1 -1
  10. package/dist/extensions/instrument-highlevel.js +1 -1
  11. package/dist/extensions/instrument-highlevel.mjs +1 -1
  12. package/dist/extensions/instrument-lowlevel.d.ts.map +1 -1
  13. package/dist/extensions/instrument-lowlevel.js +6 -5
  14. package/dist/extensions/instrument-lowlevel.mjs +7 -6
  15. package/dist/extensions/instrumentation.d.ts +22 -0
  16. package/dist/extensions/instrumentation.d.ts.map +1 -1
  17. package/dist/extensions/instrumentation.js +48 -23
  18. package/dist/extensions/instrumentation.mjs +46 -24
  19. package/dist/extensions/internal.d.ts +8 -1
  20. package/dist/extensions/internal.d.ts.map +1 -1
  21. package/dist/extensions/internal.js +2 -2
  22. package/dist/extensions/internal.mjs +2 -2
  23. package/dist/extensions/mcp-sdk-compat.d.ts +10 -0
  24. package/dist/extensions/mcp-sdk-compat.d.ts.map +1 -1
  25. package/dist/extensions/mcp-sdk-compat.js +11 -2
  26. package/dist/extensions/mcp-sdk-compat.mjs +7 -1
  27. package/dist/extensions/output-instructions.d.ts +70 -0
  28. package/dist/extensions/output-instructions.d.ts.map +1 -0
  29. package/dist/extensions/output-instructions.js +131 -0
  30. package/dist/extensions/output-instructions.mjs +82 -0
  31. package/dist/extensions/session.d.ts +26 -5
  32. package/dist/extensions/session.d.ts.map +1 -1
  33. package/dist/extensions/session.js +32 -14
  34. package/dist/extensions/session.mjs +30 -15
  35. package/dist/types.d.ts +23 -1
  36. package/dist/types.d.ts.map +1 -1
  37. package/dist/version.d.ts +1 -1
  38. package/dist/version.js +1 -1
  39. package/dist/version.mjs +1 -1
  40. package/package.json +4 -4
  41. package/src/extensions/analytics-parameters.ts +15 -2
  42. package/src/extensions/conversation-id.ts +51 -29
  43. package/src/extensions/instrument-highlevel.ts +3 -1
  44. package/src/extensions/instrument-lowlevel.ts +14 -9
  45. package/src/extensions/instrumentation.ts +130 -38
  46. package/src/extensions/internal.ts +10 -2
  47. package/src/extensions/mcp-sdk-compat.ts +18 -0
  48. package/src/extensions/output-instructions.ts +231 -0
  49. package/src/extensions/session.ts +127 -28
  50. package/src/types.ts +23 -1
  51. package/src/version.ts +1 -1
@@ -15,6 +15,7 @@ import { INACTIVITY_TIMEOUT_IN_MINUTES } from './constants'
15
15
  import { deterministicPrefixedId, newPrefixedId } from './ids'
16
16
  import { getServerTrackingData, setServerTrackingData } from './internal'
17
17
  import { decodeSessionId, readMcpSessionHeader } from './session-token'
18
+ import type { SessionTokenPayload } from './session-token'
18
19
 
19
20
  export function newSessionId(): string {
20
21
  return newPrefixedId('ses')
@@ -29,39 +30,77 @@ export function deriveSessionIdFromMCPSession(mcpSessionId: string): string {
29
30
  }
30
31
 
31
32
  /**
32
- * Resolves the session id for a request: from the replayed session token, from
33
- * the transport's MCP session id, or — when the request carries nothing — from
34
- * this instance's memory. Also saves the token's client name/version so events
35
- * built later in the request can use them (see getSessionInfo).
33
+ * Derives the SDK session id from the agent's conversation handle. Deterministic
34
+ * and unsalted on purpose: two pods that never met must agree on the session, and
35
+ * the 2026-07-28 revision leaves them no shared state to agree through.
36
+ *
37
+ * Hashed rather than used verbatim so an MCP session can never collide with a
38
+ * Session Replay id — a bare uuidv7 would render a "View recording" button that
39
+ * resolves to nothing.
40
+ *
41
+ * Exported because this *is* the cross-SDK contract: posthog-python has to
42
+ * reproduce it byte for byte or the same conversation splits into two sessions
43
+ * depending on which SDK served the call.
36
44
  */
37
- export function getSessionId(server: MCPServerLike, extra?: CompatibleRequestHandlerExtra): string {
45
+ export function deriveSessionIdFromConversation(conversationId: string): string {
46
+ return deterministicPrefixedId('ses', conversationId)
47
+ }
48
+
49
+ /**
50
+ * Resolves the session id for a request. Three steps, first match wins:
51
+ *
52
+ * 1. the agent carried a `conversation_id` tool argument — 2026-07-28
53
+ * 2. the request carried a session id — 2025-11-25
54
+ * 3. nothing was carried, so reuse this instance's own id — stdio
55
+ *
56
+ * The split mirrors the two protocol revisions. 2026-07-28 removed protocol-level
57
+ * sessions, so the only thing that can carry a session across calls is the agent
58
+ * itself (1). 2025-11-25 kept the session on the connection, so the request
59
+ * carries it (2). 3 is for stdio, where neither applies — there is no
60
+ * HTTP request to carry anything, and no agent handle unless the host opted in.
61
+ */
62
+ export function getSessionId(
63
+ server: MCPServerLike,
64
+ extra?: CompatibleRequestHandlerExtra,
65
+ conversationId?: string
66
+ ): string {
38
67
  const data = getServerTrackingData(server)
39
68
  if (!data) {
40
69
  throw new Error('Server tracking data not found')
41
70
  }
42
71
 
43
- // Our token rides the `mcp-session-id` request header, which stateless
44
- // transports ignore — so read it ourselves.
45
- const sessionHeader = readMcpSessionHeader(extra?.requestInfo?.headers)
46
- const token = decodeSessionId(sessionHeader)
47
-
48
- let sessionId: string
49
- if (token) {
50
- // Token we minted at `initialize` (see session-token.ts).
51
- data.sessionSource = 'token'
52
- data.sessionInfo.clientName = token.clientName
53
- data.sessionInfo.clientVersion = token.clientVersion
54
- data.sessionInfo.protocolVersion = token.protocolVersion
55
- sessionId = token.sessionId
56
- } else if (extra?.sessionId) {
57
- // Session id issued by a stateful transport: hash it so the same MCP
58
- // session maps to the same SDK session across restarts.
59
- data.sessionSource = 'mcp'
60
- sessionId = deriveSessionIdFromMCPSession(extra.sessionId)
61
- } else {
62
- sessionId = getSessionIdFromMemory(data)
72
+ // 1. The agent's conversation handle. It outranks both steps below because it
73
+ // is the only id that survives reconnects, restarts, and the per-request
74
+ // server instances the 2026-07-28 revision introduces. Hashed rather than used
75
+ // verbatim so it can never collide with Session Replay ids.
76
+ //
77
+ // This returns instead of falling through to the shared-state writes at the
78
+ // end: the handle belongs to this one request, and persisting it would leak
79
+ // one chat's session onto a concurrent chat's `tools/list`.
80
+ //
81
+ // `lastActivity` therefore does not advance either, so a conversation that
82
+ // always echoes lets the in-memory fallback age past the inactivity timeout —
83
+ // and a later call carrying no handle rotates it. That is the honest reading:
84
+ // the fallback session really has been idle for that whole time.
85
+ if (conversationId) {
86
+ // The handle decides the *session*, but the request may still carry our token,
87
+ // and on a stateless instance that never processed `initialize` its payload is
88
+ // the only place client identity exists. Restore that before returning: the
89
+ // early return is about not persisting a session, not about ignoring what the
90
+ // request told us about the client. Without this, tool calls and `$identify`
91
+ // go out unattributed on exactly the deployments this branch exists for.
92
+ applyTokenClientIdentity(data, extra)
93
+ return deriveSessionIdFromConversation(conversationId)
63
94
  }
64
95
 
96
+ // 2. A session id the request itself carried (undefined if it carried none).
97
+ const carriedByRequest = readSessionIdFromRequest(data, extra)
98
+
99
+ // 3. Nothing carried, so fall back to the id this instance already holds.
100
+ const sessionId = carriedByRequest ?? getSessionIdFromMemory(data)
101
+
102
+ // 2 and 3 are connection-scoped, so their result is remembered for the
103
+ // next request on this instance. 1 never reaches here.
65
104
  data.sessionId = sessionId
66
105
  data.lastActivity = new Date()
67
106
  setServerTrackingData(server, data)
@@ -69,9 +108,69 @@ export function getSessionId(server: MCPServerLike, extra?: CompatibleRequestHan
69
108
  }
70
109
 
71
110
  /**
72
- * Nothing replayed on this request: keep the id we already have. Only
73
- * generated sessions roll over on inactivity — token/MCP ids live as long as
74
- * the client replays them, and regenerating one would split the session.
111
+ * Restores the client name/version and protocol version baked into our token at
112
+ * mint time, and hands the decoded token back so the caller can also take the
113
+ * session id from it.
114
+ *
115
+ * Split out from step 2 because the two halves of the token have different
116
+ * scopes. The session id it carries is per-chat, so a conversation-anchored
117
+ * request must not adopt it. The client identity is per-connection — the same
118
+ * client for every request on it — so a conversation-anchored request should,
119
+ * and on a stateless instance that never saw `initialize` this is the only
120
+ * source of it.
121
+ */
122
+ function applyTokenClientIdentity(
123
+ data: MCPAnalyticsData,
124
+ extra?: CompatibleRequestHandlerExtra
125
+ ): SessionTokenPayload | undefined {
126
+ const token = decodeSessionId(readMcpSessionHeader(extra?.requestInfo?.headers))
127
+ if (!token) {
128
+ return undefined
129
+ }
130
+ data.sessionInfo.clientName = token.clientName
131
+ data.sessionInfo.clientVersion = token.clientVersion
132
+ data.sessionInfo.protocolVersion = token.protocolVersion
133
+ return token
134
+ }
135
+
136
+ /**
137
+ * 2. The session id a request carried. Two sources, tried in order: our own token
138
+ * on the `mcp-session-id` header, then the raw session id a stateful transport
139
+ * issued. Returns undefined when the request carried neither, which is what
140
+ * sends the caller on to 3.
141
+ *
142
+ * Both are 2025-11-25 mechanisms. The 2026-07-28 revision removed that header
143
+ * outright — servers MUST NOT mint or echo it — so this entire step becomes
144
+ * legacy-only once era detection lands.
145
+ */
146
+ function readSessionIdFromRequest(data: MCPAnalyticsData, extra?: CompatibleRequestHandlerExtra): string | undefined {
147
+ // 2a. A token we minted at `initialize` and the client replayed. It rides the
148
+ // `mcp-session-id` header, which stateless transports don't surface as
149
+ // extra.sessionId, so read the header ourselves.
150
+ const token = applyTokenClientIdentity(data, extra)
151
+ if (token) {
152
+ data.sessionSource = 'token'
153
+ return token.sessionId
154
+ }
155
+
156
+ // 2b. No token, but a stateful transport issued its own session id. Hash it so
157
+ // the same MCP session maps to the same SDK session across restarts.
158
+ if (extra?.sessionId) {
159
+ data.sessionSource = 'mcp'
160
+ return deriveSessionIdFromMCPSession(extra.sessionId)
161
+ }
162
+
163
+ return undefined
164
+ }
165
+
166
+ /**
167
+ * 3. The request carried nothing, so keep the id this instance already holds —
168
+ * minted once at `instrument()`. This is what groups a stdio server's calls,
169
+ * where there is no header and no transport session to read.
170
+ *
171
+ * Only self-generated ids roll over on inactivity. Token and transport ids live
172
+ * as long as the client replays them, so regenerating one would split a session
173
+ * that is still very much alive.
75
174
  */
76
175
  function getSessionIdFromMemory(data: MCPAnalyticsData): string {
77
176
  const timeoutMs = INACTIVITY_TIMEOUT_IN_MINUTES * 60 * 1000
package/src/types.ts CHANGED
@@ -57,7 +57,21 @@ export interface MCPAnalyticsOptions {
57
57
  * detected under the same name.
58
58
  */
59
59
  missingCapabilityToolName?: string
60
- /** Enables the `conversation_id` tool parameter + prompt-back loop. */
60
+ /**
61
+ * Opt in to session correlation for the MCP **2026-07-28** revision, which removed
62
+ * protocol-level sessions: no `initialize`, no `mcp-session-id` header, and a fresh
63
+ * server instance per HTTP request. With none of those left to anchor on, the only
64
+ * thing that can carry a session across calls is the agent itself.
65
+ *
66
+ * Turning this on injects a `conversation_id` parameter into every tool, mints one
67
+ * on the first call, asks the agent to echo it back, and derives `$session_id` from
68
+ * that handle — so calls correlate across reconnects, restarts, and per-request
69
+ * instances.
70
+ *
71
+ * Off by default, and fully inert when off: no parameter is injected, no schema is
72
+ * touched, no prompt-back is appended, and `$session_id` resolves exactly as it did
73
+ * before (the request's own session id, else this instance's).
74
+ */
61
75
  enableConversationId?: boolean
62
76
  /**
63
77
  * Emit a `$exception` event alongside any failed tool call. Defaults to `true`.
@@ -123,6 +137,8 @@ export type RegisteredTool = {
123
137
  /** MCP tool `_meta` block (spec-allowed arbitrary metadata, e.g. `category`). */
124
138
  _meta?: Record<string, unknown>
125
139
  inputSchema?: unknown
140
+ /** Present when the tool was registered with a declared output schema. */
141
+ outputSchema?: unknown
126
142
  update?: (...args: unknown[]) => unknown
127
143
  } & ({ callback: ToolCallback; handler?: never } | { handler: ToolCallback; callback?: never })
128
144
 
@@ -274,6 +290,12 @@ export interface SessionInfo {
274
290
  export interface AnalyticsParameterOwnership {
275
291
  context: boolean
276
292
  conversationId: boolean
293
+ /**
294
+ * True when we declared `_mcp_instructions` on this tool's advertised output
295
+ * schema, so writing that key into `structuredContent` will validate. False
296
+ * for tools with no output schema, or one we could not extend.
297
+ */
298
+ outputInstructions: boolean
277
299
  }
278
300
 
279
301
  export interface MCPAnalyticsData {
package/src/version.ts CHANGED
@@ -1 +1 @@
1
- export const version = '0.10.7'
1
+ export const version = '0.10.9'