amazon-seller-mcp
Version:
Model Context Protocol (MCP) client for Amazon Selling Partner API
726 lines • 27.7 kB
JavaScript
/**
* 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