Add MCP server

This commit is contained in:
Arnaud Delcasse
2025-11-03 11:45:23 +01:00
parent d992a7984f
commit 52de8d363e
18 changed files with 997 additions and 210 deletions

View File

@@ -1,7 +1,6 @@
package api
import (
"encoding/json"
"net/http"
)
@@ -14,13 +13,13 @@ func (h *Handler) GeoAutocomplete(w http.ResponseWriter, r *http.Request) {
text := t[0]
result, err := h.geoService.Autocomplete(text)
featureCollection, err := h.geoService.Autocomplete(text)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
return
}
j, err := json.Marshal(result.Features)
j, err := featureCollection.MarshalJSON()
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
return

View File

@@ -21,7 +21,12 @@ type Handler struct {
func NewHandler(cfg *viper.Viper, idp *identification.IdentificationProvider, appHandler *application.ApplicationHandler, cacheHandler cacheStorage.CacheHandler) *Handler {
cacheService := cache.NewCacheService(cacheHandler)
geoService := geo.NewGeoService(cfg.GetString("geo.pelias.url"))
// Get geocoding configuration
geoType := cfg.GetString("geo.type")
baseURL := cfg.GetString("geo." + geoType + ".url")
autocompleteEndpoint := cfg.GetString("geo." + geoType + ".autocomplete")
geoService := geo.NewGeoService(geoType, baseURL, autocompleteEndpoint)
return &Handler{
config: cfg,

View File

@@ -27,4 +27,5 @@ func (ws *WebServer) setupSolidarityTransportRoutes(appRouter *mux.Router) {
solidarityTransport.HandleFunc("/bookings/{bookingid}/confirm", ws.appHandler.SolidarityTransportBookingStatusHTTPHandler("confirm"))
solidarityTransport.HandleFunc("/bookings/{bookingid}/cancel", ws.appHandler.SolidarityTransportBookingStatusHTTPHandler("cancel"))
solidarityTransport.HandleFunc("/bookings/{bookingid}/waitconfirmation", ws.appHandler.SolidarityTransportBookingStatusHTTPHandler("waitconfirmation"))
solidarityTransport.HandleFunc("/bookings/{bookingid}/create-replacement", ws.appHandler.SolidarityTransportCreateReplacementBookingHTTPHandler())
}

View File

@@ -105,6 +105,8 @@ func (h *Handler) JourneysSearchHTTPHandler() http.HandlerFunc {
destinationGeo,
passengerID,
solidarityTransportExcludeDriver,
"", // solidarityExcludeGroupId - for modal search replacement only
nil, // options - use defaults
)
if err != nil {
log.Error().Err(err).Msg("error in journey search")
@@ -389,6 +391,8 @@ func (h *Handler) JourneysSearchCompactHTTPHandler() http.HandlerFunc {
destinationGeo,
passengerID,
solidarityTransportExcludeDriver,
"", // solidarityExcludeGroupId - for modal search replacement only
nil, // options - use defaults
)
if err != nil {
log.Error().Err(err).Msg("error in journey search")

View File

@@ -10,6 +10,8 @@ import (
"time"
groupstorage "git.coopgo.io/coopgo-platform/groups-management/storage"
mobilityaccountsstorage "git.coopgo.io/coopgo-platform/mobility-accounts/storage"
gen "git.coopgo.io/coopgo-platform/solidarity-transport/servers/grpc/proto/gen"
"git.coopgo.io/coopgo-apps/parcoursmob/core/utils/identification"
"github.com/gorilla/mux"
"github.com/rs/zerolog/log"
@@ -520,6 +522,7 @@ func (h *Handler) SolidarityTransportDriverJourneyHTTPHandler() http.HandlerFunc
driverID := vars["driverid"]
journeyID := vars["journeyid"]
passengerID := r.URL.Query().Get("passengerid")
replacesBookingID := r.URL.Query().Get("replaces_booking_id")
if r.Method == "POST" {
// Parse form data
@@ -531,7 +534,8 @@ func (h *Handler) SolidarityTransportDriverJourneyHTTPHandler() http.HandlerFunc
fmt.Sscanf(r.PostFormValue("return_waiting_time"), "%d", &returnWaitingTimeMinutes)
}
bookingID, err := h.applicationHandler.CreateSolidarityTransportJourneyBooking(r.Context(), driverID, journeyID, passengerID, motivation, message, doNotSend, returnWaitingTimeMinutes)
replacesBookingID := r.PostFormValue("replaces_booking_id")
bookingID, err := h.applicationHandler.CreateSolidarityTransportJourneyBooking(r.Context(), driverID, journeyID, passengerID, motivation, message, doNotSend, returnWaitingTimeMinutes, replacesBookingID)
if err != nil {
log.Error().Err(err).Msg("error creating solidarity transport journey booking")
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
@@ -559,7 +563,7 @@ func (h *Handler) SolidarityTransportDriverJourneyHTTPHandler() http.HandlerFunc
return
}
h.renderer.SolidarityTransportDriverJourney(w, r, result.Journey, result.Driver, result.Passenger, result.Beneficiaries, result.PassengerWalletBalance, result.PricingResult)
h.renderer.SolidarityTransportDriverJourney(w, r, result.Journey, result.Driver, result.Passenger, result.Beneficiaries, result.PassengerWalletBalance, result.PricingResult, replacesBookingID)
}
}
@@ -592,7 +596,141 @@ func (h *Handler) SolidarityTransportBookingDisplayHTTPHandler() http.HandlerFun
return
}
h.renderer.SolidarityTransportBookingDisplay(w, r, result.Booking, result.Driver, result.Passenger, result.PassengerWalletBalance)
// If booking is cancelled, search for replacement drivers
var replacementDrivers any
var replacementDriversMap map[string]mobilityaccountsstorage.Account
var replacementPricing map[string]map[string]interface{}
var replacementLocations map[string]string
if result.Booking.Status == "CANCELLED" {
// Initialize maps to avoid nil pointer in template
replacementDriversMap = make(map[string]mobilityaccountsstorage.Account)
replacementPricing = make(map[string]map[string]interface{})
replacementLocations = make(map[string]string)
searchResult, err := h.applicationHandler.SearchJourneys(
r.Context(),
result.Booking.Journey.PassengerPickupDate,
result.Booking.Journey.PassengerPickup,
result.Booking.Journey.PassengerDrop,
result.Booking.PassengerId,
result.Booking.DriverId, // Exclude the original driver
result.Booking.GroupId, // Exclude drivers with bookings in this group
nil, // options - use defaults
)
if err == nil {
replacementDrivers = searchResult.DriverJourneys
replacementDriversMap = searchResult.Drivers
// Calculate pricing for each replacement driver journey
for _, dj := range searchResult.DriverJourneys {
// Extract driver departure location
if dj.DriverDeparture != nil && dj.DriverDeparture.Serialized != "" {
var feature map[string]interface{}
if err := json.Unmarshal([]byte(dj.DriverDeparture.Serialized), &feature); err == nil {
if props, ok := feature["properties"].(map[string]interface{}); ok {
if name, ok := props["name"].(string); ok {
replacementLocations[dj.Id] = name
} else if label, ok := props["label"].(string); ok {
replacementLocations[dj.Id] = label
}
}
}
}
pricingResult, err := h.applicationHandler.CalculateSolidarityTransportPricing(r.Context(), dj, result.Booking.PassengerId)
if err == nil {
pricing := map[string]interface{}{
"passenger": map[string]interface{}{
"amount": pricingResult["passenger"].Amount,
"currency": pricingResult["passenger"].Currency,
},
"driver": map[string]interface{}{
"amount": pricingResult["driver"].Amount,
"currency": pricingResult["driver"].Currency,
},
}
replacementPricing[dj.Id] = pricing
}
}
}
}
h.renderer.SolidarityTransportBookingDisplay(w, r, result.Booking, result.Driver, result.Passenger, result.PassengerWalletBalance, replacementDrivers, replacementDriversMap, replacementPricing, replacementLocations)
}
}
func (h *Handler) SolidarityTransportCreateReplacementBookingHTTPHandler() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
if r.Method != "POST" {
http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed)
return
}
vars := mux.Vars(r)
oldBookingID := vars["bookingid"]
// Get the old booking to retrieve its data
oldBookingResult, err := h.applicationHandler.GetSolidarityTransportBookingData(r.Context(), oldBookingID)
if err != nil {
log.Error().Err(err).Msg("error retrieving old booking")
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
return
}
// Parse form data for new driver/journey
driverID := r.PostFormValue("driver_id")
journeyID := r.PostFormValue("journey_id")
message := r.PostFormValue("message")
doNotSend := r.PostFormValue("do_not_send") == "on"
// Use old booking's data
passengerID := oldBookingResult.Booking.PassengerId
motivation := ""
if oldBookingResult.Booking.Data != nil {
if m, ok := oldBookingResult.Booking.Data["motivation"].(string); ok {
motivation = m
}
}
// Get the new driver journey to retrieve journey information
driverJourneyResp, err := h.services.GRPC.SolidarityTransport.GetDriverJourney(r.Context(), &gen.GetDriverJourneyRequest{
DriverId: driverID,
JourneyId: journeyID,
})
if err != nil {
log.Error().Err(err).Msg("error retrieving new driver journey")
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
return
}
// Calculate return waiting time based on journey type
returnWaitingTimeMinutes := 30 // Default for round trips
if driverJourneyResp.DriverJourney.Noreturn {
returnWaitingTimeMinutes = 0
}
// Create the replacement booking with pricing calculated in CreateSolidarityTransportJourneyBooking
bookingID, err := h.applicationHandler.CreateSolidarityTransportJourneyBooking(
r.Context(),
driverID,
journeyID,
passengerID,
motivation,
message, // message from form
doNotSend, // doNotSend from form checkbox
returnWaitingTimeMinutes,
oldBookingID,
)
if err != nil {
log.Error().Err(err).Msg("error creating replacement booking")
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
return
}
log.Info().Str("booking_id", bookingID).Str("replaces", oldBookingID).Msg("Replacement booking created successfully")
// Redirect to the new booking
http.Redirect(w, r, fmt.Sprintf("/app/solidarity-transport/bookings/%s", bookingID), http.StatusFound)
}
}