server.py 38 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144
  1. """
  2. TrendRadar MCP Server - FastMCP 2.0 实现
  3. 使用 FastMCP 2.0 提供生产级 MCP 工具服务器。
  4. 支持 stdio 和 HTTP 两种传输模式。
  5. """
  6. import asyncio
  7. import json
  8. from typing import List, Optional, Dict, Union
  9. from fastmcp import FastMCP
  10. from .tools.data_query import DataQueryTools
  11. from .tools.analytics import AnalyticsTools
  12. from .tools.search_tools import SearchTools
  13. from .tools.config_mgmt import ConfigManagementTools
  14. from .tools.system import SystemManagementTools
  15. from .tools.storage_sync import StorageSyncTools
  16. from .tools.article_reader import ArticleReaderTools
  17. from .utils.date_parser import DateParser
  18. from .utils.errors import MCPError
  19. # 创建 FastMCP 2.0 应用
  20. mcp = FastMCP('trendradar-news')
  21. # 全局工具实例(在第一次请求时初始化)
  22. _tools_instances = {}
  23. def _get_tools(project_root: Optional[str] = None):
  24. """获取或创建工具实例(单例模式)"""
  25. if not _tools_instances:
  26. _tools_instances['data'] = DataQueryTools(project_root)
  27. _tools_instances['analytics'] = AnalyticsTools(project_root)
  28. _tools_instances['search'] = SearchTools(project_root)
  29. _tools_instances['config'] = ConfigManagementTools(project_root)
  30. _tools_instances['system'] = SystemManagementTools(project_root)
  31. _tools_instances['storage'] = StorageSyncTools(project_root)
  32. _tools_instances['article'] = ArticleReaderTools(project_root)
  33. return _tools_instances
  34. # ==================== MCP Resources ====================
  35. @mcp.resource("config://platforms")
  36. async def get_platforms_resource() -> str:
  37. """
  38. 获取支持的平台列表
  39. 返回 config.yaml 中配置的所有平台信息,包括 ID 和名称。
  40. """
  41. tools = _get_tools()
  42. config = await asyncio.to_thread(
  43. tools['config'].get_current_config, section="crawler"
  44. )
  45. return json.dumps({
  46. "platforms": config.get("platforms", []),
  47. "description": "TrendRadar 支持的热榜平台列表"
  48. }, ensure_ascii=False, indent=2)
  49. @mcp.resource("config://rss-feeds")
  50. async def get_rss_feeds_resource() -> str:
  51. """
  52. 获取 RSS 订阅源列表
  53. 返回当前配置的所有 RSS 源信息。
  54. """
  55. tools = _get_tools()
  56. status = await asyncio.to_thread(tools['data'].get_rss_feeds_status)
  57. return json.dumps({
  58. "feeds": status.get("today_feeds", {}),
  59. "description": "TrendRadar 支持的 RSS 订阅源列表"
  60. }, ensure_ascii=False, indent=2)
  61. @mcp.resource("data://available-dates")
  62. async def get_available_dates_resource() -> str:
  63. """
  64. 获取可用的数据日期范围
  65. 返回本地存储中可查询的日期列表。
  66. """
  67. tools = _get_tools()
  68. result = await asyncio.to_thread(
  69. tools['storage'].list_available_dates, source="local"
  70. )
  71. return json.dumps({
  72. "dates": result.get("data", {}).get("local", {}).get("dates", []),
  73. "description": "本地存储中可查询的日期列表"
  74. }, ensure_ascii=False, indent=2)
  75. @mcp.resource("config://keywords")
  76. async def get_keywords_resource() -> str:
  77. """
  78. 获取关注词配置
  79. 返回 frequency_words.txt 中配置的关注词分组。
  80. """
  81. tools = _get_tools()
  82. config = await asyncio.to_thread(
  83. tools['config'].get_current_config, section="keywords"
  84. )
  85. return json.dumps({
  86. "word_groups": config.get("word_groups", []),
  87. "total_groups": config.get("total_groups", 0),
  88. "description": "TrendRadar 关注词配置"
  89. }, ensure_ascii=False, indent=2)
  90. # ==================== 日期解析工具(优先调用)====================
  91. @mcp.tool
  92. async def resolve_date_range(
  93. expression: str
  94. ) -> str:
  95. """
  96. 【推荐优先调用】将自然语言日期表达式解析为标准日期范围
  97. **为什么需要这个工具?**
  98. 用户经常使用"本周"、"最近7天"等自然语言表达日期,但 AI 模型自己计算日期
  99. 可能导致不一致的结果。此工具在服务器端使用精确的当前时间计算,确保所有
  100. AI 模型获得一致的日期范围。
  101. **推荐使用流程:**
  102. 1. 用户说"分析AI本周的情感倾向"
  103. 2. AI 调用 resolve_date_range("本周") → 获取精确日期范围
  104. 3. AI 调用 analyze_sentiment(topic="ai", date_range=上一步返回的date_range)
  105. Args:
  106. expression: 自然语言日期表达式,支持:
  107. - 单日: "今天", "昨天", "today", "yesterday"
  108. - 周: "本周", "上周", "this week", "last week"
  109. - 月: "本月", "上月", "this month", "last month"
  110. - 最近N天: "最近7天", "最近30天", "last 7 days", "last 30 days"
  111. - 动态: "最近5天", "last 10 days"(任意天数)
  112. Returns:
  113. JSON格式的日期范围,可直接用于其他工具的 date_range 参数:
  114. {
  115. "success": true,
  116. "expression": "本周",
  117. "date_range": {
  118. "start": "2025-11-18",
  119. "end": "2025-11-26"
  120. },
  121. "current_date": "2025-11-26",
  122. "description": "本周(周一到周日,11-18 至 11-26)"
  123. }
  124. Examples:
  125. 用户:"分析AI本周的情感倾向"
  126. AI调用步骤:
  127. 1. resolve_date_range("本周")
  128. → {"date_range": {"start": "2025-11-18", "end": "2025-11-26"}, ...}
  129. 2. analyze_sentiment(topic="ai", date_range={"start": "2025-11-18", "end": "2025-11-26"})
  130. 用户:"看看最近7天的特斯拉新闻"
  131. AI调用步骤:
  132. 1. resolve_date_range("最近7天")
  133. → {"date_range": {"start": "2025-11-20", "end": "2025-11-26"}, ...}
  134. 2. search_news(query="特斯拉", date_range={"start": "2025-11-20", "end": "2025-11-26"})
  135. """
  136. try:
  137. result = await asyncio.to_thread(DateParser.resolve_date_range_expression, expression)
  138. return json.dumps(result, ensure_ascii=False, indent=2)
  139. except MCPError as e:
  140. return json.dumps({
  141. "success": False,
  142. "error": e.to_dict()
  143. }, ensure_ascii=False, indent=2)
  144. except Exception as e:
  145. return json.dumps({
  146. "success": False,
  147. "error": {
  148. "code": "INTERNAL_ERROR",
  149. "message": str(e)
  150. }
  151. }, ensure_ascii=False, indent=2)
  152. # ==================== 数据查询工具 ====================
  153. @mcp.tool
  154. async def get_latest_news(
  155. platforms: Optional[List[str]] = None,
  156. limit: int = 50,
  157. include_url: bool = False
  158. ) -> str:
  159. """
  160. 获取最新一批爬取的新闻数据,快速了解当前热点
  161. Args:
  162. platforms: 平台ID列表,如 ['zhihu', 'weibo'],不指定则使用所有平台
  163. limit: 返回条数限制,默认50,最大1000
  164. include_url: 是否包含URL链接,默认False(节省token)
  165. Returns:
  166. JSON格式的新闻列表
  167. **数据展示建议**
  168. - 默认展示全部返回数据,除非用户明确要求总结
  169. - 用户说"总结"或"挑重点"时才进行筛选
  170. - 用户问"为什么只显示部分"说明需要完整数据
  171. """
  172. tools = _get_tools()
  173. result = await asyncio.to_thread(
  174. tools['data'].get_latest_news,
  175. platforms=platforms, limit=limit, include_url=include_url
  176. )
  177. return json.dumps(result, ensure_ascii=False, indent=2)
  178. @mcp.tool
  179. async def get_trending_topics(
  180. top_n: int = 10,
  181. mode: str = 'current',
  182. extract_mode: str = 'keywords'
  183. ) -> str:
  184. """
  185. 获取热点话题统计
  186. Args:
  187. top_n: 返回TOP N话题,默认10
  188. mode: 时间模式
  189. - "daily": 当日累计数据统计
  190. - "current": 最新一批数据统计(默认)
  191. extract_mode: 提取模式
  192. - "keywords": 统计预设关注词(基于 config/frequency_words.txt,默认)
  193. - "auto_extract": 自动从新闻标题提取高频词(无需预设,自动发现热点)
  194. Returns:
  195. JSON格式的话题频率统计列表
  196. Examples:
  197. - 使用预设关注词: get_trending_topics(mode="current")
  198. - 自动提取热点: get_trending_topics(extract_mode="auto_extract", top_n=20)
  199. """
  200. tools = _get_tools()
  201. result = await asyncio.to_thread(
  202. tools['data'].get_trending_topics,
  203. top_n=top_n, mode=mode, extract_mode=extract_mode
  204. )
  205. return json.dumps(result, ensure_ascii=False, indent=2)
  206. # ==================== RSS 数据查询工具 ====================
  207. @mcp.tool
  208. async def get_latest_rss(
  209. feeds: Optional[List[str]] = None,
  210. days: int = 1,
  211. limit: int = 50,
  212. include_summary: bool = False
  213. ) -> str:
  214. """
  215. 获取最新的 RSS 订阅数据(支持多日查询)
  216. RSS 数据与热榜新闻分开存储,按时间流展示,适合获取特定来源的最新内容。
  217. Args:
  218. feeds: RSS 源 ID 列表,如 ['hacker-news', '36kr'],不指定则返回所有源
  219. days: 获取最近 N 天的数据,默认 1(仅今天),最大 30 天
  220. limit: 返回条数限制,默认50,最大500
  221. include_summary: 是否包含文章摘要,默认False(节省token)
  222. Returns:
  223. JSON格式的 RSS 条目列表
  224. Examples:
  225. - get_latest_rss()
  226. - get_latest_rss(days=7, feeds=['hacker-news'])
  227. """
  228. tools = _get_tools()
  229. result = await asyncio.to_thread(
  230. tools['data'].get_latest_rss,
  231. feeds=feeds, days=days, limit=limit, include_summary=include_summary
  232. )
  233. return json.dumps(result, ensure_ascii=False, indent=2)
  234. @mcp.tool
  235. async def search_rss(
  236. keyword: str,
  237. feeds: Optional[List[str]] = None,
  238. days: int = 7,
  239. limit: int = 50,
  240. include_summary: bool = False
  241. ) -> str:
  242. """
  243. 搜索 RSS 数据
  244. 在 RSS 订阅数据中搜索包含指定关键词的文章。
  245. Args:
  246. keyword: 搜索关键词(必需)
  247. feeds: RSS 源 ID 列表,如 ['hacker-news', '36kr']
  248. - 不指定时:搜索所有 RSS 源
  249. days: 搜索最近 N 天的数据,默认 7 天,最大 30 天
  250. limit: 返回条数限制,默认50
  251. include_summary: 是否包含文章摘要,默认False
  252. Returns:
  253. JSON格式的匹配 RSS 条目列表
  254. Examples:
  255. - search_rss(keyword="AI")
  256. - search_rss(keyword="machine learning", feeds=['hacker-news'], days=14)
  257. """
  258. tools = _get_tools()
  259. result = await asyncio.to_thread(
  260. tools['data'].search_rss,
  261. keyword=keyword,
  262. feeds=feeds,
  263. days=days,
  264. limit=limit,
  265. include_summary=include_summary
  266. )
  267. return json.dumps(result, ensure_ascii=False, indent=2)
  268. @mcp.tool
  269. async def get_rss_feeds_status() -> str:
  270. """
  271. 获取 RSS 源状态信息
  272. 查看当前配置的 RSS 源及其数据统计信息。
  273. Returns:
  274. JSON格式的 RSS 源状态,包含:
  275. - available_dates: 有 RSS 数据的日期列表
  276. - total_dates: 总日期数
  277. - today_feeds: 今日各 RSS 源的数据统计
  278. - {feed_id}: { name, item_count }
  279. - generated_at: 生成时间
  280. Examples:
  281. - get_rss_feeds_status() # 查看所有 RSS 源状态
  282. """
  283. tools = _get_tools()
  284. result = await asyncio.to_thread(tools['data'].get_rss_feeds_status)
  285. return json.dumps(result, ensure_ascii=False, indent=2)
  286. @mcp.tool
  287. async def get_news_by_date(
  288. date_range: Optional[Union[Dict[str, str], str]] = None,
  289. platforms: Optional[List[str]] = None,
  290. limit: int = 50,
  291. include_url: bool = False
  292. ) -> str:
  293. """
  294. 获取指定日期的新闻数据,用于历史数据分析和对比
  295. Args:
  296. date_range: 日期范围,支持多种格式:
  297. - 范围对象: {"start": "2025-01-01", "end": "2025-01-07"}
  298. - 自然语言: "今天", "昨天", "本周", "最近7天"
  299. - 单日字符串: "2025-01-15"
  300. - 默认值: "今天"
  301. platforms: 平台ID列表,如 ['zhihu', 'weibo'],不指定则使用所有平台
  302. limit: 返回条数限制,默认50,最大1000
  303. include_url: 是否包含URL链接,默认False(节省token)
  304. Returns:
  305. JSON格式的新闻列表,包含标题、平台、排名等信息
  306. """
  307. tools = _get_tools()
  308. result = await asyncio.to_thread(
  309. tools['data'].get_news_by_date,
  310. date_range=date_range,
  311. platforms=platforms,
  312. limit=limit,
  313. include_url=include_url
  314. )
  315. return json.dumps(result, ensure_ascii=False, indent=2)
  316. # ==================== 高级数据分析工具 ====================
  317. @mcp.tool
  318. async def analyze_topic_trend(
  319. topic: str,
  320. analysis_type: str = "trend",
  321. date_range: Optional[Union[Dict[str, str], str]] = None,
  322. granularity: str = "day",
  323. spike_threshold: float = 3.0,
  324. time_window: int = 24,
  325. lookahead_hours: int = 6,
  326. confidence_threshold: float = 0.7
  327. ) -> str:
  328. """
  329. 统一话题趋势分析工具 - 整合多种趋势分析模式
  330. 建议:使用自然语言日期时,先调用 resolve_date_range 获取精确日期范围。
  331. Args:
  332. topic: 话题关键词(必需)
  333. analysis_type: 分析类型
  334. - "trend": 热度趋势分析(默认)
  335. - "lifecycle": 生命周期分析
  336. - "viral": 异常热度检测
  337. - "predict": 话题预测
  338. date_range: 日期范围,格式 {"start": "YYYY-MM-DD", "end": "YYYY-MM-DD"},默认最近7天
  339. granularity: 时间粒度,默认"day"
  340. spike_threshold: 热度突增倍数阈值(viral模式),默认3.0
  341. time_window: 检测时间窗口小时数(viral模式),默认24
  342. lookahead_hours: 预测未来小时数(predict模式),默认6
  343. confidence_threshold: 置信度阈值(predict模式),默认0.7
  344. Returns:
  345. JSON格式的趋势分析结果
  346. Examples:
  347. - analyze_topic_trend(topic="AI", date_range={"start": "2025-01-01", "end": "2025-01-07"})
  348. - analyze_topic_trend(topic="特斯拉", analysis_type="lifecycle")
  349. """
  350. tools = _get_tools()
  351. result = await asyncio.to_thread(
  352. tools['analytics'].analyze_topic_trend_unified,
  353. topic=topic,
  354. analysis_type=analysis_type,
  355. date_range=date_range,
  356. granularity=granularity,
  357. threshold=spike_threshold,
  358. time_window=time_window,
  359. lookahead_hours=lookahead_hours,
  360. confidence_threshold=confidence_threshold
  361. )
  362. return json.dumps(result, ensure_ascii=False, indent=2)
  363. @mcp.tool
  364. async def analyze_data_insights(
  365. insight_type: str = "platform_compare",
  366. topic: Optional[str] = None,
  367. date_range: Optional[Union[Dict[str, str], str]] = None,
  368. min_frequency: int = 3,
  369. top_n: int = 20
  370. ) -> str:
  371. """
  372. 统一数据洞察分析工具 - 整合多种数据分析模式
  373. Args:
  374. insight_type: 洞察类型,可选值:
  375. - "platform_compare": 平台对比分析(对比不同平台对话题的关注度)
  376. - "platform_activity": 平台活跃度统计(统计各平台发布频率和活跃时间)
  377. - "keyword_cooccur": 关键词共现分析(分析关键词同时出现的模式)
  378. topic: 话题关键词(可选,platform_compare模式适用)
  379. date_range: **【对象类型】** 日期范围(可选)
  380. - **格式**: {"start": "YYYY-MM-DD", "end": "YYYY-MM-DD"}
  381. - **示例**: {"start": "2025-01-01", "end": "2025-01-07"}
  382. - **重要**: 必须是对象格式,不能传递整数
  383. min_frequency: 最小共现频次(keyword_cooccur模式),默认3
  384. top_n: 返回TOP N结果(keyword_cooccur模式),默认20
  385. Returns:
  386. JSON格式的数据洞察分析结果
  387. Examples:
  388. - analyze_data_insights(insight_type="platform_compare", topic="人工智能")
  389. - analyze_data_insights(insight_type="platform_activity", date_range={"start": "2025-01-01", "end": "2025-01-07"})
  390. - analyze_data_insights(insight_type="keyword_cooccur", min_frequency=5, top_n=15)
  391. """
  392. tools = _get_tools()
  393. result = await asyncio.to_thread(
  394. tools['analytics'].analyze_data_insights_unified,
  395. insight_type=insight_type,
  396. topic=topic,
  397. date_range=date_range,
  398. min_frequency=min_frequency,
  399. top_n=top_n
  400. )
  401. return json.dumps(result, ensure_ascii=False, indent=2)
  402. @mcp.tool
  403. async def analyze_sentiment(
  404. topic: Optional[str] = None,
  405. platforms: Optional[List[str]] = None,
  406. date_range: Optional[Union[Dict[str, str], str]] = None,
  407. limit: int = 50,
  408. sort_by_weight: bool = True,
  409. include_url: bool = False
  410. ) -> str:
  411. """
  412. 分析新闻的情感倾向和热度趋势
  413. 建议:使用自然语言日期时,先调用 resolve_date_range 获取精确日期范围。
  414. Args:
  415. topic: 话题关键词(可选)
  416. platforms: 平台ID列表,如 ['zhihu', 'weibo'],不指定则使用所有平台
  417. date_range: 日期范围,格式 {"start": "YYYY-MM-DD", "end": "YYYY-MM-DD"},默认今天
  418. limit: 返回新闻数量,默认50,最大100(会对标题去重)
  419. sort_by_weight: 是否按热度权重排序,默认True
  420. include_url: 是否包含URL链接,默认False(节省token)
  421. Returns:
  422. JSON格式的分析结果,包含情感分布、热度趋势和相关新闻
  423. Examples:
  424. - analyze_sentiment(topic="AI", date_range={"start": "2025-01-01", "end": "2025-01-07"})
  425. """
  426. tools = _get_tools()
  427. result = await asyncio.to_thread(
  428. tools['analytics'].analyze_sentiment,
  429. topic=topic,
  430. platforms=platforms,
  431. date_range=date_range,
  432. limit=limit,
  433. sort_by_weight=sort_by_weight,
  434. include_url=include_url
  435. )
  436. return json.dumps(result, ensure_ascii=False, indent=2)
  437. @mcp.tool
  438. async def find_related_news(
  439. reference_title: str,
  440. date_range: Optional[Union[Dict[str, str], str]] = None,
  441. threshold: float = 0.5,
  442. limit: int = 50,
  443. include_url: bool = False
  444. ) -> str:
  445. """
  446. 查找与指定新闻标题相关的其他新闻(支持当天和历史数据)
  447. Args:
  448. reference_title: 参考新闻标题(完整或部分)
  449. date_range: 日期范围(可选)
  450. - 不指定: 只查询今天的数据
  451. - "today", "yesterday", "last_week", "last_month": 预设值
  452. - {"start": "YYYY-MM-DD", "end": "YYYY-MM-DD"}: 自定义范围
  453. threshold: 相似度阈值,0-1之间,默认0.5(越高匹配越严格)
  454. limit: 返回条数限制,默认50
  455. include_url: 是否包含URL链接,默认False(节省token)
  456. Returns:
  457. JSON格式的相关新闻列表,按相似度排序
  458. Examples:
  459. - find_related_news(reference_title="特斯拉降价")
  460. - find_related_news(reference_title="AI突破", date_range="last_week")
  461. """
  462. tools = _get_tools()
  463. result = await asyncio.to_thread(
  464. tools['search'].find_related_news_unified,
  465. reference_title=reference_title,
  466. date_range=date_range,
  467. threshold=threshold,
  468. limit=limit,
  469. include_url=include_url
  470. )
  471. return json.dumps(result, ensure_ascii=False, indent=2)
  472. @mcp.tool
  473. async def generate_summary_report(
  474. report_type: str = "daily",
  475. date_range: Optional[Union[Dict[str, str], str]] = None
  476. ) -> str:
  477. """
  478. 每日/每周摘要生成器 - 自动生成热点摘要报告
  479. Args:
  480. report_type: 报告类型(daily/weekly)
  481. date_range: **【对象类型】** 自定义日期范围(可选)
  482. - **格式**: {"start": "YYYY-MM-DD", "end": "YYYY-MM-DD"}
  483. - **示例**: {"start": "2025-01-01", "end": "2025-01-07"}
  484. - **重要**: 必须是对象格式,不能传递整数
  485. Returns:
  486. JSON格式的摘要报告,包含Markdown格式内容
  487. """
  488. tools = _get_tools()
  489. result = await asyncio.to_thread(
  490. tools['analytics'].generate_summary_report,
  491. report_type=report_type,
  492. date_range=date_range
  493. )
  494. return json.dumps(result, ensure_ascii=False, indent=2)
  495. @mcp.tool
  496. async def aggregate_news(
  497. date_range: Optional[Union[Dict[str, str], str]] = None,
  498. platforms: Optional[List[str]] = None,
  499. similarity_threshold: float = 0.7,
  500. limit: int = 50,
  501. include_url: bool = False
  502. ) -> str:
  503. """
  504. 跨平台新闻聚合 - 对相似新闻进行去重合并
  505. 将不同平台报道的同一事件合并为一条聚合新闻,显示跨平台覆盖情况和综合热度。
  506. Args:
  507. date_range: 日期范围,不指定则查询今天
  508. platforms: 平台ID列表,如 ['zhihu', 'weibo'],不指定则使用所有平台
  509. similarity_threshold: 相似度阈值,0.3-1.0,默认0.7(越高越严格)
  510. limit: 返回聚合新闻数量,默认50
  511. include_url: 是否包含URL链接,默认False
  512. Returns:
  513. JSON格式的聚合结果,包含去重统计、聚合新闻列表和平台覆盖统计
  514. Examples:
  515. - aggregate_news()
  516. - aggregate_news(similarity_threshold=0.8)
  517. """
  518. tools = _get_tools()
  519. result = await asyncio.to_thread(
  520. tools['analytics'].aggregate_news,
  521. date_range=date_range,
  522. platforms=platforms,
  523. similarity_threshold=similarity_threshold,
  524. limit=limit,
  525. include_url=include_url
  526. )
  527. return json.dumps(result, ensure_ascii=False, indent=2)
  528. @mcp.tool
  529. async def compare_periods(
  530. period1: Union[Dict[str, str], str],
  531. period2: Union[Dict[str, str], str],
  532. topic: Optional[str] = None,
  533. compare_type: str = "overview",
  534. platforms: Optional[List[str]] = None,
  535. top_n: int = 10
  536. ) -> str:
  537. """
  538. 时期对比分析 - 比较两个时间段的新闻数据
  539. 对比不同时期的热点话题、平台活跃度、新闻数量等维度。
  540. **使用场景:**
  541. - 对比本周和上周的热点变化
  542. - 分析某个话题在两个时期的热度差异
  543. - 查看各平台活跃度的周期性变化
  544. Args:
  545. period1: 第一个时间段(基准期)
  546. - {"start": "YYYY-MM-DD", "end": "YYYY-MM-DD"}: 日期范围
  547. - "today", "yesterday", "this_week", "last_week", "this_month", "last_month": 预设值
  548. period2: 第二个时间段(对比期,格式同 period1)
  549. topic: 可选的话题关键词(聚焦特定话题的对比)
  550. compare_type: 对比类型
  551. - "overview": 总体概览(默认)- 新闻数量、关键词变化、TOP新闻
  552. - "topic_shift": 话题变化分析 - 上升话题、下降话题、新出现话题
  553. - "platform_activity": 平台活跃度对比 - 各平台新闻数量变化
  554. platforms: 平台过滤列表,如 ['zhihu', 'weibo']
  555. top_n: 返回 TOP N 结果,默认10
  556. Returns:
  557. JSON格式的对比分析结果,包含:
  558. - periods: 两个时期的日期范围
  559. - compare_type: 对比类型
  560. - overview/topic_shift/platform_comparison: 具体对比结果(根据类型)
  561. Examples:
  562. - compare_periods(period1="last_week", period2="this_week") # 周环比
  563. - compare_periods(period1="last_month", period2="this_month", compare_type="topic_shift")
  564. - compare_periods(
  565. period1={"start": "2025-01-01", "end": "2025-01-07"},
  566. period2={"start": "2025-01-08", "end": "2025-01-14"},
  567. topic="人工智能"
  568. )
  569. """
  570. tools = _get_tools()
  571. result = await asyncio.to_thread(
  572. tools['analytics'].compare_periods,
  573. period1=period1,
  574. period2=period2,
  575. topic=topic,
  576. compare_type=compare_type,
  577. platforms=platforms,
  578. top_n=top_n
  579. )
  580. return json.dumps(result, ensure_ascii=False, indent=2)
  581. # ==================== 智能检索工具 ====================
  582. @mcp.tool
  583. async def search_news(
  584. query: str,
  585. search_mode: str = "keyword",
  586. date_range: Optional[Union[Dict[str, str], str]] = None,
  587. platforms: Optional[List[str]] = None,
  588. limit: int = 50,
  589. sort_by: str = "relevance",
  590. threshold: float = 0.6,
  591. include_url: bool = False,
  592. include_rss: bool = False,
  593. rss_limit: int = 20
  594. ) -> str:
  595. """
  596. 统一搜索接口,支持多种搜索模式,可同时搜索热榜和RSS
  597. 建议:使用自然语言日期时,先调用 resolve_date_range 获取精确日期范围。
  598. Args:
  599. query: 搜索关键词或内容片段
  600. search_mode: 搜索模式
  601. - "keyword": 精确关键词匹配(默认)
  602. - "fuzzy": 模糊内容匹配
  603. - "entity": 实体名称搜索(人物/地点/机构)
  604. date_range: 日期范围,格式 {"start": "YYYY-MM-DD", "end": "YYYY-MM-DD"},默认今天
  605. platforms: 平台ID列表,如 ['zhihu', 'weibo'],不指定则使用所有平台
  606. limit: 热榜返回条数限制,默认50
  607. sort_by: 排序方式 - "relevance"(相关度)/ "weight"(权重)/ "date"(日期)
  608. threshold: 相似度阈值(仅fuzzy模式),0-1,默认0.6
  609. include_url: 是否包含URL链接,默认False
  610. include_rss: 是否同时搜索RSS数据,默认False
  611. rss_limit: RSS返回条数限制,默认20
  612. Returns:
  613. JSON格式的搜索结果,包含热榜新闻列表和可选的RSS结果
  614. Examples:
  615. - search_news(query="AI")
  616. - search_news(query="AI", include_rss=True)
  617. - search_news(query="特斯拉", date_range={"start": "2025-01-01", "end": "2025-01-07"})
  618. """
  619. tools = _get_tools()
  620. result = await asyncio.to_thread(
  621. tools['search'].search_news_unified,
  622. query=query,
  623. search_mode=search_mode,
  624. date_range=date_range,
  625. platforms=platforms,
  626. limit=limit,
  627. sort_by=sort_by,
  628. threshold=threshold,
  629. include_url=include_url,
  630. include_rss=include_rss,
  631. rss_limit=rss_limit
  632. )
  633. return json.dumps(result, ensure_ascii=False, indent=2)
  634. # ==================== 配置与系统管理工具 ====================
  635. @mcp.tool
  636. async def get_current_config(
  637. section: str = "all"
  638. ) -> str:
  639. """
  640. 获取当前系统配置
  641. Args:
  642. section: 配置节,可选值:
  643. - "all": 所有配置(默认)
  644. - "crawler": 爬虫配置
  645. - "push": 推送配置
  646. - "keywords": 关键词配置
  647. - "weights": 权重配置
  648. Returns:
  649. JSON格式的配置信息
  650. """
  651. tools = _get_tools()
  652. result = await asyncio.to_thread(tools['config'].get_current_config, section=section)
  653. return json.dumps(result, ensure_ascii=False, indent=2)
  654. @mcp.tool
  655. async def get_system_status() -> str:
  656. """
  657. 获取系统运行状态和健康检查信息
  658. 返回系统版本、数据统计、缓存状态等信息
  659. Returns:
  660. JSON格式的系统状态信息
  661. """
  662. tools = _get_tools()
  663. result = await asyncio.to_thread(tools['system'].get_system_status)
  664. return json.dumps(result, ensure_ascii=False, indent=2)
  665. @mcp.tool
  666. async def check_version(
  667. proxy_url: Optional[str] = None
  668. ) -> str:
  669. """
  670. 检查版本更新(同时检查 TrendRadar 和 MCP Server)
  671. 比较本地版本与 GitHub 远程版本,判断是否需要更新。
  672. Args:
  673. proxy_url: 可选的代理URL,用于访问 GitHub(如 http://127.0.0.1:7890)
  674. Returns:
  675. JSON格式的版本检查结果,包含两个组件的版本对比和是否需要更新
  676. Examples:
  677. - check_version()
  678. - check_version(proxy_url="http://127.0.0.1:7890")
  679. """
  680. tools = _get_tools()
  681. result = await asyncio.to_thread(tools['system'].check_version, proxy_url=proxy_url)
  682. return json.dumps(result, ensure_ascii=False, indent=2)
  683. @mcp.tool
  684. async def trigger_crawl(
  685. platforms: Optional[List[str]] = None,
  686. save_to_local: bool = False,
  687. include_url: bool = False
  688. ) -> str:
  689. """
  690. 手动触发一次爬取任务(可选持久化)
  691. Args:
  692. platforms: 平台ID列表,如 ['zhihu', 'weibo'],不指定则使用所有平台
  693. save_to_local: 是否保存到本地 output 目录,默认 False
  694. include_url: 是否包含URL链接,默认False(节省token)
  695. Returns:
  696. JSON格式的任务状态信息,包含成功/失败平台列表和新闻数据
  697. Examples:
  698. - trigger_crawl(platforms=['zhihu'])
  699. - trigger_crawl(save_to_local=True)
  700. """
  701. tools = _get_tools()
  702. result = await asyncio.to_thread(
  703. tools['system'].trigger_crawl,
  704. platforms=platforms, save_to_local=save_to_local, include_url=include_url
  705. )
  706. return json.dumps(result, ensure_ascii=False, indent=2)
  707. # ==================== 存储同步工具 ====================
  708. @mcp.tool
  709. async def sync_from_remote(
  710. days: int = 7
  711. ) -> str:
  712. """
  713. 从远程存储拉取数据到本地
  714. 用于 MCP Server 等场景:爬虫存到远程云存储(如 Cloudflare R2),
  715. MCP Server 拉取到本地进行分析查询。
  716. Args:
  717. days: 拉取最近 N 天的数据,默认 7 天
  718. - 0: 不拉取
  719. - 7: 拉取最近一周的数据
  720. - 30: 拉取最近一个月的数据
  721. Returns:
  722. JSON格式的同步结果,包含:
  723. - success: 是否成功
  724. - synced_files: 成功同步的文件数量
  725. - synced_dates: 成功同步的日期列表
  726. - skipped_dates: 跳过的日期(本地已存在)
  727. - failed_dates: 失败的日期及错误信息
  728. - message: 操作结果描述
  729. Examples:
  730. - sync_from_remote() # 拉取最近7天
  731. - sync_from_remote(days=30) # 拉取最近30天
  732. Note:
  733. 需要在 config/config.yaml 中配置远程存储(storage.remote)或设置环境变量:
  734. - S3_ENDPOINT_URL: 服务端点
  735. - S3_BUCKET_NAME: 存储桶名称
  736. - S3_ACCESS_KEY_ID: 访问密钥 ID
  737. - S3_SECRET_ACCESS_KEY: 访问密钥
  738. """
  739. tools = _get_tools()
  740. result = await asyncio.to_thread(tools['storage'].sync_from_remote, days=days)
  741. return json.dumps(result, ensure_ascii=False, indent=2)
  742. @mcp.tool
  743. async def get_storage_status() -> str:
  744. """
  745. 获取存储配置和状态
  746. 查看当前存储后端配置、本地和远程存储的状态信息。
  747. Returns:
  748. JSON格式的存储状态信息,包含本地/远程存储状态和拉取配置
  749. """
  750. tools = _get_tools()
  751. result = await asyncio.to_thread(tools['storage'].get_storage_status)
  752. return json.dumps(result, ensure_ascii=False, indent=2)
  753. @mcp.tool
  754. async def list_available_dates(
  755. source: str = "both"
  756. ) -> str:
  757. """
  758. 列出本地/远程可用的日期范围
  759. 查看本地和远程存储中有哪些日期的数据可用。
  760. Args:
  761. source: 数据来源
  762. - "local": 仅本地
  763. - "remote": 仅远程
  764. - "both": 同时列出并对比(默认)
  765. Returns:
  766. JSON格式的日期列表,包含各来源的日期信息和对比结果
  767. Examples:
  768. - list_available_dates()
  769. - list_available_dates(source="local")
  770. """
  771. tools = _get_tools()
  772. result = await asyncio.to_thread(tools['storage'].list_available_dates, source=source)
  773. return json.dumps(result, ensure_ascii=False, indent=2)
  774. # ==================== 文章内容读取工具 ====================
  775. @mcp.tool
  776. async def read_article(
  777. url: str,
  778. timeout: int = 30
  779. ) -> str:
  780. """
  781. 读取指定 URL 的文章内容,返回 LLM 友好的 Markdown 格式
  782. 通过 Jina AI Reader 将网页转换为干净的 Markdown,自动去除广告、导航栏等噪音内容。
  783. 适合用于:阅读新闻正文、获取文章详情、分析文章内容。
  784. **典型使用流程:**
  785. 1. 先用 search_news(include_url=True) 搜索新闻获取链接
  786. 2. 再用 read_article(url=链接) 读取正文内容
  787. 3. AI 对 Markdown 正文进行分析、摘要、翻译等
  788. Args:
  789. url: 文章链接(必需),以 http:// 或 https:// 开头
  790. timeout: 请求超时时间(秒),默认 30,最大 60
  791. Returns:
  792. JSON格式的文章内容,包含完整 Markdown 正文
  793. Examples:
  794. - read_article(url="https://example.com/news/123")
  795. Note:
  796. - 使用 Jina AI Reader 免费服务(100 RPM 限制)
  797. - 每次请求间隔 5 秒(内置速率控制)
  798. - 部分付费墙/登录墙页面可能无法完整获取
  799. """
  800. tools = _get_tools()
  801. timeout = min(max(timeout, 10), 60)
  802. result = await asyncio.to_thread(
  803. tools['article'].read_article,
  804. url=url, timeout=timeout
  805. )
  806. return json.dumps(result, ensure_ascii=False, indent=2)
  807. @mcp.tool
  808. async def read_articles_batch(
  809. urls: List[str],
  810. timeout: int = 30
  811. ) -> str:
  812. """
  813. 批量读取多篇文章内容(最多 5 篇,间隔 5 秒)
  814. 逐篇请求文章内容,每篇之间自动间隔 5 秒以遵守速率限制。
  815. **典型使用流程:**
  816. 1. 先用 search_news(include_url=True) 搜索新闻获取多个链接
  817. 2. 再用 read_articles_batch(urls=[...]) 批量读取正文
  818. 3. AI 对多篇文章进行对比分析、综合报告
  819. Args:
  820. urls: 文章链接列表(必需),最多处理 5 篇
  821. timeout: 每篇的请求超时时间(秒),默认 30
  822. Returns:
  823. JSON格式的批量读取结果,包含每篇的完整内容和状态
  824. Examples:
  825. - read_articles_batch(urls=["https://a.com/1", "https://b.com/2"])
  826. Note:
  827. - 单次最多读取 5 篇,超出部分会被跳过
  828. - 5 篇约需 25-30 秒(每篇间隔 5 秒)
  829. - 单篇失败不影响其他篇的读取
  830. """
  831. tools = _get_tools()
  832. timeout = min(max(timeout, 10), 60)
  833. result = await asyncio.to_thread(
  834. tools['article'].read_articles_batch,
  835. urls=urls, timeout=timeout
  836. )
  837. return json.dumps(result, ensure_ascii=False, indent=2)
  838. # ==================== 启动入口 ====================
  839. def run_server(
  840. project_root: Optional[str] = None,
  841. transport: str = 'stdio',
  842. host: str = '0.0.0.0',
  843. port: int = 3333
  844. ):
  845. """
  846. 启动 MCP 服务器
  847. Args:
  848. project_root: 项目根目录路径
  849. transport: 传输模式,'stdio' 或 'http'
  850. host: HTTP模式的监听地址,默认 0.0.0.0
  851. port: HTTP模式的监听端口,默认 3333
  852. """
  853. # 初始化工具实例
  854. _get_tools(project_root)
  855. # 打印启动信息
  856. print()
  857. print("=" * 60)
  858. print(" TrendRadar MCP Server - FastMCP 2.0")
  859. print("=" * 60)
  860. print(f" 传输模式: {transport.upper()}")
  861. if transport == 'stdio':
  862. print(" 协议: MCP over stdio (标准输入输出)")
  863. print(" 说明: 通过标准输入输出与 MCP 客户端通信")
  864. elif transport == 'http':
  865. print(f" 协议: MCP over HTTP (生产环境)")
  866. print(f" 服务器监听: {host}:{port}")
  867. if project_root:
  868. print(f" 项目目录: {project_root}")
  869. else:
  870. print(" 项目目录: 当前目录")
  871. print()
  872. print(" 已注册的工具:")
  873. print(" === 日期解析工具(推荐优先调用)===")
  874. print(" 0. resolve_date_range - 解析自然语言日期为标准格式")
  875. print()
  876. print(" === 基础数据查询(P0核心)===")
  877. print(" 1. get_latest_news - 获取最新新闻")
  878. print(" 2. get_news_by_date - 按日期查询新闻(支持自然语言)")
  879. print(" 3. get_trending_topics - 获取趋势话题(支持自动提取)")
  880. print()
  881. print(" === RSS 数据查询 ===")
  882. print(" 4. get_latest_rss - 获取最新 RSS 订阅数据")
  883. print(" 5. search_rss - 搜索 RSS 数据")
  884. print(" 6. get_rss_feeds_status - 获取 RSS 源状态")
  885. print()
  886. print(" === 智能检索工具 ===")
  887. print(" 7. search_news - 统一新闻搜索(关键词/模糊/实体)")
  888. print(" 8. find_related_news - 相关新闻查找(支持历史数据)")
  889. print()
  890. print(" === 高级数据分析 ===")
  891. print(" 9. analyze_topic_trend - 统一话题趋势分析(热度/生命周期/爆火/预测)")
  892. print(" 10. analyze_data_insights - 统一数据洞察分析(平台对比/活跃度/关键词共现)")
  893. print(" 11. analyze_sentiment - 情感倾向分析")
  894. print(" 12. aggregate_news - 跨平台新闻聚合去重")
  895. print(" 13. compare_periods - 时期对比分析(周环比/月环比)")
  896. print(" 14. generate_summary_report - 每日/每周摘要生成")
  897. print()
  898. print(" === 配置与系统管理 ===")
  899. print(" 15. get_current_config - 获取当前系统配置")
  900. print(" 16. get_system_status - 获取系统运行状态")
  901. print(" 17. check_version - 检查版本更新(对比本地与远程版本)")
  902. print(" 18. trigger_crawl - 手动触发爬取任务")
  903. print()
  904. print(" === 存储同步工具 ===")
  905. print(" 19. sync_from_remote - 从远程存储拉取数据到本地")
  906. print(" 20. get_storage_status - 获取存储配置和状态")
  907. print(" 21. list_available_dates - 列出本地/远程可用日期")
  908. print()
  909. print(" === 文章内容读取 ===")
  910. print(" 22. read_article - 读取单篇文章内容(Markdown格式)")
  911. print(" 23. read_articles_batch - 批量读取多篇文章(自动限速)")
  912. print("=" * 60)
  913. print()
  914. # 根据传输模式运行服务器
  915. if transport == 'stdio':
  916. mcp.run(transport='stdio')
  917. elif transport == 'http':
  918. # HTTP 模式(生产推荐)
  919. mcp.run(
  920. transport='http',
  921. host=host,
  922. port=port,
  923. path='/mcp' # HTTP 端点路径
  924. )
  925. else:
  926. raise ValueError(f"不支持的传输模式: {transport}")
  927. if __name__ == '__main__':
  928. import argparse
  929. parser = argparse.ArgumentParser(
  930. description='TrendRadar MCP Server - 新闻热点聚合 MCP 工具服务器',
  931. formatter_class=argparse.RawDescriptionHelpFormatter,
  932. epilog="""
  933. 详细配置教程请查看: README-Cherry-Studio.md
  934. """
  935. )
  936. parser.add_argument(
  937. '--transport',
  938. choices=['stdio', 'http'],
  939. default='stdio',
  940. help='传输模式:stdio (默认) 或 http (生产环境)'
  941. )
  942. parser.add_argument(
  943. '--host',
  944. default='0.0.0.0',
  945. help='HTTP模式的监听地址,默认 0.0.0.0'
  946. )
  947. parser.add_argument(
  948. '--port',
  949. type=int,
  950. default=3333,
  951. help='HTTP模式的监听端口,默认 3333'
  952. )
  953. parser.add_argument(
  954. '--project-root',
  955. help='项目根目录路径'
  956. )
  957. args = parser.parse_args()
  958. run_server(
  959. project_root=args.project_root,
  960. transport=args.transport,
  961. host=args.host,
  962. port=args.port
  963. )