sl-mcp
This MCP server lets you query and plan Stockholm public transport (SL) trips, departures, and disruptions.
Find SL stops by name with typo tolerance, returning stop IDs, names, towns, and transport modes.
Get live departures from a stop, soonest first, with line, mode, destination, direction, platform/stop letter, time/minutes, delays, cancellations, and SL notes; filter by line, mode, final destination, or minutes ahead (up to 3 hours).
List announced disruptions such as track work, closed entrances, broken lifts, and replacement buses; filter by line numbers/metro colours or transport mode (messages in Swedish).
Plan up to 3 SL trips between stops or street addresses, leaving or arriving at a specified Stockholm time, including walking legs, station exits, platforms, stops, crowding, delays, and mode/preference filters (fastest, fewest changes, least walking).
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@sl-mcpWhen are the next departures from Stockholm Central?"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
sl-mcp
An MCP server for Stockholm public transport (SL). Connect it to Claude or another MCP client and ask about departures, disruptions and trips in plain Swedish or English.
När går nästa tunnelbana från Slussen mot Ropsten?
Är det några störningar på röda linjen?
How do I get from Odenplan to Arlanda?
Hur tar jag mig från Sundbyberg till Götgatan 76?It uses SL's open APIs, which don't need an API key.
An answer looks like this:
Gå 08:31 från Sundbybergs centrum, så är du framme på Götgatan 76 kl 08:58.
08:31 Tunnelbana 10 mot Kungsträdgården från spår 2. Åk 7 stationer till T-Centralen.
08:47 Byt till tunnelbana 17 mot Skarpnäck från spår 4. Åk 3 stationer till Medborgarplatsen.
08:52–08:58 Gå ut via uppgång Folkungagatan Götgatan och gå 6 min till Götgatan 76.
Use it without installing
A hosted server runs on Cloudflare Workers. No API key or login is needed:
https://sl-mcp.miltonlindblad.workers.dev/mcpAdd it as a custom connector in claude.ai or ChatGPT, or in any MCP client that supports Streamable HTTP. In Claude Code:
claude mcp add --transport http sl https://sl-mcp.miltonlindblad.workers.dev/mcpRelated MCP server: TFL MCP Server for Poke
Tools
find_stop: search for a stop by name, typos are fine, and get its idget_departures: live departures from a stop with platform or bus stop letter, delays and cancellations. Filter by mode, line or destinationget_disruptions: announced disruptions such as track work, broken lifts and replacement buses, by line, mode or metro colour ("grön linje")plan_trip: up to 3 trips between stops or street addresses, leaving or arriving at a given time. Each ride shows platform, stops, crowding and delay, and each walk shows minutes and which station exit to use. You can ask for only some modes (e.g. boat) and for fewest changes or least walking
Prompts
Ready-made questions to pick in your client, e.g. as slash commands:
trip: how to get from one place to another, optionally arriving by a given timedepartures: the next departures from a stop
Install
Requires Node.js 20 or later.
git clone https://github.com/Hashtagsmile/sl-mcp.git
cd sl-mcp
npm install
npm run buildClaude Code
claude mcp add sl -- node /path/to/sl-mcp/dist/index.jsClaude Desktop
Add this to claude_desktop_config.json and restart Claude Desktop:
{
"mcpServers": {
"sl": {
"command": "node",
"args": ["/path/to/sl-mcp/dist/index.js"]
}
}
}Development
npm run dev # compile on save
npm test # run the tests
npm run inspect # try the tools in the MCP InspectorThe tests use recorded API responses in test/fixtures/, so they don't call SL.
Deploy to Cloudflare Workers
src/worker.ts serves the same tools over HTTP at /mcp.
npx wrangler dev # run it locally on http://localhost:8787/mcp
npx wrangler deploy # deploy to your Cloudflare accountLimitations
SL returns at most 3 departures per line and direction.
Disruption messages are in Swedish only.
There is no "nearest stop" search, since SL has no keyless API for it.
SL can't plan step-free trips. For a wheelchair, the tools suggest least walking and checking for broken lifts.
Addresses work, business names don't: "Götgatan 76" works, "Sellpy" doesn't.
Departures reach at most 3 hours ahead. For later, use
plan_tripwith a time.
Data
Data from SL via Trafiklab.
License
MIT
Available Tools
4 toolsfind_stopFind stopARead-onlyInspect
Find Stockholm public transport (SL) stops by name and return their ids, names and transport modes. Use it to get the siteId for get_departures, or a stop id for plan_trip. Not for street addresses: pass those to plan_trip as text instead. A name can match several stops ('Arlanda' gives the train station and bus stops): pick by name and modes, or ask the user. Typos are fine, abbreviations are not: write 'Tekniska högskolan', not 'KTH'. Returns one line per stop: ': , ()'.
| Name | Required | Description | Default |
|---|---|---|---|
| limit | No | Maximum number of stops to return | |
| query | Yes | Stop or station name, e.g. 'Slussen' or 'Arlanda'. Typos are fine, abbreviations are not. |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Beyond the readOnlyHint/openWorldHint annotations, it discloses fuzzy matching behavior ('typos are fine, abbreviations are not'), that a query can return several distinct stops, how to disambiguate, and the exact one-line-per-stop return shape. These are behavioral traits an agent could not infer from the structured fields.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
Front-loaded with purpose and return values, then usage routing, then the negative case, then ambiguity handling, then output shape. Every sentence carries distinct information; there is no filler or restatement of the title.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
For a two-parameter lookup with no output schema, the description supplies the missing return format ('<id>: <town>, <name> (<modes>)'), the disambiguation policy, and the sibling routing. Nothing an agent needs to call it correctly and interpret results is absent.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema coverage is 100% and the schema already documents both 'query' (including the typo/abbreviation note) and 'limit' with its bounds and default. The description reinforces query semantics and hints that results may be multiple, but adds no new naming syntax or formatting details beyond the schema, so baseline 3 applies.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
States a specific verb and resource ('Find Stockholm public transport (SL) stops by name') plus what is returned (ids, names, transport modes). It clearly distinguishes itself from siblings by naming get_departures and plan_trip as consumers of the resulting ids.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
Explicitly routes the agent: use this to obtain a siteId for get_departures or a stop id for plan_trip, and explicitly excludes street addresses ('pass those to plan_trip as text instead'). It also gives a resolution strategy for ambiguous multi-match results (pick by name/modes or ask the user).
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
get_departuresGet departuresARead-onlyInspect
List live departures from one SL stop, soonest first: line, mode, final destination, direction (1 or 2), platform or bus stop letter, minutes or clock time, delay, cancellation and SL's notes. Use it for 'when does the next X leave from Y'. To get from A to B, use plan_trip; for announced track work or broken lifts, use get_disruptions. Real-time data from SL: at most 3 departures per line and direction, up to 3 hours ahead. towards matches the final destination only: for a station on the way (e.g. 'mot T-Centralen' from Slussen), leave it out and pick the direction from the results. An unknown siteId gives no departures, not an error.
| Name | Required | Description | Default |
|---|---|---|---|
| line | No | Only this line number, e.g. '14' or '43X' | |
| limit | No | Maximum number of departures | |
| siteId | Yes | Stop id from find_stop | |
| minutes | No | How many minutes ahead to look, at most 180 | |
| towards | No | Only departures whose final destination contains this, e.g. 'Ropsten' | |
| transportMode | No | Only this mode |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Annotations only cover read-only and open-world, but the description adds real behavioral traits: real-time SL data capped at 3 departures per line and direction, a 3-hour horizon, and the important edge case that an unknown siteId returns an empty list rather than an error. It stops short of covering error modes, rate limiting, or what live-vs-scheduled mixing looks like, so it is strong but not exhaustive.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
Front-loaded with the core action and return payload, then routing, then data limits and edge cases — a sound order with no filler. The final `towards` guidance is a long compound sentence that could be split for readability, which keeps it off a 5.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
There is no output schema, and the description compensates by enumerating returned fields, the 3-per-line/direction cap, and the time horizon. Combined with the sibling routing and the empty-result caveat, an agent has everything needed to call this correctly.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema description coverage is 100%, so the baseline is 3. The description does add real meaning beyond the schema for `towards` (final destination only, with the 'mot T-Centralen' workaround), which lifts it slightly, but the other five parameters are left to the schema.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
States a specific verb and resource ('List live departures from one SL stop, soonest first') and then enumerates exactly what comes back: line, mode, final destination, direction, platform/bus stop letter, time, delay, cancellation, notes. It explicitly names the siblings it is not (plan_trip, get_disruptions), so an agent can route without opening a schema.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
Gives a concrete use case ('when does the next X leave from Y') and names both alternatives with the condition that selects them: plan_trip for A-to-B, get_disruptions for track work and broken lifts. It also supplies a negative rule for the `towards` filter, telling the agent to omit it and read the direction from results rather than guessing.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
get_disruptionsGet disruptionsARead-onlyInspect
List disruptions SL has announced: track work, closed entrances, broken lifts and replacement buses, with the lines affected and until when. Use it to check a line or the stops of a trip before travelling, e.g. lifts for a wheelchair. For live delays and cancelled departures, use get_departures instead. The text is in Swedish, so translate it if needed, and is cut at 800 characters. Results are cached for 1 minute, as SL asks. Leave both filters out to list everything.
| Name | Required | Description | Default |
|---|---|---|---|
| lines | No | Only these line numbers, e.g. ['13', '14'], or metro colours, e.g. ['grön linje'] | |
| transportMode | No | Only this mode |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Annotations only declare readOnlyHint and openWorldHint; the description goes well beyond them by disclosing that the text is Swedish, truncated at 800 characters, and server-cached for 1 minute. These are exactly the traits an agent needs to interpret results correctly and avoid spurious retries.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
Front-loaded with what the tool returns, followed by when to use it and the behavioral caveats; nearly every clause earns its place. Slightly dense as a single run-on paragraph, but no filler sentences.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
With no output schema, the description still tells the agent what a result contains (affected lines, until when), what language and length to expect, and how fresh it is. For a two-parameter read-only lookup, nothing material is missing.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema coverage is 100% and the schema already documents both parameters with examples and an enum, so the description only adds the omission semantics ("Leave both filters out to list everything"). Baseline 3 is appropriate since the schema carries the parameter meaning.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
States a specific verb and resource ("List disruptions SL has announced") and enumerates the content covered: track work, closed entrances, broken lifts, replacement buses, affected lines, and validity window. An agent can immediately distinguish this from find_stop, get_departures, and plan_trip.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
Gives a concrete scenario ("check a line or the stops of a trip before travelling, e.g. lifts for a wheelchair") and explicitly names the alternative for the adjacent case: "For live delays and cancelled departures, use get_departures instead." It also clarifies the default behavior when filters are omitted, which prevents unnecessary parameter guessing.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
plan_tripPlan tripARead-onlyInspect
Plan trips by SL public transport between two stops or street addresses, leaving or arriving at a given time. Returns up to 3 trips, plus walking all the way when that takes at most 30 min. Each ride lists line, direction, platform, stops, crowding and delay; each walk lists minutes and the station exit (uppgång). Use it for 'how do I get to X' and 'when must I leave'; for departures from one stop, use get_departures. Addresses are fuzzy matched, so the result starts with what each one matched. Times are Stockholm time. Unknown stops, the same stop twice and times outside the timetable give a clear error. SL cannot plan step-free trips: for a wheelchair or pram, use prefer 'least walking' and check get_disruptions for broken lifts at the stops.
| Name | Required | Description | Default |
|---|---|---|---|
| to | Yes | Destination: a stop id from find_stop, or a street address | |
| from | Yes | Start: a stop id from find_stop, or a street address like 'Götgatan 76, Stockholm' | |
| time | No | Stockholm time as YYYY-MM-DDTHH:MM. Leave out to leave now. | |
| modes | No | Only these modes, e.g. ['boat'] for 'by boat'. Leave out for all. | |
| prefer | No | What to optimise for: fastest (default), fewest changes or least walking | fastest |
| arriveBy | No | true: arrive by `time`. false: leave at `time`. |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Beyond the readOnly/openWorld annotations, the description discloses result cardinality (up to 3 trips), a non-obvious fallback (walking all the way when under 30 min), the per-item fields returned and the fuzzy-match echo behaviour. It also states the error surface (unknown stops, identical stops, out-of-timetable times) and a real capability limit (SL cannot plan step-free trips), which no annotation conveys.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
The primary purpose and the sibling exclusion are front-loaded, and the denser return-shape sentence earns its space because there is no output schema. It is a long, dense block with a slightly tangential accessibility coda, but nearly every clause carries information an agent would otherwise have to discover by trial.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
For a 6-parameter routing tool with no output schema, the description covers input interpretation (stops vs fuzzy-matched addresses), output shape (rides vs walks and their fields), failure modes and a domain limitation. Nothing an agent needs in order to call it correctly and interpret the response is missing.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema description coverage is 100%, so all six parameters are already documented, including the Stockholm-time format and prefer's default. The description reinforces a few meanings (Stockholm time, prefer 'least walking' as the accessibility workaround) but adds no new syntax or semantic detail about from/to, modes, or arriveBy, so the baseline 3 applies.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
The opening sentence states a specific verb (plan), the resource (trips), the exact scope (between two stops or street addresses) and the time semantics (leave or arrive at a given time). It is immediately distinguishable from get_departures because of the origin/destination requirement.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
It names the concrete user intents it serves ('how do I get to X', 'when must I leave') and explicitly routes single-stop departure queries to get_departures. It also gives a conditional alternative path for step-free travel (use prefer 'least walking' plus get_disruptions), which is when-to-use guidance for a whole class of requests.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
Tool Schema Changelog
Recent tool additions, removals, and schema changes observed during successful MCP inspections.
4 tool updates
v0.2.0- Changed
find_stop3 fields changed- changed
Input schema / properties / limit / descriptionPrevious value: -"TODO"New value: +"Maximum number of stops to return" - changed
Input schema / properties / query / descriptionPrevious value: -"TODO: describe for the model, e.g. part of a stop name"New value: +"Stop or station name, e.g. 'Slussen' or 'Arlanda'. Typos are fine, abbreviations are not." - added
Input schema / properties / query / minLengthAdded value: +1
- Changed
get_departures8 fields changed- changed
Input schema / properties / limit / descriptionPrevious value: -"TODO"New value: +"Maximum number of departures" - changed
Input schema / properties / limit / maximumPrevious value: -20New value: +30 - added
Input schema / properties / lineAdded value: +{ + "description": "Only this line number, e.g. '14' or '43X'", + "type": "string" +} - added
Input schema / properties / minutesAdded value: +{ + "default": 60, + "description": "How many minutes ahead to look, at most 180", + "maximum": 180, + "minimum": 1, + "type": "integer" +} - changed
Input schema / properties / siteId / descriptionPrevious value: -"TODO: the id returned by find_stop"New value: +"Stop id from find_stop" - added
Input schema / properties / towardsAdded value: +{ + "description": "Only departures whose final destination contains this, e.g. 'Ropsten'", + "type": "string" +} - changed
Input schema / properties / transportMode / descriptionPrevious value: -"TODO"New value: +"Only this mode" - changed
Input schema / properties / transportMode / enumPrevious value: -[ - "METRO", - "BUS", - "TRAIN", - "TRAM", - "SHIP" -]New value: +[ + "METRO", + "BUS", + "TRAIN", + "TRAM", + "SHIP", + "FERRY" +]
- Changed
get_disruptions4 fields changed- removed
Input schema / properties / lineRemoved value: -{ - "description": "TODO: e.g. a line number like 14", - "type": "string" -} - added
Input schema / properties / linesAdded value: +{ + "description": "Only these line numbers, e.g. ['13', '14'], or metro colours, e.g. ['grön linje']", + "items": { + "type": "string" + }, + "type": "array" +} - changed
Input schema / properties / transportMode / descriptionPrevious value: -"TODO"New value: +"Only this mode" - changed
Input schema / properties / transportMode / enumPrevious value: -[ - "METRO", - "BUS", - "TRAIN", - "TRAM", - "SHIP" -]New value: +[ + "METRO", + "BUS", + "TRAIN", + "TRAM", + "SHIP", + "FERRY" +]
- Added
plan_trip
3 tool updates
v0.1.0- First observed
find_stop - First observed
get_departures - First observed
get_disruptions
TDQS
Scored across 4 tools
Each tool has a distinct role: stop lookup, live departures, announced disruptions, or door-to-door trip planning. Descriptions explicitly cross-reference alternatives and clarify potentially confusing cases such as delays versus track work.
All four tools use a consistent snake_case verb_noun pattern. The verbs get, find, and plan accurately reflect their respective operations.
Four tools are well matched to a focused Stockholm public-transit server. Each earns its place without introducing overlapping or narrowly redundant operations.
The set supports the core travel workflow from resolving stops to checking departures, planning journeys, and reviewing disruptions. It also accommodates street addresses and accessibility-related checks, leaving no obvious dead ends for its read-only purpose.
Maintenance
Related MCP Connectors
Stockholm transit: realtime departures, trip planning, disruptions and personal commute status.
Belgian trains for AI agents: live journeys, departures, delays, disruptions. Free, no key.
Search award flights and cash fares, optimize points, and predict fares inside ChatGPT and Claude.
Connects AI assistants to TrackingTime for querying time entries, projects, tasks, customers, and team data using natural language.
Related MCP Servers
- AlicenseAqualityCmaintenanceEnables AI assistants to query Prague's public transit departures, air quality, parking availability, and other city data via the Golemio open data API.12GPL 3.0
- FlicenseNot gradedqualityDmaintenanceEnables AI assistants to access real-time Transport for London data, including tube/bus arrivals, line status, journey planning, and disruptions.1-
- AlicenseAqualityDmaintenanceAn MCP server that exposes the Deutsche Bahn public transport API to any MCP-compatible client (Claude Desktop, Cursor, Cline, Continue, etc.). Five tools cover station search, departures, journey planning, trip details, and nearby stations.61MIT
- AlicenseAqualityCmaintenanceEnables AI agents to search for stops and stations across all Norwegian public transport and to fetch real-time departure data — planned versus expected times, delays, platform, operator, and disruption messages — with no API key required. Covers bus, tram, metro, train, and ferry nationwide through Entur's open APIs, with filtering by line, destination, and future departure time.2MIT