# 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.

## 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:** 
## 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]
}
}
]
}
```