Two things: the server moves to the current protocol revision without asking a
single client to move with it, and it can run entirely against your own
OpenAI-compatible endpoint.
Added
-
Workflow operations:
workflow_deployments_list,workflow_runs_list,workflow_stop. The first live run against a real account exposed the gap:hello-worldwas listed bymistral://workflows,workflow_executeanswered404 No active deployment found, and nothing in the tool surface let an agent see that coming. A workflow returned bygetWorkflowsis a definition; running it needs a deployment with at least one live worker, which is a separate object with separate state.workflow_deployments_listreports it —is_active,worker_count,active_worker_count,is_hardened, managed vs self-hosted — and computesrunnable_count, the one field an agent has to read before it executes anything; when it is zero the summary says so in words rather than returning an empty list.workflow_runs_listfinds executions (filterable by workflow, status or deployment) so an agent can obtain anexecution_idwithout having started the run itself, andworkflow_stopends one:cancelby default, which lets the workflow run its cleanup handlers, andterminateonly on request, which does not — hencedestructiveHint: true. Together they close the operations loop the three original tools left open: what can run, what is running, and how to stop it. A live test asserts the two halves agree, executing a listed workflow whenrunnable_countis zero and requiring the deployment error. -
MCP 2026-07-28, with the 2025-era handshake served alongside it. Built on
@modelcontextprotocol/server2.x:serveStdio(factory, { legacy: "serve" })andcreateMcpHandler(factory, { legacy: "stateless" })both take one factory and decide the era from the opening exchange, so the same registrations answer a 2026-07-28 client and a 2025-era one. That second half is the point — practically every client shipping today (Claude Code, Cursor, Zed, Windsurf, Claude Desktop) still opens withinitialize, and upgrading this server must not ask them to upgrade too.test/stdio/protocol-eras.test.tsdrives the built binary with a real 1.30.x client and a real pinned-2026 client and asserts both see an identical tool set;@modelcontextprotocol/sdkstays as a devDependency for exactly that purpose. -
Cache hints (
ttlMs/cacheScope, SEP-2549). The catalogue is fixed at boot by the profile, sotools/list,prompts/list,resources/listandserver/discoverare advertised aspublicfor 5 minutes. Resource reads carry their own:mistral://capabilitiespublic/5 min (it is a projection of the process config),mistral://modelsandmistral://voicesprivate/5 min (entitlements are per-key, and each read is a live API call),mistral://workflowsprivate/30 s (the account holder deploys and retires those). The default for anything unhinted stays the conservativettlMs: 0, cacheScope: "private". 2025-era responses are untouched — the revision has no cache fields. -
Trace propagation and an audit trail (
src/observability.ts).traceparent/tracestate/baggagearrive in an MCP request's_metaand are stamped onto every outgoing call to Mistral (or to your own endpoint) through abeforeRequesthook on the SDK's HTTP client, with the context held in anAsyncLocalStoragefor the life of the handler — so a customer's collector joins the MCP span to the inference span it caused, and no tool handler has to know any of this exists. Alongside it, one JSON line per tool call on stderr: tool, outcome, duration, trace and span ids. Never a payload — no prompts, documents, transcripts, arguments or model output, asserted as a negative against the built binary intest/stdio/observability.test.ts. On by default (MISTRAL_MCP_AUDIT=offsilences it): an audit trail an operator has to discover is one they will not have when they need it, and MCP's ownloggingcapability is deprecated in 2026-07-28 in favour of exactly this. Zero new dependencies —node:async_hooksis core and the traceparent grammar is 55 characters of hex parsed here. -
test/stdio/http-transport.test.ts— the Streamable HTTP path over a real socket:/healthz, 401 unauthenticated, 401 on a wrong token, 404 on an unknown path, and both protocol eras against the same endpoint. -
MISTRAL_BASE_URL— point the server at any OpenAI-compatible endpoint (vLLM, TGI, LiteLLM, an internal token factory) and every request goes there instead ofapi.mistral.ai. Wired to the Mistral SDK'sserverURL. Validated at boot: absolute http(s) only, trailing slashes stripped, andhttps://api.mistral.airecognised as not custom so the default path is unchanged. -
self-hostedprofile — inferred automatically from a customMISTRAL_BASE_URL, and registering only the five families such an endpoint actually serves:mistral_chat,mistral_chat_stream,mistral_embed,mistral_tool_call,mistral_vision. OCR, Voxtral, Files, Batch, Agents, Conversations, Libraries and Workflows are Mistral-platform endpoints — advertising them in front of vLLM only produces 404s the calling model has to guess its way out of. An explicitMISTRAL_MCP_PROFILEalways wins over the inference, for gateways that do proxy the full API. -
mistral://capabilitiesresource — the active profile, whether it was inferred, the endpoint and its kind (mistral/custom), the list of registered tools, and for every tool family whether it is available plus a one-line reason when it is not. An agent can now discover why a tool is missing instead of calling it to find out. -
rag_indexes_list— lists the search-index deployments on the account (backend, status, per-index document counts, ISO timestamps). Read-only,coreprofile. Deliberately not a retrieval tool: Mistral's Agentic Search ships its own MCP server, and reimplementing hybrid retrieval here would duplicate it badly. Register/unregister stay out for the same reasonconnectors_*andlibraries_*writes do — they are deploy-pipeline operations, not agent-loop operations. -
A document ingestion corpus and evaluation harness.
npm run fixtures:generaterebuilds eight synthetic PDFs from readable source;npm run eval:docsscores them against real OCR and reports, per document, whetherkind: "auto"classified correctly, whether the required fields survived, and the OCR confidence. The corpus is chosen for what breaks ingestion rather than what flatters it: a/Rotate 90landscape scan, ruled line-item tables, side-by-side address columns, a blank page mid-document, mixed FR/EN, accents and the euro sign, and one near-empty page. Ground truth lives intest/fixtures/corpus.jsonand is checked against the bytes on disk without a key, so a manifest that drifts from the PDFs fails the build instead of quietly invalidating every eval result. All content is invented — no real PII, by design, because a corpus is only useful if it can be published. -
On-prem deployment deliverables —
deploy/docker-compose.yml(bearer token required, loopback-only publish, read-only rootfs, all capabilities dropped, healthcheck; optionalvllmprofile for local inference) anddeploy/k8s/mistral-mcp.yaml(ConfigMap, Service, Deployment withrunAsNonRoot/readOnlyRootFilesystem/seccompProfile: RuntimeDefault, default-deny NetworkPolicy, PodDisruptionBudget). Plain manifests, no Helm chart: the delivery model is files the customer can read, diff and apply.deploy/README.mddocuments both plus the full environment reference. -
test/stdio/self-hosted.test.ts— spins up a real OpenAI-compatible HTTP server, spawns the built binary against it, and asserts the profile inference, the 5-tool catalogue, the absent Mistral-only resources, and a realmistral_chatcall landing on the fake endpoint with a non-Mistral model id. No key, no egress, runs on every push.
Changed
-
A thesis line instead of a feature list. The npm description and both README taglines enumerated ten capabilities, which reads as a server that has wandered. They now lead with what the ten have in common — Mistral, wherever you run it / Mistral, où que vous le fassiez tourner — and keep the enumeration behind it for discovery. The scope rule that follows from it: a feature that only makes sense when the model is not Mistral does not belong here.
-
One retry policy for the whole repo (
MISTRAL_RETRY_CONFIG/MISTRAL_TIMEOUT_MSinsrc/shared.ts). Three had drifted apart:src/index.tscarried the full backoff policy, five live tests carried{ strategy, retryConnectionErrors }without any backoff parameters, andtest/live/mistral.test.tscarried none at all. So the suite whose entire job is to prove the wrapper survives the real API was the only code the policy did not cover, and the first503 Service temporarily unavailablefailed the run outright — even though the SDK lists 503 in itsretryCodesand the production client would have absorbed it. Every client now reads the same constant. -
The document classifier decides on what a document is, not how it is laid out. The first prompt listed categories without criteria, and a technical dossier with numbered annexes and a "Procédure de recette" was filed as
contract: numbered articles and annexes are as common in technical documentation as in agreements.contractnow requires named parties and mutual obligations,genericexplicitly names technical dossiers, specifications and procedures, and the tie-break says to choosegenericwhen both are not present. Measured, not assumed: the corpus went from 7/8 to 8/8 correctly classified against real OCR. -
minOcrConfidence's default no longer claims to be empirical. It shipped as0.3with a comment calling it "empirical — tune via eval", and nothing had ever measured it. A threshold nobody can reproduce is folklore, and the first customer to hit a false reject has no way to argue with it. The value is unchanged (0.3 is a defensible conservative floor), but it is now described as a starting point, andnpm run eval:docsderives a justified number from an actual run: the midpoint between the worst cleanly-extracted document and the best low-signal one. When those populations overlap the harness reports that no threshold is defensible rather than inventing one. -
@modelcontextprotocol/sdk1.30.0 →@modelcontextprotocol/server2.0.0 (runtime), with the v1 SDK retained as a devDependency for the compatibility test.@modelcontextprotocol/nodewas deliberately not taken: it exists to bridge web-standardRequest/Responseintonode:httpand pulls@hono/node-serverto do it, while Node 20 hasRequest,ResponseandReadable.toWebnatively — the bridge is forty lines insrc/transport.tsand the runtime dependency budget stays at three. -
zod 3 → 4 (the v2 SDK's floor is 4.2.0;
@mistralai/mistralaiaccepts^3.25 || ^4). One real breaking change in this repo:.default()now applies to the output type, soprocess_document'soptionsobject — whose inner fields carry their own defaults — became.prefault({}), which is the zod 4 spelling of the old semantics. Two unit tests that asserted zod's exact size-error prose now assert the rejection and the offending field instead. -
Model identifiers are no longer validated against a closed
z.enum. They are now non-empty strings whose.describe()carries the known Mistral aliases and points atmistral://models. This is the same failure class as theConfidenceScoresGranularityenum fixed in 0.9.1: a client-side allow-list rejects valid identifiers before the API can answer, goes stale on every Mistral release, and madeMISTRAL_BASE_URLunusable (my-org/mistral-small-3.2is a normal answer, not an error). Empty strings are still rejected; the endpoint remains the authority on what it serves. -
Profile gating is table-driven (
TOOL_FAMILIESinsrc/profile.ts). The rules had been duplicated acrossindex.ts, fourtools-*.tsmodules andresources.ts, which is how 0.8.0 leakedcodestral_fimandvoxtral_transcribeinto theworkflowsprofile. A third instance of the same class is fixed here:resources.tsaccepted aprofileargument it never read, somistral://voicesandmistral://workflowswere advertised under every profile. Unit tests now assert the table's invariants directly. -
Docker image
node:20-alpine → node:22-alpine, runs asUSER node, npm cache cleaned, cache directory created and owned so a read-only root filesystem works. -
engines.node>=18 → >=20. Node 18 reached end-of-life on 2025-04-30 and CI has only tested 20 and 22 for several releases. -
OCR_MODELSgainsmistral-ocr-4-1andmistral-ocr-4-0as documented aliases.
Fixed
-
The document cache never expired.
stored_atwas written on every entry and never read back, so the only invalidation was aPIPELINE_VERSIONbump: extracted content — a contract's parties and clauses, an invoice's line items — stayed on disk in plaintext indefinitely. It is personal data, and a server sold on European compliance cannot hold it without a retention rule. Entries now carry a window (MISTRAL_MCP_CACHE_TTL_HOURS, default 168 h,0to disable reuse entirely) and are deleted past it, on read and by a sweep, so the content stops existing rather than merely stops being served. The sweep advances a cursor one shard per write on top of the shard just written: read-time expiry alone only reaches entries somebody asks for again, which would have left a document processed once and never revisited on disk forever. An entry with no usablestored_at— anything written before this existed — is treated as expired. -
npm run eval:docscould not have worked. Two defects that only surfaced the first time it ran with a key: it never loaded.env(every live test does), so it reported a missing key on a machine where every other live target worked; and it sentsource: { type: "file" }where the schema's discriminated union declares"file_id", so every document would have failed input validation. The harness had been written, committed and documented without ever being executed. -
The threshold calibration accepted a separation that was noise. On the first real run every document scored between 0.950 and 0.985 — the deliberately low-signal one included — and the midpoint rule proposed
minOcrConfidence: 0.95, a value that would reject essentially every genuine scan. The overlap guard missed it by 0.009.suggestThresholdnow also compares the gap between populations against the clean population's own spread, which needs no invented constant: it asks whether confidence separates these documents any better than it separates documents of the same quality from each other. On this corpus it now refuses to suggest anything and says why — the synthetic PDFs are pure vector text with no image stream, so they exercise the pipeline, not OCR difficulty. A real threshold needs genuinely degraded documents. -
Two live tests encoded assumptions the API does not honour.
test/live/workflows.test.tsassumed a listed workflow is executable andtest/live/connectors.test.tsassumed a listed connector is authenticated; against a real account the first answers 404 and the second 401No credentials found ... Please authenticate. Both are legitimate states that the tools already handled correctly, so the tests now assert the error contract — that the reason reaches the caller as readable text — instead of asserting a success that depends on how the workspace happens to be provisioned. -
mistral_moderatepinned an alias the API resolves. The stdio test asserted the response echoedmistral-moderation-latest; the API returns the dated build that actually served the request (observed:mistral-moderation-2603).
Removed
mcp_sample(breaking). It asked the client to run a completion via MCP sampling — a capability almost no client implements, so the tool's honest answer to nearly every caller was an error. Sampling is deprecated in MCP 2026-07-28 besides. Usemistral_chat, which does the same job against an endpoint that exists.examples/rate-it.mjsand theclawhub/directory — neither was reachable from the documented surface.test/fixtures/generate-pdfs.pyand the four PDFs it produced, replaced by the corpus above. The generator needed Python and a third-party package, neither of which the repo's own toolchain provides, so the fixtures were frozen binaries nobody could regenerate or review. The replacement is dependency-free Node with uncompressed content streams — a reviewer cangrepa fixture and see the text it is supposed to contain.DEFAULT_TOOL_MODEL— callers usedefaultChatModel(), which honoursMISTRAL_DEFAULT_MODEL.MCP_HTTP_STATELESS. Both protocol eras are served per-request now, so there is no sessionful mode left for it to switch off. Setting it is harmless; it just does nothing, and pretending otherwise in the docs would be a lie.
Moved
examples/deploy/README.md→deploy/connector-public.md, next to the manifests it belongs with. Both READMEs' links updated.