viafrei 1.4.8 → 1.5.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (4) hide show
  1. package/API.md +32 -8
  2. package/CHANGELOG.md +356 -1
  3. package/SOURCES.md +36 -10
  4. package/package.json +9 -1
package/API.md CHANGED
@@ -9,7 +9,7 @@ the server's own text, reproduced verbatim, because that text is what an
9
9
  assistant reads when it decides which tool to call; paraphrasing it here would
10
10
  document a different server.
11
11
 
12
- **It is a dated snapshot, taken on 2026-09-29.** Generating this file makes
12
+ **It is a dated snapshot, taken on 2026-10-02.** Generating this file makes
13
13
  it impossible for the document and the snapshot to disagree — CI regenerates and
14
14
  compares — but it cannot keep the snapshot from ageing against the live server,
15
15
  because a capture is a point in time. **The source of truth is the running
@@ -17,12 +17,12 @@ server:** connect any MCP client and call `tools/list`.
17
17
 
18
18
  | | |
19
19
  | --- | --- |
20
- | Server | `viafrei` 1.4.8 |
20
+ | Server | `viafrei` 1.5.4 |
21
21
  | MCP protocol | `2025-06-18` |
22
22
  | Streamable HTTP | https://mcp.viafrei.de/mcp |
23
23
  | Legacy HTTP+SSE | https://mcp.viafrei.de/sse |
24
- | Captured from | `https://mcp.viafrei.de/mcp` on 2026-09-29 |
25
- | Surface | 19 tools, 10 resources, 2 resource templates, 9 prompts |
24
+ | Captured from | `https://mcp.viafrei.de/mcp` on 2026-10-02 |
25
+ | Surface | 20 tools, 10 resources, 2 resource templates, 9 prompts |
26
26
  | Parameter schemas | JSON Schema draft-07 |
27
27
  | Capabilities | `tools`, `resources`, `prompts`, `logging` |
28
28
 
@@ -31,7 +31,7 @@ No API key. No account. No sign-up.
31
31
  ## Contents
32
32
 
33
33
  - [How to read a result](#how-to-read-a-result)
34
- - [Tools](#tools) — 19
34
+ - [Tools](#tools) — 20
35
35
  - [Resources](#resources) — 10
36
36
  - [Resource templates](#resource-templates) — 2
37
37
  - [Prompts](#prompts) — 9
@@ -410,7 +410,7 @@ The server's own instructions to a connecting client, verbatim:
410
410
 
411
411
  **Read-only** — it changes nothing. Reaches a third-party source (open world). Idempotent: true. Destructive: false.
412
412
 
413
- > Return the next departures from a German railway station: time, line, destination, platform, delay and cancellations. Use when someone asks when their train, S-Bahn or ICE leaves, whether it is late, or what is leaving a station now — give the station name as the person said it ("Hamburg Hbf", "Munich Central"); an ambiguous name comes back as a list. Do NOT use for buses or trams (punctuality: check_transit_disruption), for tickets, fares or journey planning, or for motorway traffic — call check_autobahn_traffic. At most 15 departures, window 120 min. Results carry their attribution line.
413
+ > Next departures from a German railway station, with platform, delay and cancellations. Use when asked when a train, S-Bahn or ICE leaves a named station, or whether THAT departure is late; vague later-today wording ("heute Abend") stays here. Whether ONE line is punctual ("ist die S1 pünktlich?") is NOT this tool, though it is rail and about delay — call check_transit_disruption. Do NOT use for buses, trams, a non-railway stop, another day or a time over 2 h away — get_departures. No destination filter: read the board. Max 15 departures, window 120 min. Results carry their attribution line.
414
414
 
415
415
  | parameter | type | required | default | constraints |
416
416
  | --- | --- | --- | --- | --- |
@@ -425,9 +425,33 @@ The server's own instructions to a connecting client, verbatim:
425
425
  - **`eva_no`** — The station's EVA number (6–8 digits, e.g. 8002549 for Hamburg Hbf), when a previous result gave you one. It skips the name lookup and is exact — use it to answer a follow-up about a station this tool has already named.
426
426
  - **`language`** — Set this on every call to the language the person is writing in: "en" if they wrote English, "de" if they wrote German. Do not leave it out because it has a default — the default is only the fallback when the language is genuinely unclear, and an English question answered in German is a wrong answer. Place names, station names and road numbers are never translated in either language; in English the German term is kept in parentheses so the person recognises it on signs and in local apps.
427
427
  - **`limit`** — How many departures to return, earliest first (1–15, default 10). More than 15 is refused — that is a board a person can read, not a dataset.
428
- - **`station`** — The railway station, as the person says it: "Hamburg Hbf", "Köln Hbf", "Munich Central", "Frankfurt (Main) Hbf". Pass their words — English names and "central station" are understood. If the name fits several stations the result lists them and asks which; do not guess one yourself. Give either station OR eva_no, never both.
428
+ - **`station`** — The railway station, as the person says it: "Hamburg Hbf", "Köln Hbf", "Munich Central", "Frankfurt (Main) Hbf". Pass their words — English names and "central station" are understood. If the name fits several stations (a bare "Hauptbahnhof"), call anyway: the result lists them and asks which; do not guess one yourself. Give either station OR eva_no, never both.
429
429
  - **`when`** — Start of the window as an ISO-8601 instant with an offset ("2026-09-20T18:30:00+02:00"). Leave it out for "now", which is what almost every question means. Times in the answer are Europe/Berlin whatever you pass.
430
430
 
431
+ ### `get_departures` — Scheduled departures (bus, tram, train)
432
+
433
+ **Read-only** — it changes nothing. Answers from data this service already holds (closed world). Idempotent: true. Destructive: false.
434
+
435
+ > Scheduled departures from any German public-transport stop — bus, tram, U-Bahn, S-Bahn, train, ferry — with line, destination, platform. Use when someone asks when a bus, tram, U-Bahn or ferry goes, or for another DAY or a clock time over 2 h away: "Wann fährt der nächste Bus ab Fulda Bahnhof?" Do NOT use for a railway station's trains now or later today — get_train_departures. Planned times only: for "is my bus late?" give the plan and say so; regional punctuality check_transit_disruption. No destination filter: read the board. Window 48 h, 15 per call. Results carry their attribution line.
436
+
437
+ | parameter | type | required | default | constraints |
438
+ | --- | --- | --- | --- | --- |
439
+ | `duration_min` | integer | no | `60` | min 5; max 1440 |
440
+ | `language` | string | no | `"de"` | one of `"de"`, `"en"` |
441
+ | `limit` | integer | no | `10` | min 1; max 15 |
442
+ | `modes` | array of string | no | — | min 1 item(s); each item: one of `"rail"`, `"subway"`, `"tram"`, `"bus"`, `"ferry"` |
443
+ | `stop` | string | no | — | min length 2 |
444
+ | `stop_id` | string | no | — | min length 3 |
445
+ | `when` | string | no | — | format `date-time`; pattern (288 characters — see the description; the `format` above is the short answer) |
446
+
447
+ - **`duration_min`** — How far past that moment to look, in minutes (5–1440, default 60). Small for "what goes now", a few hours for an evening. To reach the far end of the 48 h timetable, move `when` instead of widening this: a window of a whole day returns at most 15 rows and would answer about the wrong half of it.
448
+ - **`language`** — Set this on every call to the language the person is writing in: "en" if they wrote English, "de" if they wrote German. Do not leave it out because it has a default — the default is only the fallback when the language is genuinely unclear, and an English question answered in German is a wrong answer. Place names, station names and road numbers are never translated in either language; in English the German term is kept in parentheses so the person recognises it on signs and in local apps.
449
+ - **`limit`** — How many departures to return, earliest first (1–15, default 10). The result always says how many more were in the window.
450
+ - **`modes`** — Keep only these kinds of service: "bus", "tram", "subway" (U-Bahn), "rail" (every train, including S-Bahn and regional) or "ferry". Omit it unless the person named a kind — "nur Busse", "welche Tram". Several are allowed, which is what "die Busse und Bahnen vor dem Hbf" means. An S-Bahn is "rail": the feed does not always distinguish it, and the line name ("S 6") says which it is.
451
+ - **`stop`** — The stop, as the person says it: "Fulda, Bahnhof", "München, Marienplatz", "Köln, Hbf", "Hamburg, Rathausmarkt" (town first). Include the town when the person did — half the names in Germany exist in twenty towns, and a bare "Bahnhof" or "Hauptbahnhof" comes back as a list of candidates to choose from, so call it and let the result ask. Pass their words; do not guess an id. Give either stop OR the stop id, never both.
452
+ - **`stop_id`** — The stop's timetable id, exactly as a previous result of this tool gave it ("de:06631:1234"). It skips the name lookup and is exact — use it for a follow-up about a stop this tool has already named, and for one the person picked out of a candidate list.
453
+ - **`when`** — Start of the window as an ISO-8601 instant with an offset ("2026-10-02T07:30:00+02:00"). Leave it out for "now". Convert the person's words yourself — "morgen früh", "tonight" — and pass the instant; the answer is always rendered in Europe/Berlin.
454
+
431
455
  ### `check_station_facilities` — Station lifts and escalators
432
456
 
433
457
  **Read-only** — it changes nothing. Reaches a third-party source (open world). Idempotent: true. Destructive: false.
@@ -655,5 +679,5 @@ single call.
655
679
  ---
656
680
 
657
681
  Generated from `catalogue.json` by `scripts/gen-api-doc.mjs`. The snapshot was
658
- read from `https://mcp.viafrei.de/mcp` on 2026-09-29; no tool was invoked to
682
+ read from `https://mcp.viafrei.de/mcp` on 2026-10-02; no tool was invoked to
659
683
  produce it, so no data provider was contacted.
package/CHANGELOG.md CHANGED
@@ -5,7 +5,360 @@ a Changelog and the versions follow Semantic Versioning.
5
5
 
6
6
  ## [Unreleased]
7
7
 
8
- Nothing yet.
8
+ ## [1.5.4] - 2026-10-02
9
+
10
+ **Mirrors the server.** The bridge is versioned to match the ViaFrei MCP server it
11
+ relays to. The running server reports 1.5.4 while the registry's latest is 1.4.9, so
12
+ this release moves the package to the server's number and carries whatever had been
13
+ waiting under `[Unreleased]`. Prepared by the `Version sync` workflow: the shipped
14
+ reference was re-captured from the running server, and the probe reported the surface
15
+ **CHANGED** — the automation knows what moved, not what it means:
16
+
17
+ - tools: the server has get_departures, the snapshot does not
18
+ - tools: get_train_departures differs between the server and the snapshot
19
+
20
+ What that means, written by a person after reading the diff and asking the server:
21
+
22
+ - **`get_departures` is new**: scheduled departures from any German public-transport
23
+ stop — bus, tram, U-Bahn, S-Bahn, train, ferry — with line, destination and
24
+ platform, planned times only, a 48 h window and 15 per call, answering from the
25
+ DELFI static timetable (CC BY 4.0). It is the tool for "when does the next bus go",
26
+ for another day, or for a clock time more than two hours away. Asked on 2026-10-02,
27
+ prod answered that the timetable is **not loaded yet** and said so plainly instead
28
+ of inventing a board; `SOURCES.md` records that on the static-GTFS row rather than
29
+ promoting the source to *live*.
30
+ - **`get_train_departures` was re-described, not re-shaped**: its description now
31
+ draws the line against the new tool (buses, trams, a non-railway stop, another day
32
+ or a time over two hours away go to `get_departures`), keeps vague later-today
33
+ wording for itself, and sends "is the S1 punctual?" to `check_transit_disruption`
34
+ even though it is rail and about delay. The `station` argument's text adds that a
35
+ bare "Hauptbahnhof" should be sent as-is because the server lists the candidates.
36
+ No argument was added, removed or re-typed.
37
+ - The catalogue is **20 tools, 18 read-only**; `SOURCES.md` moves from nineteen /
38
+ seventeen to those numbers, and the live calls a re-measurement would cost from
39
+ fifteen to sixteen, because the tool gained is read-only and not fuel.
40
+
41
+ ### Added
42
+
43
+ - **A scheduled freshness probe: is the service alive, not merely answering?**
44
+ (`scripts/probe-freshness.mjs`, `npm run probe:freshness`, and the `Freshness`
45
+ workflow every six hours.)
46
+
47
+ The gap it closes was real and nothing here could see it. A server can return
48
+ `200` with a correctly shaped body for days while an ingest worker is dead, and
49
+ every existing check survives that: the stub tests prove the bridge's
50
+ **transport**, `check:docs` proves the document matches the **snapshot**, and the
51
+ cut-time probe proves the **surface** still matches. None of them reads the age
52
+ of the data in an answer.
53
+
54
+ This asks the running server and judges `_meta.asOf`, which every real-time tool
55
+ carries, against a limit **per tool**. That is not a detail: measured against
56
+ prod on 2026-10-01, autobahn, transit, weather and departures all answered within
57
+ a minute while `check_road_status` was **7.5 h** old, because it blends the BASt
58
+ roadworks feed, which the Mobilithek catalogue declares as **twice daily**. One
59
+ global limit would be either useless for the fast feeds or permanently red for the
60
+ slow one — and a check that is permanently red is a check that gets switched off.
61
+
62
+ Each limit sits about an order of magnitude above the source's own **declared
63
+ cadence** rather than above the single reading this was written against, so it
64
+ catches a dead worker and cannot fire on normal variation. 72 h for roadworks is
65
+ roughly six times a twice-daily interval and clears a weekend; 90 min for the
66
+ other four is many times their providers' own floors.
67
+
68
+ **The callable set is an allow-list, not a deny-rule**, and the direction is the
69
+ point: with a deny-rule a new entry runs unless it matches, and here the failure
70
+ mode is a licence breach against a provider that can revoke access. Fail-closed is
71
+ the right default on that path, so a tool nobody named is refused.
72
+
73
+ **No fuel tool is ever called**, stated separately because it carries the reason
74
+ the allow-list does not: `find_cheapest_fuel` and `find_fuel_station` answer from
75
+ MTS-K / Tankerkönig, which sets a minimum interval per station and limits use to
76
+ answering a consumer's question. A monitoring query is not that. Both guards
77
+ **refuse before any request**, and the fuel arm is not redundant — a self-test
78
+ case adds a fuel tool to **both** lists, which is the realistic way the exclusion
79
+ would be lost, and it is still refused on licence grounds.
80
+
81
+ **Scheduled, not on push**, because CI here reaches nothing by design: a
82
+ freshness failure is news about the service, not about the commit, and reddening
83
+ a contributor's push for it would teach people to ignore red.
84
+
85
+ **The instrument is proved before it is trusted**, and the workflow runs the two
86
+ steps in that order. The self-test is hermetic — every case against a local stub
87
+ on loopback — and it is the only place the staleness verdict is ever exercised,
88
+ because a healthy endpoint cannot produce a stale payload. So the self-test runs
89
+ **first**: if it fails the instrument is broken, and if the live step fails the
90
+ service is. A monitor whose verdict is never proved reports success about a dead
91
+ service exactly as convincingly as about a live one.
92
+
93
+ Three exit codes, because "could not check" and "checked and it is wrong" are
94
+ different answers: `0` every feed inside its limit, `1` a real defect (stale, or
95
+ no `asOf`, or no attribution, or `isError`, or an `asOf` in the future — a clock
96
+ fault must not read as very fresh), and `2` could not check (endpoint, session,
97
+ or a response shape nobody recognises). An unparsable body is deliberately `2`
98
+ rather than `1`.
99
+
100
+ Found by its own self-test and fixed: `--json` printed the human summary to
101
+ stdout after the document, so the stream did not parse. Both streams are now
102
+ pinned by a case.
103
+
104
+ **Found in review, and it is the case the first draft had no test for.** A
105
+ JSON-RPC *error* — what the server returns for a retired tool or a renamed
106
+ argument, and this watch list hard-codes seven argument names — fell through to
107
+ the no-result branch: **exit 1, every feed printed `STALE`, on a healthy
108
+ service**, and `error.message`, the entire diagnosis, discarded. It is now exit
109
+ **2** with the server's code and message printed, a distinct `?????` label
110
+ because `STALE` is a claim about the *data* rather than about our ability to
111
+ measure it, and a refusal sentence that no longer says the body failed to parse —
112
+ it parsed perfectly. Four assertions cover it, and removing the guard turns all
113
+ four red.
114
+
115
+ - **The registry is compared with the server every six hours, and a release that
116
+ would close the gap is PREPARED — never performed** (`scripts/propose-release.mjs`,
117
+ `npm run propose:release`, and the `Version sync` workflow).
118
+
119
+ The bridge is versioned to match the server it relays to, and until now the only
120
+ thing that noticed the registry falling behind was a person checking by hand: 1.4.9
121
+ sat on prod while npm said 1.4.8 until somebody asked. Every step of catching up
122
+ was mechanical and identical each time — `npm version`, the probe's `--write`,
123
+ `docs:api`, the `## [X.Y.Z]` block, the gates — so the workflow does them and
124
+ pushes the result as `release/X.Y.Z` with a pull request.
125
+
126
+ **What it will not do, by design.** It does not merge, tag or publish, for three
127
+ reasons each sufficient alone: the merge needs a reviewer verdict covering HEAD,
128
+ which a bot merging through the API would bypass; an npm version is immutable, so
129
+ a wrong one is forever; and when the server's **surface** changed rather than its
130
+ number, the release note needs a sentence about what the change means, which
131
+ nothing here can write — the 1.4.9 cut carried a licence-relevant fix for exactly
132
+ that case. So the verdict, the merge and the tag stay with a person, and the tag
133
+ publishes as it always has.
134
+
135
+ **Four states, each named in the output**, because the workflow branches on them:
136
+ `in-sync`, `awaiting-tag` (main already carries the server's version; the tag is
137
+ the missing step), `drift` (the one that is prepared), and `behind` — the server
138
+ BEHIND the registry, which is exit 1 and proposes nothing, because a downgrade is
139
+ a decision about whether prod rolled back or a publish was premature.
140
+
141
+ **Every version string read from the network is checked against `X.Y.Z` before it
142
+ is used anywhere**, since it ends up in a branch name, a commit and an `npm
143
+ version` argument; a prerelease on either side is a refusal. A mutant with the
144
+ guard removed is part of the self-test, so the guard is proved live rather than
145
+ present. Detect **writes nothing**, asserted by hashing the five files it may
146
+ later touch. The server is read through the catalogue probe — one reader of that
147
+ endpoint, `initialize` and the four list calls, no tool invoked — and the probe's
148
+ own WRONG/DATED verdict decides the lead of the CHANGELOG block: DATED says the
149
+ surface is unchanged; WRONG lists what moved and says **a person must describe it
150
+ before this merges**, and the pull request is opened as a **draft**. A failing
151
+ offline gate is also a draft rather than a lost run: the gate's output goes into
152
+ the pull request, where the person who has to act on it will read it.
153
+
154
+ A proposal is idempotent across runs — a `release/X.Y.Z` branch already on origin
155
+ is left alone, so a pull request waiting for its review is not joined by a twin
156
+ every six hours — and a CHANGELOG already carrying the block is a refusal. The
157
+ workflow dispatches CI on the branch explicitly, because a push or a pull request
158
+ made with the workflow token starts no workflow by GitHub's rule. **Two
159
+ preconditions are asserted before anything is read**, because each failure would
160
+ otherwise conceal itself: the run must be on the default branch (a dispatch from
161
+ another ref would branch off it and open a pull request carrying its commits), and
162
+ the repository must allow Actions to open pull requests — a setting that is OFF by
163
+ default (and was, here, until 2026-10-02), without which `gh pr create` fails after
164
+ the branch is pushed and the orphan branch then silences every later run. That
165
+ read is administration-class and the workflow token may not be able to make it,
166
+ so it has **three outcomes**: a successful `false` refuses, a failed read is named
167
+ and the run continues, because if the pull request still cannot be opened the
168
+ branch just pushed is deleted again for the same reason.
169
+
170
+ The MCP stub the catalogue probe's self-test ran on moved to `scripts/mcp-stub.mjs`,
171
+ and the pass/fail counter both self-tests print through to `scripts/check-harness.mjs`,
172
+ so this self-test shares them rather than carrying copies; the probe's own case
173
+ count is unchanged.
174
+
175
+ ### Fixed
176
+
177
+ - **The `Version sync` workflow runs the leak sweep on the tree it prepares.** Its
178
+ first real run was this release, and the proposal it pushed carried two bare
179
+ numbers from the new tool's schema text — a minutes-per-day maximum and an example
180
+ stop id — that the public-repo sweep refuses. Nothing in the workflow had asked:
181
+ its three gates compare the shipped files with each other, not whether the
182
+ snapshot may be published. What caught it was the CI run the workflow itself
183
+ dispatched — the pull request's own `pull_request` run never executed, it sat at
184
+ `action_required` — and the step that failed was the publish-hygiene gate on the
185
+ **built tarball**, because `API.md` ships in the package: the numbers were on their
186
+ way into the published artefact, not only into the repository. The sweep now runs
187
+ after the five files are staged, and a finding makes the proposal a **draft** with
188
+ the findings in the pull-request body. That job builds nothing, so it cannot run
189
+ the tarball gate itself: it covers this class of finding, not the exact gate that
190
+ fired. The two numbers are on `numbers.allowed` as the harmless values they are;
191
+ both exist only in text the server controls, so a re-wording upstream makes the
192
+ sweep refuse them as unused — loudly, which is the right direction.
193
+ - **`check-sources`' self-test reads its counts off the check's own summary line**
194
+ instead of carrying them: three of its cases said "seventeen" and "fifteen", and
195
+ when the page correctly moved to eighteen and sixteen they went inert — one passed
196
+ without mutating anything — or asserted the previous release's numbers. A mutation
197
+ that changes nothing, or that would change one of two occurrences, is now a
198
+ refusal. The check's own pattern for the re-run sentence accepts "would now mean"
199
+ beside "would still mean", because the count did move this time and the page says
200
+ so.
201
+
202
+ ## [1.4.9] - 2026-10-01
203
+
204
+ **Mirrors the server.** The bridge is versioned to match the ViaFrei MCP server
205
+ it relays to, and prod moved to 1.4.9; this release carries the three changes
206
+ that had been waiting under `[Unreleased]` for a number to mirror. The shipped
207
+ reference was re-captured from the running server at the cut, and the probe that
208
+ did it reported the surface **unchanged** — only the version string and the
209
+ capture date moved.
210
+
211
+ ### Added
212
+
213
+ - **The sources page is now checked against the catalogue snapshot** (`scripts/check-sources.mjs`,
214
+ `npm run check:sources`), in CI and in the publish workflow (#32).
215
+
216
+ `SOURCES.md` states counts *about the server* — how many tools it exposes, how many are
217
+ read-only, which are fuel and therefore excluded from its spot check. Nothing compared
218
+ them with `catalogue.json`, which ships in the same tarball and holds the answers, and
219
+ the page spent two releases describing a server it no longer matched.
220
+
221
+ **Offline on purpose, and that is what makes it worth having.** Both files already ship
222
+ together, so this needs no network and runs on every push rather than at the cut. It
223
+ would have caught all three defects of the previous entry, including the one that got
224
+ past a first review round: the **derived** call count is computed as read-only minus the
225
+ excluded fuel tools, never read from the prose, because that is the one number on the
226
+ page a reader might act on.
227
+
228
+ The licence arm is the one that matters: a fuel tool present in the snapshot and not
229
+ named by the page is a failure, because that exclusion is an MTS-K condition rather
230
+ than a convenience, and a tool counts as fuel-constrained if its NAME says so **or**
231
+ its DESCRIPTION names the provider — measured on this snapshot, both fuel tools name
232
+ theirs in prose and no other tool does, so a rename alone cannot hide one. `npm run
233
+ test:sources` prints the case count; no number is written here, because this entry's
234
+ first draft stated one and it was stale within the round.
235
+
236
+ Two limits are pinned rather than assumed. A fuel rule gone inert REFUSES (exit 2)
237
+ instead of reporting an empty excluded set. And a tool renamed away from `fuel` *whose
238
+ description also stops naming the provider* matches neither arm and is not on the floor,
239
+ so that one still fails through the arithmetic, with a message about a call count rather
240
+ than about an unprotected tool. That is the honest reach of two text rules, and the
241
+ weaker behaviour is asserted rather than hoped for.
242
+
243
+ - **A cut-time probe that compares the shipped reference with the running server**
244
+ (`scripts/probe-catalogue.mjs`, `npm run probe:catalogue`; `--write` re-captures) (#33).
245
+
246
+ At the 1.4.6 cut the shipped reference described server 1.3.22 and omitted a tool the
247
+ server exposed. `check:docs` passed throughout — it proves `API.md` matches
248
+ `catalogue.json`, so **a stale pair passes together**. Nothing compared either with the
249
+ server.
250
+
251
+ **It is deliberately not a CI step.** This repository's test posture is that CI reaches
252
+ nothing, which is why every other check here is offline; a comparison with the running
253
+ server needs a network call, so this is run by a person at the cut and is the only
254
+ script here that touches the network. It is named `probe:` rather than `check:` so that
255
+ distinction is visible in `package.json`.
256
+
257
+ Read-only: `initialize` and the four list calls, **no tool invoked**, session deleted
258
+ afterwards. That is a licence requirement and not courtesy — the fuel source sets a
259
+ per-station floor.
260
+
261
+ **Its report distinguishes WRONG from DATED**, because those are the two real histories
262
+ and they need different remedies: 1.4.6's reference omitted a tool, which misleads a
263
+ reader about what they are holding; 1.4.8's had only a stale version string. Reporting
264
+ one as the other would be worse than no probe. Ten self-test cases, none of which touch
265
+ the network — every one runs against a local stub — including both halves of that
266
+ distinction, an empty list refused rather than compared equal, and `--write` proved to
267
+ produce a snapshot the comparator then accepts.
268
+
269
+ - **`runToolAsync`** in `scripts/tools.mjs`, with the same deadline and refusal as
270
+ `runTool`. The synchronous runner blocks the caller's event loop, so a caller that is
271
+ itself serving the child cannot use it — the probe's self-test serves a stub in-process
272
+ and spawns the probe against it, and every case deadlocked until this existed. Added to
273
+ the one `SPAWNERS` array the bare-name sweep derives from, so it is covered by the same
274
+ rule as its sibling rather than being a quiet exemption.
275
+
276
+ ### Fixed
277
+
278
+ - **Both CI deprecation warnings, in both workflows.** `build-and-test` was emitting two
279
+ notices: the Node-20 runtime of `actions/checkout@v4` and `actions/setup-node@v4` is
280
+ deprecated and GitHub is already forcing those actions onto Node 24, and the
281
+ `ubuntu-latest` label migrates to Ubuntu 26 from 2026-10-19.
282
+
283
+ **The warning named one workflow; the measurement named two.** `publish.yml` pinned both
284
+ actions by commit sha, which looked like the careful half of the repository — but
285
+ `action.yml` at each of those pinned shas declares `using: node20`. So the workflow that
286
+ publishes to npm was on the deprecated runtime too and said nothing about it, because a
287
+ sha pin does not report its own age. A fix confined to `ci.yml` would have cleared the
288
+ log and left the publish path exactly where it was.
289
+
290
+ Both files now pin the same runner image, `ubuntu-24.04`, and the same two actions by
291
+ sha: checkout **v7.0.1** and setup-node **v7.0.0**, resolved from the API rather than
292
+ transcribed, and each verified to declare `using: node24` at the sha actually pinned —
293
+ which is what closes the notice rather than deferring it. That also leaves one version
294
+ of each action in the repository instead of two.
295
+
296
+ **v7, not v5, and it is a three-major move.** v5 clears today's warning and leaves this
297
+ repository two majors behind the same deadline. The first draft of this entry described
298
+ v7.0.0's release notes and called them the only behaviour change — true of that release,
299
+ false of the upgrade, and the paragraph's whole job is to justify the size of the jump.
300
+
301
+ The jump was therefore checked mechanically instead, which is shorter and re-runnable:
302
+ **diff the declared input sets at the two shas.** Across v4.4.0 → v7.0.0 setup-node
303
+ removes exactly one input, `always-auth`, and adds `package-manager-cache`; checkout
304
+ removes and adds none. Between them the two workflows pass four inputs — `node-version`,
305
+ `registry-url`, `cache` and `fetch-depth` — and all four are still declared, so the
306
+ `registry-url` → `.npmrc` path the publish depends on is intact.
307
+
308
+ The breaking changes the intervening majors do declare are inert here, by enumeration
309
+ rather than by assumption: setup-node v5's automatic package-manager detection and v6's
310
+ narrowing of it to npm cannot apply, because both jobs pass `cache: npm` explicitly and
311
+ this `package.json` has no `packageManager` field; checkout v5's minimum runner version
312
+ (2.327.1) is far below what GitHub-hosted runners run; checkout v7's refusal to check out
313
+ a fork's head applies to `pull_request_target` and `workflow_run`, neither of which
314
+ appears anywhere under `.github/`; and setup-node v7's removal of the dummy
315
+ `NODE_AUTH_TOKEN` export is an **improvement** on this path — that variable appears
316
+ nowhere in either workflow, and upstream's own pull request says the dummy value could
317
+ corrupt an `.npmrc` during an OIDC publish, which is how publishing here works.
318
+
319
+ One is named rather than waved past, because it touches the publish gate. checkout v6
320
+ moved the persisted git credential into a separate file, and `publish.yml` runs one git
321
+ command that touches the remote after checkout: the `git fetch origin main` the
322
+ tag-containment guard needs (the step's other two, a `rev-parse` and a `merge-base`, are
323
+ local). This repository is public, so that fetch succeeds with or without a credential;
324
+ and if it ever did not, the guard exits **2** and refuses to publish rather than
325
+ publishing a commit `main` does not contain. Fail-closed, so the bad outcome is a blocked
326
+ release and never a wrong one. It is also the one step a `workflow_dispatch` dry run
327
+ cannot exercise, since the guard is gated on a tag.
328
+
329
+ A floating tag is what hid this, so nothing here floats: all four `- uses:` lines in the
330
+ repository are now `@<sha> # vX.Y.Z`, and neither `runs-on` is a label that can change
331
+ under a workflow nobody re-read.
332
+
333
+ Written while prod and the registry were both at 1.4.8, so this said "no version bump:
334
+ there is no number to mirror". Prod moved to 1.4.9 before the cut, so the number now
335
+ exists and this ships under it.
336
+
337
+ - **`SOURCES.md` claimed a measurement that stopped being true, and the stale half is
338
+ a licence condition.** The page said its status column was measured by calling
339
+ "fifteen of the server's sixteen read-only tools — every one except
340
+ `find_cheapest_fuel`". As of the 2026-09-29 capture shipped alongside it, the server
341
+ exposes nineteen tools, seventeen of them read-only, and — this is the part that
342
+ matters — a **second** fuel tool, `find_fuel_station`.
343
+
344
+ The exclusion of `find_cheapest_fuel` is not a convenience: MTS-K sets a minimum
345
+ interval per station and its terms make needless querying a real risk to the access
346
+ itself. That reasoning applies to `find_fuel_station` identically, and the page did
347
+ not name it, so a reader following the page's own method would have called a fuel
348
+ tool the page meant to exclude.
349
+
350
+ The measurement is now scoped to the date and the server it was taken against, the
351
+ growth since is stated, and **both** fuel tools are named as excluded. It is
352
+ deliberately **not** re-run: that would still cost **fifteen** live calls against
353
+ real providers to re-confirm statuses already known — the same fifteen as at the
354
+ original measurement, because the one read-only tool the server gained is the
355
+ second fuel tool and is excluded. Sixteen minus one was fifteen; seventeen minus
356
+ two is fifteen again. Dated on purpose, and said out loud — a measurement carried forward under a present-tense sentence is
357
+ the failure that section exists to avoid.
358
+
359
+ Not released on its own: when it was written prod was at 1.4.8, already published, and
360
+ putting the package a patch ahead of the endpoint it relays to would have been worse than
361
+ waiting. It rode the next version sync, which is this one.
9
362
 
10
363
  ## [1.4.8] - 2026-09-29
11
364
 
@@ -1292,6 +1645,8 @@ for it, so the number is free; the bridge will use it when the platform does.
1292
1645
  commits, and a squash makes them unreachable from `main` - which would turn
1293
1646
  the check red on `main` for everybody, for something no contributor did.
1294
1647
 
1648
+ [1.5.4]: https://github.com/mavrovde/viafrei-bridge/releases/tag/v1.5.4
1649
+ [1.4.9]: https://github.com/mavrovde/viafrei-bridge/releases/tag/v1.4.9
1295
1650
  [1.3.22]: https://github.com/mavrovde/viafrei-bridge/releases/tag/v1.3.22
1296
1651
  [1.3.16]: https://github.com/mavrovde/viafrei-bridge/releases/tag/v1.3.16
1297
1652
  [1.3.15]: https://github.com/mavrovde/viafrei-bridge/releases/tag/v1.3.15
package/SOURCES.md CHANGED
@@ -134,18 +134,44 @@ one labelled `not re-measured`** was checked by calling the public endpoint on
134
134
  **2026-09-27** and reading which source the answer named. A source can be licensed, cleared and loaded and still not answer
135
135
  a question today; where that is so, this page says it.
136
136
 
137
- The measurement was taken by calling **fifteen of the server's sixteen read-only
138
- tools — every one except `find_cheapest_fuel`** — once each, and reading
139
- `_meta.sources` out of the result: not by reading the code, and not by asking
140
- whether a feed was running. Thirteen sources were named by at least one answer.
141
- The tool not called is the one whose row is deliberately not re-measured, and
142
- the count above says so rather than absorbing it: a status this page cannot stand behind is
143
- worse than an honest gap.
137
+ The measurement was taken **on 2026-09-27**, when the server exposed sixteen
138
+ read-only tools, by calling **fifteen of them — every one except
139
+ `find_cheapest_fuel`** — once each, and reading `_meta.sources` out of the result:
140
+ not by reading the code, and not by asking whether a feed was running. Thirteen
141
+ sources were named by at least one answer. The tool not called is the one whose row
142
+ is deliberately not re-measured, and the count says so rather than absorbing it: a
143
+ status this page cannot stand behind is worse than an honest gap.
144
+
145
+ **The server has grown since that measurement, and this page has not re-run it.**
146
+ As of the 2026-10-02 capture shipped alongside this page it exposes twenty tools,
147
+ eighteen of them read-only. Between that measurement and the 2026-10-02 capture it
148
+ gained two tools, and both matter here. One is a SECOND fuel tool,
149
+ `find_fuel_station`, excluded from any spot check for exactly the same reason
150
+ `find_cheapest_fuel` is, and the reason is a licence condition rather than a
151
+ convenience: MTS-K sets a minimum interval per station and its terms make needless
152
+ querying a real risk to the access itself. **Both fuel tools are excluded, not one.**
153
+ The other is `get_departures` (server 1.5.4): scheduled departures from any
154
+ public-transport stop, answering from the DELFI static timetable — a source this
155
+ page had listed as *read* with nothing using it. Something uses it now, and on
156
+ 2026-10-02 the server's own answer was that the timetable is not loaded yet; the row
157
+ below says exactly that rather than promoting it.
158
+
159
+ The sentence above therefore describes what was measured on 2026-09-27 and not what
160
+ the server offers today. Re-running it would now mean **sixteen** live calls against
161
+ real providers: the read-only tools minus the two excluded fuel tools. The count moved
162
+ by one because `get_departures` is read-only and not fuel, so a re-run would call
163
+ it. Those calls would mostly re-confirm statuses this page already knows, so it is
164
+ dated on purpose rather than refreshed on a schedule — and dated is said out loud,
165
+ because a measurement silently carried forward under a present-tense sentence is the
166
+ failure this section exists to avoid.
144
167
 
145
168
  - **live** — an answer came back naming it when this page was checked;
146
169
  - **in the service** — licensed and loaded, and the spot check produced no
147
170
  answer that named it, so it is reported as unconfirmed rather than as live;
148
- - **read** — the licence is read and cleared, and nothing uses it yet.
171
+ - **read** — the licence is read and cleared, and no answer has been seen from it:
172
+ because nothing asks it, because what asks it is told the data is not loaded, or
173
+ because what asks it has not been seen to get an answer either way. Where it is not
174
+ simply that nothing asks it, the row says so.
149
175
 
150
176
  There used to be a fourth value, **not on the public service today**, and no row
151
177
  carries it any more: the three rows that did now answer. It is removed from this
@@ -169,8 +195,8 @@ status somebody could still be relying on.
169
195
  | Public-transport realtime (GTFS-RT Trip Updates) | DELFI e.V., via the national access point (Mobilithek) | Germany-wide departure and arrival forecasts | real time | **CC BY-SA (version unstated)** | live |
170
196
  | FaSta — Facility Status | Deutsche Bahn AG (DB API Marketplace) | Live state of lifts and escalators at stations | live status | CC BY 4.0 | live |
171
197
  | Geocoding (addresses and points of interest) | OpenStreetMap contributors | Street and house-number points and mapped points of interest in Germany | refreshed from the OSM extract | **ODbL 1.0** | live |
172
- | Timetable data (static GTFS) | DELFI e.V. | Germany-wide scheduled public transport | weekly release | CC BY 4.0 | read |
173
- | Stop directory (zHV) | DELFI e.V. | Every public-transport stop in Germany with its identifier and coordinates | weekly release | CC BY 4.0 | read |
198
+ | Timetable data (static GTFS) | DELFI e.V. | Germany-wide scheduled public transport | weekly release | CC BY 4.0 | read — asked by `get_departures` since server 1.5.4; on 2026-10-02 the server answered that the timetable is not loaded yet, and said so rather than inventing a board |
199
+ | Stop directory (zHV) | DELFI e.V. | Every public-transport stop in Germany with its identifier and coordinates | weekly release | CC BY 4.0 | read — `get_departures` resolves stop names against it since server 1.5.4; the 2026-10-02 answer named no stop and said only that the timetable is not loaded, so whether the directory answered cannot be read off it |
174
200
  | Disruption reports (Störungsmeldungen) | DELFI e.V., via the national access point (Mobilithek) | Germany-wide public-transport disruption messages | real time | **CC BY-SA 4.0** | read |
175
201
  | Station car parks (DB BahnPark) | Deutsche Bahn AG (DB API Marketplace) | Car parks at railway stations, with their operator and access details | continuous | **dl-de/by-2-0** | read |
176
202
  | Administrative units and place names | Bundesamt für Kartographie und Geodäsie (BKG), product GN250 | Länder, Regierungsbezirke, Kreise, Gemeinden with their official keys and names | yearly release | **dl-de/by-2-0** | live |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "viafrei",
3
- "version": "1.4.8",
3
+ "version": "1.5.4",
4
4
  "description": "German traffic, rail, parking, charging, fuel, address and weather data in your AI assistant - MCP, no API key. stdio bridge to the hosted ViaFrei MCP server.",
5
5
  "keywords": [
6
6
  "mcp",
@@ -75,6 +75,14 @@
75
75
  "check:versions": "node scripts/check-versions.mjs",
76
76
  "docs:api": "node scripts/gen-api-doc.mjs",
77
77
  "check:docs": "node scripts/gen-api-doc.mjs --check",
78
+ "check:sources": "node scripts/check-sources.mjs",
79
+ "test:sources": "node scripts/check-sources.test.mjs",
80
+ "probe:catalogue": "node scripts/probe-catalogue.mjs",
81
+ "test:probe": "node scripts/probe-catalogue.test.mjs",
82
+ "probe:freshness": "node scripts/probe-freshness.mjs",
83
+ "test:freshness": "node scripts/probe-freshness.test.mjs",
84
+ "propose:release": "node scripts/propose-release.mjs",
85
+ "test:propose": "node scripts/propose-release.test.mjs",
78
86
  "rules:show": "node scripts/show-rules.mjs"
79
87
  },
80
88
  "dependencies": {