@showmeonmap/mcp 0.2.0 → 0.3.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.
Files changed (2) hide show
  1. package/dist/tools.js +56 -24
  2. package/package.json +2 -2
package/dist/tools.js CHANGED
@@ -394,6 +394,33 @@ async function persistShareableMap(baseUrl, apiKey, mapId, mapPlan, query) {
394
394
  /* best-effort — a share-save failure never fails the build */
395
395
  }
396
396
  }
397
+ // MCP tool annotations — safety hints every tool advertises so clients can
398
+ // reason about read-only vs mutating vs destructive behavior (and the model
399
+ // picks tools correctly). `destructiveHint` derives from MCP_TOOL_META (the
400
+ // single source of truth, kept in sync with the agent-side meta); the rest
401
+ // from a small intent classification.
402
+ const READ_ONLY_TOOLS = new Set([
403
+ 'get_state', 'query_features', 'select_within', 'correlate_layers',
404
+ 'export_workspace', 'export_layer_data', 'export_image', 'report_blocker',
405
+ ]);
406
+ // Tools that reach external/open-world data sources, vs operating purely on
407
+ // already-loaded workspace state.
408
+ const OPEN_WORLD_TOOLS = new Set(['build_map', 'mutate_map', 'add_layer', 'add_fusion_layer']);
409
+ // Mutating-but-idempotent setters: re-applying the same call converges to the
410
+ // same state (unlike append-style build_map / add_annotation / tag / undo).
411
+ const IDEMPOTENT_SETTERS = new Set([
412
+ 'set_camera', 'set_time', 'clear_filters', 'rename_layer', 'restyle_layer',
413
+ 'remove_layer', 'remove_annotation',
414
+ ]);
415
+ function toolAnnotations(name) {
416
+ const readOnly = READ_ONLY_TOOLS.has(name);
417
+ return {
418
+ readOnlyHint: readOnly,
419
+ destructiveHint: MCP_TOOL_META[name]?.destructive ?? false,
420
+ idempotentHint: readOnly || IDEMPOTENT_SETTERS.has(name),
421
+ openWorldHint: OPEN_WORLD_TOOLS.has(name),
422
+ };
423
+ }
397
424
  export function createServer(opts) {
398
425
  const baseUrl = opts.baseUrl;
399
426
  const apiKey = opts.apiKey;
@@ -402,6 +429,11 @@ export function createServer(opts) {
402
429
  name: opts.name ?? 'showmeonmap',
403
430
  version: opts.version ?? '0.1.0',
404
431
  });
432
+ // Stamp MCP annotations onto every tool from the central classification so we
433
+ // don't thread them through 24 registerTool call sites. `title` mirrors the
434
+ // tool's own title; any inline annotations a call sets win over the defaults.
435
+ const baseRegisterTool = server.registerTool.bind(server);
436
+ const registerTool = (name, config, cb) => baseRegisterTool(name, { ...config, annotations: { title: config.title, ...toolAnnotations(name), ...config.annotations } }, cb);
405
437
  // -------------------------------------------------------------------------
406
438
  // Resource: workspace-ops-spec
407
439
  // -------------------------------------------------------------------------
@@ -463,7 +495,7 @@ export function createServer(opts) {
463
495
  .describe('Natural language: "coffee shops in Tokyo", "GDP by country", "volcanoes in Indonesia", etc.'),
464
496
  rationale: rationaleSchema,
465
497
  };
466
- server.registerTool('build_map', {
498
+ registerTool('build_map', {
467
499
  title: 'Build a map',
468
500
  description: 'Create a new workspace from a natural-language query. Returns a map_id you can pass to mutate_map or get_state for follow-up actions. ' +
469
501
  'For the full Op catalog and predicate grammar that downstream tools (mutate_map, filter_layer, etc.) accept, fetch the MCP resource at showmeonmap://docs/workspace-ops-spec — do NOT guess the shapes.',
@@ -503,7 +535,7 @@ export function createServer(opts) {
503
535
  .describe('e.g., "filter to where magnitude is greater than 5", "change basemap to dark"'),
504
536
  rationale: rationaleSchema,
505
537
  };
506
- server.registerTool('mutate_map', {
538
+ registerTool('mutate_map', {
507
539
  title: 'Mutate a map',
508
540
  description: 'Apply a natural-language instruction to an existing workspace. Valid mutations: filter a layer, remove a layer, change camera, change basemap. For ambiguous instructions the tool emits a clarification field. For unrelated instructions it emits outOfScope — build a new map in that case. ' +
509
541
  'For the full Op catalog and predicate grammar, fetch the MCP resource showmeonmap://docs/workspace-ops-spec.',
@@ -540,7 +572,7 @@ export function createServer(opts) {
540
572
  map_id: z.string().min(1),
541
573
  rationale: rationaleSchema,
542
574
  };
543
- server.registerTool('get_state', {
575
+ registerTool('get_state', {
544
576
  title: 'Read map state',
545
577
  description: 'Return the current workspace, map plan, and active filters for a given map_id.',
546
578
  inputSchema: getStateShape,
@@ -627,7 +659,7 @@ export function createServer(opts) {
627
659
  '"comparison" = a layer added to compare against another; ' +
628
660
  '"other" = none of the above.'),
629
661
  };
630
- server.registerTool('add_layer', {
662
+ registerTool('add_layer', {
631
663
  title: 'Add a layer (direct)',
632
664
  description: 'Append a fully-constructed LayerPlan to an existing workspace without going through NL. ' +
633
665
  'Pass the same shape the workspace pipeline emits — { id, title, source, viz, geometry, ... }. ' +
@@ -656,7 +688,7 @@ export function createServer(opts) {
656
688
  new_layer_id: z.string().optional(),
657
689
  rationale: rationaleSchema,
658
690
  };
659
- server.registerTool('add_fusion_layer', {
691
+ registerTool('add_fusion_layer', {
660
692
  title: 'Add bivariate fusion layer',
661
693
  description: 'Spatially join two existing layers and add a new layer that ' +
662
694
  'encodes both values per region as a 3x3 bivariate choropleth. ' +
@@ -707,7 +739,7 @@ export function createServer(opts) {
707
739
  value_field_b: z.string().optional(),
708
740
  rationale: rationaleSchema,
709
741
  };
710
- server.registerTool('correlate_layers', {
742
+ registerTool('correlate_layers', {
711
743
  title: 'Compute correlation between two layers',
712
744
  description: 'Spatially join two existing workspace layers and return the ' +
713
745
  'Pearson correlation coefficient r, sample size n, two-sided ' +
@@ -770,7 +802,7 @@ export function createServer(opts) {
770
802
  '"preview-data" = exploratory peek; ' +
771
803
  '"other" = none of the above.'),
772
804
  };
773
- server.registerTool('filter_layer', {
805
+ registerTool('filter_layer', {
774
806
  title: 'Filter a layer (direct)',
775
807
  description: 'Narrow a layer to features matching a structured predicate. `append:true` (default) conjoins ' +
776
808
  'with any existing filter; `append:false` replaces the chain.',
@@ -808,7 +840,7 @@ export function createServer(opts) {
808
840
  .describe('Default true. false emits a new layer with minimal default styling.'),
809
841
  rationale: rationaleSchema,
810
842
  };
811
- server.registerTool('select_within', {
843
+ registerTool('select_within', {
812
844
  title: 'Subset a layer to a polygon selection',
813
845
  description: 'Create a new layer containing the source layer\'s features that lie within the supplied ' +
814
846
  'polygon. The source layer is untouched. Useful for "show me only the X inside this ' +
@@ -841,7 +873,7 @@ export function createServer(opts) {
841
873
  .describe('Default 100, max 1000. Response sets `truncated:true` when clipped.'),
842
874
  rationale: rationaleSchema,
843
875
  };
844
- server.registerTool('query_features', {
876
+ registerTool('query_features', {
845
877
  title: 'Query features from a layer (read-only)',
846
878
  description: 'Evaluate a predicate against a layer\'s inline features and return the matching subset. ' +
847
879
  'No Op is emitted and the workspace is not mutated. Handy for introspection ("how many X ' +
@@ -889,7 +921,7 @@ export function createServer(opts) {
889
921
  'returns a needs_confirmation envelope. Layer ids are tombstoned — once removed, ' +
890
922
  'the same id cannot be re-added later in this workspace.'),
891
923
  };
892
- server.registerTool('remove_layer', {
924
+ registerTool('remove_layer', {
893
925
  title: 'Remove a layer (direct, destructive)',
894
926
  description: 'Drop a layer from the workspace by id. Strict: removing an already-removed ' +
895
927
  'or never-added layer fails. Layer ids do NOT recycle — a removed id cannot be ' +
@@ -917,7 +949,7 @@ export function createServer(opts) {
917
949
  'returns a needs_confirmation envelope. Wiping a filter chain is recoverable ' +
918
950
  'via undo, but autonomous agents lack a UI surface to confirm intent.'),
919
951
  };
920
- server.registerTool('clear_filters', {
952
+ registerTool('clear_filters', {
921
953
  title: 'Clear all filters on a layer (direct, gated)',
922
954
  description: 'Wipe every predicate currently applied to a layer\'s filter chain, making ' +
923
955
  'every source feature visible again. Inverse of filter_layer. No-op on layers ' +
@@ -939,7 +971,7 @@ export function createServer(opts) {
939
971
  newLayerId: z.string().min(1).describe('New id. Must be unique within the workspace.'),
940
972
  rationale: rationaleSchema,
941
973
  };
942
- server.registerTool('rename_layer', {
974
+ registerTool('rename_layer', {
943
975
  title: 'Rename a layer (direct)',
944
976
  description: 'Change a layer\'s id from oldLayerId to newLayerId. Re-keys any existing filter ' +
945
977
  'chain and preserves insertion order among sibling layers. Strict: newLayerId must ' +
@@ -971,7 +1003,7 @@ export function createServer(opts) {
971
1003
  .describe('0 (transparent) to 1 (opaque).'),
972
1004
  rationale: rationaleSchema,
973
1005
  };
974
- server.registerTool('restyle_layer', {
1006
+ registerTool('restyle_layer', {
975
1007
  title: 'Restyle a layer (direct)',
976
1008
  description: 'Adjust a layer\'s color and/or opacity. Within-palette changes only — the ' +
977
1009
  'renderer silently ignores fields the current viz variant doesn\'t support. ' +
@@ -1029,7 +1061,7 @@ export function createServer(opts) {
1029
1061
  camera: cameraPlanSchema,
1030
1062
  rationale: rationaleSchema,
1031
1063
  };
1032
- server.registerTool('set_camera', {
1064
+ registerTool('set_camera', {
1033
1065
  title: 'Set the map camera (direct)',
1034
1066
  description: 'Override the workspace camera. Latest set_camera op wins during replay. ' +
1035
1067
  'Two modes: `fitBounds` with a [w,s,e,n] bbox (plus optional padding/animateMs), ' +
@@ -1061,7 +1093,7 @@ export function createServer(opts) {
1061
1093
  .describe('How rangeStart/rangeEnd encode time. Default: unix_ms at render time.'),
1062
1094
  rationale: rationaleSchema,
1063
1095
  };
1064
- server.registerTool('set_time', {
1096
+ registerTool('set_time', {
1065
1097
  title: 'Set the time window (direct)',
1066
1098
  description: 'Override the current time-range filter. Latest set_time op wins during replay. ' +
1067
1099
  'Use for "filter to 2020-2024" or "show only the last 7 days" intents. Not yet ' +
@@ -1090,7 +1122,7 @@ export function createServer(opts) {
1090
1122
  .describe('Checkpoint label, e.g. "before EV layer added" or "v1.0".'),
1091
1123
  rationale: rationaleSchema,
1092
1124
  };
1093
- server.registerTool('tag', {
1125
+ registerTool('tag', {
1094
1126
  title: 'Save a named checkpoint (direct)',
1095
1127
  description: 'Label the current workspace state with a user-supplied name. The label is ' +
1096
1128
  'mirrored into workspace.meta.tags so UIs can render a named-revision history. ' +
@@ -1119,7 +1151,7 @@ export function createServer(opts) {
1119
1151
  .describe('Optional CSS color for the marker + label.'),
1120
1152
  rationale: rationaleSchema,
1121
1153
  };
1122
- server.registerTool('add_annotation', {
1154
+ registerTool('add_annotation', {
1123
1155
  title: 'Place an annotation on the map (direct)',
1124
1156
  description: 'Add a user-placed label at a specific coordinate. Annotations are independent of ' +
1125
1157
  'layers and live in their own id namespace. Strict: the id must not already be in ' +
@@ -1146,7 +1178,7 @@ export function createServer(opts) {
1146
1178
  annotationId: z.string().min(1),
1147
1179
  rationale: rationaleSchema,
1148
1180
  };
1149
- server.registerTool('remove_annotation', {
1181
+ registerTool('remove_annotation', {
1150
1182
  title: 'Remove an annotation (direct)',
1151
1183
  description: 'Drop a previously-placed annotation by id. Strict: removing an already-removed ' +
1152
1184
  'or never-placed id fails. After removal, the id CAN be re-used by a later ' +
@@ -1193,7 +1225,7 @@ export function createServer(opts) {
1193
1225
  map_id: z.string().min(1),
1194
1226
  rationale: rationaleSchema,
1195
1227
  };
1196
- server.registerTool('export_workspace', {
1228
+ registerTool('export_workspace', {
1197
1229
  title: 'Export the workspace JSON',
1198
1230
  description: 'Return the current workspace document (ops log + cursor + meta) plus the ' +
1199
1231
  'materialized mapPlan and layerFilters, in a shape suitable for save-to-file / ' +
@@ -1221,7 +1253,7 @@ export function createServer(opts) {
1221
1253
  format: z.enum(['csv', 'geojson']),
1222
1254
  rationale: rationaleSchema,
1223
1255
  };
1224
- server.registerTool('export_layer_data', {
1256
+ registerTool('export_layer_data', {
1225
1257
  title: 'Export layer data',
1226
1258
  description: 'Export a single layer\'s features as a downloadable file. Returns ' +
1227
1259
  'a URL with Content-Disposition: attachment so the user\'s browser ' +
@@ -1242,7 +1274,7 @@ export function createServer(opts) {
1242
1274
  legend: z.boolean().default(true),
1243
1275
  rationale: rationaleSchema,
1244
1276
  };
1245
- server.registerTool('export_image', {
1277
+ registerTool('export_image', {
1246
1278
  title: 'Export map as image',
1247
1279
  description: 'Returns a deep-link URL that opens the map in the user\'s browser ' +
1248
1280
  'and automatically triggers a PNG download. The user clicks the ' +
@@ -1281,7 +1313,7 @@ export function createServer(opts) {
1281
1313
  'returns a needs_confirmation envelope. Autonomous undo without user intent is ' +
1282
1314
  'a footgun — an agent that decides to "fix" something can erase prior work.'),
1283
1315
  };
1284
- server.registerTool('undo', {
1316
+ registerTool('undo', {
1285
1317
  title: 'Undo the most recent op (gated)',
1286
1318
  description: 'Rewind the workspace cursor by one, re-materializing the state as it was before ' +
1287
1319
  'the last mutation. Does NOT delete any ops — redo will fast-forward. Calling ' +
@@ -1306,7 +1338,7 @@ export function createServer(opts) {
1306
1338
  map_id: z.string().min(1),
1307
1339
  rationale: rationaleSchema,
1308
1340
  };
1309
- server.registerTool('redo', {
1341
+ registerTool('redo', {
1310
1342
  title: 'Redo a previously-undone op',
1311
1343
  description: 'Advance the workspace cursor by one toward the tail of the ops log, replaying ' +
1312
1344
  'the next op that was previously undone. Calling redo when the cursor is already ' +
@@ -1365,7 +1397,7 @@ export function createServer(opts) {
1365
1397
  .optional()
1366
1398
  .describe('Optional. A new tool, op, or schema field that would have unblocked you.'),
1367
1399
  };
1368
- server.registerTool('report_blocker', {
1400
+ registerTool('report_blocker', {
1369
1401
  title: 'Report a blocker (agent feedback)',
1370
1402
  description: 'Tell the ShowMeOnMap team your agent hit something it could not accomplish — an ' +
1371
1403
  'unsupported intent, a missing tool, an unclear schema, an op that failed in a ' +
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
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)",
3
+ "version": "0.3.0",
4
+ "description": "MCP server for ShowMeOnMap \u2014 geospatial visualization tools for any MCP-speaking agent (Claude Desktop, Cursor, Zed, ChatGPT Apps SDK)",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
7
7
  "bin": {