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