@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.
- package/README.md +39 -0
- package/dist/extensions/client-identity.d.ts +70 -15
- package/dist/extensions/client-identity.d.ts.map +1 -1
- package/dist/extensions/client-identity.js +72 -17
- package/dist/extensions/client-identity.mjs +59 -13
- package/dist/extensions/compatibility.d.ts.map +1 -1
- package/dist/extensions/compatibility.js +14 -18
- package/dist/extensions/compatibility.mjs +10 -14
- package/dist/extensions/detect.d.ts +51 -0
- package/dist/extensions/detect.d.ts.map +1 -0
- package/dist/extensions/detect.js +96 -0
- package/dist/extensions/detect.mjs +38 -0
- package/dist/extensions/instrument-highlevel.d.ts.map +1 -1
- package/dist/extensions/instrumentation.d.ts +4 -5
- package/dist/extensions/instrumentation.d.ts.map +1 -1
- package/dist/extensions/instrumentation.js +6 -5
- package/dist/extensions/instrumentation.mjs +7 -6
- package/dist/extensions/internal.js +1 -1
- package/dist/extensions/internal.mjs +2 -2
- package/dist/extensions/request-headers.d.ts +36 -0
- package/dist/extensions/request-headers.d.ts.map +1 -0
- package/dist/extensions/request-headers.js +62 -0
- package/dist/extensions/request-headers.mjs +28 -0
- package/dist/extensions/session.d.ts.map +1 -1
- package/dist/extensions/session.js +2 -1
- package/dist/extensions/session.mjs +2 -1
- package/dist/extensions/tools.d.ts +4 -4
- package/dist/extensions/tools.d.ts.map +1 -1
- package/dist/extensions/transport-identity.d.ts +8 -3
- package/dist/extensions/transport-identity.d.ts.map +1 -1
- package/dist/extensions/transport-identity.js +2 -1
- package/dist/extensions/transport-identity.mjs +2 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +11 -7
- package/dist/index.mjs +2 -1
- package/dist/types.d.ts +77 -2
- 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 +12 -2
- package/src/extensions/client-identity.ts +146 -31
- package/src/extensions/compatibility.ts +25 -24
- package/src/extensions/detect.ts +87 -0
- package/src/extensions/instrument-highlevel.ts +4 -4
- package/src/extensions/instrumentation.ts +22 -15
- package/src/extensions/internal.ts +2 -2
- package/src/extensions/request-headers.ts +76 -0
- package/src/extensions/session.ts +2 -1
- package/src/extensions/tools.ts +4 -4
- package/src/extensions/transport-identity.ts +10 -4
- package/src/index.ts +4 -0
- package/src/types.ts +78 -3
- 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<
|
|
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<
|
|
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<
|
|
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 {
|
|
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
|
-
|
|
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
|
|
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:
|
|
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
|
-
|
|
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:
|
|
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:
|
|
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<
|
|
583
|
+
): Promise<CompatibleToolsListLike['tools']> {
|
|
583
584
|
try {
|
|
584
585
|
const data = getServerTrackingData(server)
|
|
585
|
-
const originalResponse = (await originalListToolsHandler(request, extra)) as
|
|
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(
|
|
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(
|
|
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
|
|
692
|
-
if (!headers
|
|
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
|
-
|
|
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 {
|
|
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
|
-
|
|
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
|
|
114
|
+
const token = decodeSessionId(readMcpSessionHeader(getRequestHeaders(extra)))
|
|
114
115
|
if (!token) {
|
|
115
116
|
return undefined
|
|
116
117
|
}
|
package/src/extensions/tools.ts
CHANGED
|
@@ -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 {
|
|
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 =
|
|
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():
|
|
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):
|
|
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
|
|
47
|
-
*
|
|
48
|
-
*
|
|
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
|
|
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
|
-
| ((
|
|
132
|
-
|
|
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.
|
|
1
|
+
export const version = '0.11.2'
|