From b59c6fa19e807d8217b18a5b30a9a83ac19d091f Mon Sep 17 00:00:00 2001 From: rubenbaraut <116556424+rubenbaraut@users.noreply.github.com> Date: Tue, 14 Jul 2026 11:42:48 +0000 Subject: [PATCH] chore: sync tools, entrypoint, and tests from postman-mcp-server MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Andrea De Pirro Co-authored-by: Christos Gkoros Co-authored-by: Claude Co-authored-by: Diego Garcia Co-authored-by: Diego García Co-authored-by: Ruben Baraut <116556424+rubenbaraut@users.noreply.github.com> Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- dist/src/clients/postman.js | 6 ++- dist/src/index.js | 43 +++++++++--------- dist/src/telemetry/client.js | 4 +- dist/src/telemetry/session.js | 4 +- dist/src/telemetry/srvTrace.js | 6 +++ dist/src/telemetry/types.js | 1 + src/clients/postman.ts | 7 ++- src/index.ts | 80 +++++++++++++++------------------- src/telemetry/client.ts | 26 ++++++++--- src/telemetry/index.ts | 2 +- src/telemetry/noop.ts | 2 +- src/telemetry/session.ts | 9 +++- src/telemetry/srvTrace.ts | 17 ++++++++ src/telemetry/types.ts | 4 ++ 14 files changed, 132 insertions(+), 79 deletions(-) create mode 100644 dist/src/telemetry/srvTrace.js create mode 100644 src/telemetry/srvTrace.ts diff --git a/dist/src/clients/postman.js b/dist/src/clients/postman.js index c96c757..2afb410 100644 --- a/dist/src/clients/postman.js +++ b/dist/src/clients/postman.js @@ -10,11 +10,13 @@ export class PostmanAPIClient { baseUrl; apiKey; serverContext; + srvTraceId; static instance = null; - constructor(apiKey, baseUrl = env.POSTMAN_API_BASE_URL, serverContext) { + constructor(apiKey, baseUrl = env.POSTMAN_API_BASE_URL, serverContext, srvTraceId) { this.apiKey = apiKey; this.baseUrl = baseUrl; this.serverContext = serverContext; + this.srvTraceId = srvTraceId; } static getInstance(apiKey, baseUrl) { if (!PostmanAPIClient.instance) { @@ -59,6 +61,7 @@ export class PostmanAPIClient { 'host', 'accept-encoding', 'keep-alive', + 'x-srv-trace', ]); const extra = Object.fromEntries(Object.entries(options.headers ?? {}).filter(([k]) => !disallowed.has(k.toLowerCase()))); const hasBody = options.body !== undefined && options.body !== null; @@ -67,6 +70,7 @@ export class PostmanAPIClient { ...extra, 'x-api-key': currentApiKey, 'user-agent': userAgentHeader, + ...(this.srvTraceId ? { 'x-srv-trace': this.srvTraceId } : {}), }; const { headers: _ignored, ...optionsWithoutHeaders } = options; const response = await fetch(`${this.baseUrl}${endpoint}`, { diff --git a/dist/src/index.js b/dist/src/index.js index f0cb0a7..3c100ee 100755 --- a/dist/src/index.js +++ b/dist/src/index.js @@ -11,7 +11,8 @@ import { SERVER_NAME, APP_VERSION } from './constants.js'; import { env } from './env.js'; import { createTemplateRenderer } from './tools/utils/templateRenderer.js'; import { createErrorTemplateRenderer } from './tools/utils/errorTemplateRenderer.js'; -import { createTelemetryClient, detectPaginationUsed, parseTelemetryFlag, TelemetrySession, } from './telemetry/index.js'; +import { createTelemetryClient, detectPaginationUsed, parseTelemetryFlag, TelemetrySession } from './telemetry/index.js'; +import { generateSrvTrace } from './telemetry/srvTrace.js'; const SUPPORTED_REGIONS = { us: 'https://api.postman.com', eu: 'https://api.eu.postman.com', @@ -134,7 +135,7 @@ async function run() { const tools = allGeneratedTools .filter((t) => enabledMethods.includes(t.method)) .sort(toolSorter); - const region = regionIndex !== -1 && regionIndex + 1 < args.length && isValidRegion(args[regionIndex + 1]) + const region = (regionIndex !== -1 && regionIndex + 1 < args.length && isValidRegion(args[regionIndex + 1])) ? args[regionIndex + 1] : 'us'; const telemetryEnabled = parseTelemetryFlag(process.env.POSTMAN_MCP_TELEMETRY) ?? false; @@ -189,7 +190,6 @@ async function run() { const renderTemplate = createTemplateRenderer(viewsDir); const errorsDir = join(__dirname, './views/errors'); const renderErrorTemplate = createErrorTemplateRenderer(errorsDir); - const client = new PostmanAPIClient(apiKey, undefined, serverContext); log('info', 'Registering tools with McpServer'); for (const tool of tools) { server.registerTool(tool.method, { @@ -202,6 +202,8 @@ async function run() { const start = Date.now(); const paginationUsed = detectPaginationUsed(args); const metaRaw = extra?._meta ? JSON.stringify(extra._meta) : ''; + const srvTraceId = generateSrvTrace(); + const client = new PostmanAPIClient(apiKey, undefined, serverContext, srvTraceId); try { const result = await tool.handler(args, { client, @@ -224,14 +226,13 @@ async function run() { isError: result.isError ?? false, authMethod: 'api_key', paginationUsed, - meta: extra?._meta - ? { - trigger: extra._meta.trigger ?? '', - conversation_id: extra._meta.conversation_id ?? '', - task_type: extra._meta.task_type ?? '', - model_name: extra._meta.model_name ?? '', - } - : undefined, + srvTraceId, + meta: extra?._meta ? { + trigger: extra._meta.trigger ?? '', + conversation_id: extra._meta.conversation_id ?? '', + task_type: extra._meta.task_type ?? '', + model_name: extra._meta.model_name ?? '', + } : undefined, metaRaw, }); if (result.content?.[0]?.type === 'text') { @@ -246,14 +247,12 @@ async function run() { const errMsg = String(error?.message || error); logBoth(server, 'error', `Tool invocation failed: ${toolName}: ${errMsg}`, { toolName }); const errDurationMs = Date.now() - start; - const errorMeta = extra?._meta - ? { - trigger: extra._meta.trigger ?? '', - conversation_id: extra._meta.conversation_id ?? '', - task_type: extra._meta.task_type ?? '', - model_name: extra._meta.model_name ?? '', - } - : undefined; + const errorMeta = extra?._meta ? { + trigger: extra._meta.trigger ?? '', + conversation_id: extra._meta.conversation_id ?? '', + task_type: extra._meta.task_type ?? '', + model_name: extra._meta.model_name ?? '', + } : undefined; if (error instanceof McpError) { const httpStatus = error.data?.httpStatus; if (typeof httpStatus === 'number') { @@ -265,6 +264,7 @@ async function run() { isError: true, authMethod: 'api_key', paginationUsed, + srvTraceId, errorType: 'tool', errorCode: typeof httpStatus === 'number' && (httpStatus === 401 || httpStatus === 403) ? 'AUTH_ERROR' @@ -272,8 +272,7 @@ async function run() { ? 'RATE_LIMITED' : 'UPSTREAM_ERROR', errorStage: typeof httpStatus === 'number' && (httpStatus === 401 || httpStatus === 403) - ? 'auth' - : 'upstream', + ? 'auth' : 'upstream', errorUpstream: 'postman-api', rateLimited: typeof httpStatus === 'number' && httpStatus === 429, meta: errorMeta, @@ -310,6 +309,7 @@ async function run() { isError: true, authMethod: 'api_key', paginationUsed, + srvTraceId, errorType: 'tool', errorCode: 'UPSTREAM_ERROR', errorStage: 'upstream', @@ -329,6 +329,7 @@ async function run() { isError: true, authMethod: 'api_key', paginationUsed, + srvTraceId, errorType: 'protocol', errorCode: 'INTERNAL_ERROR', errorStage: '', diff --git a/dist/src/telemetry/client.js b/dist/src/telemetry/client.js index f455d73..110b2e7 100644 --- a/dist/src/telemetry/client.js +++ b/dist/src/telemetry/client.js @@ -56,6 +56,7 @@ export class TelemetryClient { task_type: params.meta?.task_type ?? '', model_name: params.meta?.model_name ?? '', meta_raw: params.metaRaw ?? '', + srv_trace_id: params.srvTraceId ?? '', }; this.enqueue(session, event); } @@ -102,7 +103,8 @@ export class TelemetryClient { this.buffer.push(event); this.bufferSizeBytes += eventSize; this.resetIdleTimer(); - if (this.buffer.length >= MAX_BUFFER_COUNT || this.bufferSizeBytes >= MAX_BUFFER_BYTES) { + if (this.buffer.length >= MAX_BUFFER_COUNT || + this.bufferSizeBytes >= MAX_BUFFER_BYTES) { void this.flush(); } } diff --git a/dist/src/telemetry/session.js b/dist/src/telemetry/session.js index 2da8e6c..6503e8f 100644 --- a/dist/src/telemetry/session.js +++ b/dist/src/telemetry/session.js @@ -56,7 +56,9 @@ export class TelemetrySession { setToolNames(toolNames) { this.toolListCount = toolNames.length; const sorted = [...toolNames].sort(); - const hash = createHash('sha256').update(sorted.join(',')).digest('hex'); + const hash = createHash('sha256') + .update(sorted.join(',')) + .digest('hex'); this.toolsetSnapshotId = hash.substring(0, 12); } getToolListCount() { diff --git a/dist/src/telemetry/srvTrace.js b/dist/src/telemetry/srvTrace.js new file mode 100644 index 0000000..a601ce1 --- /dev/null +++ b/dist/src/telemetry/srvTrace.js @@ -0,0 +1,6 @@ +import { randomBytes } from 'node:crypto'; +const SRV_TRACE_PREFIX = 'v=1;t='; +const SRV_TRACE_BYTES = 8; +export function generateSrvTrace() { + return SRV_TRACE_PREFIX + randomBytes(SRV_TRACE_BYTES).toString('hex'); +} diff --git a/dist/src/telemetry/types.js b/dist/src/telemetry/types.js index 2c61e42..1227568 100644 --- a/dist/src/telemetry/types.js +++ b/dist/src/telemetry/types.js @@ -57,6 +57,7 @@ export const ToolCallEventSchema = BaseTelemetryEventSchema.extend({ task_type: z.string(), model_name: z.string(), meta_raw: z.string(), + srv_trace_id: z.string(), }); export const TelemetryEventSchema = z.discriminatedUnion('event_type', [ SessionInitEventSchema, diff --git a/src/clients/postman.ts b/src/clients/postman.ts index a380eca..2e20868 100644 --- a/src/clients/postman.ts +++ b/src/clients/postman.ts @@ -39,16 +39,19 @@ export class PostmanAPIClient implements IPostmanAPIClient { private readonly baseUrl: string; private readonly apiKey?: string; private readonly serverContext?: ServerContext; + private readonly srvTraceId?: string; private static instance: PostmanAPIClient | null = null; constructor( apiKey?: string, baseUrl: string = env.POSTMAN_API_BASE_URL, - serverContext?: ServerContext + serverContext?: ServerContext, + srvTraceId?: string ) { this.apiKey = apiKey; this.baseUrl = baseUrl; this.serverContext = serverContext; + this.srvTraceId = srvTraceId; } /** @@ -127,6 +130,7 @@ export class PostmanAPIClient implements IPostmanAPIClient { 'host', 'accept-encoding', 'keep-alive', + 'x-srv-trace', ]); const extra = Object.fromEntries( Object.entries(options.headers ?? {}).filter(([k]) => !disallowed.has(k.toLowerCase())) @@ -139,6 +143,7 @@ export class PostmanAPIClient implements IPostmanAPIClient { ...extra, 'x-api-key': currentApiKey, 'user-agent': userAgentHeader, + ...(this.srvTraceId ? { 'x-srv-trace': this.srvTraceId } : {}), }; const { headers: _ignored, ...optionsWithoutHeaders } = options; diff --git a/src/index.ts b/src/index.ts index 622fbc0..04b5037 100644 --- a/src/index.ts +++ b/src/index.ts @@ -22,13 +22,8 @@ import { ServerContext } from './tools/utils/toolHelpers.js'; import { env } from './env.js'; import { createTemplateRenderer } from './tools/utils/templateRenderer.js'; import { createErrorTemplateRenderer } from './tools/utils/errorTemplateRenderer.js'; -import { - createTelemetryClient, - detectPaginationUsed, - parseTelemetryFlag, - TelemetrySession, - type ITelemetryClient, -} from './telemetry/index.js'; +import { createTelemetryClient, detectPaginationUsed, parseTelemetryFlag, TelemetrySession, type ITelemetryClient } from './telemetry/index.js'; +import { generateSrvTrace } from './telemetry/srvTrace.js'; const SUPPORTED_REGIONS = { us: 'https://api.postman.com', @@ -206,20 +201,16 @@ async function run() { .sort(toolSorter); // Determine region for telemetry - const region: 'us' | 'eu' = - regionIndex !== -1 && regionIndex + 1 < args.length && isValidRegion(args[regionIndex + 1]) - ? (args[regionIndex + 1] as 'us' | 'eu') - : 'us'; + const region: 'us' | 'eu' = (regionIndex !== -1 && regionIndex + 1 < args.length && isValidRegion(args[regionIndex + 1])) + ? args[regionIndex + 1] as 'us' | 'eu' + : 'us'; // Telemetry is OFF by default for the open-source STDIO server. Users must // opt in explicitly with POSTMAN_MCP_TELEMETRY=true; any other value (or // unset) keeps telemetry disabled. const telemetryEnabled = parseTelemetryFlag(process.env.POSTMAN_MCP_TELEMETRY) ?? false; if (telemetryEnabled) { - log( - 'info', - 'Telemetry enabled (POSTMAN_MCP_TELEMETRY=true). Set POSTMAN_MCP_TELEMETRY=false to disable.' - ); + log('info', 'Telemetry enabled (POSTMAN_MCP_TELEMETRY=true). Set POSTMAN_MCP_TELEMETRY=false to disable.'); } const telemetry: ITelemetryClient = createTelemetryClient({ telemetryEnabled, @@ -286,9 +277,6 @@ async function run() { const errorsDir = join(__dirname, './views/errors'); const renderErrorTemplate = createErrorTemplateRenderer(errorsDir); - // Create a client instance with the API key and server context for STDIO mode - const client = new PostmanAPIClient(apiKey, undefined, serverContext); - log('info', 'Registering tools with McpServer'); // Register all tools using the McpServer registerTool method @@ -308,6 +296,13 @@ async function run() { const start = Date.now(); const paginationUsed = detectPaginationUsed(args as Record); const metaRaw = extra?._meta ? JSON.stringify(extra._meta) : ''; + + // One correlation id per tool call: sent as the x-srv-trace header on + // every outbound API request and recorded on the telemetry event. + const srvTraceId = generateSrvTrace(); + // Create a client instance with the API key, server context, and + // this call's trace id for STDIO mode. + const client = new PostmanAPIClient(apiKey, undefined, serverContext, srvTraceId); try { const result = await tool.handler(args, { client, @@ -334,14 +329,13 @@ async function run() { isError: result.isError ?? false, authMethod: 'api_key', paginationUsed, - meta: extra?._meta - ? { - trigger: (extra._meta as any).trigger ?? '', - conversation_id: (extra._meta as any).conversation_id ?? '', - task_type: (extra._meta as any).task_type ?? '', - model_name: (extra._meta as any).model_name ?? '', - } - : undefined, + srvTraceId, + meta: extra?._meta ? { + trigger: (extra._meta as any).trigger ?? '', + conversation_id: (extra._meta as any).conversation_id ?? '', + task_type: (extra._meta as any).task_type ?? '', + model_name: (extra._meta as any).model_name ?? '', + } : undefined, metaRaw, }); @@ -360,14 +354,12 @@ async function run() { logBoth(server, 'error', `Tool invocation failed: ${toolName}: ${errMsg}`, { toolName }); const errDurationMs = Date.now() - start; - const errorMeta = extra?._meta - ? { - trigger: (extra._meta as any).trigger ?? '', - conversation_id: (extra._meta as any).conversation_id ?? '', - task_type: (extra._meta as any).task_type ?? '', - model_name: (extra._meta as any).model_name ?? '', - } - : undefined; + const errorMeta = extra?._meta ? { + trigger: (extra._meta as any).trigger ?? '', + conversation_id: (extra._meta as any).conversation_id ?? '', + task_type: (extra._meta as any).task_type ?? '', + model_name: (extra._meta as any).model_name ?? '', + } : undefined; if (error instanceof McpError) { const httpStatus = (error.data as Record)?.httpStatus; @@ -381,17 +373,15 @@ async function run() { isError: true, authMethod: 'api_key', paginationUsed, + srvTraceId, errorType: 'tool', - errorCode: - typeof httpStatus === 'number' && (httpStatus === 401 || httpStatus === 403) - ? 'AUTH_ERROR' - : typeof httpStatus === 'number' && httpStatus === 429 - ? 'RATE_LIMITED' - : 'UPSTREAM_ERROR', - errorStage: - typeof httpStatus === 'number' && (httpStatus === 401 || httpStatus === 403) - ? 'auth' - : 'upstream', + errorCode: typeof httpStatus === 'number' && (httpStatus === 401 || httpStatus === 403) + ? 'AUTH_ERROR' + : typeof httpStatus === 'number' && httpStatus === 429 + ? 'RATE_LIMITED' + : 'UPSTREAM_ERROR', + errorStage: typeof httpStatus === 'number' && (httpStatus === 401 || httpStatus === 403) + ? 'auth' : 'upstream', errorUpstream: 'postman-api', rateLimited: typeof httpStatus === 'number' && httpStatus === 429, meta: errorMeta, @@ -432,6 +422,7 @@ async function run() { isError: true, authMethod: 'api_key', paginationUsed, + srvTraceId, errorType: 'tool', errorCode: 'UPSTREAM_ERROR', errorStage: 'upstream', @@ -452,6 +443,7 @@ async function run() { isError: true, authMethod: 'api_key', paginationUsed, + srvTraceId, errorType: 'protocol', errorCode: 'INTERNAL_ERROR', errorStage: '', diff --git a/src/telemetry/client.ts b/src/telemetry/client.ts index a8c164a..237be5b 100644 --- a/src/telemetry/client.ts +++ b/src/telemetry/client.ts @@ -1,5 +1,9 @@ import { TelemetrySession } from './session.js'; -import type { TelemetryEvent, SessionInitEvent, ToolCallEvent } from './types.js'; +import type { + TelemetryEvent, + SessionInitEvent, + ToolCallEvent, +} from './types.js'; /** * Options for constructing a TelemetryClient. @@ -69,6 +73,8 @@ export interface ToolCallParams { }; /** JSON-stringified raw `_meta` object as received from the client. */ metaRaw?: string; + /** Per-tool-call correlation id (`v=1;t=`) also sent as `x-srv-trace`. */ + srvTraceId?: string; } /** Maximum events in the buffer before auto-flush. */ @@ -124,7 +130,7 @@ export class TelemetryClient { params: { clientCapabilities: string[]; serverCapabilities: string[]; - } + }, ): void { const event: SessionInitEvent = { ...this.buildBaseEvent(session, 'session_init'), @@ -166,6 +172,7 @@ export class TelemetryClient { task_type: params.meta?.task_type ?? '', model_name: params.meta?.model_name ?? '', meta_raw: params.metaRaw ?? '', + srv_trace_id: params.srvTraceId ?? '', }; this.enqueue(session, event); } @@ -209,7 +216,7 @@ export class TelemetryClient { private buildBaseEvent( session: TelemetrySession, eventType: T, - conversationId?: string + conversationId?: string, ): { event_type: T; timestamp: string; @@ -263,7 +270,10 @@ export class TelemetryClient { this.resetIdleTimer(); // Auto-flush if thresholds exceeded - if (this.buffer.length >= MAX_BUFFER_COUNT || this.bufferSizeBytes >= MAX_BUFFER_BYTES) { + if ( + this.buffer.length >= MAX_BUFFER_COUNT || + this.bufferSizeBytes >= MAX_BUFFER_BYTES + ) { // Fire-and-forget flush void this.flush(); } @@ -277,9 +287,13 @@ export class TelemetryClient { * a single flush batch are assumed to share a region (true in practice — * a single pod targets a single region). */ - private async send(events: TelemetryEvent[], attempt: number): Promise { + private async send( + events: TelemetryEvent[], + attempt: number, + ): Promise { const region = events[0]?.region ?? 'us'; - const baseUrl = process.env[GATEWAY_BASE_URL_OVERRIDE_ENV] ?? GATEWAY_BASE_URL[region]; + const baseUrl = + process.env[GATEWAY_BASE_URL_OVERRIDE_ENV] ?? GATEWAY_BASE_URL[region]; const ingestUrl = `${baseUrl}${TELEMETRY_INGEST_PATH}`; try { diff --git a/src/telemetry/index.ts b/src/telemetry/index.ts index 132e1bb..1dc13aa 100644 --- a/src/telemetry/index.ts +++ b/src/telemetry/index.ts @@ -12,7 +12,7 @@ export type { ToolCallParams } from './client.js'; export interface ITelemetryClient { sessionInit( session: TelemetrySession, - params: { clientCapabilities: string[]; serverCapabilities: string[] } + params: { clientCapabilities: string[]; serverCapabilities: string[] }, ): void; toolCall(session: TelemetrySession, params: ToolCallParams): void; flush(): Promise; diff --git a/src/telemetry/noop.ts b/src/telemetry/noop.ts index 5924ab8..b8a7ff1 100644 --- a/src/telemetry/noop.ts +++ b/src/telemetry/noop.ts @@ -8,7 +8,7 @@ import type { TelemetrySession } from './session.js'; export class NoopTelemetryClient { sessionInit( _session: TelemetrySession, - _params: { clientCapabilities: string[]; serverCapabilities: string[] } + _params: { clientCapabilities: string[]; serverCapabilities: string[] }, ): void {} toolCall(_session: TelemetrySession, _params: ToolCallParams): void {} async flush(): Promise {} diff --git a/src/telemetry/session.ts b/src/telemetry/session.ts index 952e5b7..59ad359 100644 --- a/src/telemetry/session.ts +++ b/src/telemetry/session.ts @@ -131,7 +131,10 @@ export class TelemetrySession { } /** Stores client metadata received during the MCP initialize handshake. */ - setClientInfo(clientInfo: { name: string; version: string }, protocolVersion: string): void { + setClientInfo( + clientInfo: { name: string; version: string }, + protocolVersion: string, + ): void { this.clientName = clientInfo.name; this.clientVersion = clientInfo.version; this.protocolVersion = protocolVersion; @@ -155,7 +158,9 @@ export class TelemetrySession { setToolNames(toolNames: string[]): void { this.toolListCount = toolNames.length; const sorted = [...toolNames].sort(); - const hash = createHash('sha256').update(sorted.join(',')).digest('hex'); + const hash = createHash('sha256') + .update(sorted.join(',')) + .digest('hex'); this.toolsetSnapshotId = hash.substring(0, 12); } diff --git a/src/telemetry/srvTrace.ts b/src/telemetry/srvTrace.ts new file mode 100644 index 0000000..b060a77 --- /dev/null +++ b/src/telemetry/srvTrace.ts @@ -0,0 +1,17 @@ +import { randomBytes } from 'node:crypto'; + +/** Prefix for the correlation token value. */ +const SRV_TRACE_PREFIX = 'v=1;t='; + +/** Number of random bytes in the correlation token (→ 16 hex chars). */ +const SRV_TRACE_BYTES = 8; + +/** + * Generates a per-tool-call correlation id (`v=1;t=<16 lowercase hex chars>`) + * sent as the outbound `x-srv-trace` request header and recorded on the + * telemetry `tool_call` event, so a single tool call can be traced across the + * requests it makes. + */ +export function generateSrvTrace(): string { + return SRV_TRACE_PREFIX + randomBytes(SRV_TRACE_BYTES).toString('hex'); +} diff --git a/src/telemetry/types.ts b/src/telemetry/types.ts index fc00b04..e02e5db 100644 --- a/src/telemetry/types.ts +++ b/src/telemetry/types.ts @@ -166,6 +166,10 @@ export const ToolCallEventSchema = BaseTelemetryEventSchema.extend({ /** Raw `_meta` object as the client sent it, JSON-stringified. Empty string when no `_meta` was provided. */ meta_raw: z.string(), + + /** Per-tool-call correlation id (`v=1;t=`) sent as the outbound + * `x-srv-trace` request header. */ + srv_trace_id: z.string(), }); export type ToolCallEvent = z.infer;