@mbstudio/sbb-mcp
Version:
A MCP (Model Context Protocol) server for the Swiss Federal Railways
410 lines (402 loc) • 9.3 kB
JavaScript
#!/usr/bin/env node
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
const PLACE_QUERY = `query GetPlaces($input: PlaceInput, $language: LanguageEnum!) {
places(input: $input, language: $language) {
id
name
__typename
}
}`;
const TRIP_QUERY = `query getTrips($input: TripInput!, $pagingCursor: String, $language: LanguageEnum!) {
trips(tripInput: $input, pagingCursor: $pagingCursor, language: $language) {
trips {
...TripFields
__typename
}
paginationCursor {
previous
next
__typename
}
__typename
}
}
fragment NoticesFields on Notice {
name
text {
template
arguments {
type
values
__typename
}
__typename
}
type
priority
advertised
__typename
}
fragment ArrivalDepartureFields on ScheduledStopPointDetail {
time
delay
delayText
quayFormatted
quayChanged
quayChangedText
__typename
}
fragment BoardingAFields on AccessibilityBoardingAlighting {
limitation
name
description
assistanceService {
template
arguments {
type
values
__typename
}
__typename
}
__typename
}
fragment ServiceProductFields on ServiceProduct {
name
line
number
vehicleMode
vehicleSubModeShortName
corporateIdentityIcon
corporateIdentityPictogram
__typename
}
fragment SituationFields on PTSituation {
cause
broadcastMessages {
id
priority
title
detail
detailShort
distributionPeriod {
startDate
endDate
__typename
}
audiences {
urls {
name
url
__typename
}
__typename
}
__typename
}
affectedStopPointFromIdx
affectedStopPointToIdx
__typename
}
fragment TripStatusFields on TripStatus {
alternative
alternativeText
cancelled
cancelledText
partiallyCancelled
delayed
delayedUnknown
quayChanged
__typename
}
fragment TripFields on Trip {
id
legs {
duration
id
... on AccessLeg {
__typename
duration
distance
start {
__typename
id
name
}
end {
__typename
id
name
}
}
... on PTConnectionLeg {
__typename
duration
start {
__typename
id
name
}
end {
__typename
id
name
}
notices {
...NoticesFields
__typename
}
}
... on AlternativeModeLeg {
__typename
mode
duration
}
... on PTRideLeg {
__typename
duration
start {
__typename
id
name
}
end {
__typename
id
name
}
arrival {
...ArrivalDepartureFields
__typename
}
departure {
...ArrivalDepartureFields
__typename
}
serviceJourney {
id
stopPoints {
place {
id
name
__typename
}
occupancy {
firstClass
secondClass
__typename
}
accessibilityBoardingAlighting {
...BoardingAFields
__typename
}
stopStatus
stopStatusFormatted
delayUndefined
__typename
}
serviceProducts {
...ServiceProductFields
routeIndexFrom
routeIndexTo
__typename
}
direction
serviceAlteration {
cancelled
cancelledText
partiallyCancelled
partiallyCancelledText
redirected
redirectedText
reachable
reachableText
delayText
unplannedStopPointsText
quayChangedText
__typename
}
situations {
...SituationFields
__typename
}
notices {
...NoticesFields
__typename
}
quayTypeName
quayTypeShortName
__typename
}
}
__typename
}
situations {
...SituationFields
__typename
}
notices {
...NoticesFields
__typename
}
valid
isBuyable
summary {
duration
arrival {
...ArrivalDepartureFields
__typename
}
arrivalWalk
lastStopPlace {
__typename
id
name
canton
}
tripStatus {
...TripStatusFields
__typename
}
departure {
...ArrivalDepartureFields
__typename
}
departureWalk
firstStopPlace {
__typename
id
name
canton
}
product {
...ServiceProductFields
__typename
}
direction
occupancy {
firstClass
secondClass
__typename
}
tripStatus {
...TripStatusFields
__typename
}
boardingAlightingAccessibility {
...BoardingAFields
__typename
}
international
__typename
}
searchHint
__typename
}`;
function registerTools(server) {
server.tool("sbb_get_places", "Return a list of places available from name", { value: z.string().min(1) }, async ({ value }) => {
const resp = await fetch("https://graphql.www.sbb.ch/", {
method: "POST",
headers: {
"Content-Type": "application/json",
"Accept-Language": "fr-FR,fr;q=0.9,en-US;q=0.8,en;q=0.7",
},
body: JSON.stringify({
operationName: "GetPlaces",
query: PLACE_QUERY,
variables: {
input: {
type: "NAME",
value: value,
},
language: "FR",
},
}),
});
const data = await resp.json();
return {
content: [{ type: "text", text: JSON.stringify(data.data.places) }],
};
});
server.tool("sbb_get_trips", `Return a list of trips available from two places ID or name.
It's recommended to use the "sbb_get_places" to get the best results.`, {
from: z.string().min(1),
to: z.string().min(1),
date: z
.string()
.regex(/^(\d{4}-\d{2}-\d{2})?$/)
.optional()
.nullable(),
time: z
.string()
.regex(/^(\d{2}:\d{2})?$/)
.optional()
.nullable(),
}, async ({ from, to, date, time }) => {
// If date is not provided, use today's date
if (!date || date === "") {
date = new Date().toISOString().split("T")[0];
}
// If time is not provided, use current time
if (!time || time === "") {
const now = new Date();
time = now.toTimeString().slice(0, 5); // Format as HH:MM
}
const resp = await fetch("https://graphql.www.sbb.ch/", {
method: "POST",
headers: {
"Content-Type": "application/json",
"Accept-Language": "fr-FR,fr;q=0.9,en-US;q=0.8,en;q=0.7",
},
body: JSON.stringify({
operationName: "getTrips",
query: TRIP_QUERY,
variables: {
input: {
directConnection: false,
includeTransportModes: ["HIGH_SPEED_TRAIN", "INTERCITY", "INTERREGIO", "REGIO", "URBAN_TRAIN", "SPECIAL_TRAIN", "SHIP", "BUS", "TRAMWAY", "CABLEWAY_GONDOLA_CHAIRLIFT_FUNICULAR"],
occupancy: "ALL",
places: [
{ type: "NAME", value: from },
{ type: "NAME", value: to },
],
time: {
date: date,
time: time,
},
walkSpeed: 100,
},
language: "FR",
},
}),
});
const data = await resp.json();
return {
content: [{ type: "text", text: JSON.stringify(data.data.trips.trips) }],
};
});
}
async function main() {
const server = new McpServer({
name: "sbb",
version: "1.0.0",
description: "SBB/CFF Model Context Protocol Server",
});
registerTools(server);
let transport = new StdioServerTransport();
await server.connect(transport);
const cleanup = async () => {
console.error("\n⚠️ Shutting down MCP server...");
await transport.close();
process.exit(0);
};
process.on("SIGINT", cleanup);
process.on("SIGTERM", cleanup);
console.error("SBB/CFF MCP Server running on stdio");
}
main().catch((err) => {
console.error("Error initializing MCP server:\n");
console.error(`${err.message}\n`);
});