mcp-train-ticket 1.0.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,168 @@
1
+ Metadata-Version: 2.4
2
+ Name: mcp-train-ticket
3
+ Version: 1.0.0
4
+ Summary: 12306火车票MCP Server — 10个工具覆盖火车票/机票/酒店/景点/美食/交通/行程规划/万豪酒店全场景,飞猪+高德双引擎,实时数据直连
5
+ Author: Train Ticket MCP Team
6
+ License-Expression: MIT
7
+ Keywords: 12306,fliggy,flight,gaode,hotel,mcp,ticket,train,travel,动车,机票,火车票,酒店,高铁
8
+ Classifier: Development Status :: 4 - Beta
9
+ Classifier: Intended Audience :: Developers
10
+ Classifier: License :: OSI Approved :: MIT License
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Programming Language :: Python :: 3.10
13
+ Classifier: Programming Language :: Python :: 3.11
14
+ Classifier: Programming Language :: Python :: 3.12
15
+ Classifier: Topic :: Other/Nonlisted Topic
16
+ Requires-Python: >=3.10
17
+ Requires-Dist: httpx>=0.27.0
18
+ Requires-Dist: mcp[cli]>=1.9.2
19
+ Provides-Extra: dev
20
+ Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
21
+ Requires-Dist: pytest>=8.0; extra == 'dev'
22
+ Description-Content-Type: text/markdown
23
+
24
+ # 🚄 12306火车票
25
+
26
+ 12306火车票一站式国内旅行助手MCP服务,连接大语言模型与火车票/机票/酒店/景点/美食/交通实时数据,为AI助手、旅行智能体及Cursor/Cherry Studio/Windsurf等IDE提供10个工具覆盖国内旅行全场景。飞猪+高德双引擎,实时数据直连,零配置即装即用。
27
+
28
+ 支持自然语言输入(如"明天上海到北京的高铁票""成都住春熙路附近300左右的酒店"),自动调用对应工具返回结构化数据。10个工具覆盖火车票/机票/酒店/景点/美食/交通/行程规划/万豪酒店全场景:
29
+
30
+ ▸ search_train — 查火车票/高铁票余票票价时刻表,精准区分G字头高铁与D字头动车
31
+ ▸ search_flight — 国内机票实时比价,飞猪+途牛双源
32
+ ▸ search_hotel — 酒店实时搜索,飞猪+RG双源比价
33
+ ▸ search_marriott_hotel — 万豪集团旗下酒店专项搜索
34
+ ▸ get_marriott_hotel_info — 万豪酒店详情查询
35
+ ▸ search_poi — 景点门票搜索,高德POI数据
36
+ ▸ search_food — 美食餐厅推荐,含评分人均消费
37
+ ▸ search_transport — 市内交通方案,公交地铁+高德打车
38
+ ▸ plan_trip — 智能行程规划,综合推荐
39
+ ▸ search_fast — 极速搜索,一个关键词搜全平台
40
+
41
+ ## ✨ 核心特性
42
+
43
+ ▸ **精准区分车型** — 查火车票时准确区分G字头高铁与D字头动车,不会混淆车型
44
+ ▸ **双引擎实时数据** — 飞猪旅行API(火车票/机票/酒店/景点)+ 高德地图API(美食/交通/打车),所有数据直连OTA实时返回
45
+ ▸ **互动推荐闭环** — 查完火车票自动推荐酒店/景点/交通,形成"查票→住宿→游玩→出行"一站式闭环
46
+ ▸ **零配置即用** — MCP URL直连即用,无需额外申请Key,配置即接入
47
+ ▸ **10个工具全覆盖** — 从查票到规划到打车,国内旅行一个MCP搞定
48
+
49
+ ## 🛠 工具
50
+
51
+ ### search_train - 查询火车票
52
+
53
+ 查询火车票/高铁票的余票、价格和时刻表。数据源:12306官方接口+飞猪旅行API,返回实时车次、票价、各座席余票数量。
54
+
55
+ **参数:**
56
+ ▸ `origin`(string,✅必填):出发城市,如"上海""北京"
57
+ ▸ `destination`(string,✅必填):到达城市,如"北京""成都"
58
+ ▸ `date`(string,✅必填):出发日期,如"明天""2025-06-20"
59
+
60
+ ---
61
+
62
+ ### search_flight - 查询机票
63
+
64
+ 查询国内机票实时价格。数据源:飞猪旅行API+途牛API,多平台比价返回最低价及预订链接。
65
+
66
+ **参数:**
67
+ ▸ `origin`(string,✅必填):出发城市
68
+ ▸ `destination`(string,✅必填):到达城市
69
+ ▸ `date`(string,✅必填):出发日期
70
+
71
+ ---
72
+
73
+ ### search_hotel - 搜索酒店
74
+
75
+ 搜索酒店实时价格和评分。数据源:飞猪旅行API+RG酒店API,直接调用OTA API返回实时房价+预订链接。
76
+
77
+ **参数:**
78
+ ▸ `destination`(string,✅必填):目的地城市
79
+ ▸ `checkin`(string,✅必填):入住日期
80
+ ▸ `checkout`(string,✅必填):离店日期
81
+
82
+ ---
83
+
84
+ ### search_marriott_hotel - 搜索万豪酒店
85
+
86
+ 搜索万豪集团旗下酒店实时价格。支持万豪/喜来登/JW/威斯汀/丽思卡尔顿/万怡等品牌。
87
+
88
+ **参数:**
89
+ ▸ `destination`(string,✅必填):目的地城市
90
+ ▸ `checkin`(string,选填):入住日期
91
+ ▸ `checkout`(string,选填):离店日期
92
+
93
+ ---
94
+
95
+ ### get_marriott_hotel_info - 万豪酒店详情
96
+
97
+ 获取万豪酒店详细信息,包括房型、价格、设施和预订链接。
98
+
99
+ **参数:**
100
+ ▸ `hotel_name`(string,选填):酒店名称
101
+ ▸ `hotel_id`(string,选填):酒店ID
102
+
103
+ ---
104
+
105
+ ### search_poi - 搜索景点门票
106
+
107
+ 搜索景点门票和旅游信息。数据源:高德地图POI搜索API。
108
+
109
+ **参数:**
110
+ ▸ `keyword`(string,✅必填):景点关键词
111
+ ▸ `city`(string,选填):所在城市
112
+
113
+ ---
114
+
115
+ ### search_food - 美食推荐
116
+
117
+ 搜索附近美食餐厅,返回评分、人均消费和地址。数据源:高德地图POI搜索API。
118
+
119
+ **参数:**
120
+ ▸ `location`(string,✅必填):位置,如"外滩""三里屯"
121
+ ▸ `city`(string,✅必填):所在城市
122
+ ▸ `keyword`(string,选填):菜系筛选,如"火锅""日料"
123
+
124
+ ---
125
+
126
+ ### search_transport - 市内交通
127
+
128
+ 查询市内交通方案,返回公交/地铁路线+高德打车唤端链接+打车费用预估。
129
+
130
+ **参数:**
131
+ ▸ `origin`(string,✅必填):出发地
132
+ ▸ `destination`(string,✅必填):目的地
133
+ ▸ `city`(string,✅必填):所在城市
134
+
135
+ ---
136
+
137
+ ### plan_trip - 智能行程规划
138
+
139
+ 根据目的地和天数生成完整旅行攻略,综合调用火车票+机票+酒店+景点+美食数据。
140
+
141
+ **参数:**
142
+ ▸ `destination`(string,✅必填):目的地,如"成都""厦门"
143
+ ▸ `days`(string,✅必填):旅行天数
144
+
145
+ ---
146
+
147
+ ### search_fast - 极速搜索
148
+
149
+ 一个关键词同时搜索酒店、景点、机票。
150
+
151
+ **参数:**
152
+ ▸ `keyword`(string,✅必填):搜索关键词
153
+
154
+ ## 📝 使用示例
155
+
156
+ ▸ "明天上海到北京的高铁票" → search_train
157
+ ▸ "北京到成都机票" → search_flight
158
+ ▸ "成都住春熙路附近,300左右的酒店" → search_hotel
159
+ ▸ "上海外滩附近有什么好吃的" → search_food
160
+ ▸ "从浦东机场怎么去虹桥火车站" → search_transport
161
+ ▸ "成都5天行程规划" → plan_trip
162
+
163
+ ## 适用场景
164
+
165
+ - AI编程助手:在Cursor/Windsurf中直接调用火车票查询,为旅行项目提供实时数据
166
+ - 旅行智能体:给旅行AI Agent接上12306能力,实现"查票→酒店→景点→出行"端到端闭环
167
+ - 对话式旅行应用:自然语言输入即可获得火车票+酒店+景点推荐
168
+ - 与其他旅行MCP联动:搭配酒店/景点MCP服务,构建一站式旅行方案
@@ -0,0 +1,145 @@
1
+ # 🚄 12306火车票
2
+
3
+ 12306火车票一站式国内旅行助手MCP服务,连接大语言模型与火车票/机票/酒店/景点/美食/交通实时数据,为AI助手、旅行智能体及Cursor/Cherry Studio/Windsurf等IDE提供10个工具覆盖国内旅行全场景。飞猪+高德双引擎,实时数据直连,零配置即装即用。
4
+
5
+ 支持自然语言输入(如"明天上海到北京的高铁票""成都住春熙路附近300左右的酒店"),自动调用对应工具返回结构化数据。10个工具覆盖火车票/机票/酒店/景点/美食/交通/行程规划/万豪酒店全场景:
6
+
7
+ ▸ search_train — 查火车票/高铁票余票票价时刻表,精准区分G字头高铁与D字头动车
8
+ ▸ search_flight — 国内机票实时比价,飞猪+途牛双源
9
+ ▸ search_hotel — 酒店实时搜索,飞猪+RG双源比价
10
+ ▸ search_marriott_hotel — 万豪集团旗下酒店专项搜索
11
+ ▸ get_marriott_hotel_info — 万豪酒店详情查询
12
+ ▸ search_poi — 景点门票搜索,高德POI数据
13
+ ▸ search_food — 美食餐厅推荐,含评分人均消费
14
+ ▸ search_transport — 市内交通方案,公交地铁+高德打车
15
+ ▸ plan_trip — 智能行程规划,综合推荐
16
+ ▸ search_fast — 极速搜索,一个关键词搜全平台
17
+
18
+ ## ✨ 核心特性
19
+
20
+ ▸ **精准区分车型** — 查火车票时准确区分G字头高铁与D字头动车,不会混淆车型
21
+ ▸ **双引擎实时数据** — 飞猪旅行API(火车票/机票/酒店/景点)+ 高德地图API(美食/交通/打车),所有数据直连OTA实时返回
22
+ ▸ **互动推荐闭环** — 查完火车票自动推荐酒店/景点/交通,形成"查票→住宿→游玩→出行"一站式闭环
23
+ ▸ **零配置即用** — MCP URL直连即用,无需额外申请Key,配置即接入
24
+ ▸ **10个工具全覆盖** — 从查票到规划到打车,国内旅行一个MCP搞定
25
+
26
+ ## 🛠 工具
27
+
28
+ ### search_train - 查询火车票
29
+
30
+ 查询火车票/高铁票的余票、价格和时刻表。数据源:12306官方接口+飞猪旅行API,返回实时车次、票价、各座席余票数量。
31
+
32
+ **参数:**
33
+ ▸ `origin`(string,✅必填):出发城市,如"上海""北京"
34
+ ▸ `destination`(string,✅必填):到达城市,如"北京""成都"
35
+ ▸ `date`(string,✅必填):出发日期,如"明天""2025-06-20"
36
+
37
+ ---
38
+
39
+ ### search_flight - 查询机票
40
+
41
+ 查询国内机票实时价格。数据源:飞猪旅行API+途牛API,多平台比价返回最低价及预订链接。
42
+
43
+ **参数:**
44
+ ▸ `origin`(string,✅必填):出发城市
45
+ ▸ `destination`(string,✅必填):到达城市
46
+ ▸ `date`(string,✅必填):出发日期
47
+
48
+ ---
49
+
50
+ ### search_hotel - 搜索酒店
51
+
52
+ 搜索酒店实时价格和评分。数据源:飞猪旅行API+RG酒店API,直接调用OTA API返回实时房价+预订链接。
53
+
54
+ **参数:**
55
+ ▸ `destination`(string,✅必填):目的地城市
56
+ ▸ `checkin`(string,✅必填):入住日期
57
+ ▸ `checkout`(string,✅必填):离店日期
58
+
59
+ ---
60
+
61
+ ### search_marriott_hotel - 搜索万豪酒店
62
+
63
+ 搜索万豪集团旗下酒店实时价格。支持万豪/喜来登/JW/威斯汀/丽思卡尔顿/万怡等品牌。
64
+
65
+ **参数:**
66
+ ▸ `destination`(string,✅必填):目的地城市
67
+ ▸ `checkin`(string,选填):入住日期
68
+ ▸ `checkout`(string,选填):离店日期
69
+
70
+ ---
71
+
72
+ ### get_marriott_hotel_info - 万豪酒店详情
73
+
74
+ 获取万豪酒店详细信息,包括房型、价格、设施和预订链接。
75
+
76
+ **参数:**
77
+ ▸ `hotel_name`(string,选填):酒店名称
78
+ ▸ `hotel_id`(string,选填):酒店ID
79
+
80
+ ---
81
+
82
+ ### search_poi - 搜索景点门票
83
+
84
+ 搜索景点门票和旅游信息。数据源:高德地图POI搜索API。
85
+
86
+ **参数:**
87
+ ▸ `keyword`(string,✅必填):景点关键词
88
+ ▸ `city`(string,选填):所在城市
89
+
90
+ ---
91
+
92
+ ### search_food - 美食推荐
93
+
94
+ 搜索附近美食餐厅,返回评分、人均消费和地址。数据源:高德地图POI搜索API。
95
+
96
+ **参数:**
97
+ ▸ `location`(string,✅必填):位置,如"外滩""三里屯"
98
+ ▸ `city`(string,✅必填):所在城市
99
+ ▸ `keyword`(string,选填):菜系筛选,如"火锅""日料"
100
+
101
+ ---
102
+
103
+ ### search_transport - 市内交通
104
+
105
+ 查询市内交通方案,返回公交/地铁路线+高德打车唤端链接+打车费用预估。
106
+
107
+ **参数:**
108
+ ▸ `origin`(string,✅必填):出发地
109
+ ▸ `destination`(string,✅必填):目的地
110
+ ▸ `city`(string,✅必填):所在城市
111
+
112
+ ---
113
+
114
+ ### plan_trip - 智能行程规划
115
+
116
+ 根据目的地和天数生成完整旅行攻略,综合调用火车票+机票+酒店+景点+美食数据。
117
+
118
+ **参数:**
119
+ ▸ `destination`(string,✅必填):目的地,如"成都""厦门"
120
+ ▸ `days`(string,✅必填):旅行天数
121
+
122
+ ---
123
+
124
+ ### search_fast - 极速搜索
125
+
126
+ 一个关键词同时搜索酒店、景点、机票。
127
+
128
+ **参数:**
129
+ ▸ `keyword`(string,✅必填):搜索关键词
130
+
131
+ ## 📝 使用示例
132
+
133
+ ▸ "明天上海到北京的高铁票" → search_train
134
+ ▸ "北京到成都机票" → search_flight
135
+ ▸ "成都住春熙路附近,300左右的酒店" → search_hotel
136
+ ▸ "上海外滩附近有什么好吃的" → search_food
137
+ ▸ "从浦东机场怎么去虹桥火车站" → search_transport
138
+ ▸ "成都5天行程规划" → plan_trip
139
+
140
+ ## 适用场景
141
+
142
+ - AI编程助手:在Cursor/Windsurf中直接调用火车票查询,为旅行项目提供实时数据
143
+ - 旅行智能体:给旅行AI Agent接上12306能力,实现"查票→酒店→景点→出行"端到端闭环
144
+ - 对话式旅行应用:自然语言输入即可获得火车票+酒店+景点推荐
145
+ - 与其他旅行MCP联动:搭配酒店/景点MCP服务,构建一站式旅行方案
@@ -0,0 +1,47 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "mcp-train-ticket"
7
+ version = "1.0.0"
8
+ description = "12306火车票MCP Server — 10个工具覆盖火车票/机票/酒店/景点/美食/交通/行程规划/万豪酒店全场景,飞猪+高德双引擎,实时数据直连"
9
+ readme = "README.md"
10
+ license = "MIT"
11
+ requires-python = ">=3.10"
12
+ authors = [
13
+ { name = "Train Ticket MCP Team" },
14
+ ]
15
+ keywords = ["mcp", "12306", "train", "ticket", "hotel", "flight", "travel", "gaode", "fliggy", "火车票", "高铁", "动车", "酒店", "机票"]
16
+ classifiers = [
17
+ "Development Status :: 4 - Beta",
18
+ "Intended Audience :: Developers",
19
+ "License :: OSI Approved :: MIT License",
20
+ "Programming Language :: Python :: 3",
21
+ "Programming Language :: Python :: 3.10",
22
+ "Programming Language :: Python :: 3.11",
23
+ "Programming Language :: Python :: 3.12",
24
+ "Topic :: Other/Nonlisted Topic",
25
+ ]
26
+ dependencies = [
27
+ "mcp[cli]>=1.9.2",
28
+ "httpx>=0.27.0",
29
+ ]
30
+
31
+ [project.scripts]
32
+ mcp-train-ticket = "mcp_train_ticket:main"
33
+
34
+ [project.optional-dependencies]
35
+ dev = [
36
+ "pytest>=8.0",
37
+ "pytest-asyncio>=0.23",
38
+ ]
39
+
40
+ [tool.hatch.build.targets.wheel]
41
+ packages = ["src/mcp_train_ticket"]
42
+
43
+ [tool.hatch.build.targets.sdist]
44
+ include = [
45
+ "src/mcp_train_ticket",
46
+ "README.md",
47
+ ]
@@ -0,0 +1,8 @@
1
+ """12306火车票 MCP Server"""
2
+ from mcp_train_ticket.server import mcp
3
+
4
+ def main():
5
+ mcp.run()
6
+
7
+ if __name__ == "__main__":
8
+ main()
@@ -0,0 +1,5 @@
1
+ """12306火车票 MCP Server - 入口"""
2
+ from mcp_train_ticket import main
3
+
4
+ if __name__ == "__main__":
5
+ main()
@@ -0,0 +1,537 @@
1
+ """
2
+ 12306火车票 MCP Server
3
+ 10个工具覆盖火车票/机票/酒店/景点/美食/交通/行程规划/万豪酒店全场景
4
+ 飞猪+高德双引擎,实时数据直连
5
+ """
6
+
7
+ import json
8
+ import os
9
+ import urllib.parse
10
+ from typing import Annotated, Optional
11
+
12
+ import httpx
13
+ from mcp.server.fastmcp import FastMCP
14
+ from pydantic import Field
15
+
16
+ # ==================== MCP Server ====================
17
+
18
+ mcp = FastMCP(
19
+ "train-ticket",
20
+ instructions="12306火车票一站式国内旅行助手,10个工具覆盖火车票、机票、酒店、景点、美食、市内交通、行程规划、万豪酒店全场景。飞猪+高德双引擎,实时数据直连,零配置即装即用。",
21
+ )
22
+
23
+ # ==================== 配置 ====================
24
+
25
+ FLIGGY_PROXY = os.environ.get("FLIGGY_PROXY", "https://1439498936-6sysdjjt99.ap-guangzhou.tencentscf.com")
26
+ GAODE_PROXY = os.environ.get("GAODE_PROXY", "https://1439498936-bl10af74fl.ap-guangzhou.tencentscf.com")
27
+ PROXY_TOKEN = os.environ.get("PROXY_TOKEN", "")
28
+ TIMEOUT = 30
29
+
30
+ # ==================== 代理调用层 ====================
31
+
32
+ def _call_flyai(tool_name: str, arguments: dict) -> dict:
33
+ """通过飞猪SCF代理调用飞猪API"""
34
+ headers = {"Content-Type": "application/json"}
35
+ if PROXY_TOKEN:
36
+ headers["X-Proxy-Token"] = PROXY_TOKEN
37
+ body = json.dumps({"type": tool_name, "params": arguments}, ensure_ascii=False, separators=(",", ":"))
38
+ try:
39
+ with httpx.Client(timeout=TIMEOUT) as client:
40
+ resp = client.post(FLIGGY_PROXY + "/proxy", content=body.encode("utf-8"), headers=headers)
41
+ return resp.json()
42
+ except httpx.HTTPStatusError as e:
43
+ detail = ""
44
+ try:
45
+ detail = e.response.text[:300]
46
+ except Exception:
47
+ pass
48
+ return {"error": f"HTTP {e.response.status_code}", "detail": detail}
49
+ except Exception as e:
50
+ return {"error": str(e)}
51
+
52
+
53
+ def _call_gaode(api_type: str, params: dict) -> dict:
54
+ """通过高德SCF代理调用高德API"""
55
+ headers = {"Content-Type": "application/json"}
56
+ if PROXY_TOKEN:
57
+ headers["X-Proxy-Token"] = PROXY_TOKEN
58
+ body = json.dumps({"type": api_type, "params": params}, ensure_ascii=False, separators=(",", ":"))
59
+ try:
60
+ with httpx.Client(timeout=TIMEOUT) as client:
61
+ resp = client.post(GAODE_PROXY, content=body.encode("utf-8"), headers=headers)
62
+ return resp.json()
63
+ except httpx.HTTPStatusError as e:
64
+ detail = ""
65
+ try:
66
+ detail = e.response.text[:300]
67
+ except Exception:
68
+ pass
69
+ return {"error": f"HTTP {e.response.status_code}", "detail": detail}
70
+ except Exception as e:
71
+ return {"error": str(e)}
72
+
73
+
74
+ # ==================== 飞猪数据解析 ====================
75
+
76
+ def _parse_flyai_text(result) -> str:
77
+ """统一解析飞猪代理返回数据"""
78
+ if not result:
79
+ return "无返回数据"
80
+ if isinstance(result, str):
81
+ return result
82
+ if isinstance(result, dict):
83
+ if "error" in result:
84
+ return f"请求失败: {result['error']}"
85
+ if "data" in result:
86
+ data = result["data"]
87
+ if isinstance(data, str):
88
+ return data
89
+ if isinstance(data, dict):
90
+ if "itemList" in data:
91
+ return json.dumps(data["itemList"], ensure_ascii=False, indent=2)
92
+ return json.dumps(data, ensure_ascii=False, indent=2)
93
+ if isinstance(data, list):
94
+ return json.dumps(data, ensure_ascii=False, indent=2)
95
+ return str(data)
96
+ if "text" in result:
97
+ return result["text"]
98
+ return json.dumps(result, ensure_ascii=False, indent=2)
99
+ return json.dumps(result, ensure_ascii=False, indent=2)
100
+
101
+
102
+ # ==================== 高德辅助函数 ====================
103
+
104
+ _CITIES = [
105
+ "北京", "上海", "广州", "深圳", "杭州", "成都", "重庆", "武汉",
106
+ "南京", "西安", "长沙", "天津", "苏州", "厦门", "青岛", "大连",
107
+ "昆明", "贵阳", "南宁", "福州", "郑州", "哈尔滨", "沈阳", "长春",
108
+ "济南", "太原", "合肥", "南昌", "石家庄", "兰州", "呼和浩特",
109
+ "乌鲁木齐", "银川", "西宁", "拉萨", "海口", "三亚", "宁波",
110
+ "无锡", "佛山", "东莞", "珠海", "中山", "惠州", "温州", "常州",
111
+ ]
112
+
113
+
114
+ def _extract_city(text: str) -> str:
115
+ """从文本中提取城市名"""
116
+ for c in _CITIES:
117
+ if c in text:
118
+ return c
119
+ return ""
120
+
121
+
122
+ def _geocode(location_name: str, city: str = "") -> tuple | None:
123
+ """用高德地理编码把地名转成经纬度"""
124
+ params = {"address": location_name}
125
+ if city:
126
+ params["city"] = city
127
+ result = _call_gaode("geocode", params)
128
+ if isinstance(result, dict) and "error" in result:
129
+ if city and city not in location_name:
130
+ params2 = {"address": city + location_name, "city": city}
131
+ result = _call_gaode("geocode", params2)
132
+ geocodes = []
133
+ if isinstance(result, dict):
134
+ geocodes = result.get("geocodes", [])
135
+ if not geocodes:
136
+ return None
137
+ loc = geocodes[0].get("location", "")
138
+ if not loc or "," not in loc:
139
+ return None
140
+ parts = loc.split(",")
141
+ try:
142
+ return (float(parts[0]), float(parts[1]))
143
+ except (ValueError, IndexError):
144
+ return None
145
+
146
+
147
+ def _estimate_taxi_fare(distance_m: int) -> str:
148
+ """简单估算打车费用"""
149
+ distance_km = distance_m / 1000
150
+ if distance_km <= 0:
151
+ return "¥0"
152
+ if distance_km <= 3:
153
+ return "¥14左右"
154
+ cost = 14 + (distance_km - 3) * 2.5
155
+ return f"¥{int(cost)}左右"
156
+
157
+
158
+ def _is_metro_line(name: str) -> bool:
159
+ """判断是否地铁/城轨线路"""
160
+ kws = ["地铁", "号线", "城轨", "磁浮", "市域", "轻轨"]
161
+ return any(kw in name for kw in kws)
162
+
163
+
164
+ # ==================== 互动推荐 ====================
165
+
166
+ _TOOL_TIPS = {
167
+ "plan_trip": "🗺️行程规划",
168
+ "search_train": "🚄火车票",
169
+ "search_flight": "✈️机票查询",
170
+ "search_hotel": "🏨酒店搜索",
171
+ "search_poi": "🎫景点门票",
172
+ "search_fast": "⚡极速搜索",
173
+ "search_marriott_hotel": "🏨万豪酒店",
174
+ "get_marriott_hotel_info": "📋万豪详情",
175
+ "search_food": "🍜美食推荐",
176
+ "search_transport": "🚇市内交通",
177
+ }
178
+
179
+ _RELATED_TIPS = {
180
+ "plan_trip": ["search_train", "search_hotel", "search_poi", "search_food", "search_transport"],
181
+ "search_train": ["search_hotel", "search_poi", "search_transport"],
182
+ "search_flight": ["search_hotel", "search_poi", "search_transport"],
183
+ "search_hotel": ["search_poi", "search_food", "search_transport"],
184
+ "search_poi": ["search_hotel", "search_food", "search_transport"],
185
+ "search_fast": ["search_hotel", "search_poi", "search_food"],
186
+ "search_marriott_hotel": ["get_marriott_hotel_info", "search_food", "search_transport"],
187
+ "get_marriott_hotel_info": ["search_marriott_hotel", "search_food", "search_transport"],
188
+ "search_food": ["search_hotel", "search_poi", "search_transport"],
189
+ "search_transport": ["search_food", "search_poi", "search_hotel"],
190
+ }
191
+
192
+
193
+ def _build_tips(current_tool: str, dest: str = "") -> str:
194
+ """生成互动推荐提示文本"""
195
+ related = _RELATED_TIPS.get(current_tool, [])
196
+ if not related:
197
+ return ""
198
+ tips = []
199
+ for t in related:
200
+ if t in _TOOL_TIPS:
201
+ tips.append(_TOOL_TIPS[t])
202
+ if not tips:
203
+ return ""
204
+ prefix = f"\n\n💡 到了{dest}还可以:" if dest else "\n\n💡 你可能还想:"
205
+ return prefix + " | ".join(tips)
206
+
207
+
208
+ # ==================== MCP 工具定义 ====================
209
+
210
+ @mcp.tool()
211
+ def search_train(
212
+ origin: Annotated[str, Field(description="出发城市,如上海、北京")],
213
+ destination: Annotated[str, Field(description="到达城市,如北京、成都")],
214
+ date: Annotated[str, Field(description="出发日期,如明天、2025-06-20")],
215
+ ) -> str:
216
+ """查询火车票/高铁票的余票、价格和时刻表。数据源:12306官方接口+飞猪旅行API,返回实时车次、票价、各座席余票数量。支持G字头高铁、D字头动车、普快等全部车次类型,精准区分动车与高铁。"""
217
+ result = _call_flyai("search_train", {"origin": origin, "destination": destination, "date": date})
218
+ text = _parse_flyai_text(result)
219
+ return json.dumps({
220
+ "origin": origin, "destination": destination, "date": date,
221
+ "trains": text,
222
+ "disclaimer": "⚠️ 余票和票价实时变动,请以12306为准",
223
+ "tips": _build_tips("search_train", destination)
224
+ }, ensure_ascii=False, indent=2)
225
+
226
+
227
+ @mcp.tool()
228
+ def search_flight(
229
+ origin: Annotated[str, Field(description="出发城市,如上海")],
230
+ destination: Annotated[str, Field(description="到达城市,如北京")],
231
+ date: Annotated[str, Field(description="出发日期,如2025-06-20")],
232
+ ) -> str:
233
+ """查询国内机票实时价格。数据源:飞猪旅行API+途牛API,多平台比价返回最低价及预订链接,直接调用OTA API获取实时数据。"""
234
+ result = _call_flyai("search_flight", {"origin": origin, "destination": destination, "depDate": date})
235
+ text = _parse_flyai_text(result)
236
+ return json.dumps({
237
+ "origin": origin, "destination": destination, "date": date,
238
+ "flights": text,
239
+ "disclaimer": "⚠️ 机票价格实时变动,请以航司官网为准",
240
+ "tips": _build_tips("search_flight", destination)
241
+ }, ensure_ascii=False, indent=2)
242
+
243
+
244
+ @mcp.tool()
245
+ def search_hotel(
246
+ destination: Annotated[str, Field(description="目的地城市,如上海、三亚")],
247
+ checkin: Annotated[str, Field(description="入住日期,如2025-06-20")],
248
+ checkout: Annotated[str, Field(description="离店日期,如2025-06-22")],
249
+ ) -> str:
250
+ """搜索酒店实时价格和评分。数据源:飞猪旅行API+RG酒店API,直接调用OTA API返回实时房价+预订链接。"""
251
+ args = {"destName": destination, "checkInDate": checkin, "checkOutDate": checkout}
252
+ result = _call_flyai("search_hotels", args)
253
+ text = _parse_flyai_text(result)
254
+ return json.dumps({
255
+ "destination": destination, "checkin": checkin, "checkout": checkout,
256
+ "hotels": text,
257
+ "disclaimer": "⚠️ 酒店价格实时变动,请以预订页面为准",
258
+ "tips": _build_tips("search_hotel", destination)
259
+ }, ensure_ascii=False, indent=2)
260
+
261
+
262
+ @mcp.tool()
263
+ def search_poi(
264
+ keyword: Annotated[str, Field(description="景点关键词,如故宫、外滩")],
265
+ city: Annotated[Optional[str], Field(description="所在城市,如北京")] = None,
266
+ ) -> str:
267
+ """搜索景点门票和旅游信息。数据源:高德地图POI搜索API,返回景点详情+门票预订链接。"""
268
+ args = {}
269
+ if keyword:
270
+ args["keyword"] = keyword
271
+ if city:
272
+ args["cityName"] = city
273
+ result = _call_flyai("search_poi", args)
274
+ text = _parse_flyai_text(result)
275
+ return json.dumps({
276
+ "keyword": keyword, "city": city,
277
+ "attractions": text,
278
+ "disclaimer": "⚠️ 门票价格可能变动,请以景区官方为准",
279
+ "tips": _build_tips("search_poi", city or keyword)
280
+ }, ensure_ascii=False, indent=2)
281
+
282
+
283
+ @mcp.tool()
284
+ def plan_trip(
285
+ destination: Annotated[str, Field(description="目的地,如成都、厦门")],
286
+ days: Annotated[str, Field(description="旅行天数,如3、5")],
287
+ ) -> str:
288
+ """智能行程规划,根据目的地和天数生成完整旅行攻略。结合火车票+机票+酒店+景点+美食数据综合推荐。"""
289
+ result = _call_flyai("plan_trip", {"destination": destination, "days": str(days)})
290
+ text = _parse_flyai_text(result)
291
+ return json.dumps({
292
+ "destination": destination, "days": days,
293
+ "itinerary": text,
294
+ "disclaimer": "⚠️ 行程仅供参考,请根据实际情况调整",
295
+ "tips": _build_tips("plan_trip", destination)
296
+ }, ensure_ascii=False, indent=2)
297
+
298
+
299
+ @mcp.tool()
300
+ def search_fast(
301
+ keyword: Annotated[str, Field(description="搜索关键词,如成都旅游、上海亲子")],
302
+ ) -> str:
303
+ """极速搜索,一个关键词同时搜索酒店、景点、机票。数据源:飞猪+高德联合查询。"""
304
+ result = _call_flyai("fliggy_fast_search", {"query": keyword})
305
+ text = _parse_flyai_text(result)
306
+ return json.dumps({
307
+ "keyword": keyword,
308
+ "results": text,
309
+ "disclaimer": "⚠️ 搜索结果仅供参考",
310
+ "tips": _build_tips("search_fast", keyword)
311
+ }, ensure_ascii=False, indent=2)
312
+
313
+
314
+ @mcp.tool()
315
+ def search_marriott_hotel(
316
+ destination: Annotated[str, Field(description="目的地城市,如上海、北京")],
317
+ checkin: Annotated[Optional[str], Field(description="入住日期,如2025-06-20")] = None,
318
+ checkout: Annotated[Optional[str], Field(description="离店日期,如2025-06-22")] = None,
319
+ ) -> str:
320
+ """搜索万豪集团旗下酒店实时价格。数据源:RG万豪API,支持万豪/喜来登/JW/威斯汀/丽思卡尔顿/万怡等品牌。"""
321
+ args = {"destName": destination}
322
+ if checkin:
323
+ args["checkInDate"] = checkin
324
+ if checkout:
325
+ args["checkOutDate"] = checkout
326
+ result = _call_flyai("search_marriott_hotels", args)
327
+ text = _parse_flyai_text(result)
328
+ return json.dumps({
329
+ "destination": destination, "checkin": checkin or "未指定", "checkout": checkout or "未指定",
330
+ "hotels": text,
331
+ "disclaimer": "⚠️ 万豪酒店价格实时变动,请以万豪官网为准",
332
+ "tips": _build_tips("search_marriott_hotel", destination)
333
+ }, ensure_ascii=False, indent=2)
334
+
335
+
336
+ @mcp.tool()
337
+ def get_marriott_hotel_info(
338
+ hotel_name: Annotated[Optional[str], Field(description="酒店名称,如上海威斯汀大饭店")] = None,
339
+ hotel_id: Annotated[Optional[str], Field(description="酒店ID")] = None,
340
+ ) -> str:
341
+ """获取万豪酒店详情。数据源:RG万豪API,返回房型、价格、设施和预订链接。"""
342
+ if not hotel_name and not hotel_id:
343
+ return json.dumps({"error": "请提供hotel_name或hotel_id"}, ensure_ascii=False)
344
+ args = {}
345
+ if hotel_name:
346
+ args["hotelName"] = hotel_name
347
+ if hotel_id:
348
+ args["shid"] = hotel_id
349
+ result = _call_flyai("get_marriott_hotel_info", args)
350
+ text = _parse_flyai_text(result)
351
+ return json.dumps({
352
+ "hotel_name": hotel_name, "hotel_id": hotel_id,
353
+ "detail": text,
354
+ "disclaimer": "⚠️ 房型和价格实时变动,请以万豪官网为准",
355
+ "tips": _build_tips("get_marriott_hotel_info", hotel_name or "")
356
+ }, ensure_ascii=False, indent=2)
357
+
358
+
359
+ @mcp.tool()
360
+ def search_food(
361
+ location: Annotated[str, Field(description="位置,如外滩、三里屯")],
362
+ city: Annotated[str, Field(description="所在城市,如上海、北京")],
363
+ keyword: Annotated[Optional[str], Field(description="菜系筛选,如火锅、日料")] = None,
364
+ ) -> str:
365
+ """搜索附近美食餐厅。数据源:高德地图POI搜索API,返回评分、人均消费和推荐菜。"""
366
+ search_city = city or _extract_city(location)
367
+
368
+ # 1. 地理编码
369
+ coords = _geocode(location, search_city)
370
+ if not coords:
371
+ return json.dumps({"error": f"无法获取「{location}」的地理位置,请尝试更具体的地址"}, ensure_ascii=False)
372
+
373
+ lng, lat = coords
374
+
375
+ # 2. 周边搜索餐饮
376
+ around_params = {
377
+ "location": f"{lng},{lat}",
378
+ "types": "050000",
379
+ "offset": 15,
380
+ "sortrule": "weight",
381
+ }
382
+ if search_city:
383
+ around_params["city"] = search_city
384
+ if keyword:
385
+ around_params["keywords"] = keyword
386
+
387
+ result = _call_gaode("place/around", around_params)
388
+ if isinstance(result, dict) and "error" in result:
389
+ return json.dumps({"error": f"美食搜索失败: {result['error']}"}, ensure_ascii=False)
390
+
391
+ # 3. 解析
392
+ pois = []
393
+ if isinstance(result, dict):
394
+ pois = result.get("pois", [])
395
+
396
+ if not pois:
397
+ return json.dumps({"location": location, "message": f"在「{location}」附近未找到餐厅"}, ensure_ascii=False)
398
+
399
+ foods = []
400
+ for i, poi in enumerate(pois[:15], 1):
401
+ name = poi.get("name", "未知")
402
+ rating = poi.get("rating", "")
403
+ biz_ext = poi.get("biz_ext", {}) or {}
404
+ cost = poi.get("cost", "") or biz_ext.get("cost", "")
405
+ address = poi.get("address", "暂无")
406
+ poi_type = poi.get("type", "")
407
+ type_parts = poi_type.split(";") if poi_type else []
408
+ cuisine = type_parts[1] if len(type_parts) >= 2 else "美食"
409
+
410
+ foods.append({
411
+ "index": i,
412
+ "name": name,
413
+ "cuisine": cuisine,
414
+ "rating": rating if rating and rating not in ("0", "-1") else "暂无",
415
+ "avg_cost": f"¥{cost}" if cost and cost not in ("0", "-1") else "暂无",
416
+ "address": address,
417
+ })
418
+
419
+ return json.dumps({
420
+ "location": location,
421
+ "count": len(foods),
422
+ "restaurants": foods,
423
+ "disclaimer": "⚠️ 评分和人均消费仅供参考,请以实际为准",
424
+ "tips": _build_tips("search_food", location)
425
+ }, ensure_ascii=False, indent=2)
426
+
427
+
428
+ @mcp.tool()
429
+ def search_transport(
430
+ origin: Annotated[str, Field(description="出发地,如浦东机场")],
431
+ destination: Annotated[str, Field(description="目的地,如虹桥火车站")],
432
+ city: Annotated[str, Field(description="所在城市,如上海")],
433
+ ) -> str:
434
+ """查询市内交通方案。数据源:高德地图路线规划API,返回公交/地铁路线+高德打车唤端链接+打车费用预估。"""
435
+ transport_city = city or _extract_city(origin) or _extract_city(destination)
436
+
437
+ # 1. 地理编码
438
+ origin_coords = _geocode(origin, transport_city)
439
+ if not origin_coords:
440
+ return json.dumps({"error": f"无法获取「{origin}」的地理位置"}, ensure_ascii=False)
441
+
442
+ dest_coords = _geocode(destination, transport_city)
443
+ if not dest_coords:
444
+ return json.dumps({"error": f"无法获取「{destination}」的地理位置"}, ensure_ascii=False)
445
+
446
+ origin_loc = f"{origin_coords[0]},{origin_coords[1]}"
447
+ dest_loc = f"{dest_coords[0]},{dest_coords[1]}"
448
+
449
+ # 2. 驾车路线(打车预估)
450
+ driving_result = _call_gaode("direction/driving", {"origin": origin_loc, "destination": dest_loc, "strategy": 0})
451
+
452
+ taxi_info = {}
453
+ if isinstance(driving_result, dict) and "error" not in driving_result:
454
+ route = driving_result.get("route", {})
455
+ paths = route.get("paths", [])
456
+ if paths:
457
+ path = paths[0]
458
+ distance = int(path.get("distance", 0))
459
+ duration = int(path.get("duration", 0))
460
+ taxi_info = {
461
+ "distance_km": round(distance / 1000, 1),
462
+ "duration_min": round(duration / 60),
463
+ "taxi_fare_estimate": _estimate_taxi_fare(distance),
464
+ }
465
+
466
+ # 3. 高德打车唤端链接
467
+ dest_encoded = urllib.parse.quote(destination)
468
+ taxi_url = f"https://uri.amap.com/navigation?to={dest_coords[0]},{dest_coords[1]},{dest_encoded}&mode=car&callnative=1"
469
+ origin_encoded = urllib.parse.quote(origin)
470
+ taxi_url += f"&from={origin_coords[0]},{origin_coords[1]},{origin_encoded}"
471
+
472
+ # 4. 公交地铁路线
473
+ transit_params = {
474
+ "origin": origin_loc, "destination": dest_loc,
475
+ "city": transport_city, "strategy": 0, "nightflag": 0,
476
+ }
477
+ transit_result = _call_gaode("direction/transit/integrated", transit_params)
478
+
479
+ metro_routes = []
480
+ bus_routes = []
481
+
482
+ if isinstance(transit_result, dict) and "error" not in transit_result:
483
+ route = transit_result.get("route", {})
484
+ transits = route.get("transits", [])
485
+ for t in transits[:6]:
486
+ segments = t.get("segments", [])
487
+ duration = int(t.get("duration", 0))
488
+ walking_dist = int(t.get("walking_distance", "0") or 0)
489
+ cost = t.get("cost", "")
490
+
491
+ lines = []
492
+ is_metro = False
493
+
494
+ for seg in segments:
495
+ bus_seg = seg.get("bus", {})
496
+ if bus_seg:
497
+ bus_lines = bus_seg.get("buslines", [])
498
+ if bus_lines:
499
+ bl = bus_lines[0]
500
+ bl_name = bl.get("name", "")
501
+ dep_stop = bl.get("departure_stop", {}).get("name", "")
502
+ arr_stop = bl.get("arrival_stop", {}).get("name", "")
503
+ via_num = bl.get("via_num", 0)
504
+ if _is_metro_line(bl_name):
505
+ is_metro = True
506
+ lines.append({
507
+ "name": bl_name,
508
+ "departure": dep_stop,
509
+ "arrival": arr_stop,
510
+ "stops": via_num,
511
+ "is_metro": _is_metro_line(bl_name),
512
+ })
513
+
514
+ route_item = {
515
+ "lines": lines,
516
+ "duration_min": round(duration / 60),
517
+ "walking_distance_m": walking_dist,
518
+ "cost": f"¥{cost}" if cost and cost != "0" else "",
519
+ }
520
+
521
+ if is_metro:
522
+ metro_routes.append(route_item)
523
+ else:
524
+ bus_routes.append(route_item)
525
+
526
+ return json.dumps({
527
+ "origin": origin,
528
+ "destination": destination,
529
+ "city": transport_city,
530
+ "taxi": taxi_info,
531
+ "taxi_url": taxi_url,
532
+ "taxi_tip": "🚕 点击链接可唤起高德地图APP打车",
533
+ "metro_routes": metro_routes[:3],
534
+ "bus_routes": bus_routes[:3],
535
+ "disclaimer": "⚠️ 打车费用为估算,公交路线仅供参考,请以实际为准",
536
+ "tips": _build_tips("search_transport", destination)
537
+ }, ensure_ascii=False, indent=2)