Skip to main content
Glama
devkindhq

Boilerplate MCP Server

by devkindhq
swell.products.service.ts24.2 kB
// TODO: Revisit type definitions after Swell API documentation response // Currently using 'any' types for some error handling due to incomplete type definitions import { z } from 'zod'; import { Logger } from '../utils/logger.util.js'; import { swellClient } from '../utils/swell-client.util.js'; import { config } from '../utils/config.util.js'; import { createApiError, createUnexpectedError, McpError, } from '../utils/error.util.js'; import { SwellProduct, SwellProductSchema, SwellProductsList, SwellProductsListSchema, ProductListOptions, ProductSearchOptions, ProductGetOptions, StockCheckOptions, ProductUpdateOptions, ProductUpdateOptionsSchema, StockUpdateOptions, StockUpdateOptionsSchema, PricingUpdateOptions, PricingUpdateOptionsSchema, UpdateResult, } from './swell.products.types.js'; // Create a contextualized logger for this file const serviceLogger = Logger.forContext('services/swell.products.service.ts'); // Log service initialization serviceLogger.debug('Swell Products service initialized'); /** * @namespace SwellProductsService * @description Service layer for interacting with Swell Products API. * Handles product listing, retrieval, search, and inventory operations. */ /** * @function list * @description Fetches a paginated list of products from Swell with optional filtering. * @memberof SwellProductsService * @param {ProductListOptions} [options={}] - Optional filtering and pagination options * @returns {Promise<SwellProductsList>} A promise that resolves to the products list with pagination info * @throws {McpError} Throws an `McpError` if the API call fails or response validation fails * @example * // Get first 10 active products * const products = await list({ active: true, limit: 10 }); * // Get products in a specific category * const categoryProducts = await list({ category: 'electronics', page: 2 }); */ async function list( options: ProductListOptions = {}, ): Promise<SwellProductsList> { const methodLogger = serviceLogger.forMethod('list'); methodLogger.debug('Fetching products list', options); try { // Ensure client is initialized if (!swellClient.isClientInitialized()) { swellClient.initWithAutoConfig(); } const client = swellClient.getClient(); // Build query parameters const queryParams: Record<string, unknown> = {}; if (options.page !== undefined) { queryParams.page = options.page; } if (options.limit !== undefined) { queryParams.limit = options.limit; } if (options.active !== undefined) { queryParams.active = options.active; } if (options.category) { queryParams.category = options.category; } if (options.tags && options.tags.length > 0) { queryParams.tags = options.tags.join(','); } if (options.search) { queryParams.search = options.search; } if (options.sort) { queryParams.sort = options.sort; } if (options.where) { queryParams.where = options.where; } if (options.expand && options.expand.length > 0) { queryParams.expand = options.expand.join(','); } // Make the API call const rawData = await client.get<unknown>('/products', queryParams); // Check if debug mode is enabled const isDebugMode = config.getBoolean('DEBUG', false); if (isDebugMode) { methodLogger.debug( 'Debug mode enabled - returning raw data without validation', ); return rawData as SwellProductsList; } // Validate response with Zod schema const validatedData = SwellProductsListSchema.parse(rawData); methodLogger.debug( `Successfully fetched ${validatedData.results.length} products`, { count: validatedData.count, page: validatedData.page, pages: validatedData.pages, }, ); return validatedData; } catch (error) { methodLogger.error('Service error fetching products list', error); // Handle Zod validation errors if (error instanceof z.ZodError) { throw createApiError( `Products list response validation failed: ${error.issues .map((e: z.ZodIssue) => `${e.path.join('.')}: ${e.message}`) .join(', ')}`, 500, error, ); } // Rethrow other McpErrors if (error instanceof McpError) { throw error; } // Wrap any other unexpected errors throw createUnexpectedError( 'Unexpected service error while fetching products list', error, ); } } /** * @function get * @description Fetches detailed information for a specific product by ID. * @memberof SwellProductsService * @param {string} productId - The ID of the product to retrieve * @param {ProductGetOptions} [options={}] - Optional retrieval options * @returns {Promise<SwellProduct>} A promise that resolves to the product details * @throws {McpError} Throws an `McpError` if the product is not found or API call fails * @example * // Get basic product details * const product = await get('product-id-123'); * // Get product with expanded relationships * const productWithVariants = await get('product-id-123', { expand: ['variants', 'categories'] }); */ async function get( productId: string, options: ProductGetOptions = {}, ): Promise<SwellProduct> { const methodLogger = serviceLogger.forMethod('get'); methodLogger.debug( `Fetching product details for ID: ${productId}`, options, ); if (!productId || productId.trim().length === 0) { throw createApiError('Product ID is required', 400); } try { // Ensure client is initialized if (!swellClient.isClientInitialized()) { swellClient.initWithAutoConfig(); } const client = swellClient.getClient(); // Build query parameters const queryParams: Record<string, unknown> = {}; if (options.expand && options.expand.length > 0) { queryParams.expand = options.expand; } // Make the API call const rawData = await client.get<unknown>( `/products/${productId}`, queryParams, ); // Handle null response (product not found) if (!rawData) { throw createApiError(`Product not found: ${productId}`, 404); } // Check if debug mode is enabled const isDebugMode = config.getBoolean('DEBUG', false); if (isDebugMode) { methodLogger.debug( 'Debug mode enabled - returning raw data without validation', ); return rawData as SwellProduct; } // Validate response with Zod schema const validatedData = SwellProductSchema.parse(rawData); methodLogger.debug( `Successfully fetched product: ${validatedData.name}`, ); return validatedData; } catch (error) { methodLogger.error( `Service error fetching product ${productId}`, error, ); // Handle Zod validation errors if (error instanceof z.ZodError) { throw createApiError( `Product response validation failed: ${error.issues .map((e: z.ZodIssue) => `${e.path.join('.')}: ${e.message}`) .join(', ')}`, 500, error, ); } // Rethrow other McpErrors if (error instanceof McpError) { throw error; } // Wrap any other unexpected errors throw createUnexpectedError( `Unexpected service error while fetching product ${productId}`, error, ); } } /** * @function search * @description Searches for products using various criteria. * @memberof SwellProductsService * @param {ProductSearchOptions} options - Search options including query and filters * @returns {Promise<SwellProductsList>} A promise that resolves to the search results * @throws {McpError} Throws an `McpError` if the search fails or response validation fails * @example * // Search for products by name * const results = await search({ query: 'laptop', limit: 20 }); * // Search with additional filters * const filteredResults = await search({ * query: 'shirt', * active: true, * category: 'clothing', * sort: 'price_asc' * }); */ async function search( options: ProductSearchOptions, ): Promise<SwellProductsList> { const methodLogger = serviceLogger.forMethod('search'); methodLogger.debug('Searching products', options); if (!options.query || options.query.trim().length === 0) { throw createApiError('Search query is required', 400); } try { // Use the list function with search parameter const searchOptions: ProductListOptions = { search: options.query, page: options.page, limit: options.limit, active: options.active, category: options.category, tags: options.tags, sort: options.sort, expand: options.expand, }; const results = await list(searchOptions); methodLogger.debug( `Search completed: found ${results.count} products matching "${options.query}"`, ); return results; } catch (error) { methodLogger.error( `Service error searching products with query "${options.query}"`, error, ); // Rethrow McpErrors if (error instanceof McpError) { throw error; } // Wrap any other unexpected errors throw createUnexpectedError( `Unexpected service error while searching products with query "${options.query}"`, error, ); } } /** * @function checkStock * @description Checks stock levels for a product and optionally its variants. * @memberof SwellProductsService * @param {string} productId - The ID of the product to check stock for * @param {StockCheckOptions} [options={}] - Optional stock check options * @returns {Promise<SwellProduct>} A promise that resolves to the product with stock information * @throws {McpError} Throws an `McpError` if the product is not found or API call fails * @example * // Check stock for a product * const stock = await checkStock('product-id-123'); * // Check stock including variants * const fullStock = await checkStock('product-id-123', { include_variants: true }); */ async function checkStock( productId: string, options: StockCheckOptions = {}, ): Promise<SwellProduct> { const methodLogger = serviceLogger.forMethod('checkStock'); methodLogger.debug(`Checking stock for product ID: ${productId}`, options); if (!productId || productId.trim().length === 0) { throw createApiError('Product ID is required', 400); } try { // Build expand options to include inventory-related data const expandOptions: string[] = []; if (options.include_variants) { expandOptions.push('variants'); } expandOptions.push('stock'); // Get product with inventory information const product = await get(productId, { expand: expandOptions }); methodLogger.debug( `Successfully checked inventory for product: ${product.name}`, { stock_level: product.stock_level, stock_status: product.stock_status, variants_count: Array.isArray(product.variants) ? product.variants.length : 0, }, ); return product; } catch (error) { methodLogger.error( `Service error checking inventory for product ${productId}`, error, ); // Rethrow McpErrors if (error instanceof McpError) { throw error; } // Wrap any other unexpected errors throw createUnexpectedError( `Unexpected service error while checking inventory for product ${productId}`, error, ); } } /** * @function update * @description Updates a product with the provided data. * @memberof SwellProductsService * @param {string} productId - The ID of the product to update * @param {ProductUpdateOptions} updateData - The data to update the product with * @returns {Promise<UpdateResult<SwellProduct>>} A promise that resolves to the update result * @throws {McpError} Throws an `McpError` if the product is not found or update fails * @example * // Update product name and price * const result = await update('product-id-123', { name: 'New Name', price: 29.99 }); * // Update product metadata * const metaResult = await update('product-id-123', { * meta_title: 'SEO Title', * meta_description: 'SEO Description' * }); */ async function update( productId: string, updateData: ProductUpdateOptions, ): Promise<UpdateResult<SwellProduct>> { const methodLogger = serviceLogger.forMethod('update'); methodLogger.debug(`Updating product ID: ${productId}`, updateData); if (!productId || productId.trim().length === 0) { throw createApiError('Product ID is required', 400); } if (!updateData || Object.keys(updateData).length === 0) { throw createApiError('Update data is required', 400); } try { // Ensure client is initialized if (!swellClient.isClientInitialized()) { swellClient.initWithAutoConfig(); } const client = swellClient.getClient(); // Check if debug mode is enabled const isDebugMode = config.getBoolean('DEBUG', false); // Get the current product for comparison (unless in debug mode) let originalProduct: SwellProduct | null = null; if (!isDebugMode) { try { originalProduct = await get(productId); } catch (error) { if ( error instanceof McpError && // eslint-disable-next-line @typescript-eslint/no-explicit-any (error as any).status === 404 ) { throw createApiError( `Product not found: ${productId}`, 404, ); } throw error; } } // Validate update data (unless in debug mode) if (!isDebugMode) { ProductUpdateOptionsSchema.parse(updateData); } // Prepare the update payload const updatePayload: Record<string, unknown> = { ...updateData }; // Handle special cases for pricing and inventory if (updateData.price !== undefined) { updatePayload.price = updateData.price; } if (updateData.sale_price !== undefined) { updatePayload.sale_price = updateData.sale_price; } if (updateData.stock_level !== undefined) { updatePayload.stock_level = updateData.stock_level; } if (updateData.stock_status !== undefined) { updatePayload.stock_status = updateData.stock_status; } // Handle metadata updates if (updateData.meta_title !== undefined) { updatePayload.meta_title = updateData.meta_title; } if (updateData.meta_description !== undefined) { updatePayload.meta_description = updateData.meta_description; } // Handle tags and categories if (updateData.tags !== undefined) { updatePayload.tags = updateData.tags; } if (updateData.categories !== undefined) { updatePayload.categories = updateData.categories; } // Handle attributes if (updateData.attributes !== undefined) { updatePayload.attributes = updateData.attributes; } // Make the API call const rawData = await client.put<unknown>( `/products/${productId}`, updatePayload, ); if (isDebugMode) { methodLogger.debug( 'Debug mode enabled - returning raw data without validation', ); return { success: true, data: rawData as SwellProduct, }; } // Validate response with Zod schema const updatedProduct = SwellProductSchema.parse(rawData); // Calculate changes const changes: Array<{ field: string; oldValue: unknown; newValue: unknown; }> = []; if (originalProduct) { Object.keys(updateData).forEach((key) => { const oldValue = (originalProduct as Record<string, unknown>)[ key ]; const newValue = (updatedProduct as Record<string, unknown>)[ key ]; if (oldValue !== newValue) { changes.push({ field: key, oldValue, newValue, }); } }); } methodLogger.debug( `Successfully updated product: ${updatedProduct.name}`, { changes: changes.length }, ); return { success: true, data: updatedProduct, changes, }; } catch (error) { methodLogger.error( `Service error updating product ${productId}`, error, ); // Handle Zod validation errors if (error instanceof z.ZodError) { throw createApiError( `Product update validation failed: ${error.issues .map((e: z.ZodIssue) => `${e.path.join('.')}: ${e.message}`) .join(', ')}`, 400, error, ); } // Rethrow other McpErrors if (error instanceof McpError) { throw error; } // Wrap any other unexpected errors throw createUnexpectedError( `Unexpected service error while updating product ${productId}`, error, ); } } /** * @function updateStock * @description Updates stock-specific fields for a product. * @memberof SwellProductsService * @param {string} productId - The ID of the product to update stock for * @param {StockUpdateOptions} stockData - The stock data to update * @returns {Promise<UpdateResult<SwellProduct>>} A promise that resolves to the update result * @throws {McpError} Throws an `McpError` if the product is not found or update fails * @example * // Update stock level * const result = await updateStock('product-id-123', { stock_level: 50 }); * // Update stock status * const statusResult = await updateStock('product-id-123', { stock_status: 'out_of_stock' }); */ async function updateStock( productId: string, stockData: StockUpdateOptions, ): Promise<UpdateResult<SwellProduct>> { const methodLogger = serviceLogger.forMethod('updateStock'); methodLogger.debug( `Updating stock for product ID: ${productId}`, stockData, ); if (!productId || productId.trim().length === 0) { throw createApiError('Product ID is required', 400); } if (!stockData || Object.keys(stockData).length === 0) { throw createApiError('Inventory data is required', 400); } try { // Check if debug mode is enabled const isDebugMode = config.getBoolean('DEBUG', false); // Validate inventory data (unless in debug mode) if (!isDebugMode) { StockUpdateOptionsSchema.parse(stockData); } // Ensure client is initialized if (!swellClient.isClientInitialized()) { swellClient.initWithAutoConfig(); } const client = swellClient.getClient(); // Prepare payload for stock adjustment endpoint // Swell expects parent_id and quantity (delta). If caller provided an absolute stock_level, // compute the delta by fetching current stock level. const payload: Record<string, unknown> = { parent_id: productId, }; // If quantity provided, use it directly if (typeof stockData.quantity === 'number') { payload.quantity = stockData.quantity; } else { throw createApiError( 'Quantity is required for stock adjustments', 400, ); } // Attach optional adjustment fields if (stockData.reason !== undefined) { payload.reason = stockData.reason; } if (stockData.reason_message !== undefined) { payload.reason_message = stockData.reason_message; } if (stockData.variant_id !== undefined) { payload.variant_id = stockData.variant_id; } if (stockData.order_id !== undefined) { payload.order_id = stockData.order_id; } // If quantity is 0 (no-op), just return current product if (payload.quantity === 0 || payload.quantity === '0') { const prod = await get(productId); return { success: true, data: prod, changes: [] }; } // Make the adjustment call to Swell await client.post('/products:stock', payload); // After adjustment, fetch updated product for response const updatedProduct = await get(productId); // Build change record: stock_level changed by quantity const changes: Array<{ field: string; oldValue: unknown; newValue: unknown; }> = []; // Build a stock_level change record based on the provided quantity if (typeof stockData.quantity === 'number') { const newLevel = updatedProduct.stock_level ?? 0; const oldLevel = newLevel - (stockData.quantity || 0); if (oldLevel !== newLevel) { changes.push({ field: 'stock_level', oldValue: oldLevel, newValue: newLevel, }); } } methodLogger.debug( `Successfully created stock adjustment for product: ${updatedProduct.name}`, { quantity: payload.quantity }, ); return { success: true, data: updatedProduct, changes }; } catch (error) { methodLogger.error( `Service error updating inventory for product ${productId}`, error, ); // Rethrow McpErrors if (error instanceof McpError) { throw error; } // Wrap any other unexpected errors throw createUnexpectedError( `Unexpected service error while updating inventory for product ${productId}`, error, ); } } /** * @function updatePricing * @description Updates pricing-specific fields for a product. * @memberof SwellProductsService * @param {string} productId - The ID of the product to update pricing for * @param {PricingUpdateOptions} pricingData - The pricing data to update * @returns {Promise<UpdateResult<SwellProduct>>} A promise that resolves to the update result * @throws {McpError} Throws an `McpError` if the product is not found or update fails * @example * // Update regular price * const result = await updatePricing('product-id-123', { price: 29.99 }); * // Update both regular and sale price * const priceResult = await updatePricing('product-id-123', { * price: 39.99, * sale_price: 29.99 * }); */ async function updatePricing( productId: string, pricingData: PricingUpdateOptions, ): Promise<UpdateResult<SwellProduct>> { const methodLogger = serviceLogger.forMethod('updatePricing'); methodLogger.debug( `Updating pricing for product ID: ${productId}`, pricingData, ); if (!productId || productId.trim().length === 0) { throw createApiError('Product ID is required', 400); } if (!pricingData || Object.keys(pricingData).length === 0) { throw createApiError('Pricing data is required', 400); } try { // Check if debug mode is enabled const isDebugMode = config.getBoolean('DEBUG', false); // Validate pricing data (unless in debug mode) if (!isDebugMode) { PricingUpdateOptionsSchema.parse(pricingData); } // Convert pricing data to product update format const updateData: ProductUpdateOptions = {}; if (pricingData.price !== undefined) { updateData.price = pricingData.price; } if (pricingData.sale_price !== undefined) { updateData.sale_price = pricingData.sale_price; } // Use the main update method const result = await update(productId, updateData); methodLogger.debug( `Successfully updated pricing for product: ${result.data?.name}`, ); return result; } catch (error) { methodLogger.error( `Service error updating pricing for product ${productId}`, error, ); // Rethrow McpErrors if (error instanceof McpError) { throw error; } // Wrap any other unexpected errors throw createUnexpectedError( `Unexpected service error while updating pricing for product ${productId}`, error, ); } } /** * @function updateStatus * @description Updates the active status of a product (activation/deactivation). * @memberof SwellProductsService * @param {string} productId - The ID of the product to update status for * @param {boolean} active - Whether the product should be active or inactive * @returns {Promise<UpdateResult<SwellProduct>>} A promise that resolves to the update result * @throws {McpError} Throws an `McpError` if the product is not found or update fails * @example * // Activate a product * const result = await updateStatus('product-id-123', true); * // Deactivate a product * const deactivateResult = await updateStatus('product-id-123', false); */ async function updateStatus( productId: string, active: boolean, ): Promise<UpdateResult<SwellProduct>> { const methodLogger = serviceLogger.forMethod('updateStatus'); methodLogger.debug( `Updating status for product ID: ${productId} to ${active ? 'active' : 'inactive'}`, ); if (!productId || productId.trim().length === 0) { throw createApiError('Product ID is required', 400); } if (typeof active !== 'boolean') { throw createApiError('Active status must be a boolean value', 400); } try { // Use the main update method const result = await update(productId, { active }); methodLogger.debug( `Successfully updated status for product: ${result.data?.name} to ${active ? 'active' : 'inactive'}`, ); return result; } catch (error) { methodLogger.error( `Service error updating status for product ${productId}`, error, ); // Rethrow McpErrors if (error instanceof McpError) { throw error; } // Wrap any other unexpected errors throw createUnexpectedError( `Unexpected service error while updating status for product ${productId}`, error, ); } } export default { list, get, search, checkStock, update, updateStock, updatePricing, updateStatus, };

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/devkindhq/swell-mcp'

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