fix: 添加工具注册表和 HTTP 路由,修复 MCP 工具调用 404 错误

## 问题
Product MCP 工具调用返回 404:
```
POST /tools/search_products HTTP/1.1 404 Not Found
```

## 根本原因
1. Product MCP 缺少 `/tools/{tool_name}` HTTP 路由
2. FastMCP 的 `mcp.http_app()` 默认不暴露此路由
3. Order MCP 有自定义路由处理,Product MCP 没有

## 解决方案

### 1. 添加工具注册表
**位置**: 第 32-41 行

```python
# Tool registry for HTTP access
_tools: Dict[str, Any] = {}

def register_tool(name: str):
    """Decorator to register tool in _tools dict"""
    def decorator(func):
        _tools[name] = func
        return func
    return decorator
```

### 2. 为所有工具添加注册装饰器
**修改的工具**:
- `get_product_detail`
- `recommend_products`
- `get_quote`
- `check_inventory`
- `get_categories`
- `search_products`
- `health_check`

**示例**:
```python
@register_tool("search_products")
@mcp.tool()
async def search_products(...):
```

### 3. 添加 HTTP 路由处理
**位置**: 第 352-401 行

参考 Order MCP 实现,添加:
- `/tools/{tool_name}` POST 路由
- 工具调用逻辑:`tool_obj.run(arguments)`
- 结果提取和 JSON 解析
- 错误处理(404, 400, 500)

### 4. 配置路由列表
**位置**: 第 407-415 行

```python
routes = [
    Route('/health', health_check, methods=['GET']),
    Route('/tools/{tool_name}', execute_tool, methods=['POST'])
]
```

## 测试结果

```bash
curl -X POST http://localhost:8004/tools/search_products \
  -H "Content-Type: application/json" \
  -d '{"keyword": "ring"}'
```

返回:
```json
{
  "success": true,
  "result": {
    "success": false,
    "error": "用户未登录,请先登录账户以搜索商品",
    "products": [],
    "total": 0,
    "require_login": true
  }
}
```

 工具调用成功(user_token 缺失是预期行为)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
wangliang
2026-01-26 18:32:16 +08:00
parent ad7f30d54c
commit 74c28eb838

View File

@@ -3,15 +3,13 @@ Product MCP Server - Product search, recommendations, and quotes
"""
import sys
import os
from typing import Optional, List
from typing import Optional, List, Dict, Any
# 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
@@ -31,12 +29,24 @@ mcp = FastMCP(
"Product Service"
)
# Tool registry for HTTP access
_tools: Dict[str, Any] = {}
def register_tool(name: str):
"""Decorator to register tool in _tools dict"""
def decorator(func):
_tools[name] = func
return func
return decorator
# Hyperf client for this server
from shared.hyperf_client import HyperfClient
hyperf = HyperfClient(settings.hyperf_api_url, settings.hyperf_api_token)
@register_tool("get_product_detail")
@mcp.tool()
async def get_product_detail(
product_id: str
@@ -64,6 +74,7 @@ async def get_product_detail(
}
@register_tool("recommend_products")
@mcp.tool()
async def recommend_products(
user_id: str,
@@ -112,6 +123,7 @@ async def recommend_products(
}
@register_tool("get_quote")
@mcp.tool()
async def get_quote(
product_id: str,
@@ -171,6 +183,7 @@ async def get_quote(
}
@register_tool("check_inventory")
@mcp.tool()
async def check_inventory(
product_ids: List[str],
@@ -204,6 +217,7 @@ async def check_inventory(
}
@register_tool("get_categories")
@mcp.tool()
async def get_categories() -> dict:
"""Get product category tree
@@ -226,6 +240,7 @@ async def get_categories() -> dict:
}
@register_tool("search_products")
@mcp.tool()
async def search_products(
keyword: str,
@@ -317,6 +332,7 @@ async def search_products(
# Health check endpoint
@register_tool("health_check")
@mcp.tool()
async def health_check() -> dict:
"""Check server health status"""
@@ -329,17 +345,76 @@ async def health_check() -> dict:
if __name__ == "__main__":
import uvicorn
# Create FastAPI app from MCP
app = mcp.http_app()
# Add health endpoint
from starlette.responses import JSONResponse
from starlette.routing import Route
from starlette.requests import Request
# Custom tool execution endpoint
async def execute_tool(request: Request):
"""Execute an MCP tool via HTTP"""
tool_name = request.path_params["tool_name"]
try:
# Get arguments from request body
arguments = await request.json()
# Get tool function from registry
if tool_name not in _tools:
return JSONResponse({
"success": False,
"error": f"Tool '{tool_name}' not found"
}, status_code=404)
tool_obj = _tools[tool_name]
# Call the tool with arguments
# FastMCP FunctionTool.run() takes a dict of arguments
tool_result = await tool_obj.run(arguments)
# Extract content from ToolResult
# ToolResult.content is a list of TextContent objects with a 'text' attribute
if tool_result.content and len(tool_result.content) > 0:
content = tool_result.content[0].text
# Try to parse as JSON if possible
try:
import json
result = json.loads(content)
except:
result = content
else:
result = None
return JSONResponse({
"success": True,
"result": result
})
except TypeError as e:
return JSONResponse({
"success": False,
"error": f"Invalid arguments: {str(e)}"
}, status_code=400)
except Exception as e:
return JSONResponse({
"success": False,
"error": str(e)
}, status_code=500)
# Health check endpoint
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']))
# Create routes list
routes = [
Route('/health', health_check, methods=['GET']),
Route('/tools/{tool_name}', execute_tool, methods=['POST'])
]
# Create app from MCP with custom routes
app = mcp.http_app()
# Add our custom routes to the existing app
for route in routes:
app.router.routes.append(route)
uvicorn.run(app, host="0.0.0.0", port=8004)