MCP Logistics — Spécification des outils & Docker Compose (France + Madagascar)



MCP Logistics — Spécification des outils & Docker Compose (France + Madagascar)

Ce document fournit :

  1. une spécification JSON des outils MCP de logistique (géocodage, itinéraire, matrices, optimisation),
  2. un docker-compose prêt pour un PoC avec OSRM (France + Madagascar), VROOM (VRP/TSP open‑source), GraphHopper (profils camion), Redis (cache), et un serveur MCP “logistics” (squelette).

Remarque : placez les fichiers .osm.pbf (extraits OSM) dans ./data/osm/ avant de lancer.

1) Spécification MCP (JSON)

{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "mcp_server": {
    "name": "logistics",
    "version": "0.1.0",
    "auth": { "type": "bearer", "header": "Authorization" },
    "tools": [
      {
        "name": "geocode",
        "description": "Géocode une adresse ou un POI en (lat, lon). Peut utiliser Nominatim, Google Geocoding, etc.",
        "input_schema": {
          "type": "object",
          "properties": {
            "query": {"type": "string"},
            "country_hint": {"type": "string", "description": "Code pays ISO‑3166‑1 alpha‑2, ex. FR, MG"},
            "provider": {"type": "string", "enum": ["nominatim","google"], "default": "nominatim"}
          },
          "required": ["query"]
        },
        "output_schema": {
          "type": "object",
          "properties": {
            "lat": {"type": "number"},
            "lon": {"type": "number"},
            "precision": {"type": "string", "enum": ["rooftop","range_interpolated","street","place"]},
            "raw": {"type": "object"}
          },
          "required": ["lat","lon"]
        }
      },
      {
        "name": "route_plan",
        "description": "Calcule un itinéraire A→B (ou avec étapes) et retourne distance, durée, ETA et géométrie.",
        "input_schema": {
          "type": "object",
          "properties": {
            "origin": {"type": "object", "properties": {"lat": {"type": "number"}, "lon": {"type": "number"}}, "required": ["lat","lon"]},
            "destination": {"type": "object", "properties": {"lat": {"type": "number"}, "lon": {"type": "number"}}, "required": ["lat","lon"]},
            "waypoints": {"type": "array", "items": {"type": "object", "properties": {"lat": {"type": "number"}, "lon": {"type": "number"}}, "required": ["lat","lon"]}},
            "vehicle_profile": {"type": "string", "enum": ["car","van","truck","moto","bike","foot"], "default": "car"},
            "departure_time": {"type": "string", "format": "date-time"},
            "avoid": {"type": "array", "items": {"type": "string", "enum": ["tolls","ferries","unpaved","low_emission_zones"]}},
            "router": {"type": "string", "enum": ["osrm-fr","osrm-mg","graphhopper"], "description": "Force un moteur; sinon auto"}
          },
          "required": ["origin","destination"]
        },
        "output_schema": {
          "type": "object",
          "properties": {
            "distance_m": {"type": "integer"},
            "duration_s": {"type": "integer"},
            "eta_iso": {"type": "string"},
            "geometry_polyline": {"type": "string"},
            "steps": {"type": "array", "items": {"type": "object", "properties": {
              "instr": {"type": "string"},
              "distance_m": {"type": "integer"},
              "duration_s": {"type": "integer"},
              "road": {"type": "string"}
            }}}
          },
          "required": ["distance_m","duration_s","geometry_polyline"]
        }
      },
      {
        "name": "distance_matrix",
        "description": "Calcule une matrice Origins×Destinations (durées/distances).",
        "input_schema": {
          "type": "object",
          "properties": {
            "origins": {"type": "array", "minItems": 1, "items": {"type": "object", "properties": {"lat": {"type": "number"}, "lon": {"type": "number"}}, "required": ["lat","lon"]}},
            "destinations": {"type": "array", "minItems": 1, "items": {"type": "object", "properties": {"lat": {"type": "number"}, "lon": {"type": "number"}}, "required": ["lat","lon"]}},
            "vehicle_profile": {"type": "string", "enum": ["car","van","truck","moto","bike","foot"], "default": "car"},
            "router": {"type": "string", "enum": ["osrm-fr","osrm-mg","graphhopper","google"]}
          },
          "required": ["origins","destinations"]
        },
        "output_schema": {
          "type": "object",
          "properties": {
            "matrix_seconds": {"type": "array", "items": {"type": "array", "items": {"type": "integer"}}},
            "matrix_meters": {"type": "array", "items": {"type": "array", "items": {"type": "integer"}}}
          },
          "required": ["matrix_seconds"]
        }
      },
      {
        "name": "optimize_stops",
        "description": "Optimise l’ordre des arrêts (TSP/VRP). Utilise VROOM (self‑host) ou un provider externe.",
        "input_schema": {
          "type": "object",
          "properties": {
            "depot": {"type": "object", "properties": {"lat": {"type": "number"}, "lon": {"type": "number"}}, "required": ["lat","lon"]},
            "stops": {"type": "array", "minItems": 1, "items": {"type": "object", "properties": {
              "id": {"type": "string"},
              "lat": {"type": "number"},
              "lon": {"type": "number"},
              "demand_kg": {"type": "number"},
              "time_window": {"type": "array", "items": {"type": "string"}, "minItems": 2, "maxItems": 2}
            }, "required": ["lat","lon"]}},
            "vehicle_profile": {"type": "string", "enum": ["van","truck","car"], "default": "van"},
            "capacity_kg": {"type": "number"},
            "router": {"type": "string", "enum": ["vroom-osrm-fr","vroom-osrm-mg"]}
          },
          "required": ["depot","stops"]
        },
        "output_schema": {
          "type": "object",
          "properties": {
            "ordered_stops": {"type": "array", "items": {"type": "object", "properties": {
              "id": {"type": "string"},
              "eta_iso": {"type": "string"},
              "sequence": {"type": "integer"}
            }}},
            "total_distance_m": {"type": "integer"},
            "total_duration_s": {"type": "integer"}
          },
          "required": ["ordered_stops"]
        }
      },
      {
        "name": "eta_live",
        "description": "Donne une ETA (avec trafic si provider compatible).",
        "input_schema": {
          "type": "object",
          "properties": {
            "origin": {"type": "object", "properties": {"lat": {"type": "number"}, "lon": {"type": "number"}}, "required": ["lat","lon"]},
            "destination": {"type": "object", "properties": {"lat": {"type": "number"}, "lon": {"type": "number"}}, "required": ["lat","lon"]},
            "provider": {"type": "string", "enum": ["google","graphhopper"], "default": "google"},
            "departure_time": {"type": "string", "format": "date-time"}
          },
          "required": ["origin","destination"]
        },
        "output_schema": {
          "type": "object",
          "properties": {
            "eta_iso": {"type": "string"},
            "duration_s": {"type": "integer"}
          },
          "required": ["eta_iso","duration_s"]
        }
      },
      {
        "name": "map_snapshot",
        "description": "Génère une image statique (snapshot) d’un itinéraire ou point.",
        "input_schema": {
          "type": "object",
          "properties": {
            "geometry_polyline": {"type": "string"},
            "provider": {"type": "string", "enum": ["staticmaps","google"], "default": "staticmaps"},
            "size": {"type": "string", "pattern": "^\\d+x\\d+$", "default": "800x600"}
          },
          "required": ["geometry_polyline"]
        },
        "output_schema": {
          "type": "object",
          "properties": {"png_data_uri": {"type": "string"}}
        }
      },
      {
        "name": "geofence_check",
        "description": "Teste si un point est dans une (ou plusieurs) zone(s) (polygones).",
        "input_schema": {
          "type": "object",
          "properties": {
            "point": {"type": "object", "properties": {"lat": {"type": "number"}, "lon": {"type": "number"}}, "required": ["lat","lon"]},
            "polygons": {"type": "array", "items": {"type": "array", "items": {"type": "array", "items": {"type": "number"}}}}
          },
          "required": ["point","polygons"]
        },
        "output_schema": {
          "type": "object",
          "properties": {"inside": {"type": "boolean"}, "which_ids": {"type": "array", "items": {"type": "integer"}}},
          "required": ["inside"]
        }
      }
    ],
    "errors": [
      {"code": "ROUTER_UNAVAILABLE", "message": "Le moteur de routage demandé est indisponible."},
      {"code": "GEOCODING_FAILED", "message": "Échec du géocodage pour la requête."},
      {"code": "NO_FEASIBLE_ROUTE", "message": "Aucun itinéraire réalisable avec les contraintes."},
      {"code": "OPTIMIZATION_FAILED", "message": "Échec de l’optimisation TSP/VRP."},
      {"code": "RATE_LIMIT", "message": "Limite d’appels atteinte."},
      {"code": "INVALID_INPUT", "message": "Paramètres manquants ou invalides."}
    ]
  }
}

2) docker-compose.yml (PoC France + Madagascar)

Pré‑requis :

  • Placez france-latest.osm.pbf & madagascar-latest.osm.pbf dans ./data/osm/ (depuis geofabrik.de).
  • Les conteneurs OSRM lanceront osrm-extract et osrm-customize au démarrage.
version: "3.9"

services:
  osrm-fr:
    image: osrm/osrm-backend:latest
    container_name: osrm-fr
    command: >
      bash -lc "osrm-extract -p /opt/car.lua /data/osm/france-latest.osm.pbf &&
                osrm-partition /data/osm/france-latest.osrm &&
                osrm-customize /data/osm/france-latest.osrm &&
                osrm-routed --algorithm mld /data/osm/france-latest.osrm"
    volumes:
      - ./data:/data
    ports:
      - "5000:5000"
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:5000/health"]
      interval: 20s
      timeout: 5s
      retries: 10

  osrm-mg:
    image: osrm/osrm-backend:latest
    container_name: osrm-mg
    command: >
      bash -lc "osrm-extract -p /opt/car.lua /data/osm/madagascar-latest.osm.pbf &&
                osrm-partition /data/osm/madagascar-latest.osrm &&
                osrm-customize /data/osm/madagascar-latest.osrm &&
                osrm-routed --algorithm mld -p 5001 /data/osm/madagascar-latest.osrm"
    volumes:
      - ./data:/data
    ports:
      - "5001:5001"

  graphhopper:
    image: ghcr.io/graphhopper/graphhopper:latest
    container_name: graphhopper
    environment:
      - JAVA_OPTS=-Xmx6g -Xms6g
    command: |
      -Ddw.graphhopper.datareader.file=/data/osm/madagascar-latest.osm.pbf \
      server config=/data/graphhopper/config.yml
    volumes:
      - ./data:/data
    ports:
      - "8989:8989"

  vroom:
    image: vroomvrp/vroom-docker:latest
    container_name: vroom
    environment:
      - VROOM_ROUTER=osrm
      - OSRM_PORT=5000
      - OSRM_HOST=osrm-fr
    depends_on:
      - osrm-fr
    ports:
      - "3000:3000"

  redis:
    image: redis:7-alpine
    container_name: redis
    ports:
      - "6379:6379"

  mcp-logistics:
    image: node:20-alpine
    container_name: mcp-logistics
    working_dir: /app
    command: node dist/index.js
    volumes:
      - ./mcp-server:/app
    environment:
      - PORT=3030
      - REDIS_URL=redis://redis:6379
      - OSRM_FR_URL=http://osrm-fr:5000
      - OSRM_MG_URL=http://osrm-mg:5001
      - VROOM_URL=http://vroom:3000
      - GRAPHHOPPER_URL=http://graphhopper:8989
      - GOOGLE_MAPS_API_KEY=${GOOGLE_MAPS_API_KEY:-}
    depends_on:
      - osrm-fr
      - osrm-mg
      - vroom
      - graphhopper
      - redis
    ports:
      - "3030:3030"

networks:
  default:
    name: logistics-net

3) data/graphhopper/config.yml (profils van & truck)

graphhopper:
  graph.location: /data/graphhopper/gh
  datareader.file: /data/osm/madagascar-latest.osm.pbf
  prepare.ch.weightings: fastest
  profiles:
    - name: van
      vehicle: car
      weighting: fastest
      turn_costs: true
    - name: truck
      vehicle: car
      weighting: fastest
      turn_costs: true
      custom_model_file: /data/graphhopper/custom_models/truck.json
  custom_models.directory: /data/graphhopper/custom_models
  routing.non_ch:
    enabled: true
server:
  application_connectors:
    - type: http
      port: 8989
  admin_connectors:
    - type: http
      port: 8990

data/graphhopper/custom_models/truck.json (exemple simplifié)

{
  "priority": [
    {"if": "road_class == MOTORWAY", "multiply_by": 0.9},
    {"if": "tunnel == YES", "multiply_by": 0.8}
  ],
  "speed": [
    {"if": "road_class == MOTORWAY", "limit_to": 90},
    {"if": "road_environment == URBAN", "limit_to": 50}
  ],
  "areas": {
    "restricted": {
      "type": "FeatureCollection",
      "features": []
    }
  },
  "priority_area": [
    {"if": "in_area('restricted')", "multiply_by": 0.5}
  ]
}

Pour des contraintes hauteur/poids, ajoutez des règles spécifiques si vos données OSM locales les exposent ; sinon, complétez via zones interdites (GeoJSON) ou utilisez Google Routes (Restrictions camion) côté eta_live.

4) Variables d’environnement (MCP server)

Créez mcp-server/.env (chargé au runtime) :

PORT=3030
REDIS_URL=redis://redis:6379
OSRM_FR_URL=http://osrm-fr:5000
OSRM_MG_URL=http://osrm-mg:5001
VROOM_URL=http://vroom:3000
GRAPHHOPPER_URL=http://graphhopper:8989
GOOGLE_MAPS_API_KEY=

5) Smoke tests (cURL)

OSRM France — route simple

curl "http://localhost:5000/route/v1/driving/2.3522,48.8566;5.3698,43.2965?overview=full&steps=true"

VROOM (optimisation) — mini TSP

curl -H "Content-Type: application/json" -d '{
  "vehicles":[{"id":1,"start":[2.3522,48.8566]}],
  "jobs":[{"id":11,"location":[2.295,48.873]}, {"id":12,"location":[2.36,48.85]}]
}' http://localhost:3000

GraphHopper — profil truck

curl "http://localhost:8989/route?profile=truck&point=48.8566,2.3522&point=43.2965,5.3698&points_encoded=false"

6) Notes d’intégration Odoo

  • Exposer ces outils MCP à un assistant appelé depuis :
    • Odoo Sales/CRM → bouton « Calculer prix & ETA » (devis, livraison).
    • Odoo Stock/Delivery → génération de tournée (VROOM), affectation livreur.
    • WhatsApp bot → intention « devis transport » → geocode → distance_matrix → règle tarifaire.
  • Tarification : créez une grille (€/km + palier poids) dans Odoo ; le MCP renvoie la distance/ETA, Odoo calcule le prix.

7) Checklist PoC

  • PBF FR & MG dans ./data/osm/.
  • docker compose up -d.
  • Tester OSRM, VROOM, GraphHopper avec les cURL ci‑dessus.
  • Appeler route_plan & optimize_stops via MCP (agent IA ou Postman).
  • Brancher un bouton de test dans Odoo (devis/livraison) qui appelle route_plan.