UNPKG

@postman/postman-mcp-server

Version:

Postman MCP Server — connect AI agents (Claude Code, Cursor, VS Code Copilot, Gemini CLI) to your Postman collections, specifications, and environments via Model Context Protocol (MCP)

423 lines (422 loc) • 18.3 kB
#!/usr/bin/env node import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; import { ErrorCode, isInitializeRequest, McpError, } from '@modelcontextprotocol/sdk/types.js'; import { readdir, readFile } from 'node:fs/promises'; import { join, dirname } from 'node:path'; import { fileURLToPath, pathToFileURL } from 'node:url'; import { enabledResources } from './enabledResources.js'; import { PostmanAPIClient } from './clients/postman.js'; import { SERVER_NAME, APP_VERSION } from './constants.js'; import { createProgressReporter } from './tools/utils/progress.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 { generateSrvTrace } from './telemetry/srvTrace.js'; const SUPPORTED_REGIONS = { us: 'https://api.postman.com', eu: 'https://api.eu.postman.com', }; function isValidRegion(region) { return region in SUPPORTED_REGIONS; } function setRegionEnvironment(region) { if (!isValidRegion(region)) { throw new Error(`Invalid region: ${region}. Supported regions: us, eu`); } env.POSTMAN_API_BASE_URL = SUPPORTED_REGIONS[region]; } const quietMode = process.argv.includes('--quiet'); function log(level, message, context) { if (quietMode && (level === 'debug' || level === 'info')) return; const timestamp = new Date().toISOString(); const suffix = context ? ` ${JSON.stringify(context)}` : ''; console.error(`[${timestamp}] [${level.toUpperCase()}] ${message}${suffix}`); } function sendClientLog(server, level, data) { try { server.sendLoggingMessage?.({ level, data }); } catch { } } function logBoth(server, level, message, context) { log(level, message, context); if (server) sendClientLog(server, level, message); } async function loadAllTools() { const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); const generatedToolsDir = join(__dirname, './tools'); const ext = __filename.endsWith('.ts') ? '.ts' : '.js'; const tools = []; try { log('info', 'Loading tools from directory', { toolsDir: generatedToolsDir, ext }); const files = await readdir(generatedToolsDir); const toolFiles = files.filter((file) => file.endsWith(ext)); log('debug', 'Discovered tool files', { count: toolFiles.length }); const importResults = await Promise.allSettled(toolFiles.map(async (file) => { const toolPath = join(generatedToolsDir, file); const toolModule = await import(pathToFileURL(toolPath).href); return { toolModule, file }; })); for (const result of importResults) { if (result.status === 'rejected') { log('error', 'Failed to load tool module', { error: String(result.reason?.message || result.reason), }); continue; } const { toolModule, file } = result.value; if (toolModule.method && toolModule.description && toolModule.parameters && toolModule.handler) { tools.push(toolModule); log('info', 'Loaded tool', { method: toolModule.method, file }); } else { log('warn', 'Tool module missing required exports; skipping', { file }); } } } catch (error) { log('error', 'Failed to read tools directory', { toolsDir: generatedToolsDir, error: String(error?.message || error), }); } log('info', 'Tool loading completed', { totalLoaded: tools.length }); return tools; } let clientInfo = undefined; async function run() { const args = process.argv.slice(2); const useFull = args.includes('--full'); const useCode = args.includes('--code'); const useLearn = args.includes('--learn'); const useContextGraph = args.includes('--context-graph'); const regionIndex = args.findIndex((arg) => arg === '--region'); if (regionIndex !== -1 && regionIndex + 1 < args.length) { const region = args[regionIndex + 1]; if (isValidRegion(region)) { setRegionEnvironment(region); log('info', `Using region: ${region}`, { region, baseUrl: env.POSTMAN_API_BASE_URL, }); } else { log('error', `Invalid region: ${region}`); console.error(`Supported regions: ${Object.keys(SUPPORTED_REGIONS).join(', ')}`); process.exit(1); } } const apiKey = env.POSTMAN_API_KEY; if (!apiKey) { log('error', 'POSTMAN_API_KEY environment variable is required for STDIO mode'); process.exit(1); } const allGeneratedTools = await loadAllTools(); log('info', 'Server initialization starting', { serverName: SERVER_NAME, version: APP_VERSION, toolCount: allGeneratedTools.length, }); const enabledMethods = useContextGraph ? enabledResources.contextGraph : useLearn ? enabledResources.learn : useCode ? enabledResources.code : useFull ? enabledResources.full : enabledResources.minimal; const toolSorter = (a, b) => a.method < b.method ? -1 : a.method > b.method ? 1 : 0; const tools = allGeneratedTools .filter((t) => enabledMethods.includes(t.method)) .sort(toolSorter); 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; if (telemetryEnabled) { log('info', 'Telemetry enabled (POSTMAN_MCP_TELEMETRY=true). Set POSTMAN_MCP_TELEMETRY=false to disable.'); } const telemetry = createTelemetryClient({ telemetryEnabled, apiKey, }); const telemetrySession = new TelemetrySession({ installId: TelemetrySession.loadOrCreateInstallIdFromDisk(), region, transport: 'stdio', toolset: useContextGraph ? 'contextGraph' : useLearn ? 'learn' : useCode ? 'code' : useFull ? 'full' : 'minimal', serverVersion: APP_VERSION, }); telemetrySession.setToolNames(tools.map((t) => t.method)); const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); let instructionsContent; try { const resourcesDir = join(__dirname, './resources'); instructionsContent = await readFile(join(resourcesDir, 'Instructions.md'), 'utf-8'); log('info', 'Loaded Instructions.md resource'); } catch (error) { log('warn', 'Failed to load Instructions.md resource', { error: String(error?.message || error), }); } const server = new McpServer({ name: SERVER_NAME, title: 'Postman', version: APP_VERSION, websiteUrl: 'https://www.postman.com', description: 'Find and manage Postman workspaces, collections, environments, mocks, monitors, and API specifications, run collections, and publish API documentation.', }, instructionsContent ? { instructions: 'Before answering any API-related questions, fetch the MCP resource at URI `postman://instructions` using FetchMcpResource from this MCP server, and follow the usage instructions contained within.', } : {}); server.onerror = (error) => { const msg = String(error?.message || error); logBoth(server, 'error', `MCP server error: ${msg}`, { error: msg }); }; process.on('SIGINT', async () => { logBoth(server, 'warn', 'SIGINT received; shutting down'); await telemetry.shutdown(); await server.close(); process.exit(0); }); const serverContext = { serverType: useContextGraph ? 'contextGraph' : useLearn ? 'learn' : useCode ? 'code' : useFull ? 'full' : 'minimal', availableTools: tools.map((t) => t.method), }; const viewsDir = join(__dirname, './views'); const renderTemplate = createTemplateRenderer(viewsDir); const errorsDir = join(__dirname, './views/errors'); const renderErrorTemplate = createErrorTemplateRenderer(errorsDir); log('info', 'Registering tools with McpServer'); for (const tool of tools) { server.registerTool(tool.method, { title: tool.title, description: tool.description, inputSchema: tool.parameters.shape, annotations: tool.annotations || {}, }, async (args, extra) => { const toolName = tool.method; log('info', `Tool invocation started: ${toolName}`, { toolName }); const start = Date.now(); const paginationUsed = detectPaginationUsed(args); const metaRaw = extra?._meta ? JSON.stringify(extra._meta) : ''; const srvTraceId = generateSrvTrace(); const progress = createProgressReporter({ sendNotification: extra.sendNotification, meta: extra._meta, signal: extra.signal, }); const client = new PostmanAPIClient(apiKey, undefined, serverContext, srvTraceId); try { const result = await tool.handler(args, { client, headers: { ...extra?.requestInfo?.headers, 'user-agent': clientInfo?.name, }, serverContext, progress, signal: extra.signal, }); const durationMs = Date.now() - start; log('info', `Tool invocation completed: ${toolName} (${durationMs}ms)`, { toolName, durationMs, }); telemetry.toolCall(telemetrySession, { toolName, args: args, result, latencyMs: durationMs, isError: result.isError ?? false, authMethod: 'api_key', paginationUsed, 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') { const rendered = renderTemplate(toolName, result.content[0].text); if (rendered) { return { content: [{ type: 'text', text: rendered }] }; } } return result; } catch (error) { 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; if (error instanceof McpError) { const httpStatus = error.data?.httpStatus; if (typeof httpStatus === 'number') { telemetry.toolCall(telemetrySession, { toolName, args: args, result: { content: [] }, latencyMs: errDurationMs, 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', errorUpstream: 'postman-api', rateLimited: typeof httpStatus === 'number' && httpStatus === 429, meta: errorMeta, metaRaw, }); const rawBody = String(error.data?.cause ?? ''); let parsedBody = null; try { parsedBody = JSON.parse(rawBody); } catch { } const errorObj = parsedBody?.error && typeof parsedBody.error === 'object' ? parsedBody.error : parsedBody; const rendered = renderErrorTemplate(toolName, httpStatus, { toolName, statusCode: httpStatus, args, errorMessage: error.message, errorBody: rawBody, error: errorObj, }); if (rendered) { throw new McpError(error.code, rendered, error.data); } } else { telemetry.toolCall(telemetrySession, { toolName, args: args, result: { content: [] }, latencyMs: errDurationMs, isError: true, authMethod: 'api_key', paginationUsed, srvTraceId, errorType: 'tool', errorCode: 'UPSTREAM_ERROR', errorStage: 'upstream', errorUpstream: 'postman-api', rateLimited: false, meta: errorMeta, metaRaw, }); } throw error; } telemetry.toolCall(telemetrySession, { toolName, args: args, result: { content: [] }, latencyMs: errDurationMs, isError: true, authMethod: 'api_key', paginationUsed, srvTraceId, errorType: 'protocol', errorCode: 'INTERNAL_ERROR', errorStage: '', meta: errorMeta, metaRaw, }); throw new McpError(ErrorCode.InternalError, `API error: ${error.message}`); } finally { progress.stop(); } }); } if (instructionsContent) { server.registerResource('instructions', 'postman://instructions', { description: 'Instructions for using the Postman MCP server', mimeType: 'text/markdown' }, async (uri) => ({ contents: [{ uri: uri.href, mimeType: 'text/markdown', text: instructionsContent }], })); log('info', 'Registered resource: instructions'); } log('info', 'Starting stdio transport'); const transport = new StdioServerTransport(); transport.onmessage = (message) => { if (isInitializeRequest(message)) { clientInfo = message.params.clientInfo; log('debug', '📥 Received MCP initialize request', { clientInfo }); telemetrySession.setClientInfo({ name: clientInfo?.name ?? 'unknown', version: clientInfo?.version ?? 'unknown' }, message.params.protocolVersion ?? ''); telemetrySession.setClientCapabilities(message.params.capabilities); telemetry.sessionInit(telemetrySession, { clientCapabilities: telemetrySession.clientCapabilities, serverCapabilities: ['tools', 'resources'], }); } }; await server.connect(transport); const toolsetName = useContextGraph ? 'contextGraph' : useLearn ? 'learn' : useCode ? 'code' : useFull ? 'full' : 'minimal'; logBoth(server, 'info', `Server connected and ready: ${SERVER_NAME}@${APP_VERSION} with ${tools.length} tools (${toolsetName})`); } run().catch((error) => { log('error', 'Unhandled error during server execution', { error: String(error?.message || error), }); process.exit(1); });