mcp-server-maptec 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.
- maptec_mcp_python/__init__.py +3 -0
- maptec_mcp_python/__main__.py +4 -0
- maptec_mcp_python/client/__init__.py +0 -0
- maptec_mcp_python/client/maptec_client.py +157 -0
- maptec_mcp_python/config.py +30 -0
- maptec_mcp_python/schemas/tools.json +237 -0
- maptec_mcp_python/server.py +54 -0
- maptec_mcp_python/utils/__init__.py +0 -0
- maptec_mcp_python/utils/filter.py +28 -0
- maptec_mcp_python/utils/mask.py +13 -0
- mcp_server_maptec-1.0.0.dist-info/METADATA +225 -0
- mcp_server_maptec-1.0.0.dist-info/RECORD +15 -0
- mcp_server_maptec-1.0.0.dist-info/WHEEL +4 -0
- mcp_server_maptec-1.0.0.dist-info/entry_points.txt +2 -0
- mcp_server_maptec-1.0.0.dist-info/licenses/LICENSE +21 -0
|
File without changes
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from importlib.resources import files
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any
|
|
7
|
+
from urllib.parse import quote, urlencode
|
|
8
|
+
|
|
9
|
+
import httpx
|
|
10
|
+
|
|
11
|
+
from maptec_mcp_python.config import MaptecConfig, format_authorization
|
|
12
|
+
from maptec_mcp_python.utils.filter import filter_large_fields
|
|
13
|
+
from maptec_mcp_python.utils.mask import mask_api_key
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class ToolDefinition:
|
|
17
|
+
def __init__(self, name: str, description: str, input_schema: dict[str, Any]) -> None:
|
|
18
|
+
self.name = name
|
|
19
|
+
self.description = description
|
|
20
|
+
self.input_schema = input_schema
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _read_tools_json() -> dict[str, Any]:
|
|
24
|
+
try:
|
|
25
|
+
content = files("maptec_mcp_python").joinpath("schemas/tools.json").read_text(encoding="utf-8")
|
|
26
|
+
return json.loads(content)
|
|
27
|
+
except (FileNotFoundError, TypeError, OSError):
|
|
28
|
+
pass
|
|
29
|
+
|
|
30
|
+
candidates = [
|
|
31
|
+
Path(__file__).resolve().parent.parent / "schemas" / "tools.json",
|
|
32
|
+
Path(__file__).resolve().parents[3] / "schemas" / "tools.json",
|
|
33
|
+
]
|
|
34
|
+
for path in candidates:
|
|
35
|
+
if path.is_file():
|
|
36
|
+
return json.loads(path.read_text(encoding="utf-8"))
|
|
37
|
+
raise FileNotFoundError("schemas/tools.json not found")
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def load_tools() -> list[ToolDefinition]:
|
|
41
|
+
raw = _read_tools_json()
|
|
42
|
+
return [
|
|
43
|
+
ToolDefinition(
|
|
44
|
+
name=item["name"],
|
|
45
|
+
description=item["description"],
|
|
46
|
+
input_schema=item["inputSchema"],
|
|
47
|
+
)
|
|
48
|
+
for item in raw["tools"]
|
|
49
|
+
]
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class MaptecClient:
|
|
53
|
+
def __init__(self, config: MaptecConfig) -> None:
|
|
54
|
+
self._config = config
|
|
55
|
+
self._http: httpx.AsyncClient | None = None
|
|
56
|
+
|
|
57
|
+
def _get_http(self) -> httpx.AsyncClient:
|
|
58
|
+
if self._http is None or self._http.is_closed:
|
|
59
|
+
timeout = httpx.Timeout(self._config.timeout_ms / 1000)
|
|
60
|
+
self._http = httpx.AsyncClient(timeout=timeout)
|
|
61
|
+
return self._http
|
|
62
|
+
|
|
63
|
+
async def aclose(self) -> None:
|
|
64
|
+
if self._http is not None and not self._http.is_closed:
|
|
65
|
+
await self._http.aclose()
|
|
66
|
+
self._http = None
|
|
67
|
+
|
|
68
|
+
async def call_tool(self, name: str, args: dict[str, Any]) -> str:
|
|
69
|
+
match name:
|
|
70
|
+
case "map_search_text":
|
|
71
|
+
return await self._post_json("/search/v1/text", args)
|
|
72
|
+
case "map_search_nearby":
|
|
73
|
+
return await self._post_json("/search/v1/nearby", args)
|
|
74
|
+
case "map_search_suggest":
|
|
75
|
+
return await self._post_json("/search/v1/suggest", args)
|
|
76
|
+
case "map_search_places":
|
|
77
|
+
return await self._get_place_details(args)
|
|
78
|
+
case "map_search_geocode":
|
|
79
|
+
return await self._get_json("/search/v1/geocode", args, skip_keys=["placeId"])
|
|
80
|
+
case "map_search_reverse":
|
|
81
|
+
return await self._get_json("/search/v1/reverse", args)
|
|
82
|
+
case "map_routes_directions":
|
|
83
|
+
return await self._post_json(
|
|
84
|
+
"/routes/v1/directions", self._ensure_driving_mode(dict(args))
|
|
85
|
+
)
|
|
86
|
+
case "map_matrix_distance":
|
|
87
|
+
return await self._post_json(
|
|
88
|
+
"/matrix/v1/distance", self._ensure_driving_mode(dict(args))
|
|
89
|
+
)
|
|
90
|
+
case "map_track_snap_to_roads":
|
|
91
|
+
return await self._post_json("/track/v1/snapToRoads", args)
|
|
92
|
+
case "map_location_ip":
|
|
93
|
+
return await self._post_json("/location/v1/ip", args)
|
|
94
|
+
case _:
|
|
95
|
+
raise ValueError(f"Unknown tool: {name}")
|
|
96
|
+
|
|
97
|
+
@staticmethod
|
|
98
|
+
def _ensure_driving_mode(body: dict[str, Any]) -> dict[str, Any]:
|
|
99
|
+
mode = body.get("mode")
|
|
100
|
+
if mode is None or str(mode).strip() == "":
|
|
101
|
+
body["mode"] = "driving"
|
|
102
|
+
return body
|
|
103
|
+
|
|
104
|
+
async def _get_place_details(self, args: dict[str, Any]) -> str:
|
|
105
|
+
place_id = str(args.get("placeId") or "").strip()
|
|
106
|
+
if not place_id:
|
|
107
|
+
raise ValueError("placeId is required")
|
|
108
|
+
query = {key: value for key, value in args.items() if key != "placeId"}
|
|
109
|
+
path = f"/search/v1/places/{quote(place_id, safe='')}"
|
|
110
|
+
return await self._get_json(path, query)
|
|
111
|
+
|
|
112
|
+
async def _post_json(self, path: str, body: dict[str, Any]) -> str:
|
|
113
|
+
url = f"{self._config.base_url}{path}"
|
|
114
|
+
response = await self._get_http().post(
|
|
115
|
+
url,
|
|
116
|
+
headers={
|
|
117
|
+
"Content-Type": "application/json",
|
|
118
|
+
"Authorization": format_authorization(self._config.api_key),
|
|
119
|
+
},
|
|
120
|
+
json=body or {},
|
|
121
|
+
)
|
|
122
|
+
return self._parse_response(response)
|
|
123
|
+
|
|
124
|
+
async def _get_json(
|
|
125
|
+
self,
|
|
126
|
+
path: str,
|
|
127
|
+
query_params: dict[str, Any],
|
|
128
|
+
skip_keys: list[str] | None = None,
|
|
129
|
+
) -> str:
|
|
130
|
+
skip = set(skip_keys or ["placeId"])
|
|
131
|
+
params = {
|
|
132
|
+
key: str(value)
|
|
133
|
+
for key, value in (query_params or {}).items()
|
|
134
|
+
if value is not None and key not in skip
|
|
135
|
+
}
|
|
136
|
+
query = f"?{urlencode(params)}" if params else ""
|
|
137
|
+
url = f"{self._config.base_url}{path}{query}"
|
|
138
|
+
response = await self._get_http().get(
|
|
139
|
+
url,
|
|
140
|
+
headers={"Authorization": format_authorization(self._config.api_key)},
|
|
141
|
+
)
|
|
142
|
+
return self._parse_response(response)
|
|
143
|
+
|
|
144
|
+
def _parse_response(self, response: httpx.Response) -> str:
|
|
145
|
+
text = response.text
|
|
146
|
+
bare_key = self._config.api_key.removeprefix("Bearer ").removeprefix("bearer ")
|
|
147
|
+
if not response.is_success:
|
|
148
|
+
try:
|
|
149
|
+
payload = filter_large_fields(response.json())
|
|
150
|
+
return mask_api_key(json.dumps(payload, ensure_ascii=False), bare_key)
|
|
151
|
+
except ValueError:
|
|
152
|
+
return mask_api_key(text, bare_key)
|
|
153
|
+
try:
|
|
154
|
+
payload = filter_large_fields(response.json())
|
|
155
|
+
return json.dumps(payload, ensure_ascii=False)
|
|
156
|
+
except ValueError:
|
|
157
|
+
return text
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@dataclass(frozen=True)
|
|
8
|
+
class MaptecConfig:
|
|
9
|
+
api_key: str
|
|
10
|
+
base_url: str
|
|
11
|
+
timeout_ms: int
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def load_config() -> MaptecConfig:
|
|
15
|
+
api_key = (os.getenv("MAPTEC_API_KEY") or "").strip()
|
|
16
|
+
base_url = (os.getenv("MAPTEC_BASE_URL") or "http://localhost:8090").strip().rstrip("/")
|
|
17
|
+
timeout_ms = int(os.getenv("MAPTEC_TIMEOUT_MS") or "30000")
|
|
18
|
+
|
|
19
|
+
if not api_key:
|
|
20
|
+
raise ValueError("MAPTEC_API_KEY is required")
|
|
21
|
+
|
|
22
|
+
return MaptecConfig(api_key=api_key, base_url=base_url, timeout_ms=timeout_ms)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def format_authorization(api_key: str) -> str:
|
|
26
|
+
if api_key.lower().startswith("bearer "):
|
|
27
|
+
return api_key
|
|
28
|
+
if api_key.startswith("mp_"):
|
|
29
|
+
return api_key
|
|
30
|
+
return f"Bearer {api_key}"
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
{
|
|
2
|
+
"tools": [
|
|
3
|
+
{
|
|
4
|
+
"name": "map_search_text",
|
|
5
|
+
"description": "文本检索。入参与 POST /search/v1/text 请求体字段一致(query、pageSize、region、language、locationBias 等)。",
|
|
6
|
+
"inputSchema": {
|
|
7
|
+
"type": "object",
|
|
8
|
+
"properties": {
|
|
9
|
+
"query": { "type": "string", "description": "检索关键词" },
|
|
10
|
+
"pageSize": { "type": "integer", "minimum": 1, "maximum": 20, "description": "每页结果数" },
|
|
11
|
+
"region": { "type": "string", "description": "区域代码,如 SG" },
|
|
12
|
+
"language": { "type": "string", "description": "语言代码" },
|
|
13
|
+
"locationBias": {
|
|
14
|
+
"type": "object",
|
|
15
|
+
"properties": {
|
|
16
|
+
"center": {
|
|
17
|
+
"type": "object",
|
|
18
|
+
"properties": {
|
|
19
|
+
"latitude": { "type": "number" },
|
|
20
|
+
"longitude": { "type": "number" }
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
"radius": { "type": "number" }
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
"rank": { "type": "string", "description": "排序方式,如 RELEVANCE" }
|
|
27
|
+
},
|
|
28
|
+
"required": ["query"]
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
"name": "map_search_nearby",
|
|
33
|
+
"description": "附近搜索。入参与 POST /search/v1/nearby 请求体字段一致(locationLimit、resultLimit 等)。",
|
|
34
|
+
"inputSchema": {
|
|
35
|
+
"type": "object",
|
|
36
|
+
"properties": {
|
|
37
|
+
"query": { "type": "string", "description": "检索关键词" },
|
|
38
|
+
"locationLimit": {
|
|
39
|
+
"type": "object",
|
|
40
|
+
"properties": {
|
|
41
|
+
"center": {
|
|
42
|
+
"type": "object",
|
|
43
|
+
"properties": {
|
|
44
|
+
"latitude": { "type": "number" },
|
|
45
|
+
"longitude": { "type": "number" }
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
"radius": { "type": "number" }
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
"resultLimit": { "type": "integer", "description": "结果数量上限" },
|
|
52
|
+
"language": { "type": "string" },
|
|
53
|
+
"region": { "type": "string" }
|
|
54
|
+
},
|
|
55
|
+
"required": ["query"]
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
"name": "map_search_suggest",
|
|
60
|
+
"description": "输入提示。入参与 POST /search/v1/suggest 请求体字段一致(query、resultLimit 等)。",
|
|
61
|
+
"inputSchema": {
|
|
62
|
+
"type": "object",
|
|
63
|
+
"properties": {
|
|
64
|
+
"query": { "type": "string", "description": "输入关键词" },
|
|
65
|
+
"resultLimit": { "type": "integer" },
|
|
66
|
+
"language": { "type": "string" },
|
|
67
|
+
"region": { "type": "string" },
|
|
68
|
+
"locationBias": {
|
|
69
|
+
"type": "object",
|
|
70
|
+
"properties": {
|
|
71
|
+
"center": {
|
|
72
|
+
"type": "object",
|
|
73
|
+
"properties": {
|
|
74
|
+
"latitude": { "type": "number" },
|
|
75
|
+
"longitude": { "type": "number" }
|
|
76
|
+
}
|
|
77
|
+
},
|
|
78
|
+
"radius": { "type": "number" }
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
},
|
|
82
|
+
"required": ["query"]
|
|
83
|
+
}
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
"name": "map_search_places",
|
|
87
|
+
"description": "地点详情。arguments 需含 placeId,可选 language、region、sessionToken、includeExtraFields。",
|
|
88
|
+
"inputSchema": {
|
|
89
|
+
"type": "object",
|
|
90
|
+
"properties": {
|
|
91
|
+
"placeId": { "type": "string", "description": "地点 ID" },
|
|
92
|
+
"language": { "type": "string" },
|
|
93
|
+
"region": { "type": "string" },
|
|
94
|
+
"sessionToken": { "type": "string" },
|
|
95
|
+
"includeExtraFields": { "type": "boolean" }
|
|
96
|
+
},
|
|
97
|
+
"required": ["placeId"]
|
|
98
|
+
}
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
"name": "map_search_geocode",
|
|
102
|
+
"description": "正地理编码。arguments 字段与 GET /search/v1/geocode 查询参数一致。",
|
|
103
|
+
"inputSchema": {
|
|
104
|
+
"type": "object",
|
|
105
|
+
"properties": {
|
|
106
|
+
"address": { "type": "string", "description": "地址" },
|
|
107
|
+
"components": { "type": "string" },
|
|
108
|
+
"locationBias": { "type": "string" },
|
|
109
|
+
"language": { "type": "string" },
|
|
110
|
+
"region": { "type": "string" }
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
"name": "map_search_reverse",
|
|
116
|
+
"description": "逆地理编码。arguments 字段与 GET /search/v1/reverse 查询参数一致(location 为 lat,lng 字符串)。",
|
|
117
|
+
"inputSchema": {
|
|
118
|
+
"type": "object",
|
|
119
|
+
"properties": {
|
|
120
|
+
"location": { "type": "string", "description": "纬度,经度,如 1.3521,103.8198" },
|
|
121
|
+
"resultType": { "type": "string" },
|
|
122
|
+
"locationType": { "type": "string" },
|
|
123
|
+
"language": { "type": "string" },
|
|
124
|
+
"region": { "type": "string" }
|
|
125
|
+
},
|
|
126
|
+
"required": ["location"]
|
|
127
|
+
}
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
"name": "map_routes_directions",
|
|
131
|
+
"description": "驾车路线规划。入参与 POST /routes/v1/directions 一致;未传 mode 时默认 driving。",
|
|
132
|
+
"inputSchema": {
|
|
133
|
+
"type": "object",
|
|
134
|
+
"properties": {
|
|
135
|
+
"origin": {
|
|
136
|
+
"type": "object",
|
|
137
|
+
"properties": {
|
|
138
|
+
"latitude": { "type": "number" },
|
|
139
|
+
"longitude": { "type": "number" }
|
|
140
|
+
}
|
|
141
|
+
},
|
|
142
|
+
"destination": {
|
|
143
|
+
"type": "object",
|
|
144
|
+
"properties": {
|
|
145
|
+
"latitude": { "type": "number" },
|
|
146
|
+
"longitude": { "type": "number" }
|
|
147
|
+
}
|
|
148
|
+
},
|
|
149
|
+
"waypoints": {
|
|
150
|
+
"type": "array",
|
|
151
|
+
"items": {
|
|
152
|
+
"type": "object",
|
|
153
|
+
"properties": {
|
|
154
|
+
"latitude": { "type": "number" },
|
|
155
|
+
"longitude": { "type": "number" }
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
},
|
|
159
|
+
"mode": { "type": "string", "description": "出行方式,默认 driving" },
|
|
160
|
+
"strategy": { "type": "string" },
|
|
161
|
+
"departureTime": { "type": "string" },
|
|
162
|
+
"trafficEnabled": { "type": "boolean" }
|
|
163
|
+
},
|
|
164
|
+
"required": ["origin", "destination"]
|
|
165
|
+
}
|
|
166
|
+
},
|
|
167
|
+
{
|
|
168
|
+
"name": "map_matrix_distance",
|
|
169
|
+
"description": "距离/时间矩阵。入参与 POST /matrix/v1/distance 一致(origins、destinations、mode、traffic_enabled 等)。",
|
|
170
|
+
"inputSchema": {
|
|
171
|
+
"type": "object",
|
|
172
|
+
"properties": {
|
|
173
|
+
"origins": {
|
|
174
|
+
"type": "array",
|
|
175
|
+
"items": {
|
|
176
|
+
"type": "object",
|
|
177
|
+
"properties": {
|
|
178
|
+
"lat": { "type": "number" },
|
|
179
|
+
"lng": { "type": "number" },
|
|
180
|
+
"lon": { "type": "number" }
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
},
|
|
184
|
+
"destinations": {
|
|
185
|
+
"type": "array",
|
|
186
|
+
"items": {
|
|
187
|
+
"type": "object",
|
|
188
|
+
"properties": {
|
|
189
|
+
"lat": { "type": "number" },
|
|
190
|
+
"lng": { "type": "number" },
|
|
191
|
+
"lon": { "type": "number" }
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
},
|
|
195
|
+
"mode": { "type": "string" },
|
|
196
|
+
"traffic_enabled": { "type": "boolean" }
|
|
197
|
+
},
|
|
198
|
+
"required": ["origins", "destinations"]
|
|
199
|
+
}
|
|
200
|
+
},
|
|
201
|
+
{
|
|
202
|
+
"name": "map_track_snap_to_roads",
|
|
203
|
+
"description": "轨迹绑路。入参与 POST /track/v1/snapToRoads 一致(points[].latitude/longitude 等)。",
|
|
204
|
+
"inputSchema": {
|
|
205
|
+
"type": "object",
|
|
206
|
+
"properties": {
|
|
207
|
+
"points": {
|
|
208
|
+
"type": "array",
|
|
209
|
+
"items": {
|
|
210
|
+
"type": "object",
|
|
211
|
+
"properties": {
|
|
212
|
+
"latitude": { "type": "number" },
|
|
213
|
+
"longitude": { "type": "number" },
|
|
214
|
+
"timestamp": { "type": "string" }
|
|
215
|
+
},
|
|
216
|
+
"required": ["latitude", "longitude"]
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
},
|
|
220
|
+
"required": ["points"]
|
|
221
|
+
}
|
|
222
|
+
},
|
|
223
|
+
{
|
|
224
|
+
"name": "map_location_ip",
|
|
225
|
+
"description": "IP 定位。入参与 POST /location/v1/ip 请求体一致(ip、output、coord_type)。",
|
|
226
|
+
"inputSchema": {
|
|
227
|
+
"type": "object",
|
|
228
|
+
"properties": {
|
|
229
|
+
"ip": { "type": "string", "description": "IPv4 或 IPv6 地址" },
|
|
230
|
+
"output": { "type": "string", "description": "输出格式,如 json" },
|
|
231
|
+
"coord_type": { "type": "string", "description": "坐标系,如 wgs84" }
|
|
232
|
+
},
|
|
233
|
+
"required": ["ip"]
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
]
|
|
237
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import mcp.types as types
|
|
4
|
+
from mcp.server.fastmcp import FastMCP
|
|
5
|
+
|
|
6
|
+
from maptec_mcp_python.client.maptec_client import MaptecClient, load_tools
|
|
7
|
+
from maptec_mcp_python.config import load_config
|
|
8
|
+
|
|
9
|
+
mcp = FastMCP(
|
|
10
|
+
name="maptec-mcp-python",
|
|
11
|
+
instructions="MapTech MCP Server - geospatial APIs for search, routing, matrix, track and IP location.",
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
_client: MaptecClient | None = None
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _get_client() -> MaptecClient:
|
|
18
|
+
global _client
|
|
19
|
+
if _client is None:
|
|
20
|
+
_client = MaptecClient(load_config())
|
|
21
|
+
return _client
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
async def list_tools() -> list[types.Tool]:
|
|
25
|
+
return [
|
|
26
|
+
types.Tool(
|
|
27
|
+
name=tool.name,
|
|
28
|
+
description=tool.description,
|
|
29
|
+
inputSchema=tool.input_schema,
|
|
30
|
+
)
|
|
31
|
+
for tool in load_tools()
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
async def dispatch(
|
|
36
|
+
name: str, arguments: dict
|
|
37
|
+
) -> list[types.TextContent | types.ImageContent | types.EmbeddedResource]:
|
|
38
|
+
try:
|
|
39
|
+
text = await _get_client().call_tool(name, arguments or {})
|
|
40
|
+
return [types.TextContent(type="text", text=text)]
|
|
41
|
+
except Exception as error:
|
|
42
|
+
return [types.TextContent(type="text", text=str(error))]
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
mcp._mcp_server.list_tools()(list_tools)
|
|
46
|
+
mcp._mcp_server.call_tool()(dispatch)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def main() -> None:
|
|
50
|
+
mcp.run()
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
if __name__ == "__main__":
|
|
54
|
+
main()
|
|
File without changes
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
STRIP_FIELDS = frozenset(
|
|
6
|
+
{
|
|
7
|
+
"polyline",
|
|
8
|
+
"encodedPolyline",
|
|
9
|
+
"encoded_polyline",
|
|
10
|
+
"geometry",
|
|
11
|
+
"overview_polyline",
|
|
12
|
+
"overviewPolyline",
|
|
13
|
+
}
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def filter_large_fields(value: Any) -> Any:
|
|
18
|
+
if value is None:
|
|
19
|
+
return value
|
|
20
|
+
if isinstance(value, list):
|
|
21
|
+
return [filter_large_fields(item) for item in value]
|
|
22
|
+
if isinstance(value, dict):
|
|
23
|
+
return {
|
|
24
|
+
key: filter_large_fields(child)
|
|
25
|
+
for key, child in value.items()
|
|
26
|
+
if key not in STRIP_FIELDS
|
|
27
|
+
}
|
|
28
|
+
return value
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def mask_api_key(text: str, api_key: str) -> str:
|
|
5
|
+
if not text or not api_key or api_key not in text:
|
|
6
|
+
return text
|
|
7
|
+
|
|
8
|
+
if len(api_key) <= 8:
|
|
9
|
+
masked = api_key[:2] + "*" * max(0, len(api_key) - 4) + api_key[-2:]
|
|
10
|
+
else:
|
|
11
|
+
masked = api_key[:4] + "*" * max(0, len(api_key) - 8) + api_key[-4:]
|
|
12
|
+
|
|
13
|
+
return text.replace(api_key, masked)
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: mcp-server-maptec
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: MapTech MCP Server (Python) - stdio transport for Cursor / Claude Desktop
|
|
5
|
+
Project-URL: Homepage, https://github.com/maptechbj/maptec-mcp-python
|
|
6
|
+
Project-URL: Repository, https://github.com/maptechbj/maptec-mcp-python
|
|
7
|
+
Author: MapTech
|
|
8
|
+
License: MIT
|
|
9
|
+
Keywords: http,lbs,map,maptec,mcp
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
17
|
+
Requires-Python: >=3.11
|
|
18
|
+
Requires-Dist: httpx>=0.27.0
|
|
19
|
+
Requires-Dist: mcp[cli]>=1.12.0
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
|
|
22
|
+
# mcp-server-maptec
|
|
23
|
+
|
|
24
|
+
Maptec的webapi服务已全面兼容[MCP协议](https://modelcontextprotocol.io/docs/getting-started/intro)。
|
|
25
|
+
Maptec MCP Server支持10个LBS服务能力,包含了地点检索、周边检索、输入提示、地点详情、地理编码、逆地理编码、路线规划、距离矩阵、轨迹纠偏、IP定位。
|
|
26
|
+
|
|
27
|
+
## 环境变量
|
|
28
|
+
|
|
29
|
+
| 变量 | 默认值 | 说明 |
|
|
30
|
+
|------|--------|------|
|
|
31
|
+
| `MAPTEC_API_KEY` | — | **必填**,`mp_1_...` 格式 |
|
|
32
|
+
| `MAPTEC_BASE_URL` | `https://api.maptec.cn` | WebAPI 地址 |
|
|
33
|
+
| `MAPTEC_TIMEOUT_MS` | `30000` | HTTP 超时(毫秒) |
|
|
34
|
+
|
|
35
|
+
## 快速开始
|
|
36
|
+
|
|
37
|
+
### 1. 注册账号并申请 AK
|
|
38
|
+
|
|
39
|
+
前往 **Maptec 开发者中心** 注册账号,创建应用并申请 API Key(AK,`mp_1_...` 格式)。
|
|
40
|
+
|
|
41
|
+
### 2. 安装 Python 包
|
|
42
|
+
|
|
43
|
+
推荐使用 [uv](https://docs.astral.sh/uv/) 一键运行(无需预先安装):
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
uvx mcp-server-maptec
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
或通过 pip 安装:
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
pip install mcp-server-maptec
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
安装后可执行:`maptec-mcp-python`
|
|
56
|
+
|
|
57
|
+
### 3. 在 Cursor 中配置 MCP
|
|
58
|
+
|
|
59
|
+
以 Cursor 为例,在其 MCP 配置中加入以下内容,填入上一步申请的 API Key 后保存,进入 **Settings → MCP → Reload**:
|
|
60
|
+
|
|
61
|
+
```json
|
|
62
|
+
{
|
|
63
|
+
"mcpServers": {
|
|
64
|
+
"maptec": {
|
|
65
|
+
"command": "uvx",
|
|
66
|
+
"args": ["mcp-server-maptec"],
|
|
67
|
+
"env": {
|
|
68
|
+
"MAPTEC_API_KEY": "mp_1_YOUR_KEY",
|
|
69
|
+
"MAPTEC_BASE_URL": "https://api.maptec.cn"
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
确认 `maptec` 为绿点 Connected,展开可见 10 个 Tool。
|
|
77
|
+
|
|
78
|
+
上述 `mcpServers` 为 MCP stdio 通用配置,Claude Desktop、Cline、Windsurf 等支持 MCP 的客户端同样适用,差异仅在于配置文件位置与重载方式。
|
|
79
|
+
|
|
80
|
+
### 4. 应用场景示例:第三方智能体调用 MCP 查询美食
|
|
81
|
+
|
|
82
|
+
除 Cursor 等 IDE 外,任何支持 MCP 的**第三方智能体**(如基于 LangChain、OpenAI Agents SDK、Claude Agent、或自研 Agent 框架)都可通过官方 MCP 客户端以 stdio 方式拉起本服务,把 10 个 LBS Tool 暴露给大模型自动编排。下面以「查询美食」为例。
|
|
83
|
+
|
|
84
|
+
**第三方 Agent 接入(stdio 启动 + 传入 API Key):**
|
|
85
|
+
|
|
86
|
+
以官方 [MCP Python SDK](https://github.com/modelcontextprotocol/python-sdk) 为例,Agent 在初始化时以子进程方式拉起 MCP Server,随后 `list_tools()` 拿到工具清单交给大模型:
|
|
87
|
+
|
|
88
|
+
```python
|
|
89
|
+
import asyncio
|
|
90
|
+
from mcp import ClientSession, StdioServerParameters
|
|
91
|
+
from mcp.client.stdio import stdio_client
|
|
92
|
+
|
|
93
|
+
async def main():
|
|
94
|
+
server_params = StdioServerParameters(
|
|
95
|
+
command="uvx",
|
|
96
|
+
args=["mcp-server-maptec"],
|
|
97
|
+
env={
|
|
98
|
+
"MAPTEC_API_KEY": "mp_1_YOUR_KEY",
|
|
99
|
+
"MAPTEC_BASE_URL": "https://api.maptec.cn",
|
|
100
|
+
},
|
|
101
|
+
)
|
|
102
|
+
async with stdio_client(server_params) as (read, write):
|
|
103
|
+
async with ClientSession(read, write) as session:
|
|
104
|
+
await session.initialize()
|
|
105
|
+
|
|
106
|
+
# 交给大模型:把工具清单作为 function/tool 列表
|
|
107
|
+
tools = await session.list_tools()
|
|
108
|
+
|
|
109
|
+
# 大模型决定调用某个工具时:
|
|
110
|
+
result = await session.call_tool(
|
|
111
|
+
"map_search_nearby",
|
|
112
|
+
arguments={
|
|
113
|
+
"query": "火锅",
|
|
114
|
+
"locationLimit": {
|
|
115
|
+
"center": {"latitude": 1.3048, "longitude": 103.8318},
|
|
116
|
+
"radius": 2000,
|
|
117
|
+
},
|
|
118
|
+
"resultLimit": 5,
|
|
119
|
+
"region": "SG",
|
|
120
|
+
},
|
|
121
|
+
)
|
|
122
|
+
print(result)
|
|
123
|
+
|
|
124
|
+
asyncio.run(main())
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
> Node.js 智能体同理:用 `@modelcontextprotocol/sdk` 的 `StdioClientTransport`,`command`/`args`/`env` 改为 `npx -y @maptec/mcp-server-node` 即可。
|
|
128
|
+
|
|
129
|
+
**用户提问(发给第三方智能体):**
|
|
130
|
+
|
|
131
|
+
> 我在新加坡乌节路附近,想找评分高的火锅店,推荐几家,并告诉我从这里去最近一家怎么走。
|
|
132
|
+
|
|
133
|
+
**Agent 编排流程:**
|
|
134
|
+
|
|
135
|
+
```mermaid
|
|
136
|
+
sequenceDiagram
|
|
137
|
+
participant U as 用户
|
|
138
|
+
participant LLM as 智能体大模型
|
|
139
|
+
participant MCP as Maptec MCP
|
|
140
|
+
U->>LLM: 找乌节路附近评分高的火锅店并给出路线
|
|
141
|
+
LLM->>MCP: map_search_geocode(乌节路)
|
|
142
|
+
MCP-->>LLM: 坐标
|
|
143
|
+
LLM->>MCP: map_search_nearby(火锅, center, resultLimit)
|
|
144
|
+
MCP-->>LLM: POI 列表(placeId...)
|
|
145
|
+
LLM->>MCP: map_search_places(placeId)
|
|
146
|
+
MCP-->>LLM: 店铺详情
|
|
147
|
+
LLM->>MCP: map_routes_directions(origin, destination, walking)
|
|
148
|
+
MCP-->>LLM: 步行路线
|
|
149
|
+
LLM-->>U: 自然语言推荐 + 导航
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
**关键工具调用示例:**
|
|
153
|
+
|
|
154
|
+
1. `map_search_geocode` 把地址转为坐标:
|
|
155
|
+
|
|
156
|
+
```json
|
|
157
|
+
{
|
|
158
|
+
"address": "乌节路, 新加坡",
|
|
159
|
+
"region": "SG"
|
|
160
|
+
}
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
2. `map_search_nearby` 以坐标为中心检索周边火锅店:
|
|
164
|
+
|
|
165
|
+
```json
|
|
166
|
+
{
|
|
167
|
+
"query": "火锅",
|
|
168
|
+
"locationLimit": {
|
|
169
|
+
"center": { "latitude": 1.3048, "longitude": 103.8318 },
|
|
170
|
+
"radius": 2000
|
|
171
|
+
},
|
|
172
|
+
"resultLimit": 5,
|
|
173
|
+
"region": "SG"
|
|
174
|
+
}
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
3. `map_search_places` 取 Top 店铺详情,`placeId` 来自上一步结果:
|
|
178
|
+
|
|
179
|
+
```json
|
|
180
|
+
{
|
|
181
|
+
"placeId": "PLACE_ID_FROM_NEARBY",
|
|
182
|
+
"language": "zh-CN",
|
|
183
|
+
"region": "SG"
|
|
184
|
+
}
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
4. `map_routes_directions` 规划到最近一家的步行路线:
|
|
188
|
+
|
|
189
|
+
```json
|
|
190
|
+
{
|
|
191
|
+
"origin": { "latitude": 1.3048, "longitude": 103.8318 },
|
|
192
|
+
"destination": { "latitude": 1.3021, "longitude": 103.8360 },
|
|
193
|
+
"mode": "walking"
|
|
194
|
+
}
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
**Agent 最终回答示例:**
|
|
198
|
+
|
|
199
|
+
> 已为你找到乌节路附近 3 家评分较高的火锅店:
|
|
200
|
+
> 1. 海底捞火锅(ION Orchard)— 评分 4.6,营业至 22:30,步行约 6 分钟;
|
|
201
|
+
> 2. 小龙坎老火锅(313@Somerset)— 评分 4.5,营业至 23:00,步行约 9 分钟;
|
|
202
|
+
> 3. 谭鸭血老火锅(Orchard Central)— 评分 4.4,营业至 22:00,步行约 11 分钟。
|
|
203
|
+
>
|
|
204
|
+
> 最近的是海底捞(ION Orchard),从你当前位置沿乌节路向东步行约 450 米、6 分钟即可到达。
|
|
205
|
+
|
|
206
|
+
以上全部由大模型自动调用 MCP 工具完成,用户无需关心任何接口参数。
|
|
207
|
+
|
|
208
|
+
## Tool 列表
|
|
209
|
+
|
|
210
|
+
| Tool | 说明 |
|
|
211
|
+
|------|------|
|
|
212
|
+
| `map_search_text` | 关键字检索 |
|
|
213
|
+
| `map_search_nearby` | 周边检索 |
|
|
214
|
+
| `map_search_suggest` | 输入提示 |
|
|
215
|
+
| `map_search_places` | 地点详情 |
|
|
216
|
+
| `map_search_geocode` | 地理编码 |
|
|
217
|
+
| `map_search_reverse` | 逆地理编码 |
|
|
218
|
+
| `map_routes_directions` | 路线规划 |
|
|
219
|
+
| `map_matrix_distance` | 距离矩阵 |
|
|
220
|
+
| `map_track_snap_to_roads` | 轨迹纠偏 |
|
|
221
|
+
| `map_location_ip` | IP 定位 |
|
|
222
|
+
|
|
223
|
+
## License
|
|
224
|
+
|
|
225
|
+
[MIT](./LICENSE) © 2026 Maptec
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
maptec_mcp_python/__init__.py,sha256=do5ladNQs7W3rZZDf0pA2TF4k7tlUNmEIOGkn0saX_I,62
|
|
2
|
+
maptec_mcp_python/__main__.py,sha256=uuTvHM2UjhXonzmHfBs7_9YTnxd-b7QwMOI6hDPESj4,74
|
|
3
|
+
maptec_mcp_python/config.py,sha256=VTVJ8tapjl8SOMS5msArDGcgUgtlgSQ-b3asW5PHsZk,799
|
|
4
|
+
maptec_mcp_python/server.py,sha256=BRzoaaHTQnWopDZ68auiCUdjLdA0dlWslaSwwJW3IT0,1325
|
|
5
|
+
maptec_mcp_python/client/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
6
|
+
maptec_mcp_python/client/maptec_client.py,sha256=9GP_C22Lu7Hh65aOMufxveOwb_aYeJLsT6RzddxMgjY,5906
|
|
7
|
+
maptec_mcp_python/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
8
|
+
maptec_mcp_python/utils/filter.py,sha256=XsOwMB7TC0hI1xU1yF6uSpubtAbutkgyBT5OwXGI_Kc,637
|
|
9
|
+
maptec_mcp_python/utils/mask.py,sha256=XrQ799WHLRYci_FCPp9dt_ubWGdEjWi8ehMp7XYMOPc,395
|
|
10
|
+
maptec_mcp_python/schemas/tools.json,sha256=xeLqKQEGNwWg5qg_5m8j_EY2-uJowSL4HrZZWReeNj0,8151
|
|
11
|
+
mcp_server_maptec-1.0.0.dist-info/METADATA,sha256=gaOTy7zrQYkv6P0kM0nBwM0uv0RPcBd0JEcWPn_flc8,7227
|
|
12
|
+
mcp_server_maptec-1.0.0.dist-info/WHEEL,sha256=C2FUgwZgiLbznR-k0b_5k3Ai_1aASOXDss3lzCUsUug,87
|
|
13
|
+
mcp_server_maptec-1.0.0.dist-info/entry_points.txt,sha256=sWuShRSXEMn4OYtYE7usW5z-1TzDkDoaLeeYOQdt4WY,61
|
|
14
|
+
mcp_server_maptec-1.0.0.dist-info/licenses/LICENSE,sha256=kJ5Ac7XiSNgG7LklEWofZ_tZ7ulNdt-4T0hoZ5221cI,1063
|
|
15
|
+
mcp_server_maptec-1.0.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Maptec
|
|
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.
|