@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.
- package/dist/extensions/analytics-parameters.d.ts +7 -2
- package/dist/extensions/analytics-parameters.d.ts.map +1 -1
- package/dist/extensions/analytics-parameters.js +4 -2
- package/dist/extensions/analytics-parameters.mjs +4 -2
- package/dist/extensions/conversation-id.d.ts +37 -8
- package/dist/extensions/conversation-id.d.ts.map +1 -1
- package/dist/extensions/conversation-id.js +8 -13
- package/dist/extensions/conversation-id.mjs +8 -13
- package/dist/extensions/instrument-highlevel.d.ts.map +1 -1
- package/dist/extensions/instrument-highlevel.js +1 -1
- package/dist/extensions/instrument-highlevel.mjs +1 -1
- package/dist/extensions/instrument-lowlevel.d.ts.map +1 -1
- package/dist/extensions/instrument-lowlevel.js +6 -5
- package/dist/extensions/instrument-lowlevel.mjs +7 -6
- package/dist/extensions/instrumentation.d.ts +22 -0
- package/dist/extensions/instrumentation.d.ts.map +1 -1
- package/dist/extensions/instrumentation.js +48 -23
- package/dist/extensions/instrumentation.mjs +46 -24
- package/dist/extensions/internal.d.ts +8 -1
- package/dist/extensions/internal.d.ts.map +1 -1
- package/dist/extensions/internal.js +2 -2
- package/dist/extensions/internal.mjs +2 -2
- package/dist/extensions/mcp-sdk-compat.d.ts +10 -0
- package/dist/extensions/mcp-sdk-compat.d.ts.map +1 -1
- package/dist/extensions/mcp-sdk-compat.js +11 -2
- package/dist/extensions/mcp-sdk-compat.mjs +7 -1
- package/dist/extensions/output-instructions.d.ts +70 -0
- package/dist/extensions/output-instructions.d.ts.map +1 -0
- package/dist/extensions/output-instructions.js +131 -0
- package/dist/extensions/output-instructions.mjs +82 -0
- package/dist/extensions/session.d.ts +26 -5
- package/dist/extensions/session.d.ts.map +1 -1
- package/dist/extensions/session.js +32 -14
- package/dist/extensions/session.mjs +30 -15
- package/dist/types.d.ts +23 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/version.mjs +1 -1
- package/package.json +4 -4
- package/src/extensions/analytics-parameters.ts +15 -2
- package/src/extensions/conversation-id.ts +51 -29
- package/src/extensions/instrument-highlevel.ts +3 -1
- package/src/extensions/instrument-lowlevel.ts +14 -9
- package/src/extensions/instrumentation.ts +130 -38
- package/src/extensions/internal.ts +10 -2
- package/src/extensions/mcp-sdk-compat.ts +18 -0
- package/src/extensions/output-instructions.ts +231 -0
- package/src/extensions/session.ts +127 -28
- package/src/types.ts +23 -1
- 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
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
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
|
|
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
|
-
//
|
|
44
|
-
//
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
//
|
|
58
|
-
//
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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
|
-
*
|
|
73
|
-
*
|
|
74
|
-
*
|
|
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
|
-
/**
|
|
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.
|
|
1
|
+
export const version = '0.10.9'
|