Skip to content

v0.10.0 — Mistral, wherever you run it

Latest

Choose a tag to compare

@Swih Swih released this 28 Aug 09:00
· 1 commit to main since this release

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-world was listed by mistral://workflows, workflow_execute answered 404 No active deployment found, and nothing in the tool surface let an agent see that coming. A workflow returned by getWorkflows is a definition; running it needs a deployment with at least one live worker, which is a separate object with separate state. workflow_deployments_list reports it — is_active, worker_count, active_worker_count, is_hardened, managed vs self-hosted — and computes runnable_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_list finds executions (filterable by workflow, status or deployment) so an agent can obtain an execution_id without having started the run itself, and workflow_stop ends one: cancel by default, which lets the workflow run its cleanup handlers, and terminate only on request, which does not — hence destructiveHint: 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 when runnable_count is zero and requiring the deployment error.

  • MCP 2026-07-28, with the 2025-era handshake served alongside it. Built on @modelcontextprotocol/server 2.x: serveStdio(factory, { legacy: "serve" }) and createMcpHandler(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 with initialize, and upgrading this server must not ask them to upgrade too. test/stdio/protocol-eras.test.ts drives 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/sdk stays as a devDependency for exactly that purpose.

  • Cache hints (ttlMs / cacheScope, SEP-2549). The catalogue is fixed at boot by the profile, so tools/list, prompts/list, resources/list and server/discover are advertised as public for 5 minutes. Resource reads carry their own: mistral://capabilities public/5 min (it is a projection of the process config), mistral://models and mistral://voices private/5 min (entitlements are per-key, and each read is a live API call), mistral://workflows private/30 s (the account holder deploys and retires those). The default for anything unhinted stays the conservative ttlMs: 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 / baggage arrive in an MCP request's _meta and are stamped onto every outgoing call to Mistral (or to your own endpoint) through a beforeRequest hook on the SDK's HTTP client, with the context held in an AsyncLocalStorage for 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 in test/stdio/observability.test.ts. On by default (MISTRAL_MCP_AUDIT=off silences it): an audit trail an operator has to discover is one they will not have when they need it, and MCP's own logging capability is deprecated in 2026-07-28 in favour of exactly this. Zero new dependencies — node:async_hooks is 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 of api.mistral.ai. Wired to the Mistral SDK's serverURL. Validated at boot: absolute http(s) only, trailing slashes stripped, and https://api.mistral.ai recognised as not custom so the default path is unchanged.

  • self-hosted profile — inferred automatically from a custom MISTRAL_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 explicit MISTRAL_MCP_PROFILE always wins over the inference, for gateways that do proxy the full API.

  • mistral://capabilities resource — 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, core profile. 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 reason connectors_* and libraries_* writes do — they are deploy-pipeline operations, not agent-loop operations.

  • A document ingestion corpus and evaluation harness. npm run fixtures:generate rebuilds eight synthetic PDFs from readable source; npm run eval:docs scores them against real OCR and reports, per document, whether kind: "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 90 landscape 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 in test/fixtures/corpus.json and 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 deliverablesdeploy/docker-compose.yml (bearer token required, loopback-only publish, read-only rootfs, all capabilities dropped, healthcheck; optional vllm profile for local inference) and deploy/k8s/mistral-mcp.yaml (ConfigMap, Service, Deployment with runAsNonRoot/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.md documents 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 real mistral_chat call 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_MS in src/shared.ts). Three had drifted apart: src/index.ts carried the full backoff policy, five live tests carried { strategy, retryConnectionErrors } without any backoff parameters, and test/live/mistral.test.ts carried 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 first 503 Service temporarily unavailable failed the run outright — even though the SDK lists 503 in its retryCodes and 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. contract now requires named parties and mutual obligations, generic explicitly names technical dossiers, specifications and procedures, and the tie-break says to choose generic when 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 as 0.3 with 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, and npm run eval:docs derives 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/sdk 1.30.0 → @modelcontextprotocol/server 2.0.0 (runtime), with the v1 SDK retained as a devDependency for the compatibility test. @modelcontextprotocol/node was deliberately not taken: it exists to bridge web-standard Request/Response into node:http and pulls @hono/node-server to do it, while Node 20 has Request, Response and Readable.toWeb natively — the bridge is forty lines in src/transport.ts and the runtime dependency budget stays at three.

  • zod 3 → 4 (the v2 SDK's floor is 4.2.0; @mistralai/mistralai accepts ^3.25 || ^4). One real breaking change in this repo: .default() now applies to the output type, so process_document's options object — 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 at mistral://models. This is the same failure class as the ConfidenceScoresGranularity enum 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 made MISTRAL_BASE_URL unusable (my-org/mistral-small-3.2 is 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_FAMILIES in src/profile.ts). The rules had been duplicated across index.ts, four tools-*.ts modules and resources.ts, which is how 0.8.0 leaked codestral_fim and voxtral_transcribe into the workflows profile. A third instance of the same class is fixed here: resources.ts accepted a profile argument it never read, so mistral://voices and mistral://workflows were advertised under every profile. Unit tests now assert the table's invariants directly.

  • Docker image node:20-alpine → node:22-alpine, runs as USER 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_MODELS gains mistral-ocr-4-1 and mistral-ocr-4-0 as documented aliases.

Fixed

  • The document cache never expired. stored_at was written on every entry and never read back, so the only invalidation was a PIPELINE_VERSION bump: 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, 0 to 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 usable stored_at — anything written before this existed — is treated as expired.

  • npm run eval:docs could 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 sent source: { 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. suggestThreshold now 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.ts assumed a listed workflow is executable and test/live/connectors.test.ts assumed a listed connector is authenticated; against a real account the first answers 404 and the second 401 No 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_moderate pinned an alias the API resolves. The stdio test asserted the response echoed mistral-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. Use mistral_chat, which does the same job against an endpoint that exists.
  • examples/rate-it.mjs and the clawhub/ directory — neither was reachable from the documented surface.
  • test/fixtures/generate-pdfs.py and 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 can grep a fixture and see the text it is supposed to contain.
  • DEFAULT_TOOL_MODEL — callers use defaultChatModel(), which honours MISTRAL_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.mddeploy/connector-public.md, next to the manifests it belongs with. Both READMEs' links updated.