server.py•12.8 kB
"""PrestaShop Documentation MCP Server."""
import logging
from typing import List, Optional
from fastmcp import FastMCP
from .ingest import get_hook, list_hooks, search_hooks
from .ingest_v2 import index_documentation_v2
from .search_v2 import search_documentation, get_document, list_documents, get_stats
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
# Initialize FastMCP server
mcp = FastMCP("PrestaShop Docs")
@mcp.tool()
def search_prestashop_hooks(
queries: List[str],
hook_type: Optional[str] = None,
origin: Optional[str] = None,
) -> str:
"""Search PrestaShop hooks using full-text search.
Args:
queries: List of search terms (maximum 3)
hook_type: Filter by hook type (display, action)
origin: Filter by origin (core, module, theme)
Returns:
Formatted search results with hook details
"""
if not queries:
return "Error: Please provide at least one search query"
# Limit to 3 queries
queries = queries[:3]
logger.info(f"Searching hooks for: {queries}")
results = search_hooks(queries, hook_type=hook_type, origin=origin, limit=10)
if not results:
filters = []
if hook_type:
filters.append(f"type={hook_type}")
if origin:
filters.append(f"origin={origin}")
filter_str = f" (filters: {', '.join(filters)})" if filters else ""
return f"No results found for: {', '.join(queries)}{filter_str}"
# Format results
output = [f"Found {len(results)} hooks for: {', '.join(queries)}\n"]
for i, hook in enumerate(results, 1):
output.append(f"{i}. **{hook['name']}** ({hook['type']} hook)")
output.append(f" Origin: {hook['origin']}")
output.append(f" Locations: {hook['locations']}")
if hook.get("description"):
desc = hook["description"][:150]
if len(hook["description"]) > 150:
desc += "..."
output.append(f" Description: {desc}")
if hook.get("aliases"):
output.append(f" Aliases: {', '.join(hook['aliases'])}")
if hook.get("snippet"):
output.append(f" {hook['snippet']}")
output.append("")
return "\n".join(output)
@mcp.tool()
def get_prestashop_hook(hook_name: str) -> str:
"""Get complete documentation for a specific PrestaShop hook.
Args:
hook_name: Name of the hook (e.g., 'displayHeader', 'actionProductAdd')
Returns:
Complete hook documentation including description, parameters, examples
"""
logger.info(f"Getting hook: {hook_name}")
hook = get_hook(hook_name)
if not hook:
return f"Hook '{hook_name}' not found. Use list_prestashop_hooks() to see available hooks."
# Format hook documentation
output = [f"# {hook['name']}\n"]
output.append(f"**Type:** {hook['type']}")
output.append(f"**Origin:** {hook['origin']}")
output.append(f"**Locations:** {hook['locations']}\n")
if hook.get("description"):
output.append(f"## Description\n")
output.append(f"{hook['description']}\n")
if hook.get("aliases"):
output.append(f"## Aliases\n")
for alias in hook["aliases"]:
output.append(f"- {alias}")
output.append("")
if hook.get("github_refs"):
output.append(f"## Source Files\n")
for ref in hook["github_refs"]:
output.append(f"- {ref}")
output.append("")
if hook.get("code_examples"):
output.append(f"## Code Examples\n")
for i, example in enumerate(hook["code_examples"], 1):
output.append(f"### Example {i}\n")
output.append(f"```php\n{example}\n```\n")
# Add full markdown content
if hook.get("content"):
output.append("## Full Documentation\n")
output.append(hook["content"])
return "\n".join(output)
@mcp.tool()
def list_prestashop_hooks(
hook_type: Optional[str] = None, origin: Optional[str] = None
) -> str:
"""List all available PrestaShop hooks.
Args:
hook_type: Filter by type (display, action)
origin: Filter by origin (core, module, theme)
Returns:
List of all hooks organized by type and origin
"""
logger.info(f"Listing hooks (type={hook_type}, origin={origin})")
hooks = list_hooks(hook_type=hook_type, origin=origin)
if not hooks:
return "No hooks found matching the filters"
# Organize hooks by type
display_hooks = [h for h in hooks if h["type"] == "display"]
action_hooks = [h for h in hooks if h["type"] == "action"]
other_hooks = [h for h in hooks if h["type"] not in ["display", "action"]]
output = [f"Available PrestaShop Hooks ({len(hooks)} total)\n"]
if display_hooks:
output.append(f"## Display Hooks ({len(display_hooks)})\n")
for hook in display_hooks[:20]: # Limit to first 20
desc = hook.get("description", "")
if desc:
desc = desc[:80] + "..." if len(desc) > 80 else desc
output.append(f"- **{hook['name']}** ({hook['origin']}) - {desc}")
else:
output.append(f"- **{hook['name']}** ({hook['origin']})")
if len(display_hooks) > 20:
output.append(f"\n ... and {len(display_hooks) - 20} more display hooks")
output.append("")
if action_hooks:
output.append(f"## Action Hooks ({len(action_hooks)})\n")
for hook in action_hooks[:20]: # Limit to first 20
desc = hook.get("description", "")
if desc:
desc = desc[:80] + "..." if len(desc) > 80 else desc
output.append(f"- **{hook['name']}** ({hook['origin']}) - {desc}")
else:
output.append(f"- **{hook['name']}** ({hook['origin']})")
if len(action_hooks) > 20:
output.append(f"\n ... and {len(action_hooks) - 20} more action hooks")
output.append("")
if other_hooks:
output.append(f"## Other Hooks ({len(other_hooks)})\n")
for hook in other_hooks:
output.append(f"- **{hook['name']}** ({hook['origin']}, {hook['type']})")
output.append("")
output.append(
"\nUse get_prestashop_hook('hook_name') to get detailed documentation for a specific hook."
)
return "\n".join(output)
@mcp.tool()
def search_prestashop_docs(
query: str,
doc_type: Optional[str] = None,
category: Optional[str] = None
) -> str:
"""Search ALL PrestaShop documentation (guides, tutorials, API docs, hooks, etc.).
Args:
query: Search query (e.g., "install", "Mac", "deployment")
doc_type: Filter by type: hook, guide, tutorial, api, reference, component, faq, general
category: Filter by category: basics, development, modules, themes, etc.
Returns:
Search results with snippets and metadata
"""
logger.info(f"Searching documentation for: {query} (type={doc_type}, category={category})")
results = search_documentation(query, doc_type=doc_type, category=category, limit=10)
if not results:
filters = []
if doc_type:
filters.append(f"type={doc_type}")
if category:
filters.append(f"category={category}")
filter_str = f" (filters: {', '.join(filters)})" if filters else ""
return f"No results found for: {query}{filter_str}"
# Format results
output = [f"Found {len(results)} documents for: {query}\n"]
for i, doc in enumerate(results, 1):
output.append(f"{i}. **{doc['title']}** ({doc['doc_type']})")
output.append(f" Category: {doc['category']}")
if doc.get('subcategory'):
output.append(f" Subcategory: {doc['subcategory']}")
output.append(f" Path: {doc['path']}")
if doc.get('snippet'):
output.append(f" {doc['snippet']}")
output.append("")
output.append("\nUse get_prestashop_doc('path') to get the full document content.")
return "\n".join(output)
@mcp.tool()
def get_prestashop_doc(path: str) -> str:
"""Get the full content of a specific PrestaShop documentation file.
Args:
path: Document path (e.g., 'basics/installation/environments/macos-specific.md')
Returns:
Full document content with metadata
"""
logger.info(f"Getting document: {path}")
doc = get_document(path)
if not doc:
return f"Document '{path}' not found. Use search_prestashop_docs() to find documents."
# Format document
output = [f"# {doc['title']}\n"]
output.append(f"**Type:** {doc['doc_type']}")
output.append(f"**Category:** {doc['category']}")
if doc.get('subcategory'):
output.append(f"**Subcategory:** {doc['subcategory']}")
if doc.get('version'):
output.append(f"**Version:** {doc['version']}")
output.append(f"**Path:** {doc['path']}\n")
output.append("---\n")
output.append(doc['content'])
return "\n".join(output)
@mcp.tool()
def list_prestashop_docs(
doc_type: Optional[str] = None,
category: Optional[str] = None
) -> str:
"""List PrestaShop documentation files.
Args:
doc_type: Filter by type: hook, guide, tutorial, api, reference, component, faq, general
category: Filter by category: basics, development, modules, themes, etc.
Returns:
List of available documents
"""
logger.info(f"Listing documents (type={doc_type}, category={category})")
docs = list_documents(doc_type=doc_type, category=category, limit=50)
if not docs:
return "No documents found matching the filters"
# Group by category
by_category = {}
for doc in docs:
cat = doc['category']
if cat not in by_category:
by_category[cat] = []
by_category[cat].append(doc)
output = [f"Available PrestaShop Documentation ({len(docs)} documents)\n"]
for category, category_docs in sorted(by_category.items()):
output.append(f"## {category.title()} ({len(category_docs)} docs)\n")
for doc in category_docs[:20]: # Limit to 20 per category
subcat = f" / {doc['subcategory']}" if doc.get('subcategory') else ""
output.append(f"- **{doc['title']}** ({doc['doc_type']}){subcat}")
output.append(f" Path: {doc['path']}")
if len(category_docs) > 20:
output.append(f"\n ... and {len(category_docs) - 20} more documents")
output.append("")
return "\n".join(output)
@mcp.tool()
def get_prestashop_stats() -> str:
"""Get statistics about indexed PrestaShop documentation.
Returns:
Statistics about documents, types, categories, and specialized data
"""
logger.info("Getting documentation statistics")
stats = get_stats()
output = ["# PrestaShop Documentation Statistics\n"]
output.append(f"**Total Documents:** {stats['total_documents']}\n")
output.append("## By Document Type\n")
for doc_type, count in sorted(stats['by_type'].items(), key=lambda x: x[1], reverse=True):
output.append(f"- {doc_type}: {count}")
output.append("")
output.append("## By Category\n")
for category, count in sorted(stats['by_category'].items(), key=lambda x: x[1], reverse=True):
output.append(f"- {category}: {count}")
output.append("")
output.append("## Specialized Data\n")
output.append(f"- Domain References (CQRS): {stats['domain_references']}")
output.append(f"- UI Components: {stats['components']}")
return "\n".join(output)
def main():
"""Main entry point for the MCP server."""
import os
# Index documentation on startup if not already indexed
logger.info("Initializing PrestaShop Documentation MCP Server...")
try:
count = index_documentation_v2(force=False)
logger.info(f"Documentation indexed: {count} documents")
except Exception as e:
logger.error(f"Error indexing documentation: {e}")
logger.info("Server will start but search may not work properly")
# Check for transport configuration
transport = os.getenv("PRESTASHOP_TRANSPORT", "stdio").lower()
if transport in ["http", "sse"]:
# HTTP/SSE transport for remote deployment
host = os.getenv("PRESTASHOP_HOST", "0.0.0.0")
port = int(os.getenv("PRESTASHOP_PORT", "8765"))
logger.info(f"Starting MCP server with {transport.upper()} transport on {host}:{port}")
mcp.run(transport=transport, host=host, port=port)
else:
# Default stdio transport for local/CLI usage
logger.info("Starting MCP server with STDIO transport")
mcp.run()
if __name__ == "__main__":
main()