Files
assistant/mcp_servers/product_mcp/server.py
wangliang 7b676f8015 refactor: 将 search_spu_products 重命名为 search_products
## 修改内容

### 1. Product Agent prompt
- 将工具名从 `search_spu_products` 改为 `search_products`
- 更新所有示例代码
- 保持功能说明不变(Mall API SPU 搜索)

### 2. Product Agent 代码
**文件**: agent/agents/product.py

**修改**:
- 第 24 行:工具名改为 `search_products`
- 第 65、77 行:示例中的工具名更新
- 第 219-230 行:注入逻辑改为检查 `search_products`
- 第 284 行:工具结果检查改为 `search_products`
- 第 279-333 行:变量名 `spu_products` → `products`
- 第 280 行:`has_spu_search_result` → `has_product_search_result`

### 3. Product MCP Server
**文件**: mcp_servers/product_mcp/server.py

**修改**:
- 第 292 行:函数名 `search_spu_products` → `search_products`
- 第 300 行:文档字符串更新
- 功能完全相同,只是重命名

### 4. 移除映射逻辑
- 移除了 `search_products` → `search_spu_products` 的工具名映射
- 保留了 `query` → `keyword` 的参数映射(向后兼容)

## 好处

1. **简化命名**:`search_products` 比 `search_spu_products` 更简洁
2. **统一接口**:与系统中其他搜索工具命名一致
3. **降低复杂度**:减少名称长度和冗余

## 向后兼容

参数映射保留:
```python
# 仍然支持旧参数名
{"query": "ring"} → {"keyword": "ring"}
```

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-26 18:22:23 +08:00

408 lines
11 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
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": []
}
@mcp.tool()
async def search_products(
keyword: str,
page_size: int = 60,
page: int = 1,
user_token: str = None,
user_id: str = None,
account_id: str = None
) -> dict:
"""Search products from Mall API
从 Mall API 搜索商品 SPU根据关键词
Args:
keyword: 搜索关键词(商品名称、编号等)
page_size: 每页数量 (default: 60, max 100)
page: 页码 (default: 1)
user_token: 用户 JWT token必需用于 Mall API 认证)
user_id: 用户 ID自动注入
account_id: 账户 ID自动注入
Returns:
商品列表,包含 SPU 信息、商品图片、价格等
Product list including SPU ID, name, image, price, etc.
"""
if not user_token:
return {
"success": False,
"error": "用户未登录,请先登录账户以搜索商品",
"products": [],
"total": 0,
"require_login": True
}
try:
from shared.mall_client import MallClient
# 使用用户 token 创建 Mall 客户端
mall = MallClient(
api_url=settings.mall_api_url,
api_token=user_token,
tenant_id=settings.mall_tenant_id,
currency_code=settings.mall_currency_code,
language_id=settings.mall_language_id,
source=settings.mall_source
)
result = await mall.search_spu_products(
keyword=keyword,
page_size=page_size,
page=page
)
# 解析返回结果
products = result.get("list", [])
total = result.get("total", 0)
# 格式化商品数据
formatted_products = []
for product in products:
formatted_products.append({
"spu_id": product.get("spuId"),
"spu_sn": product.get("spuSn"),
"product_name": product.get("productName"),
"product_image": product.get("productImage"),
"price": product.get("price"),
"special_price": product.get("specialPrice"),
"stock": product.get("stock"),
"sales_count": product.get("salesCount", 0)
})
return {
"success": True,
"products": formatted_products,
"total": total,
"keyword": keyword
}
except Exception as e:
return {
"success": False,
"error": str(e),
"products": [],
"total": 0
}
finally:
# 关闭客户端
if 'client' in dir() and 'mall' in dir():
await mall.close()
# 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)