@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
JavaScript
#!/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);
});