Skip to main content
Glama
mercadolibre

Mercado Pago MCP Server

Official
by mercadolibre
index.ts•13.6 kB
#!/usr/bin/env node import { Server } from '@modelcontextprotocol/sdk/server/index.js'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; import { CallToolRequestSchema, ErrorCode, ListToolsRequestSchema, McpError, } from '@modelcontextprotocol/sdk/types.js'; import axios, { AxiosInstance, AxiosError, InternalAxiosRequestConfig } from 'axios'; // Extend AxiosConfig to include retry flag declare module 'axios' { export interface InternalAxiosRequestConfig { __isRetry?: boolean; } } // Simple logger class class Logger { log(level: 'info' | 'error' | 'debug', message: string, context?: any) { const timestamp = new Date().toISOString(); const contextStr = context ? `\n${JSON.stringify(context, null, 2)}` : ''; const logMessage = `[${timestamp}] [${level.toUpperCase()}] ${message}${contextStr}\n`; process.stderr.write(logMessage); } info(message: string, context?: any) { this.log('info', message, context); } error(message: string, context?: any) { this.log('error', message, context); } debug(message: string, context?: any) { if (process.env.DEBUG) { this.log('debug', message, context); } } } // Initialize global logger const logger = new Logger(); // Environment variables if (!process.env.CLIENT_ID || !process.env.CLIENT_SECRET) { logger.error('Missing required environment variables', { CLIENT_ID: !!process.env.CLIENT_ID, CLIENT_SECRET: !!process.env.CLIENT_SECRET }); process.exit(1); } // Interfaces interface AuthResponse { access_token: string; token_type: string; expires_in: number; scope: string; } type SiteId = 'MLA' | 'MLB' | 'MLM' | 'MLU' | 'MLC' | 'MCO' | 'MPE'; type Language = 'es' | 'pt'; interface SearchResult { title: string; content: string; url: string; score?: number; } interface SearchDocumentationArgs { language: Language; // maps to lang query: string; // maps to term siteId: SiteId; // Required parameter limit?: number; // maps to maxResults } interface RequestContext { toolName: string; requestId: string; startTime: number; } // Auth service class AuthService { private accessToken: string | null = null; private logger: Logger; constructor(logger: Logger) { this.logger = logger; } async getAccessToken(): Promise<string> { if (this.accessToken) return this.accessToken; try { this.logger.debug('Requesting OAuth token'); const response = await axios.post<AuthResponse>( 'https://api.mercadolibre.com/oauth/token', { grant_type: 'client_credentials', client_id: process.env.CLIENT_ID, client_secret: process.env.CLIENT_SECRET }, { headers: { 'Content-Type': 'application/json', 'Accept': 'application/json', } } ); this.accessToken = response.data.access_token; this.logger.debug('OAuth token obtained', { expires_in: response.data.expires_in }); return this.accessToken; } catch (error) { this.logger.error('Failed to obtain OAuth token', { error }); if (error instanceof Error) { throw new McpError( ErrorCode.InternalError, `MercadoPago Auth error: ${error.message}` ); } throw error; } } clearToken() { this.accessToken = null; } } // Main server class class DemoMercadoPagoServer { private server: Server; private api: AxiosInstance; private logger: Logger; private authService: AuthService; private requestCount: number = 0; private buildDocumentationUrl(siteDomain: string, language: Language, path: string): string { if (!siteDomain || !language) { throw new McpError(ErrorCode.InvalidParams, 'Missing required parameters for URL construction'); } return `https://${siteDomain}/developers/${language}${path || ''}`; } private static readonly VALID_SITES: readonly SiteId[] = [ 'MLA', 'MLB', 'MLM', 'MLU', 'MLC', 'MCO', 'MPE' ] as const; private static readonly SITE_DOMAINS: Record<SiteId, string> = { MLA: "www.mercadopago.com.ar", MLB: "www.mercadopago.com.br", MLM: "www.mercadopago.com.mx", MLU: "www.mercadopago.com.uy", MLC: "www.mercadopago.cl", MCO: "www.mercadopago.com.co", MPE: "www.mercadopago.com.pe" } as const; constructor() { this.logger = new Logger(); this.logger.info('Initializing MercadoPago MCP Server'); this.authService = new AuthService(this.logger); this.server = new Server( { name: 'mercadopago', version: '0.1.0', }, { capabilities: { tools: {} }, } ); this.api = axios.create({ baseURL: 'https://api.mercadopago.com/developers', headers: { 'Content-Type': 'application/json', 'Accept': 'application/json', }, }); this.setupAxiosInterceptors(); this.setupToolHandlers(); } private setupAxiosInterceptors() { // Auth interceptor this.api.interceptors.request.use(async (config) => { const token = await this.authService.getAccessToken(); config.headers.Authorization = `Bearer ${token}`; return config; }); // Request logging this.api.interceptors.request.use((config) => { this.logger.debug('API Request', { method: config.method, url: config.url, params: config.params, data: config.data, }); return config; }); // Response logging and error handling this.api.interceptors.response.use( (response) => { this.logger.debug('API Response', { status: response.status, url: response.config.url, data: response.data, }); return response; }, async (error: AxiosError) => { this.logger.error('API Error', { url: error.config?.url, method: error.config?.method, status: error.response?.status, data: error.response?.data, message: error.message, }); // If auth error, clear token and retry once if (error.response?.status === 401 && error.config) { const config = error.config as InternalAxiosRequestConfig; if (!config.__isRetry) { this.logger.debug('Auth token expired, retrying request'); this.authService.clearToken(); config.__isRetry = true; return this.api(config); } } throw error; } ); } private createRequestContext(toolName: string): RequestContext { this.requestCount++; return { toolName, requestId: `req_${this.requestCount}`, startTime: Date.now(), }; } private logRequestEnd(context: RequestContext) { const duration = Date.now() - context.startTime; this.logger.debug('Tool execution completed', { ...context, duration: `${duration}ms`, }); } private setupToolHandlers() { this.server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: [ { name: 'search_documentation', description: 'Search Mercado Pago documentation', inputSchema: { type: 'object', properties: { language: { type: 'string', enum: ['es', 'pt'], description: 'Language of the documentation (es: Spanish, pt: Portuguese)' }, query: { type: 'string', description: 'Search query' }, siteId: { type: 'string', description: 'Site ID for documentation', enum: ['MLB', 'MLM', 'MLA', 'MLU', 'MLC', 'MCO', 'MPE'] }, limit: { type: 'number', description: 'Maximum number of results to return (default: 10)', minimum: 1, maximum: 100 } }, required: ['language', 'query', 'siteId'] } } ], })); this.server.setRequestHandler(CallToolRequestSchema, async (request) => { const context = this.createRequestContext(request.params.name); this.logger.info('Tool invoked', { context, args: request.params.arguments }); try { if (request.params.name === 'search_documentation') { if (this.isSearchDocumentationArgs(request.params.arguments)) { const result = await this.handleSearchDocumentation(request.params.arguments, context); this.logRequestEnd(context); return result; } throw new McpError(ErrorCode.InvalidParams, 'Invalid search_documentation arguments'); } throw new McpError( ErrorCode.MethodNotFound, `Unknown tool: ${request.params.name}` ); } catch (error) { this.logger.error('Tool execution failed', { context, error: error instanceof Error ? { name: error.name, message: error.message, stack: error.stack, } : error, }); if (axios.isAxiosError(error)) { return { content: [ { type: 'text', text: `MercadoPago API error: ${ error.response?.data?.message || error.message }`, }, ], isError: true, }; } throw error; } }); } private isSearchDocumentationArgs(args: unknown): args is SearchDocumentationArgs { if (!args || typeof args !== 'object') return false; const a = args as any; const validLanguage = (a.language && ['es', 'pt'].includes(a.language as Language)); const validQuery = typeof a.query === 'string'; const validSiteId = (a.siteId && DemoMercadoPagoServer.VALID_SITES.includes(a.siteId as SiteId)); const validLimit = a.limit === undefined || (typeof a.limit === 'number' && a.limit >= 1 && a.limit <= 100); return validLanguage && validQuery && validSiteId && validLimit; } private async handleSearchDocumentation(args: SearchDocumentationArgs, context: RequestContext) { try { const response = await this.api.get<SearchResult[]>( '/docs/v1/search', { params: { term: args.query, lang: args.language, siteId: args.siteId, maxResults: args.limit || 10 } } ); const results = response.data; if (!results || !Array.isArray(results)) { throw new McpError( ErrorCode.InternalError, 'No search results available for the given query' ); } if (results.length === 0) { return { content: [ { type: 'text', text: `No documentation found for query "${args.query}". Try a different search term.` } ] }; } // Format results as markdown const markdown = results.map(result => { if (!result.title && !result.content) { this.logger.debug('Incomplete search result', { result }); return null; // Skip invalid results } const title = result.title || 'Untitled'; const content = result.content || 'No description available'; const siteDomain = DemoMercadoPagoServer.SITE_DOMAINS[args.siteId]; const url = this.buildDocumentationUrl(siteDomain, args.language, result.url || ''); const score = result.score || 0; return `## ${title} ${content} đź”— [Read more](${url}) Score: ${score} `; }).join('\n\n---\n\n'); const formattedResults = `# Search Results for "${args.query}" Showing ${results.length} results ${results.length > 0 ? markdown : 'No results found.'}`; return { content: [ { type: 'text', text: formattedResults } ] }; } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred'; return { content: [ { type: 'text', text: `Error searching documentation: ${errorMessage}` } ], isError: true }; } } async run() { try { this.logger.info('Starting MercadoPago MCP server'); const transport = new StdioServerTransport(); this.logger.debug('Connecting to stdio transport'); await this.server.connect(transport); this.logger.info('Server running and ready'); } catch (error) { this.logger.error('Failed to start server', { error: error instanceof Error ? { name: error.name, message: error.message, stack: error.stack, } : error, }); throw error; } } } // Initialize and run server with error handling process.on('uncaughtException', (error: Error) => { logger.error('Uncaught exception', { error }); process.exit(1); }); process.on('unhandledRejection', (reason: any) => { logger.error('Unhandled rejection', { reason }); process.exit(1); }); const server = new DemoMercadoPagoServer(); server.run().catch((error: Error) => { logger.error('Server failed to start', { 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/mercadolibre/demo-mercadopago-mcp-server'

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