@posthog/mcp 0.10.6 → 0.10.8

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 (46) 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/capture.d.ts +2 -2
  6. package/dist/extensions/capture.d.ts.map +1 -1
  7. package/dist/extensions/capture.js +2 -2
  8. package/dist/extensions/capture.mjs +2 -2
  9. package/dist/extensions/conversation-id.d.ts +37 -8
  10. package/dist/extensions/conversation-id.d.ts.map +1 -1
  11. package/dist/extensions/conversation-id.js +8 -13
  12. package/dist/extensions/conversation-id.mjs +8 -13
  13. package/dist/extensions/instrument-highlevel.d.ts.map +1 -1
  14. package/dist/extensions/instrument-highlevel.js +1 -1
  15. package/dist/extensions/instrument-highlevel.mjs +1 -1
  16. package/dist/extensions/instrumentation.d.ts.map +1 -1
  17. package/dist/extensions/instrumentation.js +86 -42
  18. package/dist/extensions/instrumentation.mjs +88 -44
  19. package/dist/extensions/internal.d.ts +10 -2
  20. package/dist/extensions/internal.d.ts.map +1 -1
  21. package/dist/extensions/internal.js +25 -9
  22. package/dist/extensions/internal.mjs +19 -6
  23. package/dist/extensions/output-instructions.d.ts +70 -0
  24. package/dist/extensions/output-instructions.d.ts.map +1 -0
  25. package/dist/extensions/output-instructions.js +131 -0
  26. package/dist/extensions/output-instructions.mjs +82 -0
  27. package/dist/extensions/session.d.ts +26 -5
  28. package/dist/extensions/session.d.ts.map +1 -1
  29. package/dist/extensions/session.js +35 -15
  30. package/dist/extensions/session.mjs +33 -16
  31. package/dist/types.d.ts +23 -1
  32. package/dist/types.d.ts.map +1 -1
  33. package/dist/version.d.ts +1 -1
  34. package/dist/version.js +1 -1
  35. package/dist/version.mjs +1 -1
  36. package/package.json +3 -3
  37. package/src/extensions/analytics-parameters.ts +15 -2
  38. package/src/extensions/capture.ts +13 -6
  39. package/src/extensions/conversation-id.ts +51 -29
  40. package/src/extensions/instrument-highlevel.ts +3 -1
  41. package/src/extensions/instrumentation.ts +151 -60
  42. package/src/extensions/internal.ts +30 -7
  43. package/src/extensions/output-instructions.ts +231 -0
  44. package/src/extensions/session.ts +128 -29
  45. package/src/types.ts +23 -1
  46. package/src/version.ts +1 -1
@@ -0,0 +1,231 @@
1
+ import type { LoggerFn } from './logger'
2
+ import { log } from './logger'
3
+ import type { AnalyticsInjectableJsonSchema } from './analytics-parameters'
4
+
5
+ /**
6
+ * The `conversation_id` session handle reaches the agent as a text block appended to the
7
+ * tool result. That block is invisible to any client that reads
8
+ * `structuredContent` instead of `content` — which is what clients do whenever a
9
+ * tool declares an `outputSchema`. Measured against Claude Code, correlation for
10
+ * such tools drops to zero.
11
+ *
12
+ * The fix is to mirror the session handle into `structuredContent` as well, which this
13
+ * module does in two halves that must stay in that order:
14
+ *
15
+ * 1. declare `_mcp_instructions` on the tool's advertised `outputSchema`
16
+ * 2. write it into the result's `structuredContent`
17
+ *
18
+ * The declaration is what makes the write safe. The MCP client ajv-validates
19
+ * `structuredContent` against the schema from `tools/list`, and
20
+ * `zod-to-json-schema` emits `additionalProperties: false` for a plain
21
+ * `z.object`, so an undeclared key is not ignored — it fails the entire tool
22
+ * result. Only tools that got the declaration are ever written to.
23
+ */
24
+ export const MCP_INSTRUCTIONS_KEY = '_mcp_instructions'
25
+
26
+ const INSTRUCTIONS_FIELD_DESCRIPTION =
27
+ 'Server-issued handles for this conversation, and what to do with them. Read and follow.'
28
+
29
+ const CONVERSATION_ID_FIELD_DESCRIPTION =
30
+ 'Echo this exact value as the conversation_id argument on every subsequent tool call.'
31
+
32
+ export interface OutputInstructionsInjectableTool {
33
+ name?: string
34
+ outputSchema?: unknown
35
+ [key: string]: unknown
36
+ }
37
+
38
+ /**
39
+ * True when we can safely declare {@link MCP_INSTRUCTIONS_KEY} on this tool's
40
+ * advertised output schema.
41
+ *
42
+ * Requires a plain-object schema we can extend. A tool with no `outputSchema`
43
+ * has nothing to mirror into and keeps working through `content`; a composed
44
+ * schema (`oneOf`/`allOf`/`anyOf`/`$ref`) has no single `properties` bag to add
45
+ * to. Both stay content-only, matching the policy on the input side.
46
+ */
47
+ export function canDeclareOutputInstructions(outputSchema: unknown): boolean {
48
+ if (!outputSchema || typeof outputSchema !== 'object') {
49
+ return false
50
+ }
51
+
52
+ // Only the JSON Schema a tool advertises can answer this. A Zod schema or raw
53
+ // shape — what the high-level registry stores — has no `properties` bag, so
54
+ // every check below would pass vacuously and report `true` for a tool whose
55
+ // schema we actually skipped. Refuse rather than guess: callers holding a Zod
56
+ // value must read the recorded answer off the ownership registry instead.
57
+ if (isSchemaObjectLike(outputSchema)) {
58
+ return false
59
+ }
60
+
61
+ const schema = outputSchema as AnalyticsInjectableJsonSchema
62
+ if (schema.$ref || schema.oneOf || schema.allOf || schema.anyOf) {
63
+ return false
64
+ }
65
+
66
+ // `properties` must be a plain object if it is present at all. A tool that
67
+ // advertises something else is malformed, and harmlessly so until we try to
68
+ // declare into it — assigning a key to a boolean throws, and that throw
69
+ // surfaces inside the `tools/list` wrapper, failing the entire listing over a
70
+ // schema we only wanted to annotate.
71
+ const { properties } = schema
72
+ if (
73
+ properties !== undefined &&
74
+ (typeof properties !== 'object' || properties === null || Array.isArray(properties))
75
+ ) {
76
+ return false
77
+ }
78
+ return !properties || !Object.prototype.hasOwnProperty.call(properties, MCP_INSTRUCTIONS_KEY)
79
+ }
80
+
81
+ /** A single Zod schema — the internals every version brands its instances with. */
82
+ function isZodLike(entry: unknown): boolean {
83
+ return !!entry && typeof entry === 'object' && ('_def' in entry || '_zod' in entry || '~standard' in entry)
84
+ }
85
+
86
+ /**
87
+ * Whether this is a JSON Schema, judged on the *value* of each marker rather
88
+ * than the key alone — a raw Zod shape is free to name a field `type` or
89
+ * `properties`, but its values are Zod schemas, not a string and a property bag.
90
+ */
91
+ function looksLikeJsonSchema(candidate: Record<string, unknown>): boolean {
92
+ if (typeof candidate.type === 'string' || typeof candidate.$ref === 'string') {
93
+ return true
94
+ }
95
+ if (Array.isArray(candidate.oneOf) || Array.isArray(candidate.allOf) || Array.isArray(candidate.anyOf)) {
96
+ return true
97
+ }
98
+ const { properties } = candidate
99
+ return !!properties && typeof properties === 'object' && !isZodLike(properties)
100
+ }
101
+
102
+ /** A Zod schema, or a raw shape whose values are Zod schemas. */
103
+ function isSchemaObjectLike(value: unknown): boolean {
104
+ const candidate = value as Record<string, unknown>
105
+ if (isZodLike(candidate)) {
106
+ return true
107
+ }
108
+
109
+ // Settle "is this a JSON Schema" before scanning the values, because a schema
110
+ // may legitimately declare a property named `_def` — and the scan below would
111
+ // read that property's schema object as a Zod value and skip a tool we could
112
+ // have annotated perfectly well.
113
+ if (looksLikeJsonSchema(candidate)) {
114
+ return false
115
+ }
116
+ return Object.values(candidate).some(isZodLike)
117
+ }
118
+
119
+ /**
120
+ * Returns a copy of the tool with an optional {@link MCP_INSTRUCTIONS_KEY}
121
+ * property added to its output schema, or the tool untouched when it has no
122
+ * output schema to extend.
123
+ *
124
+ * The property is never added to `required` — a result without it must stay
125
+ * valid, since every tool result predating this change lacks it.
126
+ */
127
+ export function addInstructionsToOutputSchema<TTool extends OutputInstructionsInjectableTool>(
128
+ tool: TTool,
129
+ logger: LoggerFn = log
130
+ ): TTool {
131
+ const toolName = tool.name || 'unknown'
132
+ const original = tool.outputSchema
133
+
134
+ // No output schema means the client reads `content`, where the session handle already
135
+ // rides. Nothing to declare, and nothing broken.
136
+ if (!original || typeof original !== 'object') {
137
+ return tool
138
+ }
139
+
140
+ if (!canDeclareOutputInstructions(original)) {
141
+ const schema = original as AnalyticsInjectableJsonSchema
142
+ if (schema.properties && Object.prototype.hasOwnProperty.call(schema.properties, MCP_INSTRUCTIONS_KEY)) {
143
+ logger(
144
+ `WARN: Tool "${toolName}" already declares '${MCP_INSTRUCTIONS_KEY}' in its output schema. Leaving it alone.`
145
+ )
146
+ } else {
147
+ logger(
148
+ `WARN: Tool "${toolName}" has a complex output schema (oneOf/allOf/anyOf/$ref). Skipping '${MCP_INSTRUCTIONS_KEY}' declaration; its session handle stays content-only.`
149
+ )
150
+ }
151
+ return tool
152
+ }
153
+
154
+ const modifiedTool = { ...tool }
155
+ // Deep copy: the server may reuse or freeze the schema object it handed us.
156
+ const outputSchema = JSON.parse(JSON.stringify(original)) as AnalyticsInjectableJsonSchema
157
+
158
+ if (!outputSchema.properties) {
159
+ outputSchema.properties = {}
160
+ }
161
+
162
+ outputSchema.properties[MCP_INSTRUCTIONS_KEY] = {
163
+ type: 'object',
164
+ description: INSTRUCTIONS_FIELD_DESCRIPTION,
165
+ properties: {
166
+ conversation_id: {
167
+ type: 'string',
168
+ description: CONVERSATION_ID_FIELD_DESCRIPTION,
169
+ },
170
+ instructions: { type: 'string' },
171
+ },
172
+ }
173
+
174
+ modifiedTool.outputSchema = outputSchema
175
+ return modifiedTool
176
+ }
177
+
178
+ /**
179
+ * Declares {@link MCP_INSTRUCTIONS_KEY} across a tool listing. Tools without an
180
+ * output schema, and tools whose schema we cannot extend, pass through unchanged.
181
+ */
182
+ export function addInstructionsToOutputSchemas<TTool extends OutputInstructionsInjectableTool>(
183
+ tools: TTool[],
184
+ logger: LoggerFn = log
185
+ ): TTool[] {
186
+ return tools.map((tool) => addInstructionsToOutputSchema(tool, logger))
187
+ }
188
+
189
+ export interface ConversationInstructions {
190
+ conversation_id: string
191
+ instructions: string
192
+ }
193
+
194
+ const ECHO_INSTRUCTION = 'Send this conversation_id as an argument on every subsequent tool call in this conversation.'
195
+
196
+ /** The payload mirrored into `structuredContent` for a tool we declared the key on. */
197
+ export function buildConversationInstructions(conversationId: string): ConversationInstructions {
198
+ return { conversation_id: conversationId, instructions: ECHO_INSTRUCTION }
199
+ }
200
+
201
+ /**
202
+ * Adds {@link MCP_INSTRUCTIONS_KEY} to a result's `structuredContent`, which is
203
+ * where clients look once a tool declares an `outputSchema` — the `content` text
204
+ * block carrying the session handle is invisible to them.
205
+ *
206
+ * Unlike the text block, this rides *every* response rather than only the one
207
+ * that minted the session handle, so an agent that drops it can read it back.
208
+ *
209
+ * Returns the result untouched when there is no plain-object `structuredContent`
210
+ * to extend, or when the tool already produced its own key — customer data wins.
211
+ * Never mutates the input.
212
+ */
213
+ export function mirrorInstructionsIntoStructuredContent(result: unknown, conversationId: string): unknown {
214
+ if (!result || typeof result !== 'object') {
215
+ return result
216
+ }
217
+ const structuredContent = (result as { structuredContent?: unknown }).structuredContent
218
+ if (!structuredContent || typeof structuredContent !== 'object' || Array.isArray(structuredContent)) {
219
+ return result
220
+ }
221
+ if (Object.prototype.hasOwnProperty.call(structuredContent, MCP_INSTRUCTIONS_KEY)) {
222
+ return result
223
+ }
224
+ return {
225
+ ...result,
226
+ structuredContent: {
227
+ ...structuredContent,
228
+ [MCP_INSTRUCTIONS_KEY]: buildConversationInstructions(conversationId),
229
+ },
230
+ }
231
+ }
@@ -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
@@ -125,5 +224,5 @@ export function getSessionInfo(
125
224
 
126
225
  data.sessionInfo = sessionInfo
127
226
  setServerTrackingData(server, data)
128
- return data.sessionInfo
227
+ return { ...sessionInfo }
129
228
  }
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.6'
1
+ export const version = '0.10.8'