This is page 1 of 2. Use http://codebase.md/boldcommerce/magento2-mcp?lines=true&page={x} to view the full context. # Directory Structure ``` ├── .clinerules ├── .gitignore ├── LICENSE ├── mcp-instructions │ ├── modelcontextprotocol.md │ └── typescript-sdk.md ├── mcp-server.js ├── memory-bank │ ├── activeContext.md │ ├── productContext.md │ ├── progress.md │ ├── projectbrief.md │ ├── systemPatterns.md │ └── techContext.md ├── package-lock.json ├── package.json ├── README.md └── test-mcp-server.js ``` # Files -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- ``` 1 | # Node.js 2 | node_modules/ 3 | npm-debug.log 4 | yarn-debug.log 5 | yarn-error.log 6 | .npm/ 7 | .yarn/ 8 | *.tgz 9 | .pnp.* 10 | .yarn-integrity 11 | 12 | # Environment variables 13 | .env 14 | .env.local 15 | .env.development.local 16 | .env.test.local 17 | .env.production.local 18 | 19 | # Logs 20 | logs/ 21 | *.log 22 | npm-debug.log* 23 | yarn-debug.log* 24 | yarn-error.log* 25 | lerna-debug.log* 26 | 27 | # Build output 28 | dist/ 29 | build/ 30 | out/ 31 | .output/ 32 | 33 | # Docker 34 | .docker/ 35 | docker-compose.override.yml 36 | 37 | # IDE - VSCode 38 | .vscode/* 39 | !.vscode/settings.json 40 | !.vscode/tasks.json 41 | !.vscode/launch.json 42 | !.vscode/extensions.json 43 | *.code-workspace 44 | 45 | # IDE - JetBrains (WebStorm, IntelliJ, etc) 46 | .idea/ 47 | *.iml 48 | *.iws 49 | *.ipr 50 | .idea_modules/ 51 | 52 | # IDE - Other 53 | .project 54 | .classpath 55 | .c9/ 56 | *.launch 57 | .settings/ 58 | *.sublime-workspace 59 | .netbeans/ 60 | 61 | # OS specific 62 | .DS_Store 63 | .DS_Store? 64 | ._* 65 | .Spotlight-V100 66 | .Trashes 67 | ehthumbs.db 68 | Thumbs.db 69 | 70 | # Testing 71 | coverage/ 72 | .nyc_output/ 73 | 74 | # Temporary files 75 | tmp/ 76 | temp/ 77 | .tmp/ 78 | .temp/ 79 | *.tmp 80 | *.temp 81 | 82 | # MCP specific 83 | claude_desktop_config.json 84 | ``` -------------------------------------------------------------------------------- /.clinerules: -------------------------------------------------------------------------------- ``` 1 | # Cline Rules for Magento 2 MCP Server 2 | 3 | ## Project Structure 4 | - The main server implementation is in `mcp-server.js` 5 | - The test client is in `test-mcp-server.js` 6 | - Memory Bank files are stored in the `memory-bank` directory 7 | 8 | ## Coding Patterns 9 | - Use camelCase for variable and function names 10 | - Use PascalCase for class names 11 | - Use UPPER_SNAKE_CASE for constants 12 | - Use async/await for asynchronous operations 13 | - Use try/catch blocks for error handling 14 | - Use the zod library for input validation 15 | - Use the axios library for HTTP requests 16 | - Use the dotenv library for environment variables 17 | 18 | ## MCP Tool Implementation Pattern 19 | When implementing a new MCP tool, follow this pattern: 20 | ```javascript 21 | server.tool( 22 | "tool_name", 23 | "Tool description", 24 | { 25 | param1: z.string().describe("Parameter 1 description"), 26 | param2: z.number().optional().describe("Parameter 2 description") 27 | }, 28 | async ({ param1, param2 }) => { 29 | try { 30 | // Implementation 31 | return { 32 | content: [ 33 | { 34 | type: "text", 35 | text: JSON.stringify(result, null, 2) 36 | } 37 | ] 38 | }; 39 | } catch (error) { 40 | return { 41 | content: [ 42 | { 43 | type: "text", 44 | text: `Error: ${error.message}` 45 | } 46 | ], 47 | isError: true 48 | }; 49 | } 50 | } 51 | ); 52 | ``` 53 | 54 | ## Date Handling 55 | - Use ISO 8601 format (YYYY-MM-DD) for date representation 56 | - Implement a date parser that can handle relative date expressions 57 | - Use the date-fns library for date manipulation 58 | 59 | ## Error Handling 60 | - Always wrap API calls in try/catch blocks 61 | - Return detailed error messages to the client 62 | - Log errors to the console for debugging 63 | - Include the original error message in the response 64 | 65 | ## Response Formatting 66 | - Use JSON.stringify with null, 2 for pretty-printing JSON responses 67 | - Include metadata about the query in the response 68 | - Format numbers with appropriate precision 69 | - Format dates in a human-readable format 70 | 71 | ## Testing 72 | - Use the test client to verify tool functionality 73 | - Test with various input parameters 74 | - Test error handling 75 | - Test with edge cases 76 | 77 | ## Documentation 78 | - Document all tools with clear descriptions 79 | - Document all parameters with descriptions 80 | - Document the expected response format 81 | - Document any error conditions 82 | ``` -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- ```markdown 1 | # Magento 2 MCP Server 2 | 3 | This is a Model Context Protocol (MCP) server that connects to a Magento 2 REST API, allowing Claude and other MCP clients to query product information from a Magento store. 4 | 5 | ## Features 6 | 7 | ### Product Features 8 | - Query product information by SKU or ID 9 | - Search for products using various criteria 10 | - Get product categories 11 | - Get related products 12 | - Get product stock information 13 | - Get product attributes 14 | - Update product attributes by specifying attribute code and value 15 | - Advanced product search with filtering and sorting 16 | 17 | ### Customer Features 18 | - Get all ordered products for a customer by email address 19 | 20 | ### Order and Revenue Features 21 | - Get order count for specific date ranges 22 | - Get revenue for specific date ranges 23 | - Get revenue filtered by country for specific date ranges 24 | - Get product sales statistics including quantity sold and top-selling products 25 | - Support for relative date expressions like "today", "yesterday", "last week", "this month", "YTD" 26 | - Support for country filtering using both country codes and country names 27 | 28 | ## Prerequisites 29 | 30 | - Node.js (v14 or higher) 31 | - A Magento 2 instance with REST API access 32 | - API token for the Magento 2 instance 33 | 34 | ## Installation 35 | 36 | 1. Clone this repository 37 | 2. Install dependencies: 38 | 39 | ```bash 40 | npm install 41 | ``` 42 | 43 | ## Usage 44 | 45 | ### Running the server directly 46 | 47 | ```bash 48 | node mcp-server.js 49 | ``` 50 | 51 | ### Testing with the test client 52 | 53 | ```bash 54 | node test-mcp-server.js 55 | ``` 56 | 57 | ### Using with Claude Desktop 58 | 59 | 1. Check your path node with `which node` 60 | 2. Go to the Developer settings and click "Edit config". This will open a JSON file. 61 | 3. Add the following snippet within the `mcpServers`: 62 | 63 | ``` 64 | "magento2": { 65 | "command": "/path/to/your/node", 66 | "args": ["/path/to/mcp-server.js"], 67 | "env": { 68 | "MAGENTO_BASE_URL": "https://YOUR_DOMAIN/rest/V1", 69 | "MAGENTO_API_TOKEN": "your-api-token" 70 | } 71 | } 72 | ``` 73 | 74 | 3. Replace `/path/to/your/node` with the path you checked in step 1 75 | 4. Replace `/path/to/mcp-server.js` with the path where you cloned this repo 76 | 5. You can get an API token from System > Integrations in the Magento admin 77 | 6. Restart Claude Desktop. 78 | 7. You should now be able to ask Claude questions about products in your Magento store. 79 | 80 | ## Available Tools 81 | 82 | The server exposes the following tools: 83 | 84 | ### Product Tools 85 | - `get_product_by_sku`: Get detailed information about a product by its SKU 86 | - `search_products`: Search for products using Magento search criteria 87 | - `get_product_categories`: Get categories for a specific product by SKU 88 | - `get_related_products`: Get products related to a specific product by SKU 89 | - `get_product_stock`: Get stock information for a product by SKU 90 | - `get_product_attributes`: Get all attributes for a product by SKU 91 | - `get_product_by_id`: Get detailed information about a product by its ID 92 | - `advanced_product_search`: Search for products with advanced filtering options 93 | - `update_product_attribute`: Update a specific attribute of a product by SKU 94 | 95 | ### Customer Tools 96 | - `get_customer_ordered_products_by_email`: Get all ordered products for a customer by email address 97 | 98 | ### Order and Revenue Tools 99 | - `get_order_count`: Get the number of orders for a given date range 100 | - `get_revenue`: Get the total revenue for a given date range 101 | - `get_revenue_by_country`: Get revenue filtered by country for a given date range 102 | - `get_product_sales`: Get statistics about the quantity of products sold in a given date range 103 | 104 | ## Example Queries for Claude 105 | 106 | Once the MCP server is connected to Claude Desktop, you can ask questions like: 107 | 108 | ### Product Queries 109 | - "What products do you have that are shirts?" 110 | - "Tell me about product with SKU SKU-xxx" 111 | - "What categories does product SKU-xxx belong to?" 112 | - "Are there any related products to SKU-SKU-xxx?" 113 | - "What's the stock status of product SKU-xxx?" 114 | - "Show me all products sorted by price" 115 | - "Update the price of product SKU-xxx to $49.99" 116 | - "Change the description of product ABC-123 to describe it as water-resistant" 117 | - "Set the status of product XYZ-456 to 'enabled'" 118 | 119 | ### Customer Queries 120 | - "What products has customer [email protected] ordered?" 121 | - "Show me the order history and products for customer with email [email protected]" 122 | 123 | ### Order and Revenue Queries 124 | - "How many orders do we have today?" 125 | - "What's our order count for last week?" 126 | - "How much revenue did we generate yesterday?" 127 | - "What was our total revenue last month?" 128 | - "How much revenue did we make in The Netherlands this year to date?" 129 | - "What's our revenue in Germany for the last week?" 130 | - "Compare our revenue between the US and Canada for this month" 131 | - "What's our average order value for completed orders this month?" 132 | - "How many products did we sell last month?" 133 | - "What are our top-selling products this year?" 134 | - "What's the average number of products per order?" 135 | - "How many units of product XYZ-123 did we sell in Germany last quarter?" 136 | - "Which products generated the most revenue in the US this month?" 137 | 138 | 139 | ## Development 140 | 141 | ### SSL Certificate Verification 142 | 143 | For development purposes, the server is configured to bypass SSL certificate verification. In a production environment, you should use proper SSL certificates and remove the `httpsAgent` configuration from the `callMagentoApi` function. 144 | 145 | ### Adding New Tools 146 | 147 | To add new tools, follow the pattern in the existing code. Each tool is defined with: 148 | 149 | 1. A unique name 150 | 2. A description 151 | 3. Input parameters with validation using Zod 152 | 4. An async handler function that processes the request and returns a response 153 | 154 | ## License 155 | 156 | ISC 157 | ``` -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- ```json 1 | { 2 | "name": "mcp-magento2", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "claude-magento-client.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "keywords": [], 10 | "author": "", 11 | "license": "ISC", 12 | "dependencies": { 13 | "@anthropic-ai/sdk": "^0.39.0", 14 | "@modelcontextprotocol/sdk": "^1.6.1", 15 | "axios": "^1.8.1", 16 | "body-parser": "^1.20.3", 17 | "date-fns": "^3.6.0", 18 | "dotenv": "^16.4.7", 19 | "express": "^4.21.2", 20 | "zod": "^3.24.2" 21 | } 22 | } 23 | ``` -------------------------------------------------------------------------------- /memory-bank/projectbrief.md: -------------------------------------------------------------------------------- ```markdown 1 | # Project Brief: Magento 2 MCP Server 2 | 3 | ## Overview 4 | This project implements a Model Context Protocol (MCP) server that provides tools for interacting with a Magento 2 e-commerce platform. The server exposes various capabilities through tools that can be used to query and manipulate Magento 2 data. 5 | 6 | ## Core Requirements 7 | 1. Provide tools to query product information from Magento 2 8 | 2. Provide tools to query customer information from Magento 2 9 | 3. Provide tools to query order information from Magento 2 10 | 4. Provide tools to query revenue and sales metrics from Magento 2 11 | 5. Support filtering by date ranges, including relative dates like "today", "last week", "YTD" 12 | 6. Support filtering by geographic regions like countries 13 | 14 | ## Goals 15 | - Enable natural language queries about Magento 2 store data 16 | - Provide accurate and timely information about sales, orders, and revenue 17 | - Support business intelligence and reporting needs 18 | - Make e-commerce data easily accessible through conversational interfaces 19 | 20 | ## Success Criteria 21 | - Successfully retrieve accurate order counts for specified date ranges 22 | - Successfully retrieve accurate revenue figures for specified date ranges 23 | - Successfully filter data by geographic regions 24 | - Support common date range expressions like "today", "yesterday", "last week", "this month", "YTD" 25 | - Provide clear and concise responses to queries 26 | 27 | ## Constraints 28 | - Requires valid Magento 2 API credentials 29 | - Depends on the Magento 2 API being available and responsive 30 | - Limited to the data and capabilities exposed by the Magento 2 API 31 | ``` -------------------------------------------------------------------------------- /memory-bank/productContext.md: -------------------------------------------------------------------------------- ```markdown 1 | # Product Context: Magento 2 MCP Server 2 | 3 | ## Why This Project Exists 4 | The Magento 2 MCP Server exists to bridge the gap between natural language interfaces (like Claude) and the structured data in a Magento 2 e-commerce platform. It enables users to query business-critical information using conversational language rather than having to learn complex query languages or navigate through multiple admin screens. 5 | 6 | ## Problems It Solves 7 | 1. **Accessibility of Data**: E-commerce data is often locked behind complex admin interfaces or requires technical knowledge to query. This server makes that data accessible through natural language. 8 | 9 | 2. **Time Efficiency**: Instead of navigating through multiple screens or running reports, users can simply ask questions like "how many orders do we have today" or "what is our revenue in The Netherlands this YTD". 10 | 11 | 3. **Decision Support**: By making it easier to access sales and revenue data, the server supports better and faster business decision-making. 12 | 13 | 4. **Integration with AI Assistants**: The MCP server enables AI assistants like Claude to directly interact with Magento 2 data, expanding their capabilities in the e-commerce domain. 14 | 15 | ## How It Should Work 16 | 1. The user asks a question about Magento 2 data in natural language. 17 | 2. The AI assistant (Claude) interprets the question and calls the appropriate MCP tool with the right parameters. 18 | 3. The MCP server translates these parameters into Magento 2 API calls. 19 | 4. The server processes the response from Magento 2 and formats it in a way that's easy for the AI assistant to understand. 20 | 5. The AI assistant presents the information to the user in a natural, conversational way. 21 | 22 | ## User Experience Goals 23 | 1. **Simplicity**: Users should be able to get the information they need without understanding the underlying technical details. 24 | 25 | 2. **Accuracy**: The data provided should be accurate and consistent with what's available in the Magento 2 admin interface. 26 | 27 | 3. **Contextual Understanding**: The system should understand relative date ranges like "today", "last week", or "YTD" without requiring explicit date formatting. 28 | 29 | 4. **Geographical Filtering**: Users should be able to filter data by geographical regions like countries or regions. 30 | 31 | 5. **Comprehensive Coverage**: The system should cover all key e-commerce metrics, including orders, revenue, products, and customers. 32 | 33 | 6. **Responsiveness**: Queries should be processed quickly, providing near real-time access to e-commerce data. 34 | ``` -------------------------------------------------------------------------------- /memory-bank/activeContext.md: -------------------------------------------------------------------------------- ```markdown 1 | # Active Context: Magento 2 MCP Server 2 | 3 | ## Current Work Focus 4 | The current focus is on enhancing and refining the MCP server tools for fetching data about revenue and orders. This includes: 5 | 6 | 1. Improving error handling for the new tools 7 | 2. Adding comprehensive documentation for the new tools 8 | 3. Creating additional test cases for the new functionality 9 | 4. Optimizing performance for large result sets 10 | 11 | ## Recent Changes 12 | - Implementation of the `get_order_count` tool to retrieve the number of orders for a given date range 13 | - Implementation of the `get_revenue` tool to retrieve the total revenue for a given date range 14 | - Implementation of the `get_revenue_by_country` tool to retrieve revenue filtered by country for a given date range 15 | - Implementation of date parsing utilities to handle relative date expressions like "today", "last week", "YTD" 16 | - Implementation of country normalization to handle both country codes and names 17 | - Update of the test client to test the new tools 18 | - Update of the Claude client to demonstrate the new tools 19 | 20 | ## Next Steps 21 | 1. Implement pagination handling for large result sets 22 | 2. Add caching mechanism for frequently requested data 23 | 3. Improve error handling for Magento 2 API rate limits 24 | 4. Add comprehensive documentation for the API endpoints and parameters 25 | 5. Create automated tests for the server functionality 26 | 6. Implement a logging system for debugging and monitoring 27 | 28 | ## Active Decisions and Considerations 29 | 30 | ### Date Range Parsing 31 | We need to implement a robust date parsing system that can handle various relative date expressions: 32 | - "today" -> Current day 33 | - "yesterday" -> Previous day 34 | - "last week" -> Previous 7 days 35 | - "this month" -> Current month 36 | - "last month" -> Previous month 37 | - "YTD" (Year to Date) -> January 1st of current year to current date 38 | 39 | ### Country Filtering 40 | For the `get_revenue_by_country` tool, we need to: 41 | - Determine how countries are represented in the Magento 2 API (country codes, full names, etc.) 42 | - Handle case-insensitive matching for country names 43 | - Support filtering by multiple countries 44 | 45 | ### Performance Optimization 46 | For large date ranges or high-volume stores, we need to consider: 47 | - Pagination of results 48 | - Efficient filtering at the API level rather than client-side 49 | - Potential caching of frequently requested data 50 | 51 | ### Error Handling 52 | We need robust error handling for: 53 | - Invalid date ranges (e.g., end date before start date) 54 | - Unknown country names 55 | - API errors from Magento 2 56 | - Rate limiting or timeout issues 57 | 58 | ### Response Formatting 59 | The response format should be: 60 | - Consistent across all tools 61 | - Easy for Claude to parse and present to users 62 | - Include metadata about the query (date range, filters applied, etc.) 63 | - Include summary statistics where appropriate 64 | ``` -------------------------------------------------------------------------------- /memory-bank/progress.md: -------------------------------------------------------------------------------- ```markdown 1 | # Progress: Magento 2 MCP Server 2 | 3 | ## What Works 4 | - Basic MCP server setup with stdio transport 5 | - Authentication with Magento 2 API using API tokens 6 | - Product-related tools: 7 | - `get_product_by_sku`: Get detailed information about a product by its SKU 8 | - `search_products`: Search for products using Magento search criteria 9 | - `get_product_categories`: Get categories for a specific product by SKU 10 | - `get_related_products`: Get products related to a specific product by SKU 11 | - `get_product_stock`: Get stock information for a product by SKU 12 | - `get_product_attributes`: Get all attributes for a product by SKU 13 | - `get_product_by_id`: Get detailed information about a product by its ID 14 | - `advanced_product_search`: Search for products with advanced filtering options 15 | - `update_product_attribute`: Update a specific attribute of a product by SKU 16 | - Customer-related tools: 17 | - `get_customer_ordered_products_by_email`: Get all ordered products for a customer by email address 18 | - Test client for verifying server functionality 19 | 20 | ## What's Left to Build 21 | - Enhanced error handling for the new tools 22 | - Documentation for the new tools 23 | - Additional test cases for the new functionality 24 | 25 | ## Current Status 26 | - The basic MCP server infrastructure is in place and working 27 | - Product-related tools are implemented and tested 28 | - Customer-related tools are implemented 29 | - Order and revenue related tools are implemented: 30 | - `get_order_count`: Get the number of orders for a given date range 31 | - `get_revenue`: Get the total revenue for a given date range 32 | - `get_revenue_by_country`: Get revenue filtered by country for a given date range 33 | - `get_product_sales`: Get statistics about the quantity of products sold in a given date range 34 | - Date parsing utilities are implemented, supporting: 35 | - "today" 36 | - "yesterday" 37 | - "this week" 38 | - "last week" 39 | - "this month" 40 | - "last month" 41 | - "ytd" (Year to Date) 42 | - "last year" 43 | - Specific dates in ISO format 44 | - Date ranges in "YYYY-MM-DD to YYYY-MM-DD" format 45 | - Country filtering functionality is implemented, supporting: 46 | - Country codes (e.g., "US", "NL", "GB") 47 | - Country names (e.g., "United States", "The Netherlands", "United Kingdom") 48 | - Common variations (e.g., "USA", "Holland", "UK") 49 | 50 | ## Known Issues 51 | - No comprehensive error handling for Magento 2 API rate limits 52 | - No caching mechanism for frequently requested data 53 | - No pagination handling for large result sets 54 | - No authentication mechanism for the MCP server itself (relies on the security of the stdio transport) 55 | - No logging system for debugging and monitoring 56 | - No automated tests for the server functionality 57 | - No documentation for the API endpoints and parameters 58 | 59 | ## Next Milestone 60 | Enhance the existing tools with additional features and optimizations: 61 | - Implement pagination handling for large result sets 62 | - Add caching mechanism for frequently requested data 63 | - Improve error handling for Magento 2 API rate limits 64 | - Add comprehensive documentation for the API endpoints and parameters 65 | - Create automated tests for the server functionality 66 | - Implement a logging system for debugging and monitoring 67 | 68 | This milestone will be considered complete when all these enhancements are implemented and tested. 69 | ``` -------------------------------------------------------------------------------- /memory-bank/techContext.md: -------------------------------------------------------------------------------- ```markdown 1 | # Technical Context: Magento 2 MCP Server 2 | 3 | ## Technologies Used 4 | 5 | ### Core Technologies 6 | - **Node.js**: The runtime environment for the MCP server 7 | - **JavaScript**: The programming language used for implementation 8 | - **Model Context Protocol (MCP)**: The protocol for communication between Claude and the server 9 | - **Magento 2 REST API**: The API used to interact with the Magento 2 e-commerce platform 10 | 11 | ### Key Libraries and Dependencies 12 | - **@modelcontextprotocol/sdk**: The official MCP SDK for implementing MCP servers and clients 13 | - **axios**: HTTP client for making requests to the Magento 2 API 14 | - **zod**: Schema validation library for defining tool input schemas 15 | - **dotenv**: For loading environment variables from a .env file 16 | - **date-fns**: For date manipulation and parsing 17 | 18 | ## Development Setup 19 | 20 | ### Environment Variables 21 | The server requires the following environment variables: 22 | - `MAGENTO_BASE_URL`: The base URL of the Magento 2 REST API 23 | - `MAGENTO_API_TOKEN`: The API token for authenticating with the Magento 2 API 24 | 25 | These can be set in a `.env` file in the project root or provided directly when running the server. 26 | 27 | ### Running the Server 28 | The server can be run directly with Node.js: 29 | ```bash 30 | node mcp-server.js 31 | ``` 32 | 33 | Or it can be run through the test client: 34 | ```bash 35 | node test-mcp-server.js 36 | ``` 37 | 38 | ### Testing 39 | The `test-mcp-server.js` file provides a simple client for testing the MCP server. It connects to the server, lists available tools, and tests some of the tools with sample parameters. 40 | 41 | ## Technical Constraints 42 | 43 | ### Magento 2 API Limitations 44 | - The Magento 2 API may have rate limits that could affect performance 45 | - Some operations may be slow for large datasets 46 | - Not all Magento 2 data is exposed through the API 47 | - API structure and capabilities may vary between Magento 2 versions 48 | 49 | ### MCP Protocol Constraints 50 | - Communication is synchronous and request-response based 51 | - Tools must have well-defined input schemas 52 | - Complex data structures must be serialized to JSON 53 | 54 | ### Performance Considerations 55 | - Large result sets should be paginated 56 | - Expensive operations should be optimized or cached 57 | - Error handling should be robust to prevent crashes 58 | 59 | ## Dependencies 60 | 61 | ### External Systems 62 | - **Magento 2 E-commerce Platform**: The primary data source for the MCP server 63 | - **Claude AI Assistant**: The primary client for the MCP server 64 | 65 | ### Internal Dependencies 66 | - **callMagentoApi**: Helper function for making authenticated requests to the Magento 2 API 67 | - **Date parsing utilities**: For converting relative date expressions to concrete date ranges 68 | - **Formatting functions**: For formatting API responses for better readability 69 | 70 | ## Integration Points 71 | 72 | ### Magento 2 API Endpoints 73 | - `/orders`: For retrieving order information 74 | - `/invoices`: For retrieving invoice and revenue information 75 | - `/customers`: For retrieving customer information 76 | - `/products`: For retrieving product information 77 | - `/store/storeConfigs`: For retrieving store configuration information 78 | 79 | ### MCP Tools 80 | The server exposes various tools for interacting with the Magento 2 API, including: 81 | - Tools for retrieving product information 82 | - Tools for searching products 83 | - Tools for retrieving customer information 84 | - Tools for retrieving order information 85 | - Tools for retrieving revenue information 86 | ``` -------------------------------------------------------------------------------- /test-mcp-server.js: -------------------------------------------------------------------------------- ```javascript 1 | #!/usr/bin/env node 2 | const { Client } = require('@modelcontextprotocol/sdk/client/index.js'); 3 | const { StdioClientTransport } = require('@modelcontextprotocol/sdk/client/stdio.js'); 4 | 5 | async function main() { 6 | try { 7 | console.log('Connecting to Magento MCP Server...'); 8 | 9 | // Create a transport that will start the server process 10 | const transport = new StdioClientTransport({ 11 | command: 'node', 12 | args: ['./mcp-server.js'] 13 | }); 14 | 15 | // Create a client 16 | const client = new Client( 17 | { 18 | name: 'test-client', 19 | version: '1.0.0' 20 | }, 21 | { 22 | capabilities: { 23 | tools: {} 24 | } 25 | } 26 | ); 27 | 28 | // Connect to the server 29 | await client.connect(transport); 30 | console.log('Connected to Magento MCP Server'); 31 | 32 | // List available tools 33 | const tools = await client.listTools(); 34 | console.log('Available tools:'); 35 | tools.tools.forEach(tool => { 36 | console.log(`- ${tool.name}: ${tool.description}`); 37 | }); 38 | 39 | // Test a tool: search for products 40 | console.log('\nSearching for products with "shirt"...'); 41 | const searchResult = await client.callTool({ 42 | name: 'search_products', 43 | arguments: { 44 | query: 'shirt', 45 | page_size: 5 46 | } 47 | }); 48 | 49 | console.log('Search results:'); 50 | console.log(searchResult.content[0].text); 51 | 52 | // Test the order count tool 53 | console.log('\nGetting order count for today...'); 54 | try { 55 | const orderCountResult = await client.callTool({ 56 | name: 'get_order_count', 57 | arguments: { 58 | date_range: 'today' 59 | } 60 | }); 61 | 62 | console.log('Order count:'); 63 | console.log(orderCountResult.content[0].text); 64 | } catch (error) { 65 | console.log('Error getting order count:', error.message); 66 | } 67 | 68 | // Test the revenue tool 69 | console.log('\nGetting revenue for last week...'); 70 | try { 71 | const revenueResult = await client.callTool({ 72 | name: 'get_revenue', 73 | arguments: { 74 | date_range: 'last week', 75 | include_tax: true 76 | } 77 | }); 78 | 79 | console.log('Revenue:'); 80 | console.log(revenueResult.content[0].text); 81 | } catch (error) { 82 | console.log('Error getting revenue:', error.message); 83 | } 84 | 85 | // Test the revenue by country tool 86 | console.log('\nGetting revenue for The Netherlands this YTD...'); 87 | try { 88 | const revenueByCountryResult = await client.callTool({ 89 | name: 'get_revenue_by_country', 90 | arguments: { 91 | date_range: 'ytd', 92 | country: 'The Netherlands', 93 | include_tax: true 94 | } 95 | }); 96 | 97 | console.log('Revenue by country:'); 98 | console.log(revenueByCountryResult.content[0].text); 99 | } catch (error) { 100 | console.log('Error getting revenue by country:', error.message); 101 | } 102 | 103 | // Test the product sales tool 104 | console.log('\nGetting product sales statistics for last month...'); 105 | try { 106 | const productSalesResult = await client.callTool({ 107 | name: 'get_product_sales', 108 | arguments: { 109 | date_range: 'last month' 110 | } 111 | }); 112 | 113 | console.log('Product sales statistics:'); 114 | console.log(productSalesResult.content[0].text); 115 | } catch (error) { 116 | console.log('Error getting product sales statistics:', error.message); 117 | } 118 | 119 | // Test the customer ordered products by email tool 120 | console.log('\nGetting ordered products for customer by email...'); 121 | try { 122 | const customerOrdersResult = await client.callTool({ 123 | name: 'get_customer_ordered_products_by_email', 124 | arguments: { 125 | email: '[email protected]' // Replace with a valid customer email 126 | } 127 | }); 128 | 129 | console.log('Customer ordered products:'); 130 | console.log(customerOrdersResult.content[0].text); 131 | } catch (error) { 132 | console.log('Error getting customer ordered products:', error.message); 133 | } 134 | 135 | // Close the connection 136 | await client.close(); 137 | console.log('Connection closed'); 138 | } catch (error) { 139 | console.error('Error:', error); 140 | } 141 | } 142 | 143 | main().catch(console.error); 144 | ``` -------------------------------------------------------------------------------- /mcp-instructions/typescript-sdk.md: -------------------------------------------------------------------------------- ```markdown 1 | # MCP TypeScript SDK   2 | 3 | ## Table of Contents 4 | - [MCP TypeScript SDK ](#mcp-typescript-sdk--) 5 | - [Table of Contents](#table-of-contents) 6 | - [Overview](#overview) 7 | - [Installation](#installation) 8 | - [Quick Start](#quick-start) 9 | - [What is MCP?](#what-is-mcp) 10 | - [Core Concepts](#core-concepts) 11 | - [Server](#server) 12 | - [Resources](#resources) 13 | - [Tools](#tools) 14 | - [Prompts](#prompts) 15 | - [Running Your Server](#running-your-server) 16 | - [stdio](#stdio) 17 | - [HTTP with SSE](#http-with-sse) 18 | - [Testing and Debugging](#testing-and-debugging) 19 | - [Examples](#examples) 20 | - [Echo Server](#echo-server) 21 | - [SQLite Explorer](#sqlite-explorer) 22 | - [Advanced Usage](#advanced-usage) 23 | - [Low-Level Server](#low-level-server) 24 | - [Writing MCP Clients](#writing-mcp-clients) 25 | - [Documentation](#documentation) 26 | - [Contributing](#contributing) 27 | - [License](#license) 28 | 29 | ## Overview 30 | 31 | The Model Context Protocol allows applications to provide context for LLMs in a standardized way, separating the concerns of providing context from the actual LLM interaction. This TypeScript SDK implements the full MCP specification, making it easy to: 32 | 33 | - Build MCP clients that can connect to any MCP server 34 | - Create MCP servers that expose resources, prompts and tools 35 | - Use standard transports like stdio and SSE 36 | - Handle all MCP protocol messages and lifecycle events 37 | 38 | ## Installation 39 | 40 | ```bash 41 | npm install @modelcontextprotocol/sdk 42 | ``` 43 | 44 | ## Quick Start 45 | 46 | Let's create a simple MCP server that exposes a calculator tool and some data: 47 | 48 | ```typescript 49 | import { McpServer, ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js"; 50 | import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; 51 | import { z } from "zod"; 52 | 53 | // Create an MCP server 54 | const server = new McpServer({ 55 | name: "Demo", 56 | version: "1.0.0" 57 | }); 58 | 59 | // Add an addition tool 60 | server.tool("add", 61 | { a: z.number(), b: z.number() }, 62 | async ({ a, b }) => ({ 63 | content: [{ type: "text", text: String(a + b) }] 64 | }) 65 | ); 66 | 67 | // Add a dynamic greeting resource 68 | server.resource( 69 | "greeting", 70 | new ResourceTemplate("greeting://{name}", { list: undefined }), 71 | async (uri, { name }) => ({ 72 | contents: [{ 73 | uri: uri.href, 74 | text: `Hello, ${name}!` 75 | }] 76 | }) 77 | ); 78 | 79 | // Start receiving messages on stdin and sending messages on stdout 80 | const transport = new StdioServerTransport(); 81 | await server.connect(transport); 82 | ``` 83 | 84 | ## What is MCP? 85 | 86 | The [Model Context Protocol (MCP)](https://modelcontextprotocol.io) lets you build servers that expose data and functionality to LLM applications in a secure, standardized way. Think of it like a web API, but specifically designed for LLM interactions. MCP servers can: 87 | 88 | - Expose data through **Resources** (think of these sort of like GET endpoints; they are used to load information into the LLM's context) 89 | - Provide functionality through **Tools** (sort of like POST endpoints; they are used to execute code or otherwise produce a side effect) 90 | - Define interaction patterns through **Prompts** (reusable templates for LLM interactions) 91 | - And more! 92 | 93 | ## Core Concepts 94 | 95 | ### Server 96 | 97 | The McpServer is your core interface to the MCP protocol. It handles connection management, protocol compliance, and message routing: 98 | 99 | ```typescript 100 | const server = new McpServer({ 101 | name: "My App", 102 | version: "1.0.0" 103 | }); 104 | ``` 105 | 106 | ### Resources 107 | 108 | Resources are how you expose data to LLMs. They're similar to GET endpoints in a REST API - they provide data but shouldn't perform significant computation or have side effects: 109 | 110 | ```typescript 111 | // Static resource 112 | server.resource( 113 | "config", 114 | "config://app", 115 | async (uri) => ({ 116 | contents: [{ 117 | uri: uri.href, 118 | text: "App configuration here" 119 | }] 120 | }) 121 | ); 122 | 123 | // Dynamic resource with parameters 124 | server.resource( 125 | "user-profile", 126 | new ResourceTemplate("users://{userId}/profile", { list: undefined }), 127 | async (uri, { userId }) => ({ 128 | contents: [{ 129 | uri: uri.href, 130 | text: `Profile data for user ${userId}` 131 | }] 132 | }) 133 | ); 134 | ``` 135 | 136 | ### Tools 137 | 138 | Tools let LLMs take actions through your server. Unlike resources, tools are expected to perform computation and have side effects: 139 | 140 | ```typescript 141 | // Simple tool with parameters 142 | server.tool( 143 | "calculate-bmi", 144 | { 145 | weightKg: z.number(), 146 | heightM: z.number() 147 | }, 148 | async ({ weightKg, heightM }) => ({ 149 | content: [{ 150 | type: "text", 151 | text: String(weightKg / (heightM * heightM)) 152 | }] 153 | }) 154 | ); 155 | 156 | // Async tool with external API call 157 | server.tool( 158 | "fetch-weather", 159 | { city: z.string() }, 160 | async ({ city }) => { 161 | const response = await fetch(`https://api.weather.com/${city}`); 162 | const data = await response.text(); 163 | return { 164 | content: [{ type: "text", text: data }] 165 | }; 166 | } 167 | ); 168 | ``` 169 | 170 | ### Prompts 171 | 172 | Prompts are reusable templates that help LLMs interact with your server effectively: 173 | 174 | ```typescript 175 | server.prompt( 176 | "review-code", 177 | { code: z.string() }, 178 | ({ code }) => ({ 179 | messages: [{ 180 | role: "user", 181 | content: { 182 | type: "text", 183 | text: `Please review this code:\n\n${code}` 184 | } 185 | }] 186 | }) 187 | ); 188 | ``` 189 | 190 | ## Running Your Server 191 | 192 | MCP servers in TypeScript need to be connected to a transport to communicate with clients. How you start the server depends on the choice of transport: 193 | 194 | ### stdio 195 | 196 | For command-line tools and direct integrations: 197 | 198 | ```typescript 199 | import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; 200 | import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; 201 | 202 | const server = new McpServer({ 203 | name: "example-server", 204 | version: "1.0.0" 205 | }); 206 | 207 | // ... set up server resources, tools, and prompts ... 208 | 209 | const transport = new StdioServerTransport(); 210 | await server.connect(transport); 211 | ``` 212 | 213 | ### HTTP with SSE 214 | 215 | For remote servers, start a web server with a Server-Sent Events (SSE) endpoint, and a separate endpoint for the client to send its messages to: 216 | 217 | ```typescript 218 | import express from "express"; 219 | import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; 220 | import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js"; 221 | 222 | const server = new McpServer({ 223 | name: "example-server", 224 | version: "1.0.0" 225 | }); 226 | 227 | // ... set up server resources, tools, and prompts ... 228 | 229 | const app = express(); 230 | 231 | app.get("/sse", async (req, res) => { 232 | const transport = new SSEServerTransport("/messages", res); 233 | await server.connect(transport); 234 | }); 235 | 236 | app.post("/messages", async (req, res) => { 237 | // Note: to support multiple simultaneous connections, these messages will 238 | // need to be routed to a specific matching transport. (This logic isn't 239 | // implemented here, for simplicity.) 240 | await transport.handlePostMessage(req, res); 241 | }); 242 | 243 | app.listen(3001); 244 | ``` 245 | 246 | ### Testing and Debugging 247 | 248 | To test your server, you can use the [MCP Inspector](https://github.com/modelcontextprotocol/inspector). See its README for more information. 249 | 250 | ## Examples 251 | 252 | ### Echo Server 253 | 254 | A simple server demonstrating resources, tools, and prompts: 255 | 256 | ```typescript 257 | import { McpServer, ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js"; 258 | import { z } from "zod"; 259 | 260 | const server = new McpServer({ 261 | name: "Echo", 262 | version: "1.0.0" 263 | }); 264 | 265 | server.resource( 266 | "echo", 267 | new ResourceTemplate("echo://{message}", { list: undefined }), 268 | async (uri, { message }) => ({ 269 | contents: [{ 270 | uri: uri.href, 271 | text: `Resource echo: ${message}` 272 | }] 273 | }) 274 | ); 275 | 276 | server.tool( 277 | "echo", 278 | { message: z.string() }, 279 | async ({ message }) => ({ 280 | content: [{ type: "text", text: `Tool echo: ${message}` }] 281 | }) 282 | ); 283 | 284 | server.prompt( 285 | "echo", 286 | { message: z.string() }, 287 | ({ message }) => ({ 288 | messages: [{ 289 | role: "user", 290 | content: { 291 | type: "text", 292 | text: `Please process this message: ${message}` 293 | } 294 | }] 295 | }) 296 | ); 297 | ``` 298 | 299 | ### SQLite Explorer 300 | 301 | A more complex example showing database integration: 302 | 303 | ```typescript 304 | import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; 305 | import sqlite3 from "sqlite3"; 306 | import { promisify } from "util"; 307 | import { z } from "zod"; 308 | 309 | const server = new McpServer({ 310 | name: "SQLite Explorer", 311 | version: "1.0.0" 312 | }); 313 | 314 | // Helper to create DB connection 315 | const getDb = () => { 316 | const db = new sqlite3.Database("database.db"); 317 | return { 318 | all: promisify<string, any[]>(db.all.bind(db)), 319 | close: promisify(db.close.bind(db)) 320 | }; 321 | }; 322 | 323 | server.resource( 324 | "schema", 325 | "schema://main", 326 | async (uri) => { 327 | const db = getDb(); 328 | try { 329 | const tables = await db.all( 330 | "SELECT sql FROM sqlite_master WHERE type='table'" 331 | ); 332 | return { 333 | contents: [{ 334 | uri: uri.href, 335 | text: tables.map((t: {sql: string}) => t.sql).join("\n") 336 | }] 337 | }; 338 | } finally { 339 | await db.close(); 340 | } 341 | } 342 | ); 343 | 344 | server.tool( 345 | "query", 346 | { sql: z.string() }, 347 | async ({ sql }) => { 348 | const db = getDb(); 349 | try { 350 | const results = await db.all(sql); 351 | return { 352 | content: [{ 353 | type: "text", 354 | text: JSON.stringify(results, null, 2) 355 | }] 356 | }; 357 | } catch (err: unknown) { 358 | const error = err as Error; 359 | return { 360 | content: [{ 361 | type: "text", 362 | text: `Error: ${error.message}` 363 | }], 364 | isError: true 365 | }; 366 | } finally { 367 | await db.close(); 368 | } 369 | } 370 | ); 371 | ``` 372 | 373 | ## Advanced Usage 374 | 375 | ### Low-Level Server 376 | 377 | For more control, you can use the low-level Server class directly: 378 | 379 | ```typescript 380 | import { Server } from "@modelcontextprotocol/sdk/server/index.js"; 381 | import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; 382 | import { 383 | ListPromptsRequestSchema, 384 | GetPromptRequestSchema 385 | } from "@modelcontextprotocol/sdk/types.js"; 386 | 387 | const server = new Server( 388 | { 389 | name: "example-server", 390 | version: "1.0.0" 391 | }, 392 | { 393 | capabilities: { 394 | prompts: {} 395 | } 396 | } 397 | ); 398 | 399 | server.setRequestHandler(ListPromptsRequestSchema, async () => { 400 | return { 401 | prompts: [{ 402 | name: "example-prompt", 403 | description: "An example prompt template", 404 | arguments: [{ 405 | name: "arg1", 406 | description: "Example argument", 407 | required: true 408 | }] 409 | }] 410 | }; 411 | }); 412 | 413 | server.setRequestHandler(GetPromptRequestSchema, async (request) => { 414 | if (request.params.name !== "example-prompt") { 415 | throw new Error("Unknown prompt"); 416 | } 417 | return { 418 | description: "Example prompt", 419 | messages: [{ 420 | role: "user", 421 | content: { 422 | type: "text", 423 | text: "Example prompt text" 424 | } 425 | }] 426 | }; 427 | }); 428 | 429 | const transport = new StdioServerTransport(); 430 | await server.connect(transport); 431 | ``` 432 | 433 | ### Writing MCP Clients 434 | 435 | The SDK provides a high-level client interface: 436 | 437 | ```typescript 438 | import { Client } from "@modelcontextprotocol/sdk/client/index.js"; 439 | import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js"; 440 | 441 | const transport = new StdioClientTransport({ 442 | command: "node", 443 | args: ["server.js"] 444 | }); 445 | 446 | const client = new Client( 447 | { 448 | name: "example-client", 449 | version: "1.0.0" 450 | }, 451 | { 452 | capabilities: { 453 | prompts: {}, 454 | resources: {}, 455 | tools: {} 456 | } 457 | } 458 | ); 459 | 460 | await client.connect(transport); 461 | 462 | // List prompts 463 | const prompts = await client.listPrompts(); 464 | 465 | // Get a prompt 466 | const prompt = await client.getPrompt("example-prompt", { 467 | arg1: "value" 468 | }); 469 | 470 | // List resources 471 | const resources = await client.listResources(); 472 | 473 | // Read a resource 474 | const resource = await client.readResource("file:///example.txt"); 475 | 476 | // Call a tool 477 | const result = await client.callTool({ 478 | name: "example-tool", 479 | arguments: { 480 | arg1: "value" 481 | } 482 | }); 483 | ``` 484 | 485 | ## Documentation 486 | 487 | - [Model Context Protocol documentation](https://modelcontextprotocol.io) 488 | - [MCP Specification](https://spec.modelcontextprotocol.io) 489 | - [Example Servers](https://github.com/modelcontextprotocol/servers) 490 | 491 | ## Contributing 492 | 493 | Issues and pull requests are welcome on GitHub at https://github.com/modelcontextprotocol/typescript-sdk. 494 | 495 | ## License 496 | 497 | This project is licensed under the MIT License—see the [LICENSE](LICENSE) file for details. 498 | ``` -------------------------------------------------------------------------------- /mcp-server.js: -------------------------------------------------------------------------------- ```javascript 1 | #!/usr/bin/env node 2 | const { McpServer } = require('@modelcontextprotocol/sdk/server/mcp.js'); 3 | const { StdioServerTransport } = require('@modelcontextprotocol/sdk/server/stdio.js'); 4 | const { z } = require('zod'); 5 | const axios = require('axios'); 6 | const dotenv = require('dotenv'); 7 | const { format, parse, parseISO, isValid, addDays, subDays, startOfDay, endOfDay, startOfWeek, endOfWeek, startOfMonth, endOfMonth, startOfYear, isAfter, isBefore } = require('date-fns'); 8 | 9 | // Load environment variables from .env file 10 | dotenv.config(); 11 | 12 | // Magento 2 API Configuration 13 | const MAGENTO_BASE_URL = process.env.MAGENTO_BASE_URL || 'https://your-magento-store.com/rest/V1'; 14 | const MAGENTO_API_TOKEN = process.env.MAGENTO_API_TOKEN; 15 | 16 | // Validate environment variables 17 | if (!MAGENTO_API_TOKEN) { 18 | console.error('ERROR: MAGENTO_API_TOKEN environment variable is required'); 19 | process.exit(1); 20 | } 21 | 22 | // Date parsing utilities 23 | function parseDateExpression(dateExpression) { 24 | const now = new Date(); 25 | const currentYear = now.getFullYear(); 26 | const currentMonth = now.getMonth(); 27 | const currentDay = now.getDate(); 28 | 29 | // Normalize the date expression 30 | const normalizedExpression = dateExpression.toLowerCase().trim(); 31 | 32 | // Handle relative date expressions 33 | switch (normalizedExpression) { 34 | case 'today': 35 | return { 36 | startDate: startOfDay(now), 37 | endDate: endOfDay(now), 38 | description: 'Today' 39 | }; 40 | case 'yesterday': 41 | const yesterday = subDays(now, 1); 42 | return { 43 | startDate: startOfDay(yesterday), 44 | endDate: endOfDay(yesterday), 45 | description: 'Yesterday' 46 | }; 47 | case 'this week': 48 | return { 49 | startDate: startOfWeek(now, { weekStartsOn: 1 }), // Week starts on Monday 50 | endDate: endOfDay(now), 51 | description: 'This week' 52 | }; 53 | case 'last week': 54 | const lastWeekStart = subDays(startOfWeek(now, { weekStartsOn: 1 }), 7); 55 | const lastWeekEnd = subDays(endOfWeek(now, { weekStartsOn: 1 }), 7); 56 | return { 57 | startDate: lastWeekStart, 58 | endDate: lastWeekEnd, 59 | description: 'Last week' 60 | }; 61 | case 'this month': 62 | return { 63 | startDate: startOfMonth(now), 64 | endDate: endOfDay(now), 65 | description: 'This month' 66 | }; 67 | case 'last month': 68 | const lastMonth = new Date(currentYear, currentMonth - 1, 1); 69 | return { 70 | startDate: startOfMonth(lastMonth), 71 | endDate: endOfMonth(lastMonth), 72 | description: 'Last month' 73 | }; 74 | case 'ytd': 75 | case 'this ytd': 76 | case 'this year to date': 77 | case 'year to date': 78 | return { 79 | startDate: startOfYear(now), 80 | endDate: endOfDay(now), 81 | description: 'Year to date' 82 | }; 83 | case 'last year': 84 | const lastYear = new Date(currentYear - 1, 0, 1); 85 | return { 86 | startDate: startOfYear(lastYear), 87 | endDate: endOfYear(lastYear), 88 | description: 'Last year' 89 | }; 90 | default: 91 | // Try to parse as ISO date or other common formats 92 | try { 93 | // Check if it's a single date (not a range) 94 | const parsedDate = parseISO(normalizedExpression); 95 | if (isValid(parsedDate)) { 96 | return { 97 | startDate: startOfDay(parsedDate), 98 | endDate: endOfDay(parsedDate), 99 | description: format(parsedDate, 'yyyy-MM-dd') 100 | }; 101 | } 102 | 103 | // Check if it's a date range in format "YYYY-MM-DD to YYYY-MM-DD" 104 | const rangeParts = normalizedExpression.split(' to '); 105 | if (rangeParts.length === 2) { 106 | const startDate = parseISO(rangeParts[0]); 107 | const endDate = parseISO(rangeParts[1]); 108 | 109 | if (isValid(startDate) && isValid(endDate)) { 110 | return { 111 | startDate: startOfDay(startDate), 112 | endDate: endOfDay(endDate), 113 | description: `${format(startDate, 'yyyy-MM-dd')} to ${format(endDate, 'yyyy-MM-dd')}` 114 | }; 115 | } 116 | } 117 | 118 | // If we can't parse it, throw an error 119 | throw new Error(`Unable to parse date expression: ${dateExpression}`); 120 | } catch (error) { 121 | throw new Error(`Invalid date expression: ${dateExpression}. ${error.message}`); 122 | } 123 | } 124 | } 125 | 126 | // Helper function to get the end of a year 127 | function endOfYear(date) { 128 | return new Date(date.getFullYear(), 11, 31, 23, 59, 59, 999); 129 | } 130 | 131 | // Helper function to format a date for Magento API 132 | function formatDateForMagento(date) { 133 | return format(date, "yyyy-MM-dd HH:mm:ss"); 134 | } 135 | 136 | // Helper function to build date range filter for Magento API 137 | function buildDateRangeFilter(field, startDate, endDate) { 138 | const formattedStartDate = formatDateForMagento(startDate); 139 | const formattedEndDate = formatDateForMagento(endDate); 140 | 141 | return [ 142 | `searchCriteria[filter_groups][0][filters][0][field]=${field}`, 143 | `searchCriteria[filter_groups][0][filters][0][value]=${encodeURIComponent(formattedStartDate)}`, 144 | `searchCriteria[filter_groups][0][filters][0][condition_type]=gteq`, 145 | `searchCriteria[filter_groups][1][filters][0][field]=${field}`, 146 | `searchCriteria[filter_groups][1][filters][0][value]=${encodeURIComponent(formattedEndDate)}`, 147 | `searchCriteria[filter_groups][1][filters][0][condition_type]=lteq` 148 | ].join('&'); 149 | } 150 | 151 | // Helper function to normalize country input 152 | function normalizeCountry(country) { 153 | // Normalize the country input (handle both country codes and names) 154 | const countryInput = country.trim().toLowerCase(); 155 | 156 | // Map of common country names to ISO country codes 157 | const countryMap = { 158 | // Common variations for The Netherlands 159 | 'netherlands': 'NL', 160 | 'the netherlands': 'NL', 161 | 'holland': 'NL', 162 | 'nl': 'NL', 163 | 164 | // Common variations for United States 165 | 'united states': 'US', 166 | 'usa': 'US', 167 | 'us': 'US', 168 | 'america': 'US', 169 | 170 | // Common variations for United Kingdom 171 | 'united kingdom': 'GB', 172 | 'uk': 'GB', 173 | 'great britain': 'GB', 174 | 'gb': 'GB', 175 | 'england': 'GB', 176 | 177 | // Add more countries as needed 178 | 'canada': 'CA', 179 | 'ca': 'CA', 180 | 181 | 'australia': 'AU', 182 | 'au': 'AU', 183 | 184 | 'germany': 'DE', 185 | 'de': 'DE', 186 | 187 | 'france': 'FR', 188 | 'fr': 'FR', 189 | 190 | 'italy': 'IT', 191 | 'it': 'IT', 192 | 193 | 'spain': 'ES', 194 | 'es': 'ES', 195 | 196 | 'belgium': 'BE', 197 | 'be': 'BE', 198 | 199 | 'sweden': 'SE', 200 | 'se': 'SE', 201 | 202 | 'norway': 'NO', 203 | 'no': 'NO', 204 | 205 | 'denmark': 'DK', 206 | 'dk': 'DK', 207 | 208 | 'finland': 'FI', 209 | 'fi': 'FI', 210 | 211 | 'ireland': 'IE', 212 | 'ie': 'IE', 213 | 214 | 'switzerland': 'CH', 215 | 'ch': 'CH', 216 | 217 | 'austria': 'AT', 218 | 'at': 'AT', 219 | 220 | 'portugal': 'PT', 221 | 'pt': 'PT', 222 | 223 | 'greece': 'GR', 224 | 'gr': 'GR', 225 | 226 | 'poland': 'PL', 227 | 'pl': 'PL', 228 | 229 | 'japan': 'JP', 230 | 'jp': 'JP', 231 | 232 | 'china': 'CN', 233 | 'cn': 'CN', 234 | 235 | 'india': 'IN', 236 | 'in': 'IN', 237 | 238 | 'brazil': 'BR', 239 | 'br': 'BR', 240 | 241 | 'mexico': 'MX', 242 | 'mx': 'MX', 243 | 244 | 'south africa': 'ZA', 245 | 'za': 'ZA' 246 | }; 247 | 248 | // Check if the input is in our map 249 | if (countryMap[countryInput]) { 250 | return [countryMap[countryInput]]; 251 | } 252 | 253 | // If it's not in our map, assume it's a country code or name and return as is 254 | // For a more robust solution, we would validate against a complete list of country codes 255 | return [countryInput.toUpperCase()]; 256 | } 257 | 258 | // Helper function to fetch all pages for a given search criteria 259 | async function fetchAllPages(endpoint, baseSearchCriteria) { 260 | const pageSize = 100; // Or make this configurable if needed 261 | let currentPage = 1; 262 | let allItems = []; 263 | let totalCount = 0; 264 | 265 | do { 266 | // Build search criteria for the current page, ensuring baseSearchCriteria doesn't already have pagination 267 | let currentPageSearchCriteria = baseSearchCriteria; 268 | if (!currentPageSearchCriteria.includes('searchCriteria[pageSize]')) { 269 | currentPageSearchCriteria += `&searchCriteria[pageSize]=${pageSize}`; 270 | } 271 | if (!currentPageSearchCriteria.includes('searchCriteria[currentPage]')) { 272 | currentPageSearchCriteria += `&searchCriteria[currentPage]=${currentPage}`; 273 | } else { 274 | // If currentPage is already there, replace it (less common case) 275 | currentPageSearchCriteria = currentPageSearchCriteria.replace(/searchCriteria\[currentPage\]=\d+/, `searchCriteria[currentPage]=${currentPage}`); 276 | } 277 | 278 | // Make the API call for the current page 279 | const responseData = await callMagentoApi(`${endpoint}?${currentPageSearchCriteria}`); 280 | 281 | if (responseData.items && Array.isArray(responseData.items)) { 282 | allItems = allItems.concat(responseData.items); 283 | } 284 | 285 | // Update total count (only needs to be set once) 286 | if (currentPage === 1) { 287 | totalCount = responseData.total_count || 0; 288 | } 289 | 290 | // Check if we need to fetch more pages 291 | if (totalCount <= allItems.length || !responseData.items || responseData.items.length < pageSize) { 292 | break; // Exit loop if all items are fetched or last page had less than pageSize items 293 | } 294 | 295 | currentPage++; 296 | 297 | } while (true); // Loop continues until break 298 | 299 | return allItems; // Return the aggregated list of items 300 | } 301 | 302 | // Create an MCP server 303 | const server = new McpServer({ 304 | name: "magento-mcp-server", 305 | version: "1.0.0" 306 | }); 307 | 308 | // Helper function to make authenticated requests to Magento 2 API 309 | async function callMagentoApi(endpoint, method = 'GET', data = null) { 310 | try { 311 | const url = `${MAGENTO_BASE_URL}${endpoint}`; 312 | const headers = { 313 | 'Authorization': `Bearer ${MAGENTO_API_TOKEN}`, 314 | 'Content-Type': 'application/json' 315 | }; 316 | 317 | const config = { 318 | method, 319 | url, 320 | headers, 321 | data: data ? JSON.stringify(data) : undefined, 322 | // Bypass SSL certificate verification for development 323 | httpsAgent: new (require('https').Agent)({ 324 | rejectUnauthorized: false 325 | }) 326 | }; 327 | 328 | const response = await axios(config); 329 | return response.data; 330 | } catch (error) { 331 | console.error('Magento API Error:', error.response?.data || error.message); 332 | throw error; 333 | } 334 | } 335 | 336 | // Format product data for better readability 337 | function formatProduct(product) { 338 | if (!product) return "Product not found"; 339 | 340 | // Extract custom attributes into a more readable format 341 | const customAttributes = {}; 342 | if (product.custom_attributes && Array.isArray(product.custom_attributes)) { 343 | product.custom_attributes.forEach(attr => { 344 | customAttributes[attr.attribute_code] = attr.value; 345 | }); 346 | } 347 | 348 | return { 349 | id: product.id, 350 | sku: product.sku, 351 | name: product.name, 352 | price: product.price, 353 | status: product.status, 354 | visibility: product.visibility, 355 | type_id: product.type_id, 356 | created_at: product.created_at, 357 | updated_at: product.updated_at, 358 | extension_attributes: product.extension_attributes, 359 | custom_attributes: customAttributes 360 | }; 361 | } 362 | 363 | // Format search results for better readability 364 | function formatSearchResults(results) { 365 | if (!results || !results.items || !Array.isArray(results.items)) { 366 | return "No products found"; 367 | } 368 | 369 | return { 370 | total_count: results.total_count, 371 | items: results.items.map(item => ({ 372 | id: item.id, 373 | sku: item.sku, 374 | name: item.name, 375 | price: item.price, 376 | status: item.status, 377 | type_id: item.type_id 378 | })) 379 | }; 380 | } 381 | 382 | // Tool: Get product by SKU 383 | server.tool( 384 | "get_product_by_sku", 385 | "Get detailed information about a product by its SKU", 386 | { 387 | sku: z.string().describe("The SKU (Stock Keeping Unit) of the product") 388 | }, 389 | async ({ sku }) => { 390 | try { 391 | const productData = await callMagentoApi(`/products/${sku}`); 392 | const formattedProduct = formatProduct(productData); 393 | 394 | return { 395 | content: [ 396 | { 397 | type: "text", 398 | text: JSON.stringify(formattedProduct, null, 2) 399 | } 400 | ] 401 | }; 402 | } catch (error) { 403 | return { 404 | content: [ 405 | { 406 | type: "text", 407 | text: `Error fetching product: ${error.message}` 408 | } 409 | ], 410 | isError: true 411 | }; 412 | } 413 | } 414 | ); 415 | 416 | // Tool: Search products 417 | server.tool( 418 | "search_products", 419 | "Search for products using Magento search criteria", 420 | { 421 | query: z.string().describe("Search query (product name, description, etc.)"), 422 | page_size: z.number().optional().describe("Number of results per page (default: 10)"), 423 | current_page: z.number().optional().describe("Page number (default: 1)") 424 | }, 425 | async ({ query, page_size = 10, current_page = 1 }) => { 426 | try { 427 | // Build search criteria for a simple name search 428 | const searchCriteria = `searchCriteria[filter_groups][0][filters][0][field]=name&` + 429 | `searchCriteria[filter_groups][0][filters][0][value]=%25${encodeURIComponent(query)}%25&` + 430 | `searchCriteria[filter_groups][0][filters][0][condition_type]=like&` + 431 | `searchCriteria[pageSize]=${page_size}&` + 432 | `searchCriteria[currentPage]=${current_page}`; 433 | 434 | const productData = await callMagentoApi(`/products?${searchCriteria}`); 435 | const formattedResults = formatSearchResults(productData); 436 | 437 | return { 438 | content: [ 439 | { 440 | type: "text", 441 | text: JSON.stringify(formattedResults, null, 2) 442 | } 443 | ] 444 | }; 445 | } catch (error) { 446 | return { 447 | content: [ 448 | { 449 | type: "text", 450 | text: `Error searching products: ${error.message}` 451 | } 452 | ], 453 | isError: true 454 | }; 455 | } 456 | } 457 | ); 458 | 459 | // Tool: Get product categories 460 | server.tool( 461 | "get_product_categories", 462 | "Get categories for a specific product by SKU", 463 | { 464 | sku: z.string().describe("The SKU (Stock Keeping Unit) of the product") 465 | }, 466 | async ({ sku }) => { 467 | try { 468 | // First get the product to find its category IDs 469 | const productData = await callMagentoApi(`/products/${sku}`); 470 | 471 | // Find category IDs in custom attributes 472 | const categoryAttribute = productData.custom_attributes?.find( 473 | attr => attr.attribute_code === 'category_ids' 474 | ); 475 | 476 | if (!categoryAttribute || !categoryAttribute.value) { 477 | return { 478 | content: [ 479 | { 480 | type: "text", 481 | text: `No categories found for product with SKU: ${sku}` 482 | } 483 | ] 484 | }; 485 | } 486 | 487 | // Parse category IDs (they might be in string format) 488 | let categoryIds = categoryAttribute.value; 489 | if (typeof categoryIds === 'string') { 490 | try { 491 | categoryIds = JSON.parse(categoryIds); 492 | } catch (e) { 493 | // If it's not valid JSON, split by comma 494 | categoryIds = categoryIds.split(',').map(id => id.trim()); 495 | } 496 | } 497 | 498 | if (!Array.isArray(categoryIds)) { 499 | categoryIds = [categoryIds]; 500 | } 501 | 502 | // Get category details for each ID 503 | const categoryPromises = categoryIds.map(id => 504 | callMagentoApi(`/categories/${id}`) 505 | .catch(err => ({ id, error: err.message })) 506 | ); 507 | 508 | const categories = await Promise.all(categoryPromises); 509 | 510 | return { 511 | content: [ 512 | { 513 | type: "text", 514 | text: JSON.stringify(categories, null, 2) 515 | } 516 | ] 517 | }; 518 | } catch (error) { 519 | return { 520 | content: [ 521 | { 522 | type: "text", 523 | text: `Error fetching product categories: ${error.message}` 524 | } 525 | ], 526 | isError: true 527 | }; 528 | } 529 | } 530 | ); 531 | 532 | // Tool: Get related products 533 | server.tool( 534 | "get_related_products", 535 | "Get products related to a specific product by SKU", 536 | { 537 | sku: z.string().describe("The SKU (Stock Keeping Unit) of the product") 538 | }, 539 | async ({ sku }) => { 540 | try { 541 | const relatedProducts = await callMagentoApi(`/products/${sku}/links/related`); 542 | 543 | if (!relatedProducts || relatedProducts.length === 0) { 544 | return { 545 | content: [ 546 | { 547 | type: "text", 548 | text: `No related products found for SKU: ${sku}` 549 | } 550 | ] 551 | }; 552 | } 553 | 554 | // Get full details for each related product 555 | const productPromises = relatedProducts.map(related => 556 | callMagentoApi(`/products/${related.linked_product_sku}`) 557 | .then(product => formatProduct(product)) 558 | .catch(err => ({ sku: related.linked_product_sku, error: err.message })) 559 | ); 560 | 561 | const products = await Promise.all(productPromises); 562 | 563 | return { 564 | content: [ 565 | { 566 | type: "text", 567 | text: JSON.stringify(products, null, 2) 568 | } 569 | ] 570 | }; 571 | } catch (error) { 572 | return { 573 | content: [ 574 | { 575 | type: "text", 576 | text: `Error fetching related products: ${error.message}` 577 | } 578 | ], 579 | isError: true 580 | }; 581 | } 582 | } 583 | ); 584 | 585 | // Tool: Get product stock information 586 | server.tool( 587 | "get_product_stock", 588 | "Get stock information for a product by SKU", 589 | { 590 | sku: z.string().describe("The SKU (Stock Keeping Unit) of the product") 591 | }, 592 | async ({ sku }) => { 593 | try { 594 | const stockData = await callMagentoApi(`/stockItems/${sku}`); 595 | 596 | return { 597 | content: [ 598 | { 599 | type: "text", 600 | text: JSON.stringify(stockData, null, 2) 601 | } 602 | ] 603 | }; 604 | } catch (error) { 605 | return { 606 | content: [ 607 | { 608 | type: "text", 609 | text: `Error fetching stock information: ${error.message}` 610 | } 611 | ], 612 | isError: true 613 | }; 614 | } 615 | } 616 | ); 617 | 618 | // Tool: Get product attributes 619 | server.tool( 620 | "get_product_attributes", 621 | "Get all attributes for a product by SKU", 622 | { 623 | sku: z.string().describe("The SKU (Stock Keeping Unit) of the product") 624 | }, 625 | async ({ sku }) => { 626 | try { 627 | const productData = await callMagentoApi(`/products/${sku}`); 628 | 629 | // Extract and format attributes 630 | const attributes = { 631 | base_attributes: { 632 | id: productData.id, 633 | sku: productData.sku, 634 | name: productData.name, 635 | price: productData.price, 636 | status: productData.status, 637 | visibility: productData.visibility, 638 | type_id: productData.type_id 639 | }, 640 | custom_attributes: {} 641 | }; 642 | 643 | if (productData.custom_attributes && Array.isArray(productData.custom_attributes)) { 644 | productData.custom_attributes.forEach(attr => { 645 | attributes.custom_attributes[attr.attribute_code] = attr.value; 646 | }); 647 | } 648 | 649 | return { 650 | content: [ 651 | { 652 | type: "text", 653 | text: JSON.stringify(attributes, null, 2) 654 | } 655 | ] 656 | }; 657 | } catch (error) { 658 | return { 659 | content: [ 660 | { 661 | type: "text", 662 | text: `Error fetching product attributes: ${error.message}` 663 | } 664 | ], 665 | isError: true 666 | }; 667 | } 668 | } 669 | ); 670 | 671 | // Tool: Get product by ID 672 | server.tool( 673 | "get_product_by_id", 674 | "Get detailed information about a product by its ID", 675 | { 676 | id: z.number().describe("The ID of the product") 677 | }, 678 | async ({ id }) => { 679 | try { 680 | // First we need to search for the product by ID to get its SKU 681 | const searchCriteria = `searchCriteria[filter_groups][0][filters][0][field]=entity_id&` + 682 | `searchCriteria[filter_groups][0][filters][0][value]=${id}&` + 683 | `searchCriteria[filter_groups][0][filters][0][condition_type]=eq`; 684 | 685 | const searchResults = await callMagentoApi(`/products?${searchCriteria}`); 686 | 687 | if (!searchResults.items || searchResults.items.length === 0) { 688 | return { 689 | content: [ 690 | { 691 | type: "text", 692 | text: `No product found with ID: ${id}` 693 | } 694 | ] 695 | }; 696 | } 697 | 698 | // Get the SKU from the search results 699 | const sku = searchResults.items[0].sku; 700 | 701 | // Now get the full product details using the SKU 702 | const productData = await callMagentoApi(`/products/${sku}`); 703 | const formattedProduct = formatProduct(productData); 704 | 705 | return { 706 | content: [ 707 | { 708 | type: "text", 709 | text: JSON.stringify(formattedProduct, null, 2) 710 | } 711 | ] 712 | }; 713 | } catch (error) { 714 | return { 715 | content: [ 716 | { 717 | type: "text", 718 | text: `Error fetching product: ${error.message}` 719 | } 720 | ], 721 | isError: true 722 | }; 723 | } 724 | } 725 | ); 726 | 727 | // Tool: Advanced product search 728 | server.tool( 729 | "advanced_product_search", 730 | "Search for products with advanced filtering options", 731 | { 732 | field: z.string().describe("Field to search on (e.g., name, sku, price, status)"), 733 | value: z.string().describe("Value to search for"), 734 | condition_type: z.string().optional().describe("Condition type (eq, like, gt, lt, etc.). Default: eq"), 735 | page_size: z.number().optional().describe("Number of results per page (default: 10)"), 736 | current_page: z.number().optional().describe("Page number (default: 1)"), 737 | sort_field: z.string().optional().describe("Field to sort by (default: entity_id)"), 738 | sort_direction: z.string().optional().describe("Sort direction (ASC or DESC, default: DESC)") 739 | }, 740 | async ({ field, value, condition_type = 'eq', page_size = 10, current_page = 1, sort_field = 'entity_id', sort_direction = 'DESC' }) => { 741 | try { 742 | // Build search criteria 743 | const searchCriteria = `searchCriteria[filter_groups][0][filters][0][field]=${encodeURIComponent(field)}&` + 744 | `searchCriteria[filter_groups][0][filters][0][value]=${encodeURIComponent(value)}&` + 745 | `searchCriteria[filter_groups][0][filters][0][condition_type]=${encodeURIComponent(condition_type)}&` + 746 | `searchCriteria[pageSize]=${page_size}&` + 747 | `searchCriteria[currentPage]=${current_page}&` + 748 | `searchCriteria[sortOrders][0][field]=${encodeURIComponent(sort_field)}&` + 749 | `searchCriteria[sortOrders][0][direction]=${encodeURIComponent(sort_direction)}`; 750 | 751 | const productData = await callMagentoApi(`/products?${searchCriteria}`); 752 | const formattedResults = formatSearchResults(productData); 753 | 754 | return { 755 | content: [ 756 | { 757 | type: "text", 758 | text: JSON.stringify(formattedResults, null, 2) 759 | } 760 | ] 761 | }; 762 | } catch (error) { 763 | return { 764 | content: [ 765 | { 766 | type: "text", 767 | text: `Error performing advanced search: ${error.message}` 768 | } 769 | ], 770 | isError: true 771 | }; 772 | } 773 | } 774 | ); 775 | 776 | // Tool: Update product attribute 777 | server.tool( 778 | "update_product_attribute", 779 | "Update a specific attribute of a product by SKU", 780 | { 781 | sku: z.string().describe("The SKU (Stock Keeping Unit) of the product"), 782 | attribute_code: z.string().describe("The code of the attribute to update (e.g., name, price, description, status, etc.)"), 783 | value: z.any().describe("The new value for the attribute") 784 | }, 785 | async ({ sku, attribute_code, value }) => { 786 | try { 787 | // First, check if the product exists 788 | const productData = await callMagentoApi(`/products/${sku}`).catch(() => null); 789 | 790 | if (!productData) { 791 | return { 792 | content: [ 793 | { 794 | type: "text", 795 | text: `Product with SKU '${sku}' not found` 796 | } 797 | ], 798 | isError: true 799 | }; 800 | } 801 | 802 | // Prepare the update data with the correct structure 803 | // Magento 2 API requires a "product" wrapper object 804 | let updateData = { 805 | product: {} 806 | }; 807 | 808 | // Determine if this is a standard attribute or custom attribute 809 | const isCustomAttribute = productData.custom_attributes && 810 | productData.custom_attributes.some(attr => attr.attribute_code === attribute_code); 811 | 812 | if (isCustomAttribute) { 813 | // For custom attributes, we need to use the custom_attributes array 814 | updateData.product.custom_attributes = [ 815 | { 816 | attribute_code, 817 | value 818 | } 819 | ]; 820 | } else { 821 | // For standard attributes, we set them directly on the product object 822 | updateData.product[attribute_code] = value; 823 | } 824 | 825 | // Make the API call to update the product 826 | const result = await callMagentoApi(`/products/${sku}`, 'PUT', updateData); 827 | 828 | return { 829 | content: [ 830 | { 831 | type: "text", 832 | text: `Successfully updated '${attribute_code}' for product with SKU '${sku}'. Updated product: ${JSON.stringify(formatProduct(result), null, 2)}` 833 | } 834 | ] 835 | }; 836 | } catch (error) { 837 | return { 838 | content: [ 839 | { 840 | type: "text", 841 | text: `Error updating product attribute: ${error.response?.data?.message || error.message}` 842 | } 843 | ], 844 | isError: true 845 | }; 846 | } 847 | } 848 | ); 849 | 850 | // Tool: Get revenue 851 | server.tool( 852 | "get_revenue", 853 | "Get the total revenue for a given date range", 854 | { 855 | 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')"), 856 | status: z.string().optional().describe("Filter by order status (e.g., 'processing', 'complete', 'pending')"), 857 | include_tax: z.boolean().optional().describe("Whether to include tax in the revenue calculation (default: true)") 858 | }, 859 | async ({ date_range, status, include_tax = true }) => { 860 | try { 861 | // Parse the date range expression 862 | const dateRange = parseDateExpression(date_range); 863 | 864 | // Build the search criteria for the date range 865 | let searchCriteria = buildDateRangeFilter('created_at', dateRange.startDate, dateRange.endDate); 866 | 867 | // Add status filter if provided 868 | if (status) { 869 | searchCriteria += `&searchCriteria[filter_groups][2][filters][0][field]=status&` + 870 | `searchCriteria[filter_groups][2][filters][0][value]=${encodeURIComponent(status)}&` + 871 | `searchCriteria[filter_groups][2][filters][0][condition_type]=eq`; 872 | } 873 | 874 | // Fetch all orders using the helper function 875 | const allOrders = await fetchAllPages('/orders', searchCriteria); 876 | 877 | // Calculate total revenue 878 | let totalRevenue = 0; 879 | let totalTax = 0; 880 | let orderCount = 0; 881 | 882 | if (allOrders && Array.isArray(allOrders)) { 883 | orderCount = allOrders.length; 884 | 885 | allOrders.forEach(order => { 886 | // Use grand_total which includes tax, shipping, etc. 887 | totalRevenue += parseFloat(order.grand_total || 0); 888 | 889 | // Track tax separately 890 | totalTax += parseFloat(order.tax_amount || 0); 891 | }); 892 | } 893 | 894 | // Adjust revenue if tax should be excluded 895 | const revenueWithoutTax = totalRevenue - totalTax; 896 | const finalRevenue = include_tax ? totalRevenue : revenueWithoutTax; 897 | 898 | // Format the response 899 | const result = { 900 | query: { 901 | date_range: dateRange.description, 902 | status: status || 'All', 903 | include_tax: include_tax, 904 | period: { 905 | start_date: format(dateRange.startDate, 'yyyy-MM-dd'), 906 | end_date: format(dateRange.endDate, 'yyyy-MM-dd') 907 | } 908 | }, 909 | result: { 910 | revenue: parseFloat(finalRevenue.toFixed(2)), 911 | currency: 'USD', // This should be dynamically determined from the store configuration 912 | order_count: orderCount, 913 | average_order_value: orderCount > 0 ? parseFloat((finalRevenue / orderCount).toFixed(2)) : 0, 914 | tax_amount: parseFloat(totalTax.toFixed(2)) 915 | } 916 | }; 917 | 918 | return { 919 | content: [ 920 | { 921 | type: "text", 922 | text: JSON.stringify(result, null, 2) 923 | } 924 | ] 925 | }; 926 | } catch (error) { 927 | return { 928 | content: [ 929 | { 930 | type: "text", 931 | text: `Error fetching revenue: ${error.message}` 932 | } 933 | ], 934 | isError: true 935 | }; 936 | } 937 | } 938 | ); 939 | 940 | // Tool: Get order count 941 | server.tool( 942 | "get_order_count", 943 | "Get the number of orders for a given date range", 944 | { 945 | 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')"), 946 | status: z.string().optional().describe("Filter by order status (e.g., 'processing', 'complete', 'pending')") 947 | }, 948 | async ({ date_range, status }) => { 949 | try { 950 | // Parse the date range expression 951 | const dateRange = parseDateExpression(date_range); 952 | 953 | // Build the search criteria for the date range 954 | let searchCriteria = buildDateRangeFilter('created_at', dateRange.startDate, dateRange.endDate); 955 | 956 | // Add status filter if provided 957 | if (status) { 958 | searchCriteria += `&searchCriteria[filter_groups][2][filters][0][field]=status&` + 959 | `searchCriteria[filter_groups][2][filters][0][value]=${encodeURIComponent(status)}&` + 960 | `searchCriteria[filter_groups][2][filters][0][condition_type]=eq`; 961 | } 962 | 963 | // Add pagination to get all results 964 | searchCriteria += `&searchCriteria[pageSize]=1&searchCriteria[currentPage]=1`; 965 | 966 | // Make the API call to get orders 967 | const ordersData = await callMagentoApi(`/orders?${searchCriteria}`); 968 | 969 | // Format the response 970 | const result = { 971 | query: { 972 | date_range: dateRange.description, 973 | status: status || 'All', 974 | period: { 975 | start_date: format(dateRange.startDate, 'yyyy-MM-dd'), 976 | end_date: format(dateRange.endDate, 'yyyy-MM-dd') 977 | } 978 | }, 979 | result: { 980 | order_count: ordersData.total_count || 0 981 | } 982 | }; 983 | 984 | return { 985 | content: [ 986 | { 987 | type: "text", 988 | text: JSON.stringify(result, null, 2) 989 | } 990 | ] 991 | }; 992 | } catch (error) { 993 | return { 994 | content: [ 995 | { 996 | type: "text", 997 | text: `Error fetching order count: ${error.message}` 998 | } 999 | ], 1000 | isError: true 1001 | }; 1002 | } 1003 | } 1004 | ); 1005 | 1006 | // Tool: Get product sales 1007 | server.tool( 1008 | "get_product_sales", 1009 | "Get statistics about the quantity of products sold in a given date range", 1010 | { 1011 | 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')"), 1012 | status: z.string().optional().describe("Filter by order status (e.g., 'processing', 'complete', 'pending')"), 1013 | 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')") 1014 | }, 1015 | async ({ date_range, status, country }) => { 1016 | try { 1017 | // Parse the date range expression 1018 | const dateRange = parseDateExpression(date_range); 1019 | 1020 | // Build the search criteria for the date range 1021 | let searchCriteria = buildDateRangeFilter('created_at', dateRange.startDate, dateRange.endDate); 1022 | 1023 | // Add status filter if provided 1024 | if (status) { 1025 | searchCriteria += `&searchCriteria[filter_groups][2][filters][0][field]=status&` + 1026 | `searchCriteria[filter_groups][2][filters][0][value]=${encodeURIComponent(status)}&` + 1027 | `searchCriteria[filter_groups][2][filters][0][condition_type]=eq`; 1028 | } 1029 | 1030 | // Fetch all orders using the helper function 1031 | const allOrders = await fetchAllPages('/orders', searchCriteria); 1032 | 1033 | // Filter orders by country if provided 1034 | let filteredOrders = allOrders; 1035 | if (country) { 1036 | // Normalize country input 1037 | const normalizedCountry = normalizeCountry(country); 1038 | 1039 | // Filter orders by country 1040 | filteredOrders = filteredOrders.filter(order => { 1041 | // Check billing address country 1042 | const billingCountry = order.billing_address?.country_id; 1043 | 1044 | // Check shipping address country 1045 | const shippingCountry = order.extension_attributes?.shipping_assignments?.[0]?.shipping?.address?.country_id; 1046 | 1047 | // Match if either billing or shipping country matches 1048 | return normalizedCountry.includes(billingCountry) || normalizedCountry.includes(shippingCountry); 1049 | }); 1050 | } 1051 | 1052 | // Calculate statistics 1053 | let totalOrders = filteredOrders.length; 1054 | let totalOrderItems = 0; 1055 | let totalProductQuantity = 0; 1056 | let totalRevenue = 0; 1057 | let productCounts = {}; 1058 | 1059 | // Process each order 1060 | filteredOrders.forEach(order => { 1061 | // Add to total revenue 1062 | totalRevenue += parseFloat(order.grand_total || 0); 1063 | 1064 | // Process order items 1065 | if (order.items && Array.isArray(order.items)) { 1066 | // Count total order items (order lines) 1067 | totalOrderItems += order.items.length; 1068 | 1069 | // Process each item 1070 | order.items.forEach(item => { 1071 | // Add to total product quantity 1072 | const quantity = parseFloat(item.qty_ordered || 0); 1073 | totalProductQuantity += quantity; 1074 | 1075 | // Track product counts by SKU 1076 | const sku = item.sku; 1077 | if (sku) { 1078 | if (!productCounts[sku]) { 1079 | productCounts[sku] = { 1080 | name: item.name, 1081 | quantity: 0, 1082 | revenue: 0 1083 | }; 1084 | } 1085 | productCounts[sku].quantity += quantity; 1086 | productCounts[sku].revenue += parseFloat(item.row_total || 0); 1087 | } 1088 | }); 1089 | } 1090 | }); 1091 | 1092 | // Convert product counts to array and sort by quantity 1093 | const topProducts = Object.entries(productCounts) 1094 | .map(([sku, data]) => ({ 1095 | sku, 1096 | name: data.name, 1097 | quantity: data.quantity, 1098 | revenue: data.revenue 1099 | })) 1100 | .sort((a, b) => b.quantity - a.quantity) 1101 | .slice(0, 10); // Top 10 products 1102 | 1103 | // Format the response 1104 | const result = { 1105 | query: { 1106 | date_range: dateRange.description, 1107 | status: status || 'All', 1108 | country: country || 'All', 1109 | period: { 1110 | start_date: format(dateRange.startDate, 'yyyy-MM-dd'), 1111 | end_date: format(dateRange.endDate, 'yyyy-MM-dd') 1112 | } 1113 | }, 1114 | result: { 1115 | total_orders: totalOrders, 1116 | total_order_items: totalOrderItems, 1117 | total_product_quantity: totalProductQuantity, 1118 | average_products_per_order: totalOrders > 0 ? parseFloat((totalProductQuantity / totalOrders).toFixed(2)) : 0, 1119 | total_revenue: parseFloat(totalRevenue.toFixed(2)), 1120 | average_revenue_per_product: totalProductQuantity > 0 ? parseFloat((totalRevenue / totalProductQuantity).toFixed(2)) : 0, 1121 | top_products: topProducts 1122 | } 1123 | }; 1124 | 1125 | return { 1126 | content: [ 1127 | { 1128 | type: "text", 1129 | text: JSON.stringify(result, null, 2) 1130 | } 1131 | ] 1132 | }; 1133 | } catch (error) { 1134 | return { 1135 | content: [ 1136 | { 1137 | type: "text", 1138 | text: `Error fetching product sales: ${error.message}` 1139 | } 1140 | ], 1141 | isError: true 1142 | }; 1143 | } 1144 | } 1145 | ); 1146 | 1147 | // Tool: Get revenue by country 1148 | server.tool( 1149 | "get_revenue_by_country", 1150 | "Get revenue filtered by country for a given date range", 1151 | { 1152 | 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')"), 1153 | country: z.string().describe("Country code (e.g., 'US', 'NL', 'GB') or country name (e.g., 'United States', 'The Netherlands', 'United Kingdom')"), 1154 | status: z.string().optional().describe("Filter by order status (e.g., 'processing', 'complete', 'pending')"), 1155 | include_tax: z.boolean().optional().describe("Whether to include tax in the revenue calculation (default: true)") 1156 | }, 1157 | async ({ date_range, country, status, include_tax = true }) => { 1158 | try { 1159 | // Parse the date range expression 1160 | const dateRange = parseDateExpression(date_range); 1161 | 1162 | // Normalize country input (handle both country codes and names) 1163 | const normalizedCountry = normalizeCountry(country); 1164 | 1165 | // Build the search criteria for the date range 1166 | let searchCriteria = buildDateRangeFilter('created_at', dateRange.startDate, dateRange.endDate); 1167 | 1168 | // Add status filter if provided 1169 | if (status) { 1170 | searchCriteria += `&searchCriteria[filter_groups][2][filters][0][field]=status&` + 1171 | `searchCriteria[filter_groups][2][filters][0][value]=${encodeURIComponent(status)}&` + 1172 | `searchCriteria[filter_groups][2][filters][0][condition_type]=eq`; 1173 | } 1174 | 1175 | // Fetch all orders using the helper function 1176 | const allOrders = await fetchAllPages('/orders', searchCriteria); 1177 | 1178 | // Filter orders by country and calculate revenue 1179 | let totalRevenue = 0; 1180 | let totalTax = 0; 1181 | let orderCount = 0; 1182 | let filteredOrders = []; 1183 | 1184 | if (allOrders && Array.isArray(allOrders)) { 1185 | // Filter orders by country 1186 | filteredOrders = allOrders.filter(order => { 1187 | // Check billing address country 1188 | const billingCountry = order.billing_address?.country_id; 1189 | 1190 | // Check shipping address country 1191 | const shippingCountry = order.extension_attributes?.shipping_assignments?.[0]?.shipping?.address?.country_id; 1192 | 1193 | // Match if either billing or shipping country matches 1194 | return normalizedCountry.includes(billingCountry) || normalizedCountry.includes(shippingCountry); 1195 | }); 1196 | 1197 | orderCount = filteredOrders.length; 1198 | 1199 | // Calculate revenue for filtered orders 1200 | filteredOrders.forEach(order => { 1201 | // Use grand_total which includes tax, shipping, etc. 1202 | totalRevenue += parseFloat(order.grand_total || 0); 1203 | 1204 | // Track tax separately 1205 | totalTax += parseFloat(order.tax_amount || 0); 1206 | }); 1207 | } 1208 | 1209 | // Adjust revenue if tax should be excluded 1210 | const revenueWithoutTax = totalRevenue - totalTax; 1211 | const finalRevenue = include_tax ? totalRevenue : revenueWithoutTax; 1212 | 1213 | // Format the response 1214 | const result = { 1215 | query: { 1216 | date_range: dateRange.description, 1217 | country: country, 1218 | normalized_country: normalizedCountry.join(', '), 1219 | status: status || 'All', 1220 | include_tax: include_tax, 1221 | period: { 1222 | start_date: format(dateRange.startDate, 'yyyy-MM-dd'), 1223 | end_date: format(dateRange.endDate, 'yyyy-MM-dd') 1224 | } 1225 | }, 1226 | result: { 1227 | revenue: parseFloat(finalRevenue.toFixed(2)), 1228 | currency: 'USD', // This should be dynamically determined from the store configuration 1229 | order_count: orderCount, 1230 | average_order_value: orderCount > 0 ? parseFloat((finalRevenue / orderCount).toFixed(2)) : 0, 1231 | tax_amount: parseFloat(totalTax.toFixed(2)) 1232 | } 1233 | }; 1234 | 1235 | return { 1236 | content: [ 1237 | { 1238 | type: "text", 1239 | text: JSON.stringify(result, null, 2) 1240 | } 1241 | ] 1242 | }; 1243 | } catch (error) { 1244 | return { 1245 | content: [ 1246 | { 1247 | type: "text", 1248 | text: `Error fetching revenue by country: ${error.message}` 1249 | } 1250 | ], 1251 | isError: true 1252 | }; 1253 | } 1254 | } 1255 | ); 1256 | 1257 | // Tool: Get customer ordered products by email 1258 | server.tool( 1259 | "get_customer_ordered_products_by_email", 1260 | "Get all ordered products for a customer by email address", 1261 | { 1262 | email: z.string().email().describe("The email address of the customer") 1263 | }, 1264 | async ({ email }) => { 1265 | try { 1266 | // Step 1: Find the customer by email 1267 | const searchCriteria = `searchCriteria[filter_groups][0][filters][0][field]=email&` + 1268 | `searchCriteria[filter_groups][0][filters][0][value]=${encodeURIComponent(email)}&` + 1269 | `searchCriteria[filter_groups][0][filters][0][condition_type]=eq`; 1270 | 1271 | const customersData = await callMagentoApi(`/customers/search?${searchCriteria}`); 1272 | 1273 | if (!customersData.items || customersData.items.length === 0) { 1274 | return { 1275 | content: [ 1276 | { 1277 | type: "text", 1278 | text: `No customer found with email: ${email}` 1279 | } 1280 | ] 1281 | }; 1282 | } 1283 | 1284 | const customer = customersData.items[0]; 1285 | 1286 | // Step 2: Get the customer's orders 1287 | const orderSearchCriteria = `searchCriteria[filter_groups][0][filters][0][field]=customer_email&` + 1288 | `searchCriteria[filter_groups][0][filters][0][value]=${encodeURIComponent(email)}&` + 1289 | `searchCriteria[filter_groups][0][filters][0][condition_type]=eq`; 1290 | 1291 | // Fetch all orders for the customer using the helper function 1292 | const allCustomerOrders = await fetchAllPages('/orders', orderSearchCriteria); 1293 | 1294 | if (!allCustomerOrders || allCustomerOrders.length === 0) { 1295 | return { 1296 | content: [ 1297 | { 1298 | type: "text", 1299 | text: `No orders found for customer with email: ${email}` 1300 | } 1301 | ] 1302 | }; 1303 | } 1304 | 1305 | // Step 3: Extract and format the ordered products 1306 | const orderedProducts = []; 1307 | const productSkus = new Set(); 1308 | 1309 | // First, collect all unique product SKUs from all orders 1310 | allCustomerOrders.forEach(order => { 1311 | if (order.items && Array.isArray(order.items)) { 1312 | order.items.forEach(item => { 1313 | if (item.sku) { 1314 | productSkus.add(item.sku); 1315 | } 1316 | }); 1317 | } 1318 | }); 1319 | 1320 | // Get detailed product information for each SKU 1321 | const productPromises = Array.from(productSkus).map(sku => 1322 | callMagentoApi(`/products/${sku}`) 1323 | .then(product => formatProduct(product)) 1324 | .catch(err => ({ sku, error: err.message })) 1325 | ); 1326 | 1327 | const productDetails = await Promise.all(productPromises); 1328 | 1329 | // Create a map of SKU to product details for easy lookup 1330 | const productMap = {}; 1331 | productDetails.forEach(product => { 1332 | if (product.sku) { 1333 | productMap[product.sku] = product; 1334 | } 1335 | }); 1336 | 1337 | // Format the result with order information and product details 1338 | const result = { 1339 | customer: { 1340 | id: customer.id, 1341 | email: customer.email, 1342 | firstname: customer.firstname, 1343 | lastname: customer.lastname 1344 | }, 1345 | orders: allCustomerOrders.map(order => ({ 1346 | order_id: order.entity_id, 1347 | increment_id: order.increment_id, 1348 | created_at: order.created_at, 1349 | status: order.status, 1350 | total: order.grand_total, 1351 | items: order.items.map(item => { 1352 | const productDetail = productMap[item.sku] || {}; 1353 | return { 1354 | sku: item.sku, 1355 | name: item.name, 1356 | price: item.price, 1357 | qty_ordered: item.qty_ordered, 1358 | product_details: productDetail 1359 | }; 1360 | }) 1361 | })) 1362 | }; 1363 | 1364 | return { 1365 | content: [ 1366 | { 1367 | type: "text", 1368 | text: JSON.stringify(result, null, 2) 1369 | } 1370 | ] 1371 | }; 1372 | } catch (error) { 1373 | return { 1374 | content: [ 1375 | { 1376 | type: "text", 1377 | text: `Error fetching customer ordered products: ${error.message}` 1378 | } 1379 | ], 1380 | isError: true 1381 | }; 1382 | } 1383 | } 1384 | ); 1385 | 1386 | // Start the MCP server with stdio transport 1387 | async function main() { 1388 | try { 1389 | console.error('Starting Magento MCP Server...'); 1390 | const transport = new StdioServerTransport(); 1391 | await server.connect(transport); 1392 | console.error('Magento MCP Server running on stdio'); 1393 | } catch (error) { 1394 | console.error('Error starting MCP server:', error); 1395 | process.exit(1); 1396 | } 1397 | } 1398 | 1399 | main().catch(console.error); 1400 | ```