@showmeonmap/mcp 0.4.0 → 0.6.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 +10 -3
- package/dist/storyDraft.js +106 -0
- package/dist/toolMeta.js +6 -0
- package/dist/tools.js +132 -5
- package/dist/workspaceOpsSpec.md +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -90,10 +90,17 @@ Ask Claude Desktop things like:
|
|
|
90
90
|
|
|
91
91
|
---
|
|
92
92
|
|
|
93
|
-
## Tool surface (
|
|
93
|
+
## Tool surface (26 tools)
|
|
94
94
|
|
|
95
|
-
**
|
|
96
|
-
**
|
|
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; knobs below 0.6.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. Two opt-in knobs (0.6.0): `annotate: true` places ONE callout annotation at the analysis's headline feature (hottest cluster / top zone / highest joined value — label text only from computed values; no headline → no annotation), and `story: true` persists the shared map with a 2-step story rail (the grounded `contextAnswer` as overview + the memo as an analysis step that isolates the result layer at its real extent) so the `/m/` link opens as a guided read.
|
|
97
104
|
|
|
98
105
|
Every tool returns a **three-channel envelope**:
|
|
99
106
|
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP-side story drafting — the "MCP export knob" deferred from PR-C3
|
|
3
|
+
* (spec 2026-08-15 §PR-C3, ledger 11; shipped 0.6.0).
|
|
4
|
+
*
|
|
5
|
+
* `run_analysis({ story: true })` persists the shared map with a
|
|
6
|
+
* 2-step story: an overview step carrying the grounded one-line
|
|
7
|
+
* contextAnswer, and an analysis step carrying the grounded memo,
|
|
8
|
+
* isolating the result layer and framing its REAL extent. The same two
|
|
9
|
+
* iron rules as the web drafter (src/lib/share/story.ts — this package
|
|
10
|
+
* is self-contained, so the invariants are restated, not imported):
|
|
11
|
+
*
|
|
12
|
+
* 1. GROUNDED TEXT ONLY — bodies are the contextAnswer and memo the
|
|
13
|
+
* analysis just computed, verbatim (markdown chrome stripped, long
|
|
14
|
+
* memos clamped at a whitespace boundary — a verbatim prefix, never
|
|
15
|
+
* a rewrite). Missing either text → no story (a 1-step story is
|
|
16
|
+
* worse than none; /m/'s render guard rejects < 2 steps anyway).
|
|
17
|
+
* 2. NEVER INVENT GEOMETRY — the analysis step's camera is the bbox of
|
|
18
|
+
* the result layer's own features; no features → no camera.
|
|
19
|
+
*/
|
|
20
|
+
/** /m/'s isValidStory caps bodies at 4000 chars — clamp the memo to a
|
|
21
|
+
* verbatim prefix at a whitespace boundary rather than get rejected. */
|
|
22
|
+
const MAX_BODY = 4000;
|
|
23
|
+
/** Strip markdown chrome (headings, bold/italic markers, backticks) —
|
|
24
|
+
* the story rail renders plain text; the words are untouched. */
|
|
25
|
+
export function markdownToPlain(md) {
|
|
26
|
+
return md
|
|
27
|
+
.replace(/^#{1,6}\s+/gm, '')
|
|
28
|
+
.replace(/\*\*([^*]+)\*\*/g, '$1')
|
|
29
|
+
.replace(/\*([^*]+)\*/g, '$1')
|
|
30
|
+
.replace(/`([^`]+)`/g, '$1')
|
|
31
|
+
.replace(/\n{3,}/g, '\n\n')
|
|
32
|
+
.trim();
|
|
33
|
+
}
|
|
34
|
+
function clampBody(text) {
|
|
35
|
+
if (text.length <= MAX_BODY)
|
|
36
|
+
return text;
|
|
37
|
+
const cut = text.lastIndexOf(' ', MAX_BODY - 2);
|
|
38
|
+
return `${text.slice(0, cut > MAX_BODY / 2 ? cut : MAX_BODY - 2).trimEnd()}…`;
|
|
39
|
+
}
|
|
40
|
+
function bboxOfFeatures(features) {
|
|
41
|
+
let w = Infinity, s = Infinity, e = -Infinity, n = -Infinity;
|
|
42
|
+
let seen = false;
|
|
43
|
+
const walk = (g) => {
|
|
44
|
+
if (!Array.isArray(g))
|
|
45
|
+
return;
|
|
46
|
+
if (g.length >= 2 && typeof g[0] === 'number' && typeof g[1] === 'number') {
|
|
47
|
+
if (Number.isFinite(g[0]) && Number.isFinite(g[1])) {
|
|
48
|
+
seen = true;
|
|
49
|
+
if (g[0] < w)
|
|
50
|
+
w = g[0];
|
|
51
|
+
if (g[0] > e)
|
|
52
|
+
e = g[0];
|
|
53
|
+
if (g[1] < s)
|
|
54
|
+
s = g[1];
|
|
55
|
+
if (g[1] > n)
|
|
56
|
+
n = g[1];
|
|
57
|
+
}
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
for (const child of g)
|
|
61
|
+
walk(child);
|
|
62
|
+
};
|
|
63
|
+
for (const f of features) {
|
|
64
|
+
walk(f?.geometry?.coordinates);
|
|
65
|
+
}
|
|
66
|
+
if (!seen)
|
|
67
|
+
return null;
|
|
68
|
+
if (w === e) {
|
|
69
|
+
w -= 0.05;
|
|
70
|
+
e += 0.05;
|
|
71
|
+
}
|
|
72
|
+
if (s === n) {
|
|
73
|
+
s -= 0.05;
|
|
74
|
+
n += 0.05;
|
|
75
|
+
}
|
|
76
|
+
return [w, s, e, n];
|
|
77
|
+
}
|
|
78
|
+
/** Null when the run lacks the grounded material for BOTH steps. */
|
|
79
|
+
export function buildAnalysisStory(input) {
|
|
80
|
+
const overviewBody = input.contextAnswer?.trim();
|
|
81
|
+
const memoBody = input.memo ? markdownToPlain(input.memo) : '';
|
|
82
|
+
if (!overviewBody || !memoBody)
|
|
83
|
+
return null;
|
|
84
|
+
const layers = Array.isArray(input.plan?.layers) ? input.plan.layers : [];
|
|
85
|
+
const resultLayer = input.resultLayerId
|
|
86
|
+
? layers.find((l) => String(l?.id) === input.resultLayerId)
|
|
87
|
+
: undefined;
|
|
88
|
+
const bounds = resultLayer?.source?.kind === 'geojson' && Array.isArray(resultLayer.source.data?.features)
|
|
89
|
+
? bboxOfFeatures(resultLayer.source.data.features)
|
|
90
|
+
: null;
|
|
91
|
+
return {
|
|
92
|
+
version: 1,
|
|
93
|
+
steps: [
|
|
94
|
+
{
|
|
95
|
+
title: input.plan?.title?.trim() || 'Overview',
|
|
96
|
+
body: clampBody(overviewBody),
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
title: resultLayer?.title?.trim() || 'Analysis',
|
|
100
|
+
body: clampBody(memoBody),
|
|
101
|
+
...(bounds ? { camera: { bounds } } : {}),
|
|
102
|
+
...(input.resultLayerId ? { visibleLayerIds: [input.resultLayerId] } : {}),
|
|
103
|
+
},
|
|
104
|
+
],
|
|
105
|
+
};
|
|
106
|
+
}
|
package/dist/toolMeta.js
CHANGED
|
@@ -78,6 +78,12 @@ export const MCP_TOOL_META = {
|
|
|
78
78
|
long_running: false,
|
|
79
79
|
rationale: 'Filters or derives a layer to a server-resolved named area; reversible via undo/clear_filters.',
|
|
80
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
|
+
},
|
|
81
87
|
query_features: {
|
|
82
88
|
requires_confirmation: false,
|
|
83
89
|
destructive: false,
|
package/dist/tools.js
CHANGED
|
@@ -22,6 +22,7 @@ import { readFileSync, mkdirSync, appendFileSync } from 'node:fs';
|
|
|
22
22
|
import { fileURLToPath } from 'node:url';
|
|
23
23
|
import { dirname, resolve } from 'node:path';
|
|
24
24
|
import { MCP_TOOL_META, mcpRequiresConfirmation } from "./toolMeta.js";
|
|
25
|
+
import { buildAnalysisStory } from "./storyDraft.js";
|
|
25
26
|
// -----------------------------------------------------------------------------
|
|
26
27
|
// Agent-facing spec resource (Teddy Riker, "Designing for Agents" §2,
|
|
27
28
|
// 2026-04-23). Loaded once at module init from a sibling .md file. Tool
|
|
@@ -209,6 +210,10 @@ function envelope(baseUrl, mapId, workspace, mapPlan, nextActionStrings, extras
|
|
|
209
210
|
...(extras.clarification ? { clarification: extras.clarification } : {}),
|
|
210
211
|
...(extras.opsApplied ? { opsApplied: extras.opsApplied } : {}),
|
|
211
212
|
...(extras.contextAnswer ? { contextAnswer: extras.contextAnswer } : {}),
|
|
213
|
+
...(extras.resultLayerId ? { resultLayerId: extras.resultLayerId } : {}),
|
|
214
|
+
...(extras.memo ? { memo: extras.memo } : {}),
|
|
215
|
+
...(extras.stats ? { stats: extras.stats } : {}),
|
|
216
|
+
...(extras.note ? { note: extras.note } : {}),
|
|
212
217
|
};
|
|
213
218
|
}
|
|
214
219
|
function toContent(obj) {
|
|
@@ -260,7 +265,7 @@ const INVALID_INPUT_HINTS = {
|
|
|
260
265
|
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.',
|
|
261
266
|
undo: 'Required: map_id, rationale, confirmed:true (sensitive — autonomous undo can erase prior work).',
|
|
262
267
|
redo: 'Required: map_id, rationale.',
|
|
263
|
-
report_blocker: 'Required: what_i_was_trying, what_i_tried, where_i_got_stuck. Optional: map_id, suggested_capability.',
|
|
268
|
+
report_blocker: 'Required: what_i_was_trying, what_i_tried, where_i_got_stuck, rationale. Optional: map_id, suggested_capability.',
|
|
264
269
|
};
|
|
265
270
|
function buildInvalidInputResponse(toolName, error) {
|
|
266
271
|
const issues = error.issues.map((iss) => ({
|
|
@@ -452,7 +457,7 @@ export function compactFeatureCollection(fc, viz) {
|
|
|
452
457
|
}));
|
|
453
458
|
return { type: fc.type ?? 'FeatureCollection', features };
|
|
454
459
|
}
|
|
455
|
-
async function persistShareableMap(baseUrl, apiKey, mapId, mapPlan, query) {
|
|
460
|
+
async function persistShareableMap(baseUrl, apiKey, mapId, mapPlan, query, story) {
|
|
456
461
|
if (!mapPlan || !Array.isArray(mapPlan.layers))
|
|
457
462
|
return false;
|
|
458
463
|
const geojsonLayers = {};
|
|
@@ -469,7 +474,7 @@ async function persistShareableMap(baseUrl, apiKey, mapId, mapPlan, query) {
|
|
|
469
474
|
id: mapId,
|
|
470
475
|
title,
|
|
471
476
|
query,
|
|
472
|
-
blob: { mapPlan: { ...mapPlan, layers: strippedLayers }, geojsonLayers, query, version: 1 },
|
|
477
|
+
blob: { mapPlan: { ...mapPlan, layers: strippedLayers }, geojsonLayers, query, version: 1, ...(story ? { story } : {}) },
|
|
473
478
|
isPublic: true,
|
|
474
479
|
};
|
|
475
480
|
// Two attempts: the second exists for exactly the failure that lost a map
|
|
@@ -1025,6 +1030,126 @@ export function createServer(opts) {
|
|
|
1025
1030
|
...(shared ? {} : { shareUnpersisted: true }),
|
|
1026
1031
|
}));
|
|
1027
1032
|
}));
|
|
1033
|
+
const runAnalysisShape = {
|
|
1034
|
+
map_id: z.string().min(1),
|
|
1035
|
+
analysis: z
|
|
1036
|
+
.enum(['hotspot', 'zonal', 'enrich'])
|
|
1037
|
+
.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)."),
|
|
1038
|
+
layerId: z.string().min(1).describe('The workspace layer to analyze (geojson-backed; the point source for zonal).'),
|
|
1039
|
+
valueField: z
|
|
1040
|
+
.string()
|
|
1041
|
+
.min(1)
|
|
1042
|
+
.optional()
|
|
1043
|
+
.describe('Numeric property to analyze. hotspot: omit on point layers for incident-DENSITY mode (hex-binned counts). zonal: omit for count-only aggregation.'),
|
|
1044
|
+
k: z.number().int().min(3).max(24).optional().describe('hotspot only: nearest neighbors per feature (default 8).'),
|
|
1045
|
+
hexMeters: z
|
|
1046
|
+
.number()
|
|
1047
|
+
.min(100)
|
|
1048
|
+
.max(50_000)
|
|
1049
|
+
.optional()
|
|
1050
|
+
.describe('hotspot density mode only: hex cell spacing in meters (default auto from the data extent).'),
|
|
1051
|
+
zoneLayerId: z
|
|
1052
|
+
.string()
|
|
1053
|
+
.min(1)
|
|
1054
|
+
.optional()
|
|
1055
|
+
.describe('zonal only: an existing polygon layer to aggregate into. Provide EXACTLY ONE of zoneLayerId or zones.'),
|
|
1056
|
+
zones: z
|
|
1057
|
+
.object({ kind: z.literal('h3'), resolution: z.number().int().min(3).max(10).optional() })
|
|
1058
|
+
.strict()
|
|
1059
|
+
.optional()
|
|
1060
|
+
.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.'),
|
|
1061
|
+
stats: z
|
|
1062
|
+
.array(z.enum(['count', 'sum', 'mean', 'min', 'max']))
|
|
1063
|
+
.min(1)
|
|
1064
|
+
.max(5)
|
|
1065
|
+
.optional()
|
|
1066
|
+
.describe('zonal only: which statistics to attach per zone (default: count, plus sum/mean/min/max when valueField is set).'),
|
|
1067
|
+
indicator: z
|
|
1068
|
+
.string()
|
|
1069
|
+
.min(1)
|
|
1070
|
+
.max(80)
|
|
1071
|
+
.optional()
|
|
1072
|
+
.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')."),
|
|
1073
|
+
geoLevel: z
|
|
1074
|
+
.enum(['state', 'county', 'tract'])
|
|
1075
|
+
.optional()
|
|
1076
|
+
.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.'),
|
|
1077
|
+
annotate: z
|
|
1078
|
+
.boolean()
|
|
1079
|
+
.optional()
|
|
1080
|
+
.describe('Opt-in: place ONE callout annotation at the headline feature (hottest cluster / top zone / highest value). Label text comes only from computed values.'),
|
|
1081
|
+
story: z
|
|
1082
|
+
.boolean()
|
|
1083
|
+
.optional()
|
|
1084
|
+
.describe('Opt-in: persist the shared map with a 2-step story rail (grounded overview + analysis memo isolating the result layer) — the /m/ link opens with it.'),
|
|
1085
|
+
rationale: rationaleSchema,
|
|
1086
|
+
};
|
|
1087
|
+
registerTool('run_analysis', {
|
|
1088
|
+
title: 'Run a spatial analysis on a layer',
|
|
1089
|
+
description: 'Compute a spatial analysis server-side and append the finished, honestly-legended result ' +
|
|
1090
|
+
'as a new layer + camera; the result REPLACES its input layer(s) (undoable). hotspot = ' +
|
|
1091
|
+
'Getis-Ord Gi*: hot/cold significance classes (99/95/90%, Benjamini-Hochberg FDR-corrected) ' +
|
|
1092
|
+
'on a numeric field, or on hex-binned point density when valueField is omitted. zonal = ' +
|
|
1093
|
+
'aggregate points into polygon zones (an existing zone layer OR a generated h3 fishnet): ' +
|
|
1094
|
+
'count/sum/mean/min/max per zone as a quantile choropleth where zero and no-data are ' +
|
|
1095
|
+
'distinct classes. enrich = join a US Census ACS indicator (median income, home value, ' +
|
|
1096
|
+
'rent, …) onto each feature by tract/county/state containment — unmatched features stay ' +
|
|
1097
|
+
'null, never zero-filled. Returns a grounded memo + stats payload — every number is computed, ' +
|
|
1098
|
+
'never generated; findings keep the map. Costs 1 credit when the result lands; typed ' +
|
|
1099
|
+
'failures (unknown layer, too few features, >10,000-feature cap) are refunded.',
|
|
1100
|
+
inputSchema: runAnalysisShape,
|
|
1101
|
+
}, withValidation('run_analysis', runAnalysisShape, async ({ map_id, analysis, layerId, valueField, k, hexMeters, zoneLayerId, zones, stats, indicator, geoLevel, annotate, story, rationale }) => {
|
|
1102
|
+
const entry = await store.get(map_id);
|
|
1103
|
+
if (!entry)
|
|
1104
|
+
throw new Error(`Unknown map_id "${map_id}". Call build_map first.`);
|
|
1105
|
+
logRationale('run_analysis', map_id, rationale, { opKind: `run_analysis:${analysis}` });
|
|
1106
|
+
const resp = (await apiFetch(baseUrl, apiKey, '/api/workspace/run-analysis', {
|
|
1107
|
+
workspace: entry.workspace,
|
|
1108
|
+
analysis,
|
|
1109
|
+
layerId,
|
|
1110
|
+
...(valueField !== undefined ? { valueField } : {}),
|
|
1111
|
+
...(k !== undefined ? { k } : {}),
|
|
1112
|
+
...(hexMeters !== undefined ? { hexMeters } : {}),
|
|
1113
|
+
...(zoneLayerId !== undefined ? { zoneLayerId } : {}),
|
|
1114
|
+
...(zones !== undefined ? { zones } : {}),
|
|
1115
|
+
...(stats !== undefined ? { stats } : {}),
|
|
1116
|
+
...(indicator !== undefined ? { indicator } : {}),
|
|
1117
|
+
...(geoLevel !== undefined ? { geoLevel } : {}),
|
|
1118
|
+
...(annotate !== undefined ? { annotate } : {}),
|
|
1119
|
+
rationale,
|
|
1120
|
+
}));
|
|
1121
|
+
if (!resp.ok) {
|
|
1122
|
+
throw new Error(`run_analysis failed: ${resp.error?.kind ?? 'unknown'} — ${resp.error?.message ?? 'no detail'}`);
|
|
1123
|
+
}
|
|
1124
|
+
await store.put(map_id, {
|
|
1125
|
+
workspace: resp.workspace ?? entry.workspace,
|
|
1126
|
+
mapPlan: resp.mapPlan ?? entry.mapPlan,
|
|
1127
|
+
layerFilters: resp.layerFilters ?? {},
|
|
1128
|
+
createdAt: entry.createdAt,
|
|
1129
|
+
lastAccessed: Date.now(),
|
|
1130
|
+
});
|
|
1131
|
+
const analyzedPlan = (resp.mapPlan ?? entry.mapPlan);
|
|
1132
|
+
// Story knob (0.6.0): draft from the just-computed grounded material,
|
|
1133
|
+
// BEFORE persist strips inline data (the bbox needs the features).
|
|
1134
|
+
const storyDraft = story === true
|
|
1135
|
+
? buildAnalysisStory({
|
|
1136
|
+
plan: analyzedPlan,
|
|
1137
|
+
contextAnswer: resp.contextAnswer,
|
|
1138
|
+
memo: resp.memo,
|
|
1139
|
+
resultLayerId: resp.resultLayerId,
|
|
1140
|
+
})
|
|
1141
|
+
: null;
|
|
1142
|
+
const shared = await persistShareableMap(baseUrl, apiKey, map_id, analyzedPlan, rationale, storyDraft);
|
|
1143
|
+
return toContent(envelope(baseUrl, map_id, (resp.workspace ?? entry.workspace), analyzedPlan, undefined, {
|
|
1144
|
+
...(resp.opsApplied ? { opsApplied: resp.opsApplied } : {}),
|
|
1145
|
+
...(resp.contextAnswer ? { contextAnswer: resp.contextAnswer } : {}),
|
|
1146
|
+
...(resp.resultLayerId ? { resultLayerId: resp.resultLayerId } : {}),
|
|
1147
|
+
...(resp.memo ? { memo: resp.memo } : {}),
|
|
1148
|
+
...(resp.stats ? { stats: resp.stats } : {}),
|
|
1149
|
+
...(resp.note ? { note: resp.note } : {}),
|
|
1150
|
+
...(shared ? {} : { shareUnpersisted: true }),
|
|
1151
|
+
}));
|
|
1152
|
+
}));
|
|
1028
1153
|
const queryFeaturesShape = {
|
|
1029
1154
|
map_id: z.string().min(1),
|
|
1030
1155
|
layerId: z.string().min(1),
|
|
@@ -1562,6 +1687,7 @@ export function createServer(opts) {
|
|
|
1562
1687
|
.max(300)
|
|
1563
1688
|
.optional()
|
|
1564
1689
|
.describe('Optional. A new tool, op, or schema field that would have unblocked you.'),
|
|
1690
|
+
rationale: rationaleSchema,
|
|
1565
1691
|
};
|
|
1566
1692
|
registerTool('report_blocker', {
|
|
1567
1693
|
title: 'Report a blocker (agent feedback)',
|
|
@@ -1573,11 +1699,11 @@ export function createServer(opts) {
|
|
|
1573
1699
|
'session, but feeds future capability work. No map_id required — file the report ' +
|
|
1574
1700
|
'even if no map was built.',
|
|
1575
1701
|
inputSchema: reportBlockerShape,
|
|
1576
|
-
}, withValidation('report_blocker', reportBlockerShape, async ({ map_id, what_i_was_trying, what_i_tried, where_i_got_stuck, suggested_capability }) => {
|
|
1702
|
+
}, withValidation('report_blocker', reportBlockerShape, async ({ map_id, what_i_was_trying, what_i_tried, where_i_got_stuck, suggested_capability, rationale }) => {
|
|
1577
1703
|
// Mirror to the rationale NDJSON so the May-1 clustering pass sees a
|
|
1578
1704
|
// unified stream of "what did agents try, where did they get stuck".
|
|
1579
1705
|
// The agent-blockers route persists the full record separately.
|
|
1580
|
-
logRationale('report_blocker', map_id ?? null, `BLOCKER: ${what_i_was_trying} — stuck: ${where_i_got_stuck}`, { opKind: 'reportBlocker' });
|
|
1706
|
+
logRationale('report_blocker', map_id ?? null, `BLOCKER: ${what_i_was_trying} — stuck: ${where_i_got_stuck} (${rationale})`, { opKind: 'reportBlocker' });
|
|
1581
1707
|
const resp = (await apiFetch(baseUrl, apiKey, '/api/agent/report-blocker', {
|
|
1582
1708
|
...(map_id ? { map_id } : {}),
|
|
1583
1709
|
what_i_was_trying,
|
|
@@ -1592,6 +1718,7 @@ export function createServer(opts) {
|
|
|
1592
1718
|
return toContent({
|
|
1593
1719
|
ok: true,
|
|
1594
1720
|
message: `Blocker recorded — thanks. Recorded at ${resp.recorded_at ?? new Date().toISOString()}.`,
|
|
1721
|
+
...(resp.sink ? { sink: resp.sink } : {}),
|
|
1595
1722
|
});
|
|
1596
1723
|
}));
|
|
1597
1724
|
// ── JSON Schema draft normalization (2026-07-16) ─────────────────────────
|
package/dist/workspaceOpsSpec.md
CHANGED
|
@@ -50,7 +50,7 @@ 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. |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@showmeonmap/mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.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",
|