Skip to content
Merged

M3 #14

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ HELP.md
/data/*
!/data/.gitkeep
.gradle
.log

build/
!gradle/wrapper/gradle-wrapper.jar
Expand Down
28 changes: 28 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -325,6 +325,34 @@ When no data is available:
}
```

## MCP Client Setup

### Claude Desktop

1. Download and install Claude Desktop from the official website.
2. Add java 25 JDK to your system PATH.
3. In repository directory, run:
```bash
./gradlew clean bootJar
```
4. Copy absolute path of the JAR generated in `build/libs/`.
5. Go to claude_desktop_config.json
macOS: ~/Library/Application Support/Claude/claude_desktop_config.json
Windows: %APPDATA%\Claude\claude_desktop_config.json
6. Add the following entry to the main json object:
```json
"mcpServers": {
"krakow-transport": {
"command": "java",
"args": [
"-jar",
"your/copied/path/transit-location-api-0.0.1-SNAPSHOT.jar",
]
}
}
```
7. Launch Claude Desktop and select "krakow-transport" as the MCP server.

## Data Flow

1. **Initialization** (`@PostConstruct`):
Expand Down
15 changes: 14 additions & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,11 @@ configurations {

repositories {
mavenCentral()
maven { url 'https://repo.spring.io/milestone' }
}

ext {
set('springAiVersion', "1.1.2")
}

protobuf {
Expand All @@ -34,7 +39,8 @@ protobuf {

dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
compileOnly 'org.projectlombok:lombok'
implementation 'org.springframework.ai:spring-ai-starter-mcp-server'
compileOnly 'org.projectlombok:lombok'
developmentOnly 'org.springframework.boot:spring-boot-devtools'
annotationProcessor 'org.projectlombok:lombok'
implementation 'org.springframework.boot:spring-boot-starter-data-jdbc'
Expand All @@ -43,11 +49,18 @@ dependencies {

testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'org.springframework.boot:spring-boot-starter-web'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
implementation 'com.google.protobuf:protobuf-java:4.28.2'
testImplementation 'com.squareup.okhttp3:mockwebserver:4.12.0'
}

dependencyManagement {
imports {
mavenBom "org.springframework.ai:spring-ai-bom:${springAiVersion}"
}
}

tasks.named('test') {
useJUnitPlatform()
}
Expand Down
12 changes: 12 additions & 0 deletions src/main/java/pl/agh/transit/TransitLocationApiApplication.java
Original file line number Diff line number Diff line change
@@ -1,13 +1,25 @@
package pl.agh.transit;

import org.springframework.ai.support.ToolCallbacks;
import org.springframework.ai.tool.ToolCallback;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.scheduling.annotation.EnableScheduling;
import pl.agh.transit.mcp.StopMcpTools;
import pl.agh.transit.mcp.TripMcpTools;

import java.util.List;

@EnableScheduling
@SpringBootApplication
public class TransitLocationApiApplication {
public static void main(String[] args) {
SpringApplication.run(TransitLocationApiApplication.class, args);
}

@Bean
public List<ToolCallback> mcpToolCallbacks(StopMcpTools stopMcpTools, TripMcpTools tripMcpTools) {
return List.of(ToolCallbacks.from(stopMcpTools, tripMcpTools));
}
}
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
package pl.agh.transit;
package pl.agh.transit.core;


import com.google.transit.realtime.GtfsRealtime;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;

import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package pl.agh.transit;
package pl.agh.transit.core;


import com.google.transit.realtime.GtfsRealtime;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
package pl.agh.transit;
package pl.agh.transit.core;

import jakarta.annotation.PostConstruct;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import pl.agh.transit.gtfs_static.GtfsStaticService;
Expand All @@ -11,6 +12,7 @@
@Component
@AllArgsConstructor
@Slf4j
@ConditionalOnProperty(name = "gtfs.scheduler.enabled", havingValue = "true", matchIfMissing = true)
public class TripUpdateScheduler {

private final TripDataFetcher fetcher;
Expand Down
16 changes: 16 additions & 0 deletions src/main/java/pl/agh/transit/dto/NextDepartureDTO.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package pl.agh.transit.dto;

import lombok.Builder;
import lombok.Data;

/**
* DTO representing next departure for a specific stop and route
*/
@Data
Comment thread
wegorz13 marked this conversation as resolved.
@Builder
public class NextDepartureDTO {
String departureTime;
Integer departureDelay;
String stopName;
String routeName;
}
18 changes: 18 additions & 0 deletions src/main/java/pl/agh/transit/dto/PagedResponseDTO.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package pl.agh.transit.dto;

import lombok.Builder;
import lombok.Data;

import java.util.List;

@Data
@Builder
public class PagedResponseDTO<T> {
private List<T> content;
private int pageNumber;
private int totalPages;
private long totalElements;
private int pageSize;
private boolean hasNext;
private boolean hasPrevious;
}
13 changes: 13 additions & 0 deletions src/main/java/pl/agh/transit/dto/StopDTO.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package pl.agh.transit.dto;

import lombok.Builder;
import lombok.Data;

@Data
@Builder
public class StopDTO {
private String id;
private String name;
private double latitude;
private double longitude;
}
1 change: 1 addition & 0 deletions src/main/java/pl/agh/transit/dto/StopTimeUpdateDTO.java
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
@Data
@Builder
public class StopTimeUpdateDTO {
String tripId;
String stopId;
String arrivalTime;
String departureTime;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,7 @@ private void importData() {

load(calendarDatesFile,
"INSERT INTO calendar_dates (service_id, date, exception_type, service_name) VALUES (?, ?, ?, ?)",
row -> new Object[]{ row[0], toDate(row[1]), toInt(row[2]), row[3]}
row -> new Object[]{ row[0], toDate(row[1]), toInt(row[2]), get(row, 3) }
);

load(tripsFile,
Expand All @@ -159,7 +159,7 @@ private void importData() {

load(feedInfoFile,
"INSERT INTO feed_info (feed_publisher_name, feed_publisher_url, feed_lang, feed_start_date, feed_end_date, feed_version) VALUES (?, ?, ?, ?, ?, ?)",
row -> new Object[] { row[0], row[1], row[2], toDate(row[3]), toDate(row[4]), row[5] }
row -> new Object[] { row[0], row[1], row[2], toDate(row[3]), toDate(row[4]), get(row, 5) }
);

log.info("Finished importing GTFS. Time: {}ms", System.currentTimeMillis() - start);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@
import org.springframework.data.repository.ListCrudRepository;
import org.springframework.stereotype.Repository;
import pl.agh.transit.gtfs_static.model.Route;
import java.util.Optional;

@Repository
public interface RouteRepository extends ListCrudRepository<Route, String> {
Optional<Route> findByRouteShortName(String routeShortName);
}

Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
package pl.agh.transit.gtfs_static.repository;

import org.springframework.data.repository.ListCrudRepository;
import org.springframework.data.repository.PagingAndSortingRepository;
import org.springframework.stereotype.Repository;
import pl.agh.transit.gtfs_static.model.Stop;

import java.util.List;

@Repository
public interface StopRepository extends ListCrudRepository<Stop, String> {
public interface StopRepository extends ListCrudRepository<Stop, String>, PagingAndSortingRepository<Stop, String> {
List<Stop> findByStopName(String stopName);
}
}
54 changes: 54 additions & 0 deletions src/main/java/pl/agh/transit/mcp/StopMcpTools.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package pl.agh.transit.mcp;

import lombok.RequiredArgsConstructor;
import org.springframework.ai.tool.annotation.Tool;
import org.springframework.ai.tool.annotation.ToolParam;
import org.springframework.stereotype.Component;
import pl.agh.transit.stop.StopService;
import pl.agh.transit.dto.NextDepartureDTO;
import pl.agh.transit.dto.PagedResponseDTO;
import pl.agh.transit.dto.StopDTO;

import java.time.LocalDate;
import java.time.LocalTime;
import java.util.List;

@Component
@RequiredArgsConstructor
public class StopMcpTools {

private final StopService stopService;

@Tool(description = "Retrieves a paginated list of all available public transport stops.")
public PagedResponseDTO<StopDTO> getAllStops(
@ToolParam(description = "Page number (starts from 0, default is 0)", required = false) Integer page,
@ToolParam(description = "Number of stops per page (default is 10, max is 100)", required = false) Integer size,
@ToolParam(description = "Field to sort by (default is 'stopName')", required = false) String sortBy
) {
int pageNum = (page != null) ? page : 0;
int pageSize = (size != null) ? size : 10;
String sort = (sortBy != null && !sortBy.isBlank()) ? sortBy : "stopName";

if (pageSize > 100) {
pageSize = 100;
}

return stopService.getAllStops(pageNum, pageSize, sort);
}

@Tool(description = "Gets the next 5 departures for a specific stop and route (line number). " +
"Includes real-time delay information if available.")
public List<NextDepartureDTO> getNextDepartures(
@ToolParam(description = "Name of the stop (e.g., 'Teatr Bagatela')") String stopName,
@ToolParam(description = "Route name or line number (e.g., '52', '179')") String routeName,
@ToolParam(description = "Date to check schedule for (YYYY-MM-DD). If omitted, defaults to today.", required = false) LocalDate date,
@ToolParam(description = "Time to check schedule for (HH:mm). If omitted, defaults to right now.", required = false) LocalTime time
) {
if (date != null && time != null) {
return stopService.getNextDeparturesForStopAndRoute(stopName, routeName, date, time, 5);
}
else{
return stopService.getNextDeparturesForStopAndRoute(stopName, routeName,5);
}
}
}
34 changes: 34 additions & 0 deletions src/main/java/pl/agh/transit/mcp/TripMcpTools.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package pl.agh.transit.mcp;

import lombok.RequiredArgsConstructor;
import org.springframework.ai.tool.annotation.Tool;
import org.springframework.ai.tool.annotation.ToolParam;
import org.springframework.stereotype.Component;
import pl.agh.transit.trip.DirectConnectionService;
import pl.agh.transit.dto.TransportTypeDTO;

import java.time.LocalDate;
import java.time.LocalTime;
import java.util.List;

@Component
@RequiredArgsConstructor
public class TripMcpTools {

private final DirectConnectionService directConnectionService;

@Tool(description = "Finds the 3 fastest direct public transport connections between two stops. " +
"If date and time are not provided, it defaults to the current time.")
public List<TransportTypeDTO> getFastestConnections(
@ToolParam(description = "Name of the starting stop (origin)") String from,
@ToolParam(description = "Name of the destination stop") String to,
@ToolParam(description = "Travel date in YYYY-MM-DD format (optional)", required = false) LocalDate travelDate,
@ToolParam(description = "Travel time in HH:mm format (optional)", required = false) LocalTime travelTime
) {
if (travelDate != null && travelTime != null) {
return directConnectionService.findFastestDirectConnections(from, to, travelDate, travelTime, 3);
} else {
return directConnectionService.findFastestDirectConnections(from, to, 3);
}
}
}
Loading