| """ |
| 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, |
| ) |
|
|
| |
| logging.basicConfig(level=logging.INFO) |
| logger = logging.getLogger(__name__) |
|
|
| |
| 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]}***") |
| |
| |
| 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") |
| |
| |
| action = params.get('action') if params else None |
| if not action: |
| raise ValueError("action is required in params") |
| |
| |
| hub_url = f"{DEFAULT_API_BASE}/index.php?fc=module&module=ambmcpclient&controller=api" |
| |
| |
| json_body = { |
| "client_token": client_token |
| } |
| if params: |
| json_body.update(params) |
| |
| 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() |
| |
| |
| 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", |
| "product_id": product_id, |
| "lang_id": lang_id |
| }, client_token) |
| |
| |
| product = data |
| |
| |
| 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) |
| |
| |
| 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) |
| |
| |
| 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", |
| "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() |
|
|