@showmeonmap/mcp 0.2.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.
@@ -0,0 +1,250 @@
1
+ # ShowMeOnMap Workspace Ops Spec (agent-facing)
2
+
3
+ **URI:** `showmeonmap://docs/workspace-ops-spec`
4
+ **Audience:** AI agents calling the ShowMeOnMap MCP server.
5
+ **Purpose:** Give the calling agent every shape it needs to construct valid
6
+ inputs to `mutate_map`, `filter_layer`, `add_layer`, `select_within`,
7
+ `set_time`, `add_annotation`, etc. without guessing.
8
+
9
+ If you are a calling agent and you are about to construct a structured
10
+ predicate, a LayerPlan, or any other complex argument — read this first.
11
+ Do **not** guess shapes; the renderer rejects malformed input with detail.
12
+
13
+ ---
14
+
15
+ ## Mental model
16
+
17
+ A ShowMeOnMap workspace is an **append-only ops log** plus a `cursor`. Every
18
+ mutation appends a typed Op. To render, the server folds the log up to the
19
+ cursor (`materialize(workspace)`) into a `MapPlan`. Undo/redo move the cursor;
20
+ they never delete ops. Tagging stamps a named checkpoint into `workspace.meta.tags`.
21
+
22
+ You as the calling agent never construct ops by hand for `mutate_map` — that
23
+ tool takes natural language. You **do** construct ops for the V2 direct-op
24
+ tools (`add_layer`, `filter_layer`, `select_within`, `restyle_layer`,
25
+ `set_time`, `tag`, `add_annotation`, etc.). Those POST a typed Op to the
26
+ server's reducer without an LLM in the loop.
27
+
28
+ Both surfaces require a `rationale` (≤300 chars) on every call: a short
29
+ audit-log label stating the user-facing goal the call serves. The server
30
+ cannot see your chat; the label is what keeps the ops-log traceable.
31
+
32
+ ---
33
+
34
+ ## Op catalog (what `materialize` understands)
35
+
36
+ Every op carries common fields:
37
+
38
+ ```ts
39
+ {
40
+ opId: uuid, // assigned server-side if you go through MCP
41
+ turnId: uuid, // shared across ops emitted in one user turn
42
+ emittedAt: ISO8601,
43
+ emittedBy: 'user' | 'agent' | 'system',
44
+ rationale?: string, // ≤300 chars, optional in raw Op shape, required at MCP layer
45
+ kind: <one of below>,
46
+ args: { ... }
47
+ }
48
+ ```
49
+
50
+ | Op kind | args |
51
+ | -------------------- | ----------------------------------------------------------------------------------------------------- |
52
+ | `createWorkspace` | `{ originalQuery?, shell: MapPlanShell }` — emitted by the server, never by you. |
53
+ | `addLayer` | `{ layerPlan: LayerPlan, intent?: string }` — see LayerPlan below. |
54
+ | `filterLayer` | `{ layerId, predicate: Predicate, append?: boolean, intent?: string }` — see Predicate. |
55
+ | `removeLayer` | `{ layerId }` — strict; double-remove rejected. Layer ids tombstone for the session. |
56
+ | `clearFilters` | `{ layerId }` — wipe predicate chain. Inverse of filterLayer. |
57
+ | `renameLayer` | `{ oldLayerId, newLayerId }` — re-keys filters. newLayerId must be unique (incl. tombstones). |
58
+ | `restyleLayer` | `{ layerId, color?: string, opacity?: number }` — within-palette style change. |
59
+ | `subsetToSelection` | `{ sourceLayerId, newLayerId, selection: GeoJSONPolygon, inheritStyle?: boolean }` |
60
+ | `setCamera` | `{ camera: CameraPlan }` — latest setCamera op wins on replay. |
61
+ | `setBasemap` | `{ basemap: 'light'\|'dark'\|'streets'\|'satellite'\|'engineering' }` |
62
+ | `setTime` | `{ field, rangeStart, rangeEnd, format?: 'unix_ms'\|'iso'\|'integer' }` |
63
+ | `addAnnotation` | `{ annotation: { id, coords: [lng, lat], label?, color? } }` — annotation ids namespace separately. |
64
+ | `removeAnnotation` | `{ annotationId }` |
65
+ | `tag` | `{ name }` — mirrored into `workspace.meta.tags`. Idempotent on duplicate. |
66
+
67
+ ---
68
+
69
+ ## Predicate grammar (Mongo-style tree)
70
+
71
+ Predicates are recursive trees. Leaf nodes test one field; logical nodes
72
+ combine children. The renderer transpiles to a MapLibre filter expression
73
+ where possible, falls back to a Turf-based CPU filter where the predicate
74
+ exceeds the GPU's expressiveness (e.g. spatial `within`).
75
+
76
+ ### Attribute predicates
77
+
78
+ ```ts
79
+ { op: 'eq', field: 'category', value: 'cafe' }
80
+ { op: 'ne', field: 'category', value: 'cafe' }
81
+ { op: 'gt', field: 'magnitude', value: 5 }
82
+ { op: 'gte', field: 'magnitude', value: 5 }
83
+ { op: 'lt', field: 'depth_m', value: 100 }
84
+ { op: 'lte', field: 'depth_m', value: 100 }
85
+ { op: 'in', field: 'country', values: ['Japan','Indonesia','Philippines'] }
86
+ { op: 'nin', field: 'country', values: ['Antarctica'] }
87
+ { op: 'regex', field: 'name', pattern: '^Café', flags: 'i' }
88
+ { op: 'exists', field: 'wikidata_id', value: true }
89
+ { op: 'between', field: 'rating', min: 4, max: 5 }
90
+ ```
91
+
92
+ ### Spatial predicates
93
+
94
+ ```ts
95
+ { op: 'within', geometry: { type: 'Polygon', coordinates: [[ [lng,lat], ... ]] } }
96
+ { op: 'intersects', layerId: 'country_borders' } // intersect with another layer's geometry
97
+ { op: 'withinDistance', center: [lng, lat], meters: 5000 }
98
+ { op: 'bbox', box: [west, south, east, north] }
99
+ ```
100
+
101
+ ### Temporal predicates
102
+
103
+ ```ts
104
+ { op: 'before', field: 'date', value: '2025-01-01' }
105
+ { op: 'after', field: 'date', value: '2020-01-01' }
106
+ { op: 'inRange', field: 'year', min: 2020, max: 2024 }
107
+ ```
108
+
109
+ ### Logical combinators
110
+
111
+ ```ts
112
+ { op: 'and', children: [<predicate>, <predicate>, ...] }
113
+ { op: 'or', children: [<predicate>, <predicate>, ...] }
114
+ { op: 'not', child: <predicate> }
115
+ ```
116
+
117
+ ### Worked example
118
+
119
+ > "Cafés rated 4+ within 2 km of [lng,lat]"
120
+
121
+ ```json
122
+ {
123
+ "op": "and",
124
+ "children": [
125
+ { "op": "eq", "field": "category", "value": "cafe" },
126
+ { "op": "gte", "field": "rating", "value": 4 },
127
+ { "op": "withinDistance", "center": [-122.42, 37.77], "meters": 2000 }
128
+ ]
129
+ }
130
+ ```
131
+
132
+ ---
133
+
134
+ ## LayerPlan (for `addLayer`)
135
+
136
+ Minimum required fields:
137
+
138
+ ```ts
139
+ {
140
+ id: string, // unique within workspace; tombstones on remove
141
+ title: string, // human-readable
142
+ geometry: 'point' | 'line' | 'polygon' | 'raster' | 'mixed',
143
+ source: LayerSource, // see below
144
+ viz: VizConfig, // see below
145
+ visible?: boolean, // default true
146
+ legend?: { label?: string, items?: Array<...> },
147
+ }
148
+ ```
149
+
150
+ ### LayerSource shapes
151
+
152
+ ```ts
153
+ // Inline GeoJSON
154
+ { kind: 'geojson', data: { type: 'FeatureCollection', features: [...] } }
155
+
156
+ // External GeoJSON URL
157
+ { kind: 'geojson-url', url: string, attribution?: string }
158
+
159
+ // Raster tiles (XYZ)
160
+ { kind: 'raster-tile', tileUrl: string, tileSize?: number, attribution?: string,
161
+ minzoom?: number, maxzoom?: number }
162
+
163
+ // Vector tile source
164
+ { kind: 'vector-tile', tileUrl: string, sourceLayer: string, attribution?: string }
165
+ ```
166
+
167
+ ### Viz variants (must match geometry)
168
+
169
+ ```ts
170
+ // point
171
+ { type: 'circle', color?: string, valueField?: string, colorBy?: string, icon?: string }
172
+ { type: 'cluster', clusterRadius?: number, color?: string }
173
+ { type: 'heatmap', weight?: number, intensity?: number }
174
+ // line
175
+ { type: 'line', color?: string, width?: number }
176
+ // polygon / fill
177
+ { type: 'fill', color?: string, opacity?: number }
178
+ { type: 'choropleth', valueField: string, palette: string, classification?: 'quantile'|'jenks'|'equal' }
179
+ // 3D
180
+ { type: 'extrusion', heightField: string, colorBy?: string }
181
+ // raster
182
+ { type: 'raster', opacity?: number }
183
+ ```
184
+
185
+ Pass `viz` types that **match** the geometry (`circle`/`heatmap`/`cluster` →
186
+ points; `line` → lines; `fill`/`choropleth`/`extrusion` → polygons;
187
+ `raster` → raster). Mismatches are rejected.
188
+
189
+ ---
190
+
191
+ ## CameraPlan (for `setCamera`)
192
+
193
+ ```ts
194
+ {
195
+ center: [lng, lat],
196
+ zoom: number, // 0=world ... 22=building
197
+ bearing?: number, // -180..180, 0 = north up
198
+ pitch?: number, // 0..85, 0 = top-down
199
+ duration_ms?: number, // animation duration
200
+ }
201
+ ```
202
+
203
+ ---
204
+
205
+ ## Refusal & clarification surfaces (inside `mutate_map`)
206
+
207
+ When you call `mutate_map` with NL, the response can carry one of three
208
+ non-mutation signals. Treat them as structured affordances, not errors:
209
+
210
+ - `outOfScope: { reason: string }` — instruction is unrelated to map state
211
+ (e.g. "what is the weather"). Do **not** retry; build a new map with
212
+ `build_map` instead.
213
+ - `clarification: { question: string, candidates: string[] }` — instruction
214
+ is materially ambiguous (e.g. "remove it" with three candidate layers).
215
+ Surface the question + candidates back to your user; resend with the
216
+ disambiguated instruction.
217
+ - `nextActions: string[]` — 2–3 concrete follow-ups tailored to the new
218
+ workspace state. Use as conversational suggestions to the user.
219
+
220
+ The direct-op tools never emit these — they're a property of NL emission only.
221
+
222
+ ---
223
+
224
+ ## Common pitfalls
225
+
226
+ - **Layer ids do not recycle.** Once `removeLayer` runs, that id is
227
+ tombstoned for the session. Re-use a different id.
228
+ - **Annotation ids do recycle** — different namespace.
229
+ - **`zoom` must be top-level** in any zoom-driven `step`/`interpolate` you
230
+ construct. Wrapping in arithmetic silently fails.
231
+ - **`setBasemap` is enum-only** — five values exactly. Custom URLs go
232
+ through the `addLayer` raster-tile source.
233
+ - **`promoteId: 'iso3'`** is the standard for choropleth feature-state
234
+ hover; provide it in the source if you're building per-country fills.
235
+ - **Continuous scalar fields (UV, AQI, pollen, temperature, radiation)
236
+ must render as smooth rasters**, never as a point grid or MapLibre
237
+ `heatmap`. Use `raster-tile` source + `viz.type: 'raster'`. Hard rule.
238
+ - **Default basemap water color** sits at `#aadaff` — tune fill opacity if
239
+ you need legibility on water-heavy regions.
240
+
241
+ ---
242
+
243
+ ## Versioning
244
+
245
+ This spec describes **schema version 1** of the workspace ops log. The
246
+ server returns `workspace.schemaVersion === 1`. Future changes will bump
247
+ this and add an explicit migration path. If you see `schemaVersion > 1`,
248
+ your snapshot of this resource may be stale — refetch.
249
+
250
+ Last updated: 2026-04-25.
package/package.json ADDED
@@ -0,0 +1,50 @@
1
+ {
2
+ "name": "@showmeonmap/mcp",
3
+ "version": "0.2.0",
4
+ "description": "MCP server for ShowMeOnMap — geospatial visualization tools for any MCP-speaking agent (Claude Desktop, Cursor, Zed, ChatGPT Apps SDK)",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "bin": {
8
+ "showmeonmap-mcp": "dist/index.js"
9
+ },
10
+ "files": [
11
+ "dist",
12
+ "README.md"
13
+ ],
14
+ "engines": {
15
+ "node": ">=20"
16
+ },
17
+ "scripts": {
18
+ "build": "tsc -p tsconfig.json && cp src/workspaceOpsSpec.md dist/workspaceOpsSpec.md && chmod +x dist/index.js",
19
+ "prepack": "npm run build",
20
+ "clean": "rm -rf dist",
21
+ "smoke": "node dist/index.js",
22
+ "test:unit": "node ../../scripts/run-mcp-unit-tests.mjs",
23
+ "test": "npm run test:unit"
24
+ },
25
+ "dependencies": {
26
+ "@modelcontextprotocol/sdk": "^1.29.0",
27
+ "zod": "^4.3.5"
28
+ },
29
+ "devDependencies": {
30
+ "@types/node": "^20",
31
+ "typescript": "^5.4.0"
32
+ },
33
+ "license": "Apache-2.0",
34
+ "repository": {
35
+ "type": "git",
36
+ "url": "git+https://github.com/FrankAIUser/showmeonmap.git",
37
+ "directory": "packages/mcp-server"
38
+ },
39
+ "homepage": "https://github.com/FrankAIUser/showmeonmap/tree/main/packages/mcp-server#readme",
40
+ "keywords": [
41
+ "mcp",
42
+ "model-context-protocol",
43
+ "maps",
44
+ "geospatial",
45
+ "showmeonmap",
46
+ "claude",
47
+ "cursor",
48
+ "zed"
49
+ ]
50
+ }