@showmeonmap/mcp 0.3.2 → 0.5.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 CHANGED
@@ -90,10 +90,17 @@ Ask Claude Desktop things like:
90
90
 
91
91
  ---
92
92
 
93
- ## Tool surface (24 tools)
93
+ ## Tool surface (26 tools)
94
94
 
95
- **V1 — natural-language tools (3)**: `build_map`, `mutate_map`, `get_state`.
96
- **V2 — direct-op tools (18)**: `add_layer`, `filter_layer`, `clear_filters`, `remove_layer`, `rename_layer`, `restyle_layer`, `set_camera`, `set_time`, `set_basemap`, `select_within`, `subset_to_selection`, `query_features`, `add_annotation`, `remove_annotation`, `tag`, `undo`, `redo`, `report_blocker`.
95
+ **Workspace lifecycle (6)**: `build_map`, `mutate_map`, `get_state`, `export_workspace`, `undo`, `redo`.
96
+ **Direct ops (14)**: `add_layer`, `filter_layer`, `select_within`, `focus_area`, `query_features`, `remove_layer`, `clear_filters`, `rename_layer`, `restyle_layer`, `set_camera`, `set_time`, `tag`, `add_annotation`, `remove_annotation`.
97
+ **Multi-source analysis (3)**: `add_fusion_layer`, `correlate_layers`, `run_analysis`.
98
+ **Export (2)**: `export_layer_data`, `export_image`.
99
+ **Feedback channel (1)**: `report_blocker`.
100
+
101
+ `focus_area` (0.4.0) refines an existing map to a **named** area — no coordinates required. Pass a `layerId` plus an `areaRef` (`{kind:'corridor', street, from?, to?}`, `{kind:'place', name}`, or `{kind:'aroundIntersection', street, cross}`) and the server resolves the real geometry from OpenStreetMap, then applies one of three `mode`s: `filter` (hide non-matching), `select` (extract to a new layer), or `clip` (geometrically trim — lines are split at the boundary). The reply carries a grounded `contextAnswer` such as `"kept 37 of 150 features"`. A resolver miss returns a `clarification` with real nearby candidates rather than a guessed location.
102
+
103
+ `run_analysis` (0.5.0) computes a spatial analysis server-side and appends the finished, honestly-legended result as a new layer + camera. `analysis: 'hotspot'` runs **Getis-Ord Gi\***: hot/cold significance classes (99/95/90%, Benjamini-Hochberg FDR-corrected) on a numeric `valueField`, or on hex-binned point density when `valueField` is omitted (fishnet with zero cells, auto-grown under a stated cap). The reply carries a grounded `memo` + `stats` payload (Moran's I, class counts, significant share) — every number is computed, never generated — plus `resultLayerId` and a `contextAnswer` like `"Found 12 hot and 0 cold features among n=72"`. "No significant clustering" is a finding that keeps the map (and the credit); typed failures (unknown layer, too few features, the 10,000-feature cap) are refunded. `analysis: 'zonal'` aggregates a point layer into polygon zones — an existing zone layer (`zoneLayerId`) or a generated h3 hex fishnet (`zones: {kind:'h3', resolution?}`) — as count/sum/mean/min/max per zone, rendered as a quantile choropleth where a zone with zero features and a zone the data never reaches are DIFFERENT legend classes. `analysis: 'enrich'` joins a US Census ACS indicator (`median_household_income`, `median_home_value`, `median_rent`, … or a raw variable code) onto each feature by tract/county/state containment — unmatched features stay null, never zero-filled, and the joined column carries the indicator's human name. In all analyses the result replaces its input layer(s) (undoable; original attributes preserved or aggregated as stated in the memo). The catalog grows behind this one tool.
97
104
 
98
105
  Every tool returns a **three-channel envelope**:
99
106
 
package/dist/toolMeta.js CHANGED
@@ -72,6 +72,18 @@ export const MCP_TOOL_META = {
72
72
  long_running: false,
73
73
  rationale: 'Adds a new derived layer; original layer untouched.',
74
74
  },
75
+ focus_area: {
76
+ requires_confirmation: false,
77
+ destructive: false,
78
+ long_running: false,
79
+ rationale: 'Filters or derives a layer to a server-resolved named area; reversible via undo/clear_filters.',
80
+ },
81
+ run_analysis: {
82
+ requires_confirmation: false,
83
+ destructive: false,
84
+ long_running: false,
85
+ rationale: 'Appends a computed analysis layer; original layer untouched; reversible via undo/remove_layer.',
86
+ },
75
87
  query_features: {
76
88
  requires_confirmation: false,
77
89
  destructive: false,
package/dist/tools.js CHANGED
@@ -207,6 +207,12 @@ function envelope(baseUrl, mapId, workspace, mapPlan, nextActionStrings, extras
207
207
  })),
208
208
  ...(extras.outOfScope ? { outOfScope: extras.outOfScope } : {}),
209
209
  ...(extras.clarification ? { clarification: extras.clarification } : {}),
210
+ ...(extras.opsApplied ? { opsApplied: extras.opsApplied } : {}),
211
+ ...(extras.contextAnswer ? { contextAnswer: extras.contextAnswer } : {}),
212
+ ...(extras.resultLayerId ? { resultLayerId: extras.resultLayerId } : {}),
213
+ ...(extras.memo ? { memo: extras.memo } : {}),
214
+ ...(extras.stats ? { stats: extras.stats } : {}),
215
+ ...(extras.note ? { note: extras.note } : {}),
210
216
  };
211
217
  }
212
218
  function toContent(obj) {
@@ -240,6 +246,7 @@ const INVALID_INPUT_HINTS = {
240
246
  add_layer: 'Required: map_id, layerPlan (object), rationale. Optional: intent (enum). No other fields accepted.',
241
247
  filter_layer: 'Required: map_id, layerId, predicate (object), rationale. Optional: append (boolean), intent (enum).',
242
248
  select_within: 'Required: map_id, sourceLayerId, newLayerId, selection ({type:Polygon|MultiPolygon, coordinates}), rationale. Optional: inheritStyle.',
249
+ focus_area: 'Required: map_id, layerId, areaRef ({kind:"corridor", street, from?, to?} | {kind:"place", name} | {kind:"aroundIntersection", street, cross}), rationale. Optional: mode (filter|select|clip). Pass NAMES — the server resolves geometry from OSM.',
243
250
  query_features: 'Required: map_id, layerId, predicate (object), rationale. Optional: limit (1–1000 integer).',
244
251
  remove_layer: 'Required: map_id, layerId, rationale, confirmed:true (destructive — layer ids are tombstoned).',
245
252
  clear_filters: 'Required: map_id, layerId, rationale, confirmed:true (sensitive — autonomous filter wipe).',
@@ -257,7 +264,7 @@ const INVALID_INPUT_HINTS = {
257
264
  correlate_layers: 'Required: map_id, layer_a_id, layer_b_id, rationale. Optional: value_field_a, value_field_b. Pure read — no layer added, no credit charged. No other fields accepted.',
258
265
  undo: 'Required: map_id, rationale, confirmed:true (sensitive — autonomous undo can erase prior work).',
259
266
  redo: 'Required: map_id, rationale.',
260
- report_blocker: 'Required: what_i_was_trying, what_i_tried, where_i_got_stuck. Optional: map_id, suggested_capability.',
267
+ report_blocker: 'Required: what_i_was_trying, what_i_tried, where_i_got_stuck, rationale. Optional: map_id, suggested_capability.',
261
268
  };
262
269
  function buildInvalidInputResponse(toolName, error) {
263
270
  const issues = error.issues.map((iss) => ({
@@ -500,7 +507,7 @@ const READ_ONLY_TOOLS = new Set([
500
507
  ]);
501
508
  // Tools that reach external/open-world data sources, vs operating purely on
502
509
  // already-loaded workspace state.
503
- const OPEN_WORLD_TOOLS = new Set(['build_map', 'mutate_map', 'add_layer', 'add_fusion_layer']);
510
+ const OPEN_WORLD_TOOLS = new Set(['build_map', 'mutate_map', 'add_layer', 'add_fusion_layer', 'focus_area']);
504
511
  // Mutating-but-idempotent setters: re-applying the same call converges to the
505
512
  // same state (unlike append-style build_map / add_annotation / tag / undo).
506
513
  const IDEMPOTENT_SETTERS = new Set([
@@ -958,6 +965,171 @@ export function createServer(opts) {
958
965
  };
959
966
  return toContent(await applyOp(map_id, op));
960
967
  }));
968
+ const focusAreaAreaRefShape = z.discriminatedUnion('kind', [
969
+ z.object({ kind: z.literal('place'), name: z.string().min(1) }),
970
+ z.object({
971
+ kind: z.literal('corridor'),
972
+ street: z.string().min(1),
973
+ from: z.string().min(1).optional(),
974
+ to: z.string().min(1).optional(),
975
+ bufferMeters: z.number().min(30).max(1000).optional(),
976
+ }),
977
+ z.object({
978
+ kind: z.literal('aroundIntersection'),
979
+ street: z.string().min(1),
980
+ cross: z.string().min(1),
981
+ radiusMeters: z.number().min(50).max(5000).optional(),
982
+ }),
983
+ ]);
984
+ const focusAreaShape = {
985
+ map_id: z.string().min(1),
986
+ layerId: z.string().min(1),
987
+ areaRef: focusAreaAreaRefShape,
988
+ mode: z
989
+ .enum(['filter', 'select', 'clip'])
990
+ .optional()
991
+ .describe('filter (default): hide features outside, reversible. select: copy matches into a new layer. clip: new layer with geometry trimmed at the boundary.'),
992
+ rationale: rationaleSchema,
993
+ };
994
+ registerTool('focus_area', {
995
+ title: 'Focus a layer on a named area',
996
+ description: 'Refine an existing layer to a NAMED area the server resolves from OpenStreetMap — a street corridor ("Broadway from Arbutus Street to Main Street"), a place ("Mount Pleasant"), or an intersection surroundings. Pass names only, never coordinates; unresolvable names return a clarification with real nearby candidates instead of guessed geometry. Emits ordinary undoable ops (filter / new layer + camera) and a grounded summary like "kept 37 of 150 features". Costs 1 credit when a mutation lands; clarifications are refunded.',
997
+ inputSchema: focusAreaShape,
998
+ }, withValidation('focus_area', focusAreaShape, async ({ map_id, layerId, areaRef, mode, rationale }) => {
999
+ const entry = await store.get(map_id);
1000
+ if (!entry)
1001
+ throw new Error(`Unknown map_id "${map_id}". Call build_map first.`);
1002
+ logRationale('focus_area', map_id, rationale, { opKind: `focus_area:${areaRef.kind ?? 'unknown'}:${mode ?? 'filter'}` });
1003
+ const resp = (await apiFetch(baseUrl, apiKey, '/api/workspace/focus-area', {
1004
+ workspace: entry.workspace,
1005
+ layerId,
1006
+ areaRef,
1007
+ ...(mode ? { mode } : {}),
1008
+ rationale,
1009
+ }));
1010
+ if (!resp.ok) {
1011
+ throw new Error(`focus_area failed: ${resp.error?.kind ?? 'unknown'} — ${resp.error?.message ?? 'no detail'}`);
1012
+ }
1013
+ // Honest resolver failure: nothing changed; surface the clarification.
1014
+ if (resp.mutated === false) {
1015
+ return toContent(envelope(baseUrl, map_id, entry.workspace, entry.mapPlan, undefined, { ...(resp.clarification ? { clarification: resp.clarification } : {}) }));
1016
+ }
1017
+ await store.put(map_id, {
1018
+ workspace: resp.workspace ?? entry.workspace,
1019
+ mapPlan: resp.mapPlan ?? entry.mapPlan,
1020
+ layerFilters: resp.layerFilters ?? {},
1021
+ createdAt: entry.createdAt,
1022
+ lastAccessed: Date.now(),
1023
+ });
1024
+ const focusedPlan = (resp.mapPlan ?? entry.mapPlan);
1025
+ const shared = await persistShareableMap(baseUrl, apiKey, map_id, focusedPlan, rationale);
1026
+ return toContent(envelope(baseUrl, map_id, (resp.workspace ?? entry.workspace), focusedPlan, undefined, {
1027
+ ...(resp.opsApplied ? { opsApplied: resp.opsApplied } : {}),
1028
+ ...(resp.contextAnswer ? { contextAnswer: resp.contextAnswer } : {}),
1029
+ ...(shared ? {} : { shareUnpersisted: true }),
1030
+ }));
1031
+ }));
1032
+ const runAnalysisShape = {
1033
+ map_id: z.string().min(1),
1034
+ analysis: z
1035
+ .enum(['hotspot', 'zonal', 'enrich'])
1036
+ .describe("Which analysis to run. 'hotspot' = Getis-Ord Gi* with FDR-corrected significance classes. 'zonal' = aggregate a point layer into polygon zones (count/sum/mean/min/max choropleth). 'enrich' = join a US Census indicator onto each feature by containment (tract/county/state)."),
1037
+ layerId: z.string().min(1).describe('The workspace layer to analyze (geojson-backed; the point source for zonal).'),
1038
+ valueField: z
1039
+ .string()
1040
+ .min(1)
1041
+ .optional()
1042
+ .describe('Numeric property to analyze. hotspot: omit on point layers for incident-DENSITY mode (hex-binned counts). zonal: omit for count-only aggregation.'),
1043
+ k: z.number().int().min(3).max(24).optional().describe('hotspot only: nearest neighbors per feature (default 8).'),
1044
+ hexMeters: z
1045
+ .number()
1046
+ .min(100)
1047
+ .max(50_000)
1048
+ .optional()
1049
+ .describe('hotspot density mode only: hex cell spacing in meters (default auto from the data extent).'),
1050
+ zoneLayerId: z
1051
+ .string()
1052
+ .min(1)
1053
+ .optional()
1054
+ .describe('zonal only: an existing polygon layer to aggregate into. Provide EXACTLY ONE of zoneLayerId or zones.'),
1055
+ zones: z
1056
+ .object({ kind: z.literal('h3'), resolution: z.number().int().min(3).max(10).optional() })
1057
+ .strict()
1058
+ .optional()
1059
+ .describe('zonal only: generate an h3 hex fishnet over the source extent instead of using a zone layer (resolution 3-10, default auto). Provide EXACTLY ONE of zoneLayerId or zones.'),
1060
+ stats: z
1061
+ .array(z.enum(['count', 'sum', 'mean', 'min', 'max']))
1062
+ .min(1)
1063
+ .max(5)
1064
+ .optional()
1065
+ .describe('zonal only: which statistics to attach per zone (default: count, plus sum/mean/min/max when valueField is set).'),
1066
+ indicator: z
1067
+ .string()
1068
+ .min(1)
1069
+ .max(80)
1070
+ .optional()
1071
+ .describe("enrich only (required there): a US Census ACS preset id (e.g. 'median_household_income', 'median_home_value', 'median_rent') or a raw ACS variable code (e.g. 'B19013_001E')."),
1072
+ geoLevel: z
1073
+ .enum(['state', 'county', 'tract'])
1074
+ .optional()
1075
+ .describe('enrich only: containment geography (default tract). Tract/county fetch only the states the data touches, capped at 3 — filter first or use state for wider layers.'),
1076
+ rationale: rationaleSchema,
1077
+ };
1078
+ registerTool('run_analysis', {
1079
+ title: 'Run a spatial analysis on a layer',
1080
+ description: 'Compute a spatial analysis server-side and append the finished, honestly-legended result ' +
1081
+ 'as a new layer + camera; the result REPLACES its input layer(s) (undoable). hotspot = ' +
1082
+ 'Getis-Ord Gi*: hot/cold significance classes (99/95/90%, Benjamini-Hochberg FDR-corrected) ' +
1083
+ 'on a numeric field, or on hex-binned point density when valueField is omitted. zonal = ' +
1084
+ 'aggregate points into polygon zones (an existing zone layer OR a generated h3 fishnet): ' +
1085
+ 'count/sum/mean/min/max per zone as a quantile choropleth where zero and no-data are ' +
1086
+ 'distinct classes. enrich = join a US Census ACS indicator (median income, home value, ' +
1087
+ 'rent, …) onto each feature by tract/county/state containment — unmatched features stay ' +
1088
+ 'null, never zero-filled. Returns a grounded memo + stats payload — every number is computed, ' +
1089
+ 'never generated; findings keep the map. Costs 1 credit when the result lands; typed ' +
1090
+ 'failures (unknown layer, too few features, >10,000-feature cap) are refunded.',
1091
+ inputSchema: runAnalysisShape,
1092
+ }, withValidation('run_analysis', runAnalysisShape, async ({ map_id, analysis, layerId, valueField, k, hexMeters, zoneLayerId, zones, stats, indicator, geoLevel, rationale }) => {
1093
+ const entry = await store.get(map_id);
1094
+ if (!entry)
1095
+ throw new Error(`Unknown map_id "${map_id}". Call build_map first.`);
1096
+ logRationale('run_analysis', map_id, rationale, { opKind: `run_analysis:${analysis}` });
1097
+ const resp = (await apiFetch(baseUrl, apiKey, '/api/workspace/run-analysis', {
1098
+ workspace: entry.workspace,
1099
+ analysis,
1100
+ layerId,
1101
+ ...(valueField !== undefined ? { valueField } : {}),
1102
+ ...(k !== undefined ? { k } : {}),
1103
+ ...(hexMeters !== undefined ? { hexMeters } : {}),
1104
+ ...(zoneLayerId !== undefined ? { zoneLayerId } : {}),
1105
+ ...(zones !== undefined ? { zones } : {}),
1106
+ ...(stats !== undefined ? { stats } : {}),
1107
+ ...(indicator !== undefined ? { indicator } : {}),
1108
+ ...(geoLevel !== undefined ? { geoLevel } : {}),
1109
+ rationale,
1110
+ }));
1111
+ if (!resp.ok) {
1112
+ throw new Error(`run_analysis failed: ${resp.error?.kind ?? 'unknown'} — ${resp.error?.message ?? 'no detail'}`);
1113
+ }
1114
+ await store.put(map_id, {
1115
+ workspace: resp.workspace ?? entry.workspace,
1116
+ mapPlan: resp.mapPlan ?? entry.mapPlan,
1117
+ layerFilters: resp.layerFilters ?? {},
1118
+ createdAt: entry.createdAt,
1119
+ lastAccessed: Date.now(),
1120
+ });
1121
+ const analyzedPlan = (resp.mapPlan ?? entry.mapPlan);
1122
+ const shared = await persistShareableMap(baseUrl, apiKey, map_id, analyzedPlan, rationale);
1123
+ return toContent(envelope(baseUrl, map_id, (resp.workspace ?? entry.workspace), analyzedPlan, undefined, {
1124
+ ...(resp.opsApplied ? { opsApplied: resp.opsApplied } : {}),
1125
+ ...(resp.contextAnswer ? { contextAnswer: resp.contextAnswer } : {}),
1126
+ ...(resp.resultLayerId ? { resultLayerId: resp.resultLayerId } : {}),
1127
+ ...(resp.memo ? { memo: resp.memo } : {}),
1128
+ ...(resp.stats ? { stats: resp.stats } : {}),
1129
+ ...(resp.note ? { note: resp.note } : {}),
1130
+ ...(shared ? {} : { shareUnpersisted: true }),
1131
+ }));
1132
+ }));
961
1133
  const queryFeaturesShape = {
962
1134
  map_id: z.string().min(1),
963
1135
  layerId: z.string().min(1),
@@ -1495,6 +1667,7 @@ export function createServer(opts) {
1495
1667
  .max(300)
1496
1668
  .optional()
1497
1669
  .describe('Optional. A new tool, op, or schema field that would have unblocked you.'),
1670
+ rationale: rationaleSchema,
1498
1671
  };
1499
1672
  registerTool('report_blocker', {
1500
1673
  title: 'Report a blocker (agent feedback)',
@@ -1506,11 +1679,11 @@ export function createServer(opts) {
1506
1679
  'session, but feeds future capability work. No map_id required — file the report ' +
1507
1680
  'even if no map was built.',
1508
1681
  inputSchema: reportBlockerShape,
1509
- }, withValidation('report_blocker', reportBlockerShape, async ({ map_id, what_i_was_trying, what_i_tried, where_i_got_stuck, suggested_capability }) => {
1682
+ }, withValidation('report_blocker', reportBlockerShape, async ({ map_id, what_i_was_trying, what_i_tried, where_i_got_stuck, suggested_capability, rationale }) => {
1510
1683
  // Mirror to the rationale NDJSON so the May-1 clustering pass sees a
1511
1684
  // unified stream of "what did agents try, where did they get stuck".
1512
1685
  // The agent-blockers route persists the full record separately.
1513
- logRationale('report_blocker', map_id ?? null, `BLOCKER: ${what_i_was_trying} — stuck: ${where_i_got_stuck}`, { opKind: 'reportBlocker' });
1686
+ logRationale('report_blocker', map_id ?? null, `BLOCKER: ${what_i_was_trying} — stuck: ${where_i_got_stuck} (${rationale})`, { opKind: 'reportBlocker' });
1514
1687
  const resp = (await apiFetch(baseUrl, apiKey, '/api/agent/report-blocker', {
1515
1688
  ...(map_id ? { map_id } : {}),
1516
1689
  what_i_was_trying,
@@ -1525,6 +1698,7 @@ export function createServer(opts) {
1525
1698
  return toContent({
1526
1699
  ok: true,
1527
1700
  message: `Blocker recorded — thanks. Recorded at ${resp.recorded_at ?? new Date().toISOString()}.`,
1701
+ ...(resp.sink ? { sink: resp.sink } : {}),
1528
1702
  });
1529
1703
  }));
1530
1704
  // ── JSON Schema draft normalization (2026-07-16) ─────────────────────────
@@ -50,13 +50,14 @@ Every op carries common fields:
50
50
  | Op kind | args |
51
51
  | -------------------- | ----------------------------------------------------------------------------------------------------- |
52
52
  | `createWorkspace` | `{ originalQuery?, shell: MapPlanShell }` — emitted by the server, never by you. |
53
- | `addLayer` | `{ layerPlan: LayerPlan, intent?: string }` — see LayerPlan below. |
53
+ | `addLayer` | `{ layerPlan: LayerPlan, intent?: string }` — see LayerPlan below. For computed ANALYSIS layers (Gi* hot spots, zonal-statistics choropleths, census enrichment), prefer the `run_analysis` tool — the server computes, classes, and legends the result honestly and replaces the input layer(s), undoably. |
54
54
  | `filterLayer` | `{ layerId, predicate: Predicate, append?: boolean, intent?: string }` — see Predicate. |
55
55
  | `removeLayer` | `{ layerId }` — strict; double-remove rejected. Layer ids tombstone for the session. |
56
56
  | `clearFilters` | `{ layerId }` — wipe predicate chain. Inverse of filterLayer. |
57
57
  | `renameLayer` | `{ oldLayerId, newLayerId }` — re-keys filters. newLayerId must be unique (incl. tombstones). |
58
58
  | `restyleLayer` | `{ layerId, color?: string, opacity?: number }` — within-palette style change. |
59
59
  | `subsetToSelection` | `{ sourceLayerId, newLayerId, selection: GeoJSONPolygon, inheritStyle?: boolean }` |
60
+ | `clipLayer` | `{ sourceLayerId, newLayerId, clip: GeoJSONPolygon, inheritStyle?: boolean }` — new layer with geometry TRIMMED at the clip boundary (crossing lines keep only their inside span; polygons intersected; outside features dropped). Requires an inline-geojson source. Prefer the `focus_area` tool for NAMED areas — it resolves streets/places server-side so you never hand-build the polygon. |
60
61
  | `setCamera` | `{ camera: CameraPlan }` — latest setCamera op wins on replay. |
61
62
  | `setBasemap` | `{ basemap: 'light'\|'dark'\|'streets'\|'satellite'\|'engineering' }` |
62
63
  | `setTime` | `{ field, rangeStart, rangeEnd, format?: 'unix_ms'\|'iso'\|'integer' }` |
@@ -92,8 +93,9 @@ exceeds the GPU's expressiveness (e.g. spatial `within`).
92
93
  ### Spatial predicates
93
94
 
94
95
  ```ts
95
- { op: 'within', geometry: { type: 'Polygon', coordinates: [[ [lng,lat], ... ]] } }
96
+ { op: 'within', geometry: { type: 'Polygon', coordinates: [[ [lng,lat], ... ]] } } // feature ENTIRELY inside
96
97
  { op: 'intersects', layerId: 'country_borders' } // intersect with another layer's geometry
98
+ { op: 'intersects', geometry: { type: 'Polygon', coordinates: [[ ... ]] } } // intersect an inline polygon — keeps features crossing the boundary (e.g. a street-corridor focus); pass EXACTLY ONE of layerId | geometry
97
99
  { op: 'withinDistance', center: [lng, lat], meters: 5000 }
98
100
  { op: 'bbox', box: [west, south, east, north] }
99
101
  ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@showmeonmap/mcp",
3
- "version": "0.3.2",
3
+ "version": "0.5.0",
4
4
  "mcpName": "com.showmeonmap/maps",
5
5
  "description": "MCP server for ShowMeOnMap \u2014 geospatial visualization tools for any MCP-speaking agent (Claude Desktop, Cursor, Zed, ChatGPT Apps SDK)",
6
6
  "type": "module",