前言
前面学习了 什么是 MCP,及其基本使用方式。那么今天就再用一个更接近实际场景。 想象一下你的老板特别喜欢炒股,当他听到你在 Agent 方面还有点子技术。 那么,是不是能做一个简单的 Agent 来帮助老板分析股票呢? 这时候,关于股票信息的获取、分析等本身不是 LLM 的能力,就需要外挂 MCP 来实现了。
项目背景
这是一个基于 FastAPI-MCP 的股票分析 MCP 服务。
功能亮点:
-
基于 FastAPI-MCP 构建: 支持 SSE 协议,提供强大的 API 接口。
-
丰富的股票分析工具: 提供多种股票数据获取和分析工具函数。
-
多市场支持: 支持 A 股、港股、美股及基金数据分析。
-
全维度分析:提供股票基础价格、技术指标(MA/RSI/MACD/ 布林带)、综合评分、AI 深度分析等。
-
集成 AI 分析: 结合大语言模型进行深度分析。
技术亮点:
- 异步处理: 利用 FastAPI 和 Python 的异步特性,提高并发处理能力。
- 流式响应: 支持 SSE 协议,实现实时数据流推送。
- 模块化设计: 各个服务模块职责明确,便于维护和扩展。
- 依赖注入: 使用 FastAPI 的依赖注入系统,简化代码结构。
- 全面错误处理: 包含详细的异常捕获和错误响应机制。
- MCP 工具集成: 使用 FastApiMCP 库自动将 API 转换为 MCP 工具函数。
特别鸣谢
由于我个人并不炒股也不是很懂,所以很多股票相关的代码来自开源软件 stock-scanner 在此,十分感谢🫶🫶🫶。
- services目录:基本都是 stock-scanner 代码,主要提供股票分析服务
本项目旨在通过 MCP 工具函数接口提供股票相关的综合数据和分析能力,包括价格、评分、技术报告和 AI 分析。 该服务整合了原有项目stock-scanner 中的股票数据获取和分析功能,并提供了现代化的 API 接口,作为前端与后端服务之间的桥梁。我们自己实现的核心主要是:
-
stock_server:MCP 服务端
-
stock_server:MCP 客户端
-
model:LLM 客户端
核心技术
-
FastAPI、FastAPI-MCP
-
LLM
-
MCP
-
asyncio (异步支持)
-
loguru/logger
-
langchain_mcp_adapters
项目架构
核心流程图
FastAPI-MCP
定义
FastAPI-MCP 是一个用于 “自动扫描与 MCP 协议转换” 的 Python 标准库。
-
自动发现:当你用
FastApiMCP(app)包装你的 FastAPI 应用时,它会像“扫描仪”一样,自动遍历你应用中所有已注册的路由(Endpoint)。 -
元数据提取:它会读取每个路由的 Pydantic 请求/响应模型、类型注解、依赖注入(如认证
Depends)以及接口的文档字符串(docstrings)。 -
协议转换:将这些信息实时转换为符合模型上下文协议(Model Context Protocol, MCP) 规范的工具描述。
-
服务暴露:通过
mcp.mount(),它在你的FastAPI应用中挂载一个额外的MCP服务器端点(默认在/mcp路径)。AI客户端(如Claude Desktop)通过SSE(Server-Sent Events)连接到此端点,就能发现并调用你所有的API工具。
创建
# 创建FastAPI应用
app = FastAPI(
title="股票分析 MCP 服务",
description="基于 FastAPI-MCP 的股票分析服务,提供股票数据、技术分析和AI分析功能"
)
# 创建MCP服务
# 【必须放在所有路由、接口定义 之后!!】
# 【最后几行才写这个】
mcp = FastApiMCP(
app,
name="股票分析MCP服务",
description="基于 FastAPI-MCP 的股票分析服务,提供股票数据、技术分析和AI分析功能"
)
挂载
官方标准
mcp.mount():这是最推荐的挂载方式,自动挂载
# 挂载MCP服务,自动挂载。一定要写在所有路由设置之后!!!
# 把 MCP 标准接口(/mcp、/mcp/tools、/mcp/call-tool)自动注册到 FastAPI 里!
mcp.mount()
app手动注册
# 手动挂载所有 MCP 标准路由
app.include_router(mcp.router, prefix="/mcp")
底层手动注册
@app.get("/mcp/tools") def get_mcp_tools(): return [你的工具列表]
和 FastMCP 对比
我们知道FastMCP是一个完整的、用于从头构建MCP服务器和客户端的 Python 原生框架。我们在 MCP 基础里就是用这种框架进行构建的。
| 维度 | FastAPI-MCP | FastMCP |
|---|---|---|
| 定位 | FastAPI到MCP的自动转换器(适配现有API) | MCP原生开发框架(从头构建MCP服务) |
| 核心逻辑 | 自动扫描FastAPI路由,提取元数据转换为MCP工具 | 用装饰器(@mcp.tool等)定义Python函数,生成MCP组件(工具/资源/提示) |
| 工作流 | 已有FastAPI应用→包装→自动生成MCP接口 | 定义Python函数→标记装饰器→自动生成MCP服务器 |
| 设计哲学 | API First, AI Enablement(复用存量API,最小化集成成本) | MCP First(围绕MCP协议设计,追求协议完整性) |
| 应用场景 | 1. 存量FastAPI服务快速AI化;2. 继承现有安全/业务逻辑;3. 保持API与MCP单一事实来源 | 1. 开发全新AI原生服务/工具;2. 深度定制MCP高级功能(资源/提示);3. 极致协议控制力 |
Coding
Service
这里代码主要来自 stock-scanner,所以具体实现就不做过多赘述,感兴趣的可以去 git 自己看下。(在此再次感谢 stock-scanner 作者!!!)
简单总结下,我们这里用到的服务:
-
StockDataProvider: 负责从数据源获取股票数据。
-
StockAnalyzerService: 协调数据提供、指标计算、评分和 AI 分析。
-
AIAnalyzer: 调用大语言模型 API 进行深度分析。
-
TechnicalIndicator: 计算技术指标。
-
StockScorer: 根据技术指标评分。
LLM Client
def get_lc_o_model_client(api_key=os.getenv(ALI_TONGYI_API_KEY_OS_VAR_NAME),
base_url=ALI_TONGYI_URL,
model=ALI_TONGYI_MAX_MODEL, temperature = 0.7, verbose=False, debug=False):
'''
以OpenAI兼容的方式,通过LangChain获得指定平台和模型的客户端
可以通过传入api_key,base_url,model,temperature四个参数来覆盖默认值
verbose,debug两个参数,分别控制是否输出调试信息,是否输出详细调试信息,默认不打印
:return: 指定平台和模型的客户端,默认平台和模型为阿里百炼qwen-max-latest,温度=0.7
'''
function_name = inspect.currentframe().f_code.co_name
if(verbose):
print(f"{function_name}-平台:{base_url},模型:{model},温度:{temperature}")
if(debug):
print(f"{function_name}-平台:{base_url},模型:{model},温度:{temperature},key:{api_key}")
return ChatOpenAI(api_key=api_key, base_url=base_url,model=model,temperature=temperature)
Logger
Logger 模块主要用 loguru 代替系统繁琐的 Log 类。这里我们主要实现了:
- 分 Level记录日志
- Info:控制台输出
- Debug:调试日志保存
- Error:错误日志保存
可以简单看下,运行后我这里现成保存好的日志:
# 添加标准输出处理器(控制台)
logger.add(
sys.stdout,
format="<green>{time:YYYY-MM-DD HH:mm:ss.SSS}</green> | <level>{level: <8}</level> | <cyan>{name}</cyan>:<cyan>{line}</cyan> - <level>{message}</level>",
level="INFO", # 同时显示在控制台和写入到日志文件中
)
# 添加统一的日志文件处理器,按日期自动轮转
logger.add(
os.path.join(log_dir, "stock_scanner_{time:YYYY-MM-DD}.log"),
format="{time:YYYY-MM-DD HH:mm:ss.SSS} | {level: <8} | {name}:{line} - {message}",
level="DEBUG",
rotation="00:00", # 每天午夜轮转
retention="7 days", # 保留7天的日志
compression="zip", # 压缩旧日志文件
enqueue=True # 使用队列写入,提高性能
)
# 添加错误日志文件处理器,专门记录错误信息
logger.add(
os.path.join(log_dir, "error_{time:YYYY-MM-DD}.log"),
format="{time:YYYY-MM-DD HH:mm:ss.SSS} | {level: <8} | {name}:{line} - {message}",
level="ERROR",
rotation="00:00", # 每天午夜轮转
retention="7 days", # 保留7天的错误日志
compression="zip", # 压缩旧日志文件
enqueue=True # 使用队列写入,提高性能
)
- 日志清理功能:默认最大存储 7 天
def clean_old_logs(max_days=7):
"""清理超过指定天数的日志文件"""
try:
today = datetime.now()
for filename in os.listdir(log_dir):
file_path = os.path.join(log_dir, filename)
# 跳过目录
if os.path.isdir(file_path):
continue
# 检查文件修改时间
file_time = datetime.fromtimestamp(os.path.getmtime(file_path))
days_old = (today - file_time).days
# 如果文件超过指定天数,删除它
if days_old > max_days:
os.remove(file_path)
logger.info(f"已删除过期日志文件: {filename}")
except Exception as e:
logger.error(f"清理日志文件时出错: {e}")
- 对外getter
def get_logger():
"""获取通用日志器"""
# 启动时清理旧日志
clean_old_logs()
return logger
MCP Server
这里其实就是前面说过的用 FastAPI-MCP 构建。这里是我们今天的重中之重。
创建股票分析器
# 创建股票分析服务实例
analyzer = StockAnalyzerService(
custom_api_url=ALI_TONGYI_URL,
custom_api_key=os.getenv(ALI_TONGYI_API_KEY_OS_VAR_NAME),
custom_api_model=ALI_TONGYI_PLUS_MODEL
)
请求模型
# 定义请求模型
class StockAnalysisRequest(BaseModel):
query: str
stock_code: str
market_type: str = "A"
API Route
这里我们一共有 7 个工具(即股票服务),这里仅以 stock_analyzer 为例看看具体实现。其他差不多。
@app.get("/stock_analyzer",
operation_id="analyze_stock",
summary="分析单只股票",
description="通过股票代码和市场类型分析股票,返回包括价格、评分、技术分析和AI分析等综合信息")
async def analyze_stock(
stock_code: str = Query(..., description="股票代码,如'600795'"),
market_type: str = Query("A",
description="市场类型,默认为'A'股,可选值:A(A股)、HK(港股)、US(美股)、ETF(场内ETF)、LOF(场内LOF)")
) -> StreamingResponse:
"""
分析单只股票(流式API)
Args:
stock_code: 股票代码
market_type: 市场类型,默认为'A'股
Returns:
以流式响应返回股票分析结果
"""
logger.info(f"API调用 (流式): analyze_stock({stock_code}, {market_type})")
async def stream_generator() -> AsyncGenerator[str, None]:
"""异步生成器,用于流式传输分析结果"""
try:
# 1. 获取股票数据 (直接调用 data_provider)
cleaned_stock_code = stock_code
if market_type == 'A': # 仅对A股进行前缀清理
if stock_code.lower().startswith('sh') or stock_code.lower().startswith('sz'):
cleaned_stock_code = stock_code[2:]
logger.debug(f"股票代码 {stock_code} 已清理为 {cleaned_stock_code}")
df = await analyzer.data_provider.get_stock_data(cleaned_stock_code, market_type)
# 3. 检查数据错误
if hasattr(df, 'error'):
error_msg = df.error
logger.error(f"获取股票数据时出错: {error_msg}")
yield json.dumps({"error": error_msg, "stock_code": cleaned_stock_code, "status": "error"}) + '\n'
return
if df.empty:
error_msg = f"获取到的股票 {cleaned_stock_code} 数据为空"
logger.error(error_msg)
yield json.dumps({"error": error_msg, "stock_code": cleaned_stock_code, "status": "error"}) + '\n'
return
# 4. 计算技术指标 (直接调用 indicator)
df_with_indicators = analyzer.indicator.calculate_indicators(df)
# 5. 计算评分 (直接调用 scorer)
score = analyzer.scorer.calculate_score(df_with_indicators)
recommendation = analyzer.scorer.get_recommendation(score)
# 6. 准备基本分析结果
latest_data = df_with_indicators.iloc[-1]
previous_data = df_with_indicators.iloc[-2] if len(df_with_indicators) > 1 else latest_data
price_change_value = latest_data['Close'] - previous_data['Close']
change_percent = latest_data.get('Change_pct')
if change_percent is None and previous_data['Close'] != 0:
change_percent = (price_change_value / previous_data['Close']) * 100
ma_short = latest_data.get('MA5', 0)
ma_medium = latest_data.get('MA20', 0)
ma_long = latest_data.get('MA60', 0)
if ma_short > ma_medium > ma_long:
ma_trend = "UP"
elif ma_short < ma_medium < ma_long:
ma_trend = "DOWN"
else:
ma_trend = "FLAT"
macd = latest_data.get('MACD', 0)
signal = latest_data.get('Signal', 0)
if macd > signal:
macd_signal = "BUY"
elif macd < signal:
macd_signal = "SELL"
else:
macd_signal = "HOLD"
volume = latest_data.get('Volume', 0)
volume_ma = latest_data.get('Volume_MA', 0)
if volume > volume_ma * 1.5:
volume_status = "HIGH"
elif volume < volume_ma * 0.5:
volume_status = "LOW"
else:
volume_status = "NORMAL"
basic_result = {
"stock_code": stock_code,
"market_type": market_type,
"analysis_date": datetime.now().strftime('%Y-%m-%d'),
"score": score,
"price": latest_data['Close'],
"price_change_value": price_change_value,
"change_percent": change_percent,
"ma_trend": ma_trend,
"rsi": latest_data.get('RSI', 0),
"macd_signal": macd_signal,
"volume_status": volume_status,
"recommendation": recommendation,
"ai_analysis": ""
}
# 7. Yield 基本分析结果
yield json.dumps(basic_result) + '\n'
# 8. 调用AI分析 (直接调用 ai_analyzer)
async for analysis_chunk in analyzer.ai_analyzer.get_ai_analysis(df_with_indicators, cleaned_stock_code,
market_type, stream=True):
yield analysis_chunk + '\n'
except KeyError as ke:
error_msg = f"股票代码 {stock_code} 在数据源中不存在或格式不正确: {str(ke)}"
logger.error(f"分析股票时出错: {error_msg}")
yield json.dumps({"error": error_msg, "stock_code": stock_code, "status": "error"}) + '\n'
except Exception as e:
logger.error(f"分析股票时出错: {str(e)}")
error_message = json.dumps({"error": str(e)})
yield f"{error_message}\n"
return StreamingResponse(stream_generator(), media_type="application/x-ndjson")
MCP启动挂载
# 创建MCP服务
# 【必须放在所有路由、接口定义 之后!!】
# 【最后几行才写这个】
mcp = FastApiMCP(
app,
name="股票分析MCP服务",
description="基于 FastAPI-MCP 的股票分析服务,提供股票数据、技术分析和AI分析功能"
)
# 挂载MCP服务,自动挂载。一定要写在所有路由设置之后!!!
# 把 MCP 标准接口(/mcp、/mcp/tools、/mcp/call-tool)自动注册到 FastAPI 里!
mcp.mount()
MCP服务启动
if __name__ == "__main__":
import uvicorn
# 获取端口
port = int(os.getenv("PORT", 8888))
host = os.getenv("HOST", "0.0.0.0")
# 启动服务
uvicorn.run("stock_server:app", host=host, port=port, reload=True
MCP Client
客户端用 MultiServerMCPClient 来承接和转换 MCP 服务提供的工具,转换后可直接应用到 Langchain中使用。我们前面也知道 MultiServerMCPClient 是LangChain MCP 适配器,所以他具备这个功能就不奇怪了。
async def main():
client = MultiServerMCPClient(
{
"Stock": {
"url": "http://localhost:8888/mcp",
"transport": "sse",
}
}
)
tools = await client.get_tools()
for index, tool in enumerate(tools):
logger.info(f"第{index + 1}个工具:{tool.name}")
llm = get_lc_o_ali_model_client()
logger.info(f"=================准备实际的业务工作=================")
agent = create_agent(
model=llm,
tools=tools
)
math_response = await agent.ainvoke(
{"messages": [{"role": "user", "content": "分析一下 03690 这个怎么样?啥时候值得买"}]}
)
logger.info(f"大模型返回参数抽取结果: {math_response}")
Running
Cherry Stdio 部署
Cherry Studio 是一款开源、跨平台的多模型AI客户端。其核心逻辑是充当一个“模型路由与聚合层”。
我们以 Cherry Stdio 为例看下我们自己写的 MCP 怎么部署在平台上让别人使用。
📢📢📢前提:LLM 已配置好 阿里千问大模型
- 设置- MCP 服务器-添加
- 填写我们 MCP 服务的所在地址,这里填本地地址。
- 保存后,就可以看到 MCP 会自动关联我们的 6 个股票分析工具
- 新建一个 Agent 关联 MCP 服务
- 我们尝试提问一个股票相关的问题,可见过程中确实调用了我们自建的 MCP 服务中的相关工具。
至此,我们的 MCP 服务已经可以发布并供别人使用了。当然目前我们只是在本地运行,部署到云端后才能公开。