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 ""
|
|
}
|