Upgrading from a 2.0 prerelease
This guide covers what changed after 2.0.0-beta.7. If you're upgrading from 1.x, read Migrating from v1.x to v2.0 instead.
From an earlier prerelease, read the beta.7 migration guide first; it covers the changes between prereleases up to beta.7.
Names in older sections. Each section uses the names of the change it describes. Sections from Result fields use the eval vocabulary on renamed many of them: manifests are eval configs, suites are evals, hosts are clients (claude-cli is claude-code), arms are variants, runEvalSuite is runEval, runVariantExperiment is runToolOptimization, and hostTelemetry.hostApp is clientTelemetry.clientApp. The main guide has the full mapping. Sections before The mst client is now api call the api client mst, and sections before The Codex surface is the codex-app client call it chatgpt with surface: "codex". Sections before The Codex CLI client is codex-cli call the codex-cli client codex. Sections before One name for each client option call maxActions computerUseMaxActions or nativeMaxActions. Use the current names in new code.
These sections of the main guide are also new since beta.7:
- Custom judges are plugins
- Every judge runs the same way
- LLM calls: bearer tokens and streaming
runToolOptimizationneeds clear evidence to recommend a variant- Client cases name a client and model, not
mcpHostConfig - Direct cases are Playwright tests
- The simulator and the external-host runtime are internal
- Eval configs and variants
Runs record which client app and judge version produced them
Affects: code that reads clientTelemetry, run.json's judges or a result store's manifest.json; editors or validators that fetch a schema by its $id.
- Every built-in client records its app. Each trial's
clientTelemetry.clientAppis{ name, version, source }for the app that ran it: Claude Code's version from its startup event, the Codex CLI's from its session rollout, Cowork's and the ChatGPT and Codex apps' from the installed app (the macOS bundle, or the Linux package). Only Cowork recorded it before. Theapiclient has no app:run.json'smst.versioncovers it. A client that can't tell its version records noclientApp. run.json's judges have aversion. Beside each judge'sidentity, the version a person can read, asmst judgeslists it. Runs from before have none.manifest.jsonnames its kind. A stored run's manifest keeps"format": 1and adds"kind": "manifest", with a schema atschema/run/v1/manifest.schema.json. MST still fetches runs whose manifest has nokind. A manifest with aformatthis MST doesn't know fails the fetch and says to upgrade MST.- Schema IDs resolve. Every schema's
$idis nowhttps://unpkg.com/@gleanwork/mcp-server-tester@2/schema/<path>. The eval config's was a GitHub URL, the dataset's a GitHub Pages URL, and the run and history schemas' an unversioned unpkg URL; none of those returned the schema.@2resolves once 2.0.0 is published. Until then, point$schemaat@rcor at the file innode_modules.
Runs keep what a newer MST wrote; clients' events are checked
Affects: a plugin client whose transcript events don't match the TranscriptEvent type; code that reads mst.run/v1 files with the published JSON Schemas.
MST reads a run written by a newer MST and keeps the fields it doesn't know, at every depth, including through mst grade and --resume. A value it doesn't know in a field whose values the format names, such as a write's mode, a trial's ending or a transcript event's kind, fails the read and says to upgrade MST. schema/run/v1/ now describes each trial's transcript and each case result as well.
MST checks each client's transcript events against the stored shape when the client returns them. An event without a name, with arguments that aren't an object, with an output that isn't a string, or with a kind or source the type doesn't list fails that trial and names the field. Before, MST wrote such a trial into a run it couldn't read back. A durationMs that isn't a finite number is dropped. To migrate, return events that match TranscriptEvent: put a tool's output in output as text.
Score keys are assertion keys
Affects: code that reads a trial's scores or a run's scores/ directory, or uses the GraderType type; OpenTelemetry queries on a grader's name; a stored run or saved baseline from an earlier prerelease.
A code grader now records its score under its assertion's name, as CONTEXT.md and ADR 0013 name it. The report and the scores/<grader>/ directories use the same names. So does OpenTelemetry: the span name evaluate <grader>, gen_ai.evaluation.name and mst.grader.
| Before | Now |
|---|---|
textContains | containsText |
regex | matchesPattern |
GraderType drops exact, schema, snapshot and size. Nothing has written them since direct cases became Playwright tests. error stays: it is a trial's score when its grading didn't come back.
MST refuses to read a stored run or a baseline with the old keys, and the error names the new ones. Run the eval again. A baseline from a result store gives a warning, as any baseline it can't read does, and the run has no comparison.
The CLI runs without a terminal
Affects: scripts that run mst init, mst generate, mst login or mst token.
The CLI no longer uses Ink, so these commands no longer fail with Raw mode is not supported outside a terminal. In a terminal, init and generate still ask for what the flags leave out; the prompts are now numbered lists and (Y/n) questions. Without one, they take every answer from flags and fail, naming the flag, when one is missing:
mst init --command "node server.js"or--url <url>, with--name,--install/--no-installand--forceto write into an existing directory.mst generate --tool <name> --args '<json>', with--config,--urlor--command, records one call.--name,--description,--suiteand--exactset what the prompts did.--commandkeeps a quoted argument together, so--command 'node "/Users/a/My Server/index.js"'works. Before, every space split an argument.
mst token changes for CI scripts:
- It writes each value on one line. It wrapped long tokens at 80 columns when its output was piped, so
>> $GITHUB_ENVstored a broken token. - It exits 1, and writes to stderr, when no tokens are stored for the server. It exited 0.
- An unknown
--formatfails with the allowed values. It printed nothing and exited 0.
Every write is accounted for
Affects: eval configs whose servers list tools that aren't read-only without a dry-run proxy in front; plugin connectors whose launch returns dryRunProxyServer(), or that forward writes; code that reads mst.simulated-write/v1 files.
ADR 0014 gives every server a write mode, records every write the dry-run proxy stops, and grades write cases from that record. See Writes.
-
A server MST can't see the writes of stops the run. A server with no dry-run proxy, or behind a proxy that forwards writes without the connector's
plannedWriteKey, must list only read-only tools (readOnlyHint: true, notdestructiveHint: true). Otherwise the run doesn't start:MST can't see these servers' writes: "docs" lists tools that aren't read-only (save, delete). Annotate the server's read tools, put it behind a connector's dry-run proxy, or accept its writes in the eval config:"writes": { "docs": "unguarded" }. A server whose writes should reach the service is"live", and needsmst run --allow-live-writes. -
Connectors pass
context.writesto the proxy.dryRunProxyServer({ simulateWrites })fails with what replaces it: pass the launch context'swritesunchanged, and the reply templates aswriteReplies. A launch that dropswrites, or changes whether it simulates, fails the run instead of printing a warning.Before Now context.simulateWritesin a connector'slaunchcontext.writes, in every rundryRunProxyServer({ simulateWrites: { ...simulateWrites, replies } })dryRunProxyServer({ writes: context.writes, writeReplies })mst.simulated-write/v1file, simulated writes onlymst.write-record/v1file, every stopped write -
A connector that forwards writes names its mark. Set
plannedWriteKeyon the connector to the key the server puts in each write it stopped, so MST records those writes (server-planned). Name tools that run other tools indispatchers. -
Compared variants get the same kind of reply to a write. Variants whose servers stop writes in different ways (planned, simulated, server-planned) no longer run in one eval config.
-
Write cases are graded from the record. List a case's writes in
expected.writes(server, tool, partialarguments,required,order,exclusive) and the newwritesgrader checks the trial's write record. Every score of a write case haswriteEvidence. A trial whose record and transcript disagree (unreported or unrecorded writes) fails itswritesgrader. A trial whose client failed keeps its record, and each write in it is unreported. A record with an unclassified call givescalledevidence, notrequested. -
A read-only server is held to the tools it listed. A trial that calls a tool its
read-onlyserver didn't list at the readiness check ends as asetupfailure that--resumedoesn't retry. Put a server whose tools depend on who signs in behind a dry-run proxy. -
Stored runs gain write data. Each stored trial has its
writeRecord, and each variant inrun.jsonitswriteModes. A stored run withoutwriteModesregrades as before; it has no write records to grade. -
Judge prompts changed. The built-in judges render the write record and say a planned or simulated write was requested, not made, so their versions changed. Agentic judges' workspaces have
writes.json.
A transcript event names a tool as its server lists it
Affects: client plugins whose events set server and put the server label in name ({ server: 'docs', name: 'docs.search' }).
An MCP event that has a server names the tool exactly as that server lists it. MST no longer reads a <server>. prefix off that name, so a server's tool named docs.search stays docs.search rather than counting as search. A plugin that wrote { server: 'docs', name: 'docs.search' } for the tool search now reports a different tool: its write record shows an unreported write, and on a read-only server the trial fails as a call to an unlisted tool. Report { server: 'docs', name: 'search' }. MST gives an event with no server the server of a one-server run, as before; with several servers it can't tell which one the call went to.
What run.json records so runs can be compared
Affects: code that reads run.json; plugins with settings whose judges have calibrations; evals whose runs are collected on several machines (CI, --detach); evals with connector servers.
mst history is new since rc.0. It compares an eval's runs, and starts a new era wherever their setup differs: a server's endpoint or command under the same label, a server's write mode, a plugin's version or settings, a client option, or a judge's identity in a case. See The eval's history. So it can tell, run.json records more than in rc.0:
- Hashed client options under one key per eval. A client option that looks like a credential is still in
run.jsononly ashmac:…. It is now hashed under the eval's key,option-keyin its results directory, not a new key for each run, so the eval's runs record an unchanged value alike.run.json'soptionKeyis a fingerprint of the key. rc.0 runs hashed each under its own key and record nooptionKey, so history compares their options by name only. The first run after upgrading starts a new era for each variant with such an option. Where runs are collected on several machines, such as CI, give each the sameMST_OPTION_KEY(64 hex digits) from a secret, or every run starts an era.--detachsends the coordinator the key of the machine that started the run. Don't commit or uploadoption-key. - A judge's identity covers its plugin's settings. A plugin judge whose plugin's settings are non-empty once parsed (defaults in its schema count) gets a new identity, so its calibrations go stale: run
mst judges checkagain.mst grade --resumeof a run graded before upgrading grades such a case judge's trials again. For such an eval config judge it refuses, as for any judge that changed: grade the run in full. Judges whose plugins take no settings, and the built-in judges, keep their identities. - A regrade records its source run's setup. Its
run.jsonhas the servers, client, model, client options (with their hashes), tool metadata, input template, plugins andoptionKeyof the run that collected its transcripts, not the eval config's or this machine's now. - A connector server is recorded without credentials.
run.jsonrecords a connector server as itsconnector,labelandurl(the declared URL, or else the connector's default), without the URL's credentials, query or fragment. An rc.0 local run recorded a connector with alaunchas the launched proxy's command instead. An rc.0 run on a runner recorded the URL as written, so if a declared connector URL held a credential, it is in that run'srun.json, including in a result store: check those runs, and rotate the credential. A regrade of such a run doesn't copy it. - New fields.
run.jsonhasplugins(namespace,name,version, andsettings, a hash of the plugin's parsed settings; empty when the run loaded none) andoptionKey.
Traces are transcripts
Affects: code that reads a result's trace, toolCallTrace or traceEvidence, or a stored run's traces/ directory; code that imports the trace types; question judges that set needsTrace; agentic judges that read trace/ in their workspace; calibrations of the built-in judges.
ADR 0013 renamed the record of what a client did in a trial from trace to transcript, Anthropic's word for it. "Trace" now means only an OpenTelemetry trace, as in storedRunTraces. None of the old names still works: stored runs, baselines and needsTrace fail and name their replacement, and TypeScript flags the renamed types and fields.
| Before | Now |
|---|---|
trace, toolCallTrace, traceEvidence on a result or trial | transcript, toolCallTranscript, transcriptEvidence |
traces/<variant>/<case-id>/<trial>.json in a stored run | transcripts/<variant>/<case-id>/<trial>.json |
Trace, TraceEvent, TraceEvidence, TraceSource | Transcript, TranscriptEvent, TranscriptEvidence, TranscriptSource |
traceSource, traceConfidence, traceLimitations | transcriptSource, transcriptConfidence, transcriptLimitations |
traced on a report trial | hasTranscript |
needsTrace in defineQuestionJudge | needsTranscript |
trace/events.json, trace/messages.json in a judge workspace | transcript/events.json, transcript/messages.json |
A run stored by an earlier prerelease has a traces/ directory, and a saved baseline has trace fields. This MST can't read either, and says so: run the eval again.
The built-in task-completion, grounded-in-tools, tool-use, criteria and preference judges now render <transcript> in their prompts, so their versions changed. Labels you checked against the old versions are stale: run mst judges check again.
Environments are runners
Affects: commands and scripts that pass --env, --env-option or --grade-env, or run mst env; plugins with environments; code that imports the environment types or reads environment in run.json.
ADR 0013 renamed where trials are collected from environment to runner, because in Anthropic's vocabulary the environment is what the client acts on. The old names fail and name their replacement; none of them still works.
| Before | Now |
|---|---|
--env, --env-option, --grade-env | --runner, --runner-option, --grade-runner |
mst env setup | mst runner setup |
A plugin's environments, names such as acme/env/cloud-vm | runners, acme/runner/cloud-vm |
EnvironmentDefinition, Environment, EnvironmentContext, EnvironmentKeep | RunnerDefinition, Runner, RunnerContext, RunnerKeep |
machineEnvironment, dockerEnvironment | machineRunner, dockerRunner |
env, envOptions in runEval, gradeRun and evalConfig options | runner, runnerOptions |
environment and grading.environment in run.json | runner and grading.runner |
A run stored by an earlier prerelease has environment and no runner. This MST can't read it, and says so: run the eval again.
A variant's coworkSetup applies in Docker and cloud-VM runs
Affects: Cowork evals with coworkSetup in a variant that run with --env docker, on cloud VMs, or in any other environment that collects shards with mst collect.
A shard gave its client the eval's coworkSetup, not the variant's merged over it, so a variant's toolSearch, clarifyingQuestions, approveWriteTools, disabledBuiltinTools and other coworkSetup fields were ignored there. Local runs were right. Now a shard merges them as a local run does. Variants that compared Cowork settings in such a run measured one setting twice; run them again.
On Linux, Cowork also no longer rejects an HTTP server with MST's own fields, such as requestTimeoutMs; it leaves them out of Claude Desktop's settings, as macOS does.
Case inputs must contain non-whitespace text
Affects: datasets for every client, including run-only plugins, whether loaded from files, returned by a plugin, or supplied in memory.
Empty inputs already failed dataset validation. Whitespace-only inputs now fail too, before any client opens or any trial is submitted, even when the blank case follows valid cases. Replace blank inputs with the intended prompt or remove those cases. Valid prompt text is preserved exactly; MST does not trim whitespace, normalize line endings or change Unicode. Direct native-session calls still reject blank prompts before submission.
Client collection uses sessions; runBatch is removed
Affects: plugin clients that implement ClientDefinition.runBatch and callers of the desktop built-ins' batch entry points.
Replace the client extension's runBatch(requests, context) with open(options, context). A definition may have run, open, or both. Run-only plugin definitions need no change; their datasets follow the input rule above. MST prefers open during local and worker collection.
options.config holds the fixed client options and model. options.input holds the servers and environment, without a prompt. Return a session with run(trial), reset(), optional diagnose(), and close(). Each trial has caseId, zero-based trial, and input: { prompt }. Return { result: ClientRunResult, continuation: 'continue' | 'reset' | 'stop' }. MST opens a separate session for different fixed settings.
Move setup into open and cleanup into close. If setup throws, clean up its partial state before rethrowing. Do not loop over cases or call context.reportResult or context.acquire from session operations. MST now owns those calls, readiness, reset and diagnosis, partial-result streaming, and final cleanup. Declared servers are checked before opening; Cowork's preparation-dependent servers are checked after opening. Retry delays and diagnosis deadlines do not change. A client definition that still has runBatch, even beside open, fails validation with this migration instruction.
The session types remain in @gleanwork/mcp-server-tester/evals. ClientRunResult, grader results and stored schemas do not change. runEvalBatch and collection batches are not removed. See Client sessions.
Cowork answers clarifying questions with their first option
Affects: Cowork evals on MST's Linux desktop (the Docker image and cloud VMs), prepared Linux desktops, and cases that failed as awaitingUser.
MST used to turn off Claude's AskUserQuestion tool in headless runs, so the model went on with its best assumption. ACE keeps the tool and answers with the first option, so the two measured different behavior. Now MST does the same as ACE on Linux. It leaves the tool on, and HITL reads the question's options from the task's transcript and clicks each question's first option. For a checked multi-select option, it presses the page's Submit/Next. A question whose form doesn't take its first option within 60 seconds, or that has no usable options, still fails the case as awaitingUser. computerUse.hitl.telemetry.questions_answered counts the answered pages.
- To keep the old behavior, set
"coworkSetup": { "clarifyingQuestions": "disabled" }in the eval or a variant. - Owned desktops (
MST_DESKTOP_OWNED=1): MST removesAskUserQuestionfrom the image'sdisabledBuiltinTools, unless the eval'sdisabledBuiltinToolslists it. - Prepared desktops: the managed settings no longer need
"disabledBuiltinTools": ["AskUserQuestion"], except withclarifyingQuestions: "disabled". Settings that still turn it off keep working; Cowork just never asks. - macOS: the eval profile still turns the tool off.
- Answers, tokens and time change for cases where the model asks.
Cowork has tool search on
Affects: Cowork evals, on MST's Linux desktop (the Docker image and cloud VMs) and the macOS eval profile, and comparisons of their tokens or time with earlier runs.
MST now writes Claude Desktop's toolSearchEnabled as true. Cowork lists each MCP server's tools by name and loads a tool's definition only when the model searches for it, as Cowork signed in to claude.ai does. Before, Desktop left tool search off in gateway mode, so every request carried every connector's tool definitions. Requests are smaller with many connectors, and a trace can now show ToolSearch calls. Token and time results change; answers can too.
- To keep the old behavior, set
"coworkSetup": { "toolSearch": false }in the eval or a variant. - Claude Desktop 1.21459.0 or later knows the setting; an older one ignores it.
- Through an LLM gateway, check that tool search works: the gateway must accept the
tool-search-tool-2025-10-19beta, or requests fail with HTTP 400, and a case's trace should showToolSearchcalls.
Linux desktop receipts omit the driver identity
Affects: code reading driver from SemanticDesktopTelemetry, including Cowork's submission and HITL receipts and ChatGPT or Codex app's native setup and submission receipts.
New receipts no longer write driver: 'linux-desktop'. Read the client from the trial's variant or case, and the OS from the run's environment metadata. Accounting, action counts, duration, planner and cost fields are unchanged. The public type keeps driver as an optional legacy field, and readers accept old receipts without rewriting stored files.
Cowork, ChatGPT and Codex app now use concrete macos.ts and linux.ts entry points internally. Product and worker OS select execution; this adds no user option or public runtime export.
Client listings include defaults; known environment OS is checked
Affects: consumers of mst clients text or JSON output, and evals using clients with requirements.os.
mst clients now prints the same requirements and option/default table as mst clients show, for every client. Its JSON entries now include options, the JSON Schema previously returned only by show --json. Literal runtime defaults use default. Dynamic defaults and omission semantics use x-defaultDescription; required and not declared distinguish required options from optional plugin options with no declared default. Text output omits removed options; their rejecting schemas remain in JSON. Update text parsers to use --json.
Timeout behavior is unchanged: omit timeout for no API timeout, or the existing default on other clients. 0 and null are not substitutes for omission. Desktop defaults still resolve on the trial machine.
MST now checks a client's requirements.os when loading an eval, including variants and case overrides. local means the executing machine; built-in docker means Linux. Choose a compatible environment if the new error names an unsupported OS.
Plugin environments may add os: 'macos' | 'linux' to EnvironmentDefinition when every trial machine uses that OS. MST validates the field when loading the plugin. Omit it for unknown or mixed machines; existing undeclared environments remain compatible. MST opens no machines to discover their OS. Run-only client plugins need no change.
Desktop clients expose sessions; batch entry points remain
Affects: code implementing or calling ClientDefinition.
Cowork, chatgpt and codex-app now implement open(options, context). It fixes the client options, model, servers and environment for one app session. session.run({ caseId, trial, input: { prompt } }) returns the unchanged ClientRunResult plus a typed continuation: continue, reset, or stop. The session also has reset(), optional diagnose(), and close(). See Client sessions for the types exported from ./evals and the readiness and lifecycle metadata.
This earlier step added a transitional contract, before dispatch migrated. The desktop clients retained thin runBatch wrappers, and plugin validation still required run or runBatch. Those entry points are now removed; use the session migration above. Run-only plugins need no changes. Readiness order, reset and diagnosis policy, leases, cleanup, reporting, concurrency and retry timings are unchanged.
Built-in clients return the common result
Affects: code importing MstClientSimulationResult, custom judges reading a Playwright eval's response, and code reading the API client's conversation or timing fields.
MstClientSimulationResult is removed from the root and ./types exports. Built-in clients now return ClientRunResult from @gleanwork/mcp-server-tester/evals, with finalText and ordered events instead of a simulation envelope. LLMToolCall remains exported for native trace readers. The internal simulator interface, simulateMstClient entry point and result-conversion functions are deleted.
MST derives the assertion/report response from the common result. Playwright API evals now expose the same events, evidence, and tool-call kind/source fields as mst run. Custom judges on those Playwright evals see this common view. The existing success, response and toolCalls fields remain; grader rules, error classifications and trial endings do not change. Existing custom executeCase implementations can still return their assertion view.
The API client's input and conversation history move to response.telemetry.content; MCP timing moves to response.telemetry.mcpDurationMs. llmDurationMs stays on the response. Stored results drop telemetry.content unless redactStoredResponses: false. Existing skill-load measurements retain their meaning and per-trial fields; both execution paths now carry them through the common result. Old stored runs remain readable without rewriting them.
ChatGPT results don't record a driver
Affects: code that reads driver, driverSlug, capabilitiesUsed, displayName, clientName or clientType from the clientMetadata of a chatgpt or codex-app result (in clientTelemetry.clientMetadata, or a stored trial's clientMetadata), and code that uses the ClientMetadata type from @gleanwork/mcp-server-tester/experimental/clients.
The ChatGPT app's clients run as ordinary client code (ADR 0011). The driver identities (openai.chatgpt.agent.desktop-app.macos and .linux) and the capability bindings they named are gone, and new results stop writing the fields that only named them. The client, the environment and the OS of the machine that ran the trial already say how it ran. What the client recorded about the trial itself is unchanged: correlation, session and turn, evidence, failure kind, and the computerUse and nativeController receipts.
- New results.
clientMetadatahas nodriver,driverSlug,capabilitiesUsed,displayName,clientNameorclientType. Read the client from the trial's variant or case, and the OS fromenvironmentinrun.json: itsplatform, or each worker's inmachines. - The type. Those fields of
ClientMetadataare optional, documented as recorded by earlier versions only.ClientDriverId,ClientCapabilityandExternalClientTypestay exported, for the old values. - Stored runs. A run stored before this change keeps these fields, and MST doesn't rewrite it.
mst grade,mst run --resumeand everything else that reads a stored run accept them, and a failed trial still ends as its failure kind says. - Messages. A ChatGPT step that throws instead of returning its failure now fails the trial with
ChatGPT trial failed: <reason>(failure kindclient_run_failed, as before), notExternal client capability failed: <reason>.
MST checks every client's servers before the first trial
Affects: evals on api, claude-code, codex-cli and plugin clients whose servers don't start, can't be reached or serve no tools; anything that sets MST_COWORK_MCP_PREFLIGHT_RETRY_S; code that reads ChatGPT's batch telemetry mcpPreflight or nativeReadiness.mcpPreflight, or the Linux setup code mcp_preflight_failed.
One readiness check runs before the first trial of each batch, for every client (ADR 0011): each server connects and lists at least one tool, and at least its minTools for a stdio server. Before, only Cowork and ChatGPT checked, each in its own way, and only Cowork retried. See Server readiness.
- Trials against an unready server don't run. On
api,claude-code,codex-cliand plugin clients, a trial against a server that doesn't connect, serves no tools or serves fewer than itsminToolsused to run, and usually failed as a trial. Now no trial of its batch is sent, and each ends as an infrastructure failure withbroke: 'setup'andretryable: true, and a reason that names the server, for exampleMCP server not ready, so no trial was sent: search listed no tools. Checked 3 times.Its case is incomplete instead of failed, so pass rates on such runs change, andmst run --resumecollects those trials again. - Desktop failures are results. A Cowork or ChatGPT batch whose server wasn't ready used to stop a local run, and on a worker its trials came back missing. Now each trial is the same
setupfailure, locally and on a worker, and the run goes on with its other batches. - Retries for every client. A server that isn't ready is checked again after 10 seconds and then 30 more; only the servers that failed are checked again. Rename
MST_COWORK_MCP_PREFLIGHT_RETRY_StoMST_MCP_READINESS_RETRY_S(comma-separated seconds; empty turns retries off). A run that still sets the old name fails when it starts, and the message names the new one. - One check per batch. MST checks on the machine that runs the trials, before the batch starts and before anything touches a desktop app. Cowork's stdio servers resolve only once the app is prepared, so its servers are checked after preparation instead, and not twice. A batch whose servers go through the tool-variant proxy (a variant with tool metadata) is checked at its declared servers before the proxy starts, Cowork's included, and its
minToolsapply; a proxy that still can't reach a server ends the batch the same way instead of stopping the run. MST's check no longer usesinput.checkServers. ChatGPT's own preflight is gone: MST checks before it stops your app on macOS, and the Linux setup no longer connects to the servers itself, somcpPreflightleaves its telemetry andmcp_preflight_failedits setup codes. The Linux app-server status probe, Claude Code's MCP startup check and Codex CLI'srequiredservers are unchanged. - A time limit for the whole probe. Each server gets 30 seconds to connect, list its tools and close; before, only listing tools was timed. A server that sets a longer
connectTimeoutMsgets that to connect, plus 30 seconds. A server that takes longer fails the check. - Not checked. A stdio server with client-resolved fields (
url,auth,files,${...}placeholders) is checked only by Cowork, after preparation.runEvalDatasetandrunEvalCasein a Playwright test aren't checked: the test's fixture already connected.
The Codex CLI client is codex-cli
Affects: eval configs, variants, datasets and cases that name the codex client, and code that passes client: 'codex' to runEvalDataset or runEvalCase.
The built-in client that runs the Codex CLI headless is codex-cli (ADR 0011). There are two Codex products: the CLI, and the Codex surface of the ChatGPT desktop app, which is codex-app. codex could mean either. The client's options, its requirements, @openai/codex-sdk and the Docker image don't change, and the judges' runtime: "codex" keeps its name: it is a judge runtime, not a client.
- Configs and cases. Change
"client": "codex"to"client": "codex-cli", andmst/client/codextomst/client/codex-cli. An eval config, a variant or a case that namescodexfails withClient "codex" is now "codex-cli". For the Codex surface of the ChatGPT desktop app, use "codex-app"., and so doesrunEvalDatasetorrunEvalCasegivenclient: 'codex'. - Results.
mst clientslistscodex-cli, and messages from the client name it, such ascodex-cli client timed out after 300000 ms. - Docker.
codex-cliis headless, ascodexwas: its shards run in MST's Linux image without a desktop. - Stored runs. A run stored before this change keeps
client: "codex"in its files, and MST doesn't rewrite them. Everything that reads a stored run reads it ascodex-cli, with its options:mst grade,mst run --resume,mst label,mst judges check, the reportmst openbuilds, the previous-run comparison,storedRunTraces, and a tool optimization started before the change. A report built before the change still showscodexuntil it is built again. - Telemetry. OTLP spans from the client say
codex-cli: the span isinvoke_agent codex-cli, andmst.clientandgen_ai.agent.namearecodex-cli. Codex's own exporters (codex.*records) are unchanged. A collector query that filters oncodexmatches only spans exported before the change, so match both names across older data. - Batches. Changing a config from
codextocodex-clichanges itscontentHash, somst batchruns that eval again, as it does after any config change.
Client options that had no effect fail
Affects: eval configs, variants, datasets and cases that set clientOptions.server or clientOptions.servers on api or claude-code, clientOptions.apiToken, pluginDir or pluginMcpUrl on claude-code, or clientOptions.provider on cowork, chatgpt or codex-app; code that passes these options to runEvalDataset or runEvalCase.
An option that a client can't use fails validation (ADR 0011). These options were accepted and did nothing, so removing them doesn't change what a run does.
- Servers come from the eval.
apiandclaude-coderun with the eval config'sservers(or a variant's), and in a Playwright test with the test's MCP connection. Theirserverandserversoptions never changed that: every run replaced them with the eval's servers. Remove them, and put the servers inservers. - Claude Code's legacy plugin settings are gone.
apiToken,pluginDirandpluginMcpUrlbuilt a Claude Code MCP config that every run replaced with the eval's servers;pluginDircould still writemcp-server-url.jsonunder~/.mcp-server-tester/plugins, and it no longer does. TheMCP_SERVER_URL,MCP_ACCESS_TOKENandMCP_PLUGIN_*variables they fell back to no longer reach Claude Code's config either. Put a server's token in its ownauth(accessTokenoraccessTokenEnv), and add a plugin's MCP server toservers. - A desktop client fixes its provider.
coworktook onlyprovider: "anthropic", andchatgptandcodex-apponlyprovider: "openai"; nothing read it. Remove it. Their credentials and LLM gateways come from the environment, asmst clients show <client>lists.provideronapiandclaude-codeis unchanged: it picks the API. - Messages. Each option fails on an eval config, a variant or a case, and the message names the option and says what to do instead: the
serversmessage names the eval config'sservers, and theprovidermessage names the provider the client fixes. - Shared defaults. An eval config's shared defaults (
provider,timeoutand the rest) no longer reach a client whose schema rejects that option, a plugin client's included. An eval config that setsproviderand whose only clients are desktop clients now fails, saying none of its clients takes it. - Stored runs. A run stored before this change may keep these options in its variants'
clientOptions, and MST doesn't rewrite it. Everything that reads a stored run leaves them out, so the run compares equal to a config written today:mst grade,mst run --resume, the previous-run comparison, and a tool optimization started before the change, whose rounds still match its baseline.
One name for each client option
Affects: eval configs, variants, datasets and cases that set clientOptions.computerUseMaxActions or clientOptions.nativeMaxActions on cowork, chatgpt or codex-app, or a reasoningEffort the client doesn't support; datasets whose cases set their own model or clientOptions on those three clients.
One idea has one option name, and a config that can't run fails when it loads (ADR 0011).
- The action budget is
maxActions. Oncowork, renamecomputerUseMaxActionstomaxActions: 1 to 64, default 24, on macOS and Linux. Onchatgptandcodex-app, renamecomputerUseMaxActions(macOS) andnativeMaxActions(Linux) tomaxActions. Its range and default follow the trial's OS: 1 to 64, default 32, on macOS; 1 to 64, default 24, on Linux. A value outside that range fails when the eval config loads, if MST knows the OS (as for the other options one OS uses), and otherwise on the trial's machine, before the app is prepared. The old names fail on an eval config, a variant or a case, and the message namesmaxActions. Cowork'shitlMaxActionskeeps its name: it is a separate budget, for a case's human-in-the-loop inspections after its prompt is sent. reasoningEfforthas one list of values:minimal,low,medium,high,xhigh,max,ultra. Each client takes the ones it supports:codextakesminimaltoxhigh, andchatgptandcodex-apptakelowtoultra. A value of the list a client doesn't support fails, and the message names the ones it accepts; any other value fails as unknown. The values each client accepted haven't changed.mst clients show <client>lists them.apiandclaude-codehave no reasoning effort option.- A case can't change a desktop client's settings.
cowork,chatgptandcodex-apprun a variant's cases in one app, with the variant's settings. A case that sets its ownmodelor aclientOptionsvalue that differs from its variant's now fails when its dataset loads, before anything runs, naming the case and the field, and so does a case that names another client. Before, these failed only when the batch started, except that in an environment (--env) a case naming another client ran in that client's own batch; it now fails there too, as it did locally. In a variant on another client, cases that name one of these clients with different settings now run in separate batches in an environment too, as they did locally; before, their shared batch failed when it started. Use a variant for a different setting. A case that repeats the variant's settings still runs. timeoutandappVersionare unchanged. Every client'stimeoutis the trial's time limit in ms: none by default onapi, 180000 onclaude-code, 300000 oncodex,chatgptandcodex-app, and 900000 oncowork.appVersionis Cowork's; ChatGPT's version is the Docker environment'schatgptVersion.- Stored runs. A run stored before this change keeps
computerUseMaxActionsornativeMaxActionsin its variants'clientOptions, and MST doesn't rewrite it. Everything that reads a stored run reads either asmaxActions, so the run compares equal to a config written today:mst grade,mst run --resume, the previous-run comparison, and a tool optimization started before the change, whose rounds still match its baseline. A stored ChatGPT variant with both names reads ascomputerUseMaxActions, the budget it ran with on macOS.
The trial's OS decides how a desktop client runs
Affects: eval configs, variants, datasets and cases that set clientOptions.computerUseProvider on cowork, chatgpt or codex-app; Cowork and ChatGPT runs with --env docker; code that reads run.json's environment.machines.
How a desktop client runs follows from the OS of the machine that runs the trial, not from configuration (ADR 0011). On macOS, cowork, chatgpt and codex-app use Computer Use, with computerUseModel as its planner. On Linux, they use the accessibility (AT-SPI) automation, which has no planner. Before, Cowork's computerUseProvider defaulted to the OS of the machine that read the config, so a run from macOS with --env docker sent the macOS value to its Linux workers, which rejected it.
- Configs and cases. Remove
computerUseProvider. Set on an eval config, a variant or a case, on any of the three clients, it fails, and the message says the OS of the trial's machine decides. - Options one OS uses. On Cowork,
pluginRootsandmcpDataRootare Linux options, andcomputerUseModelandappVersionare macOS options. Onchatgptandcodex-app,computerUseModelandcomputerUseMaxActionsare macOS options, andnativeMaxActionsandpluginsare Linux options. One the trial's OS can't use fails when the eval config loads, if MST knows that OS: this machine's for--env local(the default), and Linux for--env docker. A plugin's environment checks on its worker, before any app is prepared. Before, Cowork checked these againstcomputerUseProvider, and ChatGPT rejected some of them only when its app was being prepared;nativeMaxActionson macOS was ignored. Move such an option to a variant that runs on the OS that uses it. - Results. Each shard's machine in
run.json(environment.machines) recordsplatform, the OS its worker ran on, and the report shows it under Run details. A local run's OS isenvironment.platform, as before. At this earlier step, telemetry also recordeddriver: "linux-desktop"on Linux andprovider: "anthropic-computer-use"in ChatGPT's macOS Computer Use telemetry. New Linux receipts omit the driver identity; use the recorded environment and OS instead. - Stored runs. A run stored before this change keeps
computerUseProviderin its variants'clientOptions, and MST doesn't rewrite it. Everything that reads a stored run leaves it out, so the run compares equal to a config written today:mst grade,mst run --resume, the previous-run comparison, and a tool optimization started before the change, whose rounds still match its baseline. A stored machine has noplatform.
The Codex surface is the codex-app client
Affects: eval configs, variants, datasets and cases that set clientOptions.surface on the chatgpt client.
The ChatGPT desktop app's Codex surface is its own client, codex-app (ADR 0011). An option no longer changes which product a client tests: chatgpt always drives ChatGPT Work. The two clients drive the same app, with the same options, setup and Docker image, and they share the app's desktop lease, so a chatgpt variant and a codex-app variant never run at once. codex-app is not the codex client, which runs the Codex CLI.
- Configs and cases. Change
"client": "chatgpt"with"clientOptions": { "surface": "codex" }to"client": "codex-app", andmst/client/chatgpttomst/client/codex-app, and removesurface. Remove"surface": "chatgpt-work"too: it is whatchatgptdoes.surfaceon either client, on an eval config, a variant or a case, fails before any app starts, and the message names the client to use. The eval config and its variants fail when the config loads; a case fails when its dataset loads, before the run collects a trial. - Docker.
mst env setup docker --config <eval>builds the ChatGPT image for acodex-appeval, and--env dockerruns its shards in that image.chatgptVersionapplies to it. - Results. A
codex-appcase recordsrequest.clientascodex-app. Its errors name the Codex app, for exampleCodex app desktop is locked by another run.mst clientslistscodex-app. - Stored runs. A run stored before this change keeps
client: "chatgpt"andclientOptions: { "surface": "codex" }in its files, and MST doesn't rewrite them. Everything that reads a stored run reads such a variant ascodex-app, withoutsurfacein its options, and that variant's case results ascodex-app:mst grade,mst run --resume,mst label,mst judges check, the reportmst openbuilds, the previous-run comparison,storedRunTraces, and a tool optimization started before the change, whose rounds oncodex-appstill match its baseline. A storedchatgptwithoutsurface: "codex"stayschatgpt, and a storedsurface: "chatgpt-work"is dropped the same way. A shard's machine records only the client's name: it reads ascodex-appwhen everychatgptvariant of its run was the Codex surface. A report built before the change still showschatgptuntil it is built again. - Telemetry. OTLP spans from a
codex-apprun saycodex-app: the span isinvoke_agent codex-app, andmst.clientandgen_ai.agent.namearecodex-app. A collector query that filters onchatgptfor Codex runs matches only spans exported before the change. - Batches. Changing a config from
chatgptwithsurface: "codex"tocodex-appchanges itscontentHash, somst batchruns that eval again, as it does after any config change.
The mst client is now api
Affects: eval configs, variants, datasets and cases that name the mst client, and code that passes client: 'mst' to runEvalDataset or runEvalCase.
The built-in client that runs the model alone, through its provider's API, with the servers' tools, is api (ADR 0011). mst named the tool, not what the client is. The mst CLI, the mst/ plugin namespace and the client's options don't change.
- Configs and cases. Change
"client": "mst"to"client": "api", andmst/client/msttomst/client/api. An eval config, a variant or a case that namesmstfails withClient "mst" is now "api"., and so doesrunEvalDatasetorrunEvalCasegivenclient: 'mst'. The older namesvercel-sdkandanthropic-apinow nameapi. - Results. A case that names no client in a Playwright test records
request.clientasapi, notmst.mst clientslistsapi. - Stored runs. A run stored before this change keeps
client: "mst"in its files, and MST doesn't rewrite them. Everything that reads a stored run reads it asapi:mst grade,mst run --resume,mst label,mst judges check, the reportmst openbuilds, the previous-run comparison,storedRunTraces, and a tool optimization started before the change, whose rounds onapistill match its baseline. A report built before the change still showsmstuntil it is built again. - Telemetry. OTLP spans from the client say
api: the span isinvoke_agent api, andmst.clientandgen_ai.agent.nameareapi. A collector query that filters onmstmatches only spans exported before the change, so match both names across older data. - Batches. Changing a config from
msttoapichanges itscontentHash, somst batchruns that eval again, as it does after any config change.
Plugin clients' description and requirements are checked
Affects: plugin clients that already set a description or requirements field of their own.
A client definition can now say what it is (description) and what a machine needs to run it (requirements), and mst clients prints both. MST checks their shape when it loads a plugin, and a plugin whose client gets one wrong fails to load, with the reason:
descriptionmust be a string.requirementsmust be an object with onlyos,install,credentialsandsetup.osmust be a non-empty list ofmacosandlinux. Leave it out for a client that runs on any OS.install,credentialsandsetupmust be lists of strings.
Before, MST ignored both fields. A client that used either name for something else renames it, or changes it to this shape.
MST redacts every client's result
Affects: plugin clients that return telemetry, code that reads clientTelemetry, and ChatGPT runs.
- Secrets. MST replaces each secret it knows in everything a client returns, and in the error its
runthrows, before it grades or stores them. Before, it redacted onlyerrorand an infrastructurereason. Since the answer and tool calls it grades are redacted too, MST now replaces only values it can tell are secrets: the credentials it uses, and values whose names say they are credentials, from 8 characters. A name says so when one of its words, split at_,-,., camelCase (an acronym too) and the digits that end it, istoken,key,apikey,secret,password,passwd,auth,authorization,cookie,credential,credentialsorpat(ACME_API_KEY,GITHUB_PAT,Authorization,APIToken,GITHUB_TOKEN2; notGIT_AUTHOR_NAMEorAUTH0_DOMAIN). That covers environment variables, secrets file entries, server headers and stdio servers'enventries alike. Before, every secrets file value and every header value counted, and named environment values counted from 4 characters. Rename a credential stored under another name, such asACME_PINorGITHUBTOKEN, so MST replaces it. - Telemetry content. Stored results drop
clientTelemetry.contentunless they keep full traces (redactStoredResponses: false). Put text that may quote the prompt, the model or the server there. - ChatGPT. A trial's
conversationHistorymoves fromclientTelemetry.conversationHistorytoclientTelemetry.content.conversationHistory, so a redacted run no longer stores the prompt and the answer in it.
Clients say how an attempt ended
Affects: plugin clients that return an error, runs where a client errored, and code that reads ending or infrastructureError.
MST no longer decides from an error's words whether an attempt was an infrastructure failure. The client that ended the attempt says so (ADR 0010).
- Plugin clients. An
errorthat says429,503,ECONNRESETor "prompt is too long" used to make an infrastructure failure. Now anerroralone is a trial that failed. To mark one, returnending: { kind: 'infrastructure', broke, retryable, reason }, or setdiagnostics.failureKind. A thrown error is a network failure only when it, or itscause, has a Node networkcode. See Clients. - The
mstclient. A provider's rate limit, 5xx or overload is a retryableproviderfailure, and a failed connection anetworkone. Before, only a429or503was. A key the provider rejects, a model it doesn't serve, or a missing provider package is asetupfailure a retry can't help: the case is incomplete, andmst run --resumesays it needs a fix. Before, these were failed trials. - Context overflow. A prompt that doesn't fit before the model acts is still
setup, not retryable. One that fills the context after the model acted is now a trial that failed; before, it wassetuptoo. Claude Code, Codex and themstclient each read their own provider's errors to tell them apart. - Claude Code and Codex. A rate limit or other API error they report is
provider. Claude Code's turn or budget limit is a trial. So is either client running out of the case'stimeoutafter it started (Claude Code'sinit, Codex's first turn), its own time; before, that was a retryabledeadlinefailure thatmst run --resumecollected again. A timeout before it started is still adeadline. ItsfailureKindis stilltimeout. - Cowork. A Computer Use submission that stopped for a permission, a sign-in or a model setting the desktop needs is
setup, not retryable. One the planner's provider limited isprovider, and one MST's deadline stopped isdeadline. Before, every failed submission was a retryableclient-appfailure. - Old runs. A stored trial stays an infrastructure failure or a trial, as it was recorded, and its failure kind says what broke. One with no failure kind is a retryable
client-appfailure, whatever its error says, and so is a Cowork workspace failure stored beforeending.
A case short a trial is incomplete
Affects: code that reads a case's pass, a run's passed, failed, total or pass rates, isInfrastructureError or infrastructureError, and runs resumed with mst run --resume.
Each attempt at a case now ends as a trial or an infrastructure failure, and the trial records it as ending (ADR 0010).
- Incomplete cases. A case with a trial that ended in an infrastructure failure, or with one its graders couldn't score, is incomplete. It neither passes nor fails:
passisfalse, it's counted inincomplete, and it's left out ofpassed,failedand pass rates, which counted it before. A case with every trial lost to infrastructure counted as failed; it's incomplete too. total.runEvalDataset's result and each variant'sresultstill count every case intotal, soexpect(result.passed).toBe(result.total)fails while a case is incomplete. A run summary'smetrics.totalcounts complete cases only, as it did for cases with missing trials.mst runandmst batchexit 1 while a case is incomplete.- Ungraded trials aren't infrastructure failures. A trial a judge couldn't score has
infrastructureError: falseandending: { "kind": "trial" }. It's still left out of pass rates, andmst grade --resumegrades it. --resumecollects more. It collects every infrastructure failure a retry can help, not just missing and unsubmitted trials. One a retry can't help is kept, and the run says it needs a fix.- Old runs. A run stored without
endinggets one from its old fields when it's read. Its trials keep their classification, except ungraded ones.
Tool optimizations on an eval are stored
Affects: code that calls runToolOptimization({ evalConfig }), or reads the eval's latest.json after it.
An optimization on an eval config is now stored in the eval's optimizations/<id>/, as mst optimize stores it (ADR 0008), so these things change:
- The baseline runs on its own. Static
variantsused to run in one run with the baseline. Now the baseline is one run and each round another, so the baseline's metric is checked before any candidate runs. - Its runs don't become the latest. Each run's
run.jsonhasoptimization(its id and role), andlatest.jsonkeeps pointing at your last ordinarymst run. Open an optimization's runs by id, withmst open <run-id>. - Candidates are checked first. A tool key that names no served tool, a change to an input schema beyond its
descriptionandtitletext, or a repeated id fails before the round runs. So does a server whose tools changed since the baseline, or a change to the datasets, the eval config's judges or the base variant's client, model or options. proposeVariantssees the stored evidence. The callback'shistoryis read from the runs on disk, as it is when the optimization is opened again. When the eval redacts stored responses (the default), its trials have no responses, final answers, tool call outputs or judgemetadata; their tool calls and arguments, and each judge's score, pass and reasoning, stay. Set"redactStoredResponses": falsein the eval config for a callback that reads them.- Its runs aren't resumed.
mst run --resumerefuses an optimization's run, because resumed trials would run against tools nobody checked. A round whose run is missing trials is run again (mst optimize try --retry).
maxVariants caps the candidates across rounds on both forms, and evalConfig takes env and envOptions. See optimize.
The Docker environment picks its image and container by client
Affects: runs with --env docker, or a plugin environment built on it (such as cloud VMs), whose client isn't Cowork; and plugins whose environment builds MST's image.
- ChatGPT evals get their own image.
mst env setup dockerwith a ChatGPT eval ("client": "chatgpt", directly or throughextends) now buildsmst-chatgpt-linux:<MST version>fromdocker/chatgpt-linux, and its shards and grade shards run in it. Before, setup built the Cowork image even then. See docs/chatgpt-docker.md. - Headless clients start without the desktop. A collect shard whose clients are all headless (
mst,claude-code, the newcodexclient, or a plugin client withheadless: trueon itsClientDefinition) runs in a container of the Cowork image without Claude Desktop, VM devices or Cowork inference, as grade shards do.ANTHROPIC_*settings are no longer needed for those runs, and several such shards share one Docker context. Before, every shard booted Claude Desktop. Put the client's own credentials in the run's--secrets-file. - Environment hooks get the client. An environment's
open,setupandcoordinatorcontext hasclient, the eval's client as itsextendsresolve it. A plugin environment that builds or picks an image passes it on to thedockerenvironment. - A detached run's
filedatasets. The coordinator no longer looks for afiledataset's path on its own machine; it uses the datasets shipped with the run. Before, a relative path failed there and the run stayedstartinguntil it waslost.
See docs/cowork-docker.md and docs/codex.md.
The dry-run proxy tries its upstream again at startup
Affects: connectors whose launch returns dryRunProxyServer().
A dry-run proxy that can't reach its upstream server when it starts, or can't read its token file yet, now tries again after 2 and 5 seconds before it exits, and logs each failure's kind to stderr. Before, it exited at once and its client saw only "connection closed". A client that gives up on a server sooner than about 10 seconds still sees the failure. See Dry-run proxy.
Cowork on the Linux desktop reaches public hosts
Affects: Cowork evals on MST's Linux desktop (the Docker image and cloud VMs).
Cowork's web fetch, and the commands it runs in its VM, can now reach any public host. MST writes Claude Desktop's coworkEgressAllowedHosts as ["*"] unless the image or the eval names hosts. Before, the image set no list, so every fetch failed ("not on the network allowlist"), even of pages a prompt linked. Results on cases that fetch web pages can change.
It's one list, by host, for both web fetch and the VM's commands, so it can't allow reads only. Private and local addresses stay blocked. MCP servers aren't affected: their writes still go through the dry-run proxy.
- To keep the old behavior, set
"coworkSetup": { "egressAllowedHosts": [] }in the eval or a variant. - To allow only some hosts, list them:
["docs.example.com"]. - To turn off a Cowork built-in tool, set
coworkSetup.disabledBuiltinTools. For example,["WebSearch"]through an LLM gateway whose providers can't run Anthropic's server-side web search, where every search returns a 400.
macOS Computer Use needs no Python
Affects: Cowork and ChatGPT runs on macOS, and code or scripts that used the ./cowork-runtime or ./cowork-requirements exports, MST_COWORK_PYTHON on macOS, or MST_COWORK_DRIVER_ROOT.
The macOS Computer Use driver is now TypeScript and runs in the MST process. MST no longer creates a Python environment or installs anthropic, pyautogui, mss, Pillow or pyobjc. For mouse and keyboard input it compiles a small Swift helper, which needs the Xcode Command Line Tools (xcode-select --install), as MST's other macOS helpers already do. The terminal that runs MST still needs Accessibility and Screen Recording permission.
- The package exports
./cowork-runtime(the Python driver) and./cowork-requirementsare gone. Nothing replaces them: the driver is part of MST's code. MST_COWORK_PYTHONno longer applies on macOS. It still selects the interpreter for the Linux driver.MST_COWORK_DRIVER_ROOTis gone. It pointed MST at a checkout's Python driver.- A failed run's error now says
stopped before completion; not retryingordeadline reached before completion; not retrying, in place ofdriver exited unsuccessfullyanddriver terminated before completion. A blocker or provider code, such as(model_unavailable), still follows. - A
scrollto the left or right now scrolls sideways. The Python driver scrolled up. - A key name the driver doesn't know is reported to the planner and not sent. The Python driver ignored it without saying so.
Result stores hold whole runs
Affects: eval configs with results.store, plugins with resultStores, and code that read an eval's store with loadStoredEvalRunnerResult or listArtifacts.
A result store set in an eval config, or chosen with the new mst run --results <store>, now gets a copy of each run directory: <eval name>/runs/<run-id>/ and <eval name>/latest.json, the same layout as .mcp-test-results/. It no longer gets eval-runs/ and eval-summaries/ artifacts, and the run summary no longer has caseArtifactPointers. The previous run and mst batch --skip-existing read the store's runs. Runs stored by earlier prereleases aren't read; the first run after the upgrade has no previous run to compare with.
A file store now holds every file of each run, results.json and summary.json included. If its dir is inside the directory MST writes runs to (such as mst batch --output-root), each run is there twice: once where MST ran it and once under <eval name>/runs/. A script that collects runs by searching that directory for results.json finds every run twice. Point the store at a directory of its own, or drop it when the local run directory is all you need.
A plugin's result store returns a RunStore (location, put, get, list) from create, not an artifact store:
// Before
resultStores: { bucket: { schema, create: (options) => new MyArtifactStore(options) } }
// After: MST owns the layout; the store moves bytes
resultStores: {
bucket: {
schema,
create: (options) => ({
location: `s3://${options.bucket}`,
put: (key, body) => putObject(options.bucket, key, body),
get: (key) => getObjectOrNull(options.bucket, key),
list: (prefix, { shallow } = {}) => listKeys(options.bucket, prefix, shallow),
}),
},
}
A store that still returns saveArtifact fails with what to change. The artifact stores runEvalDataset and the Playwright reporter take (resultStore, FileEvalResultStore, GCSEvalResultStore) are unchanged.
Linux Cowork settings use Claude Desktop's own keys
Affects: Linux Cowork desktops prepared by the caller (/etc/claude-desktop/managed-settings.json), and callers of coworkManagedPluginSettings.
Claude Desktop ignores its whole managed settings file when the file has a key it doesn't know, and allowedMcpServers and allowManagedMcpServersOnly are Claude Code settings, not Claude Desktop's. A file with them lost its inference and MCP settings too. MST no longer writes or expects them.
A prepared desktop's settings must now set "isLocalDevMcpEnabled": false and "isDesktopExtensionEnabled": false instead (spread COWORK_MANAGED_ONLY from ./experimental/clients), or preparation fails with "Prepared Linux desktop settings do not match". coworkManagedPluginSettings no longer returns allowedMcpServers. Owned-desktop mode (MST_DESKTOP_OWNED=1) writes the new keys, and drops the old ones from an image's own settings.
Claude Desktop also reads the file only when root owns it and no one else can write it. An owned-desktop image now stages the file for MST (MST_COWORK_SETTINGS_FILE) and installs a root-owned copy in its restart command, which MST now gives 5 minutes instead of 2.
Headless Cowork disables AskUserQuestion
Affects: Linux Cowork desktops prepared by the caller (/etc/claude-desktop/managed-settings.json).
A headless run has nobody to answer a clarifying question, so Cowork evals now disable Claude's AskUserQuestion tool and the model proceeds on its best assumption. A case that asks anyway still stops as a failed case (awaitingUser).
A prepared Linux desktop's managed settings must now include "disabledBuiltinTools": ["AskUserQuestion"] (spread coworkHeadlessSettings() from ./experimental/clients), or preparation fails with "Prepared Linux desktop settings do not match". Disabling more tools is fine. Owned-desktop mode (MST_DESKTOP_OWNED=1) and the macOS eval profile MST writes set it for you.
Cowork takes tool variants
Affects: manifests that give cowork toolOverrides, and plugin clients with runBatch.
Cowork now gets an arm's toolOverrides through MST's tool-variant proxy, so a tool optimization (runVariantExperiment on a suite) can run on Cowork. Before, validation rejected it. ChatGPT still opts out.
For plugin clients:
serversPerBatch: truesays the client connects to one server set for the whole batch, the first request'sinput.servers. The batch then shares one proxy endpoint, and the check that the client listed the variant's tools is for the batch.- A proxied request carries
input.checkServers, the same servers on an endpoint for the client's own checks (a readiness probe, say), so that traffic isn't taken for the model seeing the variant.
Cowork and ChatGPT options aren't nested
Affects: manifests that use cowork or chatgpt with clientOptions.options.
Their options go directly in clientOptions, like every other client's. A nested options object fails with a message saying so.
// Before
{ "client": "cowork", "clientOptions": { "options": { "appVersion": "1.52386.6" } } }
// Now
{ "client": "cowork", "clientOptions": { "appVersion": "1.52386.6" } }
The client, its model and its options are separate keys
Affects: every manifest, arm and case that sets host, and shared configs (configs in a plugin).
host is now three keys: client names the client under test, model the model it uses, and clientOptions the client's other options. A config that still sets host fails with a message naming them.
// Before
{ "host": { "type": "cowork", "model": "claude-sonnet-4-6", "timeout": 900000 } }
// Now
{ "client": "cowork", "model": "claude-sonnet-4-6", "clientOptions": { "timeout": 900000 } }
- Arms and cases take the same three keys. An arm that sets only
modelkeeps the manifest's client and options; one that sets a differentclientdoesn't inherit the manifest'sclientOptions. - The manifest's
modelis a default for every arm and case, as before. clientOptionscan't settype,clientormodel; each has its own key.- Shared configs. A manifest that sets
clientorclientOptionsreplaces a shared config's client, so one client's options never apply to another. - Results. A validated manifest (
validateManifest) holds each level's resolved client in the same three keys. Result fields are unchanged in this release.
Tool optimization reports a comparison
Affects: code that reads runVariantExperiment results or the reporter's mcp-tool-optimization attachment.
With testInfo, runVariantExperiment also attaches a case-by-case comparison, and the reporter opens on a Comparison tab. Each candidate gains measurement and improvement, whose paired changes carry an assessment of 'better', 'worse' or 'unclear'. Case groups are capability and regression cases (measurement.capability, measurement.regression). The result says where the groups came from in grouping ('declared' from the regression tag, or 'grouping-run', with the extra run in groupingBaseline). On a suite, an arm metric such as input_tokens_mean has no per-case scores, so its improvement isn't tested; a variant that improves it is recommended unless it breaks regression cases.
Clients have canonical names
Affects: manifests and datasets that name a built-in host, plugins that contribute hosts, HostDefinition and the other Host* types, and imports from ./experimental/hosts.
What an eval tests is an MCP client with a model (ADR 0002). Built-in clients have canonical names, and how MST drives one isn't part of its name. An old name fails with a message naming its replacement, for example Client "claude-cli" is now "claude-code".; the 2.0 deprecated aliases (cowork_cu and the long driver names) fail the same way.
| Before | Now |
|---|---|
vercel-sdk, anthropic-api | mst |
claude-cli | claude-code |
chatgpt-mac, chatgpt-linux | chatgpt (the platform picks the driver) |
cowork | cowork |
anthropic-api was a second implementation of what mst does, so it's removed. mst infers the API from the model id (claude-*, gpt-*, gemini-*, claude-*@date for Vertex); set provider to override it.
For plugins and TypeScript:
- A plugin contributes clients under
clients(washosts), and a plugin withhostsfails validation namingclients. - Types:
HostDefinition→ClientDefinition,HostRunInput/HostRunContext/HostRunResult/HostRunOptions/HostBatchRequest→ClientRun*/ClientBatchRequest,HostConfig→ClientConfig,HostEvent→TraceEvent,HostEvidence→TraceEvidence,HostTrace→Trace,HostPlugin→MarketplacePlugin. @gleanwork/mcp-server-tester/experimental/hostsis now/experimental/clients.
The config key is client; see The client, its model and its options are separate keys.
Cases and run controls use common eval terms
Affects: every dataset and manifest, runEvalDataset and runVariantExperiment options, CLI host args, and plugin hosts.
MST now uses the vocabulary other eval frameworks share (ADR 0002). Old keys fail validation with a message naming the replacement, for example case "a" iterations: `iterations` is now `trials` ; none of them is an alias.
| Before | Now | Where |
|---|---|---|
scenario | input | case |
iterations | trials | case, manifest, run |
accuracyThreshold | passThreshold | case, manifest, run |
expect | assertions | case |
canonicalAnswer | expected.answer | case |
scenarioTemplate with {{scenario}} | inputTemplate with {{input}} | manifest, arm |
{{scenario}} | {{prompt}} | CLI host args (mcpHostConfig.cli.args) |
defaultLlmIterations, defaultAccuracyThreshold | defaultTrials, defaultPassThreshold | runEvalDataset and runVariantExperiment options |
EvalExpectBlock, EvalExpectBlockSchema | EvalAssertions, EvalAssertionsSchema | types |
Plugin hosts receive the case's input as input.prompt (was input.scenario). A batch request numbers its trial as request.trial (was request.iteration). Judges are unchanged: they already read case.input.prompt and case.expected.answer.
A manifest that used a renamed key has a new contentHash once it's updated, so runEvalBatch doesn't resume a run saved before the change. Result fields (iterations, assertionPassRate and the request's scenario and expect) are unchanged in this release.
Cowork runs the installed Claude Desktop unless the manifest pins one
Affects: macOS Cowork runs, and anyone who set MST_COWORK_APP_VERSION.
MST no longer downloads a built-in Claude Desktop version (1.52386.6) for each run. It runs the installed /Applications/Claude.app and records its version with every case result as hostTelemetry.hostApp (now clientTelemetry.clientApp; { name, version, source }). To run an exact version, set clientOptions.appVersion in the manifest (now the eval config); MST downloads and verifies it as before, and records source: 'pinned'. MST_COWORK_APP_VERSION now fails with a message pointing to appVersion. coworkSetup.approveWriteTools no longer requires 1.52386.6. Suite results also record MST's own packageVersion. See Which Claude Desktop runs.
The Claude CLI host runs isolated from your Claude Code setup
Affects: claude-cli hosts.
The host used your own Claude Code configuration, so your skills, plugins and settings shaped what the model did, and added to its input tokens. It now gives each run an empty CLAUDE_CONFIG_DIR, so your settings.json (its env block, apiKeyHelper) doesn't apply either. Authenticate through the environment (provider: 'vertex', or an Anthropic API key), or set isolate: false on the host (now clientOptions.isolate on the claude-code client) to use your own configuration, for example to sign in with a claude.ai account. See Claude Code isolation.
Variant experiments run on suites
Affects: runVariantExperiment result types (see below); otherwise new.
runVariantExperiment({ suite: { manifestPath }, variants }) runs tool variants as arms of a manifest, so experiments reach any host that takes tool variants, not only the SDK host, and can optimize any numeric arm metric (better: 'lower' for tokens or cost). The dataset form, runVariantExperiment(options, context), runs as before, except that metric on its results is typed as a string (any arm metric) rather than ExperimentMetric, and a candidate with no value for the metric is disqualified (metricUnavailable: true) instead of scoring as the baseline. runEvalSuite gains arms, which replaces the manifest's arms with a list or with a function of them.
Internals leave the root
Affects: code that imports BUILTIN_RESULT_SCHEMAS, getResponseSizeBytes, SKILL_LIMITS or skillDigest from @gleanwork/mcp-server-tester.
These were internals:
BUILTIN_RESULT_SCHEMASis the expectation engine's table for namedschemaexpectations.getResponseSizeBytesis howvalidateSizemeasures a response; check a size withvalidateSizeortoHaveToolResponseSize.SKILL_LIMITSandskillDigestare the skills client's size limits and digest;mcp.skillsand the skills conformance checks apply them.
Desktop hosts have one naming style
Affects: manifests whose host is chatgpt, cowork_cu, openai.chatgpt.agent.desktop-app.macos, openai.chatgpt.agent.desktop-app.linux or anthropic.claude.cowork.desktop-app.macos.
Built-in host names are kebab-case: vercel-sdk, anthropic-api, claude-cli, cowork, chatgpt-mac and chatgpt-linux. The earlier names still work and print a deprecation warning (MST_DEPRECATED_HOST) naming the replacement:
| Earlier name | Use |
|---|---|
cowork_cu | cowork |
anthropic.claude.cowork.desktop-app.macos | cowork |
openai.chatgpt.agent.desktop-app.macos | chatgpt-mac |
openai.chatgpt.agent.desktop-app.linux | chatgpt-linux |
chatgpt (the host for the local platform) | chatgpt-mac or chatgpt-linux |
The long names remain the external-host driver identities recorded in results; only the host type changes.
The Claude Agent SDK is an optional peer dependency
Affects: the anthropic-agent-sdk judge provider.
@anthropic-ai/claude-agent-sdk (about 46 MB) was a dependency, installed with MST whether or not a judge used it. It is now an optional peer dependency, loaded only when an anthropic-agent-sdk judge runs. If you use that provider, install it:
npm install --save-dev @anthropic-ai/claude-agent-sdk
Without it, the judge fails with an error that names the package. The other judge providers already worked this way.
Plugins are objects
Affects: every plugin; code that calls registerJudge or the register*, get*, list* and clear* registry functions; METRIC_REGISTRY; loadPlugins; and manifests and datasets that reference plugin extensions.
Plugins now use ESLint's model (Plugins, ADR-0001). A plugin is a plain object that MST reads, instead of a module whose hook calls MST's register functions.
// Before (2.0.0-beta.7)
import { registerJudge } from '@gleanwork/mcp-server-tester';
import { registerDatasetSource } from '@gleanwork/mcp-server-tester/evals';
export function register() {
registerJudge({ name: 'completeness', schema, evaluate });
registerDatasetSource({ name: 'legacy', schema, load });
}
// After
import type { Plugin } from '@gleanwork/mcp-server-tester';
export default {
meta: { name: '@acme/mst-plugin', version: '1.0.0', namespace: 'acme' },
judges: { completeness: { schema, evaluate } },
datasetSources: { legacy: { schema, load } },
} satisfies Plugin;
- Default-export a plugin object. The loader no longer calls
register,registerPlugins,registerGleanJudgesor a default-exported function. A module that still exports one fails with a message pointing here. - Extension definitions lose
name. The map key is the name. - Reference plugin extensions as
namespace/name:{ "type": "acme/legacy" }in manifests, and{ "judge": "acme/completeness" }in datasets andtoPassToolJudge. Bare names belong to built-ins, so a plugin can no longer register a barecorrectnessor replacefile. - A manifest must load the plugins it references. Referencing
acme/...withoutacmein that manifest'splugins(or--plugins, or thepluginsoption) is an error, even if another manifest in the same batch loaded it. --pluginsand thepluginPathsoption add to the manifest'spluginsinstead of replacing them.- Manifest plugin paths resolve relative to the manifest's directory, then
rootDir(--root-dir, the working directory by default), then as package names, resolved asimportresolves them (exports conditions, includingmodule-sync, and subpath patterns), so a package subpath must be a file or anexportsentry. Before, they resolved againstrootDironly. - Plugins are validated when they load. Unknown top-level keys are rejected, every extension needs a Zod
schemaand its kind's function (load,run/runBatch/createConfig,evaluate,compute, orcreate), and a different plugin claiming a loaded namespace throws. An oldregisterJudge(name, executor)judge becomes{ schema: z.object({}).passthrough(), evaluate: executor }. - Datasets are checked too. A case's
clientand itsjudgesmust name a built-in or a namespace the suite loads. - Plugins can share manifest settings.
configsholds named manifest fragments (PluginConfig) that a manifest applies with"extends": ["namespace/name"]. See Plugins. getBuiltinHostConfigresolves built-in hosts only.validateManifestRegistrationsis nowvalidateManifest(from./evals).- Code passes plugin objects where it used to call register functions:
runEvalSuite({ ..., plugins }),runEvalBatch({ ..., plugins }),runEvalDataset({ dataset, plugins }, ctx),runEvalCase(evalCase, ctx, { plugins }), ortest.use({ mcpPlugins: [plugin] })in Playwright. Code that callsvalidateJudgeor the matchers outside those usesinstallPlugins([plugin]), from the package root. For a one-off judge, wrap it in a small local plugin, such as{ meta: { name: 'local', namespace: 'local' }, judges: { x } }. - Removed:
- From the root:
registerJudge(both forms) and theCustomJudgeExecutorandCustomJudgeResulttypes. A plugin judge'sevaluatetakes the same arguments as the old executor; it also needs aschema. - From
./evals:registerDatasetSource,registerHost,registerMetric,registerResultStore, and theirget*,list*andclear*counterparts;METRIC_REGISTRY; andloadPlugins,loadPluginModule,EvalPluginModuleandLoadPluginsOptions. - The metric-record argument of
resolveMetricandcomputeMetrics. Metrics resolve from built-ins and loaded plugins only, andBUILT_IN_METRICSis read-only.
- From the root:
- The
glean-legacydataset source example was removed. It encoded one organization's legacy dataset schema and judge policy, which belongs in that organization's plugin, not in MST. Copy it from thev2.0.0-beta.7tag (examples/plugins/legacy-glean-datasets.ts) if you need it, and convert it to a plugin object as above. Dataset sources shows a dataset-source plugin.
A judge's schema sees only its own options
Affects: plugin judges whose schema reads type, name, threshold, reference or reps, mainly judges listed in a manifest's judges.
A manifest judge's schema used to receive the whole manifest entry, so its options included type, reference and the case's settings. It now gets only its own options on every path. Read the reference from case.expected.answer in evaluate's input. A case judge now also passes provider, model and the other LLM fields to a named judge's schema; beta.7 dropped them. A custom-text rubric's metrics key is now rubric, not judge. Two manifest judges of the same type (two rubric judges, say) stay separate, and a case entry overrides the one whose results carry the same name. The built-in rubric judge can also be listed: judges: [{ "type": "rubric", "rubric": "correctness" }].
A judge's evaluate takes ({ case, trial }, options)
Affects: every plugin judge.
// Before
evaluate: async (candidate, reference, options) => grade(candidate, reference);
// After
evaluate: async ({ case: c, trial }, options) =>
grade(trial.response, c.expected.answer);
candidate is trial.response (its text is trial.text) and reference is case.expected.answer. The input also has the case's prompt or tool call, expected.criteria, tags and metadata, and the run's tool events, messages and evidence. A judge may declare requires paths, and may return pass, skipped, subScores, usage and metadata. A dataset case may set expected (answer, criteria, other keys); expected.answer overrides canonicalAnswer. A score outside 0 to 1 is now an error. See Judge contract.
Hosts take a system prompt
Affects: manifests that compare instructions, such as an organisation's, across arms.
vercel-sdk, anthropic-api and claude-cli accept systemPrompt, as does mcpHostConfig.systemPrompt (a CLI host through a {{systemPrompt}} placeholder in cli.args); see System prompts. anthropic-api used to reject it as an unknown option. Hosts that can't apply one still reject it, and a suite rejects a case prompt that would replace the arm's.
Claude Code host tools are typed events
Affects: expectations and metrics on Claude CLI or Cowork runs that name Skill, SlashCommand, Task, Agent or ToolSearch as tool calls; plugin hosts that report tool searches.
The Claude CLI and Cowork parsers used to record these as host tool calls. They are now typed events: Skill is a skill event named for the skill, SlashCommand a command, Task and Agent a subagent, and ToolSearch a new tool_search event with the tools it returned in results. See Claude Code host-native events.
- Expectations: match a skill with
{ "name": "triage", "kind": "skill" }, not{ "name": "Skill" }.toolsTriggeredcalls acceptkind: "tool_search". - Counts: these events are no longer tool calls, so they leave
tool_count,first_tool,is_no_action(a trial that only searched or loaded a skill is now no action), thetoolCallCountexpectation and run telemetry'stoolCalls. They still count inhost_event_count. Every arm reports the newtool_search_hit_ratewhen its host searched. - Plugin hosts that search a tool catalog should report
{ kind: 'tool_search', source: 'host', name, results }.
Tool variants reach plugin hosts and the Claude CLI
Affects: plugin hosts with run or runBatch, and manifests that give claude-cli or such a host toolOverrides.
These hosts used to fail validation with toolOverrides. Now the suite serves them the arm's servers through a local MCP proxy that applies the variant: each host request gets http configs for its own loopback Streamable HTTP endpoints, one per server, with the same labels, so the host must speak Streamable HTTP. See Tool metadata on every client.
- A host request that never lists the proxied tools fails, with an error naming the variant.
- Proxied hosts don't see the variant in
context: the proxy applies it. A host that applies variants itself must declaretoolOverrides: true, as before. HostDefinition.toolSurfaceProxy: falseopts a host out, for one that connects to servers other thaninput.servers; giving ittoolOverridesstays a validation error. The Cowork and ChatGPT desktop hosts opt out until they are verified with the proxy.
Tool variants: one rule, and renames
Affects: toolOverrides passed to runEvalDataset(), and error messages matched in tests.
Every host applies a tool variant through one tool surface, so a variant means the same thing on every host:
- Keys: a tool's name on its server, or
server.toolto pick one of several servers in a suite; a qualified key wins over a bare one.runEvalDataset()serves one unlabelled server, so it takes bare names, as before. - Renames: an override can set
name. Calls to the new name reach the original tool, and are recorded under the original name, so a dataset's expectations need no change between arms. The trace'srawNamekeeps the name the model used. - Errors: an unknown key is
toolOverrides variant "v" overrides unknown tool "x"., an ambiguous one... matches a tool on several servers; use "server.tool".(wasreferences unknown tool(s)/Unknown tool override/Ambiguous tool override). A rename that isn't a valid tool name, or takes a name another tool on the same server has (originally or by rename), is an error.
Plugin hosts that declare toolOverrides: true can apply variants with buildToolSurface() from ./evals, and should record a renamed tool's calls under its original name (resolve() gives it) with the model's name in rawName.
Comparison orchestrators are arms
Affects: code that calls runServerComparison(), saveServerComparison() or runSkillsComparison(), or stores or reads server-comparison artifacts.
All three are removed, along with their types and the server-comparison result-store kind:
- Server comparisons: run two suite arms with different
servers. Arms run one after another, and per-case wins come fromcompareEvalRuns()on the two arms' results. See Server comparisons are suite arms. - Skills comparisons: run arms that differ in the
vercel-sdkhost's newskillsoption (off,catalog,preload), withmetrics: ["skill_loaded", "skill_before_tool", "skill_verification_failed"]. A case can't also setmcpHostConfig.skills: the suite rejects it, since the case setting would override every arm's. See Measuring whether skills help.
runVariantExperiment(), which proposes and scores tool-description variants over rounds, stays.
Arms report every metric
Affects: code that reads arm metrics, armDeltas or unavailableMetrics.
- More metrics per arm. Every arm now reports tool counts, MCP calls and host events, tokens, cost, duration and judge results when the host or cases report them, whatever the manifest lists.
mcp_call_countandhost_event_countare new metrics. - Unavailable metrics.
unavailableMetricslists only metrics the manifest lists. - Deltas.
armDeltasgainmetricDeltas: the change in every numeric metric, keyed likemetrics. - Pricing. A manifest's
pricing(USD per million tokens, by model) estimates cost for hosts that report tokens but not cost. Estimates areestimatedCostUsdin usage, kept apart fromtotalCostUsd; arms report theircostSource, the prices they used, andunpricedModels. See Pricing. - Shared configs can also set
pricing,accuracyThreshold,temperatureandmaxTokens.
Manifest paths are relative to the manifest
Affects: suites run from another directory than their manifest's, and file result stores with a relative dir.
- Datasets. Dataset paths resolve against the manifest's directory first, then
rootDir(--root-dir, the working directory by default), as plugin paths already did; they used to resolve againstrootDironly. If both directories have the file, the one next to the manifest now wins. - Stores. A
filestore's relativediris relative to the manifest. It used to be relative to the process's working directory. - Dataset sources get
manifestDirin their context. - Comparison with the previous run. Each run summary has a
runIdand, when the manifest ran the same arms before, apreviousRuncomparing the two, arm by arm. With a result store, the previous run is the newest summary of the same manifest, not the store'slatest(which any manifest may have written). See Compared with the previous run.
Settings a run would ignore are errors
Affects:
- manifests and datasets with keys MST doesn't define
- manifests with
toolOverridesor aconcurrencythe host can't honour anthropic-apihosts with options it doesn't useclaude-cliarms whose servers set connection policy- arms or cases that switch host type
- manifest-level
temperature,maxTokensorprovider
The changes:
- Manifests are strict. An unknown top-level key is an error; it used to be ignored.
- Newly declared:
accuracyThreshold,filterTags,run,temperature,maxTokens,redactStoredResponsesand$schemawere read or allowed without being declared, and are now part of the schema. accuracyThreshold: a default for host cases that don't set their own, at the top level or asrun.accuracyThreshold.- Editor schema:
schema/eval-manifest.schema.jsonnow declares every key, includingextendsandcoworkSetup, and rejects the rest.
- Newly declared:
- Datasets are strict. An unknown key in a dataset, a case, or an
expectblock (including insideresponseSize,toolsTriggeredcalls andtoolCallCount) is an error, and so is an unknown option on a rubric judge.- Examples:
regex(the key ismatchesPattern),judge(it's the case'sjudges), or a misspeltaccuracyThreshold. These used to be silently dropped. - Named judges are the exception: a plugin judge named by
typestill gets its own options. - Errors name the dataset and case, and suggest the right key for common mistakes.
- Examples:
- Hosts declare what they honour.
HostDefinitiongainstoolOverrides(it shows the model an arm's tool variants) andmaxConcurrency.- Tool variants:
toolOverridesfor a host without the flag is a validation error. MST'svercel-sdkandanthropic-apiset it, as does any host with onlycreateConfig, because MST's SDK host runs it. Cowork and plugin hosts used to run with the original tools while results recorded the variant;claude-cliand ChatGPT used to fail only once a case ran. A plugin host that applies variants itself, fromcontext.arm, setstoolOverrides: true. - Concurrency: ChatGPT declares
maxConcurrency: 1.
- Tool variants:
- Defaults reach only the hosts that take them. A manifest's
model,provider,maxToolCalls,timeout,temperatureormaxTokensfills in each host option its schema declares. A sharedproviderno longer reachesanthropic-api, which has none. A default that none of the run's hosts takes is an error. - A host inherits options only from the same host. An arm, or a case, that switches host type no longer gets the other host's options.
anthropic-apioptions are strict. It used to accept and ignore options such assystemPromptorprovider.- Checks run before anything does, including in
--dry-run.- Before execution:
claude-cliconnection settings it can't forward (OAuth, client credentials, TLS, proxies, retries, timeouts), case-level hosts, and host concurrency are all checked first. - Dry runs: a dry run now loads datasets, without resolving hosts or secrets.
- Before execution:
Suite metrics are per trial
Affects: suites with iterations above 1, code that reads cost_usd, first_tool or per-case metric values, code that reads arm summaries, and code that builds a MetricResult.
- Per-trial means. Arm metrics used to read the last iteration's trace and every iteration's summed usage, so with 10 iterations
tool_count_meancounted one trial andinput_tokens_meanwas ten times a trial's. Usage, timing, tool and answer metrics are now means over each case's trials. - No infrastructure failures. Runs that failed on infrastructure aren't trials, including a single run.
- Shares.
response_successandis_no_actionare shares of trials, so their per-case values are numbers, not booleans; their_ratekeys are unchanged. first_toolnames MCP toolsserver.tool, as multi-server arms already did.- New per-arm fields. Every arm now reports
passed_rateandtrial_pass_rate(the share of passing trials), andarmDeltasaddtrialPassRateandtrialPassRateDelta. Arms also reportevidence(the weakest among their cases) andunavailableMetrics, andMetricResulthasunavailable. - Cost. Hosts that don't know their cost no longer report 0: the Vercel AI SDK and
anthropic-apihosts, and CLI or Claude traces without a cost.cost_usdis unavailable for them rather than zero.
See Metrics.
External-host capabilities are built in
Affects: code that calls registerExternalHostCapability, datasets whose externalHost.capabilities bind uses: "module:<specifier>#<export>", and code that imports ExternalHostCapabilityImplementation or ExternalHostCapabilityContext.
The desktop drivers' capabilities are no longer an extension point. registerExternalHostCapability, ExternalHostCapabilityImplementation and ExternalHostCapabilityContext are removed from @gleanwork/mcp-server-tester/experimental/hosts, and uses no longer accepts module: ids. A binding can still choose among the built-in implementations (uses: "builtin:..."). A module: binding fails the case as an unsupported host, with an error that says module: capabilities were removed and points to Hosts.
To run a host MST doesn't ship, provide it from a plugin's hosts and select it in a manifest. See Hosts.
The Computer Use planner uses MST's LLM gateway settings
Affects: Cowork and macOS ChatGPT runs with ANTHROPIC_BASE_URL, ANTHROPIC_AUTH_TOKEN or MST_LLM_AUTH_COMMAND set, and Cowork on Macs whose managed preferences set the inference provider.
The Computer Use planner resolves its endpoint with the rules in LLM gateways. With ANTHROPIC_BASE_URL set, it sends the MST_LLM_AUTH_COMMAND token or ANTHROPIC_AUTH_TOKEN to that URL as a bearer token, or else ANTHROPIC_API_KEY. Without it, the planner calls the public Anthropic API with ANTHROPIC_API_KEY, and a gateway token is never sent there. In 2.0.0-beta.7 the Python SDK read these variables itself and could mix them; 2.0.0-beta.8 ignored them and always called the public API. If you export ANTHROPIC_BASE_URL for something else and want the planner on the public API, unset it for the run.
Cowork no longer requires ANTHROPIC_API_KEY when managed preferences set inferenceProvider: Claude Desktop takes inference from them, so MST stages no inference credential. Managed preferences may also set deploymentDisplayName. See Cowork inference.
Cowork on macOS continues after a failed case
Affects: Cowork batches on macOS.
After a submit, binding, HITL or trace failure, MST now resets Claude Desktop before the next case, as it already did on Linux: the Computer Use planner stops a task that is still running and declines an open permission prompt. The driver refuses typing, key presses and drags, and the planner is instructed never to click Send, approve or answer a question. Before, a submit or binding failure stopped the batch and the remaining cases were not submitted, while a HITL or trace failure let the next case start with the failed task possibly still running. If the reset fails, the remaining cases are not submitted. See Cowork.
Result fields use the eval vocabulary
Affects: code that reads results, stored artifacts, baselines or pairwise comparisons, and code that imports result, judge or pairwise types.
Results use client names, not host names (hostUsage → clientUsage, mcpHostTrace → toolCallTrace, externalHost → clientMetadata, request.scenario → request.input, and more), and record trials and scores (iterationResults → trialResults, expectations → scores, assertionPassRate → passRate). Judge and pairwise types follow: JudgeVerdict → JudgeScore, PairwiseVerdict → PairwisePreference, PairwiseCaseVerdict → PairwiseCasePreference, and pairwise cases[].verdicts → cases[].preferences. PairwisePreference now names what a pairwise judge returns; the 'baseline' | 'candidate' | 'tie' union it used to name is PreferredSide. Also ToolCallExpectation → ToolCallAssertion and reporter metrics.expectationBreakdown → metrics.graderBreakdown. Eval case results no longer carry toolName: 'mcp_host'. Stored results are schemaVersion: 2; loading an older one explicitly fails with what to do, and history skips it. See Result fields use the eval vocabulary.
Eval APIs and client contracts say eval, client and tool optimization
Affects: code that calls runEvalSuite or runVariantExperiment, client plugins, and code that reads tool-call sources, telemetry, failure kinds or the host_event_count metric.
runEvalSuite → runEval, runVariantExperiment → runToolOptimization (its suite option → evalConfig), and the *Suite*, *Host* and VariantExperiment* types follow. Client plugins get options.client (was host), and a completed executeCase returns kind: 'completed'. Tool calls the client ran itself have source: 'builtin' (was 'host'); host_event_count → builtin_event_count. See Eval APIs and client contracts say eval, client and tool optimization.
Plugin extension names say their kind
Affects: anything that names a plugin's extension.
acme/completeness is acme/judge/completeness: names are <namespace>/<kind>/<name>, and a two-part name fails with the full one. Built-ins may be written mst/<kind>/<name>. See Plugin extension names say their kind.
Servers are a map keyed by label
Affects: eval configs that set servers.
"servers": [{ "label": "acme", ... }] is "servers": { "acme": { ... } }, and a variant lists labels: "servers": ["acme"]. The old shapes fail with the new one. See Servers are a map keyed by label.
Narrowed runs are partial
Affects: code that reads run summaries, and scripts that pass --variant.
mst run takes --filter-tag <tags...> and --max-cases <n> beside --case and --trials, and --variant takes one or more names. A run narrowed by any of them has partial: true, its selection and a selectionHash in the summary. It's compared only with partial runs narrowed the same way, never becomes the result store's latest, and isn't resumed by --skip-existing. RunEvalOptions.variant takes a name or a list, plus filterTags and maxCases.
Case judges sit beside assertions
Affects: datasets whose cases set assertions.passesJudge, and eval configs that want pairwise judges.
A case's judges are a judges list beside assertions, written like an eval config's judges: "acme/judge/completeness" or { "type": "rubric", "rubric": "correctness", "threshold": 0.8 }. assertions.passesJudge fails loading, naming judges. The merge with the eval config's judges is unchanged, and so are judge scores; a result echoes the judges as request.judges. An eval config's top-level pairwiseJudges (also allowed in shared configs) compares each variant with the baseline after the run and records the result as the variant's pairwise; pairwise usage is in telemetry.totalJudgeUsage and, alone, telemetry.pairwiseJudgeUsage. See Case judges sit beside assertions.
Runs are directories in the mst.run/v1 format
Affects: anything that reads mst run output, stored results or baselines.
A run writes runs/<run-id>/ with run.json, traces/, scores/, results.json and summary.json, and the eval's latest.json; schemaVersion: 2 is format: 'mst.run/v1' and no longer loads. See Runs are directories in the mst.run/v1 format.
mst open opens a run's report
Affects: scripts that run mst open.
mst run writes runs/<run-id>/report/index.html with every run, and mst open [path] opens the newest run's report, or the run or eval directory named. See mst open opens a run's report.
The MCP reporter reports evals only
Affects: projects that use the MCP Playwright reporter.
It writes each run's eval results as a mst.run/v1 run with the run report (a variant per project), and leaves tests and conformance checks to Playwright's own report. historyLimit, includeAutoTracking and runId were removed; name names the eval. See The MCP reporter reports evals only.
The last scenario and host names
Affects: code that reads MstClientSimulationResult.scenario.
MstClientSimulationResult.scenario is input, the case's input as given to the model. The desktop drivers' run context says input and submittedInput (was scenario and submittedScenario), and the mst client's errors say "The mst client failed" (was "MCP host simulation failed").