Contract
Versioning
An agent is not a person who reads changelogs. It is a program written once and left running — so this is about what can change underneath it.
The short version: we may add optional request fields, response fields and new endpoints at any time, so ignore fields you do not recognise. Removing or renaming anything you can already see requires a new major version and a parallel-run period.
Public API and MCP — versioning and compatibility contract
Status: frozen for V1 by S-05 (Task 5 §U). Applies to: /api/public/v1
and the Swiss Touristy MCP tool set (package directory swissmcp/).
Owner: Swiss Touristy engineering.
An agent integrating with Swiss Touristy is not a person who will read a changelog. It is a program that was written once, deployed, and left running. So the question §U asks is narrow and practical: what can we change without breaking it, and how will it find out about the rest?
This document is the answer, and it is a commitment rather than an aspiration.
1. What the version number covers
The version is in the path: /api/public/v1. The MCP surface has no path, so
its version is the tool set registered in swissmcp/src/swissmcp/server.py,
pinned by PUBLIC_MCP_TOOL_NAMES.
Both are governed by the same semantic contract, because both are adapters over
app/services/public_commerce/ (Task 5 §B). A change to the shared layer
changes both surfaces at once, and that is deliberate — it is what makes "REST
and MCP agree" a structural property rather than a maintenance chore. It also
means every rule below applies to both, even when only one is mentioned.
2. What we may change without a new version
These cannot break a correctly-written client, so they ship whenever they are ready:
- Adding an optional request field. A client that omits it behaves exactly as before.
- Adding a response field. Clients must ignore fields they do not recognise; that expectation is stated in the OpenAPI description and in the tool descriptions.
- Adding a new endpoint or tool — subject to the §A capability boundary and the Founder gate on expanding the public surface, which are separate questions from versioning.
- Adding a new value to an open enumeration —
verification_state,routing_basis,status. Treat these as strings with known values, not as closed sets. - Loosening a validation rule (raising a maximum, accepting a format we previously rejected).
- Changing prose: descriptions,
next_actiontext,limitationswording. - Changing a price. Prices are commercial facts, not contract; they move when Swiss Touristy's tariffs move, and a quote's 30-minute window is the guarantee that matters.
3. What requires a new major version
These break a working client, so they do not happen inside v1:
- Removing or renaming an endpoint, a tool, a request field or a response field.
- Making an optional request field required.
- Changing a field's type, or the meaning of an existing value.
- Removing an error code from the §N catalogue, or changing which code a given situation produces.
- Changing an
operationId, which is the name of a method in every generated client library. - Narrowing validation in a way that rejects a previously accepted request.
- Changing the ownership, authentication or scope model.
The error catalogue is part of the contract, not an implementation detail.
An agent branches on QUOTE_EXPIRED the way it branches on a status code. New
codes may be added — a client should treat an unknown code as a generic failure
and fall back to retryable — but an existing code's meaning is fixed.
4. How a breaking change would be delivered
/api/public/v2is mounted alongsidev1. The MCP surface registers the new tools alongside the old ones under distinct names.- Both run in parallel for at least 90 days.
v1responses carry aDeprecationand aSunsetheader (RFC 8594) for the whole overlap period, and the deprecation is stated in the OpenAPI description and in every affected tool description — because an agent reads the tool description on every connection and never reads an email.v1is withdrawn no earlier than theSunsetdate.
We do not silently change v1 behaviour and call it a fix. If a v1 response
is wrong in a way that matters commercially, the correction ships in v1 with
an entry in §6 below, and the reasoning is recorded — a wrong price is worse
than a broken contract, but both are worse than pretending neither happened.
5. What is explicitly not promised
Stated plainly, because an unstated non-promise reads as a promise:
- Quote ids are opaque. Format, length and generation scheme may change. Do not parse them, order them, or infer anything from them.
request_idis for support correlation. It is not a stable key and is not addressable.- The hosted checkout URL is opaque. Its path, its query parameters and its continuation signature may all change. Deliver the traveler to it; do not construct, edit or reconstruct one.
- Rate limits are values, not guarantees. The published numbers are current;
they may be tightened in response to abuse without a version change. Always
honour
Retry-After. - Option ordering is not stable, and neither is
fleet_idas a long-lived identifier — it is meaningful within a quote, which is why the checkout handoff requires it to be one the quote actually priced.
6. Change log
Entries are added when a change ships, not when it is planned.
| Date | Version | Change | Breaking |
|---|---|---|---|
| 2026-08-19 | v1 | S-05: OpenAPI and MCP converged onto one shared service layer; authoritative quotes routed through the canonical snapshot writer; pickup_datetime and luggage added; caller-supplied metrics removed from the authoritative quote; §N typed error catalogue; tiered rate limits; shared idempotency; hosted checkout handoff. | Yes, on the MCP surface only — see below. |
| 2026-08-19 | v1 | adults, children and infants added to both quote operations and echoed on the quote object (§E). Optional; omitting them preserves the previous behaviour exactly (adults = passengers). Not breaking under §2 — an optional additive request field and an added response field. Worth reading anyway: the breakdown is stored on the snapshot and a booking is checked against it, so a party containing children must state it or the quote cannot be redeemed. | No |
| 2026-08-19 | v1 | waypoints[] (with stop_duration_min) and return_datetime added to the quote operations; RouteOut gained normalized_origin, normalized_destination, waypoints, routing_basis and live_context; GET /checkout-sessions/{quote_id} added to resolve a continuation link. DTO validation errors now render in the §N catalogue instead of FastAPI's default shape. All additive. | No |
| 2026-08-19 | v1 | External service name fixed (Founder decision): the MCP server identifies itself as Swiss Touristy MCP, was "Swiss Touristy Public MCP". This is the name in the initialize handshake and therefore the label a user sees in their client's connector list. The approved public origin is mcp.swisstouristy.com and the machine-safe identifier is swiss-touristy-mcp. No tool, schema, scope, field or error code changed. | No — a display name, not a contract field. Cosmetic for the protocol, visible to humans. |
| 2026-08-19 | v1 | GET /checkout-sessions/{quote_id} is now gated by PUBLIC_CHECKOUT_ENABLED on both surfaces, matching the POST that issues the links. With checkout disabled it returns UPSTREAM_UNAVAILABLE rather than resolving. Not a contract change while the surface is unexposed; recorded because it changes when the operation answers. | No |
The S-05 MCP change is breaking and is being taken before any external agent
holds an integration. create_transfer_quote lost distance_km and
duration_min and gained a required pickup_datetime. Under the rules above
that is a v2 change. It is being made in place because the public MCP has not
been exposed on a public origin, has no external consumers, and — more to the
point — the tool it replaces issued quote ids that could never become bookings.
Preserving compatibility with a broken contract would have meant versioning a
defect. The moment the surface is exposed, §4 governs.
7. Where the contract actually lives
- REST:
backend/app/api/public_v1/openapi.yaml, generated from the routes bybackend/scripts/generate_public_openapi.pyand diffed against them bybackend/tests/test_public_openapi_contract.py. If the document and the code disagree, CI fails. - MCP: the registered tools and their descriptions, pinned by
swissmcp/tests/test_tool_allowlist.py. - Shared semantics:
backend/app/services/public_commerce/models.py. Both adapters render these; neither may publish a commercial field the other cannot see.