Skip to main content
Glama
boldcommerce

Magento 2 MCP Server

by boldcommerce

get_product_sales

Retrieve product sales quantity statistics for a specified date range, with optional filters for order status and country.

Instructions

Get statistics about the quantity of products sold in a given date range

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
date_rangeYesDate range expression (e.g., 'today', 'yesterday', 'last week', 'this month', 'YTD', or a specific date range like '2023-01-01 to 2023-01-31')
statusNoFilter by order status (e.g., 'processing', 'complete', 'pending')
countryNoFilter by country code (e.g., 'US', 'NL', 'GB') or country name (e.g., 'United States', 'The Netherlands', 'United Kingdom')

Implementation Reference

  • Inline async handler function implementing the core logic: parses date range, builds API query, fetches all orders via pagination helper, filters by status/country, processes order items to compute totals (orders, items, quantity, revenue), tracks top products by SKU, formats and returns JSON response.
    async ({ date_range, status, country }) => {
      try {
        // Parse the date range expression
        const dateRange = parseDateExpression(date_range);
        
        // Build the search criteria for the date range
        let searchCriteria = buildDateRangeFilter('created_at', dateRange.startDate, dateRange.endDate);
        
        // Add status filter if provided
        if (status) {
          searchCriteria += `&searchCriteria[filter_groups][2][filters][0][field]=status&` +
                            `searchCriteria[filter_groups][2][filters][0][value]=${encodeURIComponent(status)}&` +
                            `searchCriteria[filter_groups][2][filters][0][condition_type]=eq`;
        }
        
        // Fetch all orders using the helper function
        const allOrders = await fetchAllPages('/orders', searchCriteria);
        
        // Filter orders by country if provided
        let filteredOrders = allOrders;
        if (country) {
          // Normalize country input
          const normalizedCountry = normalizeCountry(country);
          
          // Filter orders by country
          filteredOrders = filteredOrders.filter(order => {
            // Check billing address country
            const billingCountry = order.billing_address?.country_id;
            
            // Check shipping address country
            const shippingCountry = order.extension_attributes?.shipping_assignments?.[0]?.shipping?.address?.country_id;
            
            // Match if either billing or shipping country matches
            return normalizedCountry.includes(billingCountry) || normalizedCountry.includes(shippingCountry);
          });
        }
        
        // Calculate statistics
        let totalOrders = filteredOrders.length;
        let totalOrderItems = 0;
        let totalProductQuantity = 0;
        let totalRevenue = 0;
        let productCounts = {};
        
        // Process each order
        filteredOrders.forEach(order => {
          // Add to total revenue
          totalRevenue += parseFloat(order.grand_total || 0);
          
          // Process order items
          if (order.items && Array.isArray(order.items)) {
            // Count total order items (order lines)
            totalOrderItems += order.items.length;
            
            // Process each item
            order.items.forEach(item => {
              // Add to total product quantity
              const quantity = parseFloat(item.qty_ordered || 0);
              totalProductQuantity += quantity;
              
              // Track product counts by SKU
              const sku = item.sku;
              if (sku) {
                if (!productCounts[sku]) {
                  productCounts[sku] = {
                    name: item.name,
                    quantity: 0,
                    revenue: 0
                  };
                }
                productCounts[sku].quantity += quantity;
                productCounts[sku].revenue += parseFloat(item.row_total || 0);
              }
            });
          }
        });
        
        // Convert product counts to array and sort by quantity
        const topProducts = Object.entries(productCounts)
          .map(([sku, data]) => ({
            sku,
            name: data.name,
            quantity: data.quantity,
            revenue: data.revenue
          }))
          .sort((a, b) => b.quantity - a.quantity)
          .slice(0, 10); // Top 10 products
        
        // Format the response
        const result = {
          query: {
            date_range: dateRange.description,
            status: status || 'All',
            country: country || 'All',
            period: {
              start_date: format(dateRange.startDate, 'yyyy-MM-dd'),
              end_date: format(dateRange.endDate, 'yyyy-MM-dd')
            }
          },
          result: {
            total_orders: totalOrders,
            total_order_items: totalOrderItems,
            total_product_quantity: totalProductQuantity,
            average_products_per_order: totalOrders > 0 ? parseFloat((totalProductQuantity / totalOrders).toFixed(2)) : 0,
            total_revenue: parseFloat(totalRevenue.toFixed(2)),
            average_revenue_per_product: totalProductQuantity > 0 ? parseFloat((totalRevenue / totalProductQuantity).toFixed(2)) : 0,
            top_products: topProducts
          }
        };
        
        return {
          content: [
            {
              type: "text",
              text: JSON.stringify(result, null, 2)
            }
          ]
        };
      } catch (error) {
        return {
          content: [
            {
              type: "text",
              text: `Error fetching product sales: ${error.message}`
            }
          ],
          isError: true
        };
      }
    }
  • Zod-based input schema defining parameters: date_range (required string for date expressions), status (optional string for order status filter), country (optional string for country filter).
    {
      date_range: z.string().describe("Date range expression (e.g., 'today', 'yesterday', 'last week', 'this month', 'YTD', or a specific date range like '2023-01-01 to 2023-01-31')"),
      status: z.string().optional().describe("Filter by order status (e.g., 'processing', 'complete', 'pending')"),
      country: z.string().optional().describe("Filter by country code (e.g., 'US', 'NL', 'GB') or country name (e.g., 'United States', 'The Netherlands', 'United Kingdom')")
    },
  • MCP server tool registration call: server.tool('get_product_sales', description, schema, handler). Includes comment '// Tool: Get product sales'.
    server.tool(
      "get_product_sales",
      "Get statistics about the quantity of products sold in a given date range",
      {
        date_range: z.string().describe("Date range expression (e.g., 'today', 'yesterday', 'last week', 'this month', 'YTD', or a specific date range like '2023-01-01 to 2023-01-31')"),
        status: z.string().optional().describe("Filter by order status (e.g., 'processing', 'complete', 'pending')"),
        country: z.string().optional().describe("Filter by country code (e.g., 'US', 'NL', 'GB') or country name (e.g., 'United States', 'The Netherlands', 'United Kingdom')")
      },
      async ({ date_range, status, country }) => {
        try {
          // Parse the date range expression
          const dateRange = parseDateExpression(date_range);
          
          // Build the search criteria for the date range
          let searchCriteria = buildDateRangeFilter('created_at', dateRange.startDate, dateRange.endDate);
          
          // Add status filter if provided
          if (status) {
            searchCriteria += `&searchCriteria[filter_groups][2][filters][0][field]=status&` +
                              `searchCriteria[filter_groups][2][filters][0][value]=${encodeURIComponent(status)}&` +
                              `searchCriteria[filter_groups][2][filters][0][condition_type]=eq`;
          }
          
          // Fetch all orders using the helper function
          const allOrders = await fetchAllPages('/orders', searchCriteria);
          
          // Filter orders by country if provided
          let filteredOrders = allOrders;
          if (country) {
            // Normalize country input
            const normalizedCountry = normalizeCountry(country);
            
            // Filter orders by country
            filteredOrders = filteredOrders.filter(order => {
              // Check billing address country
              const billingCountry = order.billing_address?.country_id;
              
              // Check shipping address country
              const shippingCountry = order.extension_attributes?.shipping_assignments?.[0]?.shipping?.address?.country_id;
              
              // Match if either billing or shipping country matches
              return normalizedCountry.includes(billingCountry) || normalizedCountry.includes(shippingCountry);
            });
          }
          
          // Calculate statistics
          let totalOrders = filteredOrders.length;
          let totalOrderItems = 0;
          let totalProductQuantity = 0;
          let totalRevenue = 0;
          let productCounts = {};
          
          // Process each order
          filteredOrders.forEach(order => {
            // Add to total revenue
            totalRevenue += parseFloat(order.grand_total || 0);
            
            // Process order items
            if (order.items && Array.isArray(order.items)) {
              // Count total order items (order lines)
              totalOrderItems += order.items.length;
              
              // Process each item
              order.items.forEach(item => {
                // Add to total product quantity
                const quantity = parseFloat(item.qty_ordered || 0);
                totalProductQuantity += quantity;
                
                // Track product counts by SKU
                const sku = item.sku;
                if (sku) {
                  if (!productCounts[sku]) {
                    productCounts[sku] = {
                      name: item.name,
                      quantity: 0,
                      revenue: 0
                    };
                  }
                  productCounts[sku].quantity += quantity;
                  productCounts[sku].revenue += parseFloat(item.row_total || 0);
                }
              });
            }
          });
          
          // Convert product counts to array and sort by quantity
          const topProducts = Object.entries(productCounts)
            .map(([sku, data]) => ({
              sku,
              name: data.name,
              quantity: data.quantity,
              revenue: data.revenue
            }))
            .sort((a, b) => b.quantity - a.quantity)
            .slice(0, 10); // Top 10 products
          
          // Format the response
          const result = {
            query: {
              date_range: dateRange.description,
              status: status || 'All',
              country: country || 'All',
              period: {
                start_date: format(dateRange.startDate, 'yyyy-MM-dd'),
                end_date: format(dateRange.endDate, 'yyyy-MM-dd')
              }
            },
            result: {
              total_orders: totalOrders,
              total_order_items: totalOrderItems,
              total_product_quantity: totalProductQuantity,
              average_products_per_order: totalOrders > 0 ? parseFloat((totalProductQuantity / totalOrders).toFixed(2)) : 0,
              total_revenue: parseFloat(totalRevenue.toFixed(2)),
              average_revenue_per_product: totalProductQuantity > 0 ? parseFloat((totalRevenue / totalProductQuantity).toFixed(2)) : 0,
              top_products: topProducts
            }
          };
          
          return {
            content: [
              {
                type: "text",
                text: JSON.stringify(result, null, 2)
              }
            ]
          };
        } catch (error) {
          return {
            content: [
              {
                type: "text",
                text: `Error fetching product sales: ${error.message}`
              }
            ],
            isError: true
          };
        }
      }
    );
  • parseDateExpression(dateExpression): Parses relative date strings (e.g., 'today', 'last month', '2023-01-01 to 2023-01-31') into {startDate, endDate, description} using date-fns.
    function parseDateExpression(dateExpression) {
      const now = new Date();
      const currentYear = now.getFullYear();
      const currentMonth = now.getMonth();
      const currentDay = now.getDate();
      
      // Normalize the date expression
      const normalizedExpression = dateExpression.toLowerCase().trim();
      
      // Handle relative date expressions
      switch (normalizedExpression) {
        case 'today':
          return {
            startDate: startOfDay(now),
            endDate: endOfDay(now),
            description: 'Today'
          };
        case 'yesterday':
          const yesterday = subDays(now, 1);
          return {
            startDate: startOfDay(yesterday),
            endDate: endOfDay(yesterday),
            description: 'Yesterday'
          };
        case 'this week':
          return {
            startDate: startOfWeek(now, { weekStartsOn: 1 }), // Week starts on Monday
            endDate: endOfDay(now),
            description: 'This week'
          };
        case 'last week':
          const lastWeekStart = subDays(startOfWeek(now, { weekStartsOn: 1 }), 7);
          const lastWeekEnd = subDays(endOfWeek(now, { weekStartsOn: 1 }), 7);
          return {
            startDate: lastWeekStart,
            endDate: lastWeekEnd,
            description: 'Last week'
          };
        case 'this month':
          return {
            startDate: startOfMonth(now),
            endDate: endOfDay(now),
            description: 'This month'
          };
        case 'last month':
          const lastMonth = new Date(currentYear, currentMonth - 1, 1);
          return {
            startDate: startOfMonth(lastMonth),
            endDate: endOfMonth(lastMonth),
            description: 'Last month'
          };
        case 'ytd':
        case 'this ytd':
        case 'this year to date':
        case 'year to date':
          return {
            startDate: startOfYear(now),
            endDate: endOfDay(now),
            description: 'Year to date'
          };
        case 'last year':
          const lastYear = new Date(currentYear - 1, 0, 1);
          return {
            startDate: startOfYear(lastYear),
            endDate: endOfYear(lastYear),
            description: 'Last year'
          };
        default:
          // Try to parse as ISO date or other common formats
          try {
            // Check if it's a single date (not a range)
            const parsedDate = parseISO(normalizedExpression);
            if (isValid(parsedDate)) {
              return {
                startDate: startOfDay(parsedDate),
                endDate: endOfDay(parsedDate),
                description: format(parsedDate, 'yyyy-MM-dd')
              };
            }
            
            // Check if it's a date range in format "YYYY-MM-DD to YYYY-MM-DD"
            const rangeParts = normalizedExpression.split(' to ');
            if (rangeParts.length === 2) {
              const startDate = parseISO(rangeParts[0]);
              const endDate = parseISO(rangeParts[1]);
              
              if (isValid(startDate) && isValid(endDate)) {
                return {
                  startDate: startOfDay(startDate),
                  endDate: endOfDay(endDate),
                  description: `${format(startDate, 'yyyy-MM-dd')} to ${format(endDate, 'yyyy-MM-dd')}`
                };
              }
            }
            
            // If we can't parse it, throw an error
            throw new Error(`Unable to parse date expression: ${dateExpression}`);
          } catch (error) {
            throw new Error(`Invalid date expression: ${dateExpression}. ${error.message}`);
          }
      }
    }
  • normalizeCountry(country): Maps common country names/variations to ISO codes (e.g., 'netherlands' -> 'NL') for use in order filtering by billing/shipping address.
    function normalizeCountry(country) {
      // Normalize the country input (handle both country codes and names)
      const countryInput = country.trim().toLowerCase();
      
      // Map of common country names to ISO country codes
      const countryMap = {
        // Common variations for The Netherlands
        'netherlands': 'NL',
        'the netherlands': 'NL',
        'holland': 'NL',
        'nl': 'NL',
        
        // Common variations for United States
        'united states': 'US',
        'usa': 'US',
        'us': 'US',
        'america': 'US',
        
        // Common variations for United Kingdom
        'united kingdom': 'GB',
        'uk': 'GB',
        'great britain': 'GB',
        'gb': 'GB',
        'england': 'GB',
        
        // Add more countries as needed
        'canada': 'CA',
        'ca': 'CA',
        
        'australia': 'AU',
        'au': 'AU',
        
        'germany': 'DE',
        'de': 'DE',
        
        'france': 'FR',
        'fr': 'FR',
        
        'italy': 'IT',
        'it': 'IT',
        
        'spain': 'ES',
        'es': 'ES',
        
        'belgium': 'BE',
        'be': 'BE',
        
        'sweden': 'SE',
        'se': 'SE',
        
        'norway': 'NO',
        'no': 'NO',
        
        'denmark': 'DK',
        'dk': 'DK',
        
        'finland': 'FI',
        'fi': 'FI',
        
        'ireland': 'IE',
        'ie': 'IE',
        
        'switzerland': 'CH',
        'ch': 'CH',
        
        'austria': 'AT',
        'at': 'AT',
        
        'portugal': 'PT',
        'pt': 'PT',
        
        'greece': 'GR',
        'gr': 'GR',
        
        'poland': 'PL',
        'pl': 'PL',
        
        'japan': 'JP',
        'jp': 'JP',
        
        'china': 'CN',
        'cn': 'CN',
        
        'india': 'IN',
        'in': 'IN',
        
        'brazil': 'BR',
        'br': 'BR',
        
        'mexico': 'MX',
        'mx': 'MX',
        
        'south africa': 'ZA',
        'za': 'ZA'
      };
      
      // Check if the input is in our map
      if (countryMap[countryInput]) {
        return [countryMap[countryInput]];
      }
      
      // If it's not in our map, assume it's a country code or name and return as is
      // For a more robust solution, we would validate against a complete list of country codes
      return [countryInput.toUpperCase()];
    }
Behavior2/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

No annotations are provided, so the description carries full burden. It mentions getting statistics but doesn't disclose behavioral traits like whether this is a read-only operation, if it requires authentication, rate limits, pagination, or what format the statistics are returned in. For a tool with no annotation coverage, this is a significant gap.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is a single, efficient sentence that directly states the tool's purpose without unnecessary words. It's appropriately sized and front-loaded with the core functionality.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness3/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Given the tool has no annotations and no output schema, the description is incomplete. It doesn't explain what the statistics output looks like (e.g., aggregated totals, time-series data) or behavioral aspects. However, the purpose is clear and parameters are well-documented in the schema, making it minimally adequate.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

The schema description coverage is 100%, with clear descriptions for all three parameters (date_range, status, country). The description adds no additional parameter semantics beyond what's in the schema, so it meets the baseline of 3 where the schema does the heavy lifting.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose4/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description clearly states the tool's purpose: 'Get statistics about the quantity of products sold in a given date range.' It specifies the verb ('Get statistics') and resource ('quantity of products sold'), but doesn't explicitly distinguish it from sibling tools like 'get_revenue' or 'get_order_count' which might provide different metrics.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines2/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description provides no guidance on when to use this tool versus alternatives. It doesn't mention sibling tools like 'get_revenue' (which might focus on monetary metrics) or 'get_order_count' (which might count orders rather than product quantities), leaving the agent to infer usage context.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

Install Server

Other Tools

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/boldcommerce/magento2-mcp'

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