apple-maps-mcp 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,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Apple MCP Contributors
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,119 @@
1
+ Metadata-Version: 2.4
2
+ Name: apple-maps-mcp
3
+ Version: 1.0.0
4
+ Summary: Local Apple Maps MCP server for macOS
5
+ License-Expression: MIT
6
+ Project-URL: Homepage, https://github.com/JonathanRReed/Apple-MCPs
7
+ Project-URL: Repository, https://github.com/JonathanRReed/Apple-MCPs
8
+ Project-URL: Changelog, https://github.com/JonathanRReed/Apple-MCPs/blob/main/CHANGELOG.md
9
+ Project-URL: Issues, https://github.com/JonathanRReed/Apple-MCPs/issues
10
+ Keywords: mcp,model-context-protocol,macos,apple,ai-agent,automation
11
+ Classifier: Development Status :: 5 - Production/Stable
12
+ Classifier: Environment :: MacOS X
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Operating System :: MacOS
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Programming Language :: Python :: 3.13
18
+ Classifier: Programming Language :: Python :: 3.14
19
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
20
+ Requires-Python: >=3.11
21
+ Description-Content-Type: text/markdown
22
+ License-File: LICENSE
23
+ Requires-Dist: apple-mcp-common<2,>=1.0.0
24
+ Requires-Dist: mcp<3,>=2.0.0
25
+ Requires-Dist: pydantic>=2.12.0
26
+ Provides-Extra: dev
27
+ Requires-Dist: pytest<10,>=8; extra == "dev"
28
+ Requires-Dist: ruff<1,>=0.12; extra == "dev"
29
+ Dynamic: license-file
30
+
31
+ <!-- mcp-name: io.github.jonathanrreed/apple-maps-mcp -->
32
+
33
+ # Apple Maps MCP
34
+
35
+ Local MCP server for Apple Maps search and routing on macOS.
36
+
37
+ ## Capabilities
38
+
39
+ - search for places
40
+ - estimate route distance and travel time
41
+ - build Apple Maps links
42
+ - open directions in Apple Maps
43
+ - resource: `maps://status`
44
+ - prompt: `maps_plan_route`
45
+ - tool discovery helpers `search_tools` and `get_tool_info` for context-constrained clients
46
+
47
+ ## Install On This Mac
48
+
49
+ <details>
50
+ <summary>Quick start (uvx, from PyPI)</summary>
51
+
52
+ With [uv](https://docs.astral.sh/uv/getting-started/installation/) installed:
53
+
54
+ ```bash
55
+ uvx apple-maps-mcp
56
+ ```
57
+
58
+ No clone, no venv management.
59
+
60
+ </details>
61
+
62
+ <details>
63
+ <summary>From a clone</summary>
64
+
65
+ ```bash
66
+ git clone https://github.com/JonathanRReed/Apple-MCPs.git
67
+ cd Apple-MCPs
68
+ uv sync --all-packages
69
+ ```
70
+
71
+ This builds one workspace environment with every server's entry point in `.venv/bin` (for example `.venv/bin/apple-maps-mcp`). You can also point an MCP client at `AppleMaps-MCP/start.sh`, which prefers `uv run` and falls back to a plain venv bootstrap (Python 3.11+ required).
72
+
73
+ </details>
74
+
75
+ ## Install In AI Agents
76
+
77
+ ```json
78
+ {
79
+ "mcpServers": {
80
+ "apple-maps": {
81
+ "command": "uvx",
82
+ "args": ["apple-maps-mcp"],
83
+ "env": {}
84
+ }
85
+ }
86
+ }
87
+ ```
88
+
89
+ Running from a clone instead? Use `/path/to/Apple-MCPs/AppleMaps-MCP/start.sh` as the command with empty `args`.
90
+
91
+ Claude Code:
92
+
93
+ ```bash
94
+ claude mcp add --transport stdio --scope project apple-maps -- uvx apple-maps-mcp
95
+ ```
96
+
97
+ ## Transport
98
+
99
+ `stdio` is the default and recommended transport. Set `APPLE_MAPS_MCP_TRANSPORT=streamable-http` (with optional `APPLE_MAPS_MCP_HOST` and `APPLE_MAPS_MCP_PORT`) to serve Streamable HTTP instead.
100
+
101
+ ## Prompting Notes
102
+
103
+ - `tools/list` returns the full Maps tool surface. Context-constrained clients can use `search_tools` first, then `get_tool_info` for the Maps tool they need.
104
+ - Use this server when travel, routing, or place lookup affects a Calendar, Reminders, Messages, or Mail action.
105
+ - Confirm origin, destination, and transport mode before writing a time-sensitive plan.
106
+ - If helper compilation fails, install Xcode command line tools and retry.
107
+
108
+ ## Health And Recovery
109
+
110
+ - `maps_health`
111
+ - `maps_permission_guide`
112
+
113
+ ## Launch Checklist
114
+
115
+ - Add `uvx apple-maps-mcp` (or a clone's `AppleMaps-MCP/start.sh`) to your MCP client
116
+ - Reload or reconnect the client so the Maps tool surface is loaded into context
117
+ - Call `maps_health` first
118
+ - If the local helper or routing surface is blocked, call `maps_permission_guide`
119
+ - Run `maps_search_places` once to confirm the local Swift helper compiles
@@ -0,0 +1,89 @@
1
+ <!-- mcp-name: io.github.jonathanrreed/apple-maps-mcp -->
2
+
3
+ # Apple Maps MCP
4
+
5
+ Local MCP server for Apple Maps search and routing on macOS.
6
+
7
+ ## Capabilities
8
+
9
+ - search for places
10
+ - estimate route distance and travel time
11
+ - build Apple Maps links
12
+ - open directions in Apple Maps
13
+ - resource: `maps://status`
14
+ - prompt: `maps_plan_route`
15
+ - tool discovery helpers `search_tools` and `get_tool_info` for context-constrained clients
16
+
17
+ ## Install On This Mac
18
+
19
+ <details>
20
+ <summary>Quick start (uvx, from PyPI)</summary>
21
+
22
+ With [uv](https://docs.astral.sh/uv/getting-started/installation/) installed:
23
+
24
+ ```bash
25
+ uvx apple-maps-mcp
26
+ ```
27
+
28
+ No clone, no venv management.
29
+
30
+ </details>
31
+
32
+ <details>
33
+ <summary>From a clone</summary>
34
+
35
+ ```bash
36
+ git clone https://github.com/JonathanRReed/Apple-MCPs.git
37
+ cd Apple-MCPs
38
+ uv sync --all-packages
39
+ ```
40
+
41
+ This builds one workspace environment with every server's entry point in `.venv/bin` (for example `.venv/bin/apple-maps-mcp`). You can also point an MCP client at `AppleMaps-MCP/start.sh`, which prefers `uv run` and falls back to a plain venv bootstrap (Python 3.11+ required).
42
+
43
+ </details>
44
+
45
+ ## Install In AI Agents
46
+
47
+ ```json
48
+ {
49
+ "mcpServers": {
50
+ "apple-maps": {
51
+ "command": "uvx",
52
+ "args": ["apple-maps-mcp"],
53
+ "env": {}
54
+ }
55
+ }
56
+ }
57
+ ```
58
+
59
+ Running from a clone instead? Use `/path/to/Apple-MCPs/AppleMaps-MCP/start.sh` as the command with empty `args`.
60
+
61
+ Claude Code:
62
+
63
+ ```bash
64
+ claude mcp add --transport stdio --scope project apple-maps -- uvx apple-maps-mcp
65
+ ```
66
+
67
+ ## Transport
68
+
69
+ `stdio` is the default and recommended transport. Set `APPLE_MAPS_MCP_TRANSPORT=streamable-http` (with optional `APPLE_MAPS_MCP_HOST` and `APPLE_MAPS_MCP_PORT`) to serve Streamable HTTP instead.
70
+
71
+ ## Prompting Notes
72
+
73
+ - `tools/list` returns the full Maps tool surface. Context-constrained clients can use `search_tools` first, then `get_tool_info` for the Maps tool they need.
74
+ - Use this server when travel, routing, or place lookup affects a Calendar, Reminders, Messages, or Mail action.
75
+ - Confirm origin, destination, and transport mode before writing a time-sensitive plan.
76
+ - If helper compilation fails, install Xcode command line tools and retry.
77
+
78
+ ## Health And Recovery
79
+
80
+ - `maps_health`
81
+ - `maps_permission_guide`
82
+
83
+ ## Launch Checklist
84
+
85
+ - Add `uvx apple-maps-mcp` (or a clone's `AppleMaps-MCP/start.sh`) to your MCP client
86
+ - Reload or reconnect the client so the Maps tool surface is loaded into context
87
+ - Call `maps_health` first
88
+ - If the local helper or routing surface is blocked, call `maps_permission_guide`
89
+ - Run `maps_search_places` once to confirm the local Swift helper compiles
@@ -0,0 +1,60 @@
1
+ [build-system]
2
+ requires = ["setuptools>=77", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "apple-maps-mcp"
7
+ version = "1.0.0"
8
+ description = "Local Apple Maps MCP server for macOS"
9
+ readme = "README.md"
10
+ license = "MIT"
11
+ license-files = ["LICENSE"]
12
+ requires-python = ">=3.11"
13
+ keywords = ["mcp", "model-context-protocol", "macos", "apple", "ai-agent", "automation"]
14
+ classifiers = [
15
+ "Development Status :: 5 - Production/Stable",
16
+ "Environment :: MacOS X",
17
+ "Intended Audience :: Developers",
18
+ "Operating System :: MacOS",
19
+ "Programming Language :: Python :: 3.11",
20
+ "Programming Language :: Python :: 3.12",
21
+ "Programming Language :: Python :: 3.13",
22
+ "Programming Language :: Python :: 3.14",
23
+ "Topic :: Software Development :: Libraries :: Python Modules",
24
+ ]
25
+ dependencies = [
26
+ "apple-mcp-common>=1.0.0,<2",
27
+ "mcp>=2.0.0,<3",
28
+ "pydantic>=2.12.0",
29
+ ]
30
+
31
+ [project.urls]
32
+ Homepage = "https://github.com/JonathanRReed/Apple-MCPs"
33
+ Repository = "https://github.com/JonathanRReed/Apple-MCPs"
34
+ Changelog = "https://github.com/JonathanRReed/Apple-MCPs/blob/main/CHANGELOG.md"
35
+ Issues = "https://github.com/JonathanRReed/Apple-MCPs/issues"
36
+
37
+ [project.scripts]
38
+ apple-maps-mcp = "apple_maps_mcp.tools:main"
39
+
40
+ [project.optional-dependencies]
41
+ dev = [
42
+ "pytest>=8,<10",
43
+ "ruff>=0.12,<1",
44
+ ]
45
+
46
+ [tool.setuptools]
47
+ package-dir = {"" = "src"}
48
+
49
+ [tool.setuptools.package-data]
50
+ apple_maps_mcp = ["*.swift"]
51
+
52
+ [tool.setuptools.packages.find]
53
+ where = ["src"]
54
+
55
+ [tool.pytest.ini_options]
56
+ pythonpath = ["src"]
57
+ testpaths = ["tests"]
58
+
59
+ [tool.uv.sources]
60
+ apple-mcp-common = { workspace = true }
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1 @@
1
+ """Apple Maps MCP package."""
@@ -0,0 +1,234 @@
1
+ import Foundation
2
+ import MapKit
3
+
4
+ struct BridgeFailure: Error {
5
+ let errorCode: String
6
+ let message: String
7
+ let suggestion: String?
8
+ }
9
+
10
+ struct ErrorPayload: Encodable {
11
+ let ok = false
12
+ let error_code: String
13
+ let message: String
14
+ let suggestion: String?
15
+ }
16
+
17
+ struct PlaceRecord: Encodable {
18
+ let name: String
19
+ let address: String?
20
+ let latitude: Double?
21
+ let longitude: Double?
22
+ let phone: String?
23
+ let url: String?
24
+ }
25
+
26
+ struct PlaceListPayload: Encodable {
27
+ let places: [PlaceRecord]
28
+ }
29
+
30
+ struct DirectionsPayload: Encodable {
31
+ let origin: PlaceRecord
32
+ let destination: PlaceRecord
33
+ let transport: String
34
+ let distance_meters: Double
35
+ let expected_travel_time_seconds: Double
36
+ let advisory_notices: [String]
37
+ }
38
+
39
+ @main
40
+ struct AppleMapsBridge {
41
+ static let asyncTimeoutSeconds: TimeInterval = 15
42
+ static let encoder: JSONEncoder = {
43
+ let encoder = JSONEncoder()
44
+ encoder.outputFormatting = [.sortedKeys]
45
+ return encoder
46
+ }()
47
+
48
+ static func main() async {
49
+ do {
50
+ let arguments = Array(CommandLine.arguments.dropFirst())
51
+ guard arguments.count == 2 else {
52
+ throw BridgeFailure(errorCode: "USAGE_ERROR", message: "Expected a command and a JSON payload.", suggestion: "Pass search-places or directions with a JSON object.")
53
+ }
54
+ let command = arguments[0]
55
+ let payload = try decodeJSONObject(arguments[1])
56
+ let data = try await handle(command: command, payload: payload)
57
+ FileHandle.standardOutput.write(data)
58
+ } catch let failure as BridgeFailure {
59
+ writeErrorAndExit(failure)
60
+ } catch {
61
+ writeErrorAndExit(BridgeFailure(errorCode: "UNEXPECTED_ERROR", message: String(describing: error), suggestion: "Retry the Apple Maps request."))
62
+ }
63
+ }
64
+
65
+ static func handle(command: String, payload: [String: Any]) async throws -> Data {
66
+ switch command {
67
+ case "search-places":
68
+ let query = try string(payload["query"], field: "query")
69
+ let limit = try int(payload["limit"], field: "limit", defaultValue: 5)
70
+ let places = try await searchPlaces(query: query, limit: limit)
71
+ return try encoder.encode(PlaceListPayload(places: places))
72
+ case "directions":
73
+ let originQuery = try string(payload["origin"], field: "origin")
74
+ let destinationQuery = try string(payload["destination"], field: "destination")
75
+ let transport = try string(payload["transport"], field: "transport")
76
+ let route = try await directions(originQuery: originQuery, destinationQuery: destinationQuery, transport: transport)
77
+ return try encoder.encode(route)
78
+ default:
79
+ throw BridgeFailure(errorCode: "UNKNOWN_COMMAND", message: "Unknown command '\(command)'.", suggestion: "Use search-places or directions.")
80
+ }
81
+ }
82
+
83
+ static func searchPlaces(query: String, limit: Int) async throws -> [PlaceRecord] {
84
+ let request = MKLocalSearch.Request()
85
+ request.naturalLanguageQuery = query
86
+ let search = MKLocalSearch(request: request)
87
+ let response = try await withTimeout(
88
+ errorCode: "SEARCH_TIMEOUT",
89
+ message: "Apple Maps search timed out.",
90
+ suggestion: "Retry the query, or confirm Maps services are available on this Mac."
91
+ ) {
92
+ try await search.start()
93
+ }
94
+ let items = response.mapItems.prefix(limit)
95
+ return items.map(placeRecord)
96
+ }
97
+
98
+ static func directions(originQuery: String, destinationQuery: String, transport: String) async throws -> DirectionsPayload {
99
+ let originItem = try await resolveFirstMapItem(query: originQuery)
100
+ let destinationItem = try await resolveFirstMapItem(query: destinationQuery)
101
+
102
+ let request = MKDirections.Request()
103
+ request.source = originItem
104
+ request.destination = destinationItem
105
+ request.transportType = transportType(transport)
106
+
107
+ let directions = MKDirections(request: request)
108
+ let routeResponse = try await withTimeout(
109
+ errorCode: "DIRECTIONS_TIMEOUT",
110
+ message: "Apple Maps directions timed out.",
111
+ suggestion: "Retry the request, or confirm Maps services are available on this Mac."
112
+ ) {
113
+ try await directions.calculate()
114
+ }
115
+
116
+ guard let route = routeResponse.routes.first else {
117
+ throw BridgeFailure(errorCode: "ROUTE_NOT_FOUND", message: "Apple Maps did not return a route.", suggestion: "Try a different transport mode or place query.")
118
+ }
119
+
120
+ return DirectionsPayload(
121
+ origin: placeRecord(originItem),
122
+ destination: placeRecord(destinationItem),
123
+ transport: transport,
124
+ distance_meters: route.distance,
125
+ expected_travel_time_seconds: route.expectedTravelTime,
126
+ advisory_notices: route.advisoryNotices
127
+ )
128
+ }
129
+
130
+ static func resolveFirstMapItem(query: String) async throws -> MKMapItem {
131
+ let request = MKLocalSearch.Request()
132
+ request.naturalLanguageQuery = query
133
+ let search = MKLocalSearch(request: request)
134
+ let response = try await withTimeout(
135
+ errorCode: "SEARCH_TIMEOUT",
136
+ message: "Apple Maps search timed out.",
137
+ suggestion: "Retry the query, or confirm Maps services are available on this Mac."
138
+ ) {
139
+ try await search.start()
140
+ }
141
+ guard let item = response.mapItems.first else {
142
+ throw BridgeFailure(errorCode: "PLACE_NOT_FOUND", message: "Apple Maps did not find a matching place.", suggestion: "Use a more specific address or place name.")
143
+ }
144
+ return item
145
+ }
146
+
147
+ static func placeRecord(_ item: MKMapItem) -> PlaceRecord {
148
+ let placemark = item.placemark
149
+ let formattedAddress = [placemark.subThoroughfare, placemark.thoroughfare, placemark.locality, placemark.administrativeArea, placemark.postalCode]
150
+ .compactMap { $0 }
151
+ .joined(separator: " ")
152
+
153
+ return PlaceRecord(
154
+ name: item.name ?? placemark.name ?? "Unknown",
155
+ address: formattedAddress.isEmpty ? nil : formattedAddress,
156
+ latitude: placemark.coordinate.latitude,
157
+ longitude: placemark.coordinate.longitude,
158
+ phone: item.phoneNumber,
159
+ url: item.url?.absoluteString
160
+ )
161
+ }
162
+
163
+ static func transportType(_ value: String) -> MKDirectionsTransportType {
164
+ switch value.lowercased() {
165
+ case "walking":
166
+ return .walking
167
+ case "transit":
168
+ return .transit
169
+ default:
170
+ return .automobile
171
+ }
172
+ }
173
+
174
+ static func decodeJSONObject(_ raw: String) throws -> [String: Any] {
175
+ guard
176
+ let data = raw.data(using: .utf8),
177
+ let payload = try JSONSerialization.jsonObject(with: data) as? [String: Any]
178
+ else {
179
+ throw BridgeFailure(errorCode: "INVALID_INPUT", message: "Expected a JSON object payload.", suggestion: "Pass a valid JSON object.")
180
+ }
181
+ return payload
182
+ }
183
+
184
+ static func string(_ value: Any?, field: String) throws -> String {
185
+ guard let text = value as? String, !text.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else {
186
+ throw BridgeFailure(errorCode: "INVALID_INPUT", message: "\(field) must be a non-empty string.", suggestion: "Provide the missing field.")
187
+ }
188
+ return text
189
+ }
190
+
191
+ static func int(_ value: Any?, field: String, defaultValue: Int) throws -> Int {
192
+ if value == nil {
193
+ return defaultValue
194
+ }
195
+ if let number = value as? Int {
196
+ return number
197
+ }
198
+ if let number = value as? NSNumber {
199
+ return number.intValue
200
+ }
201
+ throw BridgeFailure(errorCode: "INVALID_INPUT", message: "\(field) must be an integer.", suggestion: "Provide a numeric limit.")
202
+ }
203
+
204
+ static func withTimeout<T>(
205
+ errorCode: String,
206
+ message: String,
207
+ suggestion: String?,
208
+ operation: @escaping @Sendable () async throws -> T
209
+ ) async throws -> T {
210
+ try await withThrowingTaskGroup(of: T.self) { group in
211
+ group.addTask {
212
+ try await operation()
213
+ }
214
+ group.addTask {
215
+ let duration = UInt64(asyncTimeoutSeconds * 1_000_000_000)
216
+ try await Task.sleep(nanoseconds: duration)
217
+ throw BridgeFailure(errorCode: errorCode, message: message, suggestion: suggestion)
218
+ }
219
+ guard let result = try await group.next() else {
220
+ throw BridgeFailure(errorCode: errorCode, message: message, suggestion: suggestion)
221
+ }
222
+ group.cancelAll()
223
+ return result
224
+ }
225
+ }
226
+
227
+ static func writeErrorAndExit(_ failure: BridgeFailure) -> Never {
228
+ let payload = ErrorPayload(error_code: failure.errorCode, message: failure.message, suggestion: failure.suggestion)
229
+ if let data = try? encoder.encode(payload) {
230
+ FileHandle.standardError.write(data)
231
+ }
232
+ exit(1)
233
+ }
234
+ }
@@ -0,0 +1,46 @@
1
+ import os
2
+ from dataclasses import dataclass
3
+ from functools import lru_cache
4
+ from pathlib import Path
5
+
6
+
7
+ @dataclass(frozen=True)
8
+ class Settings:
9
+ server_name: str
10
+ version: str
11
+ helper_source: Path
12
+ helper_binary: Path
13
+ transport: str
14
+ host: str
15
+ port: int
16
+ log_level: str
17
+
18
+
19
+ def _parse_int(value: str | None, default: int) -> int:
20
+ if value is None:
21
+ return default
22
+ try:
23
+ return int(value)
24
+ except ValueError:
25
+ return default
26
+
27
+
28
+ @lru_cache(maxsize=1)
29
+ def load_settings() -> Settings:
30
+ package_dir = Path(__file__).resolve().parent
31
+ build_dir = Path(
32
+ os.environ.get(
33
+ "APPLE_MAPS_MCP_HELPER_BUILD_DIR",
34
+ str(Path.home() / ".apple-mcps" / "build"),
35
+ )
36
+ ).expanduser()
37
+ return Settings(
38
+ server_name="Apple Maps MCP",
39
+ version="0.1.0",
40
+ helper_source=package_dir / "apple_maps_bridge.swift",
41
+ helper_binary=build_dir / "apple-maps-bridge",
42
+ transport=os.environ.get("APPLE_MAPS_MCP_TRANSPORT", "stdio").strip().lower() or "stdio",
43
+ host=os.environ.get("APPLE_MAPS_MCP_HOST", "127.0.0.1"),
44
+ port=_parse_int(os.environ.get("APPLE_MAPS_MCP_PORT"), 8000),
45
+ log_level=os.environ.get("APPLE_MAPS_MCP_LOG_LEVEL", "INFO").strip().upper() or "INFO",
46
+ )
@@ -0,0 +1,115 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import subprocess
5
+ from dataclasses import dataclass
6
+ from pathlib import Path
7
+ from urllib.parse import quote_plus, urlencode
8
+
9
+ from apple_maps_mcp.config import load_settings
10
+
11
+
12
+ @dataclass(frozen=True)
13
+ class MapsBridgeError(Exception):
14
+ error_code: str
15
+ message: str
16
+ suggestion: str | None = None
17
+
18
+
19
+ class AppleMapsBridge:
20
+ def __init__(self, helper_source: Path, helper_binary: Path) -> None:
21
+ self.helper_source = helper_source
22
+ self.helper_binary = helper_binary
23
+ self.timeout_seconds = 20
24
+
25
+ def helper_available(self) -> tuple[bool, bool]:
26
+ return self.helper_source.exists(), self.helper_binary.exists()
27
+
28
+ def _ensure_helper(self) -> None:
29
+ if not self.helper_source.exists():
30
+ raise MapsBridgeError(
31
+ "HELPER_SOURCE_MISSING",
32
+ f"Missing Apple Maps helper source at '{self.helper_source}'.",
33
+ "Reinstall apple-maps-mcp and retry.",
34
+ )
35
+ self.helper_binary.parent.mkdir(parents=True, exist_ok=True)
36
+ if self.helper_binary.exists() and self.helper_binary.stat().st_mtime >= self.helper_source.stat().st_mtime:
37
+ return
38
+ try:
39
+ subprocess.run(
40
+ ["swiftc", "-parse-as-library", "-O", str(self.helper_source), "-o", str(self.helper_binary)],
41
+ capture_output=True,
42
+ check=True,
43
+ text=True,
44
+ timeout=self.timeout_seconds,
45
+ )
46
+ except subprocess.TimeoutExpired as exc:
47
+ raise MapsBridgeError(
48
+ "HELPER_COMPILE_TIMEOUT",
49
+ "Timed out while compiling the Apple Maps helper.",
50
+ "Confirm Xcode command line tools are installed, then retry.",
51
+ ) from exc
52
+ except OSError as exc:
53
+ raise MapsBridgeError(
54
+ "SWIFTC_UNAVAILABLE",
55
+ f"Could not run 'swiftc': {exc}.",
56
+ "This server requires macOS with the Swift toolchain (swiftc) available.",
57
+ ) from exc
58
+ except subprocess.CalledProcessError as exc:
59
+ stderr = exc.stderr.strip() if exc.stderr else ""
60
+ raise MapsBridgeError(
61
+ "HELPER_COMPILE_FAILED",
62
+ stderr or "Failed to compile the Apple Maps helper.",
63
+ "Install Xcode command line tools and retry.",
64
+ ) from exc
65
+
66
+ def _run_helper(self, command: str, payload: dict[str, object]) -> dict[str, object]:
67
+ self._ensure_helper()
68
+ try:
69
+ result = subprocess.run(
70
+ [str(self.helper_binary), command, json.dumps(payload)],
71
+ capture_output=True,
72
+ check=True,
73
+ text=True,
74
+ timeout=self.timeout_seconds,
75
+ )
76
+ except subprocess.TimeoutExpired as exc:
77
+ raise MapsBridgeError(
78
+ "HELPER_TIMEOUT",
79
+ "Apple Maps helper timed out while waiting for MapKit.",
80
+ "Retry the request, or confirm Maps and Location Services are available on this Mac.",
81
+ ) from exc
82
+ except OSError as exc:
83
+ raise MapsBridgeError(
84
+ "HELPER_UNAVAILABLE",
85
+ f"Could not run the Apple Maps helper '{self.helper_binary}': {exc}.",
86
+ "This server requires macOS with the compiled Apple Maps helper available.",
87
+ ) from exc
88
+ except subprocess.CalledProcessError as exc:
89
+ stderr = exc.stderr.strip() or exc.stdout.strip()
90
+ raise MapsBridgeError("HELPER_FAILED", stderr or "Apple Maps helper failed.", "Retry the request.") from exc
91
+ try:
92
+ return json.loads(result.stdout)
93
+ except json.JSONDecodeError as exc:
94
+ raise MapsBridgeError("INVALID_RESPONSE", "Apple Maps helper returned invalid JSON.", "Retry the request.") from exc
95
+
96
+ def search_places(self, query: str, limit: int = 5) -> dict[str, object]:
97
+ return self._run_helper("search-places", {"query": query, "limit": limit})
98
+
99
+ def directions(self, origin: str, destination: str, transport: str = "driving") -> dict[str, object]:
100
+ payload = self._run_helper("directions", {"origin": origin, "destination": destination, "transport": transport})
101
+ payload["maps_url"] = self.maps_url(destination=destination, origin=origin, transport=transport)
102
+ return payload
103
+
104
+ def maps_url(self, destination: str, origin: str | None = None, transport: str = "driving") -> str:
105
+ params = {"daddr": destination}
106
+ if origin:
107
+ params["saddr"] = origin
108
+ transport_flag = {"driving": "d", "walking": "w", "transit": "r"}.get(transport, "d")
109
+ params["dirflg"] = transport_flag
110
+ return f"https://maps.apple.com/?{urlencode(params, quote_via=quote_plus)}"
111
+
112
+
113
+ def build_bridge() -> AppleMapsBridge:
114
+ settings = load_settings()
115
+ return AppleMapsBridge(settings.helper_source, settings.helper_binary)
@@ -0,0 +1,62 @@
1
+ from __future__ import annotations
2
+
3
+ from pydantic import BaseModel, Field
4
+
5
+
6
+ class ToolError(BaseModel):
7
+ error_code: str = Field(description="Stable machine-readable error code")
8
+ message: str = Field(description="Human-readable error message")
9
+ suggestion: str | None = Field(default=None, description="Suggested next step")
10
+
11
+
12
+ class ErrorResponse(BaseModel):
13
+ ok: bool = False
14
+ error: ToolError
15
+
16
+
17
+ class HealthResponse(BaseModel):
18
+ ok: bool = True
19
+ server_name: str
20
+ version: str
21
+ helper_available: bool
22
+ helper_compiled: bool
23
+ transport: str
24
+ capabilities: list[str]
25
+ supports: list[str]
26
+
27
+
28
+ class PlaceRecord(BaseModel):
29
+ name: str
30
+ address: str | None = None
31
+ latitude: float | None = None
32
+ longitude: float | None = None
33
+ phone: str | None = None
34
+ url: str | None = None
35
+
36
+
37
+ class PlaceSearchResponse(BaseModel):
38
+ ok: bool = True
39
+ places: list[PlaceRecord]
40
+ count: int
41
+
42
+
43
+ class DirectionsResponse(BaseModel):
44
+ ok: bool = True
45
+ origin: PlaceRecord
46
+ destination: PlaceRecord
47
+ transport: str
48
+ distance_meters: float
49
+ expected_travel_time_seconds: float
50
+ advisory_notices: list[str]
51
+ maps_url: str
52
+
53
+
54
+ class MapsLinkResponse(BaseModel):
55
+ ok: bool = True
56
+ url: str
57
+
58
+
59
+ class OpenMapsResponse(BaseModel):
60
+ ok: bool = True
61
+ opened: bool
62
+ url: str
@@ -0,0 +1,231 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import subprocess
5
+
6
+ from mcp.server.mcpserver import MCPServer
7
+ from mcp.types import Annotations, ToolAnnotations
8
+
9
+ from apple_maps_mcp.config import load_settings
10
+ from apple_maps_mcp.maps_bridge import AppleMapsBridge, MapsBridgeError, build_bridge
11
+ from apple_maps_mcp.models import DirectionsResponse, ErrorResponse, HealthResponse, MapsLinkResponse, OpenMapsResponse, PlaceRecord, PlaceSearchResponse, ToolError
12
+ from apple_mcp_common.discovery import install_search_first_discovery
13
+
14
+ SERVER_INSTRUCTIONS = (
15
+ "Use this server for Apple Maps and travel context on macOS. "
16
+ "Search here when the user wants to find a place, estimate travel time, build an Apple Maps link, or open directions in Apple Maps."
17
+ )
18
+
19
+ mcp = MCPServer("Apple Maps MCP", instructions=SERVER_INSTRUCTIONS)
20
+
21
+
22
+ def _bridge() -> AppleMapsBridge:
23
+ return build_bridge()
24
+
25
+
26
+ def _error_response(error_code: str, message: str, suggestion: str | None = None) -> ErrorResponse:
27
+ return ErrorResponse(error=ToolError(error_code=error_code, message=message, suggestion=suggestion))
28
+
29
+
30
+ def _resource_json(value: object) -> str:
31
+ return json.dumps(value, indent=2, sort_keys=True, default=str)
32
+
33
+
34
+ @mcp.resource(
35
+ "maps://status",
36
+ name="maps_status",
37
+ title="Maps Status",
38
+ description="Apple Maps helper availability and supported transport modes.",
39
+ mime_type="application/json",
40
+ annotations=Annotations(audience=["assistant"], priority=0.75),
41
+ )
42
+ def maps_status_resource() -> str:
43
+ helper_available, helper_compiled = _bridge().helper_available()
44
+ return _resource_json(
45
+ {
46
+ "helper_available": helper_available,
47
+ "helper_compiled": helper_compiled,
48
+ "supported_transports": ["driving", "walking", "transit"],
49
+ }
50
+ )
51
+
52
+
53
+ @mcp.prompt(name="maps_plan_route", title="Plan Route")
54
+ def maps_plan_route_prompt() -> str:
55
+ return (
56
+ "Use Apple Maps to find the right destination, estimate travel time, and choose the right transport mode "
57
+ "before scheduling a meeting or sending directions."
58
+ )
59
+
60
+
61
+ @mcp.tool(
62
+ title="Maps Health",
63
+ description="Report the active Apple Maps MCP configuration.",
64
+ annotations=ToolAnnotations(read_only_hint=True, idempotent_hint=True),
65
+ structured_output=True,
66
+ )
67
+ def maps_health() -> HealthResponse:
68
+ settings = load_settings()
69
+ helper_available, helper_compiled = _bridge().helper_available()
70
+ return HealthResponse(
71
+ server_name=settings.server_name,
72
+ version=settings.version,
73
+ helper_available=helper_available,
74
+ helper_compiled=helper_compiled,
75
+ transport=settings.transport,
76
+ capabilities=[
77
+ "search_places",
78
+ "get_directions",
79
+ "build_maps_link",
80
+ "open_directions_in_maps",
81
+ "resources",
82
+ "prompts",
83
+ ],
84
+ supports=["stdio", "streamable-http"],
85
+ )
86
+
87
+
88
+ @mcp.tool(
89
+ title="Maps Permission Guide",
90
+ description="Explain Apple Maps MCP local helper requirements on macOS.",
91
+ annotations=ToolAnnotations(read_only_hint=True, idempotent_hint=True),
92
+ structured_output=True,
93
+ )
94
+ def maps_permission_guide() -> dict[str, object]:
95
+ return {
96
+ "ok": True,
97
+ "domain": "maps",
98
+ "can_prompt_in_app": False,
99
+ "requires_manual_system_settings": False,
100
+ "steps": [
101
+ "Apple Maps MCP uses a local Swift helper for search and routing.",
102
+ "If helper compilation fails, install Xcode command line tools and retry.",
103
+ "Opening a route uses the standard macOS open command with an Apple Maps URL.",
104
+ ],
105
+ }
106
+
107
+
108
+ @mcp.tool(
109
+ title="Search Places",
110
+ description="Search Apple Maps for matching places.",
111
+ annotations=ToolAnnotations(read_only_hint=True, idempotent_hint=True),
112
+ structured_output=True,
113
+ )
114
+ def maps_search_places(query: str, limit: int = 5) -> PlaceSearchResponse | ErrorResponse:
115
+ try:
116
+ payload = _bridge().search_places(query=query, limit=limit)
117
+ places = [PlaceRecord(**item) for item in payload.get("places", [])]
118
+ return PlaceSearchResponse(places=places, count=len(places))
119
+ except MapsBridgeError as exc:
120
+ return _error_response(exc.error_code, exc.message, exc.suggestion)
121
+
122
+
123
+ @mcp.tool(
124
+ title="Get Directions",
125
+ description="Get Apple Maps route details between an origin and destination.",
126
+ annotations=ToolAnnotations(read_only_hint=True, idempotent_hint=True),
127
+ structured_output=True,
128
+ )
129
+ def maps_get_directions(origin: str, destination: str, transport: str = "driving") -> DirectionsResponse | ErrorResponse:
130
+ try:
131
+ payload = _bridge().directions(origin=origin, destination=destination, transport=transport)
132
+ return DirectionsResponse(
133
+ origin=PlaceRecord(**payload["origin"]),
134
+ destination=PlaceRecord(**payload["destination"]),
135
+ transport=str(payload["transport"]),
136
+ distance_meters=float(payload["distance_meters"]),
137
+ expected_travel_time_seconds=float(payload["expected_travel_time_seconds"]),
138
+ advisory_notices=[str(item) for item in payload.get("advisory_notices", [])],
139
+ maps_url=str(payload["maps_url"]),
140
+ )
141
+ except MapsBridgeError as exc:
142
+ return _error_response(exc.error_code, exc.message, exc.suggestion)
143
+
144
+
145
+ @mcp.tool(
146
+ title="Build Maps Link",
147
+ description="Build an Apple Maps URL for a destination or route.",
148
+ annotations=ToolAnnotations(read_only_hint=True, idempotent_hint=True),
149
+ structured_output=True,
150
+ )
151
+ def maps_build_maps_link(destination: str, origin: str | None = None, transport: str = "driving") -> MapsLinkResponse:
152
+ return MapsLinkResponse(url=_bridge().maps_url(destination=destination, origin=origin, transport=transport))
153
+
154
+
155
+ @mcp.tool(
156
+ title="Open Directions In Maps",
157
+ description="Open directions in the Apple Maps app.",
158
+ annotations=ToolAnnotations(destructive_hint=False, idempotent_hint=False, open_world_hint=True),
159
+ structured_output=True,
160
+ )
161
+ def maps_open_directions_in_maps(destination: str, origin: str | None = None, transport: str = "driving") -> OpenMapsResponse | ErrorResponse:
162
+ try:
163
+ url = _bridge().maps_url(destination=destination, origin=origin, transport=transport)
164
+ subprocess.run(["open", url], capture_output=True, check=True, text=True)
165
+ return OpenMapsResponse(opened=True, url=url)
166
+ except MapsBridgeError as exc:
167
+ return _error_response(exc.error_code, exc.message, exc.suggestion)
168
+ except subprocess.CalledProcessError:
169
+ return _error_response("OPEN_FAILED", "Failed to open Apple Maps.", "Retry the request.")
170
+ except OSError:
171
+ return _error_response("OPEN_UNAVAILABLE", "Could not run the 'open' command.", "This server requires macOS with the 'open' command available.")
172
+
173
+
174
+ def _serialize_prompt_messages(messages: list[object]) -> list[dict[str, object]]:
175
+ return [
176
+ {
177
+ "role": getattr(message, "role", "user"),
178
+ "content": message.content.model_dump(mode="json") if hasattr(message.content, "model_dump") else message.content,
179
+ }
180
+ for message in messages
181
+ ]
182
+
183
+
184
+ @mcp.tool(
185
+ title="Maps List Prompts",
186
+ description="Fallback prompt discovery tool for tool-only MCP clients.",
187
+ annotations=ToolAnnotations(read_only_hint=True, idempotent_hint=True),
188
+ structured_output=True,
189
+ )
190
+ async def maps_list_prompts() -> dict[str, object]:
191
+ prompts = await mcp.list_prompts()
192
+ return {
193
+ "ok": True,
194
+ "prompts": [{"name": prompt.name, "title": prompt.title, "description": prompt.description} for prompt in prompts],
195
+ "count": len(prompts),
196
+ }
197
+
198
+
199
+ @mcp.tool(
200
+ name="maps_get_prompt",
201
+ title="Maps Get Prompt",
202
+ description="Fallback prompt rendering tool for tool-only MCP clients.",
203
+ annotations=ToolAnnotations(read_only_hint=True, idempotent_hint=True),
204
+ structured_output=True,
205
+ )
206
+ async def maps_get_prompt_prompt(name: str, arguments_json: str | None = None) -> dict[str, object]:
207
+ arguments = json.loads(arguments_json) if arguments_json else None
208
+ prompt = await mcp.get_prompt(name, arguments)
209
+ return {"ok": True, "name": name, "messages": _serialize_prompt_messages(prompt.messages), "message_count": len(prompt.messages)}
210
+
211
+
212
+ TOOL_DISCOVERY = install_search_first_discovery(
213
+ mcp,
214
+ server_name="Apple Maps MCP",
215
+ domain="maps",
216
+ )
217
+
218
+
219
+ def main() -> None:
220
+ settings = load_settings()
221
+ if settings.transport == "stdio":
222
+ mcp.run(transport="stdio")
223
+ return
224
+ mcp.settings.log_level = settings.log_level
225
+ mcp.run(
226
+ transport="streamable-http",
227
+ host=settings.host,
228
+ port=settings.port,
229
+ json_response=True,
230
+ stateless_http=True,
231
+ )
@@ -0,0 +1,119 @@
1
+ Metadata-Version: 2.4
2
+ Name: apple-maps-mcp
3
+ Version: 1.0.0
4
+ Summary: Local Apple Maps MCP server for macOS
5
+ License-Expression: MIT
6
+ Project-URL: Homepage, https://github.com/JonathanRReed/Apple-MCPs
7
+ Project-URL: Repository, https://github.com/JonathanRReed/Apple-MCPs
8
+ Project-URL: Changelog, https://github.com/JonathanRReed/Apple-MCPs/blob/main/CHANGELOG.md
9
+ Project-URL: Issues, https://github.com/JonathanRReed/Apple-MCPs/issues
10
+ Keywords: mcp,model-context-protocol,macos,apple,ai-agent,automation
11
+ Classifier: Development Status :: 5 - Production/Stable
12
+ Classifier: Environment :: MacOS X
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Operating System :: MacOS
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Programming Language :: Python :: 3.13
18
+ Classifier: Programming Language :: Python :: 3.14
19
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
20
+ Requires-Python: >=3.11
21
+ Description-Content-Type: text/markdown
22
+ License-File: LICENSE
23
+ Requires-Dist: apple-mcp-common<2,>=1.0.0
24
+ Requires-Dist: mcp<3,>=2.0.0
25
+ Requires-Dist: pydantic>=2.12.0
26
+ Provides-Extra: dev
27
+ Requires-Dist: pytest<10,>=8; extra == "dev"
28
+ Requires-Dist: ruff<1,>=0.12; extra == "dev"
29
+ Dynamic: license-file
30
+
31
+ <!-- mcp-name: io.github.jonathanrreed/apple-maps-mcp -->
32
+
33
+ # Apple Maps MCP
34
+
35
+ Local MCP server for Apple Maps search and routing on macOS.
36
+
37
+ ## Capabilities
38
+
39
+ - search for places
40
+ - estimate route distance and travel time
41
+ - build Apple Maps links
42
+ - open directions in Apple Maps
43
+ - resource: `maps://status`
44
+ - prompt: `maps_plan_route`
45
+ - tool discovery helpers `search_tools` and `get_tool_info` for context-constrained clients
46
+
47
+ ## Install On This Mac
48
+
49
+ <details>
50
+ <summary>Quick start (uvx, from PyPI)</summary>
51
+
52
+ With [uv](https://docs.astral.sh/uv/getting-started/installation/) installed:
53
+
54
+ ```bash
55
+ uvx apple-maps-mcp
56
+ ```
57
+
58
+ No clone, no venv management.
59
+
60
+ </details>
61
+
62
+ <details>
63
+ <summary>From a clone</summary>
64
+
65
+ ```bash
66
+ git clone https://github.com/JonathanRReed/Apple-MCPs.git
67
+ cd Apple-MCPs
68
+ uv sync --all-packages
69
+ ```
70
+
71
+ This builds one workspace environment with every server's entry point in `.venv/bin` (for example `.venv/bin/apple-maps-mcp`). You can also point an MCP client at `AppleMaps-MCP/start.sh`, which prefers `uv run` and falls back to a plain venv bootstrap (Python 3.11+ required).
72
+
73
+ </details>
74
+
75
+ ## Install In AI Agents
76
+
77
+ ```json
78
+ {
79
+ "mcpServers": {
80
+ "apple-maps": {
81
+ "command": "uvx",
82
+ "args": ["apple-maps-mcp"],
83
+ "env": {}
84
+ }
85
+ }
86
+ }
87
+ ```
88
+
89
+ Running from a clone instead? Use `/path/to/Apple-MCPs/AppleMaps-MCP/start.sh` as the command with empty `args`.
90
+
91
+ Claude Code:
92
+
93
+ ```bash
94
+ claude mcp add --transport stdio --scope project apple-maps -- uvx apple-maps-mcp
95
+ ```
96
+
97
+ ## Transport
98
+
99
+ `stdio` is the default and recommended transport. Set `APPLE_MAPS_MCP_TRANSPORT=streamable-http` (with optional `APPLE_MAPS_MCP_HOST` and `APPLE_MAPS_MCP_PORT`) to serve Streamable HTTP instead.
100
+
101
+ ## Prompting Notes
102
+
103
+ - `tools/list` returns the full Maps tool surface. Context-constrained clients can use `search_tools` first, then `get_tool_info` for the Maps tool they need.
104
+ - Use this server when travel, routing, or place lookup affects a Calendar, Reminders, Messages, or Mail action.
105
+ - Confirm origin, destination, and transport mode before writing a time-sensitive plan.
106
+ - If helper compilation fails, install Xcode command line tools and retry.
107
+
108
+ ## Health And Recovery
109
+
110
+ - `maps_health`
111
+ - `maps_permission_guide`
112
+
113
+ ## Launch Checklist
114
+
115
+ - Add `uvx apple-maps-mcp` (or a clone's `AppleMaps-MCP/start.sh`) to your MCP client
116
+ - Reload or reconnect the client so the Maps tool surface is loaded into context
117
+ - Call `maps_health` first
118
+ - If the local helper or routing surface is blocked, call `maps_permission_guide`
119
+ - Run `maps_search_places` once to confirm the local Swift helper compiles
@@ -0,0 +1,18 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ src/apple_maps_mcp/__init__.py
5
+ src/apple_maps_mcp/apple_maps_bridge.swift
6
+ src/apple_maps_mcp/config.py
7
+ src/apple_maps_mcp/maps_bridge.py
8
+ src/apple_maps_mcp/models.py
9
+ src/apple_maps_mcp/tools.py
10
+ src/apple_maps_mcp.egg-info/PKG-INFO
11
+ src/apple_maps_mcp.egg-info/SOURCES.txt
12
+ src/apple_maps_mcp.egg-info/dependency_links.txt
13
+ src/apple_maps_mcp.egg-info/entry_points.txt
14
+ src/apple_maps_mcp.egg-info/requires.txt
15
+ src/apple_maps_mcp.egg-info/top_level.txt
16
+ tests/test_bridge.py
17
+ tests/test_config.py
18
+ tests/test_tools.py
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ apple-maps-mcp = apple_maps_mcp.tools:main
@@ -0,0 +1,7 @@
1
+ apple-mcp-common<2,>=1.0.0
2
+ mcp<3,>=2.0.0
3
+ pydantic>=2.12.0
4
+
5
+ [dev]
6
+ pytest<10,>=8
7
+ ruff<1,>=0.12
@@ -0,0 +1 @@
1
+ apple_maps_mcp
@@ -0,0 +1,22 @@
1
+ import subprocess
2
+ from pathlib import Path
3
+
4
+ import pytest
5
+
6
+ from apple_maps_mcp.maps_bridge import AppleMapsBridge, MapsBridgeError
7
+
8
+
9
+ def test_run_helper_maps_timeout_to_structured_error(monkeypatch) -> None:
10
+ bridge = AppleMapsBridge(Path("/tmp/apple_maps_bridge.swift"), Path("/tmp/apple-maps-bridge"))
11
+
12
+ monkeypatch.setattr(bridge, "_ensure_helper", lambda: None)
13
+
14
+ def fake_run(*args, **kwargs):
15
+ raise subprocess.TimeoutExpired(cmd="apple-maps-bridge", timeout=20)
16
+
17
+ monkeypatch.setattr(subprocess, "run", fake_run)
18
+
19
+ with pytest.raises(MapsBridgeError) as exc_info:
20
+ bridge.search_places("coffee")
21
+
22
+ assert exc_info.value.error_code == "HELPER_TIMEOUT"
@@ -0,0 +1,15 @@
1
+ from apple_maps_mcp.config import load_settings
2
+
3
+
4
+ def test_load_settings_uses_packaged_helper_source() -> None:
5
+ load_settings.cache_clear()
6
+
7
+ try:
8
+ settings = load_settings()
9
+ finally:
10
+ load_settings.cache_clear()
11
+
12
+ assert settings.helper_source.name == "apple_maps_bridge.swift"
13
+ assert settings.helper_source.parent.name == "apple_maps_mcp"
14
+ assert settings.helper_source.exists()
15
+ assert settings.helper_binary.name == "apple-maps-bridge"
@@ -0,0 +1,75 @@
1
+ from apple_maps_mcp import tools
2
+
3
+
4
+ class StubBridge:
5
+ def helper_available(self):
6
+ return True, True
7
+
8
+ def search_places(self, query: str, limit: int = 5):
9
+ return {"places": [{"name": "Coffee Shop", "address": "123 Main St", "latitude": 30.0, "longitude": -97.0, "phone": None, "url": None}]}
10
+
11
+ def directions(self, origin: str, destination: str, transport: str = "driving"):
12
+ return {
13
+ "origin": {"name": origin, "address": origin, "latitude": 30.0, "longitude": -97.0, "phone": None, "url": None},
14
+ "destination": {"name": destination, "address": destination, "latitude": 30.1, "longitude": -97.1, "phone": None, "url": None},
15
+ "transport": transport,
16
+ "distance_meters": 1000,
17
+ "expected_travel_time_seconds": 600,
18
+ "advisory_notices": [],
19
+ "maps_url": "https://maps.apple.com/?daddr=Coffee+Shop",
20
+ }
21
+
22
+ def maps_url(self, destination: str, origin: str | None = None, transport: str = "driving"):
23
+ return "https://maps.apple.com/?daddr=Coffee+Shop"
24
+
25
+
26
+ def test_maps_search_places(monkeypatch):
27
+ monkeypatch.setattr(tools, "_bridge", lambda: StubBridge())
28
+ result = tools.maps_search_places("coffee")
29
+ assert result.ok is True
30
+ assert result.count == 1
31
+ assert result.places[0].name == "Coffee Shop"
32
+
33
+
34
+ def test_maps_build_maps_link(monkeypatch):
35
+ monkeypatch.setattr(tools, "_bridge", lambda: StubBridge())
36
+ result = tools.maps_build_maps_link("Coffee Shop")
37
+ assert result.url.startswith("https://maps.apple.com/")
38
+
39
+
40
+ def test_maps_status_resource(monkeypatch):
41
+ monkeypatch.setattr(tools, "_bridge", lambda: StubBridge())
42
+ payload = tools.maps_status_resource()
43
+ assert "\"helper_available\": true" in payload
44
+ assert "\"driving\"" in payload
45
+
46
+
47
+ def test_main_uses_streamable_http(monkeypatch):
48
+ monkeypatch.setenv("APPLE_MAPS_MCP_TRANSPORT", "streamable-http")
49
+ monkeypatch.setenv("APPLE_MAPS_MCP_HOST", "0.0.0.0")
50
+ monkeypatch.setenv("APPLE_MAPS_MCP_PORT", "8765")
51
+ monkeypatch.setenv("APPLE_MAPS_MCP_LOG_LEVEL", "DEBUG")
52
+ tools.load_settings.cache_clear()
53
+
54
+ captured = {}
55
+
56
+ def fake_run(*, transport: str, host: str, port: int, json_response: bool, stateless_http: bool):
57
+ captured["transport"] = transport
58
+ captured["host"] = host
59
+ captured["port"] = port
60
+ captured["json_response"] = json_response
61
+ captured["stateless_http"] = stateless_http
62
+ captured["log_level"] = tools.mcp.settings.log_level
63
+
64
+ monkeypatch.setattr(tools.mcp, "run", fake_run)
65
+
66
+ tools.main()
67
+
68
+ assert captured == {
69
+ "transport": "streamable-http",
70
+ "host": "0.0.0.0",
71
+ "port": 8765,
72
+ "json_response": True,
73
+ "stateless_http": True,
74
+ "log_level": "DEBUG",
75
+ }