159 lines
		
	
	
		
			5.5 KiB
		
	
	
	
		
			Go
		
	
	
	
			
		
		
	
	
			159 lines
		
	
	
		
			5.5 KiB
		
	
	
	
		
			Go
		
	
	
	
package mcp
 | 
						|
 | 
						|
import (
 | 
						|
	"context"
 | 
						|
	"fmt"
 | 
						|
	"time"
 | 
						|
 | 
						|
	"git.coopgo.io/coopgo-apps/parcoursmob/core/application"
 | 
						|
	mcpsdk "github.com/modelcontextprotocol/go-sdk/mcp"
 | 
						|
	"github.com/paulmach/orb/geojson"
 | 
						|
	"github.com/rs/zerolog/log"
 | 
						|
)
 | 
						|
 | 
						|
// JourneySearchInput represents the input for journey search
 | 
						|
type JourneySearchInput struct {
 | 
						|
	Departure        string   `json:"departure" jsonschema:"Departure address as text (e.g. Paris or Gare de Lyon Paris)"`
 | 
						|
	Destination      string   `json:"destination" jsonschema:"Destination address as text (e.g. Lyon or Part-Dieu Lyon)"`
 | 
						|
	DepartureDate    string   `json:"departure_date" jsonschema:"Departure date in YYYY-MM-DD format"`
 | 
						|
	DepartureTime    string   `json:"departure_time" jsonschema:"Departure time in HH:MM format (24-hour)"`
 | 
						|
	PassengerID      string   `json:"passenger_id,omitempty" jsonschema:"Optional passenger ID to retrieve address from account"`
 | 
						|
	ExcludeDriverIDs []string `json:"exclude_driver_ids,omitempty" jsonschema:"Optional list of driver IDs to exclude from solidarity transport results"`
 | 
						|
}
 | 
						|
 | 
						|
// JourneySearchOutput represents the output of journey search
 | 
						|
type JourneySearchOutput struct {
 | 
						|
	SearchParameters map[string]any `json:"search_parameters"`
 | 
						|
	Results          any            `json:"results"`
 | 
						|
}
 | 
						|
 | 
						|
// registerJourneySearchTool registers the journey search tool with the MCP server
 | 
						|
func (s *MCPServer) registerJourneySearchTool() {
 | 
						|
	mcpsdk.AddTool(
 | 
						|
		s.mcpServer,
 | 
						|
		&mcpsdk.Tool{
 | 
						|
			Name:        "search_journeys",
 | 
						|
			Description: "Search for multimodal journeys including transit, carpooling, solidarity transport, organized carpool, and local solutions. Accepts departure and destination as text addresses that will be geocoded automatically.",
 | 
						|
		},
 | 
						|
		s.handleJourneySearch,
 | 
						|
	)
 | 
						|
}
 | 
						|
 | 
						|
// handleJourneySearch handles the journey search tool execution
 | 
						|
func (s *MCPServer) handleJourneySearch(ctx context.Context, req *mcpsdk.CallToolRequest, input JourneySearchInput) (*mcpsdk.CallToolResult, *JourneySearchOutput, error) {
 | 
						|
	// Geocode departure address using French government API
 | 
						|
	log.Info().Str("address", input.Departure).Msg("Geocoding departure address")
 | 
						|
	departureFeature, err := s.geocodeAddress(input.Departure)
 | 
						|
	if err != nil {
 | 
						|
		log.Error().Err(err).Msg("failed to geocode departure address")
 | 
						|
		return nil, nil, fmt.Errorf("failed to geocode departure address: %w", err)
 | 
						|
	}
 | 
						|
 | 
						|
	// Geocode destination address using French government API
 | 
						|
	log.Info().Str("address", input.Destination).Msg("Geocoding destination address")
 | 
						|
	destinationFeature, err := s.geocodeAddress(input.Destination)
 | 
						|
	if err != nil {
 | 
						|
		log.Error().Err(err).Msg("failed to geocode destination address")
 | 
						|
		return nil, nil, fmt.Errorf("failed to geocode destination address: %w", err)
 | 
						|
	}
 | 
						|
 | 
						|
	// Parse date and time
 | 
						|
	parisLoc, err := time.LoadLocation("Europe/Paris")
 | 
						|
	if err != nil {
 | 
						|
		return nil, nil, fmt.Errorf("failed to load Paris timezone: %w", err)
 | 
						|
	}
 | 
						|
 | 
						|
	departureDateTime, err := time.ParseInLocation("2006-01-02 15:04", fmt.Sprintf("%s %s", input.DepartureDate, input.DepartureTime), parisLoc)
 | 
						|
	if err != nil {
 | 
						|
		log.Error().Err(err).Msg("failed to parse date/time")
 | 
						|
		return nil, nil, fmt.Errorf("failed to parse departure date/time: %w", err)
 | 
						|
	}
 | 
						|
 | 
						|
	// Convert to UTC for the search
 | 
						|
	departureDateTime = departureDateTime.UTC()
 | 
						|
 | 
						|
	log.Info().
 | 
						|
		Str("departure", input.Departure).
 | 
						|
		Str("destination", input.Destination).
 | 
						|
		Time("departure_datetime", departureDateTime).
 | 
						|
		Msg("Executing journey search")
 | 
						|
 | 
						|
	// Prepare exclude driver ID (only first one if provided)
 | 
						|
	excludeDriverID := ""
 | 
						|
	if len(input.ExcludeDriverIDs) > 0 {
 | 
						|
		excludeDriverID = input.ExcludeDriverIDs[0]
 | 
						|
	}
 | 
						|
 | 
						|
	// Prepare search options - disable transit for MCP requests
 | 
						|
	searchOptions := &application.SearchJourneyOptions{
 | 
						|
		DisableTransit: true,
 | 
						|
	}
 | 
						|
 | 
						|
	// Call the journey search from application handler
 | 
						|
	searchResult, err := s.applicationHandler.SearchJourneys(
 | 
						|
		ctx,
 | 
						|
		departureDateTime,
 | 
						|
		departureFeature,
 | 
						|
		destinationFeature,
 | 
						|
		input.PassengerID,
 | 
						|
		excludeDriverID,
 | 
						|
		"", // solidarityExcludeGroupId - not used in MCP context
 | 
						|
		searchOptions,
 | 
						|
	)
 | 
						|
	if err != nil {
 | 
						|
		return nil, nil, fmt.Errorf("journey search failed: %w", err)
 | 
						|
	}
 | 
						|
 | 
						|
	// Format the results for MCP response
 | 
						|
	response := &JourneySearchOutput{
 | 
						|
		SearchParameters: map[string]any{
 | 
						|
			"departure": map[string]any{
 | 
						|
				"label":       getFeatureLabel(departureFeature),
 | 
						|
				"coordinates": departureFeature.Geometry,
 | 
						|
			},
 | 
						|
			"destination": map[string]any{
 | 
						|
				"label":       getFeatureLabel(destinationFeature),
 | 
						|
				"coordinates": destinationFeature.Geometry,
 | 
						|
			},
 | 
						|
			"departure_date": input.DepartureDate,
 | 
						|
			"departure_time": input.DepartureTime,
 | 
						|
		},
 | 
						|
		Results: searchResult,
 | 
						|
	}
 | 
						|
 | 
						|
	return &mcpsdk.CallToolResult{}, response, nil
 | 
						|
}
 | 
						|
 | 
						|
// geocodeAddress uses the geo service helper to geocode an address
 | 
						|
func (s *MCPServer) geocodeAddress(address string) (*geojson.Feature, error) {
 | 
						|
	// Use the geo service to get autocomplete results
 | 
						|
	featureCollection, err := s.geoService.Autocomplete(address)
 | 
						|
	if err != nil {
 | 
						|
		return nil, fmt.Errorf("geocoding request failed: %w", err)
 | 
						|
	}
 | 
						|
 | 
						|
	if len(featureCollection.Features) == 0 {
 | 
						|
		return nil, fmt.Errorf("no results found for address: %s", address)
 | 
						|
	}
 | 
						|
 | 
						|
	// Return the first feature directly
 | 
						|
	return featureCollection.Features[0], nil
 | 
						|
}
 | 
						|
 | 
						|
// getFeatureLabel extracts a human-readable label from a GeoJSON Feature
 | 
						|
func getFeatureLabel(feature *geojson.Feature) string {
 | 
						|
	if feature.Properties == nil {
 | 
						|
		return ""
 | 
						|
	}
 | 
						|
 | 
						|
	// Try common label fields
 | 
						|
	if label, ok := feature.Properties["label"].(string); ok {
 | 
						|
		return label
 | 
						|
	}
 | 
						|
	if name, ok := feature.Properties["name"].(string); ok {
 | 
						|
		return name
 | 
						|
	}
 | 
						|
 | 
						|
	return ""
 | 
						|
}
 |