#
tokens: 3125/50000 7/7 files
lines: off (toggle) GitHub
raw markdown copy
# Directory Structure

```
├── .gitignore
├── .python-version
├── generate_image.py
├── geo.json
├── pyproject.toml
├── README.md
├── server.py
├── temp_map.png
└── uv.lock
```

# Files

--------------------------------------------------------------------------------
/.python-version:
--------------------------------------------------------------------------------

```
3.13

```

--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------

```
.env
__pycache__/
```

--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------

```markdown
# Geoapify MCP Server

Convert addresses into GPS coordinates for mapping, and optionally create an image of those coordinates using the Geoapify server.

![Example Map](./temp_map.png)

## Installation

You'll need to get an API key from [Geoapify](https://www.geoapify.com/), and set it as an environment variable named `GEO_APIKEY`.

Your `claude_desktop_config.json` will look like this after:

```json
"MCP Map Demo": {
      "command": "uv",
      "args": [
	"--directory",
        "/PATH/TO/THIS/REPO",
        "run",
        "--with",
        "fastmcp",
        "--with",
        "requests",
        "--with",
        "folio",
        "--with",
        "selenium",
        "--with",
        "pillow",
        "fastmcp",
        "run",
        "/PATH/TO/THIS/REPO/server.py"
      ],
      "env": {
        "GEO_APIKEY": "YOURAPIKEY"
      }
    }
```

You'll notice we include all the dependencies in our `args`.

## Tools

`get_gps_coordinates`

Used to get GPS coordinates from the API for creating GEOJSON, etc.

`create_map_from_geojson`

Create a map image and show it. (Showing only works on MacOS for now.)


## Example Usage

**Get GPS Coordinates** 

```
can you create a geojson of the following locations including their gps coordinates: 179 avenue du Général Leclerc, côté Rive Gauche
158 avenue du Général Leclerc, côté Rive Droite à l'angle de la rue Jules Herbron
112 avenue du Général Leclerc, côté Rive Droite
34 avenue du Général Leclerc, côté Rive Droite
En face du 57 rue Gaston Boissier, à côté de la borne
Route du Pavé de Meudon - à côté du chêne de la Vierge
6 avenue de Versailles (près du centre aquatique des Bertisettes)
3 places sur parking de la rue Costes et Bellonte
Rue Joseph Chaleil
18 rue des Sables – à côté de la crèche
25 sente de la Procession
33 rue Joseph Bertrand
Place Saint Paul
Place de la bataille de Stalingrad
Placette croisement avenue Pierre Grenier / avenue Robert Hardouin
107 avenue Gaston Boissier (en face de la caserne des pompiers)
```

**Result:** [Attached JSON file](./geo.json)

Returns a GeoJSON file.

**Create a Map Image**

```
can you create a map from my attached geojson file?
```
[Attached JSON file](./geo.json)

**Result:** ![temp map](./temp_map.png)

## LICENSE

MIT

```

--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------

```toml
[project]
name = "map-mcp"
version = "0.1.0"
description = "Add your description here"
readme = "README.md"
requires-python = ">=3.13"
dependencies = [
    "folium>=0.19.4",
    "mcp[cli]===1.2.0rc1",
    "requests>=2.32.3",
    "selenium>=4.27.1",
]

```

--------------------------------------------------------------------------------
/server.py:
--------------------------------------------------------------------------------

```python
# server.py
from mcp.server.fastmcp import FastMCP, Image
import requests
from requests.structures import CaseInsensitiveDict
from urllib.parse import quote
import os
import generate_image
from PIL import Image as PILImage
import subprocess

APIKEY = os.environ.get("GEO_APIKEY")
if APIKEY is None:
    raise ValueError("GEO_APIKEY environment variable is not set")

# Create an MCP server
mcp = FastMCP("Map Demo", dependencies=["requests", "pillow", "selenium", "folium"])

def get_geocode(search_address, api_key):
    # URL encode the address to handle special characters
    encoded_address = quote(search_address)
    
    url = f"https://api.geoapify.com/v1/geocode/search?text={encoded_address}&apiKey={api_key}"
    
    headers = CaseInsensitiveDict()
    headers["Accept"] = "application/json"
    
    resp = requests.get(url, headers=headers)
    return resp.json()

# Get GPS coordinates for an address
@mcp.tool()
def get_gps_coordinates(address: str) -> dict:
    """Gets GPS coordinates for an address"""
    return get_geocode(address, APIKEY)

@mcp.tool()
def create_map_from_geojson(filename: str, geojson_coordinates: dict) -> str:
    """Creates a map image from GeoJSON coordinates"""
    generate_image.create_map_from_geojson(geojson_coordinates, "temp_map.png")
    subprocess.run(["open", "temp_map.png"])
    return f"Map image created at {os.curdir}/temp_map.png, and shown to user."

if __name__ == "__main__":
    mcp.run() 
```

--------------------------------------------------------------------------------
/generate_image.py:
--------------------------------------------------------------------------------

```python
import folium
import json
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
import time
import os
import sys


import logging

logging.basicConfig(
    filename='generator.log',
    level=logging.INFO,
    format='%(asctime)s - %(levelname)s - %(message)s'
)

def create_map_from_geojson(geojson_data, output_file="map.png", zoom_start=15):
    # Calculate the center point for the map
    coordinates = []
    for feature in geojson_data['features']:
        lon, lat = feature['geometry']['coordinates']
        coordinates.append([lat, lon])  # Folium uses lat, lon order
    
    center_lat = sum(coord[0] for coord in coordinates) / len(coordinates)
    center_lon = sum(coord[1] for coord in coordinates) / len(coordinates)
    
    # Create a map centered on the middle point
    m = folium.Map(location=[center_lat, center_lon], 
                  zoom_start=zoom_start,
                  tiles='OpenStreetMap')
    
    # Add markers for each location
    for feature in geojson_data['features']:
        lon, lat = feature['geometry']['coordinates']
        name = feature['properties'].get('name', 'Unnamed Location')
        description = feature['properties'].get('description', '')
        
        popup_text = f"{name}<br>{description}" if description else name
        
        folium.Marker(
            [lat, lon],  # Folium uses lat, lon order
            popup=popup_text,
            icon=folium.Icon(color='red', icon='info-sign')
        ).add_to(m)
    
    # Save as HTML first
    html_file = "temp_map.html"
    m.save(html_file)
    
    # Set up Chrome options for headless rendering
    chrome_options = Options()
    chrome_options.add_argument("--headless")  # Run in headless mode
    chrome_options.add_argument("--no-sandbox")
    chrome_options.add_argument("--disable-gpu")
    chrome_options.add_argument("--window-size=1024,768")
    
    # Initialize webdriver
    driver = webdriver.Chrome(options=chrome_options)
    
    # Load the HTML file
    driver.get(f"file://{os.path.abspath(html_file)}")
    
    # Wait for tiles to load
    time.sleep(2)
    
    # Take screenshot
    driver.save_screenshot(output_file)
    
    # Clean up
    driver.quit()
    os.remove(html_file)
    
    return output_file

# Example usage
if __name__ == "__main__":
    # Check if filename was provided
    if len(sys.argv) < 2:
        print("Usage: python script.py <geojson_file>")
        sys.exit(1)
    
    # Read GeoJSON from file
    try:
        with open(sys.argv[1], 'r') as f:
            geojson_data = json.load(f)
            
        # Create the map
        output_file = create_map_from_geojson(geojson_data)
        print(f"Map has been saved as {output_file}")
        
    except FileNotFoundError:
        print(f"Error: File '{sys.argv[1]}' not found")
        sys.exit(1)
    except json.JSONDecodeError:
        print(f"Error: File '{sys.argv[1]}' is not valid JSON")
        sys.exit(1)

```

--------------------------------------------------------------------------------
/geo.json:
--------------------------------------------------------------------------------

```json
{
  "type": "FeatureCollection",
  "features": [
    {
      "type": "Feature",
      "properties": {
        "name": "179 avenue du Général Leclerc",
        "description": "côté Rive Gauche"
      },
      "geometry": {
        "type": "Point",
        "coordinates": [2.2328097, 48.8300927]
      }
    },
    {
      "type": "Feature",
      "properties": {
        "name": "158 avenue du Général Leclerc",
        "description": "côté Rive Droite à l'angle de la rue Jules Herbron"
      },
      "geometry": {
        "type": "Point",
        "coordinates": [2.232786, 48.8300854]
      }
    },
    {
      "type": "Feature",
      "properties": {
        "name": "112 avenue du Général Leclerc",
        "description": "côté Rive Droite"
      },
      "geometry": {
        "type": "Point",
        "coordinates": [2.233788542758794, 48.830931500000005]
      }
    },
    {
      "type": "Feature",
      "properties": {
        "name": "34 avenue du Général Leclerc",
        "description": "côté Rive Droite"
      },
      "geometry": {
        "type": "Point",
        "coordinates": [2.2403389, 48.8328194]
      }
    }
    ,
    {
      "type": "Feature",
      "properties": {
        "name": "57 rue Gaston Boissier",
        "description": "à côté de la borne"
      },
      "geometry": {
        "type": "Point",
        "coordinates": [2.239832, 48.836534]
      }
    },
    {
      "type": "Feature",
      "properties": {
        "name": "6 avenue de Versailles",
        "description": "près du centre aquatique des Bertisettes"
      },
      "geometry": {
        "type": "Point",
        "coordinates": [2.1718888, 48.826612]
      }
    }
    ,
    {
      "type": "Feature",
      "properties": {
        "name": "Avenue Pierre Grenier",
        "description": "Placette croisement avenue Pierre Grenier / avenue Robert Hardouin"
      },
      "geometry": {
        "type": "Point",
        "coordinates": [2.2491541, 48.8264375]
      }
    },
    {
      "type": "Feature",
      "properties": {
        "name": "107 avenue Gaston Boissier",
        "description": "en face de la caserne des pompiers"
      },
      "geometry": {
        "type": "Point",
        "coordinates": [2.1849247, 48.8009949]
      }
    }
    ,
    {
      "type": "Feature",
      "properties": {
        "name": "Route du Pavé de Meudon",
        "description": "à côté du chêne de la Vierge",
        "note": "approximate location"
      },
      "geometry": {
        "type": "Point",
        "coordinates": [2.2345, 48.8250]
      }
    },
    {
      "type": "Feature",
      "properties": {
        "name": "Rue Costes et Bellonte",
        "description": "3 places sur parking",
        "note": "approximate location"
      },
      "geometry": {
        "type": "Point",
        "coordinates": [2.2428, 48.8315]
      }
    },
    {
      "type": "Feature",
      "properties": {
        "name": "Rue Joseph Chaleil",
        "description": "",
        "note": "approximate location"
      },
      "geometry": {
        "type": "Point",
        "coordinates": [2.2405, 48.8290]
      }
    },
    {
      "type": "Feature",
      "properties": {
        "name": "25 sente de la Procession",
        "description": "",
        "note": "approximate location"
      },
      "geometry": {
        "type": "Point",
        "coordinates": [2.2380, 48.8340]
      }
    },
    {
      "type": "Feature",
      "properties": {
        "name": "33 rue Joseph Bertrand",
        "description": "",
        "note": "approximate location"
      },
      "geometry": {
        "type": "Point",
        "coordinates": [2.2425, 48.8330]
      }
    },
    {
      "type": "Feature",
      "properties": {
        "name": "Place Saint Paul",
        "description": "",
        "note": "approximate location"
      },
      "geometry": {
        "type": "Point",
        "coordinates": [2.2450, 48.8345]
      }
    },
    {
      "type": "Feature",
      "properties": {
        "name": "Place de la bataille de Stalingrad",
        "description": "",
        "note": "approximate location"
      },
      "geometry": {
        "type": "Point",
        "coordinates": [2.2460, 48.8320]
      }
    }
  ]
}
```