android-mock-location-mcp 0.1.0

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.
package/README.md ADDED
@@ -0,0 +1,210 @@
1
+ # MCP Server — android-mock-location-mcp
2
+
3
+ MCP server that exposes 8 tools for controlling Android device GPS location. Connects to an Android agent app over TCP (via ADB port forwarding) and supports geocoding and street-level routing through configurable providers.
4
+
5
+ See the [root README](../README.md) for project overview and quick start.
6
+
7
+ ## Installation
8
+
9
+ **npx (no install):**
10
+ ```bash
11
+ npx android-mock-location-mcp
12
+ ```
13
+
14
+ **Global install:**
15
+ ```bash
16
+ npm install -g android-mock-location-mcp
17
+ android-mock-location-mcp
18
+ ```
19
+
20
+ **Build from source:**
21
+ ```bash
22
+ cd server
23
+ npm install
24
+ npm run build
25
+ npm start
26
+ ```
27
+
28
+ ## Configuration
29
+
30
+ ### Environment Variables
31
+
32
+ | Variable | Description | Required |
33
+ |----------|-------------|----------|
34
+ | `PROVIDER` | Provider for geocoding + routing: `osm` (default), `google`, `mapbox` | No (defaults to `osm`) |
35
+ | `GOOGLE_API_KEY` | Google Geocoding + Routes API key | When `PROVIDER=google` |
36
+ | `MAPBOX_ACCESS_TOKEN` | Mapbox Geocoding + Directions access token | When `PROVIDER=mapbox` |
37
+
38
+ Set environment variables in your MCP client configuration. See [root README](../README.md#provider-configuration) for client config examples.
39
+
40
+ ### Providers
41
+
42
+ | `PROVIDER` | Geocoding Service | Routing Service | Profiles Supported | API Key | Cost |
43
+ |------------|-------------------|-----------------|-------------------|---------|------|
44
+ | `osm` (default) | Nominatim (OpenStreetMap) | OSRM | `car` only* | None | Free (rate-limited) |
45
+ | `google` | Google Geocoding API | Google Routes API | `car`, `foot`, `bike` | `GOOGLE_API_KEY` | Paid (free tier) |
46
+ | `mapbox` | Mapbox Geocoding | Mapbox Directions | `car`, `foot`, `bike` | `MAPBOX_ACCESS_TOKEN` | Paid (free tier) |
47
+
48
+ **\*OSRM limitation:** The public OSRM demo server (`router.project-osrm.org`) only supports the `car` profile. Requesting `foot` or `bike` silently returns a driving route. For walking/cycling routing, use `google` or `mapbox`.
49
+
50
+ **Nominatim rate limit:** The OSM Nominatim API is rate-limited to 1 request per second. When using the `osm` provider, the server hints the AI to resolve place names to coordinates itself and pass `lat`/`lng` directly.
51
+
52
+ ## Tool Reference
53
+
54
+ ### `geo_list_devices`
55
+
56
+ List connected Android devices via ADB.
57
+
58
+ No parameters.
59
+
60
+ ---
61
+
62
+ ### `geo_connect_device`
63
+
64
+ Connect to an Android device for mock location control. Sets up ADB port forwarding and opens a TCP socket.
65
+
66
+ | Parameter | Type | Required | Description |
67
+ |-----------|------|----------|-------------|
68
+ | `deviceId` | string | yes | Device serial from `geo_list_devices`, e.g. `emulator-5554` |
69
+
70
+ ---
71
+
72
+ ### `geo_set_location`
73
+
74
+ Set device GPS to coordinates or any place name/address. Geocodes place names via the configured provider.
75
+
76
+ | Parameter | Type | Required | Default | Description |
77
+ |-----------|------|----------|---------|-------------|
78
+ | `lat` | number | no | — | Latitude (-90 to 90) |
79
+ | `lng` | number | no | — | Longitude (-180 to 180) |
80
+ | `place` | string | no | — | Place name or address, e.g. `'Times Square'`, `'Tokyo Station'` |
81
+ | `accuracy` | number | no | `3` | GPS accuracy in meters |
82
+
83
+ Provide either `place` or both `lat`/`lng`.
84
+
85
+ ---
86
+
87
+ ### `geo_simulate_route`
88
+
89
+ Simulate movement along a route between two points at a given speed. Routes follow real streets via the configured routing provider. Falls back to straight-line if the provider fails.
90
+
91
+ | Parameter | Type | Required | Default | Description |
92
+ |-----------|------|----------|---------|-------------|
93
+ | `from` | string | no | — | Starting place name or address |
94
+ | `to` | string | no | — | Destination place name or address |
95
+ | `fromLat` | number | no | — | Starting latitude |
96
+ | `fromLng` | number | no | — | Starting longitude |
97
+ | `toLat` | number | no | — | Destination latitude |
98
+ | `toLng` | number | no | — | Destination longitude |
99
+ | `speedKmh` | number | no | `60` | Speed in km/h |
100
+ | `trafficMultiplier` | number | no | `1.0` | Traffic slowdown factor (e.g. `1.5` = 50% slower) |
101
+ | `profile` | enum | no | `car` | Routing profile: `car`, `foot`, or `bike` |
102
+
103
+ Provide either `from`/`to` (place names) or `fromLat`/`fromLng`/`toLat`/`toLng` (coordinates) for each endpoint.
104
+
105
+ #### Routing Profiles
106
+
107
+ | Profile | Use for | Routes on |
108
+ |---------|---------|-----------|
109
+ | `car` (default) | Driving simulation | Roads, highways |
110
+ | `foot` | Walking simulation | Sidewalks, pedestrian paths |
111
+ | `bike` | Cycling simulation | Bike lanes, roads |
112
+
113
+ The AI should select `profile` based on user intent (e.g. "walk to" → `foot`, "drive to" → `car`).
114
+
115
+ ---
116
+
117
+ ### `geo_simulate_jitter`
118
+
119
+ Simulate GPS noise/jitter at a location for testing accuracy handling.
120
+
121
+ | Parameter | Type | Required | Default | Description |
122
+ |-----------|------|----------|---------|-------------|
123
+ | `lat` | number | no | — | Center latitude |
124
+ | `lng` | number | no | — | Center longitude |
125
+ | `place` | string | no | — | Center place name or address |
126
+ | `radiusMeters` | number | no | `10` | Jitter radius in meters |
127
+ | `pattern` | enum | no | `random` | Jitter pattern: `random`, `drift`, `urban_canyon` |
128
+ | `durationSeconds` | number | no | `30` | Duration in seconds |
129
+
130
+ Provide either `place` or both `lat`/`lng`.
131
+
132
+ #### Jitter Patterns
133
+
134
+ | Pattern | Behavior |
135
+ |---------|----------|
136
+ | `random` | Uniform random distribution within radius |
137
+ | `drift` | Gradual movement in one direction |
138
+ | `urban_canyon` | Alternating accurate (3m) and inaccurate (50-80m) fixes, simulating tall buildings |
139
+
140
+ ---
141
+
142
+ ### `geo_test_geofence`
143
+
144
+ Test geofence enter/exit/bounce behavior at a location.
145
+
146
+ | Parameter | Type | Required | Default | Description |
147
+ |-----------|------|----------|---------|-------------|
148
+ | `lat` | number | no | — | Geofence center latitude |
149
+ | `lng` | number | no | — | Geofence center longitude |
150
+ | `place` | string | no | — | Geofence center place name or address |
151
+ | `radiusMeters` | number | no | `100` | Geofence radius in meters |
152
+ | `action` | enum | no | `enter` | Geofence action: `enter`, `exit`, `bounce` |
153
+ | `bounceCount` | number | no | `3` | Number of boundary crossings (for `bounce` action) |
154
+
155
+ Provide either `place` or both `lat`/`lng`.
156
+
157
+ #### Geofence Actions
158
+
159
+ | Action | Behavior |
160
+ |--------|----------|
161
+ | `enter` | Move from outside to inside the geofence |
162
+ | `exit` | Move from inside to outside the geofence |
163
+ | `bounce` | Cross the boundary `bounceCount` times |
164
+
165
+ ---
166
+
167
+ ### `geo_stop`
168
+
169
+ Stop any active location simulation.
170
+
171
+ No parameters.
172
+
173
+ ---
174
+
175
+ ### `geo_get_status`
176
+
177
+ Get current connection and simulation status.
178
+
179
+ No parameters.
180
+
181
+ ## Source Structure
182
+
183
+ | File | Purpose |
184
+ |------|---------|
185
+ | `src/index.ts` | MCP server setup, all 8 tool definitions with Zod schemas |
186
+ | `src/device.ts` | ADB commands (`execFileSync`), TCP socket to agent, request/response matching |
187
+ | `src/geocode.ts` | Geocoding providers: Nominatim, Google, Mapbox |
188
+ | `src/routing.ts` | Routing providers: OSRM, Google Routes API, Mapbox Directions |
189
+ | `src/geo-math.ts` | Haversine distance, forward bearing calculation |
190
+ | `src/fetch-utils.ts` | Shared `fetchWithTimeout` helper |
191
+
192
+ ## Development
193
+
194
+ ```bash
195
+ npm install # Install dependencies
196
+ npm run build # Compile TypeScript
197
+ npm run dev # Watch mode (recompile on change)
198
+ npm start # Run the server
199
+ ```
200
+
201
+ The server communicates via stdio (MCP protocol). To test interactively, use the [MCP Inspector](https://modelcontextprotocol.io/docs/tools/inspector).
202
+
203
+ ## Adding a New Provider
204
+
205
+ Both `src/geocode.ts` and `src/routing.ts` use the same pattern:
206
+
207
+ 1. Implement the `GeocodeProvider` type (in `geocode.ts`) and/or `RoutingProvider` type (in `routing.ts`)
208
+ 2. Add a case to `selectProvider()` in the respective file
209
+ 3. Validate required env vars in the `selectProvider()` switch case
210
+ 4. Document the new env var in this README and in `CLAUDE.md`
@@ -0,0 +1,10 @@
1
+ /** Register a callback invoked when the device socket disconnects. */
2
+ export declare function onDisconnect(cb: () => void): void;
3
+ export declare function getConnectedDeviceId(): string | null;
4
+ export declare function isConnected(): boolean;
5
+ /** List connected ADB devices. Returns raw `adb devices -l` output. */
6
+ export declare function listDevices(): string;
7
+ /** Send a JSON command to the agent and await the response (5s timeout). */
8
+ export declare function sendCommand(command: Record<string, unknown>): Promise<unknown>;
9
+ /** Set up ADB port forwarding and open a TCP socket to the agent. */
10
+ export declare function connectToDevice(deviceId: string): void;
package/dist/device.js ADDED
@@ -0,0 +1,154 @@
1
+ // ── ADB + socket communication with Android agent ───────────────────────────
2
+ import * as net from "node:net";
3
+ import { execFileSync } from "node:child_process";
4
+ import { randomUUID } from "node:crypto";
5
+ // ── State ────────────────────────────────────────────────────────────────────
6
+ let socket = null;
7
+ let connectedDeviceId = null;
8
+ let socketBuffer = "";
9
+ let autoReconnectAttempted = false;
10
+ let reconnectTimer = null;
11
+ const pendingRequests = new Map();
12
+ let disconnectCallback = null;
13
+ // ── Public API ───────────────────────────────────────────────────────────────
14
+ /** Register a callback invoked when the device socket disconnects. */
15
+ export function onDisconnect(cb) {
16
+ disconnectCallback = cb;
17
+ }
18
+ export function getConnectedDeviceId() {
19
+ return connectedDeviceId;
20
+ }
21
+ export function isConnected() {
22
+ return socket !== null && !socket.destroyed;
23
+ }
24
+ /** List connected ADB devices. Returns raw `adb devices -l` output. */
25
+ export function listDevices() {
26
+ return execFileSync("adb", ["devices", "-l"], { encoding: "utf-8" });
27
+ }
28
+ /** Send a JSON command to the agent and await the response (5s timeout). */
29
+ export function sendCommand(command) {
30
+ const sock = socket;
31
+ if (!sock || sock.destroyed) {
32
+ return Promise.reject(new Error("Not connected to device. Troubleshooting: " +
33
+ "(1) Call geo_connect_device with the device serial from geo_list_devices. " +
34
+ "(2) Verify the GeoMCP Agent service is running in the app (green indicator). " +
35
+ "(3) Check adb port forwarding: run `adb forward tcp:5005 tcp:5005`."));
36
+ }
37
+ const id = randomUUID();
38
+ const msg = { ...command, id };
39
+ return new Promise((resolve, reject) => {
40
+ const timer = setTimeout(() => {
41
+ pendingRequests.delete(id);
42
+ reject(new Error("Command timed out (5s). Troubleshooting: " +
43
+ "(1) Verify the GeoMCP Agent service is running in the app (green indicator). " +
44
+ "(2) Restart adb port forwarding: `adb forward tcp:5005 tcp:5005`. " +
45
+ "(3) Check agent logs: `adb logcat -s GeoMCP`."));
46
+ }, 5000);
47
+ pendingRequests.set(id, { resolve, reject, timer });
48
+ try {
49
+ sock.write(JSON.stringify(msg) + "\n");
50
+ }
51
+ catch (err) {
52
+ clearTimeout(timer);
53
+ pendingRequests.delete(id);
54
+ reject(err instanceof Error ? err : new Error(String(err)));
55
+ }
56
+ });
57
+ }
58
+ /** Set up ADB port forwarding and open a TCP socket to the agent. */
59
+ export function connectToDevice(deviceId) {
60
+ // Cancel any pending auto-reconnect so it doesn't clobber this connection
61
+ if (reconnectTimer !== null) {
62
+ clearTimeout(reconnectTimer);
63
+ reconnectTimer = null;
64
+ }
65
+ // User-initiated connections reset the reconnect guard so the next
66
+ // unexpected disconnect is eligible for one automatic retry.
67
+ autoReconnectAttempted = false;
68
+ openSocket(deviceId);
69
+ }
70
+ /**
71
+ * Core connection logic shared by public connectToDevice and the internal
72
+ * auto-reconnect path. Does NOT touch autoReconnectAttempted — callers
73
+ * decide whether the guard should be reset.
74
+ */
75
+ function openSocket(deviceId) {
76
+ if (!/^[a-zA-Z0-9._:\-]+$/.test(deviceId)) {
77
+ throw new Error(`Invalid device ID: ${deviceId}`);
78
+ }
79
+ try {
80
+ execFileSync("adb", ["-s", deviceId, "forward", "tcp:5005", "tcp:5005"], { encoding: "utf-8" });
81
+ }
82
+ catch (err) {
83
+ throw new Error(`Failed to set up adb port forwarding for ${deviceId}: ${err.message}`);
84
+ }
85
+ if (socket && !socket.destroyed) {
86
+ socket.destroy();
87
+ }
88
+ const sock = new net.Socket();
89
+ socket = sock;
90
+ connectedDeviceId = deviceId;
91
+ setupSocketHandlers(sock);
92
+ sock.connect({ host: "127.0.0.1", port: 5005 });
93
+ }
94
+ // ── Internal ─────────────────────────────────────────────────────────────────
95
+ function setupSocketHandlers(sock) {
96
+ socketBuffer = "";
97
+ sock.on("data", (data) => {
98
+ socketBuffer += data.toString();
99
+ const lines = socketBuffer.split("\n");
100
+ socketBuffer = lines.pop(); // keep incomplete last chunk
101
+ for (const line of lines) {
102
+ if (!line.trim())
103
+ continue;
104
+ try {
105
+ const parsed = JSON.parse(line);
106
+ const id = parsed.id;
107
+ if (id && pendingRequests.has(id)) {
108
+ const entry = pendingRequests.get(id);
109
+ clearTimeout(entry.timer);
110
+ entry.resolve(parsed);
111
+ pendingRequests.delete(id);
112
+ }
113
+ }
114
+ catch {
115
+ // ignore unparseable lines
116
+ }
117
+ }
118
+ });
119
+ const cleanup = () => {
120
+ if (socket !== sock)
121
+ return; // only clean up if this socket is still current
122
+ const previousDeviceId = connectedDeviceId;
123
+ for (const [id, entry] of pendingRequests) {
124
+ clearTimeout(entry.timer);
125
+ entry.reject(new Error("Socket closed. The device may have disconnected. " +
126
+ "Auto-reconnect will be attempted once. If it fails, call geo_connect_device again."));
127
+ pendingRequests.delete(id);
128
+ }
129
+ socket = null;
130
+ connectedDeviceId = null;
131
+ disconnectCallback?.();
132
+ // Auto-reconnect once — avoid infinite retry loop on persistent failures.
133
+ // Uses openSocket (not connectToDevice) so autoReconnectAttempted stays
134
+ // true, preventing further retries if this attempt also fails.
135
+ if (previousDeviceId && !autoReconnectAttempted) {
136
+ autoReconnectAttempted = true;
137
+ reconnectTimer = setTimeout(() => {
138
+ reconnectTimer = null;
139
+ try {
140
+ openSocket(previousDeviceId);
141
+ }
142
+ catch (err) {
143
+ console.error(`Auto-reconnect failed for ${previousDeviceId}: ${err instanceof Error ? err.message : err}`);
144
+ }
145
+ }, 1000);
146
+ }
147
+ };
148
+ sock.on("error", (err) => {
149
+ console.error(`Socket error: ${err.message}`);
150
+ cleanup();
151
+ });
152
+ sock.on("close", cleanup);
153
+ }
154
+ //# sourceMappingURL=device.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"device.js","sourceRoot":"","sources":["../src/device.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Fetch with an AbortController-based timeout.
3
+ * Rejects with an AbortError if the request exceeds timeoutMs.
4
+ */
5
+ export declare function fetchWithTimeout(url: string, init: RequestInit, timeoutMs: number): Promise<Response>;
@@ -0,0 +1,16 @@
1
+ // ── Fetch utilities ──────────────────────────────────────────────────────────
2
+ /**
3
+ * Fetch with an AbortController-based timeout.
4
+ * Rejects with an AbortError if the request exceeds timeoutMs.
5
+ */
6
+ export async function fetchWithTimeout(url, init, timeoutMs) {
7
+ const controller = new AbortController();
8
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
9
+ try {
10
+ return await fetch(url, { ...init, signal: controller.signal });
11
+ }
12
+ finally {
13
+ clearTimeout(timer);
14
+ }
15
+ }
16
+ //# sourceMappingURL=fetch-utils.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fetch-utils.js","sourceRoot":"","sources":["../src/fetch-utils.ts"],"names":[],"mappings":"AAAA,gFAAgF;AAEhF;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,gBAAgB,CAAC,GAAW,EAAE,IAAiB,EAAE,SAAiB;IACtF,MAAM,UAAU,GAAG,IAAI,eAAe,EAAE,CAAC;IACzC,MAAM,KAAK,GAAG,UAAU,CAAC,GAAG,EAAE,CAAC,UAAU,CAAC,KAAK,EAAE,EAAE,SAAS,CAAC,CAAC;IAC9D,IAAI,CAAC;QACH,OAAO,MAAM,KAAK,CAAC,GAAG,EAAE,EAAE,GAAG,IAAI,EAAE,MAAM,EAAE,UAAU,CAAC,MAAM,EAAE,CAAC,CAAC;IAClE,CAAC;YAAS,CAAC;QACT,YAAY,CAAC,KAAK,CAAC,CAAC;IACtB,CAAC;AACH,CAAC"}
@@ -0,0 +1,4 @@
1
+ /** Haversine distance between two points in meters. */
2
+ export declare function haversineDistance(lat1: number, lng1: number, lat2: number, lng2: number): number;
3
+ /** Forward azimuth (bearing) in degrees 0-360 from point 1 to point 2. */
4
+ export declare function computeBearing(lat1: number, lng1: number, lat2: number, lng2: number): number;
@@ -0,0 +1,20 @@
1
+ // ── Geographic math utilities ────────────────────────────────────────────────
2
+ const R = 6_371_000; // Earth radius in meters
3
+ const toRad = (d) => (d * Math.PI) / 180;
4
+ const toDeg = (r) => (r * 180) / Math.PI;
5
+ /** Haversine distance between two points in meters. */
6
+ export function haversineDistance(lat1, lng1, lat2, lng2) {
7
+ const dLat = toRad(lat2 - lat1);
8
+ const dLng = toRad(lng2 - lng1);
9
+ const a = Math.sin(dLat / 2) ** 2 + Math.cos(toRad(lat1)) * Math.cos(toRad(lat2)) * Math.sin(dLng / 2) ** 2;
10
+ return R * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
11
+ }
12
+ /** Forward azimuth (bearing) in degrees 0-360 from point 1 to point 2. */
13
+ export function computeBearing(lat1, lng1, lat2, lng2) {
14
+ const dLng = toRad(lng2 - lng1);
15
+ const y = Math.sin(dLng) * Math.cos(toRad(lat2));
16
+ const x = Math.cos(toRad(lat1)) * Math.sin(toRad(lat2)) -
17
+ Math.sin(toRad(lat1)) * Math.cos(toRad(lat2)) * Math.cos(dLng);
18
+ return (toDeg(Math.atan2(y, x)) + 360) % 360;
19
+ }
20
+ //# sourceMappingURL=geo-math.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"geo-math.js","sourceRoot":"","sources":["../src/geo-math.ts"],"names":[],"mappings":"AAAA,gFAAgF;AAEhF,MAAM,CAAC,GAAG,SAAS,CAAC,CAAC,yBAAyB;AAC9C,MAAM,KAAK,GAAG,CAAC,CAAS,EAAE,EAAE,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC,EAAE,CAAC,GAAG,GAAG,CAAC;AACjD,MAAM,KAAK,GAAG,CAAC,CAAS,EAAE,EAAE,CAAC,CAAC,CAAC,GAAG,GAAG,CAAC,GAAG,IAAI,CAAC,EAAE,CAAC;AAEjD,uDAAuD;AACvD,MAAM,UAAU,iBAAiB,CAAC,IAAY,EAAE,IAAY,EAAE,IAAY,EAAE,IAAY;IACtF,MAAM,IAAI,GAAG,KAAK,CAAC,IAAI,GAAG,IAAI,CAAC,CAAC;IAChC,MAAM,IAAI,GAAG,KAAK,CAAC,IAAI,GAAG,IAAI,CAAC,CAAC;IAChC,MAAM,CAAC,GACL,IAAI,CAAC,GAAG,CAAC,IAAI,GAAG,CAAC,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC,IAAI,GAAG,CAAC,CAAC,IAAI,CAAC,CAAC;IACpG,OAAO,CAAC,GAAG,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,IAAI,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;AAC5D,CAAC;AAED,0EAA0E;AAC1E,MAAM,UAAU,cAAc,CAAC,IAAY,EAAE,IAAY,EAAE,IAAY,EAAE,IAAY;IACnF,MAAM,IAAI,GAAG,KAAK,CAAC,IAAI,GAAG,IAAI,CAAC,CAAC;IAChC,MAAM,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC;IACjD,MAAM,CAAC,GACL,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;QAC7C,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;IACjE,OAAO,CAAC,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,GAAG,CAAC,GAAG,GAAG,CAAC;AAC/C,CAAC"}
@@ -0,0 +1,7 @@
1
+ export interface GeocodeResult {
2
+ lat: number;
3
+ lng: number;
4
+ displayName: string;
5
+ }
6
+ export type GeocodeProvider = (place: string) => Promise<GeocodeResult | null>;
7
+ export declare function geocodePlace(place: string): Promise<GeocodeResult | null>;
@@ -0,0 +1,101 @@
1
+ // ── Geocoding ────────────────────────────────────────────────────────────────
2
+ //
3
+ // The active geocoding provider is selected via the PROVIDER environment variable:
4
+ //
5
+ // All providers use a 10-second fetch timeout to prevent indefinite hangs.
6
+ //
7
+ // PROVIDER=osm → Nominatim (default, free, no API key)
8
+ // PROVIDER=google → Google Geocoding API (requires GOOGLE_API_KEY)
9
+ // PROVIDER=mapbox → Mapbox Geocoding (requires MAPBOX_ACCESS_TOKEN)
10
+ //
11
+ // To add a new provider, implement the GeocodeProvider signature and add a
12
+ // case to selectProvider().
13
+ import { fetchWithTimeout } from "./fetch-utils.js";
14
+ // ── Nominatim (OpenStreetMap) ────────────────────────────────────────────────
15
+ const nominatimGeocode = async (place) => {
16
+ const url = `https://nominatim.openstreetmap.org/search?${new URLSearchParams({
17
+ q: place,
18
+ format: "json",
19
+ limit: "1",
20
+ })}`;
21
+ const res = await fetchWithTimeout(url, {
22
+ headers: { "User-Agent": "android-mock-location-mcp/0.1.0" },
23
+ }, 10_000);
24
+ if (!res.ok)
25
+ return null;
26
+ const data = (await res.json());
27
+ if (data.length === 0)
28
+ return null;
29
+ return { lat: parseFloat(data[0].lat), lng: parseFloat(data[0].lon), displayName: data[0].display_name };
30
+ };
31
+ // ── Google Geocoding API ─────────────────────────────────────────────────────
32
+ const googleGeocode = async (place) => {
33
+ const apiKey = process.env.GOOGLE_API_KEY; // Validated by selectProvider()
34
+ const url = `https://maps.googleapis.com/maps/api/geocode/json?${new URLSearchParams({
35
+ address: place,
36
+ key: apiKey,
37
+ })}`;
38
+ const res = await fetchWithTimeout(url, {}, 10_000);
39
+ if (!res.ok)
40
+ return null;
41
+ const data = (await res.json());
42
+ if (data.status !== "OK" || !data.results?.length)
43
+ return null;
44
+ const result = data.results[0];
45
+ return {
46
+ lat: result.geometry.location.lat,
47
+ lng: result.geometry.location.lng,
48
+ displayName: result.formatted_address,
49
+ };
50
+ };
51
+ // ── Mapbox Geocoding API ─────────────────────────────────────────────────────
52
+ const mapboxGeocode = async (place) => {
53
+ const token = process.env.MAPBOX_ACCESS_TOKEN; // Validated by selectProvider()
54
+ const url = `https://api.mapbox.com/geocoding/v5/mapbox.places/${encodeURIComponent(place)}.json?` +
55
+ new URLSearchParams({ access_token: token, limit: "1" });
56
+ const res = await fetchWithTimeout(url, {
57
+ headers: { "User-Agent": "android-mock-location-mcp/0.1.0" },
58
+ }, 10_000);
59
+ if (!res.ok)
60
+ return null;
61
+ const data = (await res.json());
62
+ if (!data.features?.length)
63
+ return null;
64
+ const feature = data.features[0];
65
+ return {
66
+ lat: feature.center[1],
67
+ lng: feature.center[0],
68
+ displayName: feature.place_name,
69
+ };
70
+ };
71
+ // ── Provider Selection ───────────────────────────────────────────────────────
72
+ // To switch providers, set PROVIDER env var to "osm", "google", or "mapbox".
73
+ function selectProvider() {
74
+ const name = (process.env.PROVIDER ?? "osm").toLowerCase();
75
+ switch (name) {
76
+ case "google": {
77
+ if (!process.env.GOOGLE_API_KEY) {
78
+ throw new Error("PROVIDER=google requires GOOGLE_API_KEY environment variable");
79
+ }
80
+ return googleGeocode;
81
+ }
82
+ case "mapbox": {
83
+ if (!process.env.MAPBOX_ACCESS_TOKEN) {
84
+ throw new Error("PROVIDER=mapbox requires MAPBOX_ACCESS_TOKEN environment variable");
85
+ }
86
+ return mapboxGeocode;
87
+ }
88
+ default:
89
+ return nominatimGeocode;
90
+ }
91
+ }
92
+ const activeProvider = selectProvider();
93
+ export async function geocodePlace(place) {
94
+ try {
95
+ return await activeProvider(place);
96
+ }
97
+ catch {
98
+ return null;
99
+ }
100
+ }
101
+ //# sourceMappingURL=geocode.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"geocode.js","sourceRoot":"","sources":["../src/geocode.ts"],"names":[],"mappings":"AAAA,gFAAgF;AAChF,EAAE;AACF,mFAAmF;AACnF,EAAE;AACF,2EAA2E;AAC3E,EAAE;AACF,8DAA8D;AAC9D,uEAAuE;AACvE,wEAAwE;AACxE,EAAE;AACF,2EAA2E;AAC3E,4BAA4B;AAE5B,OAAO,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AAUpD,gFAAgF;AAEhF,MAAM,gBAAgB,GAAoB,KAAK,EAAE,KAAK,EAAE,EAAE;IACxD,MAAM,GAAG,GAAG,8CAA8C,IAAI,eAAe,CAAC;QAC5E,CAAC,EAAE,KAAK;QACR,MAAM,EAAE,MAAM;QACd,KAAK,EAAE,GAAG;KACX,CAAC,EAAE,CAAC;IACL,MAAM,GAAG,GAAG,MAAM,gBAAgB,CAAC,GAAG,EAAE;QACtC,OAAO,EAAE,EAAE,YAAY,EAAE,iCAAiC,EAAE;KAC7D,EAAE,MAAM,CAAC,CAAC;IACX,IAAI,CAAC,GAAG,CAAC,EAAE;QAAE,OAAO,IAAI,CAAC;IACzB,MAAM,IAAI,GAAG,CAAC,MAAM,GAAG,CAAC,IAAI,EAAE,CAA8D,CAAC;IAC7F,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,IAAI,CAAC;IACnC,OAAO,EAAE,GAAG,EAAE,UAAU,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,EAAE,GAAG,EAAE,UAAU,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,EAAE,WAAW,EAAE,IAAI,CAAC,CAAC,CAAC,CAAC,YAAY,EAAE,CAAC;AAC3G,CAAC,CAAC;AAEF,gFAAgF;AAEhF,MAAM,aAAa,GAAoB,KAAK,EAAE,KAAK,EAAE,EAAE;IACrD,MAAM,MAAM,GAAG,OAAO,CAAC,GAAG,CAAC,cAAe,CAAC,CAAC,gCAAgC;IAE5E,MAAM,GAAG,GAAG,qDAAqD,IAAI,eAAe,CAAC;QACnF,OAAO,EAAE,KAAK;QACd,GAAG,EAAE,MAAM;KACZ,CAAC,EAAE,CAAC;IACL,MAAM,GAAG,GAAG,MAAM,gBAAgB,CAAC,GAAG,EAAE,EAAE,EAAE,MAAM,CAAC,CAAC;IACpD,IAAI,CAAC,GAAG,CAAC,EAAE;QAAE,OAAO,IAAI,CAAC;IAEzB,MAAM,IAAI,GAAG,CAAC,MAAM,GAAG,CAAC,IAAI,EAAE,CAM7B,CAAC;IAEF,IAAI,IAAI,CAAC,MAAM,KAAK,IAAI,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,MAAM;QAAE,OAAO,IAAI,CAAC;IAC/D,MAAM,MAAM,GAAG,IAAI,CAAC,OAAO,CAAC,CAAC,CAAE,CAAC;IAChC,OAAO;QACL,GAAG,EAAE,MAAM,CAAC,QAAQ,CAAC,QAAQ,CAAC,GAAG;QACjC,GAAG,EAAE,MAAM,CAAC,QAAQ,CAAC,QAAQ,CAAC,GAAG;QACjC,WAAW,EAAE,MAAM,CAAC,iBAAiB;KACtC,CAAC;AACJ,CAAC,CAAC;AAEF,gFAAgF;AAEhF,MAAM,aAAa,GAAoB,KAAK,EAAE,KAAK,EAAE,EAAE;IACrD,MAAM,KAAK,GAAG,OAAO,CAAC,GAAG,CAAC,mBAAoB,CAAC,CAAC,gCAAgC;IAEhF,MAAM,GAAG,GACP,qDAAqD,kBAAkB,CAAC,KAAK,CAAC,QAAQ;QACtF,IAAI,eAAe,CAAC,EAAE,YAAY,EAAE,KAAK,EAAE,KAAK,EAAE,GAAG,EAAE,CAAC,CAAC;IAC3D,MAAM,GAAG,GAAG,MAAM,gBAAgB,CAAC,GAAG,EAAE;QACtC,OAAO,EAAE,EAAE,YAAY,EAAE,iCAAiC,EAAE;KAC7D,EAAE,MAAM,CAAC,CAAC;IACX,IAAI,CAAC,GAAG,CAAC,EAAE;QAAE,OAAO,IAAI,CAAC;IAEzB,MAAM,IAAI,GAAG,CAAC,MAAM,GAAG,CAAC,IAAI,EAAE,CAK7B,CAAC;IAEF,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,MAAM;QAAE,OAAO,IAAI,CAAC;IACxC,MAAM,OAAO,GAAG,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAE,CAAC;IAClC,OAAO;QACL,GAAG,EAAE,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC;QACtB,GAAG,EAAE,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC;QACtB,WAAW,EAAE,OAAO,CAAC,UAAU;KAChC,CAAC;AACJ,CAAC,CAAC;AAEF,gFAAgF;AAChF,6EAA6E;AAE7E,SAAS,cAAc;IACrB,MAAM,IAAI,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,QAAQ,IAAI,KAAK,CAAC,CAAC,WAAW,EAAE,CAAC;IAC3D,QAAQ,IAAI,EAAE,CAAC;QACb,KAAK,QAAQ,CAAC,CAAC,CAAC;YACd,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,cAAc,EAAE,CAAC;gBAChC,MAAM,IAAI,KAAK,CAAC,8DAA8D,CAAC,CAAC;YAClF,CAAC;YACD,OAAO,aAAa,CAAC;QACvB,CAAC;QACD,KAAK,QAAQ,CAAC,CAAC,CAAC;YACd,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,mBAAmB,EAAE,CAAC;gBACrC,MAAM,IAAI,KAAK,CAAC,mEAAmE,CAAC,CAAC;YACvF,CAAC;YACD,OAAO,aAAa,CAAC;QACvB,CAAC;QACD;YACE,OAAO,gBAAgB,CAAC;IAC5B,CAAC;AACH,CAAC;AAED,MAAM,cAAc,GAAoB,cAAc,EAAE,CAAC;AAEzD,MAAM,CAAC,KAAK,UAAU,YAAY,CAAC,KAAa;IAC9C,IAAI,CAAC;QACH,OAAO,MAAM,cAAc,CAAC,KAAK,CAAC,CAAC;IACrC,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC"}
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};