map-mcp 0.1.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,27 @@
1
+ name: release
2
+
3
+ # Tag a release (e.g. `git tag v0.1.0 && git push --tags`) to build and publish to PyPI.
4
+ # Publishing uses PyPI Trusted Publishing (OIDC) — no API token stored in the repo.
5
+ # One-time setup on pypi.org: add a Trusted Publisher for project `map-mcp` with
6
+ # owner `dkedar7`, repository `map-mcp`, workflow `release.yml`, environment `pypi`.
7
+
8
+ on:
9
+ push:
10
+ tags: ["v*"]
11
+
12
+ jobs:
13
+ release:
14
+ runs-on: ubuntu-latest
15
+ environment: pypi
16
+ permissions:
17
+ id-token: write # required for Trusted Publishing (OIDC)
18
+ steps:
19
+ - uses: actions/checkout@v4
20
+ - name: Install uv
21
+ uses: astral-sh/setup-uv@v5
22
+ - name: Run tests
23
+ run: uv run --with pytest pytest -q
24
+ - name: Build
25
+ run: uv build
26
+ - name: Publish to PyPI (Trusted Publishing)
27
+ run: uv publish
@@ -0,0 +1,22 @@
1
+ name: test
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+
8
+ jobs:
9
+ test:
10
+ runs-on: ubuntu-latest
11
+ strategy:
12
+ fail-fast: false
13
+ matrix:
14
+ python-version: ["3.10", "3.11", "3.12"]
15
+ steps:
16
+ - uses: actions/checkout@v4
17
+ - name: Install uv
18
+ uses: astral-sh/setup-uv@v5
19
+ with:
20
+ python-version: ${{ matrix.python-version }}
21
+ - name: Run tests
22
+ run: uv run --with pytest pytest -q
@@ -0,0 +1,9 @@
1
+ __pycache__/
2
+ *.pyc
3
+ .pytest_cache/
4
+ dist/
5
+ build/
6
+ *.egg-info/
7
+ .venv/
8
+ .uv/
9
+ uv.lock
map_mcp-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Kedar Dabhadkar
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.
map_mcp-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,132 @@
1
+ Metadata-Version: 2.4
2
+ Name: map-mcp
3
+ Version: 0.1.0
4
+ Summary: Drive and perceive an existing live MapLibre GL map from an AI agent (MCP) or a human CLI — no test code, no browser automation.
5
+ Project-URL: Homepage, https://github.com/dkedar7/map-mcp
6
+ Project-URL: Source, https://github.com/dkedar7/map-mcp
7
+ Project-URL: Bug Tracker, https://github.com/dkedar7/map-mcp/issues
8
+ Author: Kedar Dabhadkar
9
+ License: MIT License
10
+
11
+ Copyright (c) 2026 Kedar Dabhadkar
12
+
13
+ Permission is hereby granted, free of charge, to any person obtaining a copy
14
+ of this software and associated documentation files (the "Software"), to deal
15
+ in the Software without restriction, including without limitation the rights
16
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
17
+ copies of the Software, and to permit persons to whom the Software is
18
+ furnished to do so, subject to the following conditions:
19
+
20
+ The above copyright notice and this permission notice shall be included in all
21
+ copies or substantial portions of the Software.
22
+
23
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
24
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
25
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
26
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
27
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
28
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
29
+ SOFTWARE.
30
+ License-File: LICENSE
31
+ Keywords: agent,geospatial,llm,map,maplibre,mcp,model-context-protocol
32
+ Classifier: Development Status :: 3 - Alpha
33
+ Classifier: License :: OSI Approved :: MIT License
34
+ Classifier: Programming Language :: Python :: 3
35
+ Classifier: Programming Language :: Python :: 3.10
36
+ Classifier: Programming Language :: Python :: 3.11
37
+ Classifier: Programming Language :: Python :: 3.12
38
+ Classifier: Topic :: Scientific/Engineering :: GIS
39
+ Classifier: Topic :: Software Development :: Libraries
40
+ Requires-Python: >=3.10
41
+ Requires-Dist: fastmcp<4,>=3
42
+ Requires-Dist: websockets>=12
43
+ Provides-Extra: dev
44
+ Requires-Dist: pytest>=7; extra == 'dev'
45
+ Description-Content-Type: text/markdown
46
+
47
+ # map-mcp
48
+
49
+ Drive and perceive an **existing, live MapLibre GL map** from an AI agent (over MCP) or a human
50
+ CLI — query the rendered features, read the viewport, click and read popups, navigate, toggle
51
+ layers. The agent and the CLI act on the **same map a person is looking at**, with parity by
52
+ construction.
53
+
54
+ It does **not** generate maps. Other geo-MCP servers (gis-mcp, Mapbox, CARTO) create maps or
55
+ call GIS operations; map-mcp reaches into a map that's *already on screen*. Think of it as the
56
+ agent-native counterpart to [MapGrab](https://github.com/maxlapides/mapgrab): same live-map
57
+ access, but conversational over MCP instead of written as test code.
58
+
59
+ > **Status:** v1, MapLibre GL only. Cooperation-required (your app adds a one-line hook). A
60
+ > no-cooperation path and other map libraries are future work.
61
+
62
+ ## Install
63
+
64
+ ```
65
+ uvx map-mcp --help # or: pip install map-mcp
66
+ ```
67
+
68
+ ## Quickstart
69
+
70
+ 1. **Start the bridge + MCP server.** It prints a WebSocket URL and a per-session token.
71
+ ```
72
+ map-mcp serve
73
+ ```
74
+ 2. **Add the hook to your MapLibre page** (`map` is your existing `maplibregl.Map`):
75
+ ```html
76
+ <script src="map-mcp-hook.js"></script>
77
+ <script>
78
+ mapMcp.register(map, { url: "ws://127.0.0.1:8765", token: "PASTE_TOKEN" });
79
+ </script>
80
+ ```
81
+ 3. **Point your agent at the MCP server** (stdio by default; `--transport http` for HTTP/SSE).
82
+ Or drive it yourself from the terminal:
83
+ ```
84
+ map-mcp call get_viewport
85
+ map-mcp call query_rendered_features --params '{"point":[12.5,41.9]}'
86
+ ```
87
+
88
+ There's a runnable example in [`examples/sample_app/`](examples/sample_app/).
89
+
90
+ ## Tools (the operation surface)
91
+
92
+ The agent's MCP tools and the CLI's `call` operations are exactly the same set:
93
+
94
+ | Operation | What it does |
95
+ |---|---|
96
+ | `get_viewport` | center `[lng,lat]`, zoom, bearing, pitch, bounds |
97
+ | `query_rendered_features` | features currently rendered (optionally at a point or within a bbox) |
98
+ | `get_features_at` | features rendered at a `[lng,lat]` point |
99
+ | `click_at` | fire the map's click at a point (runs your popup handlers), return features + popup |
100
+ | `read_popup` | text of any open popup(s) |
101
+ | `set_view` | center+zoom (and bearing/pitch), or fit a bbox |
102
+ | `list_layers` | the style's layers + visibility |
103
+ | `set_layer_visibility` | show/hide a layer |
104
+ | `screenshot` | a PNG data URL of the current map* |
105
+
106
+ Perception returns **structured feature properties** (GeoJSON-shaped) — agents reason over
107
+ properties, not pixels. `screenshot` is optional.
108
+
109
+ \* needs the map created with `preserveDrawingBuffer: true` (see [`hook/snippet.md`](hook/snippet.md)).
110
+
111
+ ## How it works
112
+
113
+ The hook connects *out* to a loopback WebSocket the `map-mcp` process runs. The MCP tools and
114
+ the CLI are thin frontends over one shared core-operations layer, so any operation one can do,
115
+ the other can too.
116
+
117
+ **Security model (local-only).** The bridge binds `127.0.0.1` only, so nothing off your machine
118
+ can reach it. Browsers do *not* apply same-origin policy to WebSocket connections, so the
119
+ **per-session token is the security boundary**: only a page that presents it can drive your map.
120
+ Treat the token like a secret — the convenience `?token=` pattern in the example leaks it via
121
+ browser history and server logs, so for anything sensitive paste the token into the page rather
122
+ than the URL. A hardened Origin allowlist is future work.
123
+
124
+ ## Scope (v1)
125
+
126
+ - **In:** MapLibre GL; the operations above; stdio + HTTP/SSE; a human CLI with parity.
127
+ - **Out:** generating maps; a hosted service; non-map visualizations; other map libraries
128
+ (Leaflet/deck.gl) and a no-cooperation (Playwright) path are future work.
129
+
130
+ ## License
131
+
132
+ MIT
@@ -0,0 +1,86 @@
1
+ # map-mcp
2
+
3
+ Drive and perceive an **existing, live MapLibre GL map** from an AI agent (over MCP) or a human
4
+ CLI — query the rendered features, read the viewport, click and read popups, navigate, toggle
5
+ layers. The agent and the CLI act on the **same map a person is looking at**, with parity by
6
+ construction.
7
+
8
+ It does **not** generate maps. Other geo-MCP servers (gis-mcp, Mapbox, CARTO) create maps or
9
+ call GIS operations; map-mcp reaches into a map that's *already on screen*. Think of it as the
10
+ agent-native counterpart to [MapGrab](https://github.com/maxlapides/mapgrab): same live-map
11
+ access, but conversational over MCP instead of written as test code.
12
+
13
+ > **Status:** v1, MapLibre GL only. Cooperation-required (your app adds a one-line hook). A
14
+ > no-cooperation path and other map libraries are future work.
15
+
16
+ ## Install
17
+
18
+ ```
19
+ uvx map-mcp --help # or: pip install map-mcp
20
+ ```
21
+
22
+ ## Quickstart
23
+
24
+ 1. **Start the bridge + MCP server.** It prints a WebSocket URL and a per-session token.
25
+ ```
26
+ map-mcp serve
27
+ ```
28
+ 2. **Add the hook to your MapLibre page** (`map` is your existing `maplibregl.Map`):
29
+ ```html
30
+ <script src="map-mcp-hook.js"></script>
31
+ <script>
32
+ mapMcp.register(map, { url: "ws://127.0.0.1:8765", token: "PASTE_TOKEN" });
33
+ </script>
34
+ ```
35
+ 3. **Point your agent at the MCP server** (stdio by default; `--transport http` for HTTP/SSE).
36
+ Or drive it yourself from the terminal:
37
+ ```
38
+ map-mcp call get_viewport
39
+ map-mcp call query_rendered_features --params '{"point":[12.5,41.9]}'
40
+ ```
41
+
42
+ There's a runnable example in [`examples/sample_app/`](examples/sample_app/).
43
+
44
+ ## Tools (the operation surface)
45
+
46
+ The agent's MCP tools and the CLI's `call` operations are exactly the same set:
47
+
48
+ | Operation | What it does |
49
+ |---|---|
50
+ | `get_viewport` | center `[lng,lat]`, zoom, bearing, pitch, bounds |
51
+ | `query_rendered_features` | features currently rendered (optionally at a point or within a bbox) |
52
+ | `get_features_at` | features rendered at a `[lng,lat]` point |
53
+ | `click_at` | fire the map's click at a point (runs your popup handlers), return features + popup |
54
+ | `read_popup` | text of any open popup(s) |
55
+ | `set_view` | center+zoom (and bearing/pitch), or fit a bbox |
56
+ | `list_layers` | the style's layers + visibility |
57
+ | `set_layer_visibility` | show/hide a layer |
58
+ | `screenshot` | a PNG data URL of the current map* |
59
+
60
+ Perception returns **structured feature properties** (GeoJSON-shaped) — agents reason over
61
+ properties, not pixels. `screenshot` is optional.
62
+
63
+ \* needs the map created with `preserveDrawingBuffer: true` (see [`hook/snippet.md`](hook/snippet.md)).
64
+
65
+ ## How it works
66
+
67
+ The hook connects *out* to a loopback WebSocket the `map-mcp` process runs. The MCP tools and
68
+ the CLI are thin frontends over one shared core-operations layer, so any operation one can do,
69
+ the other can too.
70
+
71
+ **Security model (local-only).** The bridge binds `127.0.0.1` only, so nothing off your machine
72
+ can reach it. Browsers do *not* apply same-origin policy to WebSocket connections, so the
73
+ **per-session token is the security boundary**: only a page that presents it can drive your map.
74
+ Treat the token like a secret — the convenience `?token=` pattern in the example leaks it via
75
+ browser history and server logs, so for anything sensitive paste the token into the page rather
76
+ than the URL. A hardened Origin allowlist is future work.
77
+
78
+ ## Scope (v1)
79
+
80
+ - **In:** MapLibre GL; the operations above; stdio + HTTP/SSE; a human CLI with parity.
81
+ - **Out:** generating maps; a hosted service; non-map visualizations; other map libraries
82
+ (Leaflet/deck.gl) and a no-cooperation (Playwright) path are future work.
83
+
84
+ ## License
85
+
86
+ MIT
@@ -0,0 +1,64 @@
1
+ <!DOCTYPE html>
2
+ <!--
3
+ map-mcp sample app — a minimal hooked MapLibre GL map for the first validated scenario.
4
+
5
+ Run:
6
+ 1) map-mcp serve # prints ws url + token
7
+ 2) serve this folder: python -m http.server 8080
8
+ 3) open http://localhost:8080/?token=PASTE_TOKEN
9
+ (the page reads ?token= and registers the live map with the bridge)
10
+
11
+ It shows a basemap + a points layer with clickable popups, created with
12
+ preserveDrawingBuffer:true so the screenshot tool works.
13
+ -->
14
+ <html>
15
+ <head>
16
+ <meta charset="utf-8" />
17
+ <title>map-mcp sample</title>
18
+ <script src="https://unpkg.com/maplibre-gl@4/dist/maplibre-gl.js"></script>
19
+ <link href="https://unpkg.com/maplibre-gl@4/dist/maplibre-gl.css" rel="stylesheet" />
20
+ <script src="../../hook/map-mcp-hook.js"></script>
21
+ <style>html, body, #map { height: 100%; margin: 0; }</style>
22
+ </head>
23
+ <body>
24
+ <div id="map"></div>
25
+ <script>
26
+ const map = new maplibregl.Map({
27
+ container: "map",
28
+ style: "https://demotiles.maplibre.org/style.json",
29
+ center: [12.5, 41.9],
30
+ zoom: 4,
31
+ preserveDrawingBuffer: true, // required for the screenshot tool
32
+ });
33
+
34
+ const cities = {
35
+ type: "FeatureCollection",
36
+ features: [
37
+ { type: "Feature", properties: { name: "Rome", pop: 2800000 },
38
+ geometry: { type: "Point", coordinates: [12.5, 41.9] } },
39
+ { type: "Feature", properties: { name: "Berlin", pop: 3600000 },
40
+ geometry: { type: "Point", coordinates: [13.4, 52.5] } },
41
+ ],
42
+ };
43
+
44
+ map.on("load", () => {
45
+ map.addSource("cities", { type: "geojson", data: cities });
46
+ map.addLayer({
47
+ id: "cities", type: "circle", source: "cities",
48
+ paint: { "circle-radius": 7, "circle-color": "#c33" },
49
+ });
50
+ map.on("click", "cities", (e) => {
51
+ const f = e.features[0];
52
+ new maplibregl.Popup()
53
+ .setLngLat(f.geometry.coordinates)
54
+ .setHTML(`<b>${f.properties.name}</b><br/>pop ${f.properties.pop}`)
55
+ .addTo(map);
56
+ });
57
+
58
+ // Register the live map with the bridge (token from ?token=).
59
+ const token = new URLSearchParams(location.search).get("token") || "";
60
+ mapMcp.register(map, { url: "ws://127.0.0.1:8765", token });
61
+ });
62
+ </script>
63
+ </body>
64
+ </html>
@@ -0,0 +1,176 @@
1
+ /*
2
+ * map-mcp hook — expose a live MapLibre GL map to the local map-mcp bridge.
3
+ *
4
+ * Add this file to your page and register your map:
5
+ *
6
+ * <script src="map-mcp-hook.js"></script>
7
+ * <script>
8
+ * mapMcp.register(map, { url: "ws://127.0.0.1:8765", token: "PASTE_TOKEN" });
9
+ * </script>
10
+ *
11
+ * The hook connects out to the bridge, presents the token (see snippet.md), and answers
12
+ * requests by calling the map's own JS API. Vanilla JS, no build step. Drives an EXISTING
13
+ * map — it never creates one. An unknown method returns an error reply; the hook never throws
14
+ * across the socket.
15
+ *
16
+ * Note: `screenshot` needs the map created with `preserveDrawingBuffer: true`, or a WebGL
17
+ * canvas reads back blank. See snippet.md.
18
+ */
19
+ (function (global) {
20
+ "use strict";
21
+
22
+ // --- method -> MapLibre operation ----------------------------------------
23
+ var HANDLERS = {
24
+ get_viewport: function (map) {
25
+ var c = map.getCenter();
26
+ var b = map.getBounds();
27
+ return {
28
+ center: [c.lng, c.lat],
29
+ zoom: map.getZoom(),
30
+ bearing: map.getBearing(),
31
+ pitch: map.getPitch(),
32
+ bounds: b ? b.toArray() : null, // [[swLng,swLat],[neLng,neLat]]
33
+ };
34
+ },
35
+
36
+ query_rendered_features: function (map, p) {
37
+ p = p || {};
38
+ var geom = _geometry(map, p); // undefined => whole viewport
39
+ var opts = p.layers ? { layers: p.layers } : undefined;
40
+ var feats = map.queryRenderedFeatures(geom, opts) || [];
41
+ var limit = p.limit || 200;
42
+ return { features: feats.slice(0, limit).map(_serializeFeature), truncated: feats.length > limit };
43
+ },
44
+
45
+ get_features_at: function (map, p) {
46
+ var pt = map.project(_lngLat(p)); // geographic -> pixel
47
+ var feats = map.queryRenderedFeatures(pt, p.layers ? { layers: p.layers } : undefined) || [];
48
+ return { features: feats.map(_serializeFeature) };
49
+ },
50
+
51
+ click_at: function (map, p) {
52
+ var lngLat = _lngLat(p);
53
+ var pt = map.project(lngLat);
54
+ // fire the map's own click so app handlers (popups, selection) run
55
+ map.fire("click", { lngLat: lngLat, point: pt, originalEvent: {} });
56
+ var feats = map.queryRenderedFeatures(pt, p.layers ? { layers: p.layers } : undefined) || [];
57
+ return { features: feats.map(_serializeFeature), popup: _readPopup() };
58
+ },
59
+
60
+ read_popup: function () {
61
+ return { popups: _readPopup() };
62
+ },
63
+
64
+ set_view: function (map, p) {
65
+ p = p || {};
66
+ if (p.bbox) {
67
+ map.fitBounds(p.bbox, { animate: false });
68
+ } else {
69
+ var to = {};
70
+ if (p.center) to.center = p.center;
71
+ if (p.zoom != null) to.zoom = p.zoom;
72
+ if (p.bearing != null) to.bearing = p.bearing;
73
+ if (p.pitch != null) to.pitch = p.pitch;
74
+ map.jumpTo(to);
75
+ }
76
+ return HANDLERS.get_viewport(map);
77
+ },
78
+
79
+ list_layers: function (map) {
80
+ var layers = (map.getStyle() && map.getStyle().layers) || [];
81
+ return {
82
+ layers: layers.map(function (l) {
83
+ var vis;
84
+ try { vis = map.getLayoutProperty(l.id, "visibility"); } catch (e) { vis = undefined; }
85
+ return { id: l.id, type: l.type, source: l.source, visibility: vis || "visible" };
86
+ }),
87
+ };
88
+ },
89
+
90
+ set_layer_visibility: function (map, p) {
91
+ var vis = p.visible ? "visible" : "none";
92
+ map.setLayoutProperty(p.layer, "visibility", vis);
93
+ return { layer: p.layer, visibility: vis };
94
+ },
95
+
96
+ screenshot: function (map) {
97
+ var canvas = map.getCanvas();
98
+ var data;
99
+ try { data = canvas.toDataURL("image/png"); } catch (e) { data = null; }
100
+ if (!data || data.length < 64) {
101
+ throw new Error("screenshot unavailable — create the map with preserveDrawingBuffer:true");
102
+ }
103
+ return { image: data, width: canvas.width, height: canvas.height };
104
+ },
105
+ };
106
+
107
+ // --- helpers --------------------------------------------------------------
108
+ function _lngLat(p) {
109
+ if (!p || !p.point) throw new Error("expected params.point as [lng, lat]");
110
+ return { lng: p.point[0], lat: p.point[1] };
111
+ }
112
+
113
+ function _geometry(map, p) {
114
+ if (p.point) return map.project(_lngLat(p));
115
+ if (p.bbox) {
116
+ var sw = map.project({ lng: p.bbox[0][0], lat: p.bbox[0][1] });
117
+ var ne = map.project({ lng: p.bbox[1][0], lat: p.bbox[1][1] });
118
+ return [sw, ne];
119
+ }
120
+ return undefined;
121
+ }
122
+
123
+ function _serializeFeature(f) {
124
+ return {
125
+ id: f.id != null ? f.id : null,
126
+ layer: f.layer ? f.layer.id : null,
127
+ source: f.source != null ? f.source : null,
128
+ sourceLayer: f.sourceLayer != null ? f.sourceLayer : null,
129
+ properties: f.properties || {},
130
+ geometryType: f.geometry ? f.geometry.type : null,
131
+ };
132
+ }
133
+
134
+ function _readPopup() {
135
+ var nodes = document.querySelectorAll(".maplibregl-popup-content, .mapboxgl-popup-content");
136
+ return Array.prototype.map.call(nodes, function (n) {
137
+ return (n.textContent || "").trim();
138
+ });
139
+ }
140
+
141
+ // --- transport ------------------------------------------------------------
142
+ function register(map, opts) {
143
+ opts = opts || {};
144
+ var url = opts.url || "ws://127.0.0.1:8765";
145
+ var ws = new WebSocket(url);
146
+
147
+ ws.onopen = function () {
148
+ // U6: present the per-session token before commands are accepted.
149
+ if (opts.token) ws.send(JSON.stringify({ type: "hello", token: opts.token }));
150
+ };
151
+
152
+ ws.onmessage = function (ev) {
153
+ var req;
154
+ try { req = JSON.parse(ev.data); } catch (e) { return; }
155
+ if (!req || req.id == null) return;
156
+ var reply = { id: req.id, ok: true, result: null };
157
+ try {
158
+ var handler = HANDLERS[req.method];
159
+ if (!handler) {
160
+ reply.ok = false;
161
+ reply.error = "unknown method: " + req.method;
162
+ } else {
163
+ reply.result = handler(map, req.params || {});
164
+ }
165
+ } catch (e) {
166
+ reply.ok = false;
167
+ reply.error = String(e && e.message ? e.message : e);
168
+ }
169
+ ws.send(JSON.stringify(reply));
170
+ };
171
+
172
+ return ws;
173
+ }
174
+
175
+ global.mapMcp = { register: register, _handlers: HANDLERS };
176
+ })(typeof window !== "undefined" ? window : this);
@@ -0,0 +1,37 @@
1
+ # Adding the map-mcp hook to your MapLibre app
2
+
3
+ map-mcp drives an **existing** map — you add one snippet so it can reach your live map
4
+ instance. Cooperation is required (v1); a no-cooperation path is deferred.
5
+
6
+ ## 1. Start the bridge
7
+
8
+ ```
9
+ map-mcp serve
10
+ ```
11
+
12
+ It prints a `ws://127.0.0.1:<port>` URL and a **per-session token** (the token gate is
13
+ local-only security — only a page presenting it can drive your map).
14
+
15
+ ## 2. Add the hook to your page
16
+
17
+ ```html
18
+ <script src="map-mcp-hook.js"></script>
19
+ <script>
20
+ // `map` is your existing maplibregl.Map instance.
21
+ mapMcp.register(map, { url: "ws://127.0.0.1:8765", token: "PASTE_TOKEN_FROM_SERVE" });
22
+ </script>
23
+ ```
24
+
25
+ That's it. The agent (over MCP) and the `map-mcp` CLI now act on the same map you're looking at.
26
+
27
+ ## Notes
28
+
29
+ - **Screenshots** need the map created with `preserveDrawingBuffer: true`:
30
+ ```js
31
+ const map = new maplibregl.Map({ /* ... */, preserveDrawingBuffer: true });
32
+ ```
33
+ Without it, a WebGL canvas reads back blank and the `screenshot` tool returns an error.
34
+ - Geographic inputs (`point`, `bbox`, `center`) are `[lng, lat]`. The hook projects them to
35
+ pixels for `queryRenderedFeatures` itself.
36
+ - The hook never creates a map and never posts anywhere — it answers read/drive requests on the
37
+ loopback socket only.
@@ -0,0 +1,45 @@
1
+ [project]
2
+ name = "map-mcp"
3
+ version = "0.1.0"
4
+ description = "Drive and perceive an existing live MapLibre GL map from an AI agent (MCP) or a human CLI — no test code, no browser automation."
5
+ readme = "README.md"
6
+ requires-python = ">=3.10"
7
+ license = { file = "LICENSE" }
8
+ authors = [{ name = "Kedar Dabhadkar" }]
9
+ keywords = ["maplibre", "map", "mcp", "model-context-protocol", "agent", "llm", "geospatial"]
10
+ classifiers = [
11
+ "Development Status :: 3 - Alpha",
12
+ "License :: OSI Approved :: MIT License",
13
+ "Programming Language :: Python :: 3",
14
+ "Programming Language :: Python :: 3.10",
15
+ "Programming Language :: Python :: 3.11",
16
+ "Programming Language :: Python :: 3.12",
17
+ "Topic :: Scientific/Engineering :: GIS",
18
+ "Topic :: Software Development :: Libraries",
19
+ ]
20
+ dependencies = [
21
+ "fastmcp>=3,<4",
22
+ "websockets>=12",
23
+ ]
24
+
25
+ [project.urls]
26
+ Homepage = "https://github.com/dkedar7/map-mcp"
27
+ Source = "https://github.com/dkedar7/map-mcp"
28
+ "Bug Tracker" = "https://github.com/dkedar7/map-mcp/issues"
29
+
30
+ [project.scripts]
31
+ map-mcp = "map_mcp.cli:_cli"
32
+
33
+ [project.optional-dependencies]
34
+ dev = ["pytest>=7"]
35
+
36
+ [build-system]
37
+ requires = ["hatchling"]
38
+ build-backend = "hatchling.build"
39
+
40
+ [tool.hatch.build.targets.wheel]
41
+ packages = ["src/map_mcp"]
42
+
43
+ [tool.pytest.ini_options]
44
+ pythonpath = ["src"]
45
+ testpaths = ["tests"]
@@ -0,0 +1,10 @@
1
+ """map-mcp — drive and perceive an existing live MapLibre GL map from an agent or a human CLI.
2
+
3
+ A map app opts in with a small JS hook that registers its live map and connects out to a
4
+ local WebSocket this process runs. The MCP tools (for agents) and the CLI (for humans) are
5
+ thin frontends over one shared core-operations layer, so any operation one can do, the other
6
+ can too (parity by construction). v1 is MapLibre-only and drives *existing* maps — it does not
7
+ generate maps.
8
+ """
9
+
10
+ __version__ = "0.1.0"