@showmeonmap/mcp 0.3.2 → 0.4.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/dist/toolMeta.js +6 -0
- package/dist/tools.js +68 -1
- package/dist/workspaceOpsSpec.md +3 -1
- package/package.json +1 -1
package/dist/toolMeta.js
CHANGED
|
@@ -72,6 +72,12 @@ 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
|
+
},
|
|
75
81
|
query_features: {
|
|
76
82
|
requires_confirmation: false,
|
|
77
83
|
destructive: false,
|
package/dist/tools.js
CHANGED
|
@@ -207,6 +207,8 @@ 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 } : {}),
|
|
210
212
|
};
|
|
211
213
|
}
|
|
212
214
|
function toContent(obj) {
|
|
@@ -240,6 +242,7 @@ const INVALID_INPUT_HINTS = {
|
|
|
240
242
|
add_layer: 'Required: map_id, layerPlan (object), rationale. Optional: intent (enum). No other fields accepted.',
|
|
241
243
|
filter_layer: 'Required: map_id, layerId, predicate (object), rationale. Optional: append (boolean), intent (enum).',
|
|
242
244
|
select_within: 'Required: map_id, sourceLayerId, newLayerId, selection ({type:Polygon|MultiPolygon, coordinates}), rationale. Optional: inheritStyle.',
|
|
245
|
+
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
246
|
query_features: 'Required: map_id, layerId, predicate (object), rationale. Optional: limit (1–1000 integer).',
|
|
244
247
|
remove_layer: 'Required: map_id, layerId, rationale, confirmed:true (destructive — layer ids are tombstoned).',
|
|
245
248
|
clear_filters: 'Required: map_id, layerId, rationale, confirmed:true (sensitive — autonomous filter wipe).',
|
|
@@ -500,7 +503,7 @@ const READ_ONLY_TOOLS = new Set([
|
|
|
500
503
|
]);
|
|
501
504
|
// Tools that reach external/open-world data sources, vs operating purely on
|
|
502
505
|
// already-loaded workspace state.
|
|
503
|
-
const OPEN_WORLD_TOOLS = new Set(['build_map', 'mutate_map', 'add_layer', 'add_fusion_layer']);
|
|
506
|
+
const OPEN_WORLD_TOOLS = new Set(['build_map', 'mutate_map', 'add_layer', 'add_fusion_layer', 'focus_area']);
|
|
504
507
|
// Mutating-but-idempotent setters: re-applying the same call converges to the
|
|
505
508
|
// same state (unlike append-style build_map / add_annotation / tag / undo).
|
|
506
509
|
const IDEMPOTENT_SETTERS = new Set([
|
|
@@ -958,6 +961,70 @@ export function createServer(opts) {
|
|
|
958
961
|
};
|
|
959
962
|
return toContent(await applyOp(map_id, op));
|
|
960
963
|
}));
|
|
964
|
+
const focusAreaAreaRefShape = z.discriminatedUnion('kind', [
|
|
965
|
+
z.object({ kind: z.literal('place'), name: z.string().min(1) }),
|
|
966
|
+
z.object({
|
|
967
|
+
kind: z.literal('corridor'),
|
|
968
|
+
street: z.string().min(1),
|
|
969
|
+
from: z.string().min(1).optional(),
|
|
970
|
+
to: z.string().min(1).optional(),
|
|
971
|
+
bufferMeters: z.number().min(30).max(1000).optional(),
|
|
972
|
+
}),
|
|
973
|
+
z.object({
|
|
974
|
+
kind: z.literal('aroundIntersection'),
|
|
975
|
+
street: z.string().min(1),
|
|
976
|
+
cross: z.string().min(1),
|
|
977
|
+
radiusMeters: z.number().min(50).max(5000).optional(),
|
|
978
|
+
}),
|
|
979
|
+
]);
|
|
980
|
+
const focusAreaShape = {
|
|
981
|
+
map_id: z.string().min(1),
|
|
982
|
+
layerId: z.string().min(1),
|
|
983
|
+
areaRef: focusAreaAreaRefShape,
|
|
984
|
+
mode: z
|
|
985
|
+
.enum(['filter', 'select', 'clip'])
|
|
986
|
+
.optional()
|
|
987
|
+
.describe('filter (default): hide features outside, reversible. select: copy matches into a new layer. clip: new layer with geometry trimmed at the boundary.'),
|
|
988
|
+
rationale: rationaleSchema,
|
|
989
|
+
};
|
|
990
|
+
registerTool('focus_area', {
|
|
991
|
+
title: 'Focus a layer on a named area',
|
|
992
|
+
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.',
|
|
993
|
+
inputSchema: focusAreaShape,
|
|
994
|
+
}, withValidation('focus_area', focusAreaShape, async ({ map_id, layerId, areaRef, mode, rationale }) => {
|
|
995
|
+
const entry = await store.get(map_id);
|
|
996
|
+
if (!entry)
|
|
997
|
+
throw new Error(`Unknown map_id "${map_id}". Call build_map first.`);
|
|
998
|
+
logRationale('focus_area', map_id, rationale, { opKind: `focus_area:${areaRef.kind ?? 'unknown'}:${mode ?? 'filter'}` });
|
|
999
|
+
const resp = (await apiFetch(baseUrl, apiKey, '/api/workspace/focus-area', {
|
|
1000
|
+
workspace: entry.workspace,
|
|
1001
|
+
layerId,
|
|
1002
|
+
areaRef,
|
|
1003
|
+
...(mode ? { mode } : {}),
|
|
1004
|
+
rationale,
|
|
1005
|
+
}));
|
|
1006
|
+
if (!resp.ok) {
|
|
1007
|
+
throw new Error(`focus_area failed: ${resp.error?.kind ?? 'unknown'} — ${resp.error?.message ?? 'no detail'}`);
|
|
1008
|
+
}
|
|
1009
|
+
// Honest resolver failure: nothing changed; surface the clarification.
|
|
1010
|
+
if (resp.mutated === false) {
|
|
1011
|
+
return toContent(envelope(baseUrl, map_id, entry.workspace, entry.mapPlan, undefined, { ...(resp.clarification ? { clarification: resp.clarification } : {}) }));
|
|
1012
|
+
}
|
|
1013
|
+
await store.put(map_id, {
|
|
1014
|
+
workspace: resp.workspace ?? entry.workspace,
|
|
1015
|
+
mapPlan: resp.mapPlan ?? entry.mapPlan,
|
|
1016
|
+
layerFilters: resp.layerFilters ?? {},
|
|
1017
|
+
createdAt: entry.createdAt,
|
|
1018
|
+
lastAccessed: Date.now(),
|
|
1019
|
+
});
|
|
1020
|
+
const focusedPlan = (resp.mapPlan ?? entry.mapPlan);
|
|
1021
|
+
const shared = await persistShareableMap(baseUrl, apiKey, map_id, focusedPlan, rationale);
|
|
1022
|
+
return toContent(envelope(baseUrl, map_id, (resp.workspace ?? entry.workspace), focusedPlan, undefined, {
|
|
1023
|
+
...(resp.opsApplied ? { opsApplied: resp.opsApplied } : {}),
|
|
1024
|
+
...(resp.contextAnswer ? { contextAnswer: resp.contextAnswer } : {}),
|
|
1025
|
+
...(shared ? {} : { shareUnpersisted: true }),
|
|
1026
|
+
}));
|
|
1027
|
+
}));
|
|
961
1028
|
const queryFeaturesShape = {
|
|
962
1029
|
map_id: z.string().min(1),
|
|
963
1030
|
layerId: z.string().min(1),
|
package/dist/workspaceOpsSpec.md
CHANGED
|
@@ -57,6 +57,7 @@ Every op carries common fields:
|
|
|
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
|
+
"version": "0.4.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",
|