Skip to main content
Glama
index.ts•9.58 kB
#!/usr/bin/env node import dotenv from 'dotenv'; import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; import { InitializeRequest } from '@modelcontextprotocol/sdk/types.js'; import { ErrorCode, isInitializeRequest, IsomorphicHeaders, McpError, CallToolResult, } from '@modelcontextprotocol/sdk/types.js'; import { readdir } from 'node:fs/promises'; import { join, dirname } from 'node:path'; import { fileURLToPath } from 'node:url'; import type { z } from 'zod'; import { enabledResources } from './enabledResources.js'; import { PostmanAPIClient } from './clients/postman.js'; import { SERVER_NAME, APP_VERSION } from './constants.js'; import { ServerContext } from './tools/utils/toolHelpers.js'; const SUPPORTED_REGIONS = { us: 'https://api.postman.com', eu: 'https://api.eu.postman.com', } as const; function isValidRegion(region: string): region is keyof typeof SUPPORTED_REGIONS { return region in SUPPORTED_REGIONS; } function setRegionEnvironment(region: string): void { if (!isValidRegion(region)) { throw new Error(`Invalid region: ${region}. Supported regions: us, eu`); } process.env.POSTMAN_API_BASE_URL = SUPPORTED_REGIONS[region]; } type LogLevel = 'debug' | 'info' | 'warn' | 'error'; function log(level: LogLevel, message: string, context?: Record<string, unknown>) { const timestamp = new Date().toISOString(); const suffix = context ? ` ${JSON.stringify(context)}` : ''; console.error(`[${timestamp}] [${level.toUpperCase()}] ${message}${suffix}`); } function sendClientLog(server: McpServer, level: LogLevel, data: string) { try { (server as any).sendLoggingMessage?.({ level, data }); } catch { // ignore } } function logBoth( server: McpServer | null | undefined, level: LogLevel, message: string, context?: Record<string, unknown> ) { log(level, message, context); if (server) sendClientLog(server, level, message); } type FullResourceMethod = (typeof enabledResources.full)[number]; type MinimalResourceMethod = (typeof enabledResources.minimal)[number]; type CodeResourceMethod = (typeof enabledResources.code)[number]; type EnabledResourceMethod = FullResourceMethod; interface ToolModule { method: EnabledResourceMethod; description: string; parameters: z.ZodObject<any>; annotations?: { title?: string; readOnlyHint?: boolean; destructiveHint?: boolean; idempotentHint?: boolean; }; handler: ( args: any, extra: { client: PostmanAPIClient; headers?: IsomorphicHeaders; serverContext?: ServerContext; } ) => Promise<CallToolResult>; } async function loadAllTools(): Promise<ToolModule[]> { const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); const toolsDir = join(__dirname, 'tools'); try { log('info', 'Loading tools from directory', { toolsDir }); const files = await readdir(toolsDir); const toolFiles = files.filter((file) => file.endsWith('.js')); log('debug', 'Discovered tool files', { count: toolFiles.length }); const tools: ToolModule[] = []; for (const file of toolFiles) { try { const toolPath = join(toolsDir, file); // If the OS is windows, prepend 'file://' to the path const isWindows = process.platform === 'win32'; const toolModule = await import(isWindows ? `file://${toolPath}` : toolPath); if ( toolModule.method && toolModule.description && toolModule.parameters && toolModule.handler ) { tools.push(toolModule as ToolModule); log('info', 'Loaded tool', { method: toolModule.method, file }); } else { log('warn', 'Tool module missing required exports; skipping', { file }); } } catch (error: any) { log('error', 'Failed to load tool module', { file, error: String(error?.message || error), }); } } log('info', 'Tool loading completed', { totalLoaded: tools.length }); return tools; } catch (error: any) { log('error', 'Failed to read tools directory', { toolsDir, error: String(error?.message || error), }); return []; } } const dotEnvOutput = dotenv.config({ quiet: true }); if (dotEnvOutput.error) { if ((dotEnvOutput.error as NodeJS.ErrnoException).code !== 'ENOENT') { log('error', `Error loading .env file: ${dotEnvOutput.error}`); process.exit(1); } } else { log( 'info', `Environment variables loaded: ${dotEnvOutput.parsed ? Object.keys(dotEnvOutput.parsed).length : 0} environment variables: ${Object.keys(dotEnvOutput.parsed || {}).join(', ')}` ); } let clientInfo: InitializeRequest['params']['clientInfo'] | undefined = undefined; async function run() { const args = process.argv.slice(2); const useFull = args.includes('--full'); const useCode = args.includes('--code'); 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: process.env.POSTMAN_API_BASE_URL, }); } else { log('error', `Invalid region: ${region}`); console.error(`Supported regions: ${Object.keys(SUPPORTED_REGIONS).join(', ')}`); process.exit(1); } } // For STDIO mode, validate API key is available in environment const apiKey = process.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 fullTools = allGeneratedTools.filter((t) => enabledResources.full.includes(t.method)); const minimalTools = allGeneratedTools.filter((t) => enabledResources.minimal.includes(t.method as MinimalResourceMethod) ); const codeTools = allGeneratedTools.filter((t) => enabledResources.code.includes(t.method as CodeResourceMethod) ); const tools = useCode ? codeTools : useFull ? fullTools : minimalTools; // Create McpServer instance const server = new McpServer({ name: SERVER_NAME, version: APP_VERSION }); // Surface MCP server errors to stderr and notify client if possible (server as any).onerror = (error: unknown) => { const msg = String((error as any)?.message || error); logBoth(server, 'error', `MCP server error: ${msg}`, { error: msg }); }; process.on('SIGINT', async () => { logBoth(server, 'warn', 'SIGINT received; shutting down'); await server.close(); process.exit(0); }); // Create server context that will be passed to all tools const serverContext: ServerContext = { serverType: useCode ? 'code' : useFull ? 'full' : 'minimal', availableTools: tools.map((t) => t.method), }; // 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 .tool() method for (const tool of tools) { server.tool( tool.method, tool.description, tool.parameters.shape, tool.annotations || {}, async (args, extra) => { const toolName = tool.method; // Keep start event on stderr only to reduce client noise log('info', `Tool invocation started: ${toolName}`, { toolName }); try { const start = Date.now(); const result = await tool.handler(args, { client, headers: { ...extra?.requestInfo?.headers, 'user-agent': clientInfo?.name, }, serverContext, }); const durationMs = Date.now() - start; // Completion: stderr only to avoid spamming client logs log('info', `Tool invocation completed: ${toolName} (${durationMs}ms)`, { toolName, durationMs, }); return result; } catch (error: any) { const errMsg = String(error?.message || error); // Failures: notify both server stderr and client logBoth(server, 'error', `Tool invocation failed: ${toolName}: ${errMsg}`, { toolName }); if (error instanceof McpError) throw error; throw new McpError(ErrorCode.InternalError, `API error: ${error.message}`); } } ); } // API key validation is handled by the singleton client 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 }); } }; await server.connect(transport); const toolsetName = useCode ? 'code' : useFull ? 'full' : 'minimal'; logBoth( server, 'info', `Server connected and ready: ${SERVER_NAME}@${APP_VERSION} with ${tools.length} tools (${toolsetName})` ); } run().catch((error: unknown) => { log('error', 'Unhandled error during server execution', { error: String((error as any)?.message || error), }); process.exit(1); });

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/postmanlabs/postman-mcp-server'

If you have feedback or need assistance with the MCP directory API, please join our Discord server