bach-baidu-maps 1.0.0__py3-none-any.whl

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,137 @@
1
+ Metadata-Version: 2.4
2
+ Name: bach-baidu-maps
3
+ Version: 1.0.0
4
+ Summary: MCP Server Baidu Maps Enhanced - HTTP call Baidu Map API for MCP
5
+ Author-email: bachstudio <bachstudio@example.com>
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/bachstudio-org/baidu-maps-mcp
8
+ Project-URL: Repository, https://github.com/bachstudio-org/baidu-maps-mcp
9
+ Project-URL: Bug Reports, https://github.com/bachstudio-org/baidu-maps-mcp/issues
10
+ Keywords: mcp,map,baidu,baidu-maps,fastmcp,http
11
+ Requires-Python: >=3.11
12
+ Description-Content-Type: text/markdown
13
+ License-File: LICENSE
14
+ Requires-Dist: mcp[cli]>=1.9.0
15
+ Requires-Dist: httpx>=0.27.0
16
+ Dynamic: license-file
17
+
18
+ ## Baidu Map MCP Server (Python)
19
+
20
+ ### 🚀 快速启动(推荐)
21
+
22
+ #### 使用 UVX 一键启动
23
+
24
+ ```bash
25
+ uvx mcp-server-baidu-maps-enhanced
26
+ ```
27
+
28
+ #### 在 Cursor/Cherry Studio 中配置
29
+
30
+ ```json
31
+ {
32
+ "mcpServers": {
33
+ "baidu-maps-enhanced": {
34
+ "command": "uvx",
35
+ "args": ["mcp-server-baidu-maps-enhanced"],
36
+ "env": {
37
+ "BAIDU_MAPS_API_KEY": "<YOUR_API_KEY>"
38
+ }
39
+ }
40
+ }
41
+ }
42
+ ```
43
+
44
+ **包地址**: https://pypi.org/project/mcp-server-baidu-maps-enhanced/
45
+
46
+ ---
47
+
48
+ ### 传统方式:搭建 Python 虚拟环境
49
+
50
+ 我们推荐通过`uv`构建虚拟环境来运行 MCP server,关于`uv 你可以在[这里](https://docs.astral.sh/uv/getting-started/features/)找到一些说明。
51
+
52
+ 按照[官方流程](https://modelcontextprotocol.io/quickstart/server),你会安装`Python`包管理工具`uv`。除此之外,你也可以尝试其他方法(如`Anaconda`)来创建你的`Python`虚拟环境。
53
+
54
+ 通过`uv`添加`mcp`依赖
55
+
56
+ ```bash
57
+ uv add "mcp[cli]"
58
+ ```
59
+
60
+ 验证 mcp 依赖是否安装成功,执行如下命令
61
+
62
+ ```bash
63
+ uv run mcp
64
+ ```
65
+
66
+ 当出现下图时代表安装成功
67
+
68
+ ![](../../../img/uv_install_success.png)
69
+
70
+ 通过`uv`安装`python`,最低版本要求为 3.11
71
+
72
+ ```bash
73
+ uv python install 3.11
74
+ ```
75
+
76
+ ### 获取 MCP Server
77
+
78
+ 前往百度地图 Mcp Server 官方[开源仓库](https://github.com/baidu-maps/mcp/tree/main/src/baidu-map/python)下载
79
+
80
+ ### 配置本地项目
81
+
82
+ 通过`uv`创建一个项目
83
+
84
+ ```bash
85
+ uv init mcp_server_baidu_maps
86
+ ```
87
+
88
+ 将`map.py`拷贝到该目录下,通过如下命令测试 mcp server 是否正常运行
89
+
90
+ ```bash
91
+ uv run --with mcp[cli] mcp run {YOUR_PATH}/mcp_server_baidu_maps/map.py
92
+ # 如果是mac,需要加转义符
93
+ uv run --with mcp\[cli\] mcp run {YOUR_PATH}/mcp_server_baidu_maps/map.py
94
+ ```
95
+
96
+ 如果没有报错则 MCP Server 启动成功
97
+
98
+ ### 在 Cursor 中使用
99
+
100
+ 打开`Cursor`配置,在 MCP 中添加 MCP Server
101
+
102
+ ![](../../../img/cursor_setting.png)
103
+
104
+ 在文件中添加如下内容后保存
105
+
106
+ ```json
107
+ {
108
+ "mcpServers": {
109
+ "baidu-map": {
110
+ "command": "uv",
111
+ "args": [
112
+ "run",
113
+ "--with",
114
+ "mcp[cli]",
115
+ "mcp",
116
+ "run",
117
+ "{YOUR_PATH}/mcp_server_baidu_maps/map.py"
118
+ ],
119
+ "env": {
120
+ "BAIDU_MAPS_API_KEY": "<YOUR_API_KEY>"
121
+ }
122
+ }
123
+ }
124
+ }
125
+ ```
126
+
127
+ 回到配置,此时百度 MCP Server 已经启用
128
+
129
+ ![](../../../img/cursor_run_mcp_success.png)
130
+
131
+ ### 测试
132
+
133
+ 行程规划:
134
+
135
+ ![](../../../img/cursor_test_1.png)
136
+
137
+ ![](../../../img/cursor_test_2.png)
@@ -0,0 +1,9 @@
1
+ bach_baidu_maps-1.0.0.dist-info/licenses/LICENSE,sha256=ovC-MPhHY8OB6XEJmrqdSh-WXAcN0QmA_eA6pq1sop8,1067
2
+ mcp_server_baidu_maps/__init__.py,sha256=KUFvx8iIVipTDnVyqlscF6gs8du5vnYfnbI1jKljDEY,166
3
+ mcp_server_baidu_maps/__main__.py,sha256=GuHt2SXprTn-nh9RIC03dl5lkIag4Mnwerl2gWPwW6E,46
4
+ mcp_server_baidu_maps/map.py,sha256=gq9HU7-bktv_D9Zv5khLQ7LB4eJXkXIBRBJOOrTeyeA,37018
5
+ bach_baidu_maps-1.0.0.dist-info/METADATA,sha256=lTbp20J-HbyqdWw1yuE9WKzQjeWbQq9qPYsqYV9XKjI,3126
6
+ bach_baidu_maps-1.0.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
7
+ bach_baidu_maps-1.0.0.dist-info/entry_points.txt,sha256=08puocWzkdPpkij1z7uPsIFyHI8Q0x9b0Qs5wb12oes,63
8
+ bach_baidu_maps-1.0.0.dist-info/top_level.txt,sha256=oRr-AQZb52CJcCu35EScJVlBLbpCm8kahVqJpzG-aF8,22
9
+ bach_baidu_maps-1.0.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (80.9.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ bach-baidu-maps = mcp_server_baidu_maps:main
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 baidu-maps
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1 @@
1
+ mcp_server_baidu_maps
@@ -0,0 +1,9 @@
1
+ # __init__.py
2
+ from .map import mcp
3
+
4
+ def main():
5
+ """MCP Baidu Maps Server - HTTP call Baidu Map API for MCP"""
6
+ mcp.run()
7
+
8
+ if __name__ == "__main__":
9
+ main()
@@ -0,0 +1,3 @@
1
+ from mcp_server_baidu_maps import main
2
+
3
+ main()
@@ -0,0 +1,932 @@
1
+ # map.py
2
+ import os
3
+ import copy
4
+ import httpx
5
+ from asyncio import sleep
6
+
7
+ from mcp.server.fastmcp import FastMCP
8
+ import mcp.types as types
9
+ import re
10
+
11
+ # 创建MCP服务器实例
12
+ mcp = FastMCP(
13
+ name="mcp-server-baidu-maps",
14
+ instructions="This is a MCP server for Baidu Maps."
15
+ )
16
+
17
+ """
18
+
19
+ 获取环境变量中的API密钥, 用于调用百度地图API
20
+ 环境变量名为: BAIDU_MAPS_API_KEY, 在客户端侧通过配置文件进行设置传入
21
+ 获取方式请参考: https://lbsyun.baidu.com/apiconsole/key;
22
+
23
+ """
24
+
25
+ api_key = os.getenv('BAIDU_MAPS_API_KEY')
26
+ api_url = "https://api.map.baidu.com"
27
+
28
+
29
+ def filter_result(data) -> dict:
30
+ """
31
+ 过滤路径规划结果, 用于剔除冗余字段信息, 保证输出给模型的数据更简洁, 避免长距离路径规划场景下chat中断
32
+ """
33
+
34
+ # 创建输入数据的深拷贝以避免修改原始数据
35
+ processed_data = copy.deepcopy(data)
36
+
37
+ # 检查是否存在'result'键
38
+ if 'result' in processed_data:
39
+ result = processed_data['result']
40
+
41
+ # 检查'result'中是否存在'routes'键
42
+ if 'routes' in result:
43
+ for route in result['routes']:
44
+ # 检查每个'route'中是否存在'steps'键
45
+ if 'steps' in route:
46
+ new_steps = []
47
+ for step in route['steps']:
48
+ # 提取'instruction'字段, 若不存在则设为空字符串
49
+ new_step = {
50
+ 'distance': step.get('distance', ''),
51
+ 'duration': step.get('duration', ''),
52
+ 'instruction': step.get('instruction', '')
53
+ }
54
+ new_steps.append(new_step)
55
+ # 替换原steps为仅含instruction的新列表
56
+ route['steps'] = new_steps
57
+
58
+ return processed_data
59
+
60
+
61
+ def is_latlng(text):
62
+ """
63
+ 判断输入是否为经纬度坐标.
64
+ """
65
+
66
+ # 允许有空格,支持正负号和小数
67
+ pattern = r'^\s*([+-]?\d+(?:\.\d+)?)\s*,\s*([+-]?\d+(?:\.\d+)?)\s*$'
68
+ match = re.match(pattern, text)
69
+ if not match:
70
+ return False
71
+ lat, lng = float(match.group(1)), float(match.group(2))
72
+ # 简单经纬度范围校验
73
+ return -90 <= lat <= 90 and -180 <= lng <= 180
74
+
75
+ def mask_api_key(text: str) -> str:
76
+ """
77
+ 信息脱敏函数,主要用于给某个字符串脱敏用户的ak
78
+ 可以处理包含API key的复杂字符串,如URL、错误信息等
79
+ """
80
+ if not text or not api_key:
81
+ return text
82
+
83
+ # 如果字符串中不包含api_key,直接返回
84
+ if api_key not in text:
85
+ return text
86
+
87
+ # 计算脱敏后的API key
88
+ if len(api_key) <= 8:
89
+ # 如果api_key长度较短,只显示前2位和后2位
90
+ masked_key = api_key[:2] + '*' * (len(api_key) - 4) + api_key[-2:]
91
+ else:
92
+ # 如果api_key长度较长,显示前4位和后4位
93
+ masked_key = api_key[:4] + '*' * (len(api_key) - 8) + api_key[-4:]
94
+
95
+ # 替换文本中所有出现的api_key
96
+ return text.replace(api_key, masked_key)
97
+
98
+
99
+ async def map_geocode(
100
+ name: str, arguments: dict
101
+ ) -> list[types.TextContent | types.ImageContent | types.EmbeddedResource]:
102
+ """
103
+ 地理编码服务, 将地址解析为对应的位置坐标.
104
+ """
105
+ try:
106
+ address = arguments.get("address", "")
107
+ is_china = arguments.get("is_china", "true")
108
+ url = ""
109
+
110
+ # 调用百度API
111
+ if is_china == "true":
112
+ url = f"{api_url}/geocoding/v3/"
113
+ else:
114
+ url = f"{api_url}/api_geocoding_abroad/v1/"
115
+
116
+ # 设置请求参数
117
+ params = {
118
+ "ak": f"{api_key}",
119
+ "output": "json",
120
+ "address": f"{address}",
121
+ "from": "py_mcp"
122
+ }
123
+
124
+ async with httpx.AsyncClient() as client:
125
+ response = await client.get(url, params=params)
126
+ response.raise_for_status()
127
+ result = response.json()
128
+
129
+ if result.get("status") != 0:
130
+ error_msg = result.get("message", "unknown error")
131
+ raise Exception(f"API response error: {mask_api_key(error_msg)}")
132
+
133
+ return [types.TextContent(type="text", text=response.text)]
134
+
135
+ except httpx.HTTPError as e:
136
+ raise Exception(f"HTTP request failed: {str(e)}") from e
137
+ except KeyError as e:
138
+ raise Exception(f"Failed to parse reponse: {str(e)}") from e
139
+
140
+
141
+ async def map_reverse_geocode(
142
+ name: str, arguments: dict
143
+ ) -> list[types.TextContent | types.ImageContent | types.EmbeddedResource]:
144
+ """
145
+ 逆地理编码服务, 根据纬经度坐标获取对应位置的地址描述.
146
+ """
147
+ try:
148
+ latitude = arguments.get("latitude", "")
149
+ longitude = arguments.get("longitude", "")
150
+
151
+ # 调用百度API
152
+ url = f"{api_url}/reverse_geocoding/v3/"
153
+
154
+ params = {
155
+ "ak": f"{api_key}",
156
+ "output": "json",
157
+ "location": f"{latitude},{longitude}",
158
+ "extensions_road": "true",
159
+ "extensions_poi": "1",
160
+ "entire_poi": "1",
161
+ "from": "py_mcp"
162
+ }
163
+
164
+ async with httpx.AsyncClient() as client:
165
+ response = await client.get(url, params=params)
166
+ response.raise_for_status()
167
+ result = response.json()
168
+
169
+ if result.get("status") != 0:
170
+ error_msg = result.get("message", "unknown error")
171
+ raise Exception(f"API response error: {mask_api_key(error_msg)}")
172
+
173
+ return [types.TextContent(type="text", text=response.text)]
174
+
175
+ except httpx.HTTPError as e:
176
+ raise Exception(f"HTTP request failed: {str(e)}") from e
177
+ except KeyError as e:
178
+ raise Exception(f"Failed to parse reponse: {str(e)}") from e
179
+
180
+
181
+ async def map_search_places(
182
+ name: str, arguments: dict
183
+ ) -> list[types.TextContent | types.ImageContent | types.EmbeddedResource]:
184
+ """
185
+ 地点检索服务, 支持检索城市内的地点信息或圆形区域内的周边地点信息.
186
+ """
187
+ try:
188
+ query = arguments.get("query", "")
189
+ tag = arguments.get("tag", "")
190
+ region = arguments.get("region", "全国") # 默认检索全国,防止出错
191
+ location = arguments.get("location", "")
192
+ radius = arguments.get("radius", "")
193
+ language = arguments.get("language", "")
194
+ is_china = arguments.get("is_china", "true")
195
+
196
+ url = ""
197
+ params = {
198
+ "ak": f"{api_key}",
199
+ "output": "json",
200
+ "query": f"{query}",
201
+ "type": f"{tag}",
202
+ # "photo_show": "true",
203
+ "region_limit": "true",
204
+ "scope": 2,
205
+ "from": "py_mcp",
206
+ "language": f"{language}"
207
+ }
208
+
209
+ if is_china == "true":
210
+ if location:
211
+ url = f"{api_url}/place/v3/around"
212
+ params["location"] = f"{location}"
213
+ params["radius"] = f"{radius}"
214
+ else:
215
+ url = f"{api_url}/place/v3/region"
216
+ params["region"] = f"{region}"
217
+ elif is_china == "false":
218
+ url = f"{api_url}/place_abroad/v1/search"
219
+ if location:
220
+ params["location"] = f"{location}"
221
+ params["radius"] = f"{radius}"
222
+ else:
223
+ params["region"] = f"{region}"
224
+ else:
225
+ raise Exception("input `is_china` invaild, please reinput `is_china` with `true` or `false`")
226
+
227
+
228
+ async with httpx.AsyncClient() as client:
229
+ response = await client.get(url, params=params)
230
+ response.raise_for_status()
231
+ result = response.json()
232
+
233
+ if result.get("status") != 0:
234
+ error_msg = result.get("message", "unknown error")
235
+
236
+ raise Exception(f"API response error: {mask_api_key(error_msg)}")
237
+
238
+ return [types.TextContent(type="text", text=response.text)]
239
+
240
+ except httpx.HTTPError as e:
241
+ raise Exception(f"HTTP request failed: {str(e)}") from e
242
+ except KeyError as e:
243
+ raise Exception(f"Failed to parse reponse: {str(e)}") from e
244
+
245
+
246
+ async def map_place_details(
247
+ name: str, arguments: dict
248
+ ) -> list[types.TextContent | types.ImageContent | types.EmbeddedResource]:
249
+ """
250
+ 地点详情检索服务, 获取指定POI的详情信息.
251
+ """
252
+ try:
253
+ uid = arguments.get("uid", "")
254
+ is_china = arguments.get("is_china", "true")
255
+ url = ""
256
+
257
+ if is_china == "true":
258
+ url = f"{api_url}/place/v2/detail"
259
+ else:
260
+ url = f"{api_url}/place_abroad/v1/detail"
261
+
262
+ params = {
263
+ "ak": f"{api_key}",
264
+ "output": "json",
265
+ "uid": f"{uid}",
266
+ "scope": 2,
267
+ "from": "py_mcp"
268
+ }
269
+
270
+ async with httpx.AsyncClient() as client:
271
+ response = await client.get(url, params=params)
272
+ response.raise_for_status()
273
+ result = response.json()
274
+
275
+ if result.get("status") != 0:
276
+ error_msg = result.get("message", "unknown error")
277
+ raise Exception(f"API response error: {mask_api_key(error_msg)}")
278
+
279
+ return [types.TextContent(type="text", text=response.text)]
280
+
281
+ except httpx.HTTPError as e:
282
+ raise Exception(f"HTTP request failed: {str(e)}") from e
283
+ except KeyError as e:
284
+ raise Exception(f"Failed to parse reponse: {str(e)}") from e
285
+
286
+
287
+ async def map_directions_matrix(
288
+ name: str, arguments: dict
289
+ ) -> list[types.TextContent | types.ImageContent | types.EmbeddedResource]:
290
+ """
291
+ 批量算路服务, 根据起点和终点坐标计算路线规划距离和行驶时间.
292
+ """
293
+ try:
294
+ origins = arguments.get("origins", "")
295
+ destinations = arguments.get("destinations", "")
296
+ model = arguments.get("model", "driving")
297
+
298
+ url = f"{api_url}/routematrix/v2/{model}"
299
+
300
+ params = {
301
+ "ak": f"{api_key}",
302
+ "output": "json",
303
+ "origins": f"{origins}",
304
+ "destinations": f"{destinations}",
305
+ "from": "py_mcp"
306
+ }
307
+
308
+ async with httpx.AsyncClient() as client:
309
+ response = await client.get(url, params=params)
310
+ response.raise_for_status()
311
+ result = response.json()
312
+
313
+ if result.get("status") != 0:
314
+ error_msg = result.get("message", "unknown error")
315
+ raise Exception(f"API response error: {mask_api_key(error_msg)}")
316
+
317
+ return [types.TextContent(type="text", text=response.text)]
318
+
319
+ except httpx.HTTPError as e:
320
+ raise Exception(f"HTTP request failed: {str(e)}") from e
321
+ except KeyError as e:
322
+ raise Exception(f"Failed to parse reponse: {str(e)}") from e
323
+
324
+
325
+ async def map_directions(
326
+ name: str, arguments: dict
327
+ ) -> list[types.TextContent | types.ImageContent | types.EmbeddedResource]:
328
+ """
329
+ 路线规划服务, 支持驾车、骑行、步行和公交路线规划.
330
+ """
331
+ try:
332
+ model = arguments.get("model", "driving")
333
+ origin = arguments.get("origin", "")
334
+ destination = arguments.get("destination", "")
335
+ is_china = arguments.get("is_china", "true")
336
+ url = ""
337
+
338
+ # 检查输入是否为地址文本(不包含逗号)
339
+ if not is_latlng(origin):
340
+ # 调用地理编码服务获取起点经纬度
341
+ geocode_url = ""
342
+ if is_china == "true":
343
+ geocode_url = f"{api_url}/geocoding/v3/"
344
+ else:
345
+ geocode_url = f"{api_url}/api_geocoding_abroad/v1/"
346
+ geocode_params = {
347
+ "ak": f"{api_key}",
348
+ "output": "json",
349
+ "address": origin,
350
+ "from": "py_mcp"
351
+ }
352
+
353
+ async with httpx.AsyncClient() as client:
354
+ geocode_response = await client.get(geocode_url, params=geocode_params)
355
+ geocode_response.raise_for_status()
356
+ geocode_result = geocode_response.json()
357
+
358
+ if geocode_result.get("status") != 0:
359
+ error_msg = geocode_result.get("message", "input `origin` invaild, please reinput more detail address")
360
+ raise Exception(f"Geocoding API error: {mask_api_key(error_msg)}")
361
+
362
+ location = geocode_result.get("result", {}).get("location", {})
363
+ origin = f"{location.get('lat')},{location.get('lng')}"
364
+
365
+ if not is_latlng(destination):
366
+ # 调用地理编码服务获取终点经纬度
367
+ geocode_url = ""
368
+ if is_china == "true":
369
+ geocode_url = f"{api_url}/geocoding/v3/"
370
+ else:
371
+ geocode_url = f"{api_url}/api_geocoding_abroad/v1/"
372
+ geocode_params = {
373
+ "ak": f"{api_key}",
374
+ "output": "json",
375
+ "address": destination,
376
+ "from": "py_mcp"
377
+ }
378
+
379
+ async with httpx.AsyncClient() as client:
380
+ geocode_response = await client.get(geocode_url, params=geocode_params)
381
+ geocode_response.raise_for_status()
382
+ geocode_result = geocode_response.json()
383
+
384
+ if geocode_result.get("status") != 0:
385
+ error_msg = geocode_result.get("message", "input `destination` invaild, please reinput more detail address")
386
+ raise Exception(f"Geocoding API error: {mask_api_key(error_msg)}")
387
+
388
+ location = geocode_result.get("result", {}).get("location", {})
389
+ destination = f"{location.get('lat')},{location.get('lng')}"
390
+
391
+ # 调用路线规划服务
392
+ url = ""
393
+ if is_china == "true":
394
+ url = f"{api_url}/directionlite/v1/{model}"
395
+ else:
396
+ url = f"{api_url}/direction_abroad/v1/{model}"
397
+
398
+ params = {
399
+ "ak": f"{api_key}",
400
+ "output": "json",
401
+ "origin": origin,
402
+ "destination": destination,
403
+ "from": "py_mcp"
404
+ }
405
+
406
+ async with httpx.AsyncClient() as client:
407
+ response = await client.get(url, params=params)
408
+ response.raise_for_status()
409
+ result = response.json()
410
+
411
+ if result.get("status") != 0:
412
+ error_msg = result.get("message", "unknown error")
413
+ raise Exception(f"API response error: {mask_api_key(error_msg)}")
414
+
415
+ # if model == 'transit':
416
+ # return [types.TextContent(type="text", text=response.text)]
417
+ # else:
418
+ # return [types.TextContent(type="text", text=str(filter_result(result)))]
419
+ return [types.TextContent(type="text", text=response.text)]
420
+
421
+ except httpx.HTTPError as e:
422
+ raise Exception(f"HTTP request failed: {str(e)}") from e
423
+ except KeyError as e:
424
+ raise Exception(f"Failed to parse response: {str(e)}") from e
425
+
426
+
427
+ async def map_weather(
428
+ name: str, arguments: dict
429
+ ) -> list[types.TextContent | types.ImageContent | types.EmbeddedResource]:
430
+ """
431
+ 天气查询服务, 查询实时天气信息及未来5天天气预报.
432
+ """
433
+ try:
434
+ location = arguments.get("location", "")
435
+ district_id = arguments.get("district_id", "")
436
+ is_china = arguments.get("is_china", "true")
437
+ url = ""
438
+
439
+ if is_china == "true":
440
+ url = f"{api_url}/weather/v1/?"
441
+ else:
442
+ url = f"{api_url}/weather_abroad/v1/?"
443
+
444
+ params = {
445
+ "ak": f"{api_key}",
446
+ "data_type": "all",
447
+ "from": "py_mcp"
448
+ }
449
+
450
+ if not location:
451
+ params["district_id"] = f"{district_id}"
452
+ else:
453
+ params["location"] = f"{location}"
454
+
455
+ async with httpx.AsyncClient() as client:
456
+ response = await client.get(url, params=params)
457
+ response.raise_for_status()
458
+ result = response.json()
459
+
460
+ if result.get("status") != 0:
461
+ error_msg = result.get("message", "unknown error")
462
+ raise Exception(f"API response error: {mask_api_key(error_msg)}")
463
+
464
+ return [types.TextContent(type="text", text=response.text)]
465
+
466
+ except httpx.HTTPError as e:
467
+ raise Exception(f"HTTP request failed: {str(e)}") from e
468
+ except KeyError as e:
469
+ raise Exception(f"Failed to parse reponse: {str(e)}") from e
470
+
471
+
472
+ async def map_ip_location(
473
+ name: str, arguments: dict
474
+ ) -> list[types.TextContent | types.ImageContent | types.EmbeddedResource]:
475
+ """
476
+ IP定位服务, 通过所给IP获取具体位置信息和城市名称, 可用于定位IP或用户当前位置.
477
+ """
478
+ try:
479
+ ip = arguments.get("ip", "")
480
+
481
+ url = f"{api_url}/location/ip"
482
+
483
+ params = {
484
+ "ak": f"{api_key}",
485
+ "from": "py_mcp",
486
+ "ip": ip
487
+ }
488
+
489
+ async with httpx.AsyncClient() as client:
490
+ response = await client.get(url, params=params)
491
+ response.raise_for_status()
492
+ result = response.json()
493
+
494
+ if result.get("status") != 0:
495
+ error_msg = result.get("message", "unknown error")
496
+ raise Exception(f"API response error: {mask_api_key(error_msg)}")
497
+
498
+ return [types.TextContent(type="text", text=response.text)]
499
+
500
+ except httpx.HTTPError as e:
501
+ raise Exception(f"HTTP request failed: {str(e)}") from e
502
+ except KeyError as e:
503
+ raise Exception(f"Failed to parse reponse: {str(e)}") from e
504
+
505
+
506
+ async def map_road_traffic(
507
+ name: str, arguments: dict
508
+ ) -> list[types.TextContent | types.ImageContent | types.EmbeddedResource]:
509
+ """
510
+ 实时路况查询服务, 查询实时交通拥堵情况.
511
+ """
512
+ try:
513
+ model = arguments.get("model", "")
514
+ road_name = arguments.get("road_name", "")
515
+ city = arguments.get("city", "")
516
+ bounds = arguments.get("bounds", "")
517
+ vertexes = arguments.get("vertexes", "")
518
+ center = arguments.get("center", "")
519
+ radius = arguments.get("radius", "")
520
+
521
+ url = f"{api_url}/traffic/v1/{model}?"
522
+
523
+ params = {
524
+ "ak": f"{api_key}",
525
+ "output": "json",
526
+ "from": "py_mcp"
527
+ }
528
+
529
+ if model == 'bound':
530
+ params['bounds'] = f'{bounds}'
531
+ elif model == 'polygon':
532
+ params['vertexes'] = f'{vertexes}'
533
+ elif model == 'around':
534
+ params['center'] = f'{center}'
535
+ params['radius'] = f'{radius}'
536
+ elif model == 'road':
537
+ params['road_name'] = f'{road_name}'
538
+ params['city'] = f'{city}'
539
+
540
+ async with httpx.AsyncClient() as client:
541
+ response = await client.get(url, params=params)
542
+ response.raise_for_status()
543
+ result = response.json()
544
+
545
+ if result.get("status") != 0:
546
+ error_msg = result.get("message", "unknown error")
547
+ raise Exception(f"API response error: {mask_api_key(error_msg)}")
548
+
549
+ return [types.TextContent(type="text", text=response.text)]
550
+
551
+ except httpx.HTTPError as e:
552
+ raise Exception(f"HTTP request failed: {str(e)}") from e
553
+ except KeyError as e:
554
+ raise Exception(f"Failed to parse reponse: {str(e)}") from e
555
+
556
+
557
+ async def map_poi_extract(
558
+ name: str,
559
+ arguments: dict
560
+ ) -> list[types.TextContent | types.ImageContent | types.EmbeddedResource]:
561
+ """
562
+ POI智能提取
563
+ """
564
+ # 关于高级权限使用的相关问题,请联系我们: https://lbsyun.baidu.com/apiconsole/fankui?typeOne=%E4%BA%A7%E5%93%81%E9%9C%80%E6%B1%82&typeTwo=%E9%AB%98%E7%BA%A7%E6%9C%8D%E5%8A%A1
565
+
566
+ try:
567
+ text_content = arguments.get("text_content", "")
568
+
569
+ # 调用POI智能提取的提交接口
570
+ headers = {"Content-Type": "application/x-www-form-urlencoded"}
571
+ submit_url = f"{api_url}/api_mark/v1/submit"
572
+ result_url = f"{api_url}/api_mark/v1/result"
573
+
574
+ # 设置上传用户描述的请求体
575
+ submit_body = {
576
+ "ak": f"{api_key}",
577
+ "id": 0,
578
+ "msg_type": "text",
579
+ "text_content": f"{text_content}",
580
+ "from": "py_mcp"
581
+ }
582
+
583
+ # 异步请求
584
+ async with httpx.AsyncClient() as client:
585
+ # 提交任务
586
+ submit_resp = await client.post(
587
+ submit_url, data=submit_body, headers=headers, timeout=10.0
588
+ )
589
+ submit_resp.raise_for_status()
590
+ submit_result = submit_resp.json()
591
+
592
+ if submit_result.get("status") != 0:
593
+ error_msg = submit_result.get("message", "unknown error")
594
+ raise Exception(f"API response error: {mask_api_key(error_msg)}")
595
+
596
+
597
+ map_id = submit_result.get("result", {}).get("map_id")
598
+ if not map_id:
599
+ raise Exception("Can not found map_id")
600
+
601
+ # 轮询获取结果(最多5次,间隔2秒)
602
+ result_body = {"ak": api_key, "id": 0, "map_id": map_id, "from": "py_mcp"}
603
+ max_retries = 5
604
+ for attempt in range(max_retries):
605
+ result_resp = await client.post(
606
+ result_url, data=result_body, headers=headers, timeout=10.0
607
+ )
608
+ result_resp.raise_for_status()
609
+ result = result_resp.json()
610
+
611
+ if result.get("status") == 0 and result.get("result"):
612
+ return result
613
+ elif attempt < max_retries - 1:
614
+ await sleep(2)
615
+
616
+ else:
617
+ raise Exception("Timeout to get the result")
618
+
619
+ if result.get("status") != 0:
620
+ error_msg = result.get("message", "unknown error")
621
+ raise Exception(f"API response error: {error_msg}")
622
+
623
+ except httpx.HTTPError as e:
624
+ raise Exception(f"HTTP request failed: {str(e)}") from e
625
+ except KeyError as e:
626
+ raise Exception(f"Failed to parse reponse: {str(e)}") from e
627
+
628
+
629
+ async def list_tools() -> list[types.Tool]:
630
+ """
631
+ 列出所有可用的工具。
632
+
633
+ Args:
634
+ None.
635
+
636
+ Returns:
637
+ list (types.Tool): 包含了所有可用的工具, 每个工具都包含了名称、描述、输入schema三个属性.
638
+ """
639
+ return [
640
+ types.Tool(
641
+ name="map_geocode",
642
+ description="地理编码服务: 将地址解析为对应的位置坐标.地址结构越完整, 地址内容越准确, 解析的坐标精度越高.",
643
+ inputSchema={
644
+ "type": "object",
645
+ "required": ["address"],
646
+ "properties": {
647
+ "address": {
648
+ "type": "string",
649
+ "description": "待解析的地址.最多支持84个字节.可以输入两种样式的值, 分别是:\n1、标准的结构化地址信息, 如北京市海淀区上地十街十号\n2、支持*路与*路交叉口描述方式, 如北一环路和阜阳路的交叉路口\n第二种方式并不总是有返回结果, 只有当地址库中存在该地址描述时才有返回",
650
+ },
651
+ "is_china": {
652
+ "type": "string",
653
+ "description": "查询地是否在中国大陆以外地区, 可选值为`true`或`false`, 默认为`true`",
654
+ },
655
+ },
656
+ }
657
+ ),
658
+ types.Tool(
659
+ name="map_reverse_geocode",
660
+ description="逆地理编码服务: 根据纬经度坐标, 获取对应位置的地址描述, 所在行政区划, 道路以及相关POI等信息",
661
+ inputSchema={
662
+ "type": "object",
663
+ "required": ["latitude", "longitude"],
664
+ "properties": {
665
+ "latitude": {
666
+ "type": "number",
667
+ "description": "纬度 (bd09ll)",
668
+ },
669
+ "longitude": {
670
+ "type": "number",
671
+ "description": "经度 (bd09ll)",
672
+ },
673
+ },
674
+ }
675
+ ),
676
+ types.Tool(
677
+ name="map_search_places",
678
+ description="地点检索服务: 支持检索全球各城市内的地点信息(最小到city级别), 也可支持圆形区域内的周边地点信息检索."
679
+ "\n城市内检索: 检索某一城市内(目前最细到城市级别)的地点信息."
680
+ "\n周边检索: 设置圆心和半径, 检索圆形区域内的地点信息(常用于周边检索场景).",
681
+ inputSchema={
682
+ "type": "object",
683
+ "required": ["query"],
684
+ "properties": {
685
+ "query": {
686
+ "type": "string",
687
+ "description": "检索关键字, 可直接使用名称或类型, 如'天安门', 且可以至多10个关键字, 用英文逗号隔开",
688
+ },
689
+ "tag": {
690
+ "type": "string",
691
+ "description": "检索分类, 以中文字符输入, 如'美食', 多个分类用英文逗号隔开, 如'美食,购物'",
692
+ },
693
+ "region": {
694
+ "type": "string",
695
+ "description": "检索的城市名称, 可为行政区划名或citycode, 格式如'北京市'或'131', 不传默认为'全国', 当is_china为false时, 该参数必传且只能传文本, 如'东京'",
696
+ },
697
+ "location": {
698
+ "type": "string",
699
+ "description": "圆形区域检索的中心点纬经度坐标, 格式为lat,lng",
700
+ },
701
+ "radius": {
702
+ "type": "integer",
703
+ "description": "圆形区域检索半径, 单位:米",
704
+ },
705
+ "is_china": {
706
+ "type": "string",
707
+ "description": "检索地是否在中国大陆以外地区, 可选值为`true`或`false`, 默认为`true`",
708
+ },
709
+ },
710
+ }
711
+ ),
712
+ types.Tool(
713
+ name="map_place_details",
714
+ description="地点详情检索服务: 地点详情检索针对指定POI, 检索其相关的详情信息."
715
+ "\n通过地点检索服务获取POI uid.使用地点详情检索功能, 传入uid, 即可检索POI详情信息, 如评分、营业时间等(不同类型POI对应不同类别详情数据).",
716
+ inputSchema={
717
+ "type": "object",
718
+ "required": ["uid"],
719
+ "properties": {
720
+ "uid": {
721
+ "type": "string",
722
+ "description": "POI的唯一标识",
723
+ },
724
+ "is_china": {
725
+ "type": "string",
726
+ "description": "查询地是否在中国大陆以外地区, 可选值为`true`或`false`, 默认为`true`",
727
+ },
728
+ },
729
+ }
730
+ ),
731
+ types.Tool(
732
+ name="map_directions_matrix",
733
+ description="批量算路服务: 根据起点和终点坐标计算路线规划距离和行驶时间."
734
+ "\n批量算路目前支持驾车、骑行、步行."
735
+ "\n步行时任意起终点之间的距离不得超过200KM, 超过此限制会返回参数错误."
736
+ "\n驾车批量算路一次最多计算100条路线, 起终点个数之积不能超过100.",
737
+ inputSchema={
738
+ "type": "object",
739
+ "required": ["origins", "destinations"],
740
+ "properties": {
741
+ "origins": {
742
+ "type": "string",
743
+ "description": "多个起点纬经度坐标, 纬度在前, 经度在后, 多个起点用|分隔",
744
+ },
745
+ "destinations": {
746
+ "type": "string",
747
+ "description": "多个终点纬经度坐标, 纬度在前, 经度在后, 多个终点用|分隔",
748
+ },
749
+ "model": {
750
+ "type": "string",
751
+ "description": "批量算路类型(driving, riding, walking)",
752
+ },
753
+ },
754
+ }
755
+ ),
756
+ types.Tool(
757
+ name="map_directions",
758
+ description="路线规划服务: 根据起终点`位置名称`或`纬经度坐标`规划出行路线."
759
+ "\n驾车路线规划: 根据起终点`位置名称`或`纬经度坐标`规划驾车出行路线."
760
+ "\n骑行路线规划: 根据起终点`位置名称`或`纬经度坐标`规划骑行出行路线."
761
+ "\n步行路线规划: 根据起终点`位置名称`或`纬经度坐标`规划步行出行路线."
762
+ "\n公交路线规划: 根据起终点`位置名称`或`纬经度坐标`规划公共交通出行路线.",
763
+ inputSchema={
764
+ "type": "object",
765
+ "required": ["origin", "destination"],
766
+ "properties": {
767
+ "model": {
768
+ "type": "string",
769
+ "description": "路线规划类型(driving, riding, walking, transit)",
770
+ },
771
+ "origin": {
772
+ "type": "string",
773
+ "description": "起点位置名称或纬经度坐标, 纬度在前, 经度在后",
774
+ },
775
+ "destination": {
776
+ "type": "string",
777
+ "description": "终点位置名称或纬经度坐标, 纬度在前, 经度在后",
778
+ },
779
+ "is_china": {
780
+ "type": "string",
781
+ "description": "查询地是否在中国大陆以外地区, 可选值为`true`或`false`, 默认为`true`",
782
+ },
783
+ },
784
+ }
785
+ ),
786
+ types.Tool(
787
+ name="map_weather",
788
+ description="天气查询服务: 通过行政区划或是经纬度坐标查询实时天气信息及未来5天天气预报.",
789
+ inputSchema={
790
+ "type": "object",
791
+ "required": [],
792
+ "properties": {
793
+ "location": {
794
+ "type": "string",
795
+ "description": "经纬度坐标, 经度在前纬度在后, 逗号分隔",
796
+ },
797
+ "district_id": {
798
+ "type": "string",
799
+ "description": "行政区划代码, 需保证为6位无符号整数",
800
+ },
801
+ "is_china": {
802
+ "type": "string",
803
+ "description": "查询地是否在中国大陆以外地区, 可选值为`true`或`false`, 默认为`true`",
804
+ },
805
+ },
806
+ }
807
+ ),
808
+ types.Tool(
809
+ name="map_ip_location",
810
+ description="IP定位服务: 通过所给IP获取具体位置信息和城市名称, 可用于定位IP或用户当前位置.",
811
+ inputSchema={
812
+ "type": "object",
813
+ "required": [],
814
+ "properties": {
815
+ "ip": {
816
+ "type": "string",
817
+ "description": "需要定位的IP地址, 如果为空则获取用户当前IP地址(支持IPv4和IPv6)",
818
+ },
819
+ },
820
+ }
821
+ ),
822
+ types.Tool(
823
+ name="map_road_traffic",
824
+ description="实时路况查询服务: 查询实时交通拥堵情况, 可通过指定道路名和区域形状(矩形, 多边形, 圆形)进行实时路况查询."
825
+ "\n道路实时路况查询: 查询具体道路的实时拥堵评价和拥堵路段、拥堵距离、拥堵趋势等信息."
826
+ "\n矩形区域实时路况查询: 查询指定矩形地理范围的实时拥堵情况和各拥堵路段信息."
827
+ "\n多边形区域实时路况查询: 查询指定多边形地理范围的实时拥堵情况和各拥堵路段信息."
828
+ "\n圆形区域(周边)实时路况查询: 查询某中心点周边半径范围内的实时拥堵情况和各拥堵路段信息.",
829
+ inputSchema={
830
+ "type": "object",
831
+ "required": ["model"],
832
+ "properties": {
833
+ "model": {
834
+ "type": "string",
835
+ "description": "路况查询类型(road, bound, polygon, around)",
836
+ },
837
+ "road_name": {
838
+ "type": "string",
839
+ "description": "道路名称和道路方向, model=road时必传 (如:朝阳路南向北)",
840
+ },
841
+ "city": {
842
+ "type": "string",
843
+ "description": "城市名称或城市adcode, model=road时必传 (如:北京市)",
844
+ },
845
+ "bounds": {
846
+ "type": "string",
847
+ "description": "区域左下角和右上角的纬经度坐标, 纬度在前, 经度在后, model=bound时必传",
848
+ },
849
+ "vertexes": {
850
+ "type": "string",
851
+ "description": "多边形区域的顶点纬经度坐标, 纬度在前, 经度在后, model=polygon时必传",
852
+ },
853
+ "center": {
854
+ "type": "string",
855
+ "description": "圆形区域的中心点纬经度坐标, 纬度在前, 经度在后, model=around时必传",
856
+ },
857
+ "radius": {
858
+ "type": "integer",
859
+ "description": "圆形区域的半径(米), 取值[1,1000], model=around时必传",
860
+ },
861
+ },
862
+ }
863
+ ),
864
+ types.Tool(
865
+ name="map_poi_extract",
866
+ description="POI智能提取",
867
+ inputSchema={
868
+ "type": "object",
869
+ "required": ["text_content"],
870
+ "properties": {
871
+ "text_content": {
872
+ "type": "string",
873
+ "description": "根据用户提供的文本描述信息, 智能提取出文本中所提及的POI相关信息. (注意: 使用该服务, api_key需要拥有对应的高级权限, 否则会报错)",
874
+ },
875
+ },
876
+ }
877
+ )
878
+ ]
879
+
880
+
881
+ async def dispatch(
882
+ name: str, arguments: dict
883
+ ) -> list[types.TextContent | types.ImageContent | types.EmbeddedResource]:
884
+ """
885
+ 根据名称调度对应的工具函数, 并返回处理结果.
886
+
887
+ Args:
888
+ name (str): 工具函数的名称, 可选值为: "map_geocode", "map_reverse_geocode",
889
+ "map_search_places", "map_place_details", "map_distance_matrix",
890
+ "map_directions", "map_weather", "map_ip_location", "map_road_traffic",
891
+ "map_mark".
892
+ arguments (dict): 传递给工具函数的参数字典, 包括必要和可选参数.
893
+
894
+ Returns:
895
+ list[types.TextContent | types.ImageContent | types.EmbeddedResource]: 返回一个列表, 包含文本内容、图片内容或嵌入资源类型的元素.
896
+
897
+ Raises:
898
+ ValueError: 如果提供了未知的工具名称.
899
+ """
900
+
901
+ match name:
902
+ case "map_geocode":
903
+ return await map_geocode(name, arguments)
904
+ case "map_reverse_geocode":
905
+ return await map_reverse_geocode(name, arguments)
906
+ case "map_search_places":
907
+ return await map_search_places(name, arguments)
908
+ case "map_place_details":
909
+ return await map_place_details(name, arguments)
910
+ case "map_directions_matrix":
911
+ return await map_directions_matrix(name, arguments)
912
+ case "map_directions":
913
+ return await map_directions(name, arguments)
914
+ case "map_weather":
915
+ return await map_weather(name, arguments)
916
+ case "map_ip_location":
917
+ return await map_ip_location(name, arguments)
918
+ case "map_road_traffic":
919
+ return await map_road_traffic(name, arguments)
920
+ case "map_poi_extract":
921
+ return await map_poi_extract(name, arguments)
922
+ case _:
923
+ raise ValueError(f"Unknown tool: {name}")
924
+
925
+
926
+ # 注册list_tools方法
927
+ mcp._mcp_server.list_tools()(list_tools)
928
+ # 注册dispatch方法
929
+ mcp._mcp_server.call_tool()(dispatch)
930
+
931
+ if __name__ == "__main__":
932
+ mcp.run()