#!/usr/bin/env python3
import asyncio
from typing import Optional
from mcp.server import Server
from mcp.types import Tool, TextContent
import mcp.server.stdio
import requests
from functools import lru_cache
# Configuration de l'API BAN
BAN_SEARCH_URL = "https://api-adresse.data.gouv.fr/search/"
@lru_cache(maxsize=10_000)
def geocode_ban_simple(
adresse: str,
ville: Optional[str] = None,
postcode: Optional[str] = None,
citycode: Optional[str] = None,
) -> tuple[Optional[float], Optional[float], Optional[str]]:
"""
Géocode une adresse via l'API BAN.
Args:
adresse: L'adresse à géocoder
ville: Le nom de la ville (optionnel)
postcode: Le code postal (optionnel)
citycode: Le code INSEE (optionnel)
Returns:
Tuple (latitude, longitude, label) ou (None, None, None)
"""
if not adresse or not str(adresse).strip():
return None, None, None
q = str(adresse).strip()
if ville:
q = f"{q}, {ville}"
params = {"q": q, "limit": 1, "autocomplete": 1}
if postcode:
params["postcode"] = postcode
if citycode:
params["citycode"] = citycode
try:
r = requests.get(BAN_SEARCH_URL, params=params, timeout=5)
r.raise_for_status()
feats = r.json().get("features", [])
if not feats:
return None, None, None
f = feats[0]
lon, lat = f["geometry"]["coordinates"]
label = f["properties"].get("label")
return float(lat), float(lon), label
except Exception as e:
print(f"Erreur lors du géocodage: {e}")
return None, None, None
# Création du serveur MCP
app = Server("geocode-ban-server")
@app.list_tools()
async def list_tools() -> list[Tool]:
"""Liste les outils disponibles."""
return [
Tool(
name="geocode_address",
description=(
"Géocode une adresse française via l'API BAN (Base Adresse Nationale). "
"Retourne les coordonnées GPS (latitude, longitude) et le label standardisé."
),
inputSchema={
"type": "object",
"properties": {
"adresse": {
"type": "string",
"description": "L'adresse à géocoder (ex: '5 place Joseph Frantz')",
},
"ville": {
"type": "string",
"description": "Nom de la ville (optionnel, ex: 'Paris')",
},
"postcode": {
"type": "string",
"description": "Code postal (optionnel, ex: '75001')",
},
"citycode": {
"type": "string",
"description": "Code INSEE de la commune (optionnel)",
},
},
"required": ["adresse"],
},
)
]
@app.call_tool()
async def call_tool(name: str, arguments: dict) -> list[TextContent]:
"""Exécute l'outil de géocodage."""
if name != "geocode_address":
raise ValueError(f"Outil inconnu: {name}")
adresse = arguments.get("adresse")
if not adresse:
return [
TextContent(type="text", text="❌ Erreur: l'argument 'adresse' est requis.")
]
ville = arguments.get("ville")
postcode = arguments.get("postcode")
citycode = arguments.get("citycode")
# Géocodage
lat, lon, label = geocode_ban_simple(adresse, ville, postcode, citycode)
if lat is None or lon is None:
return [
TextContent(
type="text",
text=f"❌ Aucun résultat trouvé pour: {adresse}",
)
]
response = f"""
✅ Adresse géocodée avec succès !
📍 Adresse standardisée: {label or adresse}
📌 Latitude: {lat}
📌 Longitude: {lon}
🔗 Google Maps: https://www.google.com/maps?q={lat},{lon}
🔗 OpenStreetMap: https://www.openstreetmap.org/?mlat={lat}&mlon={lon}&zoom=17
"""
return [TextContent(type="text", text=response)]
async def main() -> None:
"""Point d'entrée principal du serveur MCP."""
async with mcp.server.stdio.stdio_server() as (read_stream, write_stream):
await app.run(
read_stream, write_stream, app.create_initialization_options()
)
def run() -> None:
"""Fonction wrapper pour le script d'entrée."""
asyncio.run(main())
if __name__ == "__main__":
run()