@posthog/mcp 0.11.0 → 0.11.2

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 (55) hide show
  1. package/README.md +39 -0
  2. package/dist/extensions/client-identity.d.ts +70 -15
  3. package/dist/extensions/client-identity.d.ts.map +1 -1
  4. package/dist/extensions/client-identity.js +72 -17
  5. package/dist/extensions/client-identity.mjs +59 -13
  6. package/dist/extensions/compatibility.d.ts.map +1 -1
  7. package/dist/extensions/compatibility.js +14 -18
  8. package/dist/extensions/compatibility.mjs +10 -14
  9. package/dist/extensions/detect.d.ts +51 -0
  10. package/dist/extensions/detect.d.ts.map +1 -0
  11. package/dist/extensions/detect.js +96 -0
  12. package/dist/extensions/detect.mjs +38 -0
  13. package/dist/extensions/instrument-highlevel.d.ts.map +1 -1
  14. package/dist/extensions/instrumentation.d.ts +4 -5
  15. package/dist/extensions/instrumentation.d.ts.map +1 -1
  16. package/dist/extensions/instrumentation.js +6 -5
  17. package/dist/extensions/instrumentation.mjs +7 -6
  18. package/dist/extensions/internal.js +1 -1
  19. package/dist/extensions/internal.mjs +2 -2
  20. package/dist/extensions/request-headers.d.ts +36 -0
  21. package/dist/extensions/request-headers.d.ts.map +1 -0
  22. package/dist/extensions/request-headers.js +62 -0
  23. package/dist/extensions/request-headers.mjs +28 -0
  24. package/dist/extensions/session.d.ts.map +1 -1
  25. package/dist/extensions/session.js +2 -1
  26. package/dist/extensions/session.mjs +2 -1
  27. package/dist/extensions/tools.d.ts +4 -4
  28. package/dist/extensions/tools.d.ts.map +1 -1
  29. package/dist/extensions/transport-identity.d.ts +8 -3
  30. package/dist/extensions/transport-identity.d.ts.map +1 -1
  31. package/dist/extensions/transport-identity.js +2 -1
  32. package/dist/extensions/transport-identity.mjs +2 -1
  33. package/dist/index.d.ts +2 -1
  34. package/dist/index.d.ts.map +1 -1
  35. package/dist/index.js +11 -7
  36. package/dist/index.mjs +2 -1
  37. package/dist/types.d.ts +77 -2
  38. package/dist/types.d.ts.map +1 -1
  39. package/dist/version.d.ts +1 -1
  40. package/dist/version.js +1 -1
  41. package/dist/version.mjs +1 -1
  42. package/package.json +12 -2
  43. package/src/extensions/client-identity.ts +146 -31
  44. package/src/extensions/compatibility.ts +25 -24
  45. package/src/extensions/detect.ts +87 -0
  46. package/src/extensions/instrument-highlevel.ts +4 -4
  47. package/src/extensions/instrumentation.ts +22 -15
  48. package/src/extensions/internal.ts +2 -2
  49. package/src/extensions/request-headers.ts +76 -0
  50. package/src/extensions/session.ts +2 -1
  51. package/src/extensions/tools.ts +4 -4
  52. package/src/extensions/transport-identity.ts +10 -4
  53. package/src/index.ts +4 -0
  54. package/src/types.ts +78 -3
  55. package/src/version.ts +1 -1
@@ -0,0 +1,87 @@
1
+ /**
2
+ * Structural feature probes.
3
+ *
4
+ * Every question about the shape of a server object is answered here, by
5
+ * duck-typing the object in front of us — never by reading a package version or
6
+ * a protocol constant. Two reasons this is a rule and not a preference:
7
+ *
8
+ * - The two SDK majors expose the same capability under different names (v1's
9
+ * `.tool()` vs v2's `.registerTool()`), so "which SDK is installed" answers a
10
+ * different question than "can this object register a tool".
11
+ * - A single v2 server serves both protocol eras, request by request, so no
12
+ * module-level constant describes it. v2's exported `LATEST_PROTOCOL_VERSION`
13
+ * is `2025-11-25`.
14
+ *
15
+ * Nothing here imports `@modelcontextprotocol/*`; that boundary is pinned by
16
+ * `sdk-import-boundary.test.ts`.
17
+ */
18
+
19
+ type UnknownRecord = Record<string, unknown>
20
+
21
+ function asRecord(value: unknown): UnknownRecord | undefined {
22
+ return value && typeof value === 'object' ? (value as UnknownRecord) : undefined
23
+ }
24
+
25
+ function hasMethod(value: unknown, key: string): boolean {
26
+ return typeof asRecord(value)?.[key] === 'function'
27
+ }
28
+
29
+ /** A high-level `McpServer` wrapper: it holds the low-level server on `.server`. */
30
+ export function hasNestedLowLevelServer(server: unknown): server is UnknownRecord & { server: UnknownRecord } {
31
+ const record = asRecord(server)
32
+ return !!record && 'server' in record && !!asRecord(record.server)
33
+ }
34
+
35
+ /** No `.server` property, so the object is itself the low-level `Server`. */
36
+ export function isBareServerShape(server: unknown): boolean {
37
+ const record = asRecord(server)
38
+ return !!record && !('server' in record)
39
+ }
40
+
41
+ /** The high-level registry we read tool metadata from and wrap callbacks in. */
42
+ export function hasToolRegistry(server: unknown): boolean {
43
+ return !!asRecord(asRecord(server)?._registeredTools)
44
+ }
45
+
46
+ /**
47
+ * The object can register a tool.
48
+ *
49
+ * v1 offers both `tool()` and `registerTool()`; v2 dropped the deprecated
50
+ * `tool()` and keeps only `registerTool()`. Requiring `tool()` specifically is
51
+ * what made `instrument()` reject every v2 high-level server — and, because
52
+ * `instrument()` swallows compatibility errors, reject it silently.
53
+ */
54
+ export function canRegisterTools(server: unknown): boolean {
55
+ return hasMethod(server, 'registerTool') || hasMethod(server, 'tool')
56
+ }
57
+
58
+ /** Handler registration, which we patch to wrap late registrations. */
59
+ export function canSetRequestHandler(server: unknown): boolean {
60
+ return hasMethod(server, 'setRequestHandler')
61
+ }
62
+
63
+ /** The live handler map we read and write directly. A real `Map` on both majors. */
64
+ export function hasRequestHandlerMap(server: unknown): boolean {
65
+ const handlers = asRecord(server)?._requestHandlers
66
+ return handlers instanceof Map && typeof handlers.get === 'function'
67
+ }
68
+
69
+ /** `getClientVersion()` — still present on v2, and a link in B1's identity chain. */
70
+ export function hasClientVersionAccessor(server: unknown): boolean {
71
+ return hasMethod(server, 'getClientVersion')
72
+ }
73
+
74
+ /**
75
+ * `getNegotiatedProtocolVersion()` — v2 only, and the last link in the protocol
76
+ * version chain. A v1 server does not have it, which is why the chain probes for
77
+ * it instead of branching on which SDK is installed.
78
+ */
79
+ export function hasNegotiatedProtocolVersionAccessor(server: unknown): boolean {
80
+ return hasMethod(server, 'getNegotiatedProtocolVersion')
81
+ }
82
+
83
+ /** `_serverInfo`, which names the server on every event. */
84
+ export function hasServerInfo(server: unknown): boolean {
85
+ const info = asRecord(asRecord(server)?._serverInfo)
86
+ return !!info && 'name' in info
87
+ }
@@ -3,9 +3,9 @@
3
3
  // Copyright (c) 2025 AgentCat, Inc. (formerly MCPcat)
4
4
  // Licensed under the MIT License: https://github.com/agentcathq/agentcat-typescript-sdk/blob/main/LICENSE
5
5
 
6
- import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js'
7
6
  import type {
8
7
  CompatibleRequestHandlerExtra,
8
+ CompatibleToolResultLike,
9
9
  HighLevelMCPServerLike,
10
10
  MCPServerLike,
11
11
  RegisteredTool,
@@ -173,7 +173,7 @@ function addTracingToToolCallbackInternal(
173
173
  return tool
174
174
  }
175
175
 
176
- const wrappedCallback = async (...params: unknown[]): Promise<CallToolResult> => {
176
+ const wrappedCallback = async (...params: unknown[]): Promise<CompatibleToolResultLike> => {
177
177
  let args: unknown
178
178
  let extra: CompatibleRequestHandlerExtra
179
179
 
@@ -192,13 +192,13 @@ function addTracingToToolCallbackInternal(
192
192
  })
193
193
  try {
194
194
  if (cleanedArgs === undefined) {
195
- const handler = originalCallback as (extra: CompatibleRequestHandlerExtra) => Promise<CallToolResult>
195
+ const handler = originalCallback as (extra: CompatibleRequestHandlerExtra) => Promise<CompatibleToolResultLike>
196
196
  return await handler(extra)
197
197
  }
198
198
  const handler = originalCallback as (
199
199
  args: unknown,
200
200
  extra: CompatibleRequestHandlerExtra
201
- ) => Promise<CallToolResult>
201
+ ) => Promise<CompatibleToolResultLike>
202
202
  return await handler(cleanedArgs, extra)
203
203
  } catch (error) {
204
204
  if (error instanceof Error) {
@@ -3,10 +3,10 @@
3
3
  // Copyright (c) 2025 AgentCat, Inc. (formerly MCPcat)
4
4
  // Licensed under the MIT License: https://github.com/agentcathq/agentcat-typescript-sdk/blob/main/LICENSE
5
5
 
6
- import type { ListToolsResult } from '@modelcontextprotocol/sdk/types.js'
7
6
  import type {
8
7
  AnalyticsParameterOwnership,
9
8
  CompatibleRequestHandlerExtra,
9
+ CompatibleToolsListLike,
10
10
  MCPAnalyticsData,
11
11
  MCPRequestLike,
12
12
  MCPServerLike,
@@ -23,7 +23,7 @@ import {
23
23
  injectConversationIdPromptBack,
24
24
  resolveConversationId,
25
25
  } from './conversation-id'
26
- import { stampMetaClientInfo } from './client-identity'
26
+ import { stampClientIdentity } from './client-identity'
27
27
  import { stampTransportIdentity } from './transport-identity'
28
28
  import { addInstructionsToOutputSchemas, mirrorInstructionsIntoStructuredContent } from './output-instructions'
29
29
  import { captureEvent } from './capture'
@@ -34,6 +34,7 @@ import { getServerTrackingData, handleIdentify, setServerTrackingData, withIdent
34
34
  import type { LoggerFn } from './logger'
35
35
  import { buildCapturedMcpParameters } from './mcp-payloads'
36
36
  import { readRequestHandlerMethod } from './mcp-sdk-compat'
37
+ import { getRequestHeaders } from './request-headers'
37
38
  import { getSessionId, getSessionInfo, newSessionId } from './session'
38
39
  import { encodeSessionId, readMcpSessionHeader, writeSessionIdToTransport } from './session-token'
39
40
  import { getReportMissingToolDescriptor, resolveMissingCapabilityToolName } from './tools'
@@ -228,7 +229,7 @@ async function prepareToolCallEvent(
228
229
  // Modern (stateless) clients carry client name/version + protocol version in
229
230
  // `_meta` on every request rather than at `initialize`; stamp them onto this
230
231
  // event now so concurrent requests can't cross-attribute it.
231
- stampMetaClientInfo(event, request)
232
+ stampClientIdentity(event, request, extra, server)
232
233
  // Which *surface* of the client made this call lives only in the request
233
234
  // headers (HTTP transports); `clientInfo` can't tell a vendor's products apart.
234
235
  stampTransportIdentity(event, extra)
@@ -470,7 +471,7 @@ export async function isToolAdvertised(
470
471
  }
471
472
 
472
473
  try {
473
- const response = (await listHandler({ method: 'tools/list', params: {} }, extra)) as ListToolsResult
474
+ const response = (await listHandler({ method: 'tools/list', params: {} }, extra)) as CompatibleToolsListLike
474
475
  if (!response || !Array.isArray(response.tools)) {
475
476
  return undefined
476
477
  }
@@ -495,7 +496,7 @@ export async function handleListToolsRequest(
495
496
  request: MCPRequestLike,
496
497
  extra: CompatibleRequestHandlerExtra | undefined,
497
498
  logger: LoggerFn
498
- ): Promise<{ tools: ListToolsResult['tools'] }> {
499
+ ): Promise<{ tools: CompatibleToolsListLike['tools'] }> {
499
500
  const data = getServerTrackingData(server)
500
501
  const startTime = new Date()
501
502
  const sessionId = getSessionId(server, extra)
@@ -508,7 +509,7 @@ export async function handleListToolsRequest(
508
509
  eventType: MCPAnalyticsEventType.mcpToolsList,
509
510
  timestamp: startTime,
510
511
  }
511
- stampMetaClientInfo(event, request)
512
+ stampClientIdentity(event, request, extra, server)
512
513
  stampTransportIdentity(event, extra)
513
514
 
514
515
  if (data) {
@@ -553,7 +554,7 @@ export async function handleListToolsRequest(
553
554
 
554
555
  function cacheToolAnalyticsParameterOwnership(
555
556
  cache: Map<string, AnalyticsParameterOwnership>,
556
- tools: ListToolsResult['tools']
557
+ tools: CompatibleToolsListLike['tools']
557
558
  ): void {
558
559
  // Merge pages and concurrent enumerations; repeated tool names overwrite stale schemas.
559
560
  for (const tool of tools) {
@@ -563,7 +564,7 @@ function cacheToolAnalyticsParameterOwnership(
563
564
  }
564
565
  }
565
566
 
566
- function collectListedToolNames(tools: ListToolsResult['tools'] | undefined): string[] | undefined {
567
+ function collectListedToolNames(tools: CompatibleToolsListLike['tools'] | undefined): string[] | undefined {
567
568
  if (!tools || tools.length === 0) {
568
569
  return
569
570
  }
@@ -579,10 +580,10 @@ async function getTracedToolsList(
579
580
  event: McpEvent,
580
581
  logger: LoggerFn,
581
582
  requestAttribution: SessionInfo
582
- ): Promise<ListToolsResult['tools']> {
583
+ ): Promise<CompatibleToolsListLike['tools']> {
583
584
  try {
584
585
  const data = getServerTrackingData(server)
585
- const originalResponse = (await originalListToolsHandler(request, extra)) as ListToolsResult
586
+ const originalResponse = (await originalListToolsHandler(request, extra)) as CompatibleToolsListLike
586
587
  // Injection must not mutate arrays reused or frozen by the server.
587
588
  let tools = [...(originalResponse.tools || [])]
588
589
 
@@ -634,7 +635,10 @@ async function getTracedToolsList(
634
635
  }
635
636
  }
636
637
 
637
- export function cacheToolDescriptions(cache: Map<string, string>, tools: ListToolsResult['tools'] | undefined): void {
638
+ export function cacheToolDescriptions(
639
+ cache: Map<string, string>,
640
+ tools: CompatibleToolsListLike['tools'] | undefined
641
+ ): void {
638
642
  if (!tools) {
639
643
  return
640
644
  }
@@ -655,7 +659,10 @@ export function readToolMetaCategory(meta: unknown): string | undefined {
655
659
  return typeof category === 'string' && category.length > 0 ? category : undefined
656
660
  }
657
661
 
658
- export function cacheToolCategories(cache: Map<string, string>, tools: ListToolsResult['tools'] | undefined): void {
662
+ export function cacheToolCategories(
663
+ cache: Map<string, string>,
664
+ tools: CompatibleToolsListLike['tools'] | undefined
665
+ ): void {
659
666
  if (!tools) {
660
667
  return
661
668
  }
@@ -688,8 +695,8 @@ function mintStatelessSessionOnInitialize(
688
695
  extra: CompatibleRequestHandlerExtra | undefined
689
696
  ): string | undefined {
690
697
  try {
691
- const headers = extra?.requestInfo?.headers
692
- if (!headers || typeof headers !== 'object') {
698
+ const headers = getRequestHeaders(extra)
699
+ if (!headers) {
693
700
  return undefined // not an HTTP transport (stdio/in-memory) — nothing to mint into
694
701
  }
695
702
  if (readMcpSessionHeader(headers)) {
@@ -820,7 +827,7 @@ export async function handleInitializeRequest(
820
827
  // Harmless for a legacy `initialize` (client info rides the body there, and the
821
828
  // negotiated protocol version below overrides any `_meta` one); picks up client
822
829
  // info if a client also sends it in `_meta`.
823
- stampMetaClientInfo(event, request)
830
+ stampClientIdentity(event, request, extra, server)
824
831
  stampTransportIdentity(event, extra)
825
832
 
826
833
  await applyResolvedMetadata(event, data, request, extra)
@@ -14,7 +14,7 @@ import type {
14
14
  } from '../types'
15
15
  import { MCPAnalyticsEventType } from './event-types'
16
16
  import { captureEvent } from './capture'
17
- import { stampMetaClientInfo } from './client-identity'
17
+ import { stampClientIdentity } from './client-identity'
18
18
  import { stampTransportIdentity } from './transport-identity'
19
19
 
20
20
  /**
@@ -153,7 +153,7 @@ export async function handleIdentify(
153
153
  parameters: { request, extra },
154
154
  timestamp: new Date(),
155
155
  }
156
- stampMetaClientInfo(identifyEvent, request)
156
+ stampClientIdentity(identifyEvent, request, extra, server)
157
157
  stampTransportIdentity(identifyEvent, extra)
158
158
 
159
159
  try {
@@ -0,0 +1,76 @@
1
+ import type { RequestHeaderBag } from '../types'
2
+
3
+ /**
4
+ * Reads the HTTP request headers off the `extra` (v1) / `ctx` (v2) object the
5
+ * MCP SDK hands a request handler, and returns them as a plain object with
6
+ * lowercase keys. Returns `undefined` when the request did not come over HTTP —
7
+ * stdio and in-memory transports carry no headers at all.
8
+ *
9
+ * The two SDK majors put them in different places and in different shapes: v1
10
+ * attaches a plain object at `extra.requestInfo.headers`, while v2 attaches the
11
+ * WHATWG `Request` at `ctx.http.req`, whose `headers` is a `Headers` instance
12
+ * that only answers to `.get()`. A v1-shaped read returns `undefined` on v2,
13
+ * which is why this exists.
14
+ *
15
+ * Normalisation is **by shape, not by source**: a framework is free to hand us a
16
+ * plain object where the SDK hands `Headers`, so both fields are checked for
17
+ * both shapes. `Headers` is duck-typed on `.entries` rather than `instanceof` —
18
+ * workerd and other edge runtimes are a different realm and would fail the
19
+ * identity check on a perfectly good object.
20
+ *
21
+ * Exported from the package for host callbacks. `identify`, `intentFallback`,
22
+ * `eventProperties` and `beforeSend` receive the SDK's `extra` unchanged — we
23
+ * deliberately do not synthesise a v1 shape on v2, because a fabricated
24
+ * `requestInfo` is a convincing partial lie about a shape the SDK removed on
25
+ * purpose. So a host that reads headers reads them through this instead:
26
+ *
27
+ * ```ts
28
+ * import { getRequestHeaders } from '@posthog/mcp'
29
+ *
30
+ * identify: async (request, extra) => {
31
+ * const auth = getRequestHeaders(extra)?.['authorization']
32
+ * // ...
33
+ * }
34
+ * ```
35
+ */
36
+ export function getRequestHeaders(extra: unknown): RequestHeaderBag | undefined {
37
+ if (!extra || typeof extra !== 'object') {
38
+ return undefined
39
+ }
40
+ const record = extra as Record<string, unknown>
41
+ const http = record.http as { req?: { headers?: unknown } } | undefined
42
+ const requestInfo = record.requestInfo as { headers?: unknown } | undefined
43
+ const source = http?.req?.headers ?? requestInfo?.headers
44
+ if (!source || typeof source !== 'object') {
45
+ return undefined
46
+ }
47
+ return toHeaderBag(source)
48
+ }
49
+
50
+ /**
51
+ * Flattens either shape into a lowercase-keyed bag. Never throws: a header read
52
+ * failing must not take a tool call down with it.
53
+ */
54
+ function toHeaderBag(source: object): RequestHeaderBag | undefined {
55
+ try {
56
+ const entries = isHeadersLike(source) ? [...source.entries()] : Object.entries(source)
57
+ const bag: RequestHeaderBag = {}
58
+ for (const [key, value] of entries) {
59
+ if (typeof key !== 'string' || value === undefined || value === null) {
60
+ continue
61
+ }
62
+ if (typeof value === 'string' || Array.isArray(value)) {
63
+ bag[key.toLowerCase()] = value as string | string[]
64
+ }
65
+ }
66
+ return bag
67
+ } catch {
68
+ return undefined
69
+ }
70
+ }
71
+
72
+ /** Duck-typed, never `instanceof` — an edge runtime's `Headers` is another realm's class. */
73
+ function isHeadersLike(source: object): source is { entries(): Iterable<[string, string]> } {
74
+ const candidate = source as { entries?: unknown; get?: unknown }
75
+ return typeof candidate.entries === 'function' && typeof candidate.get === 'function'
76
+ }
@@ -14,6 +14,7 @@ import type {
14
14
  import { INACTIVITY_TIMEOUT_IN_MINUTES } from './constants'
15
15
  import { deterministicPrefixedId, newPrefixedId } from './ids'
16
16
  import { getServerTrackingData, setServerTrackingData } from './internal'
17
+ import { getRequestHeaders } from './request-headers'
17
18
  import { decodeSessionId, readMcpSessionHeader } from './session-token'
18
19
  import type { SessionTokenPayload } from './session-token'
19
20
 
@@ -110,7 +111,7 @@ function applyTokenClientIdentity(
110
111
  data: MCPAnalyticsData,
111
112
  extra?: CompatibleRequestHandlerExtra
112
113
  ): SessionTokenPayload | undefined {
113
- const token = decodeSessionId(readMcpSessionHeader(extra?.requestInfo?.headers))
114
+ const token = decodeSessionId(readMcpSessionHeader(getRequestHeaders(extra)))
114
115
  if (!token) {
115
116
  return undefined
116
117
  }
@@ -3,7 +3,7 @@
3
3
  // Copyright (c) 2025 AgentCat, Inc. (formerly MCPcat)
4
4
  // Licensed under the MIT License: https://github.com/agentcathq/agentcat-typescript-sdk/blob/main/LICENSE
5
5
 
6
- import { type CallToolResult, type ListToolsResult } from '@modelcontextprotocol/sdk/types.js'
6
+ import type { CompatibleTextToolResult, CompatibleToolsListLike } from '../types'
7
7
  import { log, type LoggerFn } from './logger'
8
8
 
9
9
  export const GET_MORE_TOOLS_NAME = 'get_more_tools' as const
@@ -17,7 +17,7 @@ export function resolveMissingCapabilityToolName(options?: { missingCapabilityTo
17
17
  return options?.missingCapabilityToolName ?? GET_MORE_TOOLS_NAME
18
18
  }
19
19
 
20
- type ReportMissingToolDescriptor = ListToolsResult['tools'][number]
20
+ type ReportMissingToolDescriptor = CompatibleToolsListLike['tools'][number]
21
21
 
22
22
  export function getReportMissingToolDescriptor(name: string = GET_MORE_TOOLS_NAME): ReportMissingToolDescriptor {
23
23
  return {
@@ -53,7 +53,7 @@ export function getReportMissingToolDescriptor(name: string = GET_MORE_TOOLS_NAM
53
53
  * report was recorded (custom dispatcher path); the `instrument()` path returns
54
54
  * it automatically.
55
55
  */
56
- export function getMoreToolsResult(): CallToolResult {
56
+ export function getMoreToolsResult(): CompatibleTextToolResult {
57
57
  return {
58
58
  content: [
59
59
  {
@@ -64,7 +64,7 @@ export function getMoreToolsResult(): CallToolResult {
64
64
  }
65
65
  }
66
66
 
67
- export function handleReportMissing(args: { context: string }, logger: LoggerFn = log): CallToolResult {
67
+ export function handleReportMissing(args: { context: string }, logger: LoggerFn = log): CompatibleTextToolResult {
68
68
  logger(`Missing tool reported: ${JSON.stringify(args)}`)
69
69
  return getMoreToolsResult()
70
70
  }
@@ -1,5 +1,6 @@
1
1
  import type { CompatibleRequestHandlerExtra, McpEvent } from '../types'
2
2
  import { readHeaderValue } from './headers'
3
+ import { getRequestHeaders } from './request-headers'
3
4
 
4
5
  /**
5
6
  * Transport-level client identity: the two request headers that say *which
@@ -43,12 +44,17 @@ export interface TransportIdentity {
43
44
  }
44
45
 
45
46
  /**
46
- * Reads the transport identity headers off `extra.requestInfo.headers`. Returns
47
- * `undefined` when the request carries neither — including every stdio and
48
- * in-memory request, which has no `requestInfo` at all. Never throws.
47
+ * Reads the transport identity headers off the request. Returns `undefined`
48
+ * when the request carries neither — including every stdio and in-memory
49
+ * request, which has no HTTP headers at all. Never throws.
50
+ *
51
+ * Sourced through `getRequestHeaders` rather than `extra.requestInfo.headers`
52
+ * directly: MCP SDK v2 puts the request at `extra.http.req` as a WHATWG
53
+ * `Request`, so a v1-shaped read finds nothing and this whole feature would be
54
+ * silently inert on exactly the servers being adopted now.
49
55
  */
50
56
  export function readTransportIdentity(extra: CompatibleRequestHandlerExtra | undefined): TransportIdentity | undefined {
51
- const headers = extra?.requestInfo?.headers
57
+ const headers = getRequestHeaders(extra)
52
58
  if (!headers) {
53
59
  return undefined
54
60
  }
package/src/index.ts CHANGED
@@ -210,6 +210,9 @@ export {
210
210
  PostHogMCPAnalyticsEvent,
211
211
  PostHogMCPAnalyticsProperty,
212
212
  } from './extensions/constants'
213
+ // Host callbacks receive the SDK's `extra`/`ctx` unchanged, and the two SDK
214
+ // majors carry HTTP headers in different places and shapes. This reads either.
215
+ export { getRequestHeaders } from './extensions/request-headers'
213
216
  export { PostHogMCP, type PostHogMCPOptions } from './extensions/posthog-mcp'
214
217
  export { getMoreToolsResult } from './extensions/tools'
215
218
  export { setLogger } from './extensions/logger'
@@ -229,6 +232,7 @@ export type {
229
232
  MissingCapabilityCaptureData,
230
233
  PreparedToolCall,
231
234
  PrepareToolListOptions,
235
+ RequestHeaderBag,
232
236
  ToolCallCaptureData,
233
237
  ToolsListCaptureData,
234
238
  UserIdentity,
package/src/types.ts CHANGED
@@ -3,8 +3,8 @@
3
3
  // Copyright (c) 2025 AgentCat, Inc. (formerly MCPcat)
4
4
  // Licensed under the MIT License: https://github.com/agentcathq/agentcat-typescript-sdk/blob/main/LICENSE
5
5
 
6
- import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js'
7
6
  import type { ErrorTracking } from '@posthog/core'
7
+ import type { AnalyticsInjectableJsonSchema } from './extensions/analytics-parameters'
8
8
  import type { MCPAnalyticsEventType } from './extensions/event-types'
9
9
  import type { IdentityCache } from './extensions/internal'
10
10
  import type { PostHogCaptureEvent } from './extensions/posthog-events'
@@ -18,6 +18,54 @@ export type ErrorProperties = ErrorTracking.ErrorProperties
18
18
  /** A single parsed stack frame. Re-exported from `@posthog/core`. */
19
19
  export type StackFrame = ErrorTracking.StackFrame
20
20
 
21
+ /**
22
+ * The MCP wire shapes we touch, declared structurally rather than imported from
23
+ * `@modelcontextprotocol/sdk`.
24
+ *
25
+ * Both SDK majors are **optional** peers — a v2-only consumer has no v1 SDK
26
+ * installed — so a type import of it in shipped `.d.ts` output is a `TS2307`
27
+ * for anyone type-checking without `skipLibCheck`. These are deliberately loose
28
+ * (open-ended, everything optional): they describe what we *read* off an SDK
29
+ * result, and an SDK-typed value assigns to them cleanly. What we hand back to
30
+ * the SDK is typed precisely instead — see {@link CompatibleTextToolResult}.
31
+ */
32
+ export interface CompatibleToolResultLike {
33
+ content?: unknown[]
34
+ structuredContent?: JsonRecord
35
+ isError?: boolean
36
+ _meta?: JsonRecord
37
+ [key: string]: unknown
38
+ }
39
+
40
+ /** One entry of a `tools/list` response, as we read it. */
41
+ export interface CompatibleToolDescriptorLike {
42
+ name: string
43
+ title?: string
44
+ description?: string
45
+ /** The advertised JSON Schema, as the analytics parameters are injected into it. */
46
+ inputSchema?: AnalyticsInjectableJsonSchema
47
+ outputSchema?: unknown
48
+ _meta?: JsonRecord
49
+ [key: string]: unknown
50
+ }
51
+
52
+ /** A `tools/list` response, as we read it. */
53
+ export interface CompatibleToolsListLike {
54
+ tools: CompatibleToolDescriptorLike[]
55
+ nextCursor?: string
56
+ [key: string]: unknown
57
+ }
58
+
59
+ /**
60
+ * A text-only tool result *we* construct and a host may hand straight back to
61
+ * the SDK. Typed precisely, not loosely, so it stays assignable to the SDK's own
62
+ * `CallToolResult` — the direction that would silently break callers.
63
+ */
64
+ export interface CompatibleTextToolResult {
65
+ content: { type: 'text'; text: string }[]
66
+ isError?: boolean
67
+ }
68
+
21
69
  export interface MCPRequestParamsLike {
22
70
  arguments?: JsonRecord
23
71
  name?: string
@@ -128,8 +176,11 @@ export type MaybePromise<T> = T | Promise<T>
128
176
  export type MCPAnalyticsIntentSource = 'context_parameter' | 'inferred'
129
177
 
130
178
  export type ToolCallback =
131
- | ((args: unknown, extra: CompatibleRequestHandlerExtra) => CallToolResult | Promise<CallToolResult>)
132
- | ((extra: CompatibleRequestHandlerExtra) => CallToolResult | Promise<CallToolResult>)
179
+ | ((
180
+ args: unknown,
181
+ extra: CompatibleRequestHandlerExtra
182
+ ) => CompatibleToolResultLike | Promise<CompatibleToolResultLike>)
183
+ | ((extra: CompatibleRequestHandlerExtra) => CompatibleToolResultLike | Promise<CompatibleToolResultLike>)
133
184
 
134
185
  // RegisteredTool type that supports both MCP SDK 1.23- (callback) and 1.24+ (handler)
135
186
  export type RegisteredTool = {
@@ -225,14 +276,38 @@ export interface CompatibleRequestInfoLike {
225
276
  [key: string]: unknown
226
277
  }
227
278
 
279
+ /**
280
+ * The HTTP request an MCP SDK v2 server attaches to the handler context, as a
281
+ * WHATWG `Request` — so `headers` answers to `.get()`, not to indexing.
282
+ */
283
+ export interface CompatibleHttpRequestLike {
284
+ req?: { headers?: unknown; [key: string]: unknown }
285
+ [key: string]: unknown
286
+ }
287
+
228
288
  export interface CompatibleRequestHandlerExtra {
229
289
  headers?: Record<string, string | string[]>
230
290
  sessionId?: string
231
291
  /** Present on HTTP transports only — headers ride every request, unlike `clientInfo`. */
232
292
  requestInfo?: CompatibleRequestInfoLike
293
+ /** Where MCP SDK v2 puts the same thing. Read both through `getRequestHeaders`. */
294
+ http?: CompatibleHttpRequestLike
295
+ /**
296
+ * MCP SDK v2's parsed request. It lifts the reserved `io.modelcontextprotocol/*`
297
+ * keys out of `params._meta` into `envelope`, so client identity is here rather
298
+ * than on the request by the time a handler runs.
299
+ */
300
+ mcpReq?: { envelope?: Record<string, unknown>; [key: string]: unknown }
233
301
  [key: string]: unknown
234
302
  }
235
303
 
304
+ /**
305
+ * HTTP headers normalised to lowercase keys, as `getRequestHeaders` returns
306
+ * them — a plain bag, so a host's existing `headers['authorization']` keeps
307
+ * working when only the source of the headers changes.
308
+ */
309
+ export type RequestHeaderBag = Record<string, string | string[]>
310
+
236
311
  export interface ServerClientInfoLike {
237
312
  name?: string
238
313
  version?: string
package/src/version.ts CHANGED
@@ -1 +1 @@
1
- export const version = '0.11.0'
1
+ export const version = '0.11.2'