1067 lines
36 KiB
Python
1067 lines
36 KiB
Python
"""
|
||
MCP Server for Financial Data Search
|
||
基于FastAPI的MCP服务端,整合多个金融数据搜索API
|
||
支持LLM调用和Web聊天功能
|
||
"""
|
||
|
||
from fastapi import FastAPI, HTTPException, Request
|
||
from fastapi.middleware.cors import CORSMiddleware
|
||
from fastapi.responses import JSONResponse
|
||
from pydantic import BaseModel, Field
|
||
from typing import List, Dict, Any, Optional, Literal
|
||
from datetime import datetime, date
|
||
import logging
|
||
import httpx
|
||
from enum import Enum
|
||
import mcp_database as db
|
||
|
||
# 配置日志
|
||
logging.basicConfig(level=logging.INFO)
|
||
logger = logging.getLogger(__name__)
|
||
|
||
# 创建FastAPI应用
|
||
app = FastAPI(
|
||
title="Financial Data MCP Server",
|
||
description="Model Context Protocol server for financial data search and analysis",
|
||
version="1.0.0"
|
||
)
|
||
|
||
# 添加CORS中间件
|
||
app.add_middleware(
|
||
CORSMiddleware,
|
||
allow_origins=["*"],
|
||
allow_credentials=True,
|
||
allow_methods=["*"],
|
||
allow_headers=["*"],
|
||
)
|
||
|
||
# ==================== 配置 ====================
|
||
|
||
class ServiceEndpoints:
|
||
"""API服务端点配置"""
|
||
NEWS_API = "http://222.128.1.157:21891" # 新闻API
|
||
ROADSHOW_API = "http://222.128.1.157:19800" # 路演API
|
||
CONCEPT_API = "http://localhost:6801" # 概念API(本地)
|
||
STOCK_ANALYSIS_API = "http://222.128.1.157:8811" # 涨停分析+研报API
|
||
|
||
# HTTP客户端配置
|
||
HTTP_CLIENT = httpx.AsyncClient(timeout=60.0)
|
||
|
||
# ==================== MCP协议数据模型 ====================
|
||
|
||
class ToolParameter(BaseModel):
|
||
"""工具参数定义"""
|
||
type: str
|
||
description: str
|
||
enum: Optional[List[str]] = None
|
||
default: Optional[Any] = None
|
||
|
||
class ToolDefinition(BaseModel):
|
||
"""工具定义"""
|
||
name: str
|
||
description: str
|
||
parameters: Dict[str, Dict[str, Any]]
|
||
required: List[str] = []
|
||
|
||
class ToolCallRequest(BaseModel):
|
||
"""工具调用请求"""
|
||
tool: str
|
||
arguments: Dict[str, Any] = {}
|
||
|
||
class ToolCallResponse(BaseModel):
|
||
"""工具调用响应"""
|
||
success: bool
|
||
data: Optional[Any] = None
|
||
error: Optional[str] = None
|
||
metadata: Optional[Dict[str, Any]] = None
|
||
|
||
# ==================== MCP工具定义 ====================
|
||
|
||
TOOLS: List[ToolDefinition] = [
|
||
ToolDefinition(
|
||
name="search_news",
|
||
description="搜索全球新闻,支持关键词搜索和日期过滤。适用于查找国际新闻、行业动态等。",
|
||
parameters={
|
||
"type": "object",
|
||
"properties": {
|
||
"query": {
|
||
"type": "string",
|
||
"description": "搜索关键词,例如:'人工智能'、'新能源汽车'"
|
||
},
|
||
"source": {
|
||
"type": "string",
|
||
"description": "新闻来源筛选,可选"
|
||
},
|
||
"start_date": {
|
||
"type": "string",
|
||
"description": "开始日期,格式:YYYY-MM-DD"
|
||
},
|
||
"end_date": {
|
||
"type": "string",
|
||
"description": "结束日期,格式:YYYY-MM-DD"
|
||
},
|
||
"top_k": {
|
||
"type": "integer",
|
||
"description": "返回结果数量,默认20",
|
||
"default": 20
|
||
}
|
||
},
|
||
"required": ["query"]
|
||
}
|
||
),
|
||
ToolDefinition(
|
||
name="search_china_news",
|
||
description="搜索中国新闻,使用KNN语义搜索。支持精确匹配模式,适合查找股票、公司相关新闻。",
|
||
parameters={
|
||
"type": "object",
|
||
"properties": {
|
||
"query": {
|
||
"type": "string",
|
||
"description": "搜索关键词"
|
||
},
|
||
"exact_match": {
|
||
"type": "boolean",
|
||
"description": "是否精确匹配(用于股票代码、公司名称等),默认false",
|
||
"default": False
|
||
},
|
||
"source": {
|
||
"type": "string",
|
||
"description": "新闻来源筛选"
|
||
},
|
||
"start_date": {
|
||
"type": "string",
|
||
"description": "开始日期,格式:YYYY-MM-DD"
|
||
},
|
||
"end_date": {
|
||
"type": "string",
|
||
"description": "结束日期,格式:YYYY-MM-DD"
|
||
},
|
||
"top_k": {
|
||
"type": "integer",
|
||
"description": "返回结果数量,默认20",
|
||
"default": 20
|
||
}
|
||
},
|
||
"required": ["query"]
|
||
}
|
||
),
|
||
ToolDefinition(
|
||
name="search_medical_news",
|
||
description="搜索医疗健康类新闻,包括医药、医疗设备、生物技术等领域。",
|
||
parameters={
|
||
"type": "object",
|
||
"properties": {
|
||
"query": {
|
||
"type": "string",
|
||
"description": "搜索关键词"
|
||
},
|
||
"source": {
|
||
"type": "string",
|
||
"description": "新闻来源"
|
||
},
|
||
"start_date": {
|
||
"type": "string",
|
||
"description": "开始日期,格式:YYYY-MM-DD"
|
||
},
|
||
"end_date": {
|
||
"type": "string",
|
||
"description": "结束日期,格式:YYYY-MM-DD"
|
||
},
|
||
"top_k": {
|
||
"type": "integer",
|
||
"description": "返回结果数量",
|
||
"default": 10
|
||
}
|
||
},
|
||
"required": ["query"]
|
||
}
|
||
),
|
||
ToolDefinition(
|
||
name="search_roadshows",
|
||
description="搜索上市公司路演、投资者交流活动记录。可按公司代码、日期范围搜索。",
|
||
parameters={
|
||
"type": "object",
|
||
"properties": {
|
||
"query": {
|
||
"type": "string",
|
||
"description": "搜索关键词,可以是公司名称、主题等"
|
||
},
|
||
"company_code": {
|
||
"type": "string",
|
||
"description": "公司股票代码,例如:'600519.SH'"
|
||
},
|
||
"start_date": {
|
||
"type": "string",
|
||
"description": "开始日期,格式:YYYY-MM-DD 或 YYYY-MM-DD HH:MM:SS"
|
||
},
|
||
"end_date": {
|
||
"type": "string",
|
||
"description": "结束日期,格式:YYYY-MM-DD 或 YYYY-MM-DD HH:MM:SS"
|
||
},
|
||
"size": {
|
||
"type": "integer",
|
||
"description": "返回结果数量",
|
||
"default": 10
|
||
}
|
||
},
|
||
"required": ["query"]
|
||
}
|
||
),
|
||
ToolDefinition(
|
||
name="search_concepts",
|
||
description="搜索股票概念板块,支持按涨跌幅、股票数量排序。返回概念详情及相关股票列表。",
|
||
parameters={
|
||
"type": "object",
|
||
"properties": {
|
||
"query": {
|
||
"type": "string",
|
||
"description": "搜索关键词,例如:'新能源'、'人工智能'"
|
||
},
|
||
"size": {
|
||
"type": "integer",
|
||
"description": "每页结果数量",
|
||
"default": 10
|
||
},
|
||
"page": {
|
||
"type": "integer",
|
||
"description": "页码",
|
||
"default": 1
|
||
},
|
||
"sort_by": {
|
||
"type": "string",
|
||
"description": "排序方式:change_pct(涨跌幅), _score(相关度), stock_count(股票数), concept_name(名称)",
|
||
"enum": ["change_pct", "_score", "stock_count", "concept_name"],
|
||
"default": "change_pct"
|
||
},
|
||
"trade_date": {
|
||
"type": "string",
|
||
"description": "交易日期,格式:YYYY-MM-DD,默认最新"
|
||
}
|
||
},
|
||
"required": ["query"]
|
||
}
|
||
),
|
||
ToolDefinition(
|
||
name="get_concept_details",
|
||
description="根据概念ID获取详细信息,包括描述、相关股票、涨跌幅数据等。",
|
||
parameters={
|
||
"type": "object",
|
||
"properties": {
|
||
"concept_id": {
|
||
"type": "string",
|
||
"description": "概念ID"
|
||
},
|
||
"trade_date": {
|
||
"type": "string",
|
||
"description": "交易日期,格式:YYYY-MM-DD"
|
||
}
|
||
},
|
||
"required": ["concept_id"]
|
||
}
|
||
),
|
||
ToolDefinition(
|
||
name="get_stock_concepts",
|
||
description="查询指定股票的所有相关概念板块,包括涨跌幅信息。",
|
||
parameters={
|
||
"type": "object",
|
||
"properties": {
|
||
"stock_code": {
|
||
"type": "string",
|
||
"description": "股票代码或名称"
|
||
},
|
||
"size": {
|
||
"type": "integer",
|
||
"description": "返回概念数量",
|
||
"default": 50
|
||
},
|
||
"sort_by": {
|
||
"type": "string",
|
||
"description": "排序方式",
|
||
"enum": ["stock_count", "concept_name", "recent"],
|
||
"default": "stock_count"
|
||
},
|
||
"trade_date": {
|
||
"type": "string",
|
||
"description": "交易日期,格式:YYYY-MM-DD"
|
||
}
|
||
},
|
||
"required": ["stock_code"]
|
||
}
|
||
),
|
||
ToolDefinition(
|
||
name="get_concept_statistics",
|
||
description="获取概念板块统计数据,包括涨幅榜、跌幅榜、活跃榜、波动榜、连涨榜。",
|
||
parameters={
|
||
"type": "object",
|
||
"properties": {
|
||
"days": {
|
||
"type": "integer",
|
||
"description": "统计天数(与start_date/end_date互斥)"
|
||
},
|
||
"start_date": {
|
||
"type": "string",
|
||
"description": "开始日期,格式:YYYY-MM-DD"
|
||
},
|
||
"end_date": {
|
||
"type": "string",
|
||
"description": "结束日期,格式:YYYY-MM-DD"
|
||
},
|
||
"min_stock_count": {
|
||
"type": "integer",
|
||
"description": "最少股票数量过滤",
|
||
"default": 3
|
||
}
|
||
},
|
||
"required": []
|
||
}
|
||
),
|
||
ToolDefinition(
|
||
name="search_limit_up_stocks",
|
||
description="搜索涨停股票,支持按日期、关键词、板块等条件搜索。包括混合语义搜索。",
|
||
parameters={
|
||
"type": "object",
|
||
"properties": {
|
||
"query": {
|
||
"type": "string",
|
||
"description": "搜索关键词(涨停原因、公司名称等)"
|
||
},
|
||
"date": {
|
||
"type": "string",
|
||
"description": "日期,格式:YYYYMMDD"
|
||
},
|
||
"mode": {
|
||
"type": "string",
|
||
"description": "搜索模式",
|
||
"enum": ["hybrid", "text", "vector"],
|
||
"default": "hybrid"
|
||
},
|
||
"sectors": {
|
||
"type": "array",
|
||
"items": {"type": "string"},
|
||
"description": "板块筛选"
|
||
},
|
||
"page_size": {
|
||
"type": "integer",
|
||
"description": "每页结果数",
|
||
"default": 20
|
||
}
|
||
},
|
||
"required": ["query"]
|
||
}
|
||
),
|
||
ToolDefinition(
|
||
name="get_daily_stock_analysis",
|
||
description="获取指定日期的涨停股票分析,包括板块分析、词云、趋势图表等。",
|
||
parameters={
|
||
"type": "object",
|
||
"properties": {
|
||
"date": {
|
||
"type": "string",
|
||
"description": "日期,格式:YYYYMMDD"
|
||
}
|
||
},
|
||
"required": ["date"]
|
||
}
|
||
),
|
||
ToolDefinition(
|
||
name="search_research_reports",
|
||
description="搜索研究报告,支持文本和语义混合搜索。可按作者、证券、日期等筛选。",
|
||
parameters={
|
||
"type": "object",
|
||
"properties": {
|
||
"query": {
|
||
"type": "string",
|
||
"description": "搜索关键词"
|
||
},
|
||
"mode": {
|
||
"type": "string",
|
||
"description": "搜索模式",
|
||
"enum": ["hybrid", "text", "vector"],
|
||
"default": "hybrid"
|
||
},
|
||
"exact_match": {
|
||
"type": "string",
|
||
"description": "是否精确匹配:0=模糊,1=精确",
|
||
"enum": ["0", "1"],
|
||
"default": "0"
|
||
},
|
||
"security_code": {
|
||
"type": "string",
|
||
"description": "证券代码筛选"
|
||
},
|
||
"start_date": {
|
||
"type": "string",
|
||
"description": "开始日期,格式:YYYY-MM-DD"
|
||
},
|
||
"end_date": {
|
||
"type": "string",
|
||
"description": "结束日期,格式:YYYY-MM-DD"
|
||
},
|
||
"size": {
|
||
"type": "integer",
|
||
"description": "返回结果数量",
|
||
"default": 10
|
||
}
|
||
},
|
||
"required": ["query"]
|
||
}
|
||
),
|
||
ToolDefinition(
|
||
name="get_stock_basic_info",
|
||
description="获取股票基本信息,包括公司名称、行业、地址、主营业务、高管等基础数据。",
|
||
parameters={
|
||
"type": "object",
|
||
"properties": {
|
||
"seccode": {
|
||
"type": "string",
|
||
"description": "股票代码,例如:600519"
|
||
}
|
||
},
|
||
"required": ["seccode"]
|
||
}
|
||
),
|
||
ToolDefinition(
|
||
name="get_stock_financial_index",
|
||
description="获取股票财务指标,包括每股收益、净资产收益率、营收增长率等关键财务数据。",
|
||
parameters={
|
||
"type": "object",
|
||
"properties": {
|
||
"seccode": {
|
||
"type": "string",
|
||
"description": "股票代码"
|
||
},
|
||
"start_date": {
|
||
"type": "string",
|
||
"description": "开始日期,格式:YYYY-MM-DD"
|
||
},
|
||
"end_date": {
|
||
"type": "string",
|
||
"description": "结束日期,格式:YYYY-MM-DD"
|
||
},
|
||
"limit": {
|
||
"type": "integer",
|
||
"description": "返回条数,默认10",
|
||
"default": 10
|
||
}
|
||
},
|
||
"required": ["seccode"]
|
||
}
|
||
),
|
||
ToolDefinition(
|
||
name="get_stock_trade_data",
|
||
description="获取股票交易数据,包括价格、成交量、涨跌幅、换手率等日线行情数据。",
|
||
parameters={
|
||
"type": "object",
|
||
"properties": {
|
||
"seccode": {
|
||
"type": "string",
|
||
"description": "股票代码"
|
||
},
|
||
"start_date": {
|
||
"type": "string",
|
||
"description": "开始日期,格式:YYYY-MM-DD"
|
||
},
|
||
"end_date": {
|
||
"type": "string",
|
||
"description": "结束日期,格式:YYYY-MM-DD"
|
||
},
|
||
"limit": {
|
||
"type": "integer",
|
||
"description": "返回条数,默认30",
|
||
"default": 30
|
||
}
|
||
},
|
||
"required": ["seccode"]
|
||
}
|
||
),
|
||
ToolDefinition(
|
||
name="get_stock_balance_sheet",
|
||
description="获取股票资产负债表,包括资产、负债、所有者权益等财务状况数据。",
|
||
parameters={
|
||
"type": "object",
|
||
"properties": {
|
||
"seccode": {
|
||
"type": "string",
|
||
"description": "股票代码"
|
||
},
|
||
"start_date": {
|
||
"type": "string",
|
||
"description": "开始日期,格式:YYYY-MM-DD"
|
||
},
|
||
"end_date": {
|
||
"type": "string",
|
||
"description": "结束日期,格式:YYYY-MM-DD"
|
||
},
|
||
"limit": {
|
||
"type": "integer",
|
||
"description": "返回条数,默认8",
|
||
"default": 8
|
||
}
|
||
},
|
||
"required": ["seccode"]
|
||
}
|
||
),
|
||
ToolDefinition(
|
||
name="get_stock_cashflow",
|
||
description="获取股票现金流量表,包括经营、投资、筹资活动现金流数据。",
|
||
parameters={
|
||
"type": "object",
|
||
"properties": {
|
||
"seccode": {
|
||
"type": "string",
|
||
"description": "股票代码"
|
||
},
|
||
"start_date": {
|
||
"type": "string",
|
||
"description": "开始日期,格式:YYYY-MM-DD"
|
||
},
|
||
"end_date": {
|
||
"type": "string",
|
||
"description": "结束日期,格式:YYYY-MM-DD"
|
||
},
|
||
"limit": {
|
||
"type": "integer",
|
||
"description": "返回条数,默认8",
|
||
"default": 8
|
||
}
|
||
},
|
||
"required": ["seccode"]
|
||
}
|
||
),
|
||
ToolDefinition(
|
||
name="search_stocks_by_criteria",
|
||
description="按条件搜索股票,支持按行业、地区、市值等条件筛选股票列表。",
|
||
parameters={
|
||
"type": "object",
|
||
"properties": {
|
||
"industry": {
|
||
"type": "string",
|
||
"description": "行业名称,支持模糊匹配"
|
||
},
|
||
"province": {
|
||
"type": "string",
|
||
"description": "省份名称"
|
||
},
|
||
"min_market_cap": {
|
||
"type": "number",
|
||
"description": "最小市值(亿元)"
|
||
},
|
||
"max_market_cap": {
|
||
"type": "number",
|
||
"description": "最大市值(亿元)"
|
||
},
|
||
"limit": {
|
||
"type": "integer",
|
||
"description": "返回条数,默认50",
|
||
"default": 50
|
||
}
|
||
},
|
||
"required": []
|
||
}
|
||
),
|
||
ToolDefinition(
|
||
name="get_stock_comparison",
|
||
description="股票对比分析,支持多只股票的财务指标或交易数据对比。",
|
||
parameters={
|
||
"type": "object",
|
||
"properties": {
|
||
"seccodes": {
|
||
"type": "array",
|
||
"items": {"type": "string"},
|
||
"description": "股票代码列表,至少2个"
|
||
},
|
||
"metric": {
|
||
"type": "string",
|
||
"description": "对比指标类型",
|
||
"enum": ["financial", "trade"],
|
||
"default": "financial"
|
||
}
|
||
},
|
||
"required": ["seccodes"]
|
||
}
|
||
),
|
||
]
|
||
|
||
# ==================== MCP协议端点 ====================
|
||
|
||
@app.get("/")
|
||
async def root():
|
||
"""服务根端点"""
|
||
return {
|
||
"name": "Financial Data MCP Server",
|
||
"version": "1.0.0",
|
||
"protocol": "MCP",
|
||
"description": "Model Context Protocol server for financial data search and analysis"
|
||
}
|
||
|
||
@app.get("/tools")
|
||
async def list_tools():
|
||
"""列出所有可用工具"""
|
||
return {
|
||
"tools": [tool.dict() for tool in TOOLS]
|
||
}
|
||
|
||
@app.get("/tools/{tool_name}")
|
||
async def get_tool(tool_name: str):
|
||
"""获取特定工具的定义"""
|
||
tool = next((t for t in TOOLS if t.name == tool_name), None)
|
||
if not tool:
|
||
raise HTTPException(status_code=404, detail=f"Tool '{tool_name}' not found")
|
||
return tool.dict()
|
||
|
||
@app.post("/tools/call")
|
||
async def call_tool(request: ToolCallRequest):
|
||
"""调用工具"""
|
||
logger.info(f"Tool call: {request.tool} with args: {request.arguments}")
|
||
|
||
try:
|
||
# 路由到对应的工具处理函数
|
||
handler = TOOL_HANDLERS.get(request.tool)
|
||
if not handler:
|
||
raise HTTPException(status_code=404, detail=f"Tool '{request.tool}' not found")
|
||
|
||
result = await handler(request.arguments)
|
||
|
||
return ToolCallResponse(
|
||
success=True,
|
||
data=result,
|
||
metadata={
|
||
"tool": request.tool,
|
||
"timestamp": datetime.now().isoformat()
|
||
}
|
||
)
|
||
|
||
except Exception as e:
|
||
logger.error(f"Tool call error: {str(e)}", exc_info=True)
|
||
return ToolCallResponse(
|
||
success=False,
|
||
error=str(e),
|
||
metadata={
|
||
"tool": request.tool,
|
||
"timestamp": datetime.now().isoformat()
|
||
}
|
||
)
|
||
|
||
# ==================== 工具处理函数 ====================
|
||
|
||
async def handle_search_news(args: Dict[str, Any]) -> Any:
|
||
"""处理新闻搜索"""
|
||
params = {
|
||
"query": args.get("query"),
|
||
"source": args.get("source"),
|
||
"start_date": args.get("start_date"),
|
||
"end_date": args.get("end_date"),
|
||
"top_k": args.get("top_k", 20)
|
||
}
|
||
# 移除None值
|
||
params = {k: v for k, v in params.items() if v is not None}
|
||
|
||
response = await HTTP_CLIENT.get(f"{ServiceEndpoints.NEWS_API}/search_news", params=params)
|
||
response.raise_for_status()
|
||
return response.json()
|
||
|
||
async def handle_search_china_news(args: Dict[str, Any]) -> Any:
|
||
"""处理中国新闻搜索"""
|
||
params = {
|
||
"query": args.get("query"),
|
||
"exact_match": args.get("exact_match", False),
|
||
"source": args.get("source"),
|
||
"start_date": args.get("start_date"),
|
||
"end_date": args.get("end_date"),
|
||
"top_k": args.get("top_k", 20)
|
||
}
|
||
params = {k: v for k, v in params.items() if v is not None}
|
||
|
||
response = await HTTP_CLIENT.get(f"{ServiceEndpoints.NEWS_API}/search_china_news", params=params)
|
||
response.raise_for_status()
|
||
return response.json()
|
||
|
||
async def handle_search_medical_news(args: Dict[str, Any]) -> Any:
|
||
"""处理医疗新闻搜索"""
|
||
params = {
|
||
"query": args["query"],
|
||
"source": args.get("source"),
|
||
"start_date": args.get("start_date"),
|
||
"end_date": args.get("end_date"),
|
||
"top_k": args.get("top_k", 10)
|
||
}
|
||
params = {k: v for k, v in params.items() if v is not None}
|
||
|
||
response = await HTTP_CLIENT.get(f"{ServiceEndpoints.NEWS_API}/search_medical_news", params=params)
|
||
response.raise_for_status()
|
||
return response.json()
|
||
|
||
async def handle_search_roadshows(args: Dict[str, Any]) -> Any:
|
||
"""处理路演搜索"""
|
||
params = {
|
||
"query": args["query"],
|
||
"company_code": args.get("company_code"),
|
||
"start_date": args.get("start_date"),
|
||
"end_date": args.get("end_date"),
|
||
"size": args.get("size", 10)
|
||
}
|
||
params = {k: v for k, v in params.items() if v is not None}
|
||
|
||
response = await HTTP_CLIENT.get(f"{ServiceEndpoints.ROADSHOW_API}/search", params=params)
|
||
response.raise_for_status()
|
||
return response.json()
|
||
|
||
async def handle_search_concepts(args: Dict[str, Any]) -> Any:
|
||
"""处理概念搜索"""
|
||
payload = {
|
||
"query": args["query"],
|
||
"size": args.get("size", 10),
|
||
"page": args.get("page", 1),
|
||
"search_size": 100,
|
||
"sort_by": args.get("sort_by", "change_pct"),
|
||
"use_knn": True
|
||
}
|
||
if args.get("trade_date"):
|
||
payload["trade_date"] = args["trade_date"]
|
||
|
||
response = await HTTP_CLIENT.post(f"{ServiceEndpoints.CONCEPT_API}/search", json=payload)
|
||
response.raise_for_status()
|
||
return response.json()
|
||
|
||
async def handle_get_concept_details(args: Dict[str, Any]) -> Any:
|
||
"""处理概念详情获取"""
|
||
concept_id = args["concept_id"]
|
||
params = {}
|
||
if args.get("trade_date"):
|
||
params["trade_date"] = args["trade_date"]
|
||
|
||
response = await HTTP_CLIENT.get(
|
||
f"{ServiceEndpoints.CONCEPT_API}/concept/{concept_id}",
|
||
params=params
|
||
)
|
||
response.raise_for_status()
|
||
return response.json()
|
||
|
||
async def handle_get_stock_concepts(args: Dict[str, Any]) -> Any:
|
||
"""处理股票概念获取"""
|
||
stock_code = args["stock_code"]
|
||
params = {
|
||
"size": args.get("size", 50),
|
||
"sort_by": args.get("sort_by", "stock_count"),
|
||
"include_description": True
|
||
}
|
||
if args.get("trade_date"):
|
||
params["trade_date"] = args["trade_date"]
|
||
|
||
response = await HTTP_CLIENT.get(
|
||
f"{ServiceEndpoints.CONCEPT_API}/stock/{stock_code}/concepts",
|
||
params=params
|
||
)
|
||
response.raise_for_status()
|
||
return response.json()
|
||
|
||
async def handle_get_concept_statistics(args: Dict[str, Any]) -> Any:
|
||
"""处理概念统计获取"""
|
||
params = {}
|
||
if args.get("days"):
|
||
params["days"] = args["days"]
|
||
if args.get("start_date"):
|
||
params["start_date"] = args["start_date"]
|
||
if args.get("end_date"):
|
||
params["end_date"] = args["end_date"]
|
||
if args.get("min_stock_count"):
|
||
params["min_stock_count"] = args["min_stock_count"]
|
||
|
||
response = await HTTP_CLIENT.get(f"{ServiceEndpoints.CONCEPT_API}/statistics", params=params)
|
||
response.raise_for_status()
|
||
return response.json()
|
||
|
||
async def handle_search_limit_up_stocks(args: Dict[str, Any]) -> Any:
|
||
"""处理涨停股票搜索"""
|
||
payload = {
|
||
"query": args["query"],
|
||
"mode": args.get("mode", "hybrid"),
|
||
"page_size": args.get("page_size", 20)
|
||
}
|
||
if args.get("date"):
|
||
payload["date"] = args["date"]
|
||
if args.get("sectors"):
|
||
payload["sectors"] = args["sectors"]
|
||
|
||
response = await HTTP_CLIENT.post(
|
||
f"{ServiceEndpoints.STOCK_ANALYSIS_API}/api/v1/stocks/search/hybrid",
|
||
json=payload
|
||
)
|
||
response.raise_for_status()
|
||
return response.json()
|
||
|
||
async def handle_get_daily_stock_analysis(args: Dict[str, Any]) -> Any:
|
||
"""处理每日股票分析获取"""
|
||
date = args["date"]
|
||
response = await HTTP_CLIENT.get(
|
||
f"{ServiceEndpoints.STOCK_ANALYSIS_API}/api/v1/analysis/daily/{date}"
|
||
)
|
||
response.raise_for_status()
|
||
return response.json()
|
||
|
||
async def handle_search_research_reports(args: Dict[str, Any]) -> Any:
|
||
"""处理研报搜索"""
|
||
params = {
|
||
"query": args["query"],
|
||
"mode": args.get("mode", "hybrid"),
|
||
"exact_match": args.get("exact_match", "0"),
|
||
"size": args.get("size", 10)
|
||
}
|
||
if args.get("security_code"):
|
||
params["security_code"] = args["security_code"]
|
||
if args.get("start_date"):
|
||
params["start_date"] = args["start_date"]
|
||
if args.get("end_date"):
|
||
params["end_date"] = args["end_date"]
|
||
|
||
response = await HTTP_CLIENT.get(f"{ServiceEndpoints.STOCK_ANALYSIS_API}/search", params=params)
|
||
response.raise_for_status()
|
||
return response.json()
|
||
|
||
async def handle_get_stock_basic_info(args: Dict[str, Any]) -> Any:
|
||
"""处理股票基本信息查询"""
|
||
seccode = args["seccode"]
|
||
result = await db.get_stock_basic_info(seccode)
|
||
if result:
|
||
return {"success": True, "data": result}
|
||
else:
|
||
return {"success": False, "error": f"未找到股票代码 {seccode} 的信息"}
|
||
|
||
async def handle_get_stock_financial_index(args: Dict[str, Any]) -> Any:
|
||
"""处理股票财务指标查询"""
|
||
seccode = args["seccode"]
|
||
start_date = args.get("start_date")
|
||
end_date = args.get("end_date")
|
||
limit = args.get("limit", 10)
|
||
|
||
result = await db.get_stock_financial_index(seccode, start_date, end_date, limit)
|
||
return {
|
||
"success": True,
|
||
"data": result,
|
||
"count": len(result)
|
||
}
|
||
|
||
async def handle_get_stock_trade_data(args: Dict[str, Any]) -> Any:
|
||
"""处理股票交易数据查询"""
|
||
seccode = args["seccode"]
|
||
start_date = args.get("start_date")
|
||
end_date = args.get("end_date")
|
||
limit = args.get("limit", 30)
|
||
|
||
result = await db.get_stock_trade_data(seccode, start_date, end_date, limit)
|
||
return {
|
||
"success": True,
|
||
"data": result,
|
||
"count": len(result)
|
||
}
|
||
|
||
async def handle_get_stock_balance_sheet(args: Dict[str, Any]) -> Any:
|
||
"""处理资产负债表查询"""
|
||
seccode = args["seccode"]
|
||
start_date = args.get("start_date")
|
||
end_date = args.get("end_date")
|
||
limit = args.get("limit", 8)
|
||
|
||
result = await db.get_stock_balance_sheet(seccode, start_date, end_date, limit)
|
||
return {
|
||
"success": True,
|
||
"data": result,
|
||
"count": len(result)
|
||
}
|
||
|
||
async def handle_get_stock_cashflow(args: Dict[str, Any]) -> Any:
|
||
"""处理现金流量表查询"""
|
||
seccode = args["seccode"]
|
||
start_date = args.get("start_date")
|
||
end_date = args.get("end_date")
|
||
limit = args.get("limit", 8)
|
||
|
||
result = await db.get_stock_cashflow(seccode, start_date, end_date, limit)
|
||
return {
|
||
"success": True,
|
||
"data": result,
|
||
"count": len(result)
|
||
}
|
||
|
||
async def handle_search_stocks_by_criteria(args: Dict[str, Any]) -> Any:
|
||
"""处理按条件搜索股票"""
|
||
industry = args.get("industry")
|
||
province = args.get("province")
|
||
min_market_cap = args.get("min_market_cap")
|
||
max_market_cap = args.get("max_market_cap")
|
||
limit = args.get("limit", 50)
|
||
|
||
result = await db.search_stocks_by_criteria(
|
||
industry, province, min_market_cap, max_market_cap, limit
|
||
)
|
||
return {
|
||
"success": True,
|
||
"data": result,
|
||
"count": len(result)
|
||
}
|
||
|
||
async def handle_get_stock_comparison(args: Dict[str, Any]) -> Any:
|
||
"""处理股票对比分析"""
|
||
seccodes = args["seccodes"]
|
||
metric = args.get("metric", "financial")
|
||
|
||
result = await db.get_stock_comparison(seccodes, metric)
|
||
return {
|
||
"success": True,
|
||
"data": result
|
||
}
|
||
|
||
# 工具处理函数映射
|
||
TOOL_HANDLERS = {
|
||
"search_news": handle_search_news,
|
||
"search_china_news": handle_search_china_news,
|
||
"search_medical_news": handle_search_medical_news,
|
||
"search_roadshows": handle_search_roadshows,
|
||
"search_concepts": handle_search_concepts,
|
||
"get_concept_details": handle_get_concept_details,
|
||
"get_stock_concepts": handle_get_stock_concepts,
|
||
"get_concept_statistics": handle_get_concept_statistics,
|
||
"search_limit_up_stocks": handle_search_limit_up_stocks,
|
||
"get_daily_stock_analysis": handle_get_daily_stock_analysis,
|
||
"search_research_reports": handle_search_research_reports,
|
||
"get_stock_basic_info": handle_get_stock_basic_info,
|
||
"get_stock_financial_index": handle_get_stock_financial_index,
|
||
"get_stock_trade_data": handle_get_stock_trade_data,
|
||
"get_stock_balance_sheet": handle_get_stock_balance_sheet,
|
||
"get_stock_cashflow": handle_get_stock_cashflow,
|
||
"search_stocks_by_criteria": handle_search_stocks_by_criteria,
|
||
"get_stock_comparison": handle_get_stock_comparison,
|
||
}
|
||
|
||
# ==================== Web聊天接口 ====================
|
||
|
||
class ChatMessage(BaseModel):
|
||
"""聊天消息"""
|
||
role: Literal["user", "assistant", "system"]
|
||
content: str
|
||
|
||
class ChatRequest(BaseModel):
|
||
"""聊天请求"""
|
||
messages: List[ChatMessage]
|
||
stream: bool = False
|
||
|
||
@app.post("/chat")
|
||
async def chat(request: ChatRequest):
|
||
"""
|
||
Web聊天接口
|
||
|
||
这是一个简化的接口,实际应该集成LLM API(如OpenAI、Claude等)
|
||
这里只是演示如何使用工具
|
||
"""
|
||
# TODO: 集成实际的LLM API
|
||
# 1. 将消息发送给LLM
|
||
# 2. LLM返回需要调用的工具
|
||
# 3. 调用工具并获取结果
|
||
# 4. 将工具结果返回给LLM
|
||
# 5. LLM生成最终回复
|
||
|
||
return {
|
||
"message": "Chat endpoint placeholder - integrate with your LLM provider",
|
||
"available_tools": len(TOOLS),
|
||
"hint": "Use POST /tools/call to invoke tools"
|
||
}
|
||
|
||
# ==================== 健康检查 ====================
|
||
|
||
@app.get("/health")
|
||
async def health_check():
|
||
"""健康检查"""
|
||
# 检查各个后端服务的健康状态
|
||
services_status = {}
|
||
|
||
try:
|
||
response = await HTTP_CLIENT.get(f"{ServiceEndpoints.NEWS_API}/search_news?query=test&top_k=1", timeout=5.0)
|
||
services_status["news_api"] = "healthy" if response.status_code == 200 else "unhealthy"
|
||
except:
|
||
services_status["news_api"] = "unhealthy"
|
||
|
||
try:
|
||
response = await HTTP_CLIENT.get(f"{ServiceEndpoints.CONCEPT_API}/", timeout=5.0)
|
||
services_status["concept_api"] = "healthy" if response.status_code == 200 else "unhealthy"
|
||
except:
|
||
services_status["concept_api"] = "unhealthy"
|
||
|
||
try:
|
||
response = await HTTP_CLIENT.get(f"{ServiceEndpoints.STOCK_ANALYSIS_API}/api/v1/health", timeout=5.0)
|
||
services_status["stock_analysis_api"] = "healthy" if response.status_code == 200 else "unhealthy"
|
||
except:
|
||
services_status["stock_analysis_api"] = "unhealthy"
|
||
|
||
return {
|
||
"status": "healthy",
|
||
"timestamp": datetime.now().isoformat(),
|
||
"services": services_status
|
||
}
|
||
|
||
# ==================== 错误处理 ====================
|
||
|
||
@app.exception_handler(HTTPException)
|
||
async def http_exception_handler(request: Request, exc: HTTPException):
|
||
"""HTTP异常处理"""
|
||
return JSONResponse(
|
||
status_code=exc.status_code,
|
||
content={
|
||
"success": False,
|
||
"error": exc.detail,
|
||
"timestamp": datetime.now().isoformat()
|
||
}
|
||
)
|
||
|
||
@app.exception_handler(Exception)
|
||
async def general_exception_handler(request: Request, exc: Exception):
|
||
"""通用异常处理"""
|
||
logger.error(f"Unexpected error: {str(exc)}", exc_info=True)
|
||
return JSONResponse(
|
||
status_code=500,
|
||
content={
|
||
"success": False,
|
||
"error": "Internal server error",
|
||
"detail": str(exc),
|
||
"timestamp": datetime.now().isoformat()
|
||
}
|
||
)
|
||
|
||
# ==================== 应用启动/关闭 ====================
|
||
|
||
@app.on_event("startup")
|
||
async def startup_event():
|
||
"""应用启动"""
|
||
logger.info("MCP Server starting up...")
|
||
logger.info(f"Registered {len(TOOLS)} tools")
|
||
# 初始化数据库连接池
|
||
try:
|
||
await db.get_pool()
|
||
logger.info("MySQL connection pool initialized")
|
||
except Exception as e:
|
||
logger.error(f"Failed to initialize MySQL pool: {e}")
|
||
|
||
@app.on_event("shutdown")
|
||
async def shutdown_event():
|
||
"""应用关闭"""
|
||
logger.info("MCP Server shutting down...")
|
||
await HTTP_CLIENT.aclose()
|
||
# 关闭数据库连接池
|
||
try:
|
||
await db.close_pool()
|
||
logger.info("MySQL connection pool closed")
|
||
except Exception as e:
|
||
logger.error(f"Failed to close MySQL pool: {e}")
|
||
|
||
# ==================== 主程序 ====================
|
||
|
||
if __name__ == "__main__":
|
||
import uvicorn
|
||
|
||
uvicorn.run(
|
||
"mcp_server:app",
|
||
host="0.0.0.0",
|
||
port=8900,
|
||
reload=True,
|
||
log_level="info"
|
||
)
|