An interactive map widget that demonstrates the MCP Apps pattern with geospatial data visualization using Leaflet.js.
┌─────────────────────────────────────────────────────────────────┐
│ World City Explorer │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ [Search cities...] [Category ▼] [Search] │ │
│ └─────────────────────────────────────────────────────────┘ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ 🔵 Tokyo │ │
│ │ 🔴 Beijing │ │
│ │ 🟢 London │ │
│ │ 🟣 Paris │ │
│ │ │ │
│ │ 🔵 San Francisco 🔵 New York │ │
│ │ │ │
│ │ 🟠 Cairo │ │
│ │ 🟢 Singapore │ │
│ │ │ │
│ │ 🟣 Sydney │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────┘ │
│ Cities (10) ───────────────────────────────────────────────── │
│ │ Tokyo Japan [Tech] │ │
│ │ Paris France [Cultural] │ │
│ │ New York USA [Financial]│ │
│ └──────────────────────────────────────── │
└─────────────────────────────────────────────────────────────────┘
cd examples/mcp-apps-map
cargo build --release
./target/release/mcp-apps-mapThe server will start on port 3001 by default:
City Explorer MCP server listening on http://0.0.0.0:3001
Press Ctrl+C to stop the server
You can configure the port with the PORT environment variable:
PORT=8080 ./target/release/mcp-apps-mapFor the best development experience, use the preview page with a mock MCP bridge:
open preview.html
# Or on Linux: xdg-open preview.htmlThis gives you a fully functional map explorer with:
- All 10 world cities with real coordinates
- Category filtering and search
- Real-time tool call logging in the dev panel
- Distance calculations using Haversine formula
- Simulated network latency for realistic testing
Alternatively, view just the widget UI:
open widget/map.htmlTest the server with curl:
# Initialize the MCP connection
curl -s -X POST http://localhost:3001 \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-03-26","capabilities":{},"clientInfo":{"name":"test","version":"1.0.0"}}}'
# List available tools
curl -s -X POST http://localhost:3001 \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-d '{"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}}'
# Search for tech cities
curl -s -X POST http://localhost:3001 \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-d '{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"search_cities","arguments":{"filter":"tech"}}}'Use the PMCP CLI for interactive testing:
cargo pmcp connect http://localhost:3001This provides an interactive REPL for exploring the server:
Connected to city-explorer-server v1.0.0
> tools/list
> tools/call search_cities {"filter": "tech"}
> tools/call get_city_details {"city_id": "tokyo"}
> resources/list
Add the server as an MCP endpoint:
claude mcp add city-explorer --transport http http://localhost:3001Then test the tools:
You: Search for tech cities around the world
Claude: [Calls search_cities with filter "tech"]
You: Tell me more about Tokyo
Claude: [Calls get_city_details with city_id "tokyo"]
You: What cities are within 1000km of Paris?
Claude: [Calls get_nearby_cities with center and radius]
┌────────────────────────────────────────────────────────────────┐
│ MAP WIDGET ARCHITECTURE │
├────────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ Widget (HTML/JS) │ │
│ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────────┐ │ │
│ │ │ Leaflet Map │ │ City List │ │ Detail Panel │ │ │
│ │ │ (tiles + │ │ (filtered │ │ (selected city │ │ │
│ │ │ markers) │ │ results) │ │ info) │ │ │
│ │ └─────────────┘ └─────────────┘ └─────────────────┘ │ │
│ │ │ │ │
│ │ MapState │ │
│ │ { center, zoom, filter } │ │
│ └──────────────────────────────────────────────────────────┘ │
│ │ │
│ MCP Bridge │
│ │ │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ Server (Rust) │ │
│ │ │ │
│ │ search_cities(query?, filter?, map_state?) │ │
│ │ └─> Returns matching cities with coordinates │ │
│ │ │ │
│ │ get_city_details(city_id) │ │
│ │ └─> Returns full city info + suggested map view │ │
│ │ │ │
│ │ get_nearby_cities(center, radius_km) │ │
│ │ └─> Returns cities within radius using Haversine │ │
│ │ │ │
│ └──────────────────────────────────────────────────────────┘ │
│ │
└────────────────────────────────────────────────────────────────┘
mcp-apps-map/
├── Cargo.toml
├── README.md
├── src/
│ └── main.rs
│ ├── City types (Coordinates, City, CityCategory)
│ ├── Mock city database (10 world cities)
│ ├── Haversine distance calculation
│ ├── Tool handlers
│ └── Resource handler
└── widget/
└── map.html
├── Leaflet.js map integration
├── Category-colored markers
├── Search and filter UI
├── City list sidebar
└── Detail panel
Search for cities by name, country, or category.
// Request
{
"query": "york",
"filter": "financial",
"map_state": { "center": { "lat": 40, "lon": -74 }, "zoom": 8 }
}
// Response
{
"count": 1,
"cities": [{
"id": "new-york",
"name": "New York",
"country": "United States",
"population": 18800000,
"coordinates": { "lat": 40.7128, "lon": -74.0060 },
"description": "The Big Apple, global center of finance and culture.",
"category": "financial"
}]
}Get detailed information about a specific city.
// Request
{ "city_id": "tokyo" }
// Response
{
"found": true,
"city": { /* full city object */ },
"recommended_zoom": 12,
"suggested_view": {
"center": { "lat": 35.6762, "lon": 139.6503 },
"zoom": 12
}
}Find cities within a radius of a point.
// Request
{
"center": { "lat": 48.8566, "lon": 2.3522 },
"radius_km": 500
}
// Response
{
"center": { "lat": 48.8566, "lon": 2.3522 },
"radius_km": 500,
"count": 2,
"cities": [
{ "city": { /* Paris */ }, "distance_km": 0 },
{ "city": { /* London */ }, "distance_km": 343.5 }
]
}The example includes cities in five categories, each with a distinct marker color:
| Category | Color | Example Cities |
|---|---|---|
| Capital | Red | London, Beijing |
| Tech | Blue | Tokyo, San Francisco |
| Cultural | Purple | Paris, Sydney |
| Financial | Green | New York, Singapore |
| Historical | Orange | Rome, Cairo |
Replace the mock database with a real API:
// In src/main.rs
async fn search_cities_handler(
input: SearchCitiesInput,
_extra: RequestHandlerExtra
) -> Result<Value> {
// Call external API
let response = reqwest::get(format!(
"https://api.example.com/cities?q={}",
input.query.unwrap_or_default()
)).await?;
let cities: Vec<City> = response.json().await?;
Ok(json!({ "count": cities.len(), "cities": cities }))
}Weather integration:
#[derive(Deserialize, JsonSchema)]
struct GetWeatherInput {
city_id: String,
}
fn get_weather_handler(input: GetWeatherInput, _extra: RequestHandlerExtra) -> Result<Value> {
// Fetch weather data for city
Ok(json!({
"city_id": input.city_id,
"temperature_c": 22,
"conditions": "Partly cloudy"
}))
}Route planning:
#[derive(Deserialize, JsonSchema)]
struct GetRouteInput {
from_city: String,
to_city: String,
}Modify widget/map.html:
// Change tile provider (e.g., satellite imagery)
L.tileLayer('https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}', {
attribution: 'Tiles © Esri'
}).addTo(map);
// Custom marker icons
function createMarkerIcon(category) {
return L.icon({
iconUrl: `/icons/${category}.png`,
iconSize: [32, 32],
iconAnchor: [16, 32]
});
}
// Add clustering for many markers
const markers = L.markerClusterGroup();
cities.forEach(city => {
markers.addLayer(L.marker([city.coordinates.lat, city.coordinates.lon]));
});
map.addLayer(markers);For thousands of cities:
- Server-side pagination:
#[derive(Deserialize, JsonSchema)]
struct SearchCitiesInput {
query: Option<String>,
limit: Option<usize>, // Default 50
offset: Option<usize>,
}- Viewport-based loading:
map.on('moveend', async () => {
const bounds = map.getBounds();
const cities = await callTool('search_cities_in_bounds', {
north: bounds.getNorth(),
south: bounds.getSouth(),
east: bounds.getEast(),
west: bounds.getWest()
});
renderMarkers(cities);
});- Marker clustering: Use Leaflet.markercluster for better performance with many markers.
const cityCache = new Map();
async function getCityDetails(cityId) {
if (cityCache.has(cityId)) {
return cityCache.get(cityId);
}
const result = await callTool('get_city_details', { city_id: cityId });
cityCache.set(cityId, result);
return result;
}# From repository root
cargo test --features "mcp-apps" -- mapThe preview.html file provides a complete testing environment:
- Open
preview.htmlin your browser - Search for cities, filter by category
- Click markers and cities to see tool calls in the dev panel
- Check state persistence by refreshing the page
After connecting to Claude Code (see Quick Start), test the full flow:
You: Show me all the tech hub cities in the world
Claude: [Calls search_cities with filter "tech"]
You: What's the closest city to London within 500km?
Claude: [Calls get_nearby_cities with London coordinates and 500km radius]
Test the server directly via HTTP:
# Start the server in one terminal
./target/release/mcp-apps-map
# In another terminal, test the endpoints:
# Initialize handshake
curl -s -X POST http://localhost:3001 \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-03-26","capabilities":{},"clientInfo":{"name":"test","version":"1.0.0"}}}'
# Call search_cities tool
curl -s -X POST http://localhost:3001 \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-d '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"search_cities","arguments":{"filter":"tech"}}}'cd ../../tests/playwright
npm install
npm test -- --grep "map"- Map loads with correct initial view
- Search filters cities correctly
- Category dropdown filters work
- Clicking marker shows popup
- Clicking city in list zooms to location
- Detail panel shows correct information
- Map state persists (in ChatGPT)
- Mobile viewport renders correctly
cargo build --release
./target/release/mcp-apps-map
# Server runs on http://localhost:3001The server is a standalone HTTP service that can be deployed anywhere:
# Build the release binary
cargo build --release
# Run with custom port
PORT=8080 ./target/release/mcp-apps-mapFor production, consider:
- Using a commercial tile provider (Mapbox, Google Maps)
- Self-hosting tiles with OpenMapTiles
- Caching tiles for offline use
# Server port (default: 3001)
export PORT=8080
# Optional: Configure tile server
export MAP_TILE_URL="https://your-tile-server/{z}/{x}/{y}.png"
# Optional: Configure city data source
export CITY_API_URL="https://your-api.com/cities"- Check internet connection (tiles load from OpenStreetMap)
- Verify no CORS issues in browser console
- Try a different tile provider
- Check browser console for tool call errors
- Verify server is running and responding
- Check that coordinates are valid (lat: -90 to 90, lon: -180 to 180)
- Leaflet uses [lat, lon] order, not [lon, lat]
- Verify coordinate data in server response