feat: 初始化 B2B AI Shopping Assistant 项目
- 配置 Docker Compose 多服务编排 - 实现 Chatwoot + Agent 集成 - 配置 Strapi MCP 知识库 - 支持 7 种语言的 FAQ 系统 - 实现 LangGraph AI 工作流 Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
317
mcp_servers/product_mcp/server.py
Normal file
317
mcp_servers/product_mcp/server.py
Normal file
@@ -0,0 +1,317 @@
|
||||
"""
|
||||
Product MCP Server - Product search, recommendations, and quotes
|
||||
"""
|
||||
import sys
|
||||
import os
|
||||
from typing import Optional, List
|
||||
|
||||
# Add shared module to path
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
|
||||
from fastmcp import FastMCP
|
||||
from pydantic_settings import BaseSettings
|
||||
|
||||
|
||||
from pydantic import ConfigDict
|
||||
|
||||
|
||||
class Settings(BaseSettings):
|
||||
"""Server configuration"""
|
||||
hyperf_api_url: str
|
||||
hyperf_api_token: str
|
||||
log_level: str = "INFO"
|
||||
|
||||
model_config = ConfigDict(env_file=".env")
|
||||
|
||||
|
||||
settings = Settings()
|
||||
|
||||
# Create MCP server
|
||||
mcp = FastMCP(
|
||||
"Product Service"
|
||||
)
|
||||
|
||||
|
||||
# Hyperf client for this server
|
||||
from shared.hyperf_client import HyperfClient
|
||||
hyperf = HyperfClient(settings.hyperf_api_url, settings.hyperf_api_token)
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
async def search_products(
|
||||
query: str,
|
||||
category: Optional[str] = None,
|
||||
brand: Optional[str] = None,
|
||||
price_min: Optional[float] = None,
|
||||
price_max: Optional[float] = None,
|
||||
sort: str = "relevance",
|
||||
page: int = 1,
|
||||
page_size: int = 20
|
||||
) -> dict:
|
||||
"""Search products
|
||||
|
||||
Args:
|
||||
query: Search keywords
|
||||
category: Category filter
|
||||
brand: Brand filter
|
||||
price_min: Minimum price filter
|
||||
price_max: Maximum price filter
|
||||
sort: Sort order (relevance, price_asc, price_desc, sales, latest)
|
||||
page: Page number (default: 1)
|
||||
page_size: Items per page (default: 20)
|
||||
|
||||
Returns:
|
||||
List of matching products
|
||||
"""
|
||||
payload = {
|
||||
"query": query,
|
||||
"sort": sort,
|
||||
"page": page,
|
||||
"page_size": page_size,
|
||||
"filters": {}
|
||||
}
|
||||
|
||||
if category:
|
||||
payload["filters"]["category"] = category
|
||||
if brand:
|
||||
payload["filters"]["brand"] = brand
|
||||
if price_min is not None or price_max is not None:
|
||||
payload["filters"]["price_range"] = {}
|
||||
if price_min is not None:
|
||||
payload["filters"]["price_range"]["min"] = price_min
|
||||
if price_max is not None:
|
||||
payload["filters"]["price_range"]["max"] = price_max
|
||||
|
||||
try:
|
||||
result = await hyperf.post("/products/search", json=payload)
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"products": result.get("products", []),
|
||||
"total": result.get("total", 0),
|
||||
"pagination": result.get("pagination", {})
|
||||
}
|
||||
except Exception as e:
|
||||
return {
|
||||
"success": False,
|
||||
"error": str(e),
|
||||
"products": []
|
||||
}
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
async def get_product_detail(
|
||||
product_id: str
|
||||
) -> dict:
|
||||
"""Get product details
|
||||
|
||||
Args:
|
||||
product_id: Product ID
|
||||
|
||||
Returns:
|
||||
Detailed product information including specifications, pricing, and stock
|
||||
"""
|
||||
try:
|
||||
result = await hyperf.get(f"/products/{product_id}")
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"product": result
|
||||
}
|
||||
except Exception as e:
|
||||
return {
|
||||
"success": False,
|
||||
"error": str(e),
|
||||
"product": None
|
||||
}
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
async def recommend_products(
|
||||
user_id: str,
|
||||
account_id: str,
|
||||
context: Optional[dict] = None,
|
||||
strategy: str = "hybrid",
|
||||
limit: int = 10
|
||||
) -> dict:
|
||||
"""Get personalized product recommendations
|
||||
|
||||
Args:
|
||||
user_id: User identifier
|
||||
account_id: B2B account identifier
|
||||
context: Optional context for recommendations:
|
||||
- current_query: Current search query
|
||||
- recent_views: List of recently viewed product IDs
|
||||
- cart_items: Items in cart
|
||||
strategy: Recommendation strategy (collaborative, content_based, hybrid)
|
||||
limit: Maximum recommendations to return (default: 10)
|
||||
|
||||
Returns:
|
||||
List of recommended products with reasons
|
||||
"""
|
||||
payload = {
|
||||
"user_id": user_id,
|
||||
"account_id": account_id,
|
||||
"strategy": strategy,
|
||||
"limit": limit
|
||||
}
|
||||
|
||||
if context:
|
||||
payload["context"] = context
|
||||
|
||||
try:
|
||||
result = await hyperf.post("/products/recommend", json=payload)
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"recommendations": result.get("recommendations", [])
|
||||
}
|
||||
except Exception as e:
|
||||
return {
|
||||
"success": False,
|
||||
"error": str(e),
|
||||
"recommendations": []
|
||||
}
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
async def get_quote(
|
||||
product_id: str,
|
||||
quantity: int,
|
||||
account_id: str,
|
||||
delivery_province: Optional[str] = None,
|
||||
delivery_city: Optional[str] = None
|
||||
) -> dict:
|
||||
"""Get B2B price quote
|
||||
|
||||
Args:
|
||||
product_id: Product ID
|
||||
quantity: Desired quantity
|
||||
account_id: B2B account ID (for customer-specific pricing)
|
||||
delivery_province: Delivery province (for shipping calculation)
|
||||
delivery_city: Delivery city (for shipping calculation)
|
||||
|
||||
Returns:
|
||||
Detailed quote with unit price, discounts, tax, and shipping
|
||||
"""
|
||||
payload = {
|
||||
"product_id": product_id,
|
||||
"quantity": quantity,
|
||||
"account_id": account_id
|
||||
}
|
||||
|
||||
if delivery_province or delivery_city:
|
||||
payload["delivery_address"] = {}
|
||||
if delivery_province:
|
||||
payload["delivery_address"]["province"] = delivery_province
|
||||
if delivery_city:
|
||||
payload["delivery_address"]["city"] = delivery_city
|
||||
|
||||
try:
|
||||
result = await hyperf.post("/products/quote", json=payload)
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"quote_id": result.get("quote_id"),
|
||||
"product_id": product_id,
|
||||
"quantity": quantity,
|
||||
"unit_price": result.get("unit_price"),
|
||||
"subtotal": result.get("subtotal"),
|
||||
"discount": result.get("discount", 0),
|
||||
"discount_reason": result.get("discount_reason"),
|
||||
"tax": result.get("tax"),
|
||||
"shipping_fee": result.get("shipping_fee"),
|
||||
"total_price": result.get("total_price"),
|
||||
"validity": result.get("validity"),
|
||||
"payment_terms": result.get("payment_terms"),
|
||||
"estimated_delivery": result.get("estimated_delivery")
|
||||
}
|
||||
except Exception as e:
|
||||
return {
|
||||
"success": False,
|
||||
"error": str(e)
|
||||
}
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
async def check_inventory(
|
||||
product_ids: List[str],
|
||||
warehouse: Optional[str] = None
|
||||
) -> dict:
|
||||
"""Check product inventory/stock
|
||||
|
||||
Args:
|
||||
product_ids: List of product IDs to check
|
||||
warehouse: Specific warehouse to check (optional)
|
||||
|
||||
Returns:
|
||||
Inventory status for each product
|
||||
"""
|
||||
payload = {"product_ids": product_ids}
|
||||
if warehouse:
|
||||
payload["warehouse"] = warehouse
|
||||
|
||||
try:
|
||||
result = await hyperf.post("/products/inventory/check", json=payload)
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"inventory": result.get("inventory", [])
|
||||
}
|
||||
except Exception as e:
|
||||
return {
|
||||
"success": False,
|
||||
"error": str(e),
|
||||
"inventory": []
|
||||
}
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
async def get_categories() -> dict:
|
||||
"""Get product category tree
|
||||
|
||||
Returns:
|
||||
Hierarchical category structure
|
||||
"""
|
||||
try:
|
||||
result = await hyperf.get("/products/categories")
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"categories": result.get("categories", [])
|
||||
}
|
||||
except Exception as e:
|
||||
return {
|
||||
"success": False,
|
||||
"error": str(e),
|
||||
"categories": []
|
||||
}
|
||||
|
||||
|
||||
# Health check endpoint
|
||||
@mcp.tool()
|
||||
async def health_check() -> dict:
|
||||
"""Check server health status"""
|
||||
return {
|
||||
"status": "healthy",
|
||||
"service": "product_mcp",
|
||||
"version": "1.0.0"
|
||||
}
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import uvicorn
|
||||
|
||||
# Create FastAPI app from MCP
|
||||
app = mcp.http_app()
|
||||
|
||||
# Add health endpoint
|
||||
from starlette.responses import JSONResponse
|
||||
async def health_check(request):
|
||||
return JSONResponse({"status": "healthy"})
|
||||
|
||||
# Add the route to the app
|
||||
from starlette.routing import Route
|
||||
app.router.routes.append(Route('/health', health_check, methods=['GET']))
|
||||
|
||||
uvicorn.run(app, host="0.0.0.0", port=8004)
|
||||
Reference in New Issue
Block a user