Contract
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.
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.
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.
These cannot break a correctly-written client, so they ship whenever they are ready:
verification_state,
routing_basis, status. Treat these as strings with known values, not as
closed sets.next_action text, limitations wording.These break a working client, so they do not happen inside v1:
operationId, which is the name of a method in every generated
client library.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.
/api/public/v2 is mounted alongside v1. The MCP surface registers the new
tools alongside the old ones under distinct names.v1 responses carry a Deprecation and a Sunset header (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.v1 is withdrawn no earlier than the Sunset date.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.
Stated plainly, because an unstated non-promise reads as a promise:
request_id is for support correlation. It is not a stable key and is not
addressable.Retry-After.fleet_id as 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.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.
backend/app/api/public_v1/openapi.yaml, generated from the routes
by backend/scripts/generate_public_openapi.py and diffed against them by
backend/tests/test_public_openapi_contract.py. If the document and the code
disagree, CI fails.swissmcp/tests/test_tool_allowlist.py.backend/app/services/public_commerce/models.py. Both
adapters render these; neither may publish a commercial field the other
cannot see.