@posthog/mcp 0.8.0 → 0.9.1
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/LICENSE +2 -2
- package/README.md +59 -0
- package/dist/extensions/capture.d.ts.map +1 -1
- package/dist/extensions/compatibility.d.ts.map +1 -1
- package/dist/extensions/constants.d.ts.map +1 -1
- package/dist/extensions/context-parameters.d.ts.map +1 -1
- package/dist/extensions/conversation-id.d.ts.map +1 -1
- package/dist/extensions/event-types.d.ts.map +1 -1
- package/dist/extensions/exceptions.d.ts.map +1 -1
- package/dist/extensions/ids.d.ts.map +1 -1
- package/dist/extensions/instrument-highlevel.d.ts.map +1 -1
- package/dist/extensions/instrument-lowlevel.d.ts.map +1 -1
- package/dist/extensions/instrumentation.d.ts.map +1 -1
- package/dist/extensions/instrumentation.js +39 -3
- package/dist/extensions/instrumentation.mjs +41 -5
- package/dist/extensions/intent.d.ts.map +1 -1
- package/dist/extensions/internal.d.ts +4 -2
- package/dist/extensions/internal.d.ts.map +1 -1
- package/dist/extensions/internal.js +5 -2
- package/dist/extensions/internal.mjs +5 -2
- package/dist/extensions/logger.d.ts.map +1 -1
- package/dist/extensions/mcp-payloads.d.ts.map +1 -1
- package/dist/extensions/mcp-sdk-compat.d.ts.map +1 -1
- package/dist/extensions/posthog-events.d.ts.map +1 -1
- package/dist/extensions/sanitization.d.ts.map +1 -1
- package/dist/extensions/session-token.d.ts +46 -0
- package/dist/extensions/session-token.d.ts.map +1 -0
- package/dist/extensions/session-token.js +122 -0
- package/dist/extensions/session-token.mjs +76 -0
- package/dist/extensions/session.d.ts +5 -4
- package/dist/extensions/session.d.ts.map +1 -1
- package/dist/extensions/session.js +25 -33
- package/dist/extensions/session.mjs +22 -27
- package/dist/extensions/sink.d.ts.map +1 -1
- package/dist/extensions/tools.d.ts.map +1 -1
- package/dist/extensions/tracing-helpers.d.ts.map +1 -1
- package/dist/extensions/truncation.d.ts.map +1 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +15 -2
- package/dist/index.mjs +2 -1
- package/dist/types.d.ts +18 -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 +3 -3
- package/src/extensions/capture.ts +4 -3
- package/src/extensions/compatibility.ts +4 -3
- package/src/extensions/constants.ts +4 -3
- package/src/extensions/context-parameters.ts +4 -3
- package/src/extensions/conversation-id.ts +4 -3
- package/src/extensions/event-types.ts +4 -3
- package/src/extensions/exceptions.ts +4 -3
- package/src/extensions/ids.ts +4 -3
- package/src/extensions/instrument-highlevel.ts +4 -3
- package/src/extensions/instrument-lowlevel.ts +4 -3
- package/src/extensions/instrumentation.ts +85 -9
- package/src/extensions/intent.ts +4 -3
- package/src/extensions/internal.ts +23 -7
- package/src/extensions/logger.ts +4 -3
- package/src/extensions/mcp-payloads.ts +4 -3
- package/src/extensions/mcp-sdk-compat.ts +4 -3
- package/src/extensions/posthog-events.ts +4 -3
- package/src/extensions/sanitization.ts +4 -3
- package/src/extensions/session-token.ts +168 -0
- package/src/extensions/session.ts +42 -40
- package/src/extensions/sink.ts +4 -3
- package/src/extensions/tools.ts +4 -3
- package/src/extensions/tracing-helpers.ts +4 -3
- package/src/extensions/truncation.ts +4 -3
- package/src/index.ts +14 -4
- package/src/types.ts +24 -5
- package/src/version.ts +1 -1
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
// Portions of this file are derived from
|
|
2
|
-
//
|
|
3
|
-
//
|
|
1
|
+
// Portions of this file are derived from agentcathq/agentcat-typescript-sdk
|
|
2
|
+
// (formerly MCPCat/mcpcat-typescript-sdk)
|
|
3
|
+
// Copyright (c) 2025 AgentCat, Inc. (formerly MCPcat)
|
|
4
|
+
// Licensed under the MIT License: https://github.com/agentcathq/agentcat-typescript-sdk/blob/main/LICENSE
|
|
4
5
|
|
|
5
6
|
import { ErrorTracking as CoreErrorTracking } from '@posthog/core'
|
|
6
7
|
|
package/src/extensions/ids.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
// Portions of this file are derived from
|
|
2
|
-
//
|
|
3
|
-
//
|
|
1
|
+
// Portions of this file are derived from agentcathq/agentcat-typescript-sdk
|
|
2
|
+
// (formerly MCPCat/mcpcat-typescript-sdk)
|
|
3
|
+
// Copyright (c) 2025 AgentCat, Inc. (formerly MCPcat)
|
|
4
|
+
// Licensed under the MIT License: https://github.com/agentcathq/agentcat-typescript-sdk/blob/main/LICENSE
|
|
4
5
|
|
|
5
6
|
import { uuidv7 } from '@posthog/core'
|
|
6
7
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
// Portions of this file are derived from
|
|
2
|
-
//
|
|
3
|
-
//
|
|
1
|
+
// Portions of this file are derived from agentcathq/agentcat-typescript-sdk
|
|
2
|
+
// (formerly MCPCat/mcpcat-typescript-sdk)
|
|
3
|
+
// Copyright (c) 2025 AgentCat, Inc. (formerly MCPcat)
|
|
4
|
+
// Licensed under the MIT License: https://github.com/agentcathq/agentcat-typescript-sdk/blob/main/LICENSE
|
|
4
5
|
|
|
5
6
|
import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js'
|
|
6
7
|
import type { CompatibleRequestHandlerExtra, HighLevelMCPServerLike, MCPServerLike, RegisteredTool } from '../types'
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
// Portions of this file are derived from
|
|
2
|
-
//
|
|
3
|
-
//
|
|
1
|
+
// Portions of this file are derived from agentcathq/agentcat-typescript-sdk
|
|
2
|
+
// (formerly MCPCat/mcpcat-typescript-sdk)
|
|
3
|
+
// Copyright (c) 2025 AgentCat, Inc. (formerly MCPcat)
|
|
4
|
+
// Licensed under the MIT License: https://github.com/agentcathq/agentcat-typescript-sdk/blob/main/LICENSE
|
|
4
5
|
|
|
5
6
|
import { CallToolRequestSchema } from '@modelcontextprotocol/sdk/types.js'
|
|
6
7
|
import type { CompatibleRequestHandlerExtra, MCPRequestLike, MCPServerLike } from '../types'
|
|
@@ -1,9 +1,17 @@
|
|
|
1
|
-
// Portions of this file are derived from
|
|
2
|
-
//
|
|
3
|
-
//
|
|
1
|
+
// Portions of this file are derived from agentcathq/agentcat-typescript-sdk
|
|
2
|
+
// (formerly MCPCat/mcpcat-typescript-sdk)
|
|
3
|
+
// Copyright (c) 2025 AgentCat, Inc. (formerly MCPcat)
|
|
4
|
+
// Licensed under the MIT License: https://github.com/agentcathq/agentcat-typescript-sdk/blob/main/LICENSE
|
|
4
5
|
|
|
5
6
|
import type { ListToolsResult } from '@modelcontextprotocol/sdk/types.js'
|
|
6
|
-
import type {
|
|
7
|
+
import type {
|
|
8
|
+
CompatibleRequestHandlerExtra,
|
|
9
|
+
MCPAnalyticsData,
|
|
10
|
+
MCPRequestLike,
|
|
11
|
+
MCPServerLike,
|
|
12
|
+
McpEvent,
|
|
13
|
+
ServerClientInfoLike,
|
|
14
|
+
} from '../types'
|
|
7
15
|
import { addContextParameterToTools, getContextDescription, isContextEnabled } from './context-parameters'
|
|
8
16
|
import {
|
|
9
17
|
addConversationIdToTools,
|
|
@@ -17,11 +25,12 @@ import { captureEvent } from './capture'
|
|
|
17
25
|
import { MCPAnalyticsEventType } from './event-types'
|
|
18
26
|
import { captureException } from './exceptions'
|
|
19
27
|
import { resolveToolCallIntent, setEventIntent, setExplicitContextIntent } from './intent'
|
|
20
|
-
import { getServerTrackingData, handleIdentify } from './internal'
|
|
28
|
+
import { getServerTrackingData, handleIdentify, setServerTrackingData } from './internal'
|
|
21
29
|
import { log } from './logger'
|
|
22
30
|
import { buildCapturedMcpParameters } from './mcp-payloads'
|
|
23
31
|
import { getLiteralValue, getObjectShape } from './mcp-sdk-compat'
|
|
24
|
-
import {
|
|
32
|
+
import { getSessionId, newSessionId } from './session'
|
|
33
|
+
import { encodeSessionId, readMcpSessionHeader, writeSessionIdToTransport } from './session-token'
|
|
25
34
|
import { getReportMissingToolDescriptor, resolveMissingCapabilityToolName } from './tools'
|
|
26
35
|
import { applyResolvedMetadata, isToolResultError } from './tracing-helpers'
|
|
27
36
|
|
|
@@ -129,7 +138,7 @@ async function prepareToolCallEvent(
|
|
|
129
138
|
eventType: MCPAnalyticsEventType
|
|
130
139
|
): Promise<McpEvent | null> {
|
|
131
140
|
try {
|
|
132
|
-
const sessionId =
|
|
141
|
+
const sessionId = getSessionId(server, extra)
|
|
133
142
|
await handleIdentify(server, data, sessionId, request, extra)
|
|
134
143
|
|
|
135
144
|
const toolName = request.params?.name
|
|
@@ -283,7 +292,7 @@ export async function handleListToolsRequest(
|
|
|
283
292
|
const data = getServerTrackingData(server)
|
|
284
293
|
const startTime = new Date()
|
|
285
294
|
const event: McpEvent = {
|
|
286
|
-
sessionId:
|
|
295
|
+
sessionId: getSessionId(server, extra),
|
|
287
296
|
parameters: buildCapturedMcpParameters(request),
|
|
288
297
|
eventType: MCPAnalyticsEventType.mcpToolsList,
|
|
289
298
|
timestamp: startTime,
|
|
@@ -410,6 +419,71 @@ export function cacheToolCategories(cache: Map<string, string>, tools: ListTools
|
|
|
410
419
|
|
|
411
420
|
// --- initialize -----------------------------------------------------------
|
|
412
421
|
|
|
422
|
+
/**
|
|
423
|
+
* Stateless servers never issue a session id, so sessions fragment and the
|
|
424
|
+
* client name/version is lost after `initialize`. Fix: mint the
|
|
425
|
+
* `Mcp-Session-Id` response header as a token carrying both. Clients replay
|
|
426
|
+
* the header on every request, so any pod recovers them with no server-side
|
|
427
|
+
* store (decoded in `getSessionId`).
|
|
428
|
+
*
|
|
429
|
+
* The header only reaches the wire when response headers are built after the
|
|
430
|
+
* handler runs — StreamableHTTP with `enableJsonResponse: true`. SSE flushes
|
|
431
|
+
* headers first; those servers set the header themselves with the exported
|
|
432
|
+
* `encodeSessionId`, and this mint is a harmless no-op.
|
|
433
|
+
*/
|
|
434
|
+
function mintStatelessSessionOnInitialize(
|
|
435
|
+
server: MCPServerLike,
|
|
436
|
+
data: MCPAnalyticsData,
|
|
437
|
+
request: MCPRequestLike,
|
|
438
|
+
extra: CompatibleRequestHandlerExtra | undefined
|
|
439
|
+
): void {
|
|
440
|
+
try {
|
|
441
|
+
const headers = extra?.requestInfo?.headers
|
|
442
|
+
if (!headers || typeof headers !== 'object') {
|
|
443
|
+
return // not an HTTP transport (stdio/in-memory) — nothing to mint into
|
|
444
|
+
}
|
|
445
|
+
if (readMcpSessionHeader(headers)) {
|
|
446
|
+
return // client already replays a session id (ours or the transport's)
|
|
447
|
+
}
|
|
448
|
+
const transport = server.transport
|
|
449
|
+
if (!transport || extra?.sessionId || transport.sessionId) {
|
|
450
|
+
return // stateful transports manage their own session id — leave it alone
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
const sessionId = newSessionId()
|
|
454
|
+
const clientInfo = readInitializeClientInfo(request)
|
|
455
|
+
const token = encodeSessionId({ sessionId, clientName: clientInfo?.name, clientVersion: clientInfo?.version })
|
|
456
|
+
if (!writeSessionIdToTransport(transport, token)) {
|
|
457
|
+
return // transport can't carry a response session id — keep generated behavior
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
data.sessionId = sessionId
|
|
461
|
+
data.sessionSource = 'token'
|
|
462
|
+
data.sessionInfo.clientName = clientInfo?.name
|
|
463
|
+
data.sessionInfo.clientVersion = clientInfo?.version
|
|
464
|
+
data.lastActivity = new Date()
|
|
465
|
+
setServerTrackingData(server, data)
|
|
466
|
+
} catch (error) {
|
|
467
|
+
log(`Warning: PostHog MCP analytics failed to mint a stateless session id - ${error}`)
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
/**
|
|
472
|
+
* Read the client name/version off the `initialize` request body — the SDK
|
|
473
|
+
* hasn't stored it yet (`getClientVersion()`) when our patch runs.
|
|
474
|
+
*/
|
|
475
|
+
function readInitializeClientInfo(request: MCPRequestLike): ServerClientInfoLike | undefined {
|
|
476
|
+
const clientInfo = request.params?.clientInfo
|
|
477
|
+
if (!clientInfo || typeof clientInfo !== 'object') {
|
|
478
|
+
return undefined
|
|
479
|
+
}
|
|
480
|
+
const { name, version } = clientInfo as Record<string, unknown>
|
|
481
|
+
return {
|
|
482
|
+
name: typeof name === 'string' ? name : undefined,
|
|
483
|
+
version: typeof version === 'string' ? version : undefined,
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
|
|
413
487
|
/**
|
|
414
488
|
* Captures the connection handshake (and resolves identity) on `initialize`
|
|
415
489
|
* before the original handler runs.
|
|
@@ -428,7 +502,9 @@ export async function handleInitializeRequest(
|
|
|
428
502
|
return await originalInitializeHandler(request, extra)
|
|
429
503
|
}
|
|
430
504
|
|
|
431
|
-
|
|
505
|
+
// Mint first so the `$mcp_initialize` event below already carries the minted id.
|
|
506
|
+
mintStatelessSessionOnInitialize(server, data, request, extra)
|
|
507
|
+
const sessionId = getSessionId(server, extra)
|
|
432
508
|
await handleIdentify(server, data, sessionId, request, extra)
|
|
433
509
|
|
|
434
510
|
const event: McpEvent = {
|
package/src/extensions/intent.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
// Portions of this file are derived from
|
|
2
|
-
//
|
|
3
|
-
//
|
|
1
|
+
// Portions of this file are derived from agentcathq/agentcat-typescript-sdk
|
|
2
|
+
// (formerly MCPCat/mcpcat-typescript-sdk)
|
|
3
|
+
// Copyright (c) 2025 AgentCat, Inc. (formerly MCPcat)
|
|
4
|
+
// Licensed under the MIT License: https://github.com/agentcathq/agentcat-typescript-sdk/blob/main/LICENSE
|
|
4
5
|
|
|
5
6
|
import type {
|
|
6
7
|
CompatibleRequestHandlerExtra,
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
// Portions of this file are derived from
|
|
2
|
-
//
|
|
3
|
-
//
|
|
1
|
+
// Portions of this file are derived from agentcathq/agentcat-typescript-sdk
|
|
2
|
+
// (formerly MCPCat/mcpcat-typescript-sdk)
|
|
3
|
+
// Copyright (c) 2025 AgentCat, Inc. (formerly MCPcat)
|
|
4
|
+
// Licensed under the MIT License: https://github.com/agentcathq/agentcat-typescript-sdk/blob/main/LICENSE
|
|
4
5
|
|
|
5
6
|
import type {
|
|
6
7
|
CompatibleRequestHandlerExtra,
|
|
@@ -117,8 +118,10 @@ export function mergeIdentities(previous: UserIdentity | undefined, next: UserId
|
|
|
117
118
|
}
|
|
118
119
|
|
|
119
120
|
/**
|
|
120
|
-
* Resolves the optional `identify` callback
|
|
121
|
-
*
|
|
121
|
+
* Resolves the optional `identify` callback on every request (the resolved
|
|
122
|
+
* identity is what stamps `distinct_id`/`$set` onto events), but publishes the
|
|
123
|
+
* `$identify` event at most once per session: at `initialize`, when a
|
|
124
|
+
* long-lived server first sees the identity, or when it materially changes.
|
|
122
125
|
*/
|
|
123
126
|
export async function handleIdentify(
|
|
124
127
|
server: MCPServerLike,
|
|
@@ -146,11 +149,24 @@ export async function handleIdentify(
|
|
|
146
149
|
if (identityResult) {
|
|
147
150
|
const previousIdentity = data.identifiedSessions.get(sessionId)
|
|
148
151
|
const mergedIdentity = mergeIdentities(previousIdentity, identityResult)
|
|
149
|
-
|
|
152
|
+
// "First seen" means new to this instance's cache — which on a stateless
|
|
153
|
+
// pod (fresh instrument() per request) is every request. A token session
|
|
154
|
+
// was already announced by whichever pod handled `initialize`, so only the
|
|
155
|
+
// handshake, or a genuine change a long-lived server observed, publishes
|
|
156
|
+
// $identify. Every event still carries distinct_id/$set regardless, so
|
|
157
|
+
// person properties are never lost. Known gap: if identity is null at
|
|
158
|
+
// `initialize` but resolves later on a token session, that first $identify
|
|
159
|
+
// is suppressed too, so any pre-identify (anonymous) events aren't aliased
|
|
160
|
+
// onto the user. Inherent to statelessness (no pod knows a sibling already
|
|
161
|
+
// announced); revisit with the stateless-by-default rework.
|
|
162
|
+
const changed = previousIdentity !== undefined && !areIdentitiesEqual(previousIdentity, mergedIdentity)
|
|
163
|
+
const firstSeen = previousIdentity === undefined
|
|
164
|
+
const announcedAtInitialize = data.sessionSource === 'token' && request.method !== 'initialize'
|
|
165
|
+
const shouldPublish = changed || (firstSeen && !announcedAtInitialize)
|
|
150
166
|
|
|
151
167
|
data.identifiedSessions.set(sessionId, mergedIdentity)
|
|
152
168
|
|
|
153
|
-
if (
|
|
169
|
+
if (shouldPublish) {
|
|
154
170
|
log(`Identified session ${sessionId} with identity: ${JSON.stringify(mergedIdentity)}`)
|
|
155
171
|
captureEvent(server, identifyEvent)
|
|
156
172
|
}
|
package/src/extensions/logger.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
// Portions of this file are derived from
|
|
2
|
-
//
|
|
3
|
-
//
|
|
1
|
+
// Portions of this file are derived from agentcathq/agentcat-typescript-sdk
|
|
2
|
+
// (formerly MCPCat/mcpcat-typescript-sdk)
|
|
3
|
+
// Copyright (c) 2025 AgentCat, Inc. (formerly MCPcat)
|
|
4
|
+
// Licensed under the MIT License: https://github.com/agentcathq/agentcat-typescript-sdk/blob/main/LICENSE
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* STDIO-safe logger.
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
// Portions of this file are derived from
|
|
2
|
-
//
|
|
3
|
-
//
|
|
1
|
+
// Portions of this file are derived from agentcathq/agentcat-typescript-sdk
|
|
2
|
+
// (formerly MCPCat/mcpcat-typescript-sdk)
|
|
3
|
+
// Copyright (c) 2025 AgentCat, Inc. (formerly MCPcat)
|
|
4
|
+
// Licensed under the MIT License: https://github.com/agentcathq/agentcat-typescript-sdk/blob/main/LICENSE
|
|
4
5
|
|
|
5
6
|
const CONTEXT_ARGUMENT_NAME = 'context'
|
|
6
7
|
const REDACTED_VALUE = '[redacted]'
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
// Portions of this file are derived from
|
|
2
|
-
//
|
|
3
|
-
//
|
|
1
|
+
// Portions of this file are derived from agentcathq/agentcat-typescript-sdk
|
|
2
|
+
// (formerly MCPCat/mcpcat-typescript-sdk)
|
|
3
|
+
// Copyright (c) 2025 AgentCat, Inc. (formerly MCPcat)
|
|
4
|
+
// Licensed under the MIT License: https://github.com/agentcathq/agentcat-typescript-sdk/blob/main/LICENSE
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* MCP SDK Compatibility Helpers
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
// Portions of this file are derived from
|
|
2
|
-
//
|
|
3
|
-
//
|
|
1
|
+
// Portions of this file are derived from agentcathq/agentcat-typescript-sdk
|
|
2
|
+
// (formerly MCPCat/mcpcat-typescript-sdk)
|
|
3
|
+
// Copyright (c) 2025 AgentCat, Inc. (formerly MCPcat)
|
|
4
|
+
// Licensed under the MIT License: https://github.com/agentcathq/agentcat-typescript-sdk/blob/main/LICENSE
|
|
4
5
|
|
|
5
6
|
import type { Event } from '../types'
|
|
6
7
|
import { POSTHOG_MCP_ANALYTICS_SOURCE, PostHogMCPAnalyticsEvent, PostHogMCPAnalyticsProperty } from './constants'
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
// Portions of this file are derived from
|
|
2
|
-
//
|
|
3
|
-
//
|
|
1
|
+
// Portions of this file are derived from agentcathq/agentcat-typescript-sdk
|
|
2
|
+
// (formerly MCPCat/mcpcat-typescript-sdk)
|
|
3
|
+
// Copyright (c) 2025 AgentCat, Inc. (formerly MCPcat)
|
|
4
|
+
// Licensed under the MIT License: https://github.com/agentcathq/agentcat-typescript-sdk/blob/main/LICENSE
|
|
4
5
|
|
|
5
6
|
import type { Event, McpEvent } from '../types'
|
|
6
7
|
import { sanitizeCapturedValue } from './mcp-payloads'
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Self-encoded session tokens for stateless / multi-pod MCP servers.
|
|
3
|
+
*
|
|
4
|
+
* A stateless server keeps nothing between requests, so every request starts a
|
|
5
|
+
* new session and the client name/version (only sent at `initialize`) is lost.
|
|
6
|
+
* The one value clients replay on every request is the `Mcp-Session-Id` header.
|
|
7
|
+
* So at `initialize` we mint that header as a token carrying the session id and
|
|
8
|
+
* client identity — any pod can read them back from the header alone.
|
|
9
|
+
*
|
|
10
|
+
* The token is unsigned: it holds only what the client already self-reports.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
export const MCP_SESSION_HEADER = 'mcp-session-id'
|
|
14
|
+
|
|
15
|
+
/** What a session token carries. */
|
|
16
|
+
export interface SessionTokenPayload {
|
|
17
|
+
/** PostHog session id (`ses_…`) → `$session_id`. */
|
|
18
|
+
sessionId: string
|
|
19
|
+
/** MCP client name → `$mcp_client_name`. */
|
|
20
|
+
clientName?: string
|
|
21
|
+
/** MCP client version → `$mcp_client_version`. */
|
|
22
|
+
clientVersion?: string
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// On the wire the token is base64url(JSON) with shortened keys to keep the
|
|
26
|
+
// header small: sid = sessionId, cn = clientName, cv = clientVersion.
|
|
27
|
+
const MAX_TOKEN_LENGTH = 4096
|
|
28
|
+
const MAX_SESSION_ID_LENGTH = 128
|
|
29
|
+
const MAX_CLIENT_FIELD_LENGTH = 200
|
|
30
|
+
|
|
31
|
+
const BASE64URL_PATTERN = /^[A-Za-z0-9_-]+={0,2}$/
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Encodes a session token for the `Mcp-Session-Id` response header. Also
|
|
35
|
+
* exported for SSE servers, which flush headers before handlers run and so
|
|
36
|
+
* must set the header themselves at the HTTP layer.
|
|
37
|
+
*/
|
|
38
|
+
export function encodeSessionId(payload: SessionTokenPayload): string {
|
|
39
|
+
if (!payload || typeof payload.sessionId !== 'string' || payload.sessionId.length === 0) {
|
|
40
|
+
throw new Error('encodeSessionId requires a non-empty `sessionId` (use newSessionId())')
|
|
41
|
+
}
|
|
42
|
+
const wire: Record<string, string> = { sid: payload.sessionId }
|
|
43
|
+
if (typeof payload.clientName === 'string' && payload.clientName.length > 0) {
|
|
44
|
+
wire.cn = payload.clientName.slice(0, MAX_CLIENT_FIELD_LENGTH)
|
|
45
|
+
}
|
|
46
|
+
if (typeof payload.clientVersion === 'string' && payload.clientVersion.length > 0) {
|
|
47
|
+
wire.cv = payload.clientVersion.slice(0, MAX_CLIENT_FIELD_LENGTH)
|
|
48
|
+
}
|
|
49
|
+
return utf8ToBase64Url(JSON.stringify(wire))
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Decodes an `Mcp-Session-Id` value into a token payload. Returns `null` for
|
|
54
|
+
* anything that isn't one of our tokens (transport UUIDs, JWTs, garbage) and
|
|
55
|
+
* never throws.
|
|
56
|
+
*/
|
|
57
|
+
export function decodeSessionId(value: unknown): SessionTokenPayload | null {
|
|
58
|
+
if (typeof value !== 'string' || value.length === 0 || value.length > MAX_TOKEN_LENGTH) {
|
|
59
|
+
return null
|
|
60
|
+
}
|
|
61
|
+
// JWTs carry dots; UUIDs pass this check but fail JSON.parse below.
|
|
62
|
+
if (!BASE64URL_PATTERN.test(value)) {
|
|
63
|
+
return null
|
|
64
|
+
}
|
|
65
|
+
let parsed: unknown
|
|
66
|
+
try {
|
|
67
|
+
parsed = JSON.parse(base64UrlToUtf8(value))
|
|
68
|
+
} catch {
|
|
69
|
+
return null
|
|
70
|
+
}
|
|
71
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
72
|
+
return null
|
|
73
|
+
}
|
|
74
|
+
const wire = parsed as Record<string, unknown>
|
|
75
|
+
if (typeof wire.sid !== 'string' || wire.sid.length === 0 || wire.sid.length > MAX_SESSION_ID_LENGTH) {
|
|
76
|
+
return null
|
|
77
|
+
}
|
|
78
|
+
const payload: SessionTokenPayload = { sessionId: wire.sid }
|
|
79
|
+
// A bad cn/cv just means no client info — it does not reject the token.
|
|
80
|
+
if (typeof wire.cn === 'string' && wire.cn.length > 0) {
|
|
81
|
+
payload.clientName = wire.cn.slice(0, MAX_CLIENT_FIELD_LENGTH)
|
|
82
|
+
}
|
|
83
|
+
if (typeof wire.cv === 'string' && wire.cv.length > 0) {
|
|
84
|
+
payload.clientVersion = wire.cv.slice(0, MAX_CLIENT_FIELD_LENGTH)
|
|
85
|
+
}
|
|
86
|
+
return payload
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Reads the `mcp-session-id` header off `extra.requestInfo.headers`. The SDK
|
|
91
|
+
* transports lowercase header keys; the fallback scan covers hand-built extras.
|
|
92
|
+
*/
|
|
93
|
+
export function readMcpSessionHeader(headers: unknown): string | undefined {
|
|
94
|
+
if (!headers || typeof headers !== 'object') {
|
|
95
|
+
return undefined
|
|
96
|
+
}
|
|
97
|
+
const record = headers as Record<string, unknown>
|
|
98
|
+
let value = record[MCP_SESSION_HEADER]
|
|
99
|
+
if (value === undefined) {
|
|
100
|
+
const key = Object.keys(record).find((k) => k.toLowerCase() === MCP_SESSION_HEADER)
|
|
101
|
+
value = key === undefined ? undefined : record[key]
|
|
102
|
+
}
|
|
103
|
+
const first = Array.isArray(value) ? value[0] : value
|
|
104
|
+
if (typeof first !== 'string') {
|
|
105
|
+
return undefined
|
|
106
|
+
}
|
|
107
|
+
const trimmed = first.trim()
|
|
108
|
+
return trimmed.length > 0 ? trimmed : undefined
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Puts a minted token on the transport so it goes out as the `Mcp-Session-Id`
|
|
113
|
+
* response header. The Node transport's `sessionId` is a getter backed by an
|
|
114
|
+
* inner web-standard transport, so verify the write by reading back and fall
|
|
115
|
+
* back to writing the inner one. Returns whether the write stuck; never throws.
|
|
116
|
+
*/
|
|
117
|
+
export function writeSessionIdToTransport(transport: unknown, token: string): boolean {
|
|
118
|
+
if (!transport || typeof transport !== 'object') {
|
|
119
|
+
return false
|
|
120
|
+
}
|
|
121
|
+
const outer = transport as { sessionId?: string; _webStandardTransport?: unknown }
|
|
122
|
+
try {
|
|
123
|
+
outer.sessionId = token
|
|
124
|
+
} catch {
|
|
125
|
+
// getter-only wrapper — try the inner transport below
|
|
126
|
+
}
|
|
127
|
+
if (outer.sessionId === token) {
|
|
128
|
+
return true
|
|
129
|
+
}
|
|
130
|
+
const inner = outer._webStandardTransport
|
|
131
|
+
if (inner && typeof inner === 'object') {
|
|
132
|
+
try {
|
|
133
|
+
;(inner as { sessionId?: string }).sessionId = token
|
|
134
|
+
} catch {
|
|
135
|
+
// read-only inner transport — the final read-back decides
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return outer.sessionId === token
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// base64url without assuming Node: Buffer where available, TextEncoder/btoa on
|
|
142
|
+
// edge runtimes. Must survive non-ASCII client names.
|
|
143
|
+
|
|
144
|
+
function utf8ToBase64Url(input: string): string {
|
|
145
|
+
if (typeof Buffer !== 'undefined' && typeof Buffer.from === 'function') {
|
|
146
|
+
return Buffer.from(input, 'utf8').toString('base64url')
|
|
147
|
+
}
|
|
148
|
+
const bytes = new TextEncoder().encode(input)
|
|
149
|
+
let binary = ''
|
|
150
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
151
|
+
binary += String.fromCharCode(bytes[i])
|
|
152
|
+
}
|
|
153
|
+
return btoa(binary).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '')
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function base64UrlToUtf8(input: string): string {
|
|
157
|
+
const normalized = input.replace(/-/g, '+').replace(/_/g, '/').replace(/=+$/, '')
|
|
158
|
+
const padded = normalized + '='.repeat((4 - (normalized.length % 4)) % 4)
|
|
159
|
+
if (typeof Buffer !== 'undefined' && typeof Buffer.from === 'function') {
|
|
160
|
+
return Buffer.from(padded, 'base64').toString('utf8')
|
|
161
|
+
}
|
|
162
|
+
const binary = atob(padded)
|
|
163
|
+
const bytes = new Uint8Array(binary.length)
|
|
164
|
+
for (let i = 0; i < binary.length; i++) {
|
|
165
|
+
bytes[i] = binary.charCodeAt(i)
|
|
166
|
+
}
|
|
167
|
+
return new TextDecoder().decode(bytes)
|
|
168
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
// Portions of this file are derived from
|
|
2
|
-
//
|
|
3
|
-
//
|
|
1
|
+
// Portions of this file are derived from agentcathq/agentcat-typescript-sdk
|
|
2
|
+
// (formerly MCPCat/mcpcat-typescript-sdk)
|
|
3
|
+
// Copyright (c) 2025 AgentCat, Inc. (formerly MCPcat)
|
|
4
|
+
// Licensed under the MIT License: https://github.com/agentcathq/agentcat-typescript-sdk/blob/main/LICENSE
|
|
4
5
|
|
|
5
6
|
import { version } from '../version'
|
|
6
7
|
import type {
|
|
@@ -13,6 +14,7 @@ import type {
|
|
|
13
14
|
import { INACTIVITY_TIMEOUT_IN_MINUTES } from './constants'
|
|
14
15
|
import { deterministicPrefixedId, newPrefixedId } from './ids'
|
|
15
16
|
import { getServerTrackingData, setServerTrackingData } from './internal'
|
|
17
|
+
import { decodeSessionId, readMcpSessionHeader } from './session-token'
|
|
16
18
|
|
|
17
19
|
export function newSessionId(): string {
|
|
18
20
|
return newPrefixedId('ses')
|
|
@@ -27,58 +29,58 @@ export function deriveSessionIdFromMCPSession(mcpSessionId: string): string {
|
|
|
27
29
|
}
|
|
28
30
|
|
|
29
31
|
/**
|
|
30
|
-
* Resolves the session id for a request
|
|
31
|
-
*
|
|
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).
|
|
32
36
|
*/
|
|
33
|
-
export function
|
|
37
|
+
export function getSessionId(server: MCPServerLike, extra?: CompatibleRequestHandlerExtra): string {
|
|
34
38
|
const data = getServerTrackingData(server)
|
|
35
|
-
|
|
36
39
|
if (!data) {
|
|
37
40
|
throw new Error('Server tracking data not found')
|
|
38
41
|
}
|
|
39
42
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
|
+
sessionId = token.sessionId
|
|
55
|
+
} else if (extra?.sessionId) {
|
|
56
|
+
// Session id issued by a stateful transport: hash it so the same MCP
|
|
57
|
+
// session maps to the same SDK session across restarts.
|
|
45
58
|
data.sessionSource = 'mcp'
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
59
|
+
sessionId = deriveSessionIdFromMCPSession(extra.sessionId)
|
|
60
|
+
} else {
|
|
61
|
+
sessionId = getSessionIdFromMemory(data)
|
|
49
62
|
}
|
|
50
63
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
}
|
|
64
|
+
data.sessionId = sessionId
|
|
65
|
+
data.lastActivity = new Date()
|
|
66
|
+
setServerTrackingData(server, data)
|
|
67
|
+
return sessionId
|
|
68
|
+
}
|
|
57
69
|
|
|
58
|
-
|
|
59
|
-
|
|
70
|
+
/**
|
|
71
|
+
* Nothing replayed on this request: keep the id we already have. Only
|
|
72
|
+
* generated sessions roll over on inactivity — token/MCP ids live as long as
|
|
73
|
+
* the client replays them, and regenerating one would split the session.
|
|
74
|
+
*/
|
|
75
|
+
function getSessionIdFromMemory(data: MCPAnalyticsData): string {
|
|
60
76
|
const timeoutMs = INACTIVITY_TIMEOUT_IN_MINUTES * 60 * 1000
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
setServerTrackingData(server, data)
|
|
77
|
+
const isStale = Date.now() - data.lastActivity.getTime() > timeoutMs
|
|
78
|
+
if (data.sessionSource === 'generated' && isStale) {
|
|
79
|
+
return newSessionId()
|
|
65
80
|
}
|
|
66
|
-
setLastActivity(server)
|
|
67
|
-
|
|
68
81
|
return data.sessionId
|
|
69
82
|
}
|
|
70
83
|
|
|
71
|
-
export function setLastActivity(server: MCPServerLike): void {
|
|
72
|
-
const data = getServerTrackingData(server)
|
|
73
|
-
|
|
74
|
-
if (!data) {
|
|
75
|
-
throw new Error('Server tracking data not found')
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
data.lastActivity = new Date()
|
|
79
|
-
setServerTrackingData(server, data)
|
|
80
|
-
}
|
|
81
|
-
|
|
82
84
|
/**
|
|
83
85
|
* Builds the session metadata stamped onto an event. The caller passes the
|
|
84
86
|
* session id resolved for *this* request so identity attribution can't be
|
package/src/extensions/sink.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
// Portions of this file are derived from
|
|
2
|
-
//
|
|
3
|
-
//
|
|
1
|
+
// Portions of this file are derived from agentcathq/agentcat-typescript-sdk
|
|
2
|
+
// (formerly MCPCat/mcpcat-typescript-sdk)
|
|
3
|
+
// Copyright (c) 2025 AgentCat, Inc. (formerly MCPcat)
|
|
4
|
+
// Licensed under the MIT License: https://github.com/agentcathq/agentcat-typescript-sdk/blob/main/LICENSE
|
|
4
5
|
|
|
5
6
|
import type { PostHog } from 'posthog-node'
|
|
6
7
|
import { uuidv7 } from '@posthog/core'
|
package/src/extensions/tools.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
// Portions of this file are derived from
|
|
2
|
-
//
|
|
3
|
-
//
|
|
1
|
+
// Portions of this file are derived from agentcathq/agentcat-typescript-sdk
|
|
2
|
+
// (formerly MCPCat/mcpcat-typescript-sdk)
|
|
3
|
+
// Copyright (c) 2025 AgentCat, Inc. (formerly MCPcat)
|
|
4
|
+
// Licensed under the MIT License: https://github.com/agentcathq/agentcat-typescript-sdk/blob/main/LICENSE
|
|
4
5
|
|
|
5
6
|
import { type CallToolResult, type ListToolsResult } from '@modelcontextprotocol/sdk/types.js'
|
|
6
7
|
import { log } from './logger'
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
// Portions of this file are derived from
|
|
2
|
-
//
|
|
3
|
-
//
|
|
1
|
+
// Portions of this file are derived from agentcathq/agentcat-typescript-sdk
|
|
2
|
+
// (formerly MCPCat/mcpcat-typescript-sdk)
|
|
3
|
+
// Copyright (c) 2025 AgentCat, Inc. (formerly MCPcat)
|
|
4
|
+
// Licensed under the MIT License: https://github.com/agentcathq/agentcat-typescript-sdk/blob/main/LICENSE
|
|
4
5
|
|
|
5
6
|
import type { CompatibleRequestHandlerExtra, MCPAnalyticsData, MCPRequestLike, McpEvent } from '../types'
|
|
6
7
|
import { resolveEventProperties } from './internal'
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
// Portions of this file are derived from
|
|
2
|
-
//
|
|
3
|
-
//
|
|
1
|
+
// Portions of this file are derived from agentcathq/agentcat-typescript-sdk
|
|
2
|
+
// (formerly MCPCat/mcpcat-typescript-sdk)
|
|
3
|
+
// Copyright (c) 2025 AgentCat, Inc. (formerly MCPcat)
|
|
4
|
+
// Licensed under the MIT License: https://github.com/agentcathq/agentcat-typescript-sdk/blob/main/LICENSE
|
|
4
5
|
|
|
5
6
|
import type { ErrorProperties, Event, StackFrame, McpEvent } from '../types'
|
|
6
7
|
|