@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.
- package/LICENSE +202 -0
- package/README.md +166 -0
- package/dist/http.js +204 -0
- package/dist/index.js +50 -0
- package/dist/sessionStoreD1.js +191 -0
- package/dist/toolMeta.js +182 -0
- package/dist/tools.js +1443 -0
- package/dist/workspaceOpsSpec.md +250 -0
- package/package.json +50 -0
|
@@ -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
|
+
}
|