Handle a fingerprint mismatch
One day your app will connect to a host that is newer than the SDK you built with. This page shows you how to notice that at the handshake — and what the right response is, which differs by SDK, deliberately.
The one idea to take away: the two SDKs surface the same comparison with
two postures. The TypeScript SDK treats a schema fingerprint mismatch as
advisory — a warning value in your logs, never an error — because the
protocol evolves by adding optional things and an older SDK keeps working
against a newer host. The Python SDK is strict: its wheel does not
bundle a host, you pair an installed muse with an installed SDK yourself,
and the strict gate is what makes a wrong pairing loud at connect time —
initialize fails with a typed error naming the required host version.
Neither posture is yours to re-decide: in TypeScript do not turn the warning
into an error, and in Python do not swallow the error and carry on.
Where the fingerprint comes from
Section titled “Where the fingerprint comes from”Every connection opens the same way — your client sends initialize:
{"dir":"client","raw":"{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"initialize\",\"params\":{\"clientInfo\":{\"name\":\"conformance\",\"version\":\"0.0.0\"}}}"}The host’s reply describes itself, and one member of that reply is the
schema object: its version is the served protocol-schema version and its
fingerprint is the value this whole page turns on.
This page shows you no copy of that reply — a copy is exactly the kind of
content that drifts. The tested recipe reads the real thing instead: it takes
schema and schema.fingerprint from a live release-built host’s
initialize result, by those member names, so if either member ever moves or
is renamed, the recipe jobs go red — this page cannot quietly disagree with
the wire.
The SDK pins the fingerprint it was written against as
EXPECTED_SCHEMA_FINGERPRINT and compares the served value against that pin
during initialize.
The code
Section titled “The code”After the handshake, check one field:
const msp = await handshake.initialize({ clientInfo: { name: "my-app", version: "1.0.0" },});
if (msp.fingerprintWarning !== undefined) { // The host serves a schema this SDK was not built against. Surface it // where a developer will see it — then carry on. Everything your SDK // version knows how to do still works. console.warn(msp.fingerprintWarning.message);}That is the whole recipe. The warning value tells you both sides of the
comparison (pinned is what your SDK expected, served is what the host
advertised) and carries a message written for a human reading logs.
If you want the same comparison outside a live connection — say, in a diagnostic screen — call it directly:
import { EXPECTED_SCHEMA_FINGERPRINT, checkServedFingerprint } from "@muse-code/sdk";
const warning = checkServedFingerprint(servedFingerprint);// undefined on a match; a descriptive warning value on a mismatch.// It never throws.In Python the comparison is the gate itself: a mismatch fails initialize
with the typed MuseHostMismatchError, so the code you write is an error
path, not a field check:
from muse_code.errors import MuseHostMismatchError
try: msp = await handshake.initialize( {"clientInfo": {"name": "my-app", "version": "1.0.0"}} )except MuseHostMismatchError as mismatch: # The pair is wrong: this SDK requires `mismatch.pinned`, the host # serves `mismatch.served`. The message names the host version this # SDK requires and the compatibility page's Python row — surface it # and update either side to a matching pair. print(mismatch, file=sys.stderr) raiseThe error carries both sides of the comparison (pinned, served), the
host’s self-reported version (host_version), and the version this SDK
requires (required_host_version). There is no bypass knob, deliberately:
a tolerance parameter would let a caller switch the gate off by passing the
served value. The spawned host is already shut down when the error reaches
you — nothing is orphaned.
What not to do
Section titled “What not to do”Do not refuse to run when the fingerprints differ. The three-line mistake looks responsible and is not:
// WRONG: this turns every host upgrade into an outage for your users.if (msp.fingerprintWarning !== undefined) { throw new Error("schema mismatch");}A mismatch means the host moved ahead of your SDK, which is the normal state of the world between your releases. Log it, surface it in diagnostics, and plan an SDK update — while your app keeps working.
Do not catch the error and carry on as if the pair matched:
# WRONG: the SDK's shapes no longer describe this host's wire; anything# past this point is undefined behaviour wearing a green checkmark.try: msp = await handshake.initialize(params)except MuseHostMismatchError: msp = None # "it's probably fine"The strict gate exists because the wheel and the host are installed
separately: a mismatch means the PAIRING is wrong, and the fix is to update
one side — pip install the matching SDK or install the named host version
— not to proceed against a schema this SDK was not built for.
Run it yourself
Section titled “Run it yourself”You need Node 20 or newer, an installed muse binary, and the SDK your own
application depends on — npm install @muse-code/sdk. The recipe programs live
in the SDK repository:
git clone https://github.com/meta-models/muse-code-sdkcd muse-code-sdknpm ci && npm run build --workspace @muse-code/sdkMUSE_BIN=$(command -v muse) \ npm run recipes --workspace @muse-code/sdk-cookbook -- --only fingerprint-mismatchNot yet runnable from the public clone: the Python twins are not
mirrored to the public SDK repository yet, so this command needs a source
tree that carries clients/sdk-cookbook-py/. The twins run in CI on every
change to this area either way.
You need Python 3.10 or newer, an installed muse binary, and the two SDK
packages importable. The Python twin keeps this recipe’s segment ids and
teaches the strict posture; its mismatch arm drives the same comparison the
SDK runs at handshake against the fingerprint a newer host would serve, and
asserts the exact typed failure:
PYTHONPATH=clients/sdk-cookbook-py/src \MUSE_BIN=$(command -v muse) \ python3 -m cookbook_recipes --only fingerprint-mismatchWhere the tested code lives
Section titled “Where the tested code lives”The runnable TypeScript source for this page is published in full, comments and all, as the fingerprint-mismatch example. It is the source that runs, not a retelling of it: it is executed end to end on every change to this area, and every docs build checks the wire exchange above against the committed transcript, so this page cannot drift from what actually runs. The Python twin (same recipe id, same segments, the strict fork) runs in its own CI lane against the same release-built host.
The harness spawns its hosts with MUSE_EXPERIMENTAL_SDK_ENABLED set
explicitly. muse serve now runs by default; the variable survives as an
off-switch, and the harness pins the run open so an operator’s off in the
environment cannot silently change what this page proves. That is not advice
for your application — build against muse serve as a supported command.