""" PrestaShop MCP Server A Model Context Protocol server that provides tools for accessing PrestaShop product data. """ import asyncio import json import logging from typing import Any, Dict, List, Optional from urllib.parse import urlencode import httpx from mcp.server import Server from mcp.types import ( Resource, Tool, TextContent, ImageContent, EmbeddedResource, CallToolRequest, CallToolResult, ListToolsRequest, ListToolsResult, ListResourcesRequest, ListResourcesResult, ReadResourceRequest, ReadResourceResult, ) # Configure logging logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) # Default PrestaShop API configuration DEFAULT_API_BASE = "https://hub.mcp.integration.ambris.com" DEFAULT_TIMEOUT = 30 class PrestaShopMCPServer: """MCP Server for PrestaShop integration""" def __init__(self, api_base: str = DEFAULT_API_BASE): self.api_base = api_base.rstrip('/') self.server = Server("prestashop-mcp") self.setup_handlers() def setup_handlers(self): """Set up MCP server handlers""" @self.server.list_tools() async def list_tools() -> List[Tool]: """List available tools""" return [ Tool( name="get_product_details", description="Get detailed information about a PrestaShop product including name, description, price, and features", inputSchema={ "type": "object", "properties": { "product_id": { "type": "integer", "description": "The ID of the product to retrieve" }, "lang_id": { "type": "integer", "description": "Language ID for localized content", "default": 1 }, "api_base_url": { "type": "string", "description": "The base URL of the PrestaShop API to call (e.g., https://shop.example.com)" } }, "required": ["product_id", "api_base_url"] } ), Tool( name="get_product_features", description="Get product features and specifications from PrestaShop", inputSchema={ "type": "object", "properties": { "product_id": { "type": "integer", "description": "The ID of the product to get features for" }, "api_base_url": { "type": "string", "description": "The base URL of the PrestaShop API to call (e.g., https://shop.example.com)" } }, "required": ["product_id", "api_base_url"] } ), Tool( name="get_product_images", description="Get product images and media from PrestaShop", inputSchema={ "type": "object", "properties": { "product_id": { "type": "integer", "description": "The ID of the product to get images for" }, "api_base_url": { "type": "string", "description": "The base URL of the PrestaShop API to call (e.g., https://shop.example.com)" } }, "required": ["product_id", "api_base_url"] } ), Tool( name="search_products", description="Search for products in PrestaShop catalog", inputSchema={ "type": "object", "properties": { "query": { "type": "string", "description": "Search query term" }, "limit": { "type": "integer", "description": "Maximum number of results to return", "default": 10 }, "category_id": { "type": "integer", "description": "Optional category ID to filter results" }, "api_base_url": { "type": "string", "description": "The base URL of the PrestaShop API to call (e.g., https://shop.example.com)" } }, "required": ["query", "api_base_url"] } ) ] @self.server.call_tool() async def call_tool(name: str, arguments: Dict[str, Any]) -> List[TextContent]: """Handle tool calls""" try: if name == "get_product_details": return await self._get_product_details(arguments) elif name == "get_product_features": return await self._get_product_features(arguments) elif name == "get_product_images": return await self._get_product_images(arguments) elif name == "search_products": return await self._search_products(arguments) else: return [TextContent( type="text", text=f"Unknown tool: {name}" )] except Exception as e: logger.error(f"Error calling tool {name}: {e}") return [TextContent( type="text", text=f"Error calling tool {name}: {str(e)}" )] async def _resolve_client_token(self, client_token: str) -> str: """Resolve client token to shop URL""" if not client_token: logger.warning("No client token provided, using default API base") return DEFAULT_API_BASE try: logger.info(f"Resolving client token: {client_token[:8]}***") # Call the hub's resolve endpoint resolve_url = f"{DEFAULT_API_BASE}/index.php?fc=module&module=ambmcpsubscriptiontoken&controller=resolve" async with httpx.AsyncClient(timeout=10.0) as client: response = await client.post(resolve_url, json={ "client_token": client_token }) if response.status_code == 200: data = response.json() if data.get('success') and data.get('data', {}).get('shop_url'): shop_url = data['data']['shop_url'] logger.info(f"Token resolved to: {shop_url}") return shop_url else: logger.warning(f"Token resolution failed: {data.get('error', 'Unknown error')}") return DEFAULT_API_BASE else: logger.error(f"Token resolution request failed: {response.status_code}") return DEFAULT_API_BASE except Exception as e: logger.error(f"Error resolving client token: {e}") return DEFAULT_API_BASE async def _make_api_request(self, endpoint: str, params: Dict[str, Any] = None, client_token: str = None) -> Dict[str, Any]: """Make HTTP request to PrestaShop via Hub intermediary""" if not client_token: raise ValueError("client_token is required") # Extract action from params for JSON body action = params.get('action') if params else None if not action: raise ValueError("action is required in params") # Always call the hub - it will route to the correct agent based on token hub_url = f"{DEFAULT_API_BASE}/index.php?fc=module&module=ambmcpclient&controller=api" # Prepare JSON body with token, action and all parameters json_body = { "client_token": client_token } if params: json_body.update(params) # Include action and all other parameters async with httpx.AsyncClient(timeout=DEFAULT_TIMEOUT) as client: try: response = await client.post( hub_url, json=json_body, headers={ "Content-Type": "application/json", "Accept": "application/json", "User-Agent": "MCP-PrestaShop-Client/1.0" } ) if response.status_code != 200: error_text = response.text[:500] if response.text else "No error details" raise Exception(f"Hub API error {response.status_code}: {error_text}") data = response.json() # Handle hub API response format if data.get('error'): raise Exception(f"Hub API error: {data.get('error')}") return data except httpx.TimeoutException: raise Exception("Request timeout - Hub API took too long to respond") except httpx.HTTPStatusError as e: raise Exception(f"HTTP error {e.response.status_code}: {e.response.text}") except json.JSONDecodeError as e: raise Exception(f"Invalid JSON response from Hub API: {e}") async def _get_product_details(self, arguments: Dict[str, Any]) -> List[TextContent]: """Get detailed product information""" product_id = arguments.get("product_id") lang_id = arguments.get("lang_id", 1) client_token = arguments.get("client_token") if not product_id: return [TextContent( type="text", text="Error: product_id is required" )] if not client_token: return [TextContent( type="text", text="Error: client_token is required" )] try: data = await self._make_api_request("", { "action": "get_product", # Use existing agent API action "product_id": product_id, "lang_id": lang_id }, client_token) # Agent API returns product data directly (not wrapped in "product" key) product = data # Format the product information result = f"""PRESTASHOP PRODUCT DETAILS (ID: {product_id}) Name: {product.get('name', 'N/A')} Description: {product.get('description', 'N/A')} Short Description: {product.get('description_short', 'N/A')} Price: {product.get('price', 'N/A')} Reference: {product.get('reference', 'N/A')} EAN13: {product.get('ean13', 'N/A')} Weight: {product.get('weight', 'N/A')} Quantity: {product.get('quantity', 'N/A')} Active: {product.get('active', 'N/A')} Available for Order: {product.get('available_for_order', 'N/A')} Categories: {json.dumps(product.get('categories', []), indent=2)} Features: {json.dumps(product.get('features', []), indent=2)} Images: {json.dumps(product.get('images', []), indent=2)} Full Product Data: {json.dumps(product, indent=2)}""" return [TextContent(type="text", text=result)] except Exception as e: return [TextContent( type="text", text=f"Error retrieving product {product_id}: {str(e)}" )] async def _get_product_features(self, arguments: Dict[str, Any]) -> List[TextContent]: """Get product features""" product_id = arguments.get("product_id") client_token = arguments.get("client_token") if not product_id: return [TextContent( type="text", text="Error: product_id is required" )] if not client_token: return [TextContent( type="text", text="Error: client_token is required" )] try: data = await self._make_api_request("", { "action": "get_product", "product_id": product_id }, client_token) # Agent API returns product data directly product = data features = product.get("features", []) if not features: result = f"No features found for product {product_id}" else: result = f"PRESTASHOP PRODUCT FEATURES (ID: {product_id}):\n\n" result += json.dumps(features, indent=2) return [TextContent(type="text", text=result)] except Exception as e: return [TextContent( type="text", text=f"Error retrieving features for product {product_id}: {str(e)}" )] async def _get_product_images(self, arguments: Dict[str, Any]) -> List[TextContent]: """Get product images""" product_id = arguments.get("product_id") client_token = arguments.get("client_token") if not product_id: return [TextContent( type="text", text="Error: product_id is required" )] if not client_token: return [TextContent( type="text", text="Error: client_token is required" )] try: data = await self._make_api_request("", { "action": "get_product", "product_id": product_id }, client_token) # Agent API returns product data directly product = data images = product.get("images", []) if not images: result = f"No images found for product {product_id}" else: result = f"PRESTASHOP PRODUCT IMAGES (ID: {product_id}):\n\n" result += json.dumps(images, indent=2) return [TextContent(type="text", text=result)] except Exception as e: return [TextContent( type="text", text=f"Error retrieving images for product {product_id}: {str(e)}" )] async def _search_products(self, arguments: Dict[str, Any]) -> List[TextContent]: """Search for products""" query = arguments.get("query") limit = arguments.get("limit", 10) category_id = arguments.get("category_id") client_token = arguments.get("client_token") if not query: return [TextContent( type="text", text="Error: query is required" )] if not client_token: return [TextContent( type="text", text="Error: client_token is required" )] try: params = { "action": "getStats", # Use existing agent API action "query": query, "limit": limit } if category_id: params["category_id"] = category_id data = await self._make_api_request("", params, client_token) products = data.get("products", []) if not products: result = f"No products found for query: '{query}'" else: result = f"PRESTASHOP PRODUCT SEARCH RESULTS for '{query}':\n\n" result += json.dumps(products, indent=2) return [TextContent(type="text", text=result)] except Exception as e: return [TextContent( type="text", text=f"Error searching for products with query '{query}': {str(e)}" )] async def run(self, transport_type: str = "stdio"): """Run the MCP server""" if transport_type == "stdio": from mcp.server.stdio import stdio_server async with stdio_server() as (read_stream, write_stream): await self.server.run( read_stream, write_stream, self.server.create_initialization_options() ) else: raise ValueError(f"Unsupported transport type: {transport_type}") def main(): """Main entry point""" import argparse parser = argparse.ArgumentParser(description="PrestaShop MCP Server") parser.add_argument( "--api-base", default=DEFAULT_API_BASE, help="PrestaShop API base URL" ) parser.add_argument( "--transport", choices=["stdio"], default="stdio", help="Transport mechanism" ) args = parser.parse_args() server = PrestaShopMCPServer(api_base=args.api_base) try: asyncio.run(server.run(args.transport)) except KeyboardInterrupt: logger.info("Server stopped by user") except Exception as e: logger.error(f"Server error: {e}") raise if __name__ == "__main__": main()