bbox-mcp-server 1.2.3 → 1.2.5
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 +1 -1
- package/dist/index.js +154 -38
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@ The geospatial toolkit for AI agents. **6 tools, zero config** — give any LLM
|
|
|
4
4
|
|
|
5
5
|
Every response includes a verification link to **[vibhorsingh.com/boundingbox](https://vibhorsingh.com/boundingbox/)** so you can visually confirm results on an interactive map.
|
|
6
6
|
|
|
7
|
-
[](https://nodejs.org/) [](./LICENSE)
|
|
7
|
+
[](https://nodejs.org/) [](./LICENSE) [](https://lobehub.com/mcp/iamvibhorsingh-bbox-mcp-server)
|
|
8
8
|
|
|
9
9
|
---
|
|
10
10
|
|
package/dist/index.js
CHANGED
|
@@ -7,7 +7,7 @@ import proj4 from "proj4";
|
|
|
7
7
|
import proj4defs from "./proj4defs.json" with { type: "json" };
|
|
8
8
|
import { wktToGeoJSON as parseWKT } from "@terraformer/wkt";
|
|
9
9
|
const SERVER_NAME = "bbox-mcp-server";
|
|
10
|
-
const SERVER_VERSION = "1.2.
|
|
10
|
+
const SERVER_VERSION = "1.2.5";
|
|
11
11
|
const MAX_H3_CELLS = process.env.MAX_H3_CELLS ? parseInt(process.env.MAX_H3_CELLS, 10) : 50000;
|
|
12
12
|
const MAPBOX_ACCESS_TOKEN = process.env.MAPBOX_ACCESS_TOKEN;
|
|
13
13
|
const MAX_INPUT_LENGTH = 1_000_000; // 1 MB input size guard
|
|
@@ -131,7 +131,9 @@ function parseBBox(input) {
|
|
|
131
131
|
return ensureValidBBox(minCoords[1], minCoords[0], maxCoords[1], maxCoords[0]);
|
|
132
132
|
}
|
|
133
133
|
}
|
|
134
|
-
catch (e) {
|
|
134
|
+
catch (e) {
|
|
135
|
+
log('info', 'parseBBox: ogrinfo extent parse failed, trying next parser', { error: e?.message });
|
|
136
|
+
}
|
|
135
137
|
}
|
|
136
138
|
// --- 2. GeoJSON Sniffer ---
|
|
137
139
|
try {
|
|
@@ -148,7 +150,9 @@ function parseBBox(input) {
|
|
|
148
150
|
return ensureValidBBox(safeMin(lats), safeMin(lngs), safeMax(lats), safeMax(lngs));
|
|
149
151
|
}
|
|
150
152
|
}
|
|
151
|
-
catch (e) {
|
|
153
|
+
catch (e) {
|
|
154
|
+
log('info', 'parseBBox: GeoJSON parse failed, trying next parser', { error: e?.message });
|
|
155
|
+
}
|
|
152
156
|
// --- 3. WKT Sniffer ---
|
|
153
157
|
if (input.toUpperCase().includes("POLYGON") || input.toUpperCase().includes("POINT") || input.toUpperCase().includes("LINESTRING")) {
|
|
154
158
|
try {
|
|
@@ -160,7 +164,9 @@ function parseBBox(input) {
|
|
|
160
164
|
return ensureValidBBox(safeMin(lats), safeMin(lngs), safeMax(lats), safeMax(lngs));
|
|
161
165
|
}
|
|
162
166
|
}
|
|
163
|
-
catch (e) {
|
|
167
|
+
catch (e) {
|
|
168
|
+
log('info', 'parseBBox: WKT parse failed, trying next parser', { error: e?.message });
|
|
169
|
+
}
|
|
164
170
|
}
|
|
165
171
|
// --- 4. Standard Coordinate Array Sniffer ---
|
|
166
172
|
// Strip brackets/parentheses and split by commas or spaces
|
|
@@ -316,6 +322,21 @@ function buildShareUrl(bbox) {
|
|
|
316
322
|
const hash = `${bbox.lat1.toFixed(6)},${bbox.lng1.toFixed(6)},${bbox.lat2.toFixed(6)},${bbox.lng2.toFixed(6)}`;
|
|
317
323
|
return `https://vibhorsingh.com/boundingbox/#${hash}`;
|
|
318
324
|
}
|
|
325
|
+
function buildShareUrlWithMarkers(bbox, elements) {
|
|
326
|
+
const bboxPart = `${bbox.lat1.toFixed(6)},${bbox.lng1.toFixed(6)},${bbox.lat2.toFixed(6)},${bbox.lng2.toFixed(6)}`;
|
|
327
|
+
const MAX_MARKERS = 30;
|
|
328
|
+
const markerParts = elements
|
|
329
|
+
.filter(e => e.coordinates?.lat !== undefined && e.coordinates?.lon !== undefined)
|
|
330
|
+
.slice(0, MAX_MARKERS)
|
|
331
|
+
.map(e => {
|
|
332
|
+
let name = e.name || 'Point';
|
|
333
|
+
if (name.length > 30)
|
|
334
|
+
name = name.slice(0, 27) + '...';
|
|
335
|
+
return `${e.coordinates.lat.toFixed(6)},${e.coordinates.lon.toFixed(6)},${encodeURIComponent(name)}`;
|
|
336
|
+
});
|
|
337
|
+
const hashParts = [bboxPart, ...markerParts];
|
|
338
|
+
return `https://vibhorsingh.com/boundingbox/#${hashParts.join('|')}`;
|
|
339
|
+
}
|
|
319
340
|
function getH3Cells(bbox, resolution, compact) {
|
|
320
341
|
if (resolution < 0 || resolution > 15 || !Number.isInteger(resolution)) {
|
|
321
342
|
throw new Error(`Invalid H3 resolution: ${resolution}. Must be an integer between 0 and 15.`);
|
|
@@ -428,6 +449,54 @@ async function geocodeLocation(query) {
|
|
|
428
449
|
clearTimeout(timeout);
|
|
429
450
|
}
|
|
430
451
|
}
|
|
452
|
+
async function geocodeLocationPoint(query) {
|
|
453
|
+
if (!MAPBOX_ACCESS_TOKEN) {
|
|
454
|
+
throw new Error('Mapbox API token not configured. Set MAPBOX_ACCESS_TOKEN to use location search. Provide explicit coordinates via bbox instead.');
|
|
455
|
+
}
|
|
456
|
+
const controller = new AbortController();
|
|
457
|
+
const timeout = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
|
|
458
|
+
try {
|
|
459
|
+
const url = `https://api.mapbox.com/geocoding/v5/mapbox.places/${encodeURIComponent(query)}.json?access_token=${MAPBOX_ACCESS_TOKEN}&limit=1`;
|
|
460
|
+
const response = await fetch(url, { signal: controller.signal });
|
|
461
|
+
if (!response.ok)
|
|
462
|
+
throw new Error(`Geocoding failed: ${response.status} ${response.statusText}`);
|
|
463
|
+
const data = await response.json();
|
|
464
|
+
if (!data?.features?.length)
|
|
465
|
+
throw new Error(`No results found for location: ${query}`);
|
|
466
|
+
const feature = data.features[0];
|
|
467
|
+
if (feature.center) {
|
|
468
|
+
return { lat: feature.center[1], lng: feature.center[0] };
|
|
469
|
+
}
|
|
470
|
+
else if (feature.bbox) {
|
|
471
|
+
return { lat: (feature.bbox[1] + feature.bbox[3]) / 2, lng: (feature.bbox[0] + feature.bbox[2]) / 2 };
|
|
472
|
+
}
|
|
473
|
+
else {
|
|
474
|
+
throw new Error(`Unable to determine center point for location: ${query}`);
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
catch (err) {
|
|
478
|
+
if (err.name === 'AbortError')
|
|
479
|
+
throw new Error(`Geocoding timed out after ${FETCH_TIMEOUT_MS / 1000}s for: ${query}`);
|
|
480
|
+
throw err;
|
|
481
|
+
}
|
|
482
|
+
finally {
|
|
483
|
+
clearTimeout(timeout);
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
function buildShareUrlCircleWithMarkers(center, radiusMeters, elements) {
|
|
487
|
+
const circlePart = `C:${center.lat.toFixed(6)},${center.lng.toFixed(6)},${radiusMeters.toFixed(2)}`;
|
|
488
|
+
const MAX_MARKERS = 30;
|
|
489
|
+
const markerParts = elements
|
|
490
|
+
.filter(e => e.coordinates?.lat !== undefined && e.coordinates?.lon !== undefined)
|
|
491
|
+
.slice(0, MAX_MARKERS)
|
|
492
|
+
.map(e => {
|
|
493
|
+
let name = e.name || 'Point';
|
|
494
|
+
if (name.length > 30)
|
|
495
|
+
name = name.slice(0, 27) + '...';
|
|
496
|
+
return `${e.coordinates.lat.toFixed(6)},${e.coordinates.lon.toFixed(6)},${encodeURIComponent(name)}`;
|
|
497
|
+
});
|
|
498
|
+
return `https://vibhorsingh.com/boundingbox/#${[circlePart, ...markerParts].join('|')}`;
|
|
499
|
+
}
|
|
431
500
|
// ---------------------------------------------------------------------------
|
|
432
501
|
// Tool Handlers
|
|
433
502
|
// ---------------------------------------------------------------------------
|
|
@@ -527,13 +596,13 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
527
596
|
},
|
|
528
597
|
{
|
|
529
598
|
name: "search_overpass",
|
|
530
|
-
description: "Execute an Overpass QL query to find POIs, roads, or other OSM features within a bounding box
|
|
599
|
+
description: "Execute an Overpass QL query to find POIs, roads, or other OSM features within a bounding box or radius. The server wraps your query in the appropriate spatial filter and returns structured JSON with a map link.\n\nSEARCH MODES:\n- Bounding box (default): searches a rectangular area. Use for region/area queries ('hospitals in Manhattan', 'parks in downtown Seattle').\n- Circle: searches within an exact radius. Use ONLY when the user specifies an explicit distance ('within 2km of JFK', '500 metres from the Eiffel Tower', '1 mile from Times Square'). Omit radius_meters for vague terms like 'near' or 'close to'.\n\nCOMMON TAG EXAMPLES:\n- Restaurants: `nwr[\"amenity\"=\"restaurant\"]`\n- Pizza: `nwr[\"amenity\"=\"fast_food\"][\"cuisine\"=\"pizza\"]`\n- Supermarkets: `nwr[\"shop\"=\"supermarket\"]`\n- Parks: `nwr[\"leisure\"=\"park\"]`\n- Hospitals: `nwr[\"amenity\"=\"hospital\"]`\n- Schools: `nwr[\"amenity\"=\"school\"]`\n- Parking: `nwr[\"amenity\"=\"parking\"]`\n- Highways/Roads: `way[\"highway\"]`",
|
|
531
600
|
inputSchema: {
|
|
532
601
|
type: "object",
|
|
533
602
|
properties: {
|
|
534
603
|
location: {
|
|
535
604
|
type: "string",
|
|
536
|
-
description: "A text location to search for (e.g. 'San Francisco'). Requires MAPBOX_ACCESS_TOKEN env var. Either 'location' or 'bbox' MUST be provided."
|
|
605
|
+
description: "A text location to search for (e.g. 'San Francisco', 'JFK Airport'). Requires MAPBOX_ACCESS_TOKEN env var. Either 'location' or 'bbox' MUST be provided."
|
|
537
606
|
},
|
|
538
607
|
bbox: {
|
|
539
608
|
type: "string",
|
|
@@ -541,11 +610,15 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
541
610
|
},
|
|
542
611
|
query: {
|
|
543
612
|
type: "string",
|
|
544
|
-
description: "The Overpass QL core query. Example: `node[\"amenity\"=\"cafe\"]` or `nwr[\"leisure\"=\"park\"]`. DO NOT include
|
|
613
|
+
description: "The Overpass QL core query. Example: `node[\"amenity\"=\"cafe\"]` or `nwr[\"leisure\"=\"park\"]`. DO NOT include spatial filters, bounding box, or output directives — the server handles those automatically."
|
|
614
|
+
},
|
|
615
|
+
radius_meters: {
|
|
616
|
+
type: "number",
|
|
617
|
+
description: "Search radius in metres. Enables circle mode — use ONLY when the user specifies an explicit distance (e.g. '2km' → 2000, '500 metres' → 500, '1 mile' → 1609). Omit entirely for area/region queries or vague terms like 'near'."
|
|
545
618
|
},
|
|
546
619
|
limit: {
|
|
547
620
|
type: "number",
|
|
548
|
-
description: "Maximum number of elements to return.
|
|
621
|
+
description: "Maximum number of elements to return. Default is 100. Increase if you need more results."
|
|
549
622
|
}
|
|
550
623
|
},
|
|
551
624
|
required: ["query"],
|
|
@@ -731,7 +804,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
731
804
|
content: [
|
|
732
805
|
{
|
|
733
806
|
type: "text",
|
|
734
|
-
text: `H3
|
|
807
|
+
text: `H3 cells generated: ${cells.length.toLocaleString()}\nResolution: ${resolution} | Compacted: ${compact}\n\nCells:\n${JSON.stringify(cells)}\n\nView on map: ${shareUrl}`
|
|
735
808
|
},
|
|
736
809
|
{
|
|
737
810
|
type: "text",
|
|
@@ -753,7 +826,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
753
826
|
content: [
|
|
754
827
|
{
|
|
755
828
|
type: "text",
|
|
756
|
-
text: `View on map: ${shareUrl}
|
|
829
|
+
text: `View on map: ${shareUrl}`
|
|
757
830
|
},
|
|
758
831
|
{
|
|
759
832
|
type: "text",
|
|
@@ -769,6 +842,8 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
769
842
|
const args = request.params.arguments || {};
|
|
770
843
|
const queryRaw = args.query;
|
|
771
844
|
const limit = args.limit || 100;
|
|
845
|
+
const radiusMeters = args.radius_meters;
|
|
846
|
+
const useCircle = typeof radiusMeters === 'number' && radiusMeters > 0;
|
|
772
847
|
if (!queryRaw) {
|
|
773
848
|
return { content: [{ type: "text", text: "Error: 'query' is required (e.g. `node[\"amenity\"=\"restaurant\"]`)." }], isError: true };
|
|
774
849
|
}
|
|
@@ -776,29 +851,65 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
776
851
|
return { content: [{ type: "text", text: "Error: Query is too long (maximum 2000 characters)." }], isError: true };
|
|
777
852
|
}
|
|
778
853
|
if (queryRaw.includes("[out:") || queryRaw.includes("[timeout:") || queryRaw.includes("[bbox:")) {
|
|
779
|
-
return { content: [{ type: "text", text: "Error: Query must not contain
|
|
854
|
+
return { content: [{ type: "text", text: "Error: Query must not contain [out:], [timeout:], or [bbox:] directives. The server handles these automatically." }], isError: true };
|
|
780
855
|
}
|
|
856
|
+
let overpassQuery;
|
|
857
|
+
let searchModeLabel;
|
|
858
|
+
let centerPoint;
|
|
781
859
|
let bboxObj;
|
|
782
|
-
if (
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
860
|
+
if (useCircle) {
|
|
861
|
+
if (args.location) {
|
|
862
|
+
try {
|
|
863
|
+
centerPoint = await geocodeLocationPoint(args.location);
|
|
864
|
+
}
|
|
865
|
+
catch (err) {
|
|
866
|
+
if (args.bbox) {
|
|
867
|
+
log('warn', `Geocoding failed for radius search, using bbox center`, { error: err.message });
|
|
868
|
+
const fb = parseBBox(args.bbox);
|
|
869
|
+
centerPoint = { lat: (fb.lat1 + fb.lat2) / 2, lng: (fb.lng1 + fb.lng2) / 2 };
|
|
870
|
+
}
|
|
871
|
+
else {
|
|
872
|
+
log('error', `Geocoding failed for radius search: ${args.location}`, { error: err.message });
|
|
873
|
+
return { content: [{ type: "text", text: `Error: ${err.message}` }], isError: true };
|
|
874
|
+
}
|
|
790
875
|
}
|
|
791
|
-
bboxObj = parseBBox(args.bbox);
|
|
792
876
|
}
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
877
|
+
else if (args.bbox) {
|
|
878
|
+
const fb = parseBBox(args.bbox);
|
|
879
|
+
centerPoint = { lat: (fb.lat1 + fb.lat2) / 2, lng: (fb.lng1 + fb.lng2) / 2 };
|
|
880
|
+
}
|
|
881
|
+
else {
|
|
882
|
+
return { content: [{ type: "text", text: "Error: Either 'location' or 'bbox' argument must be provided." }], isError: true };
|
|
883
|
+
}
|
|
884
|
+
overpassQuery = `[out:json][timeout:25];\n(${queryRaw}(around:${radiusMeters},${centerPoint.lat},${centerPoint.lng}););\nout center;`;
|
|
885
|
+
searchModeLabel = `circle — ${radiusMeters}m radius around (${centerPoint.lat.toFixed(5)}, ${centerPoint.lng.toFixed(5)})`;
|
|
796
886
|
}
|
|
797
887
|
else {
|
|
798
|
-
|
|
888
|
+
if (args.location) {
|
|
889
|
+
try {
|
|
890
|
+
bboxObj = await geocodeLocation(args.location);
|
|
891
|
+
}
|
|
892
|
+
catch (err) {
|
|
893
|
+
if (args.bbox) {
|
|
894
|
+
log('warn', `Geocoding failed, falling back to bbox argument`, { error: err.message });
|
|
895
|
+
bboxObj = parseBBox(args.bbox);
|
|
896
|
+
}
|
|
897
|
+
else {
|
|
898
|
+
log('error', `Geocoding failed for overpass location: ${args.location}`, { error: err.message });
|
|
899
|
+
return { content: [{ type: "text", text: `Error: ${err.message}` }], isError: true };
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
else if (args.bbox) {
|
|
904
|
+
bboxObj = parseBBox(args.bbox);
|
|
905
|
+
}
|
|
906
|
+
else {
|
|
907
|
+
return { content: [{ type: "text", text: "Error: Either 'location' or 'bbox' argument must be provided." }], isError: true };
|
|
908
|
+
}
|
|
909
|
+
overpassQuery = `[out:json][timeout:25][bbox:${bboxObj.lat1},${bboxObj.lng1},${bboxObj.lat2},${bboxObj.lng2}];\n(${queryRaw};);\nout center;`;
|
|
910
|
+
searchModeLabel = `bounding box`;
|
|
799
911
|
}
|
|
800
|
-
log('info', `search_overpass starting`, { query: queryRaw });
|
|
801
|
-
const overpassQuery = `[out:json][timeout:25][bbox:${bboxObj.lat1},${bboxObj.lng1},${bboxObj.lat2},${bboxObj.lng2}];\n(${queryRaw};);\nout center;`;
|
|
912
|
+
log('info', `search_overpass starting`, { query: queryRaw, mode: useCircle ? 'circle' : 'bbox', radius_meters: radiusMeters });
|
|
802
913
|
const endpoints = [
|
|
803
914
|
"https://overpass-api.de/api/interpreter",
|
|
804
915
|
"https://overpass.kumi.systems/api/interpreter"
|
|
@@ -808,7 +919,6 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
808
919
|
}
|
|
809
920
|
let data = null;
|
|
810
921
|
let lastError = null;
|
|
811
|
-
// Formatting payload precisely for Overpass
|
|
812
922
|
const params = new URLSearchParams();
|
|
813
923
|
params.append('data', overpassQuery);
|
|
814
924
|
for (const endpoint of endpoints) {
|
|
@@ -830,7 +940,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
830
940
|
throw new Error(`Overpass API returned status: ${response.status} ${response.statusText}`);
|
|
831
941
|
}
|
|
832
942
|
data = await response.json();
|
|
833
|
-
break;
|
|
943
|
+
break;
|
|
834
944
|
}
|
|
835
945
|
catch (err) {
|
|
836
946
|
lastError = err;
|
|
@@ -842,20 +952,17 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
842
952
|
}
|
|
843
953
|
if (!data) {
|
|
844
954
|
if (lastError?.name === 'AbortError') {
|
|
845
|
-
return { content: [{ type: "text", text: `Error: Overpass API request timed out on all endpoints after ${(FETCH_TIMEOUT_MS * 4) / 1000}s
|
|
955
|
+
return { content: [{ type: "text", text: `Error: Overpass API request timed out on all endpoints after ${(FETCH_TIMEOUT_MS * 4) / 1000}s. The query or radius may be too large.` }], isError: true };
|
|
846
956
|
}
|
|
847
957
|
throw new Error(`Overpass query failed on all endpoints: ${lastError?.message}`);
|
|
848
958
|
}
|
|
849
|
-
let rawElements = data.elements || [];
|
|
850
|
-
// Parse raw elements into structured output
|
|
851
959
|
const seenIds = new Set();
|
|
852
|
-
let parsedElements =
|
|
960
|
+
let parsedElements = (data.elements || []).reduce((acc, e) => {
|
|
853
961
|
if (seenIds.has(e.id))
|
|
854
962
|
return acc;
|
|
855
963
|
seenIds.add(e.id);
|
|
856
964
|
const lat = e.lat ?? e.center?.lat;
|
|
857
965
|
const lon = e.lon ?? e.center?.lon;
|
|
858
|
-
// Only keep elements that actually have coordinates and tags (or allow untagged if requested, but structured output is better)
|
|
859
966
|
if (lat !== undefined && lon !== undefined) {
|
|
860
967
|
acc.push({
|
|
861
968
|
id: e.id,
|
|
@@ -873,23 +980,32 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
873
980
|
parsedElements = parsedElements.slice(0, limit);
|
|
874
981
|
truncated = true;
|
|
875
982
|
}
|
|
876
|
-
const shareUrl =
|
|
877
|
-
|
|
983
|
+
const shareUrl = useCircle && centerPoint
|
|
984
|
+
? buildShareUrlCircleWithMarkers(centerPoint, radiusMeters, parsedElements)
|
|
985
|
+
: buildShareUrlWithMarkers(bboxObj, parsedElements);
|
|
986
|
+
log('info', `search_overpass completed`, { elements: elementCount, returned: parsedElements.length, truncated, mode: useCircle ? 'circle' : 'bbox' });
|
|
878
987
|
const responseData = {
|
|
988
|
+
search_mode: useCircle ? 'circle' : 'bbox',
|
|
989
|
+
...(useCircle && centerPoint
|
|
990
|
+
? { center: centerPoint, radius_meters: radiusMeters }
|
|
991
|
+
: { original_bbox: bboxObj }),
|
|
879
992
|
generator: data.generator,
|
|
880
993
|
osm3s: data.osm3s,
|
|
881
994
|
elements: parsedElements,
|
|
882
995
|
total_count: elementCount,
|
|
883
996
|
returned_count: parsedElements.length,
|
|
884
997
|
truncated,
|
|
885
|
-
original_bbox: bboxObj,
|
|
886
998
|
share_url: shareUrl
|
|
887
999
|
};
|
|
1000
|
+
const topResults = parsedElements.slice(0, 5)
|
|
1001
|
+
.map((e) => `- [${e.type}] ${e.name} (${e.coordinates.lat.toFixed(5)}, ${e.coordinates.lon.toFixed(5)})`)
|
|
1002
|
+
.join('\n');
|
|
1003
|
+
const moreNote = parsedElements.length > 5 ? `\n … and ${parsedElements.length - 5} more (see JSON)` : '';
|
|
888
1004
|
return {
|
|
889
1005
|
content: [
|
|
890
1006
|
{
|
|
891
1007
|
type: "text",
|
|
892
|
-
text: `
|
|
1008
|
+
text: `Overpass query executed within a ${searchModeLabel}.\nFound ${elementCount} element${elementCount !== 1 ? 's' : ''}${truncated ? ` (showing first ${limit})` : ''}.\n\nTop results:\n${topResults}${moreNote}\n\nView on map: ${shareUrl}`
|
|
893
1009
|
},
|
|
894
1010
|
{
|
|
895
1011
|
type: "text",
|
|
@@ -1058,7 +1174,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1058
1174
|
content: [
|
|
1059
1175
|
{
|
|
1060
1176
|
type: "text",
|
|
1061
|
-
text: `Aggregated ${totalGrouped}
|
|
1177
|
+
text: `Aggregated ${totalGrouped} element${totalGrouped !== 1 ? 's' : ''} into ${activeHexes.length} H3 hexagon${activeHexes.length !== 1 ? 's' : ''} at resolution ${resolution}.\n\nTop hexagons by density:\n${topHexes.map(([hex, count]) => `- ${hex}: ${count} element${count !== 1 ? 's' : ''}`).join('\n')}\n\nView on map: ${shareUrl}`
|
|
1062
1178
|
},
|
|
1063
1179
|
{
|
|
1064
1180
|
type: "text",
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bbox-mcp-server",
|
|
3
3
|
"mcpName": "io.github.iamvibhorsingh/bbox-mcp-server",
|
|
4
|
-
"version": "1.2.
|
|
4
|
+
"version": "1.2.5",
|
|
5
5
|
"description": "MCP server for bounding box coordinate conversion, EPSG projections, Uber H3 hexagonal indexing, Overpass OSM queries, and shareable map links",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"license": "MIT",
|
|
@@ -47,7 +47,7 @@
|
|
|
47
47
|
"prepublishOnly": "npm run build"
|
|
48
48
|
},
|
|
49
49
|
"dependencies": {
|
|
50
|
-
"@modelcontextprotocol/sdk": "^1.
|
|
50
|
+
"@modelcontextprotocol/sdk": "^1.27.1",
|
|
51
51
|
"@terraformer/wkt": "^2.2.1",
|
|
52
52
|
"h3-js": "^4.1.0",
|
|
53
53
|
"proj4": "^2.15.0"
|