From b013aee1e370646464a018449b3758078430a616 Mon Sep 17 00:00:00 2001 From: i-norden Date: Wed, 18 Mar 2026 00:17:43 -0400 Subject: [PATCH] Remove retired indoor API tool --- README.md | 1 - mcp/handlers.go | 142 --------------------------------------------- mcp/server_test.go | 141 -------------------------------------------- mcp/tools.go | 51 ---------------- 4 files changed, 335 deletions(-) diff --git a/README.md b/README.md index e287e83..af972e4 100644 --- a/README.md +++ b/README.md @@ -128,7 +128,6 @@ Add to `.mcp.json`: | `browse_stac_items` | List items in a remote STAC collection | | `import_stac_asset` | Import a STAC asset as a local dataset, optionally with namespace/catalog metadata and project attachment | | `search_stac` | Search local STAC with bbox/datetime/CQL2 filters | -| `indoor_api` | Allowlisted Indoor GIS endpoint access (buildings/floors/spaces/navigation/analytics/sensors/booking/geofences/simulations). Mutations require `confirm=true`. | | `map_api` | Allowlisted map endpoint access (publish/unpublish/stats/embed config, raster metadata/JSON analysis/export ops including contour/viewshed/profile/KDE/slope/aspect, geodesic area/length, raster classification via k-means/ISODATA/max-likelihood/random-forest, OGC feature edit ops). Mutations require `confirm=true`. | Use `list_operations` for the live vector-processing catalog. Raster operations do not currently have a live catalog endpoint, so `run_raster_process` documents the current backend families directly: terrain, hydrology, distance/cost, spectral/change, classification, and raster-vector conversion. For dataset-name-based raster JSON routes, `map_api` now also exposes contour, viewshed, profile, KDE, slope, and aspect. Geodesic area/length and raster classification (k-means, ISODATA, maximum-likelihood, random-forest) are also available via `map_api`. diff --git a/mcp/handlers.go b/mcp/handlers.go index f203dfc..072f24d 100644 --- a/mcp/handlers.go +++ b/mcp/handlers.go @@ -112,8 +112,6 @@ func HandleToolCall(client *Client, name string, args json.RawMessage) (string, return handleImportSTACAsset(client, params) case "search_stac": return handleSearchSTAC(client, params) - case "indoor_api": - return handleIndoorAPI(client, params) case "map_api": return handleMapAPI(client, params) default: @@ -127,87 +125,6 @@ type apiOperation struct { Mutating bool } -var indoorOperations = map[string]apiOperation{ - "list_campuses": {Method: "GET", Path: "/api/indoor/campuses"}, - "create_campus": {Method: "POST", Path: "/api/indoor/campuses", Mutating: true}, - "get_campus": {Method: "GET", Path: "/api/indoor/campuses/{campus_id}"}, - "update_campus": {Method: "PUT", Path: "/api/indoor/campuses/{campus_id}", Mutating: true}, - "delete_campus": {Method: "DELETE", Path: "/api/indoor/campuses/{campus_id}", Mutating: true}, - "list_campus_buildings": {Method: "GET", Path: "/api/indoor/campuses/{campus_id}/buildings"}, - "list_buildings": {Method: "GET", Path: "/api/indoor/buildings"}, - "create_building": {Method: "POST", Path: "/api/indoor/buildings", Mutating: true}, - "get_building": {Method: "GET", Path: "/api/indoor/buildings/{building_id}"}, - "update_building": {Method: "PUT", Path: "/api/indoor/buildings/{building_id}", Mutating: true}, - "delete_building": {Method: "DELETE", Path: "/api/indoor/buildings/{building_id}", Mutating: true}, - "list_floors": {Method: "GET", Path: "/api/indoor/buildings/{building_id}/floors"}, - "create_floor": {Method: "POST", Path: "/api/indoor/buildings/{building_id}/floors", Mutating: true}, - "upload_floor_plan": {Method: "POST", Path: "/api/indoor/buildings/{building_id}/floors/{floor_id}/plan", Mutating: true}, - "list_floor_spaces": {Method: "GET", Path: "/api/indoor/buildings/{building_id}/floors/{level}/spaces"}, - "create_space": {Method: "POST", Path: "/api/indoor/buildings/{building_id}/spaces", Mutating: true}, - "get_space": {Method: "GET", Path: "/api/indoor/buildings/{building_id}/spaces/{space_id}"}, - "update_space": {Method: "PUT", Path: "/api/indoor/buildings/{building_id}/spaces/{space_id}", Mutating: true}, - "delete_space": {Method: "DELETE", Path: "/api/indoor/buildings/{building_id}/spaces/{space_id}", Mutating: true}, - "list_transitions": {Method: "GET", Path: "/api/indoor/buildings/{building_id}/transitions"}, - "list_boundaries": {Method: "GET", Path: "/api/indoor/buildings/{building_id}/boundaries"}, - "create_boundary": {Method: "POST", Path: "/api/indoor/buildings/{building_id}/boundaries", Mutating: true}, - "get_boundary": {Method: "GET", Path: "/api/indoor/buildings/{building_id}/boundaries/{boundary_id}"}, - "update_boundary": {Method: "PUT", Path: "/api/indoor/buildings/{building_id}/boundaries/{boundary_id}", Mutating: true}, - "delete_boundary": {Method: "DELETE", Path: "/api/indoor/buildings/{building_id}/boundaries/{boundary_id}", Mutating: true}, - "list_assets": {Method: "GET", Path: "/api/indoor/buildings/{building_id}/assets"}, - "create_asset": {Method: "POST", Path: "/api/indoor/buildings/{building_id}/assets", Mutating: true}, - "update_asset_position": {Method: "PUT", Path: "/api/indoor/buildings/{building_id}/assets/{asset_id}", Mutating: true}, - "navigate": {Method: "POST", Path: "/api/indoor/navigate"}, - "find_nearest": {Method: "POST", Path: "/api/indoor/navigate/nearest"}, - "navigate_outdoor": {Method: "POST", Path: "/api/indoor/navigate/outdoor"}, - "import_building": {Method: "POST", Path: "/api/indoor/import", Mutating: true}, - "export_imdf": {Method: "GET", Path: "/api/indoor/buildings/{building_id}/export/imdf"}, - "export_indoorgml": {Method: "GET", Path: "/api/indoor/buildings/{building_id}/export/indoorgml"}, - "get_occupancy": {Method: "GET", Path: "/api/indoor/buildings/{building_id}/occupancy"}, - "get_stats": {Method: "GET", Path: "/api/indoor/buildings/{building_id}/stats"}, - "validate_building": {Method: "GET", Path: "/api/indoor/buildings/{building_id}/validate"}, - "get_accessibility": {Method: "GET", Path: "/api/indoor/buildings/{building_id}/analysis/accessibility"}, - "get_dead_zones": {Method: "GET", Path: "/api/indoor/buildings/{building_id}/analysis/dead-zones"}, - "get_reachable": {Method: "GET", Path: "/api/indoor/buildings/{building_id}/analysis/reachable"}, - "get_historical_analytics": {Method: "GET", Path: "/api/indoor/buildings/{building_id}/analytics"}, - "ingest_sensor_data": {Method: "POST", Path: "/api/indoor/buildings/{building_id}/sensors", Mutating: true}, - "get_sensor_data": {Method: "GET", Path: "/api/indoor/buildings/{building_id}/sensors"}, - "get_sensor_heatmap": {Method: "GET", Path: "/api/indoor/buildings/{building_id}/sensors/heatmap"}, - "get_sensor_timeseries": {Method: "GET", Path: "/api/indoor/buildings/{building_id}/sensors/timeseries"}, - "ingest_positions": {Method: "POST", Path: "/api/indoor/buildings/{building_id}/positions", Mutating: true}, - "get_latest_positions": {Method: "GET", Path: "/api/indoor/buildings/{building_id}/positions/latest"}, - "get_position_history": {Method: "GET", Path: "/api/indoor/buildings/{building_id}/positions/{device_id}/history"}, - "list_scenarios": {Method: "GET", Path: "/api/indoor/buildings/{building_id}/scenarios"}, - "create_scenario": {Method: "POST", Path: "/api/indoor/buildings/{building_id}/scenarios", Mutating: true}, - "get_scenario": {Method: "GET", Path: "/api/indoor/buildings/{building_id}/scenarios/{scenario_id}"}, - "update_scenario": {Method: "PUT", Path: "/api/indoor/buildings/{building_id}/scenarios/{scenario_id}", Mutating: true}, - "delete_scenario": {Method: "DELETE", Path: "/api/indoor/buildings/{building_id}/scenarios/{scenario_id}", Mutating: true}, - "get_stream_status": {Method: "GET", Path: "/api/indoor/buildings/{building_id}/stream/status"}, - "create_booking": {Method: "POST", Path: "/api/indoor/buildings/{building_id}/bookings", Mutating: true}, - "list_bookings": {Method: "GET", Path: "/api/indoor/buildings/{building_id}/bookings"}, - "cancel_booking": {Method: "DELETE", Path: "/api/indoor/buildings/{building_id}/bookings/{booking_id}", Mutating: true}, - "checkin_booking": {Method: "POST", Path: "/api/indoor/buildings/{building_id}/bookings/{booking_id}/checkin", Mutating: true}, - "list_geofences": {Method: "GET", Path: "/api/indoor/buildings/{building_id}/geofences"}, - "create_geofence": {Method: "POST", Path: "/api/indoor/buildings/{building_id}/geofences", Mutating: true}, - "get_geofence": {Method: "GET", Path: "/api/indoor/buildings/{building_id}/geofences/{geofence_id}"}, - "update_geofence": {Method: "PUT", Path: "/api/indoor/buildings/{building_id}/geofences/{geofence_id}", Mutating: true}, - "delete_geofence": {Method: "DELETE", Path: "/api/indoor/buildings/{building_id}/geofences/{geofence_id}", Mutating: true}, - "get_evacuation_routes": {Method: "POST", Path: "/api/indoor/buildings/{building_id}/evacuation"}, - "trigger_evacuation_alert": {Method: "POST", Path: "/api/indoor/buildings/{building_id}/evacuation/alert", Mutating: true}, - "topology_resilience": {Method: "POST", Path: "/api/indoor/buildings/{building_id}/topology/resilience"}, - "topology_track": {Method: "POST", Path: "/api/indoor/buildings/{building_id}/topology/track"}, - "topology_compare": {Method: "POST", Path: "/api/indoor/buildings/{building_id}/topology/compare"}, - "spectral_analysis": {Method: "POST", Path: "/api/indoor/buildings/{building_id}/spectral"}, - "evacuation_optimal": {Method: "POST", Path: "/api/indoor/buildings/{building_id}/evacuation/optimal"}, - "simulate_fire_evacuation": {Method: "POST", Path: "/api/indoor/buildings/{building_id}/simulate/fire-evacuation"}, - "simulate_airflow": {Method: "POST", Path: "/api/indoor/floors/{floor_id}/simulate/airflow"}, - "simulate_acoustics": {Method: "POST", Path: "/api/indoor/floors/{floor_id}/simulate/acoustics"}, - "simulate_thermal": {Method: "POST", Path: "/api/indoor/floors/{floor_id}/simulate/thermal"}, - "simulate_rf": {Method: "POST", Path: "/api/indoor/floors/{floor_id}/simulate/rf"}, - "navigate_manifold": {Method: "POST", Path: "/api/indoor/floors/{floor_id}/navigate/manifold"}, - "forecast": {Method: "POST", Path: "/api/indoor/floors/{floor_id}/forecast"}, - "simulate_infection_risk": {Method: "POST", Path: "/api/indoor/floors/{floor_id}/simulate/infection-risk"}, -} - var mapOperations = map[string]apiOperation{ "publish_map": {Method: "POST", Path: "/api/maps/publish", Mutating: true}, "list_published_maps": {Method: "GET", Path: "/api/maps/published"}, @@ -1001,10 +918,6 @@ func handleSearchSTAC(client *Client, params map[string]interface{}) (string, er return formatJSON(data), nil } -func handleIndoorAPI(client *Client, params map[string]interface{}) (string, error) { - return handleScopedAPI(client, params, indoorOperations, "indoor") -} - func handleMapAPI(client *Client, params map[string]interface{}) (string, error) { return handleScopedAPI(client, params, mapOperations, "map") } @@ -1021,14 +934,6 @@ func handleScopedAPI(client *Client, params map[string]interface{}, ops map[stri if op.Mutating && !requireConfirm(params) { return "", fmt.Errorf("%s operation %q mutates data; pass confirm=true to proceed", scope, opName) } - if scope == "indoor" && (opName == "import_building" || opName == "upload_floor_plan") { - data, err := handleIndoorMultipartOperation(client, opName, op.Path, params) - if err != nil { - return "", err - } - return formatJSON(data), nil - } - path, err := interpolatePath(op.Path, params) if err != nil { return "", err @@ -1049,26 +954,6 @@ func handleScopedAPI(client *Client, params map[string]interface{}, ops map[stri return formatJSON(data), nil } -func handleIndoorMultipartOperation(client *Client, opName, pathTemplate string, params map[string]interface{}) (json.RawMessage, error) { - path, err := interpolatePath(pathTemplate, params) - if err != nil { - return nil, err - } - filePath, err := requireString(params, "file_path") - if err != nil { - return nil, err - } - extraFields := map[string]string{} - if opName == "upload_floor_plan" { - if bounds, err := optionalMultipartValue(params, "bounds"); err != nil { - return nil, err - } else if bounds != "" { - extraFields["bounds"] = bounds - } - } - return client.UploadMultipart(path, filePath, "file", extraFields) -} - func requireConfirm(params map[string]interface{}) bool { v, ok := params["confirm"] if !ok { @@ -1090,33 +975,6 @@ func extractBody(params map[string]interface{}) interface{} { return nil } -func optionalMultipartValue(params map[string]interface{}, key string) (string, error) { - if raw, ok := params[key]; ok && raw != nil { - return stringifyMultipartValue(raw) - } - if body, ok := params["body"].(map[string]interface{}); ok { - if raw, ok := body[key]; ok && raw != nil { - return stringifyMultipartValue(raw) - } - } - return "", nil -} - -func stringifyMultipartValue(v interface{}) (string, error) { - switch value := v.(type) { - case string: - return value, nil - case map[string]interface{}, []interface{}: - data, err := json.Marshal(value) - if err != nil { - return "", err - } - return string(data), nil - default: - return stringify(v) - } -} - func extractQuery(params map[string]interface{}) (map[string]string, error) { raw, ok := params["query"] if !ok { diff --git a/mcp/server_test.go b/mcp/server_test.go index ef089e3..0ff2fe4 100644 --- a/mcp/server_test.go +++ b/mcp/server_test.go @@ -282,147 +282,6 @@ func TestToolsCall_UploadDatasetScoped(t *testing.T) { } } -func TestToolsCall_IndoorImportBuildingUsesMultipartUpload(t *testing.T) { - tmpDir := t.TempDir() - filePath := filepath.Join(tmpDir, "campus.ifc") - if err := os.WriteFile(filePath, []byte("ISO-10303-21;"), 0o600); err != nil { - t.Fatalf("write temp indoor file: %v", err) - } - - mux := http.NewServeMux() - mux.HandleFunc("POST /api/indoor/import", func(w http.ResponseWriter, r *http.Request) { - if err := r.ParseMultipartForm(1 << 20); err != nil { - w.WriteHeader(http.StatusBadRequest) - fmt.Fprintf(w, `{"error":"parse=%v"}`, err) - return - } - file, header, err := r.FormFile("file") - if err != nil { - w.WriteHeader(http.StatusBadRequest) - fmt.Fprintf(w, `{"error":"file=%v"}`, err) - return - } - defer file.Close() - if header.Filename != "campus.ifc" { - w.WriteHeader(http.StatusBadRequest) - fmt.Fprintf(w, `{"error":"filename=%s"}`, header.Filename) - return - } - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusCreated) - fmt.Fprint(w, `{"id":"building-1","name":"HQ"}`) - }) - srv := testServer(t, mux) - - resp := sendRequest(t, srv, "tools/call", 241, map[string]interface{}{ - "name": "indoor_api", - "arguments": map[string]interface{}{ - "operation": "import_building", - "file_path": filePath, - "confirm": true, - }, - }) - - if resp.Error != nil { - t.Fatalf("unexpected error: %v", resp.Error) - } - result, _ := resp.Result.(map[string]interface{}) - if isErr, _ := result["isError"].(bool); isErr { - t.Fatalf("expected success result, got error: %+v", result) - } -} - -func TestToolsCall_IndoorUploadFloorPlanUsesMultipartBounds(t *testing.T) { - tmpDir := t.TempDir() - filePath := filepath.Join(tmpDir, "floor.png") - if err := os.WriteFile(filePath, []byte("png-bytes"), 0o600); err != nil { - t.Fatalf("write temp plan file: %v", err) - } - - mux := http.NewServeMux() - mux.HandleFunc("POST /api/indoor/buildings/{id}/floors/{fid}/plan", func(w http.ResponseWriter, r *http.Request) { - if got := r.PathValue("id"); got != "building-1" { - w.WriteHeader(http.StatusBadRequest) - fmt.Fprintf(w, `{"error":"building=%s"}`, got) - return - } - if got := r.PathValue("fid"); got != "floor-2" { - w.WriteHeader(http.StatusBadRequest) - fmt.Fprintf(w, `{"error":"floor=%s"}`, got) - return - } - if err := r.ParseMultipartForm(1 << 20); err != nil { - w.WriteHeader(http.StatusBadRequest) - fmt.Fprintf(w, `{"error":"parse=%v"}`, err) - return - } - if got := r.FormValue("bounds"); got != "[[1,2],[3,4]]" { - w.WriteHeader(http.StatusBadRequest) - fmt.Fprintf(w, `{"error":"bounds=%s"}`, got) - return - } - w.Header().Set("Content-Type", "application/json") - fmt.Fprint(w, `{"status":"uploaded","floor_id":"floor-2"}`) - }) - srv := testServer(t, mux) - - resp := sendRequest(t, srv, "tools/call", 242, map[string]interface{}{ - "name": "indoor_api", - "arguments": map[string]interface{}{ - "operation": "upload_floor_plan", - "building_id": "building-1", - "floor_id": "floor-2", - "file_path": filePath, - "bounds": []interface{}{[]interface{}{1.0, 2.0}, []interface{}{3.0, 4.0}}, - "confirm": true, - }, - }) - - if resp.Error != nil { - t.Fatalf("unexpected error: %v", resp.Error) - } - result, _ := resp.Result.(map[string]interface{}) - if isErr, _ := result["isError"].(bool); isErr { - t.Fatalf("expected success result, got error: %+v", result) - } -} - -func TestToolsCall_IndoorGetScenario(t *testing.T) { - mux := http.NewServeMux() - mux.HandleFunc("GET /api/indoor/buildings/{id}/scenarios/{sid}", func(w http.ResponseWriter, r *http.Request) { - if got := r.PathValue("id"); got != "building-1" { - w.WriteHeader(http.StatusBadRequest) - fmt.Fprintf(w, `{"error":"building=%s"}`, got) - return - } - if got := r.PathValue("sid"); got != "scenario-1" { - w.WriteHeader(http.StatusBadRequest) - fmt.Fprintf(w, `{"error":"scenario=%s"}`, got) - return - } - w.Header().Set("Content-Type", "application/json") - fmt.Fprint(w, `{"id":"scenario-1","name":"Evacuation Drill"}`) - }) - srv := testServer(t, mux) - - resp := sendRequest(t, srv, "tools/call", 243, map[string]interface{}{ - "name": "indoor_api", - "arguments": map[string]interface{}{ - "operation": "get_scenario", - "building_id": "building-1", - "scenario_id": "scenario-1", - }, - }) - - if resp.Error != nil { - t.Fatalf("unexpected error: %v", resp.Error) - } - result, _ := resp.Result.(map[string]interface{}) - if isErr, _ := result["isError"].(bool); isErr { - t.Fatalf("expected success result, got error: %+v", result) - } -} - func TestToolsCall_ExecuteSQL(t *testing.T) { mux := http.NewServeMux() mux.HandleFunc("POST /api/query/sql", func(w http.ResponseWriter, r *http.Request) { diff --git a/mcp/tools.go b/mcp/tools.go index 54e5b18..34516c7 100644 --- a/mcp/tools.go +++ b/mcp/tools.go @@ -589,57 +589,6 @@ func AllTools() []Tool { }, }, }, - { - Name: "indoor_api", - Description: "Access Indoor GIS operations through a strict allowlist. Mutating operations require confirm=true.", - InputSchema: InputSchema{ - Type: "object", - Properties: map[string]PropertySchema{ - "operation": { - Type: "string", - Description: "Indoor operation name.", - Enum: []string{ - "list_campuses", "create_campus", "get_campus", "update_campus", "delete_campus", "list_campus_buildings", - "list_buildings", "create_building", "get_building", "update_building", "delete_building", - "list_floors", "create_floor", "upload_floor_plan", "list_floor_spaces", - "create_space", "get_space", "update_space", "delete_space", "list_transitions", - "list_boundaries", "create_boundary", "get_boundary", "update_boundary", "delete_boundary", - "list_assets", "create_asset", "update_asset_position", - "navigate", "find_nearest", "navigate_outdoor", - "import_building", "export_imdf", "export_indoorgml", - "get_occupancy", "get_stats", "validate_building", "get_accessibility", "get_dead_zones", "get_reachable", "get_historical_analytics", - "ingest_sensor_data", "get_sensor_data", "get_sensor_heatmap", "get_sensor_timeseries", - "ingest_positions", "get_latest_positions", "get_position_history", - "list_scenarios", "create_scenario", "get_scenario", "update_scenario", "delete_scenario", "get_stream_status", - "create_booking", "list_bookings", "cancel_booking", "checkin_booking", - "list_geofences", "create_geofence", "get_geofence", "update_geofence", "delete_geofence", - "get_evacuation_routes", "trigger_evacuation_alert", - "topology_resilience", "topology_track", "topology_compare", "spectral_analysis", - "evacuation_optimal", "simulate_fire_evacuation", - "simulate_airflow", "simulate_acoustics", "simulate_thermal", "simulate_rf", - "navigate_manifold", "forecast", "simulate_infection_risk", - }, - }, - "building_id": {Type: "string", Description: "Path parameter for building-scoped operations."}, - "campus_id": {Type: "string", Description: "Path parameter for campus-scoped operations."}, - "floor_id": {Type: "string", Description: "Path parameter for floor-scoped operations."}, - "level": {Type: "string", Description: "Path parameter for floor level routes."}, - "space_id": {Type: "string", Description: "Path parameter for space-scoped operations."}, - "asset_id": {Type: "string", Description: "Path parameter for asset-scoped operations."}, - "boundary_id": {Type: "string", Description: "Path parameter for boundary-scoped operations."}, - "device_id": {Type: "string", Description: "Path parameter for device-scoped operations."}, - "booking_id": {Type: "string", Description: "Path parameter for booking-scoped operations."}, - "geofence_id": {Type: "string", Description: "Path parameter for geofence-scoped operations."}, - "scenario_id": {Type: "string", Description: "Path parameter for scenario-scoped operations."}, - "file_path": {Type: "string", Description: "Local file path for multipart upload operations such as import_building and upload_floor_plan."}, - "bounds": {Type: "string", Description: "Optional JSON string or simple value for multipart upload fields. For upload_floor_plan this should be a JSON array like [[west,south],[east,north]]."}, - "query": {Type: "object", Description: "Optional query string key/value map."}, - "body": {Type: "object", Description: "Optional JSON request body. For upload_floor_plan, bounds may also be provided here."}, - "confirm": {Type: "boolean", Description: "Required true for mutating operations."}, - }, - Required: []string{"operation"}, - }, - }, { Name: "map_api", Description: "Access map-focused operations (publishing, raster metadata, slope/aspect, geodesic area/length, raster classification, OGC feature edits) through a strict allowlist. Mutating operations require confirm=true.",