
✨解锁 AI Agent 新姿势!手把手教你用 Python 搭建 MCP 服务,对接沪深数据 API,量化交易MCP 服务 (保姆级教程)✨
好啦!今天 PGFA 带大家从 0 到 1 搭建了一个实用的 Python MCP 服务,成功让 AI Agent 对接了沪深数据 API。是不是感觉 AI 的能力边界又被拓宽了?🤯 通过 MCP,我们可以赋予 AI 调用各种外部工具的能力,无论是查询数据库、控制智能家居,还是像今天这样获取金融数据,都变得触手可及!希望这篇保姆级教程对你有帮助!如果你觉得有用,别忘了也欢迎在评论区留言交流你的想
✨解锁 AI Agent 新姿势!手把手教你用 Python 搭建 MCP 服务,对接沪深数据 API (保姆级教程)✨
标签: MCP
, AI Agent
, Python
, API对接
, 量化交易
, 效率神器
, 后端开发
作者: PGFA
哈喽 CSDN 的小伙伴们!👋 我是 PGFA!今天给大家分享一个超酷的技术——模型上下文协议 (Model Context Protocol, MCP)!是不是听起来有点高大上?🤔 别担心,跟着 PGFA 的脚步,小白也能轻松搞定!
你是不是也遇到过这样的场景?
- 想让 AI 帮你分析最新的股票数据,结果只能手动复制粘贴 K 线图或者财务报表?😩
- 想让 AI 帮你执行一些需要调用外部 API 的任务,却发现 AI 本身做不到?🤯
- 看到大佬们都在玩 AI Agent (智能体),自己也想动手试试,却不知道从何开始?
那这篇教程就是为你准备的!🥳 今天,PGFA 就带大家用 Python 打造一个 MCP 服务,对接智兔数服的沪深数据 API (https://www.zhituapi.com/hsstockapi.html),让你的 AI Agent (比如 Cursor 或 Cline) 瞬间拥有访问实时股票数据的超能力!📈 告别繁琐操作,拥抱智能自动化,效率直接拉满!🔥
这篇文章你能学到:
- MCP 的基本工作原理 💡
- 如何用 Python 编写一个基础但完整的 MCP 服务 🐍
- 如何对接真实的外部 API (以智兔沪深数据为例) 🔗
- 如何在 Cursor 和 Cline 中配置并使用你的 MCP 服务 ✅
- 一些实用的小 Tips 和注意事项 📌
话不多说,上干货!👇
🛠️ 准备工作:磨刀不误砍柴工
在开始之前,请确保你拥有以下装备:
- Python 环境: 建议 Python 3.8 或更高版本。
requests
库: 用于发起 HTTP 请求调用 API。如果没安装,命令行运行:pip install requests
。- 智兔数服账号和 Token: 你需要注册智兔数服 (https://www.zhituapi.com/) 并获取你的 API Token。非常重要! 本教程代码中会使用
ZHITU_TOKEN_LIMIT_TEST
这个测试 Token,但它有调用次数限制且功能可能不全,实际使用请务必替换成你自己的有效 Token! - MCP 客户端 (可选其一):
- Cursor: 一款强大的 AI 代码编辑器 (https://cursor.sh/)。
- Cline: 一个开源的 AI 命令行助手或 VS Code 插件 (https://github.com/SaudriData/cline)。
🧠 核心概念科普:MCP 是个啥?
简单来说,MCP 就是由 AI 公司 Anthropic (Claude 背后的公司) 提出的一个开放标准,它定义了一种让 AI 大模型能够安全、标准化地与外部工具或服务进行交互的方式。
想象一下:
- 你的 AI (客户端,比如 Cursor) 想知道某只股票的最新价格。
- 它不会直接去访问股票 API,而是通过标准输入 (stdin) 发送一个 JSON-RPC 格式的请求给你的 MCP 服务。
- 你的 MCP 服务 (就是我们今天要写的 Python 脚本) 收到请求,知道 AI 想调用 “获取股票价格” 这个工具 (Tool)。
- MCP 服务去调用真正的智兔 API,拿到股票价格。
- MCP 服务把结果打包成 JSON-RPC 格式的响应,通过标准输出 (stdout) 还给 AI。
- AI 收到结果,然后告诉你股票价格。
整个过程就像 AI 有了一个听话的、专门负责跑腿的小助手 (MCP 服务),可以帮它连接外部世界!✨
小伙伴们如果还是有不懂的地方可以看博主之前有关MCP介绍的文章进行更加深入的了解🤯 AI Agent 开发必备技能!MCP 协议到底是个啥?告别胶水代码,让你的 AI 大模型直接“动手”!(PGFA 保姆级解析)
🚀 实战开始:一步步编写 MCP 服务代码
好了,理论讲完,撸起袖子加油干!💪 我们来创建 mcp_zhitou_server.py
文件。
Step 1: 导入库和基础配置
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import sys
import json
import requests
import logging
import traceback
# --- 配置 ---
# 日志配置,输出到标准错误流,避免干扰 stdout
# 这样调试信息就不会被 AI 客户端误认为是响应了
logging.basicConfig(level=logging.INFO, stream=sys.stderr, format='%(asctime)s - %(levelname)s - %(message)s')
# Zhitou API 配置
ZHITU_API_BASE_URL = "https://api.zhituapi.com/hs"
# 重要:请务必替换为你的有效 Token!测试 Token 有限制!
ZHITU_TOKEN = "ZHITU_TOKEN_LIMIT_TEST" # 暂时使用教程提供的测试 Token
logging.info("MCP 服务脚本初始化...")
if ZHITU_TOKEN == "ZHITU_TOKEN_LIMIT_TEST":
logging.warning("⚠️ 警告:当前使用的是测试 Token (ZHITU_TOKEN_LIMIT_TEST),功能和次数可能受限。请替换为您自己的有效 Token!")
- 我们导入了必要的库。
- 配置了日志,让调试信息打印到
stderr
,这样stdout
就能干净地输出 JSON-RPC 响应。 - 定义了智兔 API 的基础 URL 和 Token (再次强调,一定要换成你自己的!)。
- 加了个启动日志和 Token 警告,对新手更友好。
Step 2: 封装 API 调用函数
为了方便调用不同的智兔 API,我们写一个通用的请求函数:
# --- API 调用辅助函数 ---
def call_zhitou_api(endpoint_path, params=None):
"""
调用 Zhitou API 的通用函数。
Args:
endpoint_path (str): API 的端点路径 (例如: "list/all", "capital/lrqs/000001")
params (dict, optional): GET 请求的查询参数. Defaults to None.
Returns:
dict: 成功时返回 API 响应的 JSON 数据。
Raises:
ConnectionError: 如果 API 请求失败。
ValueError: 如果 API 响应不是有效的 JSON。
"""
url = f"{ZHITU_API_BASE_URL}/{endpoint_path}?token={ZHITU_TOKEN}"
logging.info(f"准备调用 Zhitou API: {url}")
try:
# 设置超时,避免服务卡死
response = requests.get(url, params=params, timeout=20)
# 检查 HTTP 状态码,非 200 则抛异常
response.raise_for_status()
logging.info(f"API 调用成功,状态码: {response.status_code}")
return response.json()
except requests.exceptions.Timeout:
logging.error(f"调用 Zhitou API 超时: {url}")
raise TimeoutError(f"请求 Zhitou API 超时 ({url})")
except requests.exceptions.RequestException as e:
logging.error(f"调用 Zhitou API 失败: {e}")
# 将底层错误包装成更通用的 ConnectionError
raise ConnectionError(f"无法连接或请求 Zhitou API 失败: {e}")
except json.JSONDecodeError as e:
logging.error(f"解析 Zhitou API 响应 JSON 失败: {e}")
raise ValueError(f"无法解析 Zhitou API 返回的 JSON 数据")
- 这个函数负责拼接 URL、添加 Token、发送 GET 请求。
- 增加了超时设置 (
timeout=20
)。 - 使用了
response.raise_for_status()
来检查 HTTP 错误。 - 添加了更详细的日志和更具体的异常类型,方便排查问题。
Step 3: 定义 MCP 工具函数
现在,我们要根据智兔的 API 文档,把每个想让 AI 使用的功能定义成一个 Python 函数。函数名将作为 MCP 的工具名。
# --- MCP 工具函数 ---
# 这些函数对应 MCP 客户端可以调用的工具
# 函数名建议清晰易懂,与 API 功能对应
# 使用 **kwargs 接收可能的多余参数,增加兼容性
# 每个函数最好有 docstring 描述其功能和所需参数
def get_stock_list(**kwargs):
"""获取基础 A 股股票列表 (代码, 名称, 交易所)。"""
logging.info("执行工具: get_stock_list")
return call_zhitou_api("list/all")
def get_new_stock_calendar(**kwargs):
"""获取新股日历 (申购信息, 上市日期等)。"""
logging.info("执行工具: get_new_stock_calendar")
return call_zhitou_api("list/new")
def get_company_profile(stock_code, **kwargs):
"""获取指定股票代码的上市公司简介。
Args:
stock_code (str): 股票代码, 例如 '000001'。
"""
if not stock_code:
raise ValueError("工具 'get_company_profile' 需要 'stock_code' 参数。")
logging.info(f"执行工具: get_company_profile, stock_code={stock_code}")
# 注意 endpoint_path 的拼接方式
return call_zhitou_api(f"gs/gsjj/{stock_code}")
def get_capital_daily_trend(stock_code, **kwargs):
"""获取指定股票代码的每日资金流入趋势 (近十年)。
Args:
stock_code (str): 股票代码, 例如 '000001'。
"""
if not stock_code:
raise ValueError("工具 'get_capital_daily_trend' 需要 'stock_code' 参数。")
logging.info(f"执行工具: get_capital_daily_trend, stock_code={stock_code}")
return call_zhitou_api(f"capital/lrqs/{stock_code}")
def get_all_announcements(stock_code, **kwargs):
"""获取指定股票代码的历史所有公告列表。
Args:
stock_code (str): 股票代码, 例如 '000001'。
"""
if not stock_code:
raise ValueError("工具 'get_all_announcements' 需要 'stock_code' 参数。")
logging.info(f"执行工具: get_all_announcements, stock_code={stock_code}")
return call_zhitou_api(f"msg/sygg/{stock_code}")
# --- 可以在这里根据智兔 API 文档继续添加其他工具函数 ---
# 比如:获取风险警示股、指数树、根据分类找股票、获取涨跌停股池等等
# def get_st_stock_list(**kwargs): ...
# def get_index_tree(**kwargs): ...
# def get_stocks_by_category_code(category_code, **kwargs): ...
# def get_limit_up_pool(trade_date, **kwargs): ...
- 每个函数对应一个 API 功能。
- 函数名就是 AI 调用时使用的工具名 (例如
get_company_profile
)。 - 对于需要参数的 API (如股票代码
stock_code
),在函数签名中定义,并在函数内部进行校验。 - 使用
docstring
描述函数功能和参数,这对于后续配置 MCP 客户端很有用。 - 鼓励读者根据文档自行添加更多工具函数,提高服务的实用性!
Step 4: 创建工具映射
我们需要一个字典,把工具名和对应的 Python 函数关联起来:
# --- MCP 工具映射 ---
# 将 MCP 客户端请求的工具名称映射到上面的 Python 函数
# 键名必须与客户端配置或 AI 调用时使用的工具名完全一致
TOOLS = {
"get_stock_list": get_stock_list,
"get_new_stock_calendar": get_new_stock_calendar,
"get_company_profile": get_company_profile,
"get_capital_daily_trend": get_capital_daily_trend,
"get_all_announcements": get_all_announcements,
# --- 如果你在上面添加了更多工具函数,记得在这里添加映射 ---
# "get_st_stock_list": get_st_stock_list,
# "get_index_tree": get_index_tree,
# "get_stocks_by_category_code": get_stocks_by_category_code,
# "get_limit_up_pool": get_limit_up_pool,
}
logging.info(f"已注册的 MCP 工具: {list(TOOLS.keys())}")
Step 5: 编写请求处理逻辑
这是 MCP 服务的核心,负责解析来自客户端的 JSON-RPC 请求,并调用相应的工具函数:
# --- 请求处理函数 ---
def handle_request(request_data):
"""处理来自 MCP 客户端的单个 JSON-RPC 请求"""
request_id = request_data.get("id") # 获取请求 ID,响应时需要原样返回
# 基础 JSON-RPC 格式校验
if request_data.get("jsonrpc") != "2.0" or "method" not in request_data:
logging.error(f"无效的请求格式 (非 JSON-RPC 2.0 或缺少 method): {request_data}")
return json.dumps({
"jsonrpc": "2.0",
"error": {"code": -32600, "message": "无效请求 (Invalid Request)"},
"id": request_id
})
method = request_data.get("method")
params = request_data.get("params", {})
response_payload = None # 初始化响应载荷
# 处理 'tools/call' 方法 (AI 调用工具)
if method == "tools/call":
tool_name = params.get("name")
# MCP 参数通常在 'arguments' 字段下,是一个字典
arguments = params.get("arguments", {})
logging.info(f"收到工具调用请求 -> Tool: '{tool_name}', Arguments: {arguments}")
if tool_name in TOOLS:
try:
tool_function = TOOLS[tool_name]
# 使用 **arguments 将参数字典解包为关键字参数传入函数
result_data = tool_function(**arguments)
# MCP 规定 result 必须是字符串,所以我们将 API 返回的 dict/list 转为 JSON 字符串
response_payload = {
"result": json.dumps(result_data, ensure_ascii=False), # ensure_ascii=False 保留中文
}
logging.info(f"工具 '{tool_name}' 执行成功。")
except (ValueError, TypeError) as e: # 参数错误
logging.warning(f"工具 '{tool_name}' 参数错误: {e}")
response_payload = {
"error": {"code": -32602, "message": f"无效参数 (Invalid params): {e}"}
}
except (ConnectionError, TimeoutError, ValueError) as e: # API 调用或解析错误
logging.error(f"调用 Zhitou API 时出错 (工具 '{tool_name}'): {e}")
response_payload = {
"error": {"code": -32000, "message": f"服务器错误: 调用外部 API 失败 - {e}"}
}
except Exception as e: # 其他未知错误
logging.error(f"执行工具 '{tool_name}' 时发生未知错误: {e}\n{traceback.format_exc()}")
response_payload = {
"error": {"code": -32000, "message": f"服务器内部错误: {type(e).__name__}: {e}"}
}
else:
logging.warning(f"请求的工具 '{tool_name}' 未在本服务中定义。")
response_payload = {
"error": {"code": -32601, "message": f"方法未找到 (Method not found): 工具 '{tool_name}' 不存在"}
}
# 处理 'tools/list' 方法 (客户端查询可用工具列表)
elif method == "tools/list":
logging.info("收到工具列表请求 (tools/list)")
tool_list = []
for name, func in TOOLS.items():
# 尝试提取函数签名中的参数信息 (这是一个简化版本)
# 更完善的方案可以使用 inspect 模块
# 这里我们简单地从 docstring 中提取信息
doc = func.__doc__ or "无描述"
param_info = {}
# 简单的基于 docstring 的参数提取 (可能不完全准确)
if "Args:" in doc:
try:
args_section = doc.split("Args:")[1].split("Returns:")[0].split("Raises:")[0].strip()
for line in args_section.split('\n'):
parts = line.strip().split('(')
if len(parts) > 1:
param_name = parts[0].strip()
# 简单类型推断 (更复杂的需要 inspect)
param_type = "string" # 默认为 string
if "int" in parts[1].lower() or "number" in parts[1].lower():
param_type = "number"
elif "bool" in parts[1].lower():
param_type = "boolean"
param_info[param_name] = {"type": param_type}
except Exception as e:
logging.warning(f"解析工具 '{name}' 的 docstring 参数失败: {e}")
tool_list.append({
"name": name,
"description": doc.split("Args:")[0].strip(), # 只取描述部分
"parameters": param_info # 添加参数信息
})
# 同样,结果需要是 JSON 字符串
response_payload = {"result": json.dumps(tool_list, ensure_ascii=False)}
logging.info(f"返回工具列表: {tool_list}")
# 不支持的方法
else:
logging.warning(f"收到不支持的方法请求: {method}")
response_payload = {
"error": {"code": -32601, "message": f"方法未找到 (Method not found): {method}"}
}
# 构建最终的 JSON-RPC 响应
final_response = {
"jsonrpc": "2.0",
"id": request_id, # 必须包含原始请求 ID
}
# 合并 result 或 error
final_response.update(response_payload)
return json.dumps(final_response, ensure_ascii=False) # 确保中文字符正确编码
- 严格按照 JSON-RPC 2.0 规范处理请求和响应。
- 区分了
tools/call
(调用工具) 和tools/list
(获取工具列表) 两种方法。 tools/call
会根据name
查找TOOLS
字典,找到对应的函数,并将arguments
解包后传递给函数。tools/list
会遍历TOOLS
字典,提取函数名和docstring
作为描述,返回给客户端。增加了简单的参数信息提取。- 对可能发生的错误(参数错误、API 调用失败、未知错误)进行了捕获和处理,返回符合规范的
error
对象。 - 注意:
result
字段的值必须是字符串,所以我们用json.dumps()
将 API 返回的 Python 对象(字典或列表)序列化。ensure_ascii=False
保证中文字符不会被转义。
Step 6: 编写主循环
最后,我们需要一个主循环来持续监听 stdin
,读取请求,调用处理函数,并将响应写回 stdout
。
# --- 主循环 ---
def main():
"""MCP 服务器主入口点,持续监听 stdin"""
logging.info("✅ Zhitou API MCP 服务已启动,开始监听标准输入 (stdin)...")
while True:
try:
# 持续读取标准输入流的每一行
line = sys.stdin.readline()
# 如果读到文件末尾 (EOF),说明客户端断开连接,退出循环
if not line:
logging.info("标准输入 (stdin) 已关闭,MCP 服务退出。")
break
line = line.strip()
# 忽略空行
if not line:
continue
logging.debug(f"收到原始输入行: {line}")
try:
# 解析收到的 JSON 请求
request_data = json.loads(line)
except json.JSONDecodeError:
# 如果输入不是有效的 JSON,记录错误并跳过,不发送响应
logging.error(f"❌ 无法解析输入的 JSON 数据: {line}")
# 不返回错误响应,避免客户端因错误格式导致死循环
continue
# 处理解析后的请求数据
response_str = handle_request(request_data)
# 如果有响应字符串,则打印到标准输出
if response_str:
logging.debug(f"准备发送响应: {response_str}")
# 打印响应到 stdout,flush=True 确保立即发送,不缓冲
print(response_str, flush=True)
except KeyboardInterrupt:
# 捕获 Ctrl+C 信号,优雅退出
logging.info("收到 KeyboardInterrupt (Ctrl+C),正在关闭 MCP 服务...")
break
except BrokenPipeError:
# 当客户端意外关闭连接时可能会发生
logging.warning("管道已断开 (BrokenPipeError),可能是客户端已关闭。MCP 服务退出。")
break
except Exception as e:
# 捕获主循环中其他未预料到的异常
logging.error(f"❌ MCP 服务主循环发生严重错误: {e}\n{traceback.format_exc()}")
# 遇到严重错误时,可以选择退出或尝试继续
# 这里选择记录错误后继续,尝试处理下一个请求
continue
logging.info("👋 MCP 服务已停止。")
# 脚本执行入口
if __name__ == "__main__":
main()
- 使用
while True
无限循环,持续监听。 sys.stdin.readline()
读取一行输入。如果读到空(EOF),表示客户端断开,退出循环。flush=True
在print()
中非常重要,确保响应立刻发送给客户端,而不是被 Python 的输出缓冲区缓存。- 增加了对
KeyboardInterrupt
(Ctrl+C) 和BrokenPipeError
的处理,让服务退出更优雅。
🎉 激动人心的完整代码
好了,把上面的步骤整合起来,就是我们完整的 mcp_zhitou_server.py
啦!
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import sys
import json
import requests
import logging
import traceback
# --- 配置 ---
logging.basicConfig(level=logging.INFO, stream=sys.stderr, format='%(asctime)s - %(levelname)s - %(message)s')
ZHITU_API_BASE_URL = "https://api.zhituapi.com/hs"
# 🔥🔥🔥【请务必替换为你自己的有效 Token! 测试 Token 有限制!】🔥🔥🔥
ZHITU_TOKEN = "ZHITU_TOKEN_LIMIT_TEST"
logging.info("MCP 服务脚本初始化...")
if ZHITU_TOKEN == "ZHITU_TOKEN_LIMIT_TEST":
logging.warning("⚠️ 警告:当前使用的是测试 Token (ZHITU_TOKEN_LIMIT_TEST),功能和次数可能受限。请替换为您自己的有效 Token!")
# --- API 调用辅助函数 ---
def call_zhitou_api(endpoint_path, params=None):
url = f"{ZHITU_API_BASE_URL}/{endpoint_path}?token={ZHITU_TOKEN}"
logging.info(f"准备调用 Zhitou API: {url}")
try:
response = requests.get(url, params=params, timeout=20)
response.raise_for_status()
logging.info(f"API 调用成功,状态码: {response.status_code}")
# 尝试解析 JSON
try:
data = response.json()
# 可以在这里检查智兔 API 可能返回的业务错误码(如果文档有定义)
# 例如: if data.get('code') != 0: logging.error(...) raise ValueError(...)
return data
except json.JSONDecodeError as e:
logging.error(f"解析 Zhitou API 响应 JSON 失败: {e}. 响应内容: {response.text[:500]}...") # 记录部分响应内容
raise ValueError(f"无法解析 Zhitou API 返回的 JSON 数据")
except requests.exceptions.Timeout:
logging.error(f"调用 Zhitou API 超时: {url}")
raise TimeoutError(f"请求 Zhitou API 超时 ({url})")
except requests.exceptions.RequestException as e:
logging.error(f"调用 Zhitou API 失败: {e}")
raise ConnectionError(f"无法连接或请求 Zhitou API 失败: {e}")
# --- MCP 工具函数 ---
def get_stock_list(**kwargs):
"""获取基础 A 股股票列表 (代码, 名称, 交易所)。"""
logging.info("执行工具: get_stock_list")
return call_zhitou_api("list/all")
def get_new_stock_calendar(**kwargs):
"""获取新股日历 (申购信息, 上市日期等)。"""
logging.info("执行工具: get_new_stock_calendar")
return call_zhitou_api("list/new")
def get_company_profile(stock_code, **kwargs):
"""获取指定股票代码的上市公司简介。
Args:
stock_code (str): 股票代码, 例如 '000001'。
"""
if not stock_code:
raise ValueError("工具 'get_company_profile' 需要 'stock_code' 参数。")
logging.info(f"执行工具: get_company_profile, stock_code={stock_code}")
return call_zhitou_api(f"gs/gsjj/{stock_code}")
def get_capital_daily_trend(stock_code, **kwargs):
"""获取指定股票代码的每日资金流入趋势 (近十年)。
Args:
stock_code (str): 股票代码, 例如 '000001'。
"""
if not stock_code:
raise ValueError("工具 'get_capital_daily_trend' 需要 'stock_code' 参数。")
logging.info(f"执行工具: get_capital_daily_trend, stock_code={stock_code}")
return call_zhitou_api(f"capital/lrqs/{stock_code}")
def get_all_announcements(stock_code, **kwargs):
"""获取指定股票代码的历史所有公告列表。
Args:
stock_code (str): 股票代码, 例如 '000001'。
"""
if not stock_code:
raise ValueError("工具 'get_all_announcements' 需要 'stock_code' 参数。")
logging.info(f"执行工具: get_all_announcements, stock_code={stock_code}")
return call_zhitou_api(f"msg/sygg/{stock_code}")
# --- 你可以继续添加更多工具函数在这里 ---
# --- MCP 工具映射 ---
TOOLS = {
"get_stock_list": get_stock_list,
"get_new_stock_calendar": get_new_stock_calendar,
"get_company_profile": get_company_profile,
"get_capital_daily_trend": get_capital_daily_trend,
"get_all_announcements": get_all_announcements,
# --- 如果添加了新函数,在这里加上映射 ---
}
logging.info(f"已注册的 MCP 工具: {list(TOOLS.keys())}")
# --- 请求处理函数 ---
def handle_request(request_data):
request_id = request_data.get("id")
if request_data.get("jsonrpc") != "2.0" or "method" not in request_data:
logging.error(f"无效的请求格式: {request_data}")
return json.dumps({
"jsonrpc": "2.0",
"error": {"code": -32600, "message": "无效请求 (Invalid Request)"},
"id": request_id
})
method = request_data.get("method")
params = request_data.get("params", {})
response_payload = None
if method == "tools/call":
tool_name = params.get("name")
arguments = params.get("arguments", {})
logging.info(f"收到工具调用请求 -> Tool: '{tool_name}', Arguments: {arguments}")
if tool_name in TOOLS:
try:
tool_function = TOOLS[tool_name]
result_data = tool_function(**arguments)
response_payload = {"result": json.dumps(result_data, ensure_ascii=False)}
logging.info(f"工具 '{tool_name}' 执行成功。")
except (ValueError, TypeError) as e:
logging.warning(f"工具 '{tool_name}' 参数错误: {e}")
response_payload = {"error": {"code": -32602, "message": f"无效参数 (Invalid params): {e}"}}
except (ConnectionError, TimeoutError, ValueError) as e:
logging.error(f"调用 Zhitou API 时出错 (工具 '{tool_name}'): {e}")
response_payload = {"error": {"code": -32000, "message": f"服务器错误: 调用外部 API 失败 - {e}"}}
except Exception as e:
logging.error(f"执行工具 '{tool_name}' 时发生未知错误: {e}\n{traceback.format_exc()}")
response_payload = {"error": {"code": -32000, "message": f"服务器内部错误: {type(e).__name__}: {e}"}}
else:
logging.warning(f"请求的工具 '{tool_name}' 未在本服务中定义。")
response_payload = {"error": {"code": -32601, "message": f"方法未找到 (Method not found): 工具 '{tool_name}' 不存在"}}
elif method == "tools/list":
logging.info("收到工具列表请求 (tools/list)")
tool_list = []
for name, func in TOOLS.items():
doc = func.__doc__ or "无描述"
param_info = {}
if "Args:" in doc:
try:
args_section = doc.split("Args:")[1].split("Returns:")[0].split("Raises:")[0].strip()
for line in args_section.split('\n'):
parts = line.strip().split('(')
if len(parts) > 1:
param_name = parts[0].strip()
param_type = "string"
if "int" in parts[1].lower() or "number" in parts[1].lower(): param_type = "number"
elif "bool" in parts[1].lower(): param_type = "boolean"
param_info[param_name] = {"type": param_type}
except Exception as e:
logging.warning(f"解析工具 '{name}' 的 docstring 参数失败: {e}")
tool_list.append({
"name": name,
"description": doc.split("Args:")[0].strip(),
"parameters": param_info
})
response_payload = {"result": json.dumps(tool_list, ensure_ascii=False)}
logging.info(f"返回工具列表: {tool_list}")
else:
logging.warning(f"收到不支持的方法请求: {method}")
response_payload = {"error": {"code": -32601, "message": f"方法未找到 (Method not found): {method}"}}
final_response = {"jsonrpc": "2.0", "id": request_id}
final_response.update(response_payload)
return json.dumps(final_response, ensure_ascii=False)
# --- 主循环 ---
def main():
logging.info("✅ Zhitou API MCP 服务已启动,开始监听标准输入 (stdin)...")
while True:
try:
line = sys.stdin.readline()
if not line:
logging.info("标准输入 (stdin) 已关闭,MCP 服务退出。")
break
line = line.strip()
if not line: continue
logging.debug(f"收到原始输入行: {line}")
try:
request_data = json.loads(line)
except json.JSONDecodeError:
logging.error(f"❌ 无法解析输入的 JSON 数据: {line}")
continue
response_str = handle_request(request_data)
if response_str:
logging.debug(f"准备发送响应: {response_str}")
print(response_str, flush=True) # 关键!确保立即发送!
except KeyboardInterrupt:
logging.info("收到 KeyboardInterrupt (Ctrl+C),正在关闭 MCP 服务...")
break
except BrokenPipeError:
logging.warning("管道已断开 (BrokenPipeError),可能是客户端已关闭。MCP 服务退出。")
break
except Exception as e:
logging.error(f"❌ MCP 服务主循环发生严重错误: {e}\n{traceback.format_exc()}")
continue
logging.info("👋 MCP 服务已停止。")
# 脚本执行入口
if __name__ == "__main__":
main()
⚙️ 配置与使用:让 AI 用起来!
代码写好了,怎么让 Cursor 或 Cline 用起来呢?很简单!
-
保存文件: 将上面的完整代码保存为
mcp_zhitou_server.py
。 -
安装依赖: 再次确认
requests
已安装 (pip install requests
)。 -
🔥【极其重要】替换 Token🔥: 打开
mcp_zhitou_server.py
,找到ZHITU_TOKEN = "ZHITU_TOKEN_LIMIT_TEST"
这一行,把测试 Token 换成你自己的智兔数服 API Token! 否则 API 调用会失败或受限! -
配置 Cline:
- 打开 Cline (无论是独立版还是 VS Code 插件)。
- 找到管理 MCP Servers 的地方 (通常在设置里)。
- 点击添加/新建 MCP Server。
- Name (名称):
ZhitouAPI
(或者你喜欢的名字) - Command (命令): 填入执行 Python 脚本的命令,注意使用绝对路径或确保脚本在 PATH 中。
- 示例 (Mac/Linux):
python3 /Users/pgfa/scripts/mcp_zhitou_server.py
- 示例 (Windows):
python C:\Users\PGFA\Documents\mcp_zhitou_server.py
- 注意:
PGFA
是博主自己电脑上的用户名这里需要替换成你们电脑上的路径哦
- 示例 (Mac/Linux):
- 保存。Cline 会自动启动这个服务进程。你应该能在 Cline 的 MCP 管理界面看到它显示为“已连接”或类似的绿色状态。
-
配置 Cursor:
- 在你的项目根目录创建一个
.cursor
文件夹。 - 在
.cursor
文件夹里创建一个mcp.json
文件。 - 将以下内容粘贴到
mcp.json
,并修改args
中的脚本路径为你的实际路径:
{ "mcpServers": { "zhitou": { "command": "python", // 或 python3,取决于你的环境 "args": ["/Users/pgfa/scripts/mcp_zhitou_server.py"], // ⚠️ 修改为你的脚本实际路径! "workingDirectory": "/Users/pgfa/scripts/", // ⚠️ 可选,建议设为脚本所在目录 "tools": [ // 这里最好把你实现的工具都声明一下,帮助 Agent 理解 {"name": "get_stock_list", "description": "获取基础 A 股股票列表 (代码, 名称, 交易所)。"}, {"name": "get_new_stock_calendar", "description": "获取新股日历 (申购信息, 上市日期等)。"}, {"name": "get_company_profile", "description": "获取指定股票代码的上市公司简介。", "parameters": {"stock_code": "string"}}, {"name": "get_capital_daily_trend", "description": "获取指定股票代码的每日资金流入趋势 (近十年)。", "parameters": {"stock_code": "string"}}, {"name": "get_all_announcements", "description": "获取指定股票代码的历史所有公告列表。", "parameters": {"stock_code": "string"}} // ... 其他你添加的工具 ... ] } } }
- 保存文件。重启 Cursor 或重新加载项目后,Cursor Agent 应该就能识别并使用这个 MCP 服务了。你可以在 Cursor 的设置 -> MCP 部分查看状态。
- 在你的项目根目录创建一个
-
开始提问!
现在,你可以在 Cline 或 Cursor 的聊天窗口里,像和真人对话一样,让 AI 调用你的 Zhitou API 工具啦!试试看:- “帮我查一下 000001 平安银行的公司简介。”
- “获取最新的 A 股列表。”
- “看看 600519 贵州茅台最近的资金流入情况。” (如果实现了
get_capital_daily_trend
) - “今天的新股日历有哪些?”
观察 AI 的回复,以及你运行 MCP 服务脚本的终端(或 Cline/Cursor 的日志),看看请求是否被正确处理,API 是否被成功调用!是不是超级方便?!🤩
💡 进阶思考与小贴士
- Token 安全: 生产环境中,千万不要把 Token 硬编码!最好使用环境变量 (
os.environ.get('ZHITU_API_TOKEN')
) 或者专门的配置文件/密钥管理服务。 - 添加更多工具: 这个框架很容易扩展,只要对着智兔 API 文档,添加新的 Python 函数和
TOOLS
映射就行。让你的 AI 助手更全能! - 更精细的错误处理: 可以根据 Zhitou API 可能返回的特定错误码,给出更友好的错误提示。
- 异步处理: 如果你觉得API 调用比较耗时,或者你想处理并发请求,可以考虑使用
asyncio
将服务改造成异步的。 - Windows 路径: Windows 用户请注意路径分隔符是
\
,在 Python 字符串中可能需要写成\\
或使用原始字符串r"C:\path"
。
总结
好啦!今天 PGFA 带大家从 0 到 1 搭建了一个实用的 Python MCP 服务,成功让 AI Agent 对接了沪深数据 API。是不是感觉 AI 的能力边界又被拓宽了?🤯 通过 MCP,我们可以赋予 AI 调用各种外部工具的能力,无论是查询数据库、控制智能家居,还是像今天这样获取金融数据,都变得触手可及!
希望这篇保姆级教程对你有帮助!如果你觉得有用,别忘了点赞👍、收藏🌟、关注 PGFA 哦! 也欢迎在评论区留言交流你的想法和遇到的问题,我们一起探讨学习!下次再给大家带来更多好玩的技术分享!拜拜~ ✨
更多推荐
所有评论(0)