viafrei 1.3.22 → 1.4.8

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.
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-28.** Generating this file makes
12
+ **It is a dated snapshot, taken on 2026-09-29.** 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.3.22 |
20
+ | Server | `viafrei` 1.4.8 |
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-28 |
25
- | Surface | 18 tools, 10 resources, 2 resource templates, 9 prompts |
24
+ | Captured from | `https://mcp.viafrei.de/mcp` on 2026-09-29 |
25
+ | Surface | 19 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) — 18
34
+ - [Tools](#tools) — 19
35
35
  - [Resources](#resources) — 10
36
36
  - [Resource templates](#resource-templates) — 2
37
37
  - [Prompts](#prompts) — 9
@@ -86,7 +86,7 @@ The server's own instructions to a connecting client, verbatim:
86
86
 
87
87
  **Read-only** — it changes nothing. Reaches a third-party source (open world). Idempotent: true. Destructive: false.
88
88
 
89
- > Returns the cheapest petrol stations for one fuel grade around a place or coordinate, with price per litre, brand, address, distance and open state. Use when the user asks where to fill up, what fuel costs nearby, or for a cheap stop on a drive. Do NOT use for charging an electric car (call find_charging_station), for price history, or for motorway traffic (call check_autobahn_traffic). Radius ≤ 25 km, at most 10 stations. The result names the age of any price over an hour old. Prices are for consumer information only; the result's attribution line and the MTS-K note must be shown to the user.
89
+ > Returns the cheapest stations for one fuel grade near a place or coordinate: price per litre, brand, address, distance, open state. Use when the user asks where to fill up or what fuel costs. Do NOT use for charging an electric car (call find_charging_station), price history, or traffic (call check_autobahn_traffic). Radius ≤ 25 km, at most 10 stations. For a brand, a name, open now or at a named time, or nearest-first, call find_fuel_station. Each line names the age of its price and opening-hours claim. Prices are consumer information only; the attribution line and MTS-K note must be shown.
90
90
 
91
91
  | parameter | type | required | default | constraints |
92
92
  | --- | --- | --- | --- | --- |
@@ -106,6 +106,42 @@ The server's own instructions to a connecting client, verbatim:
106
106
  - **`place`** — Where to look, as free text: a city ("München", "Munich"), a district or Kreis ("Kreis Fulda"), a Bundesland, a station or stop ("Hamburg Hbf"), a motorway ("A7"), or a street address with a house number ("Hauptstraße 12, 36037 Fulda"). Use this instead of coordinates whenever the person named a place. An address needs its town or postcode — a street and a number alone exist in many towns. Give either place OR lat+lon, never both.
107
107
  - **`radius_km`** — Search radius around the place in kilometres (1–25, default 5). The provider's terms cap it at 25 km — a larger circle is a dataset request, not a consumer question.
108
108
 
109
+ ### `find_fuel_station` — Find a filling station
110
+
111
+ **Read-only** — it changes nothing. Reaches a third-party source (open world). Idempotent: true. Destructive: false.
112
+
113
+ > Finds filling stations around a place or coordinate under any combination of filters — grade, brand, name, open now, open at a time you name, open 24 h — sorted by distance, price or name. Use when the question is WHICH station: the closest diesel to a stop, an ARAL open tonight, what one forecourt sells. Do NOT use for the plain "where is fuel cheapest" question (call find_cheapest_fuel) or for charging an electric car (call find_charging_station). Every result carries the attribution and the MTS-K note, and says how old each price and opening-hours claim is.
114
+
115
+ | parameter | type | required | default | constraints |
116
+ | --- | --- | --- | --- | --- |
117
+ | `brand` | string | no | — | min length 1 |
118
+ | `fuel` | string | no | — | one of `"e5"`, `"e10"`, `"diesel"` |
119
+ | `language` | string | no | `"de"` | one of `"de"`, `"en"` |
120
+ | `lat` | number | no | — | min -90; max 90 |
121
+ | `limit` | integer | no | `5` | min 1; max 10 |
122
+ | `lon` | number | no | — | min -180; max 180 |
123
+ | `name` | string | no | — | min length 1 |
124
+ | `open_at` | string | no | — | pattern `^(?:\d{1,2}:\d{2}\|\d{4}-\d{2}-\d{2}[T ]\d{1,2}:\d{2})$` |
125
+ | `open_now` | boolean | no | `false` | — |
126
+ | `place` | string | no | — | min length 1 |
127
+ | `radius_km` | number | no | `5` | min 1; max 25 |
128
+ | `sort` | string | no | `"distance"` | one of `"distance"`, `"price"`, `"name"` |
129
+ | `whole_day` | boolean | no | `false` | — |
130
+
131
+ - **`brand`** — Only stations of this brand, matched case-insensitively anywhere in the brand field: "ARAL", "Shell", "TotalEnergies", "JET". Use it when the person named a chain ("die ARAL an der B1"). Free stations often carry no brand at all and are then not matched by any brand.
132
+ - **`fuel`** — Only stations with a current price for this grade: "e5" (Super E5), "e10" (Super E10) or "diesel". Omit to get every station near the place whatever it sells — the answer then lists all the grades it holds a price for. Required when sort is "price", because a price ordering needs a grade.
133
+ - **`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.
134
+ - **`lat`** — Latitude in WGS 84, e.g. 48.137. Use with lon when the caller already holds coordinates; otherwise use place.
135
+ - **`limit`** — How many stations to return (1–10, default 5).
136
+ - **`lon`** — Longitude in WGS 84, e.g. 11.576. Use with lat; otherwise use place.
137
+ - **`name`** — Part of the station's own name or brand, case-insensitive: "Autohof", "Raststätte Fulda". Use it when the person named a specific forecourt rather than a chain. Combine with place to keep the search local.
138
+ - **`open_at`** — Only stations open at that time in Germany (Europe/Berlin): "23:30" means the next time the clock shows 23:30, and "2026-09-30 06:15" a specific local date and time. Use it for "is it still open tonight". Do not pass it together with open_now — they ask the same question about two different clocks.
139
+ - **`open_now`** — When true, only stations the published opening hours say are open at this moment. Default false. A station whose hours we have never read is NOT returned by this filter and is counted in the answer instead — the result never guesses that an unknown station is open.
140
+ - **`place`** — Where to look, as free text: a city ("München", "Munich"), a district or Kreis ("Kreis Fulda"), a Bundesland, a station or stop ("Hamburg Hbf"), a motorway ("A7"), or a street address with a house number ("Hauptstraße 12, 36037 Fulda"). Use this instead of coordinates whenever the person named a place. An address needs its town or postcode — a street and a number alone exist in many towns. Give either place OR lat+lon, never both.
141
+ - **`radius_km`** — Search radius around the place in kilometres (1–25, default 5). 25 km is the provider's own ceiling — a larger circle is a dataset request, not a consumer question.
142
+ - **`sort`** — Order of the answer: "distance" (nearest first, the default — use it for "closest diesel to Hamburg Hbf"), "price" (cheapest first, needs fuel), or "name" (alphabetical, for a person scanning a list of a brand's forecourts).
143
+ - **`whole_day`** — When true, only stations the provider flags as open around the clock (24/7). Default false. Use it for a night drive; it is a stricter filter than open_now, which is satisfied by a station that closes at 22:00.
144
+
109
145
  ### `find_parking` — Parking nearby
110
146
 
111
147
  **Read-only** — it changes nothing. Answers from data this service already holds (closed world). Idempotent: true. Destructive: false.
@@ -334,7 +370,7 @@ The server's own instructions to a connecting client, verbatim:
334
370
 
335
371
  **Read-only** — it changes nothing. Answers from data this service already holds (closed world). Idempotent: true. Destructive: false.
336
372
 
337
- > Returns how punctual public transport is right now in one German region: the share of distinct trips more than 5 minutes late at least once, trips with a cancelled stop, the trend against the previous window, and how many trips that rests on. Use when the user asks whether buses and trains are running normally, or whether a strike or storm is disrupting local transport. Do NOT use for one line, trip or station — per-line realtime is not available; the region's figures are the answer. Region-wide aggregates only; window ≤ 120 minutes. CC BY-SA 4.0: show the attribution line to the user.
373
+ > Returns how punctual public transport is right now in one German region: the share of distinct trips at least once more than 5 minutes late, trips with a cancelled stop, the trend against the previous window, and how many trips that rests on. Use when the user asks whether buses and trains are running normally, or whether a strike or storm is disrupting local transport. Do NOT use for one line, trip or station — per-line realtime is unavailable; the region's figures are the answer. Region-wide aggregates only; window ≤ 120 min. CC BY-SA (share-alike): show the attribution line to the user.
338
374
 
339
375
  | parameter | type | required | default | constraints |
340
376
  | --- | --- | --- | --- | --- |
@@ -619,5 +655,5 @@ single call.
619
655
  ---
620
656
 
621
657
  Generated from `catalogue.json` by `scripts/gen-api-doc.mjs`. The snapshot was
622
- read from `https://mcp.viafrei.de/mcp` on 2026-09-28; no tool was invoked to
658
+ read from `https://mcp.viafrei.de/mcp` on 2026-09-29; no tool was invoked to
623
659
  produce it, so no data provider was contacted.
package/CHANGELOG.md CHANGED
@@ -7,6 +7,275 @@ a Changelog and the versions follow Semantic Versioning.
7
7
 
8
8
  Nothing yet.
9
9
 
10
+ ## [1.4.8] - 2026-09-29
11
+
12
+ **A version-sync release.** The bridge is published at the version the ViaFrei server
13
+ is serving, so that `npx viafrei@X.Y.Z` and the endpoint it relays to are named by one
14
+ number. `mcp.viafrei.de` moved to `1.4.8`, so the bridge follows. 1.4.7 is absent from
15
+ the registry for the same reason 1.4.0–1.4.5 were: that platform version carried no
16
+ bridge change.
17
+
18
+ **Nothing in the bridge changed at all** — not the runtime, not the tooling, not a
19
+ gate. The diff against 1.4.6 is the three version fields plus the re-captured API
20
+ reference. If 1.4.6 works for you, this is the same code under a number that matches
21
+ the server.
22
+
23
+ ### Changed
24
+
25
+ - **The API reference is re-captured at 1.4.8** (`API.md`, `catalogue.json`). Unlike
26
+ 1.4.6's re-capture, this one is only a date and a version string: the server's
27
+ surface did **not** change between 1.4.6 and 1.4.8. Verified rather than assumed —
28
+ the same 19 tools, 10 resources, 2 resource templates and 9 prompts, with no tool
29
+ added or removed, no description altered and no `required` list moved, compared
30
+ field-by-field against the stored snapshot before the new one was written.
31
+
32
+ That distinction is the whole reason this entry says so out loud. At 1.4.6 the
33
+ shipped reference was *wrong* — it omitted a tool the server exposed. Here it was
34
+ merely *old*. The two need different remedies and only one of them is honest in each
35
+ case, so which one applied is recorded rather than left for a reader to guess.
36
+
37
+ Captured read-only: `initialize`, `tools/list`, `resources/list`,
38
+ `resources/templates/list`, `prompts/list`. No tool was invoked, so no data provider
39
+ was contacted, and the session was deleted afterwards. The two substitutions the
40
+ snapshot documents are unchanged and asserted on the way in and out: 19 of 19
41
+ `$schema` URIs dropped, exactly 2 long patterns stored as `patternLength`.
42
+
43
+ ## [1.4.6] - 2026-09-29
44
+
45
+ **The version number is prod's, not this package's own count.** The bridge is released
46
+ at the version the ViaFrei server is actually serving — `mcp.viafrei.de` reports
47
+ `1.4.6` — so that `npx viafrei@X.Y.Z` and the endpoint it relays to can be named by
48
+ one number. That is why this release skips from 1.3.22 to 1.4.6 with no 1.4.0 through
49
+ 1.4.5 on the registry: those platform versions carried no bridge change.
50
+
51
+ The shipped API reference is re-captured from that same server, so the package, the
52
+ endpoint and the documentation all name 1.4.6. It had been left at a 1.3.22 capture,
53
+ which review caught.
54
+
55
+ Nothing in the bridge's own runtime behaviour changed. Every flag, every environment
56
+ variable, every exit code and every message is what 1.3.22 shipped. What changed is the
57
+ machinery around it — two gates the 1.3.22 release wanted and could not have, one because
58
+ the failure it prevents happened *during* that release, and one that had been kept in step
59
+ by hand since 1.3.15 — plus seven refactors and one documentation fix.
60
+
61
+ ### Added
62
+
63
+ - **Every external program these scripts run now has a deadline** (`runTool` in
64
+ `scripts/tools.mjs`, `npm run test:tools`), because on 2026-09-28 one of them stopped
65
+ returning and nothing noticed.
66
+
67
+ A `git` call inside the tarball gate's self-test hung on a GitHub runner. The job had
68
+ no timeout either, so it ran for **1 hour 49 minutes** before being cancelled by hand;
69
+ the publish job hit the same stall and sat for 15 minutes with a tag already pushed.
70
+ Both stopped after the same case and the Node 22 job of the same commit passed, so it
71
+ reproduces rather than being a one-off (#25).
72
+
73
+ **A hang is the one failure mode everything else here is built to prevent.** These
74
+ scripts refuse by name, prove their controls can say no, and treat a check that read
75
+ nothing as a failure — and a hung subprocess defeats all of it at once, because it
76
+ cannot be told apart from work in progress: no exit code, no message, and a log that
77
+ simply stops. There is now no unbounded external program in `scripts/`: every one goes
78
+ through a single wrapper with a 120-second default, roughly 120 times the slowest
79
+ legitimate call here. No count is written down — the bare-name sweep is the instrument,
80
+ and it fails if a call appears that does not go through the wrapper. (This branch
81
+ CONVERTED 22 sites across six files; that is a figure about the change, not an
82
+ inventory of the tree, and an earlier draft of this sentence used it as both.)
83
+
84
+ Only a **timeout** is translated, into a refusal naming the program, its arguments and
85
+ the limit. Every other failure is re-thrown untouched, because each gate decides
86
+ "refused" from `status`, `stdout` and `stderr` on the thrown error, and wrapping those
87
+ would break the thing the gates measure. Both halves are asserted.
88
+
89
+ **Renaming the call sites nearly disabled the gate that watches them.** The bare-name
90
+ sweep looked for `execFileSync(`; routing 22 calls through `runTool(` would have left
91
+ it green over the whole set while covering none of it — the exact silent-hole failure
92
+ that file exists to refuse. It is caught structurally now: the function names live in
93
+ one `SPAWNERS` array that both the sweep and its own precondition are derived from, so
94
+ the two cannot drift again. The only reason this was noticed is that the precondition
95
+ went red on its own when it found zero spawning files.
96
+
97
+ - **A gate on the manifest and lockfile versions** (`scripts/check-versions.mjs`,
98
+ `npm run check:versions`), in **both** workflows (#18).
99
+
100
+ Cutting 1.3.15 left `package.json` at `1.3.15` while `package-lock.json` still said
101
+ `1.3.12`, on a tree seven review rounds had confirmed, and every gate passed. The
102
+ measured reason: **`npm ci` does not compare the root `version` field at all** —
103
+ checked on npm 11.19.1 and on the 12.0.2 the publish workflow pins — so nothing in
104
+ build, test, pack or the tarball gate reads that pair. It has been kept in step by hand
105
+ ever since, and 1.3.16 and 1.3.22 each said so in their commit messages. "Done
106
+ deliberately" is the failure mode, not the remedy.
107
+
108
+ It lives in `ci.yml` as well as `publish.yml` so the drift surfaces on the push that
109
+ introduces it rather than at the tag, when the only remedy is a new version. Its exit
110
+ codes are distinct on purpose — **2** could not run, **1** ran and disagreed — so "I
111
+ could not read the lockfile" can never look like "I read it and was satisfied". Its
112
+ self-test covers the mutant #18 asks for and the one that would otherwise agree about
113
+ nothing — three ABSENT fields are all equal to each other. No case count is written
114
+ here; `npm run test:versions` prints the one to trust.
115
+
116
+ ### Changed
117
+
118
+ - **All seven `javascript:S3776` cognitive-complexity findings are gone** (#19), measured
119
+ on the push rather than claimed: seven CRITICAL findings on `main` before, **zero**
120
+ after, and zero new issues of any rule. `parseOptions` 22, `remote.onmessage` 18,
121
+ `checkManifest` 20, the tarball gate's `main` 31, `tokenCandidates` 18, `decodings` 18
122
+ and `scanFile` 27 are each split by concern, and every extraction MOVED lines rather
123
+ than rewriting them. No behaviour change — that is the point of the entry being here
124
+ and not under Fixed.
125
+
126
+ The refactor exposed five things nothing was testing, each proved by deleting it and
127
+ watching the suite stay green: `-h` had no case at all; `setProtocolVersion` had none,
128
+ so the negotiate-down test asserted that the bridge *complains* about a version and
129
+ never that it *applies* it; half of one error sentence was unreachable because every
130
+ fixture carried a `supported` list; a dependency check's early exit could be deleted
131
+ because every private-scope fixture also happened to be a valid range; and
132
+ `"is this a listed private name?"` was written out by hand **three** times in
133
+ `rules.mjs`, in the file that decides whether a commit may be published. All five are
134
+ closed. The gate self-test went from 107 cases to 110.
135
+
136
+ - **Both workflow jobs carry `timeout-minutes: 15`** (#25). There was no timeout on any
137
+ job, so GitHub's default six hours applied — which is how a hung step ran for nearly
138
+ two. Fifteen minutes is nine times the observed duration of a successful run (~100 s
139
+ for CI, ~95 s for publish), so it cannot fire on a slow-but-working build, and it ends
140
+ a hung one in minutes.
141
+
142
+ - **`publish.yml` refuses a tag whose commit `main` does not contain** (#18). A tag alone
143
+ decides what is published, so a tag pushed from any branch would have published from
144
+ it. The ancestry is answerable because the checkout is already full-depth, and the ref
145
+ is fetched explicitly so an unresolvable `origin/main` is an error rather than a skip.
146
+
147
+ - **The `npm` deployment environment now says what it gates and why it has no protection
148
+ rule** (#18): it is the OIDC subject npm's trusted publisher is configured against, so
149
+ it is part of the credential, and it has no required reviewer deliberately — the
150
+ release is already gated by a verdict covering HEAD and by every gate running against
151
+ the exact tarball uploaded. Recorded so "no rule needed" is distinguishable from
152
+ "nobody looked".
153
+
154
+ ### Fixed
155
+
156
+ - **The shipped API reference described the wrong server.** `API.md` and
157
+ `catalogue.json` both ship, and both still said `viafrei 1.3.22` from a 2026-09-28
158
+ capture — in a package published as 1.4.6, whose whole point is that the package and
159
+ the endpoint are named by one number. Found in review, and it was not merely dated:
160
+ re-capturing from `mcp.viafrei.de` shows the surface genuinely moved. The server
161
+ exposes a nineteenth tool, `find_fuel_station`, that the reference did not mention at
162
+ all, and `find_cheapest_fuel` and `check_transit_disruption` have new descriptions.
163
+
164
+ Re-captured read-only — `initialize`, `tools/list`, `resources/list`,
165
+ `resources/templates/list`, `prompts/list`, no tool invoked, so no data provider was
166
+ contacted — and the session was closed afterwards. The two substitutions the snapshot
167
+ documents are unchanged and were asserted rather than assumed: every tool's `$schema`
168
+ URI is dropped (19 of 19), and the two 288-character date patterns are stored as
169
+ `patternLength`.
170
+
171
+ **No gate could have caught this**, which is the part worth keeping. `check:docs`
172
+ passed throughout: it proves `API.md` matches `catalogue.json`, so a stale pair passes
173
+ together. Nothing in this repository compares the snapshot against the running server,
174
+ and `npm ci`-style version agreement cannot see it either. The reference's currency is
175
+ checked by a person at the cut, and that is now a step rather than a habit.
176
+
177
+ - **`SOURCES.md` printed an attribution string the server had stopped sending** (#332).
178
+ The shipped document is what a reader consults to know whose data they are looking at,
179
+ so a stale attribution line there is wrong in the one place it matters.
180
+
181
+ - **Every push ran CI twice** (#23). `push: branches: ['**']` and `pull_request` both
182
+ fired for a branch with an open pull request, so one push ran the whole matrix twice —
183
+ four `build-and-test` jobs for two Node versions, doubling the wait and the minutes for
184
+ no added signal. `push` is now `main` only; everything else arrives through its pull
185
+ request. The cost is named in the workflow rather than discovered later: a branch pushed
186
+ with **no** pull request now gets no CI. That is the right trade here — the flow is
187
+ push-then-open-immediately, the reviewer reads local commits before the push, and the
188
+ publish workflow re-runs every gate against the tag regardless.
189
+
190
+ - **Four README links resolved on the package page but not inside the tarball** (#23).
191
+ `README.md` ships and linked to `CODE_OF_CONDUCT.md`, `CONTRIBUTING.md`, `SECURITY.md`
192
+ and `SUPPORT.md`, none of which do. Three were pre-existing — verified in the published
193
+ 1.3.15 tarball — and `SUPPORT.md` was added by 1.3.16, so that release made an existing
194
+ condition one worse. npm rewrites relative links in the rendered README to the
195
+ repository, so it only bit someone reading an unpacked tarball. All four are now
196
+ absolute, which is the honest form: a link that means "the repository" says so, and it
197
+ survives any packaging change. The two that remain relative, `API.md` and `SOURCES.md`,
198
+ are files the tarball carries — and that is now **asserted by the tarball gate**, which
199
+ reads the README *inside* the built tarball, extracts every relative Markdown link and
200
+ fails if one is not among the shipped paths. So the fifth such link is caught rather
201
+ than noticed three releases later. An earlier draft of this sentence said "asserted"
202
+ when nothing asserted it, which is the third time an entry in this file has claimed a
203
+ mechanism the tree did not contain; this time the mechanism was written instead of the
204
+ sentence being softened.
205
+
206
+ The rule then arrived with **no case in the gate's own self-test**, and the suite's case
207
+ count stayed where it was, which is how the review found it: a count that does not move
208
+ when a rule is added is the suite saying so. It now has two cases, because the rule's
209
+ first draft could not see an ANCHORED link — `](CONTRIBUTING.md#merging)` to a file the
210
+ tarball does not carry passed silently, and that is the likeliest fifth link there is.
211
+ Each was proved red on its own: neutering the rule fails both, and restoring the
212
+ anchor-blind capture fails only the anchored one.
213
+
214
+ - **Two comments carried counts that read as inventories** (#23). `scripts/tools.mjs` said
215
+ "TWENTY sites were changed in all" where twenty was what one commit changed, not what
216
+ the tree holds; it now says so. `scripts/tools.test.mjs` said a third caller of the
217
+ shared precondition arrived "within the week" when it arrived the **same day**, which is
218
+ the harder version of its own point.
219
+
220
+ ### Deliberately not done
221
+
222
+ - **`javascript:S2187` on every self-test** (#23, item 1) — *cannot be done from the
223
+ repository.* SonarCloud reads `scripts/*.test.mjs` as test files, finds no framework
224
+ assertions, and reports "add some tests to this file or delete it" at BLOCKER on each.
225
+ There are **five** as of this branch, because the version gate above brings its own — so
226
+ this work adds a file to the class it is declaring unfixable, which is the part worth
227
+ knowing before the next self-test is written.
228
+ Measured: analysis here is **Automatic** (no scanner step in any workflow) and
229
+ `api/settings/values` returns no `sonar.tests` or `sonar.test.inclusions`, so the test
230
+ patterns live in SonarCloud's own UI and changing them needs a token this repository
231
+ does not hold. A `sonar-project.properties` was **not** added, because Automatic
232
+ Analysis may ignore it and a config file that silently does nothing is worse than the
233
+ finding. The two real options are a UI change to the test patterns, or renaming the
234
+ convention to `*.selftest.mjs` — which touches `package.json` scripts, CI steps and the
235
+ sweep that counts them. The quality gate passes on all five conditions either way.
236
+
237
+ - **The four remaining bare `.sort()` calls** — left, and named so the next person is not
238
+ ambushed. SonarCloud raised `javascript:S2871` (CRITICAL, type BUG) on the `.sort()` in
239
+ `relativeMarkdownLinks` and took the new-code reliability rating to D against an A
240
+ threshold, which is a required check. The line's behaviour was never wrong — every
241
+ element is a string and code-unit order is what is wanted — but the round-3 refactor
242
+ moved the expression into a new exported function, so a pattern older than this branch
243
+ became *new code* and failed a gate it had never been measured by. That is the trap, and
244
+ it is still loaded four times over:
245
+
246
+ git grep -n '\.sort()' -- '*.mjs' '*.ts' | grep -v '^\S*:[0-9]*: \*'
247
+
248
+ finds them in `scripts/check-tarball.mjs`, twice in `scripts/tools.test.mjs`, and in
249
+ `test/relay.test.ts`. All four are string arrays and all four are correct today; none is
250
+ in this PR's new-code period, so none fails the gate now. **Converting them here would
251
+ add three files to a diff that is already fifteen**, so they are recorded instead: the
252
+ next PR that so much as moves one of those lines should expect a CRITICAL BUG on code it
253
+ only touched, and should fix it in that PR rather than discovering it from a red required
254
+ check after the push, which is how this one was found.
255
+
256
+ Not `localeCompare`, whichever PR does it. It is what the rule suggests and it is wrong
257
+ here twice: it reorders (`B a` becomes `a B`) and it is locale-dependent (`ä` sorts before
258
+ `z` under `en`/`de` and after it under `sv`), so a gate's output would depend on the
259
+ runner. The comparator added here says only what the default already did, and the order
260
+ is pinned by a case in the self-test.
261
+
262
+ - **A shared assertion harness for the self-tests** (#23, item 5) — declined, on the
263
+ issue's own condition. Each self-test defines its own `check()`/`refuse()` pair, and the
264
+ issue says to unify them only if it can be done without weakening the per-file refusal
265
+ wording. It cannot, cheaply: the refusals deliberately name different builders and exit
266
+ by different routes, and that difference is load-bearing — it is what tells a reader
267
+ which fixture failed. The duplication is seven small functions across five files — four
268
+ define `check()`, three define `refuse()` — not the eleven-line block that caused the
269
+ 3.1% duplication failure `fixture-root.mjs` was extracted to fix.
270
+
271
+ ### Note on a count in the 1.3.16 entry below
272
+
273
+ That entry says the tool sweep reads "thirty-two source files". It now reads **34**,
274
+ because this work adds two. The published entry is **left alone**: it was measured at
275
+ that release and editing a shipped block to keep a number current is how a changelog
276
+ stops being a record. The figure to trust is the one `npm run test:tools` prints, which
277
+ is why nothing here states a total either.
278
+
10
279
  ## [1.3.22] - 2026-09-28
11
280
 
12
281
  **A fresh snapshot of the production server, and the version number that goes with it.
package/README.md CHANGED
@@ -161,7 +161,7 @@ Restart the client and ask it one of the questions above.
161
161
  | **[API.md](API.md)** | Every tool with its parameters, types, defaults and constraints, plus the resources, resource templates and prompts. Generated from a dated snapshot of the running server, so the descriptions are the server's own words — which is what your assistant actually reads when it picks a tool. |
162
162
  | **[Wiki](https://github.com/mavrovde/viafrei-bridge/wiki)** | The prose half: [connecting your assistant](https://github.com/mavrovde/viafrei-bridge/wiki/Connecting-your-assistant), [tools at a glance](https://github.com/mavrovde/viafrei-bridge/wiki/Tools-at-a-glance), the [roadmap](https://github.com/mavrovde/viafrei-bridge/wiki/Roadmap) and an [FAQ](https://github.com/mavrovde/viafrei-bridge/wiki/FAQ). |
163
163
  | **[SOURCES.md](SOURCES.md)** | Every publisher, what it covers, its licence, and the attribution line to reproduce — including the conditions that are licence breaches rather than style problems. |
164
- | **[SUPPORT.md](SUPPORT.md)** | Where a question, a bad answer or a security report should go, and what makes a report easy to act on. |
164
+ | **[SUPPORT.md](https://github.com/mavrovde/viafrei-bridge/blob/main/SUPPORT.md)** | Where a question, a bad answer or a security report should go, and what makes a report easy to act on. |
165
165
 
166
166
  ### Worked use cases
167
167
 
@@ -339,8 +339,8 @@ This is said plainly so nobody spends an evening looking for the server code.
339
339
 
340
340
  ## Contributing
341
341
 
342
- Yes, please — see [CONTRIBUTING.md](CONTRIBUTING.md) and
343
- [CODE_OF_CONDUCT.md](CODE_OF_CONDUCT.md). Issues and discussions are open. The
342
+ Yes, please — see [CONTRIBUTING.md](https://github.com/mavrovde/viafrei-bridge/blob/main/CONTRIBUTING.md) and
343
+ [CODE_OF_CONDUCT.md](https://github.com/mavrovde/viafrei-bridge/blob/main/CODE_OF_CONDUCT.md). Issues and discussions are open. The
344
344
  bridge is small and self-contained, which is exactly what makes it a reasonable
345
345
  thing to send a first patch to.
346
346
 
@@ -352,7 +352,7 @@ cannot get any other way.
352
352
  ## Security
353
353
 
354
354
  Never open a public issue for a key, a token or anything that looks like one.
355
- See [SECURITY.md](SECURITY.md) for the private reporting path.
355
+ See [SECURITY.md](https://github.com/mavrovde/viafrei-bridge/blob/main/SECURITY.md) for the private reporting path.
356
356
 
357
357
  ## Licence
358
358
 
package/SOURCES.md CHANGED
@@ -310,13 +310,13 @@ sourceNote : null
310
310
  ```
311
311
 
312
312
  So GovData's harvest is faithful and there is no versioned statement anywhere in
313
- the chain. The attribution string the server emits today still names 4.0; that is
314
- a defect at the source, tracked there, and it will be corrected in the server
315
- rather than by rewording this page — what this page documents is what the server
316
- actually sends.
313
+ the chain, and the attribution string the server emits says exactly that: the
314
+ licence is CC BY-SA and the version is unstated. It named 4.0 until v1.4.1, which
315
+ corrected it at the source rather than by rewording this page — what this page
316
+ documents is what the server actually sends.
317
317
 
318
318
  ```
319
- Echtzeitdaten: DELFI e.V. via Mobilithek, CC BY-SA 4.0
319
+ Echtzeitdaten: DELFI e.V. via Mobilithek, CC BY-SA
320
320
  ```
321
321
 
322
322
  Share-alike. See [the obligation above](#public-transport-realtime-is-share-alike):
package/dist/bridge.d.ts CHANGED
@@ -8,16 +8,4 @@ export interface BridgeHooks {
8
8
  export interface BridgeHandle {
9
9
  close: () => Promise<void>;
10
10
  }
11
- /**
12
- * Relay every JSON-RPC message between a local stdio client and a remote
13
- * Streamable-HTTP MCP endpoint, in both directions, unchanged.
14
- *
15
- * The bridge is deliberately a *message* relay and not a client/server pair
16
- * that re-implements the protocol: it never has an opinion about a method it
17
- * has not heard of, so a tool added on the server works here the same day
18
- * without a release. The only messages it looks inside are `initialize` and its
19
- * answer - because the session's protocol version has to end up on the HTTP
20
- * headers, and because a version the server cannot speak deserves a sentence
21
- * rather than a stack trace.
22
- */
23
11
  export declare function startBridge(options: Options, hooks: BridgeHooks): Promise<BridgeHandle>;
package/dist/bridge.js CHANGED
@@ -52,17 +52,44 @@ function readSupportedVersions(data) {
52
52
  return [...new Set(versions)];
53
53
  }
54
54
  /**
55
- * Relay every JSON-RPC message between a local stdio client and a remote
56
- * Streamable-HTTP MCP endpoint, in both directions, unchanged.
55
+ * Reconcile the server's answer to `initialize`, and return the line that must
56
+ * end the session — AFTER the answer has been relayed, never instead of it, which
57
+ * is why this returns the line rather than exiting.
57
58
  *
58
- * The bridge is deliberately a *message* relay and not a client/server pair
59
- * that re-implements the protocol: it never has an opinion about a method it
60
- * has not heard of, so a tool added on the server works here the same day
61
- * without a release. The only messages it looks inside are `initialize` and its
62
- * answer - because the session's protocol version has to end up on the HTTP
63
- * headers, and because a version the server cannot speak deserves a sentence
64
- * rather than a stack trace.
59
+ * Extracted from `remote.onmessage`, which was cognitive complexity 18 (#19) with
60
+ * this nested four deep inside the relay path. Nothing here decides anything new:
61
+ * a usable version is applied through `onNegotiated`, a mismatch warns and relays
62
+ * unchanged, a rejection returns the fatal line, and any other shape is relayed
63
+ * with no comment.
64
+ *
65
+ * Two of the three behaviours this moved were NOT covered when the move was made,
66
+ * measured by deleting them: `setProtocolVersion` and the "did not say which
67
+ * versions" half of the sentence both survived with the whole suite green. Both
68
+ * now have a case. The third — `initialized = true` — is still ungated: it is
69
+ * observable only through a LATER failure's exit code, and it sits inside the
70
+ * `onNegotiated` callback whose invocation the new header case does gate. So a
71
+ * callback that stops firing is caught; a callback that fires and drops that one
72
+ * assignment is not. Said here rather than left for someone to assume otherwise.
65
73
  */
74
+ function reconcileInitialize(message, session) {
75
+ if (isJSONRPCResultResponse(message)) {
76
+ const served = readProtocolVersion(message.result);
77
+ if (served === undefined) {
78
+ return undefined;
79
+ }
80
+ session.onNegotiated(served);
81
+ if (session.requested !== undefined && served !== session.requested) {
82
+ session.warn(`viafrei: ${session.url} speaks MCP protocol ${served}, this client asked for ${session.requested}; relaying the server's answer unchanged`);
83
+ }
84
+ return undefined;
85
+ }
86
+ if (isJSONRPCErrorResponse(message)) {
87
+ const supported = readSupportedVersions(message.error.data);
88
+ const spoken = supported.length > 0 ? `the server speaks ${supported.join(', ')}` : `the server did not say which versions it speaks`;
89
+ return `viafrei: ${session.url} rejected MCP protocol version ${session.requested ?? '(unspecified)'}; ${spoken}`;
90
+ }
91
+ return undefined;
92
+ }
66
93
  export async function startBridge(options, hooks) {
67
94
  const remote = new StreamableHTTPClientTransport(new URL(options.url), {
68
95
  fetch: createFetch(options.timeoutMs),
@@ -153,21 +180,15 @@ export async function startBridge(options, hooks) {
153
180
  let fatalAfterRelay;
154
181
  if (pending !== undefined && id !== undefined) {
155
182
  pendingInitialize.delete(id);
156
- if (isJSONRPCResultResponse(message)) {
157
- const served = readProtocolVersion(message.result);
158
- if (served !== undefined) {
183
+ fatalAfterRelay = reconcileInitialize(message, {
184
+ requested: pending.requested,
185
+ url: options.url,
186
+ onNegotiated: served => {
159
187
  remote.setProtocolVersion?.(served);
160
188
  initialized = true;
161
- if (pending.requested !== undefined && served !== pending.requested) {
162
- hooks.warn(`viafrei: ${options.url} speaks MCP protocol ${served}, this client asked for ${pending.requested}; relaying the server's answer unchanged`);
163
- }
164
- }
165
- }
166
- else if (isJSONRPCErrorResponse(message)) {
167
- const supported = readSupportedVersions(message.error.data);
168
- const spoken = supported.length > 0 ? `the server speaks ${supported.join(', ')}` : `the server did not say which versions it speaks`;
169
- fatalAfterRelay = `viafrei: ${options.url} rejected MCP protocol version ${pending.requested ?? '(unspecified)'}; ${spoken}`;
170
- }
189
+ },
190
+ warn: hooks.warn
191
+ });
171
192
  }
172
193
  void local.send(message).then(() => {
173
194
  if (fatalAfterRelay !== undefined) {
package/dist/config.d.ts CHANGED
@@ -59,11 +59,5 @@ export interface Options {
59
59
  }
60
60
  export declare class UsageError extends Error {
61
61
  }
62
- /**
63
- * Parse argv (without `node` and the script) plus the environment.
64
- *
65
- * Precedence is the one people expect: a flag beats an environment variable,
66
- * an environment variable beats the built-in default.
67
- */
68
62
  export declare function parseOptions(argv: readonly string[], env?: NodeJS.ProcessEnv): Options;
69
63
  export declare function helpText(version: string): string;
package/dist/config.js CHANGED
@@ -103,6 +103,95 @@ function validateUrl(raw, source) {
103
103
  * Precedence is the one people expect: a flag beats an environment variable,
104
104
  * an environment variable beats the built-in default.
105
105
  */
106
+ /**
107
+ * Flags that take no value, and which boolean each one sets.
108
+ *
109
+ * A table rather than a chain of `===` comparisons, so an alias is a KEY: adding
110
+ * `-?` is one line and cannot be added to one spelling and forgotten in another.
111
+ * The risk a table carries is the opposite one - a key silently absent - so the
112
+ * aliases are pinned in `config.test.ts` as equivalences to their long forms
113
+ * rather than one case each. `-h` had no case at all before this table existed,
114
+ * and deleting it from the old chain left the whole suite green.
115
+ */
116
+ const BOOLEAN_FLAGS = new Map([
117
+ ['--help', 'showHelp'],
118
+ ['-h', 'showHelp'],
119
+ ['--version', 'showVersion'],
120
+ ['-V', 'showVersion']
121
+ ]);
122
+ const VALUE_FLAGS = [
123
+ {
124
+ names: ['--url'],
125
+ apply: (options, value, source) => {
126
+ options.url = validateUrl(value, source);
127
+ }
128
+ },
129
+ {
130
+ names: ['--header', '-H'],
131
+ apply: (options, value, source) => {
132
+ const [name, headerValue] = parseHeader(value, source);
133
+ options.headers[name] = headerValue;
134
+ }
135
+ },
136
+ {
137
+ names: ['--timeout'],
138
+ apply: (options, value, source) => {
139
+ options.timeoutMs = parseTimeout(value, source);
140
+ }
141
+ }
142
+ ];
143
+ /**
144
+ * The environment, applied before argv so that a flag beats a variable by
145
+ * POSITION rather than by a rule — and so a `--header` of the same name
146
+ * overwrites this one while a `--header` of a different name joins it.
147
+ */
148
+ function applyEnvironment(options, env) {
149
+ const url = env[URL_ENV_VAR]?.trim();
150
+ if (url !== undefined && url !== '') {
151
+ options.url = validateUrl(url, URL_ENV_VAR);
152
+ }
153
+ const timeout = env[TIMEOUT_ENV_VAR]?.trim();
154
+ if (timeout !== undefined && timeout !== '') {
155
+ options.timeoutMs = parseTimeout(timeout, TIMEOUT_ENV_VAR);
156
+ }
157
+ const headers = env[HEADER_ENV_VAR];
158
+ if (headers === undefined || headers.trim() === '') {
159
+ return;
160
+ }
161
+ for (const line of headers.split(HEADER_ENV_SEPARATOR)) {
162
+ // A blank line is skipped rather than refused, so a trailing newline in
163
+ // the variable is not an error.
164
+ if (line.trim() !== '') {
165
+ const [name, value] = parseHeader(line, HEADER_ENV_VAR);
166
+ options.headers[name] = value;
167
+ }
168
+ }
169
+ }
170
+ /**
171
+ * One argument. `readValue` consumes the NEXT argv entry and is called only for a
172
+ * flag that needs it, so `--url` at the end of argv still refuses by the same
173
+ * route it always did.
174
+ */
175
+ function applyArgument(options, argument, readValue) {
176
+ const booleanTarget = BOOLEAN_FLAGS.get(argument);
177
+ if (booleanTarget !== undefined) {
178
+ options[booleanTarget] = true;
179
+ return;
180
+ }
181
+ for (const flag of VALUE_FLAGS) {
182
+ const [canonical] = flag.names;
183
+ if (flag.names.includes(argument)) {
184
+ flag.apply(options, readValue(), canonical);
185
+ return;
186
+ }
187
+ const inline = `${canonical}=`;
188
+ if (argument.startsWith(inline)) {
189
+ flag.apply(options, argument.slice(inline.length), canonical);
190
+ return;
191
+ }
192
+ }
193
+ throw new UsageError(`unknown argument ${JSON.stringify(argument)} - run "viafrei --help" for the list`);
194
+ }
106
195
  export function parseOptions(argv, env = process.env) {
107
196
  const options = {
108
197
  url: DEFAULT_MCP_URL,
@@ -111,66 +200,17 @@ export function parseOptions(argv, env = process.env) {
111
200
  showVersion: false,
112
201
  showHelp: false
113
202
  };
114
- const urlFromEnv = env[URL_ENV_VAR];
115
- if (urlFromEnv !== undefined && urlFromEnv.trim() !== '') {
116
- options.url = validateUrl(urlFromEnv.trim(), URL_ENV_VAR);
117
- }
118
- const timeoutFromEnv = env[TIMEOUT_ENV_VAR];
119
- if (timeoutFromEnv !== undefined && timeoutFromEnv.trim() !== '') {
120
- options.timeoutMs = parseTimeout(timeoutFromEnv.trim(), TIMEOUT_ENV_VAR);
121
- }
122
- // Before the argv loop, so a --header of the same name overwrites this one
123
- // and a --header of a different name joins it — the same precedence --url
124
- // has over VIAFREI_MCP_URL, established by position rather than by a rule.
125
- const headersFromEnv = env[HEADER_ENV_VAR];
126
- if (headersFromEnv !== undefined && headersFromEnv.trim() !== '') {
127
- for (const line of headersFromEnv.split(HEADER_ENV_SEPARATOR)) {
128
- if (line.trim() === '') {
129
- continue;
130
- }
131
- const [name, value] = parseHeader(line, HEADER_ENV_VAR);
132
- options.headers[name] = value;
133
- }
134
- }
203
+ applyEnvironment(options, env);
135
204
  for (let index = 0; index < argv.length; index += 1) {
136
205
  const argument = argv[index];
137
- const next = () => {
206
+ applyArgument(options, argument, () => {
138
207
  const value = argv[index + 1];
139
208
  if (value === undefined || value.startsWith('--')) {
140
209
  throw new UsageError(`${argument} expects a value`);
141
210
  }
142
211
  index += 1;
143
212
  return value;
144
- };
145
- if (argument === '--help' || argument === '-h') {
146
- options.showHelp = true;
147
- }
148
- else if (argument === '--version' || argument === '-V') {
149
- options.showVersion = true;
150
- }
151
- else if (argument === '--url') {
152
- options.url = validateUrl(next(), '--url');
153
- }
154
- else if (argument.startsWith('--url=')) {
155
- options.url = validateUrl(argument.slice('--url='.length), '--url');
156
- }
157
- else if (argument === '--header' || argument === '-H') {
158
- const [name, value] = parseHeader(next());
159
- options.headers[name] = value;
160
- }
161
- else if (argument.startsWith('--header=')) {
162
- const [name, value] = parseHeader(argument.slice('--header='.length));
163
- options.headers[name] = value;
164
- }
165
- else if (argument === '--timeout') {
166
- options.timeoutMs = parseTimeout(next(), '--timeout');
167
- }
168
- else if (argument.startsWith('--timeout=')) {
169
- options.timeoutMs = parseTimeout(argument.slice('--timeout='.length), '--timeout');
170
- }
171
- else {
172
- throw new UsageError(`unknown argument ${JSON.stringify(argument)} - run "viafrei --help" for the list`);
173
- }
213
+ });
174
214
  }
175
215
  return options;
176
216
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "viafrei",
3
- "version": "1.3.22",
3
+ "version": "1.4.8",
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",
@@ -69,8 +69,10 @@
69
69
  "test:leaks": "node scripts/check-leaks.test.mjs",
70
70
  "test:tools": "node scripts/tools.test.mjs",
71
71
  "test:docs": "node scripts/gen-api-doc.test.mjs",
72
+ "test:versions": "node scripts/check-versions.test.mjs",
72
73
  "check:tarball": "node scripts/check-tarball.mjs",
73
74
  "check:leaks": "node scripts/check-leaks.mjs",
75
+ "check:versions": "node scripts/check-versions.mjs",
74
76
  "docs:api": "node scripts/gen-api-doc.mjs",
75
77
  "check:docs": "node scripts/gen-api-doc.mjs --check",
76
78
  "rules:show": "node scripts/show-rules.mjs"