Add MCP server
This commit is contained in:
158
servers/mcp/journey_tool.go
Normal file
158
servers/mcp/journey_tool.go
Normal file
@@ -0,0 +1,158 @@
|
||||
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 ""
|
||||
}
|
||||
Reference in New Issue
Block a user