Skip to main content
Glama
postman.ts6.04 kB
import { IsomorphicHeaders, McpError, ErrorCode } from '@modelcontextprotocol/sdk/types.js'; import { USER_AGENT } from '../constants.js'; import type { ServerContext } from '../tools/utils/toolHelpers.js'; export enum ContentType { Json = 'application/json', JsonPatch = 'application/json-patch+json', } export interface PostmanAPIRequestOptions extends Omit<RequestInit, 'headers'> { contentType?: ContentType; headers?: IsomorphicHeaders; } export interface IPostmanAPIClient { get<T = any>( endpoint: string, options?: Omit<PostmanAPIRequestOptions, 'method' | 'body'> ): Promise<T>; post<T = any>(endpoint: string, options?: PostmanAPIRequestOptions): Promise<T>; put<T = any>(endpoint: string, options?: PostmanAPIRequestOptions): Promise<T>; patch<T = any>(endpoint: string, options?: PostmanAPIRequestOptions): Promise<T>; delete<T = any>( endpoint: string, options?: Omit<PostmanAPIRequestOptions, 'method' | 'body'> ): Promise<T>; } /** * Postman API Client following SOLID principles * - Single Responsibility: Handles HTTP requests to Postman API * - Open/Closed: Extensible for different request types * - Liskov Substitution: Consistent interface implementation * - Interface Segregation: Focused IPostmanAPIClient interface * - Dependency Inversion: Depends on abstractions, not concretions */ export class PostmanAPIClient implements IPostmanAPIClient { private readonly baseUrl: string; private readonly apiKey?: string; private readonly serverContext?: ServerContext; private static instance: PostmanAPIClient | null = null; constructor( apiKey?: string, baseUrl: string = process.env.POSTMAN_API_BASE_URL || 'https://api.postman.com', serverContext?: ServerContext ) { this.apiKey = apiKey; this.baseUrl = baseUrl; this.serverContext = serverContext; } /** * Get singleton instance of PostmanAPIClient * Creates instance if it doesn't exist, otherwise returns existing instance */ static getInstance(apiKey?: string, baseUrl?: string): PostmanAPIClient { if (!PostmanAPIClient.instance) { PostmanAPIClient.instance = new PostmanAPIClient(apiKey, baseUrl); } return PostmanAPIClient.instance; } /** * Reset singleton instance (useful for testing or reconfiguration) */ static resetInstance(): void { PostmanAPIClient.instance = null; } async get<T = any>( endpoint: string, options: Omit<PostmanAPIRequestOptions, 'method' | 'body'> = {} ): Promise<T> { return this.request(endpoint, { ...options, method: 'GET' }); } async post<T = any>(endpoint: string, options: PostmanAPIRequestOptions = {}): Promise<T> { return this.request(endpoint, { ...options, method: 'POST' }); } async put<T = any>(endpoint: string, options: PostmanAPIRequestOptions = {}): Promise<T> { return this.request(endpoint, { ...options, method: 'PUT' }); } async patch<T = any>(endpoint: string, options: PostmanAPIRequestOptions = {}): Promise<T> { return this.request(endpoint, { ...options, method: 'PATCH' }); } async delete<T = any>( endpoint: string, options: Omit<PostmanAPIRequestOptions, 'method' | 'body'> = {} ): Promise<T> { return this.request(endpoint, { ...options, method: 'DELETE' }); } private async request<T = any>( endpoint: string, options: PostmanAPIRequestOptions & { method: string } ): Promise<T> { // Get API key at request time - check instance, then environment const currentApiKey = this.apiKey || process.env.POSTMAN_API_KEY; if (!currentApiKey) { throw new Error( 'API key is required for requests. Provide it via constructor parameter or set POSTMAN_API_KEY environment variable.' ); } const contentType = options.contentType || ContentType.Json; const userAgentKey = Object.keys(options.headers ?? {}).find( (key) => key.toLowerCase() === 'user-agent' ); const userAgentValue = userAgentKey ? options.headers?.[userAgentKey] : undefined; // Build user agent with toolset information if available let userAgentHeader = userAgentValue ? `${userAgentValue}/${USER_AGENT}` : USER_AGENT; if (this.serverContext?.serverType) { userAgentHeader = `${userAgentHeader} (toolset: ${this.serverContext.serverType})`; } const disallowed = new Set([ 'content-length', 'transfer-encoding', 'connection', 'host', 'accept-encoding', 'keep-alive', ]); const extra = Object.fromEntries( Object.entries(options.headers ?? {}).filter(([k]) => !disallowed.has(k.toLowerCase())) ); const hasBody = options.body !== undefined && options.body !== null; const headers = { ...(hasBody ? { 'content-type': contentType } : {}), ...extra, 'x-api-key': currentApiKey, 'user-agent': userAgentHeader, }; const { headers: _ignored, ...optionsWithoutHeaders } = options; const response = await fetch(`${this.baseUrl}${endpoint}`, { ...optionsWithoutHeaders, headers, signal: AbortSignal.timeout(300000), }); if (!response.ok) { await this.handleErrorResponse(response); } if (response.status === 204) return null as T; const responseContentType = response.headers.get('content-type') || ''; if (responseContentType.includes('application/json')) { return response.json(); } return response.text() as T; } private async handleErrorResponse(response: Response): Promise<never> { const errorText = await response.text(); switch (response.status) { case 400: case 422: case 401: case 403: throw new McpError( ErrorCode.InvalidParams, `API request failed: ${response.status} ${errorText}`, { cause: errorText, } ); default: throw new McpError(ErrorCode.InternalError, `API request failed: ${response.status}`, { cause: errorText, }); } } }

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