#
tokens: 4178/50000 7/7 files
lines: on (toggle) GitHub
raw markdown copy reset
# Directory Structure

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

# Files

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

```
1 | 3.13
2 | 
```

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

```
1 | .env
2 | __pycache__/
```

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

```markdown
 1 | # Geoapify MCP Server
 2 | 
 3 | Convert addresses into GPS coordinates for mapping, and optionally create an image of those coordinates using the Geoapify server.
 4 | 
 5 | ![Example Map](./temp_map.png)
 6 | 
 7 | ## Installation
 8 | 
 9 | You'll need to get an API key from [Geoapify](https://www.geoapify.com/), and set it as an environment variable named `GEO_APIKEY`.
10 | 
11 | Your `claude_desktop_config.json` will look like this after:
12 | 
13 | ```json
14 | "MCP Map Demo": {
15 |       "command": "uv",
16 |       "args": [
17 | 	"--directory",
18 |         "/PATH/TO/THIS/REPO",
19 |         "run",
20 |         "--with",
21 |         "fastmcp",
22 |         "--with",
23 |         "requests",
24 |         "--with",
25 |         "folio",
26 |         "--with",
27 |         "selenium",
28 |         "--with",
29 |         "pillow",
30 |         "fastmcp",
31 |         "run",
32 |         "/PATH/TO/THIS/REPO/server.py"
33 |       ],
34 |       "env": {
35 |         "GEO_APIKEY": "YOURAPIKEY"
36 |       }
37 |     }
38 | ```
39 | 
40 | You'll notice we include all the dependencies in our `args`.
41 | 
42 | ## Tools
43 | 
44 | `get_gps_coordinates`
45 | 
46 | Used to get GPS coordinates from the API for creating GEOJSON, etc.
47 | 
48 | `create_map_from_geojson`
49 | 
50 | Create a map image and show it. (Showing only works on MacOS for now.)
51 | 
52 | 
53 | ## Example Usage
54 | 
55 | **Get GPS Coordinates** 
56 | 
57 | ```
58 | can you create a geojson of the following locations including their gps coordinates: 179 avenue du Général Leclerc, côté Rive Gauche
59 | 158 avenue du Général Leclerc, côté Rive Droite à l'angle de la rue Jules Herbron
60 | 112 avenue du Général Leclerc, côté Rive Droite
61 | 34 avenue du Général Leclerc, côté Rive Droite
62 | En face du 57 rue Gaston Boissier, à côté de la borne
63 | Route du Pavé de Meudon - à côté du chêne de la Vierge
64 | 6 avenue de Versailles (près du centre aquatique des Bertisettes)
65 | 3 places sur parking de la rue Costes et Bellonte
66 | Rue Joseph Chaleil
67 | 18 rue des Sables – à côté de la crèche
68 | 25 sente de la Procession
69 | 33 rue Joseph Bertrand
70 | Place Saint Paul
71 | Place de la bataille de Stalingrad
72 | Placette croisement avenue Pierre Grenier / avenue Robert Hardouin
73 | 107 avenue Gaston Boissier (en face de la caserne des pompiers)
74 | ```
75 | 
76 | **Result:** [Attached JSON file](./geo.json)
77 | 
78 | Returns a GeoJSON file.
79 | 
80 | **Create a Map Image**
81 | 
82 | ```
83 | can you create a map from my attached geojson file?
84 | ```
85 | [Attached JSON file](./geo.json)
86 | 
87 | **Result:** ![temp map](./temp_map.png)
88 | 
89 | ## LICENSE
90 | 
91 | MIT
92 | 
```

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

```toml
 1 | [project]
 2 | name = "map-mcp"
 3 | version = "0.1.0"
 4 | description = "Add your description here"
 5 | readme = "README.md"
 6 | requires-python = ">=3.13"
 7 | dependencies = [
 8 |     "folium>=0.19.4",
 9 |     "mcp[cli]===1.2.0rc1",
10 |     "requests>=2.32.3",
11 |     "selenium>=4.27.1",
12 | ]
13 | 
```

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

```python
 1 | # server.py
 2 | from mcp.server.fastmcp import FastMCP, Image
 3 | import requests
 4 | from requests.structures import CaseInsensitiveDict
 5 | from urllib.parse import quote
 6 | import os
 7 | import generate_image
 8 | from PIL import Image as PILImage
 9 | import subprocess
10 | 
11 | APIKEY = os.environ.get("GEO_APIKEY")
12 | if APIKEY is None:
13 |     raise ValueError("GEO_APIKEY environment variable is not set")
14 | 
15 | # Create an MCP server
16 | mcp = FastMCP("Map Demo", dependencies=["requests", "pillow", "selenium", "folium"])
17 | 
18 | def get_geocode(search_address, api_key):
19 |     # URL encode the address to handle special characters
20 |     encoded_address = quote(search_address)
21 |     
22 |     url = f"https://api.geoapify.com/v1/geocode/search?text={encoded_address}&apiKey={api_key}"
23 |     
24 |     headers = CaseInsensitiveDict()
25 |     headers["Accept"] = "application/json"
26 |     
27 |     resp = requests.get(url, headers=headers)
28 |     return resp.json()
29 | 
30 | # Get GPS coordinates for an address
31 | @mcp.tool()
32 | def get_gps_coordinates(address: str) -> dict:
33 |     """Gets GPS coordinates for an address"""
34 |     return get_geocode(address, APIKEY)
35 | 
36 | @mcp.tool()
37 | def create_map_from_geojson(filename: str, geojson_coordinates: dict) -> str:
38 |     """Creates a map image from GeoJSON coordinates"""
39 |     generate_image.create_map_from_geojson(geojson_coordinates, "temp_map.png")
40 |     subprocess.run(["open", "temp_map.png"])
41 |     return f"Map image created at {os.curdir}/temp_map.png, and shown to user."
42 | 
43 | if __name__ == "__main__":
44 |     mcp.run() 
```

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

```python
 1 | import folium
 2 | import json
 3 | from selenium import webdriver
 4 | from selenium.webdriver.chrome.options import Options
 5 | import time
 6 | import os
 7 | import sys
 8 | 
 9 | 
10 | import logging
11 | 
12 | logging.basicConfig(
13 |     filename='generator.log',
14 |     level=logging.INFO,
15 |     format='%(asctime)s - %(levelname)s - %(message)s'
16 | )
17 | 
18 | def create_map_from_geojson(geojson_data, output_file="map.png", zoom_start=15):
19 |     # Calculate the center point for the map
20 |     coordinates = []
21 |     for feature in geojson_data['features']:
22 |         lon, lat = feature['geometry']['coordinates']
23 |         coordinates.append([lat, lon])  # Folium uses lat, lon order
24 |     
25 |     center_lat = sum(coord[0] for coord in coordinates) / len(coordinates)
26 |     center_lon = sum(coord[1] for coord in coordinates) / len(coordinates)
27 |     
28 |     # Create a map centered on the middle point
29 |     m = folium.Map(location=[center_lat, center_lon], 
30 |                   zoom_start=zoom_start,
31 |                   tiles='OpenStreetMap')
32 |     
33 |     # Add markers for each location
34 |     for feature in geojson_data['features']:
35 |         lon, lat = feature['geometry']['coordinates']
36 |         name = feature['properties'].get('name', 'Unnamed Location')
37 |         description = feature['properties'].get('description', '')
38 |         
39 |         popup_text = f"{name}<br>{description}" if description else name
40 |         
41 |         folium.Marker(
42 |             [lat, lon],  # Folium uses lat, lon order
43 |             popup=popup_text,
44 |             icon=folium.Icon(color='red', icon='info-sign')
45 |         ).add_to(m)
46 |     
47 |     # Save as HTML first
48 |     html_file = "temp_map.html"
49 |     m.save(html_file)
50 |     
51 |     # Set up Chrome options for headless rendering
52 |     chrome_options = Options()
53 |     chrome_options.add_argument("--headless")  # Run in headless mode
54 |     chrome_options.add_argument("--no-sandbox")
55 |     chrome_options.add_argument("--disable-gpu")
56 |     chrome_options.add_argument("--window-size=1024,768")
57 |     
58 |     # Initialize webdriver
59 |     driver = webdriver.Chrome(options=chrome_options)
60 |     
61 |     # Load the HTML file
62 |     driver.get(f"file://{os.path.abspath(html_file)}")
63 |     
64 |     # Wait for tiles to load
65 |     time.sleep(2)
66 |     
67 |     # Take screenshot
68 |     driver.save_screenshot(output_file)
69 |     
70 |     # Clean up
71 |     driver.quit()
72 |     os.remove(html_file)
73 |     
74 |     return output_file
75 | 
76 | # Example usage
77 | if __name__ == "__main__":
78 |     # Check if filename was provided
79 |     if len(sys.argv) < 2:
80 |         print("Usage: python script.py <geojson_file>")
81 |         sys.exit(1)
82 |     
83 |     # Read GeoJSON from file
84 |     try:
85 |         with open(sys.argv[1], 'r') as f:
86 |             geojson_data = json.load(f)
87 |             
88 |         # Create the map
89 |         output_file = create_map_from_geojson(geojson_data)
90 |         print(f"Map has been saved as {output_file}")
91 |         
92 |     except FileNotFoundError:
93 |         print(f"Error: File '{sys.argv[1]}' not found")
94 |         sys.exit(1)
95 |     except json.JSONDecodeError:
96 |         print(f"Error: File '{sys.argv[1]}' is not valid JSON")
97 |         sys.exit(1)
98 | 
```

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

```json
  1 | {
  2 |   "type": "FeatureCollection",
  3 |   "features": [
  4 |     {
  5 |       "type": "Feature",
  6 |       "properties": {
  7 |         "name": "179 avenue du Général Leclerc",
  8 |         "description": "côté Rive Gauche"
  9 |       },
 10 |       "geometry": {
 11 |         "type": "Point",
 12 |         "coordinates": [2.2328097, 48.8300927]
 13 |       }
 14 |     },
 15 |     {
 16 |       "type": "Feature",
 17 |       "properties": {
 18 |         "name": "158 avenue du Général Leclerc",
 19 |         "description": "côté Rive Droite à l'angle de la rue Jules Herbron"
 20 |       },
 21 |       "geometry": {
 22 |         "type": "Point",
 23 |         "coordinates": [2.232786, 48.8300854]
 24 |       }
 25 |     },
 26 |     {
 27 |       "type": "Feature",
 28 |       "properties": {
 29 |         "name": "112 avenue du Général Leclerc",
 30 |         "description": "côté Rive Droite"
 31 |       },
 32 |       "geometry": {
 33 |         "type": "Point",
 34 |         "coordinates": [2.233788542758794, 48.830931500000005]
 35 |       }
 36 |     },
 37 |     {
 38 |       "type": "Feature",
 39 |       "properties": {
 40 |         "name": "34 avenue du Général Leclerc",
 41 |         "description": "côté Rive Droite"
 42 |       },
 43 |       "geometry": {
 44 |         "type": "Point",
 45 |         "coordinates": [2.2403389, 48.8328194]
 46 |       }
 47 |     }
 48 |     ,
 49 |     {
 50 |       "type": "Feature",
 51 |       "properties": {
 52 |         "name": "57 rue Gaston Boissier",
 53 |         "description": "à côté de la borne"
 54 |       },
 55 |       "geometry": {
 56 |         "type": "Point",
 57 |         "coordinates": [2.239832, 48.836534]
 58 |       }
 59 |     },
 60 |     {
 61 |       "type": "Feature",
 62 |       "properties": {
 63 |         "name": "6 avenue de Versailles",
 64 |         "description": "près du centre aquatique des Bertisettes"
 65 |       },
 66 |       "geometry": {
 67 |         "type": "Point",
 68 |         "coordinates": [2.1718888, 48.826612]
 69 |       }
 70 |     }
 71 |     ,
 72 |     {
 73 |       "type": "Feature",
 74 |       "properties": {
 75 |         "name": "Avenue Pierre Grenier",
 76 |         "description": "Placette croisement avenue Pierre Grenier / avenue Robert Hardouin"
 77 |       },
 78 |       "geometry": {
 79 |         "type": "Point",
 80 |         "coordinates": [2.2491541, 48.8264375]
 81 |       }
 82 |     },
 83 |     {
 84 |       "type": "Feature",
 85 |       "properties": {
 86 |         "name": "107 avenue Gaston Boissier",
 87 |         "description": "en face de la caserne des pompiers"
 88 |       },
 89 |       "geometry": {
 90 |         "type": "Point",
 91 |         "coordinates": [2.1849247, 48.8009949]
 92 |       }
 93 |     }
 94 |     ,
 95 |     {
 96 |       "type": "Feature",
 97 |       "properties": {
 98 |         "name": "Route du Pavé de Meudon",
 99 |         "description": "à côté du chêne de la Vierge",
100 |         "note": "approximate location"
101 |       },
102 |       "geometry": {
103 |         "type": "Point",
104 |         "coordinates": [2.2345, 48.8250]
105 |       }
106 |     },
107 |     {
108 |       "type": "Feature",
109 |       "properties": {
110 |         "name": "Rue Costes et Bellonte",
111 |         "description": "3 places sur parking",
112 |         "note": "approximate location"
113 |       },
114 |       "geometry": {
115 |         "type": "Point",
116 |         "coordinates": [2.2428, 48.8315]
117 |       }
118 |     },
119 |     {
120 |       "type": "Feature",
121 |       "properties": {
122 |         "name": "Rue Joseph Chaleil",
123 |         "description": "",
124 |         "note": "approximate location"
125 |       },
126 |       "geometry": {
127 |         "type": "Point",
128 |         "coordinates": [2.2405, 48.8290]
129 |       }
130 |     },
131 |     {
132 |       "type": "Feature",
133 |       "properties": {
134 |         "name": "25 sente de la Procession",
135 |         "description": "",
136 |         "note": "approximate location"
137 |       },
138 |       "geometry": {
139 |         "type": "Point",
140 |         "coordinates": [2.2380, 48.8340]
141 |       }
142 |     },
143 |     {
144 |       "type": "Feature",
145 |       "properties": {
146 |         "name": "33 rue Joseph Bertrand",
147 |         "description": "",
148 |         "note": "approximate location"
149 |       },
150 |       "geometry": {
151 |         "type": "Point",
152 |         "coordinates": [2.2425, 48.8330]
153 |       }
154 |     },
155 |     {
156 |       "type": "Feature",
157 |       "properties": {
158 |         "name": "Place Saint Paul",
159 |         "description": "",
160 |         "note": "approximate location"
161 |       },
162 |       "geometry": {
163 |         "type": "Point",
164 |         "coordinates": [2.2450, 48.8345]
165 |       }
166 |     },
167 |     {
168 |       "type": "Feature",
169 |       "properties": {
170 |         "name": "Place de la bataille de Stalingrad",
171 |         "description": "",
172 |         "note": "approximate location"
173 |       },
174 |       "geometry": {
175 |         "type": "Point",
176 |         "coordinates": [2.2460, 48.8320]
177 |       }
178 |     }
179 |   ]
180 | }
```