UNPKG

amazon-seller-mcp

Version:

Model Context Protocol (MCP) client for Amazon Selling Partner API

726 lines 27.7 kB
/** * Amazon Seller MCP Server implementation * * This file implements the MCP server for Amazon Selling Partner API * using the Model Context Protocol SDK. */ // Node.js built-ins import { createServer } from 'node:http'; import { randomUUID } from 'node:crypto'; // Third-party dependencies import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js'; import { ResourceRegistrationManager } from './resources.js'; import { ToolRegistrationManager } from './tools.js'; import { NotificationManager } from './notifications.js'; import { setupInventoryChangeNotifications } from './inventory-notifications.js'; import { setupOrderStatusChangeNotifications } from './order-notifications.js'; import { wrapToolHandlerWithErrorHandling, wrapResourceHandlerWithErrorHandling, } from './error-handler.js'; import { configureCacheManager } from '../utils/cache-manager.js'; import { configureConnectionPool } from '../utils/connection-pool.js'; import { getLogger } from '../utils/logger.js'; /** * Amazon Seller MCP Server class * Implements the MCP protocol for Amazon Selling Partner API */ export class AmazonSellerMcpServer { /** * MCP server instance */ server; /** * Server configuration */ config; /** * Transport instance */ transport = null; /** * HTTP server instance (for streamableHttp transport) */ httpServer = null; /** * Map to store transports by session ID (for streamableHttp) */ transports = new Map(); /** * Whether the server is connected */ isConnected = false; /** * Resource registration manager */ resourceManager; /** * Tool registration manager */ toolManager; /** * Notification manager */ notificationManager; /** * Validates the server configuration * @param config Server configuration to validate * @throws Error if configuration is invalid */ validateConfiguration(config) { // Validate required fields if (!config.name || typeof config.name !== 'string') { throw new Error('Server name is required and must be a string'); } if (!config.version || typeof config.version !== 'string') { throw new Error('Server version is required and must be a string'); } if (!config.marketplaceId || typeof config.marketplaceId !== 'string') { throw new Error('Marketplace ID is required and must be a string'); } // Validate credentials if (!config.credentials) { throw new Error('Credentials are required'); } const { clientId, clientSecret, refreshToken } = config.credentials; if (!clientId || typeof clientId !== 'string' || clientId.trim() === '') { throw new Error('Client ID is required and must be a non-empty string'); } if (!clientSecret || typeof clientSecret !== 'string' || clientSecret.trim() === '') { throw new Error('Client secret is required and must be a non-empty string'); } if (!refreshToken || typeof refreshToken !== 'string' || refreshToken.trim() === '') { throw new Error('Refresh token is required and must be a non-empty string'); } // If IAM credentials are provided, validate them if (config.credentials.accessKeyId || config.credentials.secretAccessKey) { if (!config.credentials.accessKeyId || !config.credentials.secretAccessKey) { throw new Error('Both accessKeyId and secretAccessKey must be provided if using IAM authentication'); } } } /** * Creates a new instance of the Amazon Seller MCP Server * @param config Server configuration */ constructor(config) { this.config = config; // Validate configuration this.validateConfiguration(config); // Configure cache manager if provided if (config.cacheConfig) { configureCacheManager(config.cacheConfig); } // Configure connection pool if provided if (config.connectionPoolConfig) { configureConnectionPool(config.connectionPoolConfig); } // Create MCP server instance this.server = new McpServer({ name: config.name, version: config.version, description: `Amazon Selling Partner API MCP Server for marketplace ${config.marketplaceId} in region ${config.region}`, }); // Create resource registration manager this.resourceManager = new ResourceRegistrationManager(this.server); // Create tool registration manager this.toolManager = new ToolRegistrationManager(this.server); // Create notification manager this.notificationManager = new NotificationManager(this.server, { debounced: config.debouncedNotifications, debounceTime: 1000, // 1 second debounce time }); getLogger().info(`Initialized Amazon Seller MCP Server: ${config.name} v${config.version}`); } /** * Connects the server to the specified transport * @param transportConfig Transport configuration */ async connect(transportConfig) { getLogger().info(`Connecting to ${transportConfig.type} transport`); try { if (transportConfig.type === 'streamableHttp' && transportConfig.httpOptions) { await this.setupHttpTransport(transportConfig.httpOptions); } else { // Default to stdio transport this.transport = new StdioServerTransport(); await this.server.connect(this.transport); getLogger().info('STDIO transport initialized'); } this.isConnected = true; getLogger().info('Server connected successfully'); } catch (error) { getLogger().error('Failed to connect server:', { error: error.message }); throw new Error(`Failed to connect server: ${error.message}`); } } /** * Sets up HTTP transport with proper request handling */ async setupHttpTransport(httpOptions) { const { port, host, enableDnsRebindingProtection, allowedHosts, sessionManagement } = httpOptions; // Create HTTP server this.httpServer = createServer(async (req, res) => { // Set CORS headers res.setHeader('Access-Control-Allow-Origin', '*'); res.setHeader('Access-Control-Allow-Methods', 'GET, POST, DELETE, OPTIONS'); res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Mcp-Session-Id, Last-Event-ID'); res.setHeader('Access-Control-Expose-Headers', 'Mcp-Session-Id'); // Handle preflight requests if (req.method === 'OPTIONS') { res.writeHead(200); res.end(); return; } try { await this.handleHttpRequest(req, res, { enableDnsRebindingProtection, allowedHosts, sessionManagement, }); } catch (error) { getLogger().error('Error handling HTTP request:', { error: error.message }); if (!res.headersSent) { res.writeHead(500, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ jsonrpc: '2.0', error: { code: -32603, message: 'Internal server error', }, id: null, })); } } }); // Start HTTP server await new Promise((resolve, reject) => { this.httpServer.listen(port, host, (error) => { if (error) { reject(error); } else { getLogger().info(`HTTP server started on ${host}:${port}`); resolve(); } }); }); } /** * Handles HTTP requests for streamable transport */ async handleHttpRequest(req, res, options) { const sessionId = req.headers['mcp-session-id']; // Handle POST requests (JSON-RPC) if (req.method === 'POST') { let body = ''; req.on('data', (chunk) => { body += chunk.toString(); }); req.on('end', async () => { try { const parsedBody = JSON.parse(body); await this.handleMcpRequest(req, res, parsedBody, sessionId, options); } catch (error) { getLogger().error('Error parsing request body:', { error: error.message }); res.writeHead(400, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ jsonrpc: '2.0', error: { code: -32700, message: 'Parse error', }, id: null, })); } }); } // Handle GET requests (SSE streams) else if (req.method === 'GET') { if (!sessionId || !this.transports.has(sessionId)) { res.writeHead(400, { 'Content-Type': 'text/plain' }); res.end('Invalid or missing session ID'); return; } const transport = this.transports.get(sessionId); await transport.handleRequest(req, res); } // Handle DELETE requests (session termination) else if (req.method === 'DELETE') { if (!sessionId || !this.transports.has(sessionId)) { res.writeHead(400, { 'Content-Type': 'text/plain' }); res.end('Invalid or missing session ID'); return; } const transport = this.transports.get(sessionId); await transport.handleRequest(req, res); } // Handle unsupported methods else { res.writeHead(405, { 'Content-Type': 'text/plain' }); res.end('Method not allowed'); } } /** * Handles MCP JSON-RPC requests */ async handleMcpRequest(req, res, parsedBody, sessionId, options) { let transport; if (sessionId && this.transports.has(sessionId)) { // Reuse existing transport transport = this.transports.get(sessionId); } else if (!sessionId && this.isInitializeRequest(parsedBody)) { // New initialization request transport = new StreamableHTTPServerTransport({ sessionIdGenerator: options.sessionManagement ? () => randomUUID() : undefined, enableDnsRebindingProtection: options.enableDnsRebindingProtection, allowedHosts: options.allowedHosts, onsessioninitialized: (sessionId) => { getLogger().info(`Session initialized with ID: ${sessionId}`); this.transports.set(sessionId, transport); }, onsessionclosed: (sessionId) => { getLogger().info(`Session closed: ${sessionId}`); this.transports.delete(sessionId); }, }); // Set up onclose handler transport.onclose = () => { const sid = transport.sessionId; if (sid && this.transports.has(sid)) { getLogger().info(`Transport closed for session ${sid}`); this.transports.delete(sid); } }; // Connect the transport to the MCP server await this.server.connect(transport); } else { // Invalid request res.writeHead(400, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ jsonrpc: '2.0', error: { code: -32000, message: 'Bad Request: No valid session ID provided', }, id: null, })); return; } // Handle the request with the transport await transport.handleRequest(req, res, parsedBody); } /** * Checks if a request is an initialize request */ isInitializeRequest(body) { return body && body.method === 'initialize'; } /** * Registers all available tools */ async registerAllTools() { getLogger().info('Registering tools'); // Register catalog tools await this.registerCatalogTools(); // Register listings tools await this.registerListingsTools(); // Register inventory tools await this.registerInventoryTools(); // Register orders tools await this.registerOrdersTools(); // Register reports tools await this.registerReportsTools(); // Register AI-assisted tools await this.registerAiTools(); } /** * Registers catalog tools */ async registerCatalogTools() { getLogger().info('Registering catalog tools'); try { // Import and register catalog tools const { registerCatalogTools } = await import('../tools/catalog-tools.js'); registerCatalogTools(this.toolManager, { credentials: this.config.credentials, region: this.config.region, marketplaceId: this.config.marketplaceId, }); } catch (error) { getLogger().error('Failed to register catalog tools:', { error: error.message }); throw error; } } /** * Registers listings tools */ async registerListingsTools() { getLogger().info('Registering listings tools'); try { // Import and register listings tools const { registerListingsTools } = await import('../tools/listings-tools.js'); registerListingsTools(this.toolManager, { credentials: this.config.credentials, region: this.config.region, marketplaceId: this.config.marketplaceId, }); } catch (error) { getLogger().error('Failed to register listings tools:', { error: error.message }); throw error; } } /** * Registers inventory tools */ async registerInventoryTools() { getLogger().info('Registering inventory tools'); try { // Import and register inventory tools const { registerInventoryTools } = await import('../tools/inventory-tools.js'); const { InventoryClient } = await import('../api/inventory-client.js'); // Create inventory client const inventoryClient = new InventoryClient({ credentials: this.config.credentials, region: this.config.region, marketplaceId: this.config.marketplaceId, }); // Set up inventory change notifications setupInventoryChangeNotifications(inventoryClient, this.notificationManager); // Register inventory tools registerInventoryTools(this.toolManager, { credentials: this.config.credentials, region: this.config.region, marketplaceId: this.config.marketplaceId, }, inventoryClient); } catch (error) { getLogger().error('Failed to register inventory tools:', { error: error.message }); throw error; } } /** * Registers orders tools */ async registerOrdersTools() { getLogger().info('Registering orders tools'); try { // Import and register orders tools const { registerOrdersTools } = await import('../tools/orders-tools.js'); const { OrdersClient } = await import('../api/orders-client.js'); // Create orders client const ordersClient = new OrdersClient({ credentials: this.config.credentials, region: this.config.region, marketplaceId: this.config.marketplaceId, }); // Set up order status change notifications setupOrderStatusChangeNotifications(ordersClient, this.notificationManager); // Register orders tools registerOrdersTools(this.toolManager, { credentials: this.config.credentials, region: this.config.region, marketplaceId: this.config.marketplaceId, }, ordersClient); } catch (error) { getLogger().error('Failed to register orders tools:', { error: error.message }); throw error; } } /** * Registers reports tools */ async registerReportsTools() { getLogger().info('Registering reports tools'); try { // Import and register reports tools const { registerReportsTools } = await import('../tools/reports-tools.js'); registerReportsTools(this.toolManager, { credentials: this.config.credentials, region: this.config.region, marketplaceId: this.config.marketplaceId, }); } catch (error) { getLogger().error('Failed to register reports tools:', { error: error.message }); throw error; } } /** * Registers AI-assisted tools */ async registerAiTools() { getLogger().info('Registering AI-assisted tools'); try { // Import and register AI tools const { registerAiTools } = await import('../tools/ai-tools.js'); registerAiTools(this.toolManager, { credentials: this.config.credentials, region: this.config.region, marketplaceId: this.config.marketplaceId, }); } catch (error) { getLogger().error('Failed to register AI tools:', { error: error.message }); throw error; } } /** * Registers a tool with the MCP server * * @param name Tool name * @param options Tool registration options * @param handler Tool handler function * @returns True if the tool was registered, false if it was already registered */ registerTool(name, options, handler) { // Wrap the handler with error handling const wrappedHandler = wrapToolHandlerWithErrorHandling(handler); return this.toolManager.registerTool(name, options, wrappedHandler); } /** * Gets the tool registration manager */ getToolManager() { return this.toolManager; } /** * Gets the notification manager */ getNotificationManager() { return this.notificationManager; } /** * Registers all available resources */ async registerAllResources() { getLogger().info('Registering resources'); // Register catalog resources await this.registerCatalogResources(); // Register listings resources await this.registerListingsResources(); // Register inventory resources await this.registerInventoryResources(); // Register orders resources await this.registerOrdersResources(); // Register reports resources await this.registerReportsResources(); } /** * Registers catalog resources */ async registerCatalogResources() { getLogger().info('Registering catalog resources'); try { // Import and register catalog resources const { registerCatalogResources } = await import('../resources/catalog/catalog-resources.js'); registerCatalogResources(this.resourceManager, { credentials: this.config.credentials, region: this.config.region, marketplaceId: this.config.marketplaceId, }); } catch (error) { getLogger().error('Failed to register catalog resources:', { error: error.message, }); throw error; } } /** * Registers listings resources */ async registerListingsResources() { getLogger().info('Registering listings resources'); try { // Import and register listings resources const { registerListingsResources } = await import('../resources/listings/listings-resources.js'); registerListingsResources(this.resourceManager, { credentials: this.config.credentials, region: this.config.region, marketplaceId: this.config.marketplaceId, }); } catch (error) { getLogger().error('Failed to register listings resources:', { error: error.message, }); throw error; } } /** * Registers inventory resources */ async registerInventoryResources() { getLogger().info('Registering inventory resources'); try { // Import and register inventory resources const { registerInventoryResources } = await import('../resources/inventory/inventory-resources.js'); registerInventoryResources(this.resourceManager, { credentials: this.config.credentials, region: this.config.region, marketplaceId: this.config.marketplaceId, }); } catch (error) { getLogger().error('Failed to register inventory resources:', { error: error.message, }); throw error; } } /** * Registers orders resources */ async registerOrdersResources() { getLogger().info('Registering orders resources'); try { // Import and register orders resources const { registerOrdersResources } = await import('../resources/orders/orders-resources.js'); registerOrdersResources(this.resourceManager, { credentials: this.config.credentials, region: this.config.region, marketplaceId: this.config.marketplaceId, }); } catch (error) { getLogger().error('Failed to register orders resources:', { error: error.message, }); throw error; } } /** * Registers reports resources */ async registerReportsResources() { getLogger().info('Registering reports resources'); try { // Import and register reports resources const { registerReportsResources } = await import('../resources/reports/reports-resources.js'); registerReportsResources(this.resourceManager, { credentials: this.config.credentials, region: this.config.region, marketplaceId: this.config.marketplaceId, }); } catch (error) { getLogger().error('Failed to register reports resources:', { error: error.message, }); throw error; } } /** * Registers a resource with the MCP server * * @param name Resource name * @param uriTemplate URI template string * @param options Resource registration options * @param handler Resource handler function * @param listTemplate Optional list template string * @param completions Optional completions configuration * @returns True if the resource was registered, false if it was already registered */ registerResource(name, uriTemplate, options, handler, listTemplate, completions) { const template = this.resourceManager.createResourceTemplate(uriTemplate, listTemplate, completions); // Wrap the handler with error handling const wrappedHandler = wrapResourceHandlerWithErrorHandling(handler); return this.resourceManager.registerResource(name, template, options, wrappedHandler); } /** * Gets the resource registration manager */ getResourceManager() { return this.resourceManager; } /** * Closes the server and cleans up resources */ async close() { getLogger().info('Closing server'); try { // Close all active transports for (const [sessionId, transport] of this.transports) { getLogger().info(`Closing transport for session ${sessionId}`); try { await transport.close(); } catch (error) { getLogger().warn(`Error closing transport ${sessionId}:`, { error: error.message, }); } } this.transports.clear(); // Close stdio transport if it exists if (this.transport) { try { if ('close' in this.transport && typeof this.transport.close === 'function') { await this.transport.close(); } } catch (error) { getLogger().warn('Error closing stdio transport:', { error: error.message }); } this.transport = null; } // Close HTTP server if it exists if (this.httpServer) { await new Promise((resolve, reject) => { const timeout = setTimeout(() => { reject(new Error('HTTP server close timeout')); }, 5000); // 5 second timeout this.httpServer.close((error) => { clearTimeout(timeout); if (error) { reject(error); } else { getLogger().info('HTTP server closed'); resolve(); } }); }); this.httpServer = null; } // Close MCP server if (this.isConnected) { // MCP server doesn't have a close method, just mark as disconnected this.isConnected = false; getLogger().info('Server closed successfully'); } } catch (error) { getLogger().error('Error closing server:', { error: error.message }); throw new Error(`Error closing server: ${error.message}`); } } /** * Checks if the server is connected */ isServerConnected() { return this.isConnected; } /** * Gets the MCP server instance */ getMcpServer() { return this.server; } /** * Gets the server configuration */ getConfig() { return { ...this.config }; } } //# sourceMappingURL=server.js.map